Skip to content
Merged
134 changes: 132 additions & 2 deletions homeassistant/components/netatmo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Copy link
Copy Markdown
Member

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.

try:
NETATMO_AUTH = pyatmo.ClientAuth(
config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY],
Expand All @@ -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()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 hass.add_job to schedule the async function.

NETATMO_WEBHOOK_URL = hass.components.webhook.async_generate_url(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above.

webhook_id)
hass.components.webhook.async_register(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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):
Comment thread
danielperna84 marked this conversation as resolved.
"""Handle webhook callback."""
body = await request.text()
try:
data = json.loads(body) if body else {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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."""
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/netatmo/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,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

Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/netatmo/services.yaml
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.