From b9a9a92145250615b1b834dd497684a44e2e02d0 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 11 Mar 2020 01:08:59 +0100 Subject: [PATCH] Refactor netatmo webhooks (#32195) * Start webhook implementation * Add webhook implementation * Bump pyatmo 3.2.5 * Fire event after data evaluation * Setup webhooks after components * Fix logging * Wrap non async call * Wrap non async call * Add smoke detector and door tag webhook support * Catch when webhook registration fails * Log to debug * Fix persons lookup * Add dependency * Remove false requirements * Fix requirements * Replace netatmo events by a single one * Slim down code * Clean up code * Address review vomments * Undo attribute removal * Bump pyatmo to v3.3.0 * Only create webhook id once and reuse * Store and reuse cloudhook url * Wait for hass core to be up and running * Register webhook once HA is ready * Delay webhook registration --- homeassistant/components/netatmo/__init__.py | 79 ++++++++++++++++++- homeassistant/components/netatmo/const.py | 20 +---- .../components/netatmo/manifest.json | 5 +- homeassistant/components/netatmo/webhook.py | 65 +++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 152 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/netatmo/webhook.py diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index bd79f597b5b..3c318c705bf 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,27 +1,47 @@ """The Netatmo integration.""" import asyncio import logging +import secrets +import pyatmo import voluptuous as vol +from homeassistant.components import cloud +from homeassistant.components.webhook import ( + async_register as webhook_register, + async_unregister as webhook_unregister, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISCOVERY, CONF_USERNAME, + CONF_WEBHOOK_ID, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from . import api, config_flow -from .const import AUTH, DATA_PERSONS, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from .const import ( + AUTH, + CONF_CLOUDHOOK_URL, + DATA_PERSONS, + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, +) +from .webhook import handle_webhook _LOGGER = logging.getLogger(__name__) CONF_SECRET_KEY = "secret_key" CONF_WEBHOOKS = "webhooks" +WAIT_FOR_CLOUD = 5 + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -79,6 +99,46 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, component) ) + async def unregister_webhook(event): + _LOGGER.debug("Unregister Netatmo webhook (%s)", entry.data[CONF_WEBHOOK_ID]) + webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + + async def register_webhook(event): + # Wait for the could integration to be ready + await asyncio.sleep(WAIT_FOR_CLOUD) + + if CONF_WEBHOOK_ID not in entry.data: + data = {**entry.data, CONF_WEBHOOK_ID: secrets.token_hex()} + hass.config_entries.async_update_entry(entry, data=data) + + if hass.components.cloud.async_active_subscription(): + if CONF_CLOUDHOOK_URL not in entry.data: + webhook_url = await hass.components.cloud.async_create_cloudhook( + entry.data[CONF_WEBHOOK_ID] + ) + data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url} + hass.config_entries.async_update_entry(entry, data=data) + else: + webhook_url = entry.data[CONF_CLOUDHOOK_URL] + else: + webhook_url = hass.components.webhook.async_generate_url( + entry.data[CONF_WEBHOOK_ID] + ) + + try: + await hass.async_add_executor_job( + hass.data[DOMAIN][entry.entry_id][AUTH].addwebhook, webhook_url + ) + webhook_register( + hass, DOMAIN, "Netatmo", entry.data[CONF_WEBHOOK_ID], handle_webhook + ) + _LOGGER.info("Register Netatmo webhook: %s", webhook_url) + except pyatmo.ApiError as err: + _LOGGER.error("Error during webhook registration - %s", err) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, register_webhook) return True @@ -95,4 +155,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + if CONF_WEBHOOK_ID in entry.data: + await hass.async_add_executor_job( + hass.data[DOMAIN][entry.entry_id][AUTH].dropwebhook() + ) + return unload_ok + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry): + """Cleanup when entry is removed.""" + if CONF_WEBHOOK_ID in entry.data: + try: + _LOGGER.debug( + "Removing Netatmo cloudhook (%s)", entry.data[CONF_WEBHOOK_ID] + ) + await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID]) + except cloud.CloudNotAvailable: + pass diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 5d981dc23b4..4443ef23032 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -11,43 +11,29 @@ CONF_PUBLIC = "public_sensor_config" CAMERA_DATA = "netatmo_camera" HOME_DATA = "netatmo_home_data" +CONF_CLOUDHOOK_URL = "cloudhook_url" + OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" DATA_PERSONS = "netatmo_persons" NETATMO_WEBHOOK_URL = None +NETATMO_EVENT = "netatmo_event" 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_ID = "home_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_ID = "schedule_id" ATTR_SCHEDULE_NAME = "schedule_name" diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 6fe084cc885..6e1c3d9f8f4 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,10 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==3.2.4" + "pyatmo==3.3.0" + ], + "after_dependencies": [ + "cloud" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/netatmo/webhook.py b/homeassistant/components/netatmo/webhook.py new file mode 100644 index 00000000000..8b6a4d3f1e1 --- /dev/null +++ b/homeassistant/components/netatmo/webhook.py @@ -0,0 +1,65 @@ +"""The Netatmo integration.""" +import logging + +from homeassistant.core import callback + +from .const import ( + ATTR_EVENT_TYPE, + ATTR_FACE_URL, + ATTR_ID, + ATTR_IS_KNOWN, + ATTR_NAME, + ATTR_PERSONS, + DATA_PERSONS, + DEFAULT_PERSON, + DOMAIN, + NETATMO_EVENT, +) + +_LOGGER = logging.getLogger(__name__) + + +async def handle_webhook(hass, webhook_id, request): + """Handle webhook callback.""" + try: + data = await request.json() + except ValueError: + return None + + _LOGGER.debug("Got webhook data: %s", data) + + event_type = data.get(ATTR_EVENT_TYPE) + + if event_type == "outdoor": + hass.bus.async_fire( + event_type=NETATMO_EVENT, event_data={"type": event_type, "data": data} + ) + for event_data in data.get("event_list"): + async_evaluate_event(hass, event_data) + else: + async_evaluate_event(hass, data) + + +@callback +def async_evaluate_event(hass, event_data): + """Evaluate events from webhook.""" + event_type = event_data.get(ATTR_EVENT_TYPE) + + if event_type == "person": + for person in event_data.get(ATTR_PERSONS): + person_event_data = dict(event_data) + person_event_data[ATTR_ID] = person.get(ATTR_ID) + person_event_data[ATTR_NAME] = hass.data[DOMAIN][DATA_PERSONS].get( + person_event_data[ATTR_ID], DEFAULT_PERSON + ) + person_event_data[ATTR_IS_KNOWN] = person.get(ATTR_IS_KNOWN) + person_event_data[ATTR_FACE_URL] = person.get(ATTR_FACE_URL) + hass.bus.async_fire( + event_type=NETATMO_EVENT, + event_data={"type": event_type, "data": person_event_data}, + ) + else: + hass.bus.async_fire( + event_type=NETATMO_EVENT, + event_data={"type": event_type, "data": event_data}, + ) diff --git a/requirements_all.txt b/requirements_all.txt index 0dd4920e944..554ba56d084 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1158,7 +1158,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==3.2.4 +pyatmo==3.3.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ae7cb1c582..e20a739ef22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -434,7 +434,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==3.2.4 +pyatmo==3.3.0 # homeassistant.components.blackbird pyblackbird==0.5