-
-
Notifications
You must be signed in to change notification settings - Fork 37.5k
Refactor Netatmo integration #29851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
MartinHjelmare
merged 58 commits into
home-assistant:dev
from
cgtobi:refactor_netatmo_integration
Jan 11, 2020
Merged
Refactor Netatmo integration #29851
Changes from all commits
Commits
Show all changes
58 commits
Select commit
Hold shift + click to select a range
ad9b859
Refactor to use ids in data class
cgtobi ccaeee1
Use station_id
cgtobi 2e7a5c7
Refactor Netatmo to use oauth
cgtobi 22dc311
Remove old code
cgtobi 0139896
Clean up
cgtobi f5ea022
Clean up
cgtobi 451d727
Clean up
cgtobi 70b5a6e
Refactor binary sensor
cgtobi 23d44ab
Add initial light implementation
cgtobi 17f42c8
Add discovery
cgtobi 583c0b6
Add set schedule service back in
cgtobi 43ba309
Add discovery via homekit
cgtobi 724bc98
More work on the light
cgtobi ba19600
Fix set schedule service
cgtobi e6d27d1
Clean up
cgtobi f2e5b94
Remove unnecessary code
cgtobi d5e0577
Add support for multiple entities/accounts
cgtobi 0448949
Fix MANUFACTURER typo
cgtobi 7e0f2b7
Remove multiline inline if statement
cgtobi 10d30c7
Only add tags when camera type is welcome
cgtobi 750d17f
Remove on/off as it's currently broken
cgtobi 2038846
Fix camera turn_on/off
cgtobi 756f31f
Fix debug message
cgtobi cc18203
Refactor some camera code
cgtobi 5a40241
Refactor camera methods
cgtobi 622eedb
Remove old code
cgtobi 1b59e19
Rename method
cgtobi c2536fa
Update persons regularly
cgtobi e80e457
Remove unused code
cgtobi f3c743c
Refactor method
cgtobi 7f72177
Fix isort
cgtobi 4216de9
Add english strings
cgtobi c560a74
Catch NoDevice exception
cgtobi 159be98
Fix unique id and only add sensors for tags if present
cgtobi 85f767e
Address comments
cgtobi fe41285
Remove ToDo comment
cgtobi e37f2e0
Add set_light_auto back in
cgtobi cb5068b
Add debug info
cgtobi 21eb950
Fix multiple camera issue
cgtobi 90fbf8f
Move camera light service to camera
cgtobi 03b7a54
Only allow camera entities
cgtobi f18a227
Make test pass
cgtobi eb50f2b
Upgrade pyatmo module to 3.2.0
cgtobi 038dbc5
Update requirements
cgtobi 7acc035
Remove list comprehension
cgtobi 1fbcafd
Remove guideline violating code
cgtobi 07e52a7
Remove stale code
cgtobi a8d182a
Rename devices to entities
cgtobi ec72262
Remove light platform
cgtobi 7c066d1
Remove commented code
cgtobi 0ec0568
Exclude files from coverage
cgtobi aee09d3
Remove unused code
cgtobi dbfae85
Fix unique id
cgtobi fa10785
Address comments
cgtobi 62b6bfd
Fix comments
cgtobi 86124f0
Exclude sensor as well
cgtobi 5c573bf
Add another test
cgtobi f32424e
Use core interfaces
cgtobi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "config": { | ||
| "title": "Netatmo", | ||
| "step": { | ||
| "pick_implementation": { | ||
| "title": "Pick Authentication Method" | ||
| } | ||
| }, | ||
| "abort": { | ||
| "already_setup": "You can only configure one Netatmo account.", | ||
| "authorize_url_timeout": "Timeout generating authorize url.", | ||
| "missing_configuration": "The Netatmo component is not configured. Please follow the documentation." | ||
| }, | ||
| "create_entry": { | ||
| "default": "Successfully authenticated with Netatmo." | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,286 +1,86 @@ | ||
| """Support for the Netatmo devices.""" | ||
| from datetime import timedelta | ||
| """The Netatmo integration.""" | ||
| import asyncio | ||
| import logging | ||
| from urllib.error import HTTPError | ||
|
|
||
| import pyatmo | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.const import ( | ||
| CONF_API_KEY, | ||
| CONF_DISCOVERY, | ||
| CONF_PASSWORD, | ||
| CONF_URL, | ||
| CONF_USERNAME, | ||
| EVENT_HOMEASSISTANT_STOP, | ||
| ) | ||
| from homeassistant.helpers import discovery | ||
| import homeassistant.helpers.config_validation as cv | ||
| from homeassistant.util import Throttle | ||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv | ||
|
|
||
| from .const import DATA_NETATMO_AUTH, DOMAIN | ||
| from . import api, config_flow | ||
| from .const import AUTH, DATA_PERSONS, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| DATA_PERSONS = "netatmo_persons" | ||
| DATA_WEBHOOK_URL = "netatmo_webhook_url" | ||
|
|
||
| CONF_SECRET_KEY = "secret_key" | ||
| CONF_WEBHOOKS = "webhooks" | ||
|
|
||
| SERVICE_ADDWEBHOOK = "addwebhook" | ||
| SERVICE_DROPWEBHOOK = "dropwebhook" | ||
| SERVICE_SETSCHEDULE = "set_schedule" | ||
|
|
||
| NETATMO_AUTH = None | ||
| NETATMO_WEBHOOK_URL = None | ||
|
|
||
| DEFAULT_PERSON = "Unknown" | ||
| DEFAULT_DISCOVERY = True | ||
| DEFAULT_WEBHOOKS = False | ||
|
|
||
| EVENT_PERSON = "person" | ||
| EVENT_MOVEMENT = "movement" | ||
| EVENT_HUMAN = "human" | ||
| EVENT_ANIMAL = "animal" | ||
| EVENT_VEHICLE = "vehicle" | ||
|
|
||
| EVENT_BUS_PERSON = "netatmo_person" | ||
| EVENT_BUS_MOVEMENT = "netatmo_movement" | ||
| EVENT_BUS_HUMAN = "netatmo_human" | ||
| EVENT_BUS_ANIMAL = "netatmo_animal" | ||
| EVENT_BUS_VEHICLE = "netatmo_vehicle" | ||
| EVENT_BUS_OTHER = "netatmo_other" | ||
|
|
||
| ATTR_ID = "id" | ||
| ATTR_PSEUDO = "pseudo" | ||
| ATTR_NAME = "name" | ||
| ATTR_EVENT_TYPE = "event_type" | ||
| ATTR_MESSAGE = "message" | ||
| ATTR_CAMERA_ID = "camera_id" | ||
| ATTR_HOME_NAME = "home_name" | ||
| ATTR_PERSONS = "persons" | ||
| ATTR_IS_KNOWN = "is_known" | ||
| ATTR_FACE_URL = "face_url" | ||
| ATTR_SNAPSHOT_URL = "snapshot_url" | ||
| ATTR_VIGNETTE_URL = "vignette_url" | ||
| ATTR_SCHEDULE = "schedule" | ||
|
|
||
| MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) | ||
| MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) | ||
|
|
||
| CONFIG_SCHEMA = vol.Schema( | ||
| { | ||
| DOMAIN: vol.Schema( | ||
| { | ||
| vol.Required(CONF_API_KEY): cv.string, | ||
| vol.Required(CONF_PASSWORD): cv.string, | ||
| vol.Required(CONF_SECRET_KEY): cv.string, | ||
| vol.Required(CONF_USERNAME): cv.string, | ||
| vol.Optional(CONF_WEBHOOKS, default=DEFAULT_WEBHOOKS): cv.boolean, | ||
| vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, | ||
| vol.Required(CONF_CLIENT_ID): cv.string, | ||
| vol.Required(CONF_CLIENT_SECRET): cv.string, | ||
| } | ||
| ) | ||
| }, | ||
| extra=vol.ALLOW_EXTRA, | ||
| ) | ||
|
|
||
| SCHEMA_SERVICE_ADDWEBHOOK = vol.Schema({vol.Optional(CONF_URL): cv.string}) | ||
|
|
||
| SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({}) | ||
|
|
||
| SCHEMA_SERVICE_SETSCHEDULE = vol.Schema({vol.Required(ATTR_SCHEDULE): cv.string}) | ||
|
|
||
|
|
||
| def setup(hass, config): | ||
| """Set up the Netatmo devices.""" | ||
|
|
||
| hass.data[DATA_PERSONS] = {} | ||
| try: | ||
| auth = pyatmo.ClientAuth( | ||
| config[DOMAIN][CONF_API_KEY], | ||
| config[DOMAIN][CONF_SECRET_KEY], | ||
| config[DOMAIN][CONF_USERNAME], | ||
| config[DOMAIN][CONF_PASSWORD], | ||
| "read_station read_camera access_camera " | ||
| "read_thermostat write_thermostat " | ||
| "read_presence access_presence read_homecoach", | ||
| ) | ||
| except HTTPError: | ||
| _LOGGER.error("Unable to connect to Netatmo API") | ||
| return False | ||
|
|
||
| try: | ||
| home_data = pyatmo.HomeData(auth) | ||
| except pyatmo.NoDevice: | ||
| home_data = None | ||
| _LOGGER.debug("No climate device. Disable %s service", SERVICE_SETSCHEDULE) | ||
|
|
||
| # Store config to be used during entry setup | ||
| hass.data[DATA_NETATMO_AUTH] = auth | ||
|
|
||
| if config[DOMAIN][CONF_DISCOVERY]: | ||
| for component in "camera", "sensor", "binary_sensor", "climate": | ||
| discovery.load_platform(hass, component, DOMAIN, {}, config) | ||
| PLATFORMS = ["binary_sensor", "camera", "climate", "sensor"] | ||
|
|
||
| if config[DOMAIN][CONF_WEBHOOKS]: | ||
| webhook_id = hass.components.webhook.async_generate_id() | ||
| hass.data[DATA_WEBHOOK_URL] = hass.components.webhook.async_generate_url( | ||
| webhook_id | ||
| ) | ||
| hass.components.webhook.async_register( | ||
| DOMAIN, "Netatmo", webhook_id, handle_webhook | ||
| ) | ||
| auth.addwebhook(hass.data[DATA_WEBHOOK_URL]) | ||
| hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, dropwebhook) | ||
|
|
||
| def _service_addwebhook(service): | ||
| """Service to (re)add webhooks during runtime.""" | ||
| url = service.data.get(CONF_URL) | ||
| if url is None: | ||
| url = hass.data[DATA_WEBHOOK_URL] | ||
| _LOGGER.info("Adding webhook for URL: %s", url) | ||
| auth.addwebhook(url) | ||
|
|
||
| hass.services.register( | ||
| DOMAIN, | ||
| SERVICE_ADDWEBHOOK, | ||
| _service_addwebhook, | ||
| schema=SCHEMA_SERVICE_ADDWEBHOOK, | ||
| ) | ||
|
|
||
| def _service_dropwebhook(service): | ||
| """Service to drop webhooks during runtime.""" | ||
| _LOGGER.info("Dropping webhook") | ||
| auth.dropwebhook() | ||
| async def async_setup(hass: HomeAssistant, config: dict): | ||
| """Set up the Netatmo component.""" | ||
| hass.data[DOMAIN] = {} | ||
| hass.data[DOMAIN][DATA_PERSONS] = {} | ||
|
|
||
| hass.services.register( | ||
| DOMAIN, | ||
| SERVICE_DROPWEBHOOK, | ||
| _service_dropwebhook, | ||
| schema=SCHEMA_SERVICE_DROPWEBHOOK, | ||
| ) | ||
|
|
||
| def _service_setschedule(service): | ||
| """Service to change current home schedule.""" | ||
| schedule_name = service.data.get(ATTR_SCHEDULE) | ||
| home_data.switchHomeSchedule(schedule=schedule_name) | ||
| _LOGGER.info("Set home schedule to %s", schedule_name) | ||
| if DOMAIN not in config: | ||
| return True | ||
|
|
||
| if home_data is not None: | ||
| hass.services.register( | ||
| config_flow.NetatmoFlowHandler.async_register_implementation( | ||
| hass, | ||
| config_entry_oauth2_flow.LocalOAuth2Implementation( | ||
| hass, | ||
| DOMAIN, | ||
| SERVICE_SETSCHEDULE, | ||
| _service_setschedule, | ||
| schema=SCHEMA_SERVICE_SETSCHEDULE, | ||
| ) | ||
| config[DOMAIN][CONF_CLIENT_ID], | ||
| config[DOMAIN][CONF_CLIENT_SECRET], | ||
| OAUTH2_AUTHORIZE, | ||
| OAUTH2_TOKEN, | ||
| ), | ||
| ) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| def dropwebhook(hass): | ||
| """Drop the webhook subscription.""" | ||
| auth = hass.data[DATA_NETATMO_AUTH] | ||
| auth.dropwebhook() | ||
|
|
||
|
|
||
| async def handle_webhook(hass, webhook_id, request): | ||
| """Handle webhook callback.""" | ||
| try: | ||
| data = await request.json() | ||
| except ValueError: | ||
| return None | ||
| async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): | ||
| """Set up Netatmo from a config entry.""" | ||
| implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( | ||
| hass, entry | ||
| ) | ||
|
|
||
| _LOGGER.debug("Got webhook data: %s", data) | ||
| published_data = { | ||
| ATTR_EVENT_TYPE: data.get(ATTR_EVENT_TYPE), | ||
| ATTR_HOME_NAME: data.get(ATTR_HOME_NAME), | ||
| ATTR_CAMERA_ID: data.get(ATTR_CAMERA_ID), | ||
| ATTR_MESSAGE: data.get(ATTR_MESSAGE), | ||
| hass.data[DOMAIN][entry.entry_id] = { | ||
| AUTH: api.ConfigEntryNetatmoAuth(hass, entry, implementation) | ||
| } | ||
| if data.get(ATTR_EVENT_TYPE) == EVENT_PERSON: | ||
| for person in data[ATTR_PERSONS]: | ||
| published_data[ATTR_ID] = person.get(ATTR_ID) | ||
| published_data[ATTR_NAME] = hass.data[DATA_PERSONS].get( | ||
| published_data[ATTR_ID], DEFAULT_PERSON | ||
| ) | ||
| published_data[ATTR_IS_KNOWN] = person.get(ATTR_IS_KNOWN) | ||
| published_data[ATTR_FACE_URL] = person.get(ATTR_FACE_URL) | ||
| hass.bus.async_fire(EVENT_BUS_PERSON, published_data) | ||
| elif data.get(ATTR_EVENT_TYPE) == EVENT_MOVEMENT: | ||
| published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) | ||
| published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) | ||
| hass.bus.async_fire(EVENT_BUS_MOVEMENT, published_data) | ||
| elif data.get(ATTR_EVENT_TYPE) == EVENT_HUMAN: | ||
| published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) | ||
| published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) | ||
| hass.bus.async_fire(EVENT_BUS_HUMAN, published_data) | ||
| elif data.get(ATTR_EVENT_TYPE) == EVENT_ANIMAL: | ||
| published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) | ||
| published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) | ||
| hass.bus.async_fire(EVENT_BUS_ANIMAL, published_data) | ||
| elif data.get(ATTR_EVENT_TYPE) == EVENT_VEHICLE: | ||
| hass.bus.async_fire(EVENT_BUS_VEHICLE, published_data) | ||
| published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) | ||
| published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) | ||
| else: | ||
| hass.bus.async_fire(EVENT_BUS_OTHER, data) | ||
|
|
||
|
|
||
| class CameraData: | ||
| """Get the latest data from Netatmo.""" | ||
|
|
||
| def __init__(self, hass, auth, home=None): | ||
| """Initialize the data object.""" | ||
| self._hass = hass | ||
| self.auth = auth | ||
| self.camera_data = None | ||
| self.camera_names = [] | ||
| self.module_names = [] | ||
| self.home = home | ||
| self.camera_type = None | ||
| for component in PLATFORMS: | ||
| hass.async_create_task( | ||
| hass.config_entries.async_forward_entry_setup(entry, component) | ||
| ) | ||
|
|
||
| def get_camera_names(self): | ||
| """Return all camera available on the API as a list.""" | ||
| self.camera_names = [] | ||
| self.update() | ||
| if not self.home: | ||
| for home in self.camera_data.cameras: | ||
| for camera in self.camera_data.cameras[home].values(): | ||
| self.camera_names.append(camera["name"]) | ||
| else: | ||
| for camera in self.camera_data.cameras[self.home].values(): | ||
| self.camera_names.append(camera["name"]) | ||
| return self.camera_names | ||
| return True | ||
|
|
||
| def get_module_names(self, camera_name): | ||
| """Return all module available on the API as a list.""" | ||
| self.module_names = [] | ||
| self.update() | ||
| cam_id = self.camera_data.cameraByName(camera=camera_name, home=self.home)["id"] | ||
| for module in self.camera_data.modules.values(): | ||
| if cam_id == module["cam_id"]: | ||
| self.module_names.append(module["name"]) | ||
| return self.module_names | ||
|
|
||
| def get_camera_type(self, camera=None, home=None, cid=None): | ||
| """Return camera type for a camera, cid has preference over camera.""" | ||
| self.camera_type = self.camera_data.cameraType( | ||
| camera=camera, home=home, cid=cid | ||
| async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): | ||
| """Unload a config entry.""" | ||
| unload_ok = all( | ||
| await asyncio.gather( | ||
| *[ | ||
| hass.config_entries.async_forward_entry_unload(entry, component) | ||
| for component in PLATFORMS | ||
| ] | ||
| ) | ||
| return self.camera_type | ||
|
|
||
| def get_persons(self): | ||
| """Gather person data for webhooks.""" | ||
| for person_id, person_data in self.camera_data.persons.items(): | ||
| self._hass.data[DATA_PERSONS][person_id] = person_data.get(ATTR_PSEUDO) | ||
|
|
||
| @Throttle(MIN_TIME_BETWEEN_UPDATES) | ||
| def update(self): | ||
| """Call the Netatmo API to update the data.""" | ||
| self.camera_data = pyatmo.CameraData(self.auth, size=100) | ||
| ) | ||
| if unload_ok: | ||
| hass.data[DOMAIN].pop(entry.entry_id) | ||
|
|
||
| @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) | ||
| def update_event(self): | ||
| """Call the Netatmo API to update the events.""" | ||
| self.camera_data.updateEvent(home=self.home, devicetype=self.camera_type) | ||
| return unload_ok |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.