-
-
Notifications
You must be signed in to change notification settings - Fork 37.5k
Add webhook support for Netatmo Cameras #20755
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
Changes from all commits
260d864
f35c651
19fd590
5a7af68
7a4238a
469b31d
bffd304
12f99dd
f76f1b2
be30e97
4ebdaf9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,27 +5,65 @@ | |
| https://home-assistant.io/components/netatmo/ | ||
| """ | ||
| 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) | ||
|
|
@@ -36,16 +74,23 @@ | |
| 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], | ||
|
|
@@ -61,9 +106,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() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't call an async function from a sync context. Probably use |
||
| NETATMO_WEBHOOK_URL = hass.components.webhook.async_generate_url( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above. |
||
| webhook_id) | ||
| hass.components.webhook.async_register( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above. |
||
| 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): | ||
|
danielperna84 marked this conversation as resolved.
|
||
| """Handle webhook callback.""" | ||
| body = await request.text() | ||
| try: | ||
| data = json.loads(body) if body else {} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use: data = await request.json() |
||
| 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.""" | ||
|
|
||
|
|
@@ -106,6 +230,12 @@ def get_camera_type(self, camera=None, home=None, cid=None): | |
| home=home, cid=cid) | ||
| return self.camera_type | ||
|
|
||
| def get_persons(self): | ||
| """Gather person data for webhooks.""" | ||
| global NETATMO_PERSONS | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use global. |
||
| 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.""" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not use global. If this needs to be accessed from another module, use hass.data to store it.