Home Assistant Unofficial Reference 2024.12.1
device.py
Go to the documentation of this file.
1 """Support for DoorBird devices."""
2 
3 from __future__ import annotations
4 
5 from collections import defaultdict
6 from dataclasses import dataclass
7 from http import HTTPStatus
8 import logging
9 from typing import Any
10 
11 from aiohttp import ClientResponseError
12 from doorbirdpy import (
13  DoorBird,
14  DoorBirdScheduleEntry,
15  DoorBirdScheduleEntryOutput,
16  DoorBirdScheduleEntrySchedule,
17 )
18 from propcache import cached_property
19 
20 from homeassistant.const import ATTR_ENTITY_ID
21 from homeassistant.core import HomeAssistant
22 from homeassistant.helpers.network import get_url
23 from homeassistant.util import dt as dt_util, slugify
24 
25 from .const import (
26  API_URL,
27  DEFAULT_EVENT_TYPES,
28  HTTP_EVENT_TYPE,
29  MAX_WEEKDAY,
30  MIN_WEEKDAY,
31 )
32 
33 _LOGGER = logging.getLogger(__name__)
34 
35 
36 @dataclass(slots=True)
38  """Describes a doorbird event."""
39 
40  event: str
41  event_type: str
42 
43 
44 @dataclass(slots=True)
46  """Describes the configuration of doorbird events."""
47 
48  events: list[DoorbirdEvent]
49  schedule: list[DoorBirdScheduleEntry]
50  unconfigured_favorites: defaultdict[str, list[str]]
51 
52 
54  """Attach additional information to pass along with configured device."""
55 
56  def __init__(
57  self,
58  hass: HomeAssistant,
59  device: DoorBird,
60  name: str | None,
61  custom_url: str | None,
62  token: str,
63  event_entity_ids: dict[str, str],
64  ) -> None:
65  """Initialize configured device."""
66  self._hass_hass = hass
67  self._name_name = name
68  self._device_device = device
69  self._custom_url_custom_url = custom_url
70  self._token_token = token
71  self._event_entity_ids_event_entity_ids = event_entity_ids
72  # Raw events, ie "doorbell" or "motion"
73  self.eventsevents: list[str] = []
74  # Event names, ie "doorbird_1234_doorbell" or "doorbird_1234_motion"
75  self.door_station_eventsdoor_station_events: list[str] = []
76  self.event_descriptionsevent_descriptions: list[DoorbirdEvent] = []
77 
78  def update_events(self, events: list[str]) -> None:
79  """Update the doorbird events."""
80  self.eventsevents = events
81  self.door_station_eventsdoor_station_events = [
82  self._get_event_name_get_event_name(event) for event in self.eventsevents
83  ]
84 
85  @cached_property
86  def name(self) -> str | None:
87  """Get custom device name."""
88  return self._name_name
89 
90  @cached_property
91  def device(self) -> DoorBird:
92  """Get the configured device."""
93  return self._device_device
94 
95  @cached_property
96  def custom_url(self) -> str | None:
97  """Get custom url for device."""
98  return self._custom_url_custom_url
99 
100  @cached_property
101  def token(self) -> str:
102  """Get token for device."""
103  return self._token_token
104 
105  async def async_register_events(self) -> None:
106  """Register events on device."""
107  if not self.door_station_eventsdoor_station_events:
108  # The config entry might not have any events configured yet
109  return
110  http_fav = await self._async_register_events_async_register_events()
111  event_config = await self._async_get_event_config_async_get_event_config(http_fav)
112  _LOGGER.debug("%s: Event config: %s", self.namename, event_config)
113  if event_config.unconfigured_favorites:
114  await self._configure_unconfigured_favorites_configure_unconfigured_favorites(event_config)
115  event_config = await self._async_get_event_config_async_get_event_config(http_fav)
116  self.event_descriptionsevent_descriptions = event_config.events
117 
119  self, event_config: DoorbirdEventConfig
120  ) -> None:
121  """Configure unconfigured favorites."""
122  for entry in event_config.schedule:
123  modified_schedule = False
124  for identifier in event_config.unconfigured_favorites.get(entry.input, ()):
125  schedule = DoorBirdScheduleEntrySchedule()
126  schedule.add_weekday(MIN_WEEKDAY, MAX_WEEKDAY)
127  entry.output.append(
128  DoorBirdScheduleEntryOutput(
129  enabled=True,
130  event=HTTP_EVENT_TYPE,
131  param=identifier,
132  schedule=schedule,
133  )
134  )
135  modified_schedule = True
136 
137  if modified_schedule:
138  update_ok, code = await self.devicedevice.change_schedule(entry)
139  if not update_ok:
140  _LOGGER.error(
141  "Unable to update schedule entry %s to %s. Error code: %s",
142  self.namename,
143  entry.export,
144  code,
145  )
146 
147  async def _async_register_events(self) -> dict[str, Any]:
148  """Register events on device."""
149  # Override url if another is specified in the configuration
150  if custom_url := self.custom_urlcustom_url:
151  hass_url = custom_url
152  else:
153  # Get the URL of this server
154  hass_url = get_url(self._hass_hass, prefer_external=False)
155 
156  http_fav = await self._async_get_http_favorites_async_get_http_favorites()
157  if any(
158  # Note that a list comp is used here to ensure all
159  # events are registered and the any does not short circuit
160  [
161  await self._async_register_event_async_register_event(hass_url, event, http_fav)
162  for event in self.door_station_eventsdoor_station_events
163  ]
164  ):
165  # If any events were registered, get the updated favorites
166  http_fav = await self._async_get_http_favorites_async_get_http_favorites()
167 
168  return http_fav
169 
171  self, http_fav: dict[str, dict[str, Any]]
172  ) -> DoorbirdEventConfig:
173  """Get events and unconfigured favorites from http favorites."""
174  device = self.devicedevice
175  events: list[DoorbirdEvent] = []
176  unconfigured_favorites: defaultdict[str, list[str]] = defaultdict(list)
177  try:
178  schedule = await device.schedule()
179  except ClientResponseError as ex:
180  if ex.status == HTTPStatus.NOT_FOUND:
181  # D301 models do not support schedules
182  return DoorbirdEventConfig(events, [], unconfigured_favorites)
183  raise
184  favorite_input_type = {
185  output.param: entry.input
186  for entry in schedule
187  for output in entry.output
188  if output.event == HTTP_EVENT_TYPE
189  }
190  default_event_types = {
191  self._get_event_name_get_event_name(event): event_type
192  for event, event_type in DEFAULT_EVENT_TYPES
193  }
194  for identifier, data in http_fav.items():
195  title: str | None = data.get("title")
196  if not title or not title.startswith("Home Assistant"):
197  continue
198  event = title.partition("(")[2].strip(")")
199  if input_type := favorite_input_type.get(identifier):
200  events.append(DoorbirdEvent(event, input_type))
201  elif input_type := default_event_types.get(event):
202  unconfigured_favorites[input_type].append(identifier)
203 
204  return DoorbirdEventConfig(events, schedule, unconfigured_favorites)
205 
206  @cached_property
207  def slug(self) -> str:
208  """Get device slug."""
209  return slugify(self._name_name)
210 
211  def _get_event_name(self, event: str) -> str:
212  return f"{self.slug}_{event}"
213 
214  async def _async_get_http_favorites(self) -> dict[str, dict[str, Any]]:
215  """Get the HTTP favorites from the device."""
216  return (await self.devicedevice.favorites()).get(HTTP_EVENT_TYPE) or {}
217 
219  self, hass_url: str, event: str, http_fav: dict[str, dict[str, Any]]
220  ) -> bool:
221  """Register an event.
222 
223  Returns True if the event was registered, False if
224  the event was already registered or registration failed.
225  """
226  url = f"{hass_url}{API_URL}/{event}?token={self._token}"
227  _LOGGER.debug("Registering URL %s for event %s", url, event)
228  # If its already registered, don't register it again
229  if any(fav["value"] == url for fav in http_fav.values()):
230  _LOGGER.debug("URL already registered for %s", event)
231  return False
232 
233  if not await self.devicedevice.change_favorite(
234  HTTP_EVENT_TYPE, f"Home Assistant ({event})", url
235  ):
236  _LOGGER.warning(
237  'Unable to set favorite URL "%s". Event "%s" will not fire',
238  url,
239  event,
240  )
241  return False
242 
243  _LOGGER.debug("Successfully registered URL for %s on %s", event, self.namename)
244  return True
245 
246  def get_event_data(self, event: str) -> dict[str, str | None]:
247  """Get data to pass along with HA event."""
248  return {
249  "timestamp": dt_util.utcnow().isoformat(),
250  "live_video_url": self._device_device.live_video_url,
251  "live_image_url": self._device_device.live_image_url,
252  "rtsp_live_video_url": self._device_device.rtsp_live_video_url,
253  "html5_viewer_url": self._device_device.html5_viewer_url,
254  ATTR_ENTITY_ID: self._event_entity_ids_event_entity_ids.get(event),
255  }
256 
257 
258 async def async_reset_device_favorites(door_station: ConfiguredDoorBird) -> None:
259  """Handle clearing favorites on device."""
260  door_bird = door_station.device
261  favorites = await door_bird.favorites()
262  for favorite_type, favorite_ids in favorites.items():
263  for favorite_id in favorite_ids:
264  await door_bird.delete_favorite(favorite_type, favorite_id)
265  await door_station.async_register_events()
DoorbirdEventConfig _async_get_event_config(self, dict[str, dict[str, Any]] http_fav)
Definition: device.py:172
None __init__(self, HomeAssistant hass, DoorBird device, str|None name, str|None custom_url, str token, dict[str, str] event_entity_ids)
Definition: device.py:64
None _configure_unconfigured_favorites(self, DoorbirdEventConfig event_config)
Definition: device.py:120
dict[str, dict[str, Any]] _async_get_http_favorites(self)
Definition: device.py:214
dict[str, str|None] get_event_data(self, str event)
Definition: device.py:246
bool _async_register_event(self, str hass_url, str event, dict[str, dict[str, Any]] http_fav)
Definition: device.py:220
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_reset_device_favorites(ConfiguredDoorBird door_station)
Definition: device.py:258
str get_url(HomeAssistant hass, *bool require_current_request=False, bool require_ssl=False, bool require_standard_port=False, bool require_cloud=False, bool allow_internal=True, bool allow_external=True, bool allow_cloud=True, bool|None allow_ip=None, bool|None prefer_external=None, bool prefer_cloud=False)
Definition: network.py:131