From 847711ddc9afaf7d82af320eedfc4397f4a9cfd2 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sun, 17 Feb 2019 12:31:47 +0100 Subject: [PATCH] Add webhook support for Netatmo Cameras (#20755) Add webhook support for Netatmo Cameras --- homeassistant/components/netatmo/__init__.py | 134 +++++++++++++++++- homeassistant/components/netatmo/camera.py | 1 + .../components/netatmo/services.yaml | 8 ++ 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/netatmo/services.yaml diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 495e22aae24..c496553f057 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,26 +1,64 @@ """Support for the Netatmo devices.""" import logging +import json from datetime import timedelta from urllib.error import HTTPError import voluptuous as vol from homeassistant.const import ( - CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, CONF_DISCOVERY) + CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, CONF_DISCOVERY, CONF_URL, + EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle REQUIREMENTS = ['pyatmo==1.8'] +DEPENDENCIES = ['webhook'] _LOGGER = logging.getLogger(__name__) CONF_SECRET_KEY = 'secret_key' +CONF_WEBHOOKS = 'webhooks' DOMAIN = 'netatmo' +SERVICE_ADDWEBHOOK = 'addwebhook' +SERVICE_DROPWEBHOOK = 'dropwebhook' + NETATMO_AUTH = None +NETATMO_WEBHOOK_URL = None +NETATMO_PERSONS = {} + +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' MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=10) @@ -31,16 +69,23 @@ CONFIG_SCHEMA = vol.Schema({ 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, }) }, extra=vol.ALLOW_EXTRA) +SCHEMA_SERVICE_ADDWEBHOOK = vol.Schema({ + vol.Optional(CONF_URL): cv.string, +}) + +SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({}) + def setup(hass, config): """Set up the Netatmo devices.""" import pyatmo - global NETATMO_AUTH + global NETATMO_AUTH, NETATMO_WEBHOOK_URL try: NETATMO_AUTH = pyatmo.ClientAuth( config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY], @@ -56,9 +101,88 @@ def setup(hass, config): for component in 'camera', 'sensor', 'binary_sensor', 'climate': discovery.load_platform(hass, component, DOMAIN, {}, config) + if config[DOMAIN][CONF_WEBHOOKS]: + webhook_id = hass.components.webhook.async_generate_id() + NETATMO_WEBHOOK_URL = hass.components.webhook.async_generate_url( + webhook_id) + hass.components.webhook.async_register( + DOMAIN, 'Netatmo', webhook_id, handle_webhook) + NETATMO_AUTH.addwebhook(NETATMO_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 = NETATMO_WEBHOOK_URL + _LOGGER.info("Adding webhook for URL: %s", url) + NETATMO_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") + NETATMO_AUTH.dropwebhook() + + hass.services.register( + DOMAIN, SERVICE_DROPWEBHOOK, _service_dropwebhook, + schema=SCHEMA_SERVICE_DROPWEBHOOK) + return True +def dropwebhook(hass): + """Drop the webhook subscription.""" + NETATMO_AUTH.dropwebhook() + + +async def handle_webhook(hass, webhook_id, request): + """Handle webhook callback.""" + body = await request.text() + try: + data = json.loads(body) if body else {} + except ValueError: + return None + + _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) + } + 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] = NETATMO_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.""" @@ -101,6 +225,12 @@ class CameraData: home=home, cid=cid) return self.camera_type + def get_persons(self): + """Gather person data for webhooks.""" + global NETATMO_PERSONS + for person_id, person_data in self.camera_data.persons.items(): + NETATMO_PERSONS[person_id] = person_data.get(ATTR_PSEUDO) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the Netatmo API to update the data.""" diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index a3a5461631d..af56dc6e621 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -40,6 +40,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): continue add_entities([NetatmoCamera(data, camera_name, home, camera_type, verify_ssl)]) + data.get_persons() except pyatmo.NoDevice: return None diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml new file mode 100644 index 00000000000..7bb990caf97 --- /dev/null +++ b/homeassistant/components/netatmo/services.yaml @@ -0,0 +1,8 @@ +addwebhook: + description: Add webhook during runtime (e.g. if it has been banned). + fields: + url: + description: URL for which to add the webhook. + example: https://yourdomain.com:443/api/webhook/webhook_id +dropwebhook: + description: Drop active webhooks. \ No newline at end of file