From ab7c4244d2745b1915bc5ce5b6623672c21e91f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 11 Jul 2024 04:31:29 -0500 Subject: [PATCH] Pre-configure default doorbird events (#121692) --- .../components/doorbird/config_flow.py | 15 +- homeassistant/components/doorbird/const.py | 13 ++ homeassistant/components/doorbird/device.py | 169 +++++++++++++----- tests/components/doorbird/test_config_flow.py | 10 +- 4 files changed, 154 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 13e7d151d2f..f91e498b5e7 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -22,11 +22,19 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI +from .const import ( + CONF_EVENTS, + DEFAULT_DOORBELL_EVENT, + DEFAULT_MOTION_EVENT, + DOMAIN, + DOORBIRD_OUI, +) from .util import get_mac_address_from_door_station_info _LOGGER = logging.getLogger(__name__) +DEFAULT_OPTIONS = {CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT]} + def _schema_with_defaults( host: str | None = None, name: str | None = None @@ -99,7 +107,9 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN): if not errors: await self.async_set_unique_id(info["mac_addr"]) self._abort_if_unique_id_configured() - return self.async_create_entry(title=info["title"], data=user_input) + return self.async_create_entry( + title=info["title"], data=user_input, options=DEFAULT_OPTIONS + ) data = self.discovery_schema or _schema_with_defaults() return self.async_show_form(step_id="user", data_schema=data, errors=errors) @@ -176,7 +186,6 @@ class OptionsFlowHandler(OptionsFlow): """Handle options flow.""" if user_input is not None: events = [event.strip() for event in user_input[CONF_EVENTS].split(",")] - return self.async_create_entry(title="", data={CONF_EVENTS: events}) current_events = self.config_entry.options.get(CONF_EVENTS, []) diff --git a/homeassistant/components/doorbird/const.py b/homeassistant/components/doorbird/const.py index 4985b9ac9ea..40dafb5bdc8 100644 --- a/homeassistant/components/doorbird/const.py +++ b/homeassistant/components/doorbird/const.py @@ -22,3 +22,16 @@ DOORBIRD_INFO_KEY_WIFI_MAC_ADDR = "WIFI_MAC_ADDR" UNDO_UPDATE_LISTENER = "undo_update_listener" API_URL = f"/api/{DOMAIN}" + + +DEFAULT_DOORBELL_EVENT = "doorbell" +DEFAULT_MOTION_EVENT = "motion" + +DEFAULT_EVENT_TYPES = ( + (DEFAULT_DOORBELL_EVENT, "doorbell"), + (DEFAULT_MOTION_EVENT, "motion"), +) + +HTTP_EVENT_TYPE = "http" +MIN_WEEKDAY = 104400 +MAX_WEEKDAY = 104399 diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py index 84a2d3abeab..9bb3397d0ff 100644 --- a/homeassistant/components/doorbird/device.py +++ b/homeassistant/components/doorbird/device.py @@ -2,19 +2,31 @@ from __future__ import annotations +from collections import defaultdict from dataclasses import dataclass from functools import cached_property import logging from typing import Any -from doorbirdpy import DoorBird, DoorBirdScheduleEntry +from doorbirdpy import ( + DoorBird, + DoorBirdScheduleEntry, + DoorBirdScheduleEntryOutput, + DoorBirdScheduleEntrySchedule, +) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers.network import get_url from homeassistant.util import dt as dt_util, slugify -from .const import API_URL +from .const import ( + API_URL, + DEFAULT_EVENT_TYPES, + HTTP_EVENT_TYPE, + MAX_WEEKDAY, + MIN_WEEKDAY, +) _LOGGER = logging.getLogger(__name__) @@ -27,6 +39,15 @@ class DoorbirdEvent: event_type: str +@dataclass(slots=True) +class DoorbirdEventConfig: + """Describes the configuration of doorbird events.""" + + events: list[DoorbirdEvent] + schedule: list[DoorBirdScheduleEntry] + unconfigured_favorites: defaultdict[str, list[str]] + + class ConfiguredDoorBird: """Attach additional information to pass along with configured device.""" @@ -46,7 +67,9 @@ class ConfiguredDoorBird: self._custom_url = custom_url self._token = token self._event_entity_ids = event_entity_ids + # Raw events, ie "doorbell" or "motion" self.events: list[str] = [] + # Event names, ie "doorbird_1234_doorbell" or "doorbird_1234_motion" self.door_station_events: list[str] = [] self.event_descriptions: list[DoorbirdEvent] = [] @@ -79,34 +102,88 @@ class ConfiguredDoorBird: async def async_register_events(self) -> None: """Register events on device.""" - hass = self._hass - # Override url if another is specified in the configuration - if custom_url := self.custom_url: - hass_url = custom_url - else: - # Get the URL of this server - hass_url = get_url(hass, prefer_external=False) - if not self.door_station_events: # User may not have permission to get the favorites return - favorites = await self.device.favorites() - for event in self.door_station_events: - if await self._async_register_event(hass_url, event, favs=favorites): - _LOGGER.info( - "Successfully registered URL for %s on %s", event, self.name - ) + http_fav = await self._async_register_events() + event_config = await self._async_get_event_config(http_fav) + _LOGGER.debug("%s: Event config: %s", self.name, event_config) + if event_config.unconfigured_favorites: + await self._configure_unconfigured_favorites(event_config) + event_config = await self._async_get_event_config(http_fav) + self.event_descriptions = event_config.events - schedule: list[DoorBirdScheduleEntry] = await self.device.schedule() - http_fav: dict[str, dict[str, Any]] = favorites.get("http") or {} - favorite_input_type: dict[str, str] = { + async def _configure_unconfigured_favorites( + self, event_config: DoorbirdEventConfig + ) -> None: + """Configure unconfigured favorites.""" + for entry in event_config.schedule: + modified_schedule = False + for identifier in event_config.unconfigured_favorites.get(entry.input, ()): + schedule = DoorBirdScheduleEntrySchedule() + schedule.add_weekday(MIN_WEEKDAY, MAX_WEEKDAY) + entry.output.append( + DoorBirdScheduleEntryOutput( + enabled=True, + event=HTTP_EVENT_TYPE, + param=identifier, + schedule=schedule, + ) + ) + modified_schedule = True + + if modified_schedule: + update_ok, code = await self.device.change_schedule(entry) + if not update_ok: + _LOGGER.error( + "Unable to update schedule entry %s to %s. Error code: %s", + self.name, + entry.export, + code, + ) + + async def _async_register_events(self) -> dict[str, Any]: + """Register events on device.""" + # Override url if another is specified in the configuration + if custom_url := self.custom_url: + hass_url = custom_url + else: + # Get the URL of this server + hass_url = get_url(self._hass, prefer_external=False) + + http_fav = await self._async_get_http_favorites() + if any( + # Note that a list comp is used here to ensure all + # events are registered and the any does not short circuit + [ + await self._async_register_event(hass_url, event, http_fav) + for event in self.door_station_events + ] + ): + # If any events were registered, get the updated favorites + http_fav = await self._async_get_http_favorites() + + return http_fav + + async def _async_get_event_config( + self, http_fav: dict[str, dict[str, Any]] + ) -> DoorbirdEventConfig: + """Get events and unconfigured favorites from http favorites.""" + device = self.device + schedule = await device.schedule() + favorite_input_type = { output.param: entry.input for entry in schedule for output in entry.output - if output.event == "http" + if output.event == HTTP_EVENT_TYPE } events: list[DoorbirdEvent] = [] + unconfigured_favorites: defaultdict[str, list[str]] = defaultdict(list) + default_event_types = { + self._get_event_name(event): event_type + for event, event_type in DEFAULT_EVENT_TYPES + } for identifier, data in http_fav.items(): title: str | None = data.get("title") if not title or not title.startswith("Home Assistant"): @@ -114,8 +191,10 @@ class ConfiguredDoorBird: event = title.split("(")[1].strip(")") if input_type := favorite_input_type.get(identifier): events.append(DoorbirdEvent(event, input_type)) + elif input_type := default_event_types.get(event): + unconfigured_favorites[input_type].append(identifier) - self.event_descriptions = events + return DoorbirdEventConfig(events, schedule, unconfigured_favorites) @cached_property def slug(self) -> str: @@ -125,45 +204,37 @@ class ConfiguredDoorBird: def _get_event_name(self, event: str) -> str: return f"{self.slug}_{event}" + async def _async_get_http_favorites(self) -> dict[str, dict[str, Any]]: + """Get the HTTP favorites from the device.""" + return (await self.device.favorites()).get(HTTP_EVENT_TYPE) or {} + async def _async_register_event( - self, hass_url: str, event: str, favs: dict[str, Any] | None = None + self, hass_url: str, event: str, http_fav: dict[str, dict[str, Any]] ) -> bool: - """Add a schedule entry in the device for a sensor.""" - url = f"{hass_url}{API_URL}/{event}?token={self._token}" + """Register an event. - # Register HA URL as webhook if not already, then get the ID - if await self.async_webhook_is_registered(url, favs=favs): - return True + Returns True if the event was registered, False if + the event was already registered or registration failed. + """ + url = f"{hass_url}{API_URL}/{event}?token={self._token}" + _LOGGER.debug("Registering URL %s for event %s", url, event) + # If its already registered, don't register it again + if any(fav["value"] == url for fav in http_fav.values()): + _LOGGER.debug("URL already registered for %s", event) + return False - await self.device.change_favorite("http", f"Home Assistant ({event})", url) - if not await self.async_webhook_is_registered(url): + if not await self.device.change_favorite( + HTTP_EVENT_TYPE, f"Home Assistant ({event})", url + ): _LOGGER.warning( 'Unable to set favorite URL "%s". Event "%s" will not fire', url, event, ) return False - return True - - async def async_webhook_is_registered( - self, url: str, favs: dict[str, Any] | None = None - ) -> bool: - """Return whether the given URL is registered as a device favorite.""" - return await self.async_get_webhook_id(url, favs) is not None - async def async_get_webhook_id( - self, url: str, favs: dict[str, Any] | None = None - ) -> str | None: - """Return the device favorite ID for the given URL. - - The favorite must exist or there will be problems. - """ - favs = favs if favs else await self.device.favorites() - http_fav: dict[str, dict[str, Any]] = favs.get("http") or {} - for fav_id, data in http_fav.items(): - if data["value"] == url: - return fav_id - return None + _LOGGER.info("Successfully registered URL for %s on %s", event, self.name) + return True def get_event_data(self, event: str) -> dict[str, str | None]: """Get data to pass along with HA event.""" diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index d77c5a81d96..107fd1454d3 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -8,7 +8,12 @@ import pytest from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN +from homeassistant.components.doorbird.const import ( + CONF_EVENTS, + DEFAULT_DOORBELL_EVENT, + DEFAULT_MOTION_EVENT, + DOMAIN, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -82,6 +87,9 @@ async def test_user_form(hass: HomeAssistant) -> None: "password": "password", "username": "friend", } + assert result2["options"] == { + CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT] + } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -- GitLab