""" Support for DoorBird device. For more details about this component, please refer to the documentation at https://home-assistant.io/components/doorbird/ """ import logging import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_HOST, CONF_USERNAME, \ CONF_PASSWORD, CONF_NAME, CONF_DEVICES, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify, dt as dt_util REQUIREMENTS = ['doorbirdpy==2.0.4'] _LOGGER = logging.getLogger(__name__) 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' CONF_TOKEN = 'token' 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' 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.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)]), }) 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) 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)) doorstations = [] for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]): 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) name = (doorstation_config.get(CONF_NAME) or 'DoorBird {}'.format(index + 1)) device = DoorBird(device_ip, username, password) status = device.ready() if status[0]: doorstation = ConfiguredDoorBird(device, name, events, custom_url, doorbell_nums, relay_nums, token) doorstations.append(doorstation) _LOGGER.info('Connected to DoorBird "%s" as %s@%s', doorstation.name, username, device_ip) elif status[1] == 401: _LOGGER.error("Authorization rejected by DoorBird for %s@%s", username, device_ip) return False else: _LOGGER.error("Could not connect to DoorBird as %s@%s: Error %s", username, device_ip, str(status[1])) return False # Subscribe to doorbell or motion events if events: doorstation.update_schedule(hass) hass.data[DOMAIN] = doorstations def _reset_device_favorites_handler(event): """Handle clearing favorites on device.""" slug = event.data.get('slug') if slug is None: return doorstation = get_doorstation_by_slug(hass, slug) if doorstation is None: _LOGGER.error('Device not found %s', format(slug)) # Clear webhooks favorites = doorstation.device.favorites() for favorite_type in favorites: for favorite_id in favorites[favorite_type]: doorstation.device.delete_favorite(favorite_type, favorite_id) hass.bus.listen(RESET_DEVICE_FAVORITES, _reset_device_favorites_handler) return True def get_doorstation_by_slug(hass, slug): """Get doorstation by slug.""" for doorstation in hass.data[DOMAIN]: if slugify(doorstation.name) in slug: 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): """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._token = token @property def name(self): """Get custom device name.""" return self._name @property def device(self): """Get the configured device.""" return self._device @property def custom_url(self): """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 # Get the URL of this server hass_url = hass.config.api.base_url # Override url if another is specified in the configuration 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) 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) # 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) def _register_event(self, hass_url, event, schedule): """Add a schedule entry in the device for a sensor.""" from doorbirdpy import DoorBirdScheduleEntryOutput # 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) fav_id = self.get_webhook_id(hass_url) if not fav_id: _LOGGER.warning('Could not find favorite for URL "%s". ' 'Skipping sensor "%s".', hass_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) resp = self.device.change_schedule(entry) return resp 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.""" favs = favs if favs else self.device.favorites() if 'http' not in favs: return False for fav in favs['http'].values(): if fav['value'] == ha_url: return True return False def get_webhook_id(self, ha_url, favs=None) -> str or None: """ Return the device favorite ID for the given URL. The favorite must exist or there will be problems. """ favs = favs if favs else self.device.favorites() if 'http' not in favs: return None for fav_id in favs['http']: if favs['http'][fav_id]['value'] == ha_url: return fav_id return None def get_event_data(self): """Get data to pass along with HA event.""" return { 'timestamp': dt_util.utcnow().isoformat(), 'live_video_url': self._device.live_video_url, 'live_image_url': self._device.live_image_url, 'rtsp_live_video_url': self._device.rtsp_live_video_url, 'html5_viewer_url': self._device.html5_viewer_url } class DoorBirdRequestView(HomeAssistantView): """Provide a page for the device to call.""" 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 # pylint: disable=no-self-use async def get(self, request, sensor): """Respond to requests from the device.""" 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') doorstation = get_doorstation_by_slug(hass, sensor) if doorstation: event_data = doorstation.get_event_data() else: event_data = {} hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor), 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)