1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-30 12:14:20 +01:00
Files
2026-04-30 21:14:48 +02:00

268 lines
9.4 KiB
Python

"""Support for DoorBird devices."""
from collections import defaultdict
from dataclasses import dataclass
from http import HTTPStatus
import logging
from typing import Any
from aiohttp import ClientResponseError
from doorbirdpy import (
DoorBird,
DoorBirdScheduleEntry,
DoorBirdScheduleEntryOutput,
DoorBirdScheduleEntrySchedule,
)
from propcache.api import cached_property
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,
DEFAULT_EVENT_TYPES,
HTTP_EVENT_TYPE,
MAX_WEEKDAY,
MIN_WEEKDAY,
)
_LOGGER = logging.getLogger(__name__)
@dataclass(slots=True)
class DoorbirdEvent:
"""Describes a doorbird event."""
event: str
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."""
def __init__(
self,
hass: HomeAssistant,
device: DoorBird,
name: str | None,
custom_url: str | None,
token: str,
event_entity_ids: dict[str, str],
) -> None:
"""Initialize configured device."""
self._hass = hass
self._name = name
self._device = device
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] = []
def update_events(self, events: list[str]) -> None:
"""Update the doorbird events."""
self.events = events
self.door_station_events = [
self._get_event_name(event) for event in self.events
]
@cached_property
def name(self) -> str | None:
"""Get custom device name."""
return self._name
@cached_property
def device(self) -> DoorBird:
"""Get the configured device."""
return self._device
@cached_property
def custom_url(self) -> str | None:
"""Get custom url for device."""
return self._custom_url
@cached_property
def token(self) -> str:
"""Get token for device."""
return self._token
def _get_hass_url(self) -> str:
"""Get the Home Assistant URL for this device."""
if custom_url := self.custom_url:
return custom_url
return get_url(self._hass, prefer_external=False)
async def async_register_events(self) -> None:
"""Register events on device."""
if not self.door_station_events:
# The config entry might not have any events configured yet
return
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
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."""
hass_url = self._get_hass_url()
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
events: list[DoorbirdEvent] = []
unconfigured_favorites: defaultdict[str, list[str]] = defaultdict(list)
try:
schedule = await device.schedule()
except ClientResponseError as ex:
if ex.status == HTTPStatus.NOT_FOUND:
# D301 models do not support schedules
return DoorbirdEventConfig(events, [], unconfigured_favorites)
raise
favorite_input_type = {
output.param: entry.input
for entry in schedule
for output in entry.output
if output.event == HTTP_EVENT_TYPE
}
default_event_types = {
self._get_event_name(event): event_type
for event, event_type in DEFAULT_EVENT_TYPES
}
hass_url = self._get_hass_url()
for identifier, data in http_fav.items():
title: str | None = data.get("title")
if not title or not title.startswith("Home Assistant"):
continue
value: str | None = data.get("value")
if not value or not value.startswith(hass_url):
continue # Not our favorite - different HA instance or stale
event = title.partition("(")[2].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)
return DoorbirdEventConfig(events, schedule, unconfigured_favorites)
@cached_property
def slug(self) -> str:
"""Get device slug."""
return slugify(self._name)
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, http_fav: dict[str, dict[str, Any]]
) -> bool:
"""Register an event.
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
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
_LOGGER.debug("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."""
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,
ATTR_ENTITY_ID: self._event_entity_ids.get(event),
}
async def async_reset_device_favorites(door_station: ConfiguredDoorBird) -> None:
"""Handle clearing favorites on device."""
door_bird = door_station.device
favorites = await door_bird.favorites()
for favorite_type, favorite_ids in favorites.items():
for favorite_id in favorite_ids:
await door_bird.delete_favorite(favorite_type, favorite_id)
await door_station.async_register_events()