Doorbird Refactor (#23892)

* Remove schedule management. Allow custom HTTP events defined in the configuration

* Consolidate doorbird request handling.  Make token a per device configuration item.

* Lint fixes

* Do not register dummy listener

* Remove punctuation
This commit is contained in:
Matt Snyder 2019-05-18 14:46:00 -05:00 committed by Martin Hjelmare
parent a8286535eb
commit 10a1b156e3

View File

@ -6,8 +6,8 @@ import voluptuous as vol
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICES, CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_DEVICES, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_TOKEN,
CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME) CONF_USERNAME)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import dt as dt_util, slugify from homeassistant.util import dt as dt_util, slugify
@ -18,25 +18,7 @@ DOMAIN = 'doorbird'
API_URL = '/api/{}'.format(DOMAIN) API_URL = '/api/{}'.format(DOMAIN)
CONF_CUSTOM_URL = 'hass_url_override' CONF_CUSTOM_URL = 'hass_url_override'
CONF_DOORBELL_EVENTS = 'doorbell_events' CONF_EVENTS = 'events'
CONF_DOORBELL_NUMS = 'doorbell_numbers'
CONF_RELAY_NUMS = 'relay_numbers'
CONF_MOTION_EVENTS = 'motion_events'
SENSOR_TYPES = {
'doorbell': {
'name': 'Button',
'device_class': 'occupancy',
},
'motion': {
'name': 'Motion',
'device_class': 'motion',
},
'relay': {
'name': 'Relay',
'device_class': 'relay',
}
}
RESET_DEVICE_FAVORITES = 'doorbird_reset_favorites' RESET_DEVICE_FAVORITES = 'doorbird_reset_favorites'
@ -44,19 +26,15 @@ DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DOORBELL_NUMS, default=[1]): vol.All( vol.Required(CONF_TOKEN): cv.string,
cv.ensure_list, [cv.positive_int]), vol.Optional(CONF_EVENTS, default=[]): vol.All(
vol.Optional(CONF_RELAY_NUMS, default=[1]): vol.All( cv.ensure_list, [cv.string]),
cv.ensure_list, [cv.positive_int]),
vol.Optional(CONF_CUSTOM_URL): cv.string, vol.Optional(CONF_CUSTOM_URL): cv.string,
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string
vol.Optional(CONF_MONITORED_CONDITIONS, default=[]):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
}) })
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_TOKEN): cv.string,
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA]) vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA])
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -66,13 +44,8 @@ def setup(hass, config):
"""Set up the DoorBird component.""" """Set up the DoorBird component."""
from doorbirdpy import DoorBird from doorbirdpy import DoorBird
token = config[DOMAIN].get(CONF_TOKEN)
# Provide an endpoint for the doorstations to call to trigger events # Provide an endpoint for the doorstations to call to trigger events
hass.http.register_view(DoorBirdRequestView(token)) hass.http.register_view(DoorBirdRequestView)
# Provide an endpoint for the user to call to clear device changes
hass.http.register_view(DoorBirdCleanupView(token))
doorstations = [] doorstations = []
@ -80,10 +53,9 @@ def setup(hass, config):
device_ip = doorstation_config.get(CONF_HOST) device_ip = doorstation_config.get(CONF_HOST)
username = doorstation_config.get(CONF_USERNAME) username = doorstation_config.get(CONF_USERNAME)
password = doorstation_config.get(CONF_PASSWORD) password = doorstation_config.get(CONF_PASSWORD)
doorbell_nums = doorstation_config.get(CONF_DOORBELL_NUMS)
relay_nums = doorstation_config.get(CONF_RELAY_NUMS)
custom_url = doorstation_config.get(CONF_CUSTOM_URL) custom_url = doorstation_config.get(CONF_CUSTOM_URL)
events = doorstation_config.get(CONF_MONITORED_CONDITIONS) events = doorstation_config.get(CONF_EVENTS)
token = doorstation_config.get(CONF_TOKEN)
name = (doorstation_config.get(CONF_NAME) name = (doorstation_config.get(CONF_NAME)
or 'DoorBird {}'.format(index + 1)) or 'DoorBird {}'.format(index + 1))
@ -92,7 +64,7 @@ def setup(hass, config):
if status[0]: if status[0]:
doorstation = ConfiguredDoorBird(device, name, events, custom_url, doorstation = ConfiguredDoorBird(device, name, events, custom_url,
doorbell_nums, relay_nums, token) token)
doorstations.append(doorstation) doorstations.append(doorstation)
_LOGGER.info('Connected to DoorBird "%s" as %s@%s', _LOGGER.info('Connected to DoorBird "%s" as %s@%s',
doorstation.name, username, device_ip) doorstation.name, username, device_ip)
@ -108,7 +80,7 @@ def setup(hass, config):
# Subscribe to doorbell or motion events # Subscribe to doorbell or motion events
if events: if events:
try: try:
doorstation.update_schedule(hass) doorstation.register_events(hass)
except HTTPError: except HTTPError:
hass.components.persistent_notification.create( hass.components.persistent_notification.create(
'Doorbird configuration failed. Please verify that API ' 'Doorbird configuration failed. Please verify that API '
@ -124,15 +96,15 @@ def setup(hass, config):
def _reset_device_favorites_handler(event): def _reset_device_favorites_handler(event):
"""Handle clearing favorites on device.""" """Handle clearing favorites on device."""
slug = event.data.get('slug') token = event.data.get('token')
if slug is None: if token is None:
return return
doorstation = get_doorstation_by_slug(hass, slug) doorstation = get_doorstation_by_token(hass, token)
if doorstation is None: if doorstation is None:
_LOGGER.error('Device not found %s', format(slug)) _LOGGER.error('Device not found for provided token.')
# Clear webhooks # Clear webhooks
favorites = doorstation.device.favorites() favorites = doorstation.device.favorites()
@ -146,30 +118,22 @@ def setup(hass, config):
return True return True
def get_doorstation_by_slug(hass, slug): def get_doorstation_by_token(hass, token):
"""Get doorstation by slug.""" """Get doorstation by slug."""
for doorstation in hass.data[DOMAIN]: for doorstation in hass.data[DOMAIN]:
if slugify(doorstation.name) in slug: if token == doorstation.token:
return doorstation return doorstation
def handle_event(event):
"""Handle dummy events."""
return None
class ConfiguredDoorBird(): class ConfiguredDoorBird():
"""Attach additional information to pass along with configured device.""" """Attach additional information to pass along with configured device."""
def __init__(self, device, name, events, custom_url, doorbell_nums, def __init__(self, device, name, events, custom_url, token):
relay_nums, token):
"""Initialize configured device.""" """Initialize configured device."""
self._name = name self._name = name
self._device = device self._device = device
self._custom_url = custom_url self._custom_url = custom_url
self._monitored_events = events self._events = events
self._doorbell_nums = doorbell_nums
self._relay_nums = relay_nums
self._token = token self._token = token
@property @property
@ -187,14 +151,13 @@ class ConfiguredDoorBird():
"""Get custom url for device.""" """Get custom url for device."""
return self._custom_url return self._custom_url
def update_schedule(self, hass): @property
"""Register monitored sensors and deregister others.""" def token(self):
from doorbirdpy import DoorBirdScheduleEntrySchedule """Get token for device."""
return self._token
# Create a new schedule (24/7)
schedule = DoorBirdScheduleEntrySchedule()
schedule.add_weekday(0, 604800) # seconds in a week
def register_events(self, hass):
"""Register events on device."""
# Get the URL of this server # Get the URL of this server
hass_url = hass.config.api.base_url hass_url = hass.config.api.base_url
@ -202,98 +165,39 @@ class ConfiguredDoorBird():
if self.custom_url is not None: if self.custom_url is not None:
hass_url = self.custom_url hass_url = self.custom_url
# For all sensor types (enabled + disabled) for event in self._events:
for sensor_type in SENSOR_TYPES: event = self._get_event_name(event)
name = '{} {}'.format(self.name, SENSOR_TYPES[sensor_type]['name'])
slug = slugify(name)
url = '{}{}/{}?token={}'.format(hass_url, API_URL, slug, self._register_event(hass_url, event)
self._token)
if sensor_type in self._monitored_events:
# Enabled -> register
self._register_event(url, sensor_type, schedule)
_LOGGER.info('Registered for %s pushes from DoorBird "%s". '
'Use the "%s_%s" event for automations.',
sensor_type, self.name, DOMAIN, slug)
# Register a dummy listener so event is listed in GUI _LOGGER.info('Successfully registered URL for %s on %s',
hass.bus.listen('{}_{}'.format(DOMAIN, slug), handle_event) event, self.name)
else:
# Disabled -> deregister
self._deregister_event(url, sensor_type)
_LOGGER.info('Deregistered %s pushes from DoorBird "%s". '
'If any old favorites or schedules remain, '
'follow the instructions in the component '
'documentation to clear device registrations.',
sensor_type, self.name)
def _register_event(self, hass_url, event, schedule): @property
def slug(self):
"""Get device slug."""
return slugify(self._name)
def _get_event_name(self, event):
return '{}_{}'.format(self.slug, event)
def _register_event(self, hass_url, event):
"""Add a schedule entry in the device for a sensor.""" """Add a schedule entry in the device for a sensor."""
from doorbirdpy import DoorBirdScheduleEntryOutput url = '{}{}/{}?token={}'.format(hass_url, API_URL, event, self._token)
# Register HA URL as webhook if not already, then get the ID # Register HA URL as webhook if not already, then get the ID
if not self.webhook_is_registered(hass_url): if not self.webhook_is_registered(url):
self.device.change_favorite('http', 'Home Assistant ({} events)' self.device.change_favorite('http', 'Home Assistant ({})'
.format(event), hass_url) .format(event), url)
fav_id = self.get_webhook_id(hass_url) fav_id = self.get_webhook_id(url)
if not fav_id: if not fav_id:
_LOGGER.warning('Could not find favorite for URL "%s". ' _LOGGER.warning('Could not find favorite for URL "%s". '
'Skipping sensor "%s".', hass_url, event) 'Skipping sensor "%s"', url, event)
return return
# Add event handling to device schedule def webhook_is_registered(self, url, favs=None) -> bool:
output = DoorBirdScheduleEntryOutput(event='http',
param=fav_id,
schedule=schedule)
if event == 'doorbell':
# Repeat edit for each monitored doorbell number
for doorbell in self._doorbell_nums:
entry = self.device.get_schedule_entry(event, str(doorbell))
entry.output.append(output)
self.device.change_schedule(entry)
elif event == 'relay':
# Repeat edit for each monitored doorbell number
for relay in self._relay_nums:
entry = self.device.get_schedule_entry(event, str(relay))
entry.output.append(output)
else:
entry = self.device.get_schedule_entry(event)
entry.output.append(output)
self.device.change_schedule(entry)
def _deregister_event(self, hass_url, event):
"""Remove the schedule entry in the device for a sensor."""
# Find the right favorite and delete it
fav_id = self.get_webhook_id(hass_url)
if not fav_id:
return
self._device.delete_favorite('http', fav_id)
if event == 'doorbell':
# Delete the matching schedule for each doorbell number
for doorbell in self._doorbell_nums:
self._delete_schedule_action(event, fav_id, str(doorbell))
else:
self._delete_schedule_action(event, fav_id)
def _delete_schedule_action(self, sensor, fav_id, param=""):
"""Remove the HA output from a schedule."""
entries = self._device.schedule()
for entry in entries:
if entry.input != sensor or entry.param != param:
continue
for action in entry.output:
if action.event == 'http' and action.param == fav_id:
entry.output.remove(action)
self._device.change_schedule(entry)
def webhook_is_registered(self, ha_url, favs=None) -> bool:
"""Return whether the given URL is registered as a device favorite.""" """Return whether the given URL is registered as a device favorite."""
favs = favs if favs else self.device.favorites() favs = favs if favs else self.device.favorites()
@ -301,12 +205,12 @@ class ConfiguredDoorBird():
return False return False
for fav in favs['http'].values(): for fav in favs['http'].values():
if fav['value'] == ha_url: if fav['value'] == url:
return True return True
return False return False
def get_webhook_id(self, ha_url, favs=None) -> str or None: def get_webhook_id(self, url, favs=None) -> str or None:
""" """
Return the device favorite ID for the given URL. Return the device favorite ID for the given URL.
@ -318,7 +222,7 @@ class ConfiguredDoorBird():
return None return None
for fav_id in favs['http']: for fav_id in favs['http']:
if favs['http'][fav_id]['value'] == ha_url: if favs['http'][fav_id]['value'] == url:
return fav_id return fav_id
return None return None
@ -340,72 +244,33 @@ class DoorBirdRequestView(HomeAssistantView):
requires_auth = False requires_auth = False
url = API_URL url = API_URL
name = API_URL[1:].replace('/', ':') name = API_URL[1:].replace('/', ':')
extra_urls = [API_URL + '/{sensor}'] extra_urls = [API_URL + '/{event}']
def __init__(self, token):
"""Initialize view."""
HomeAssistantView.__init__(self)
self._token = token
# pylint: disable=no-self-use # pylint: disable=no-self-use
async def get(self, request, sensor): async def get(self, request, event):
"""Respond to requests from the device.""" """Respond to requests from the device."""
from aiohttp import web from aiohttp import web
hass = request.app['hass'] hass = request.app['hass']
request_token = request.query.get('token') token = request.query.get('token')
authenticated = request_token == self._token device = get_doorstation_by_token(hass, token)
if request_token == '' or not authenticated: if device is None:
return web.Response(status=401, text='Unauthorized') return web.Response(status=401, text='Invalid token provided.')
doorstation = get_doorstation_by_slug(hass, sensor) if device:
event_data = device.get_event_data()
if doorstation:
event_data = doorstation.get_event_data()
else: else:
event_data = {} event_data = {}
hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor), event_data) if event == 'clear':
hass.bus.async_fire(RESET_DEVICE_FAVORITES,
{'token': token})
message = 'HTTP Favorites cleared for {}'.format(device.slug)
return web.Response(status=200, text=message)
hass.bus.async_fire('{}_{}'.format(DOMAIN, event), event_data)
return web.Response(status=200, text='OK') return web.Response(status=200, text='OK')
class DoorBirdCleanupView(HomeAssistantView):
"""Provide a URL to call to delete ALL webhooks/schedules."""
requires_auth = False
url = API_URL + '/clear/{slug}'
name = 'DoorBird Cleanup'
def __init__(self, token):
"""Initialize view."""
HomeAssistantView.__init__(self)
self._token = token
# pylint: disable=no-self-use
async def get(self, request, slug):
"""Act on requests."""
from aiohttp import web
hass = request.app['hass']
request_token = request.query.get('token')
authenticated = request_token == self._token
if request_token == '' or not authenticated:
return web.Response(status=401, text='Unauthorized')
device = get_doorstation_by_slug(hass, slug)
# No matching device
if device is None:
return web.Response(status=404,
text='Device slug {} not found'.format(slug))
hass.bus.async_fire(RESET_DEVICE_FAVORITES,
{'slug': slug})
message = 'Clearing schedule for {}'.format(slug)
return web.Response(status=200, text=message)