mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +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.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)
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user