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
This commit is contained in:
cgtobi 2020-03-11 01:08:59 +01:00 committed by GitHub
parent ba0aaeeddb
commit b9a9a92145
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 152 additions and 21 deletions

View File

@ -1,27 +1,47 @@
"""The Netatmo integration.""" """The Netatmo integration."""
import asyncio import asyncio
import logging import logging
import secrets
import pyatmo
import voluptuous as vol 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.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_CLIENT_ID, CONF_CLIENT_ID,
CONF_CLIENT_SECRET, CONF_CLIENT_SECRET,
CONF_DISCOVERY, CONF_DISCOVERY,
CONF_USERNAME, CONF_USERNAME,
CONF_WEBHOOK_ID,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
from . import api, config_flow 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__) _LOGGER = logging.getLogger(__name__)
CONF_SECRET_KEY = "secret_key" CONF_SECRET_KEY = "secret_key"
CONF_WEBHOOKS = "webhooks" CONF_WEBHOOKS = "webhooks"
WAIT_FOR_CLOUD = 5
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: 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) 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 return True
@ -95,4 +155,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
if unload_ok: if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id) 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 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

View File

@ -11,43 +11,29 @@ CONF_PUBLIC = "public_sensor_config"
CAMERA_DATA = "netatmo_camera" CAMERA_DATA = "netatmo_camera"
HOME_DATA = "netatmo_home_data" HOME_DATA = "netatmo_home_data"
CONF_CLOUDHOOK_URL = "cloudhook_url"
OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize"
OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token"
DATA_PERSONS = "netatmo_persons" DATA_PERSONS = "netatmo_persons"
NETATMO_WEBHOOK_URL = None NETATMO_WEBHOOK_URL = None
NETATMO_EVENT = "netatmo_event"
DEFAULT_PERSON = "Unknown" DEFAULT_PERSON = "Unknown"
DEFAULT_DISCOVERY = True DEFAULT_DISCOVERY = True
DEFAULT_WEBHOOKS = False 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_ID = "id"
ATTR_PSEUDO = "pseudo" ATTR_PSEUDO = "pseudo"
ATTR_NAME = "name" ATTR_NAME = "name"
ATTR_EVENT_TYPE = "event_type" ATTR_EVENT_TYPE = "event_type"
ATTR_MESSAGE = "message"
ATTR_CAMERA_ID = "camera_id"
ATTR_HOME_ID = "home_id" ATTR_HOME_ID = "home_id"
ATTR_HOME_NAME = "home_name" ATTR_HOME_NAME = "home_name"
ATTR_PERSONS = "persons" ATTR_PERSONS = "persons"
ATTR_IS_KNOWN = "is_known" ATTR_IS_KNOWN = "is_known"
ATTR_FACE_URL = "face_url" ATTR_FACE_URL = "face_url"
ATTR_SNAPSHOT_URL = "snapshot_url"
ATTR_VIGNETTE_URL = "vignette_url"
ATTR_SCHEDULE_ID = "schedule_id" ATTR_SCHEDULE_ID = "schedule_id"
ATTR_SCHEDULE_NAME = "schedule_name" ATTR_SCHEDULE_NAME = "schedule_name"

View File

@ -3,7 +3,10 @@
"name": "Netatmo", "name": "Netatmo",
"documentation": "https://www.home-assistant.io/integrations/netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo",
"requirements": [ "requirements": [
"pyatmo==3.2.4" "pyatmo==3.3.0"
],
"after_dependencies": [
"cloud"
], ],
"dependencies": [ "dependencies": [
"webhook" "webhook"

View File

@ -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},
)

View File

@ -1158,7 +1158,7 @@ pyalmond==0.0.2
pyarlo==0.2.3 pyarlo==0.2.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==3.2.4 pyatmo==3.3.0
# homeassistant.components.atome # homeassistant.components.atome
pyatome==0.1.1 pyatome==0.1.1

View File

@ -434,7 +434,7 @@ pyalmond==0.0.2
pyarlo==0.2.3 pyarlo==0.2.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==3.2.4 pyatmo==3.3.0
# homeassistant.components.blackbird # homeassistant.components.blackbird
pyblackbird==0.5 pyblackbird==0.5