mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
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:
parent
a8286535eb
commit
10a1b156e3
@ -6,8 +6,8 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICES, CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME,
|
||||
CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME)
|
||||
CONF_DEVICES, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_TOKEN,
|
||||
CONF_USERNAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
@ -18,25 +18,7 @@ DOMAIN = 'doorbird'
|
||||
API_URL = '/api/{}'.format(DOMAIN)
|
||||
|
||||
CONF_CUSTOM_URL = 'hass_url_override'
|
||||
CONF_DOORBELL_EVENTS = 'doorbell_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',
|
||||
}
|
||||
}
|
||||
CONF_EVENTS = 'events'
|
||||
|
||||
RESET_DEVICE_FAVORITES = 'doorbird_reset_favorites'
|
||||
|
||||
@ -44,19 +26,15 @@ DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_DOORBELL_NUMS, default=[1]): vol.All(
|
||||
cv.ensure_list, [cv.positive_int]),
|
||||
vol.Optional(CONF_RELAY_NUMS, default=[1]): vol.All(
|
||||
cv.ensure_list, [cv.positive_int]),
|
||||
vol.Required(CONF_TOKEN): cv.string,
|
||||
vol.Optional(CONF_EVENTS, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_CUSTOM_URL): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
vol.Optional(CONF_NAME): cv.string
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_TOKEN): cv.string,
|
||||
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA])
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
@ -66,13 +44,8 @@ def setup(hass, config):
|
||||
"""Set up the DoorBird component."""
|
||||
from doorbirdpy import DoorBird
|
||||
|
||||
token = config[DOMAIN].get(CONF_TOKEN)
|
||||
|
||||
# Provide an endpoint for the doorstations to call to trigger events
|
||||
hass.http.register_view(DoorBirdRequestView(token))
|
||||
|
||||
# Provide an endpoint for the user to call to clear device changes
|
||||
hass.http.register_view(DoorBirdCleanupView(token))
|
||||
hass.http.register_view(DoorBirdRequestView)
|
||||
|
||||
doorstations = []
|
||||
|
||||
@ -80,10 +53,9 @@ def setup(hass, config):
|
||||
device_ip = doorstation_config.get(CONF_HOST)
|
||||
username = doorstation_config.get(CONF_USERNAME)
|
||||
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)
|
||||
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)
|
||||
or 'DoorBird {}'.format(index + 1))
|
||||
|
||||
@ -92,7 +64,7 @@ def setup(hass, config):
|
||||
|
||||
if status[0]:
|
||||
doorstation = ConfiguredDoorBird(device, name, events, custom_url,
|
||||
doorbell_nums, relay_nums, token)
|
||||
token)
|
||||
doorstations.append(doorstation)
|
||||
_LOGGER.info('Connected to DoorBird "%s" as %s@%s',
|
||||
doorstation.name, username, device_ip)
|
||||
@ -108,7 +80,7 @@ def setup(hass, config):
|
||||
# Subscribe to doorbell or motion events
|
||||
if events:
|
||||
try:
|
||||
doorstation.update_schedule(hass)
|
||||
doorstation.register_events(hass)
|
||||
except HTTPError:
|
||||
hass.components.persistent_notification.create(
|
||||
'Doorbird configuration failed. Please verify that API '
|
||||
@ -124,15 +96,15 @@ def setup(hass, config):
|
||||
|
||||
def _reset_device_favorites_handler(event):
|
||||
"""Handle clearing favorites on device."""
|
||||
slug = event.data.get('slug')
|
||||
token = event.data.get('token')
|
||||
|
||||
if slug is None:
|
||||
if token is None:
|
||||
return
|
||||
|
||||
doorstation = get_doorstation_by_slug(hass, slug)
|
||||
doorstation = get_doorstation_by_token(hass, token)
|
||||
|
||||
if doorstation is None:
|
||||
_LOGGER.error('Device not found %s', format(slug))
|
||||
_LOGGER.error('Device not found for provided token.')
|
||||
|
||||
# Clear webhooks
|
||||
favorites = doorstation.device.favorites()
|
||||
@ -146,30 +118,22 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
def get_doorstation_by_slug(hass, slug):
|
||||
def get_doorstation_by_token(hass, token):
|
||||
"""Get doorstation by slug."""
|
||||
for doorstation in hass.data[DOMAIN]:
|
||||
if slugify(doorstation.name) in slug:
|
||||
if token == doorstation.token:
|
||||
return doorstation
|
||||
|
||||
|
||||
def handle_event(event):
|
||||
"""Handle dummy events."""
|
||||
return None
|
||||
|
||||
|
||||
class ConfiguredDoorBird():
|
||||
"""Attach additional information to pass along with configured device."""
|
||||
|
||||
def __init__(self, device, name, events, custom_url, doorbell_nums,
|
||||
relay_nums, token):
|
||||
def __init__(self, device, name, events, custom_url, token):
|
||||
"""Initialize configured device."""
|
||||
self._name = name
|
||||
self._device = device
|
||||
self._custom_url = custom_url
|
||||
self._monitored_events = events
|
||||
self._doorbell_nums = doorbell_nums
|
||||
self._relay_nums = relay_nums
|
||||
self._events = events
|
||||
self._token = token
|
||||
|
||||
@property
|
||||
@ -187,14 +151,13 @@ class ConfiguredDoorBird():
|
||||
"""Get custom url for device."""
|
||||
return self._custom_url
|
||||
|
||||
def update_schedule(self, hass):
|
||||
"""Register monitored sensors and deregister others."""
|
||||
from doorbirdpy import DoorBirdScheduleEntrySchedule
|
||||
|
||||
# Create a new schedule (24/7)
|
||||
schedule = DoorBirdScheduleEntrySchedule()
|
||||
schedule.add_weekday(0, 604800) # seconds in a week
|
||||
@property
|
||||
def token(self):
|
||||
"""Get token for device."""
|
||||
return self._token
|
||||
|
||||
def register_events(self, hass):
|
||||
"""Register events on device."""
|
||||
# Get the URL of this server
|
||||
hass_url = hass.config.api.base_url
|
||||
|
||||
@ -202,98 +165,39 @@ class ConfiguredDoorBird():
|
||||
if self.custom_url is not None:
|
||||
hass_url = self.custom_url
|
||||
|
||||
# For all sensor types (enabled + disabled)
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
name = '{} {}'.format(self.name, SENSOR_TYPES[sensor_type]['name'])
|
||||
slug = slugify(name)
|
||||
for event in self._events:
|
||||
event = self._get_event_name(event)
|
||||
|
||||
url = '{}{}/{}?token={}'.format(hass_url, API_URL, slug,
|
||||
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)
|
||||
self._register_event(hass_url, event)
|
||||
|
||||
# Register a dummy listener so event is listed in GUI
|
||||
hass.bus.listen('{}_{}'.format(DOMAIN, slug), handle_event)
|
||||
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)
|
||||
_LOGGER.info('Successfully registered URL for %s on %s',
|
||||
event, 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."""
|
||||
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
|
||||
if not self.webhook_is_registered(hass_url):
|
||||
self.device.change_favorite('http', 'Home Assistant ({} events)'
|
||||
.format(event), hass_url)
|
||||
if not self.webhook_is_registered(url):
|
||||
self.device.change_favorite('http', 'Home Assistant ({})'
|
||||
.format(event), url)
|
||||
|
||||
fav_id = self.get_webhook_id(hass_url)
|
||||
fav_id = self.get_webhook_id(url)
|
||||
|
||||
if not fav_id:
|
||||
_LOGGER.warning('Could not find favorite for URL "%s". '
|
||||
'Skipping sensor "%s".', hass_url, event)
|
||||
'Skipping sensor "%s"', url, event)
|
||||
return
|
||||
|
||||
# Add event handling to device schedule
|
||||
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:
|
||||
def webhook_is_registered(self, url, favs=None) -> bool:
|
||||
"""Return whether the given URL is registered as a device favorite."""
|
||||
favs = favs if favs else self.device.favorites()
|
||||
|
||||
@ -301,12 +205,12 @@ class ConfiguredDoorBird():
|
||||
return False
|
||||
|
||||
for fav in favs['http'].values():
|
||||
if fav['value'] == ha_url:
|
||||
if fav['value'] == url:
|
||||
return True
|
||||
|
||||
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.
|
||||
|
||||
@ -318,7 +222,7 @@ class ConfiguredDoorBird():
|
||||
return None
|
||||
|
||||
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 None
|
||||
@ -340,72 +244,33 @@ class DoorBirdRequestView(HomeAssistantView):
|
||||
requires_auth = False
|
||||
url = API_URL
|
||||
name = API_URL[1:].replace('/', ':')
|
||||
extra_urls = [API_URL + '/{sensor}']
|
||||
|
||||
def __init__(self, token):
|
||||
"""Initialize view."""
|
||||
HomeAssistantView.__init__(self)
|
||||
self._token = token
|
||||
extra_urls = [API_URL + '/{event}']
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
async def get(self, request, sensor):
|
||||
async def get(self, request, event):
|
||||
"""Respond to requests from the device."""
|
||||
from aiohttp import web
|
||||
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:
|
||||
return web.Response(status=401, text='Unauthorized')
|
||||
if device is None:
|
||||
return web.Response(status=401, text='Invalid token provided.')
|
||||
|
||||
doorstation = get_doorstation_by_slug(hass, sensor)
|
||||
|
||||
if doorstation:
|
||||
event_data = doorstation.get_event_data()
|
||||
if device:
|
||||
event_data = device.get_event_data()
|
||||
else:
|
||||
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')
|
||||
|
||||
|
||||
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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user