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."""
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

View File

@ -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"

View File

@ -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"

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
# homeassistant.components.netatmo
pyatmo==3.2.4
pyatmo==3.3.0
# homeassistant.components.atome
pyatome==0.1.1

View File

@ -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