Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """Coordinator for Tedee locks."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Awaitable, Callable
6 from datetime import timedelta
7 import logging
8 import time
9 from typing import Any
10 
11 from aiotedee import (
12  TedeeClient,
13  TedeeClientException,
14  TedeeDataUpdateException,
15  TedeeLocalAuthException,
16  TedeeLock,
17  TedeeWebhookException,
18 )
19 from aiotedee.bridge import TedeeBridge
20 
21 from homeassistant.config_entries import ConfigEntry
22 from homeassistant.const import CONF_HOST
23 from homeassistant.core import HomeAssistant
24 from homeassistant.exceptions import ConfigEntryAuthFailed
25 from homeassistant.helpers.aiohttp_client import async_get_clientsession
27 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
28 
29 from .const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN
30 
31 SCAN_INTERVAL = timedelta(seconds=30)
32 GET_LOCKS_INTERVAL_SECONDS = 3600
33 
34 _LOGGER = logging.getLogger(__name__)
35 
36 type TedeeConfigEntry = ConfigEntry[TedeeApiCoordinator]
37 
38 
39 class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]):
40  """Class to handle fetching data from the tedee API centrally."""
41 
42  config_entry: TedeeConfigEntry
43  bridge: TedeeBridge
44 
45  def __init__(self, hass: HomeAssistant, entry: TedeeConfigEntry) -> None:
46  """Initialize coordinator."""
47  super().__init__(
48  hass,
49  _LOGGER,
50  config_entry=entry,
51  name=DOMAIN,
52  update_interval=SCAN_INTERVAL,
53  )
54 
55  self.tedee_clienttedee_client = TedeeClient(
56  local_token=self.config_entryconfig_entry.data[CONF_LOCAL_ACCESS_TOKEN],
57  local_ip=self.config_entryconfig_entry.data[CONF_HOST],
58  session=async_get_clientsession(hass),
59  )
60 
61  self._next_get_locks_next_get_locks = time.time()
62  self._locks_last_update_locks_last_update: set[int] = set()
63  self.new_lock_callbacks: list[Callable[[int], None]] = []
64  self.tedee_webhook_idtedee_webhook_id: int | None = None
65 
66  async def _async_setup(self) -> None:
67  """Set up the coordinator."""
68 
69  async def _async_get_bridge() -> None:
70  self.bridgebridge = await self.tedee_clienttedee_client.get_local_bridge()
71 
72  _LOGGER.debug("Update coordinator: Getting bridge from API")
73  await self._async_update_async_update(_async_get_bridge)
74 
75  async def _async_update_data(self) -> dict[int, TedeeLock]:
76  """Fetch data from API endpoint."""
77 
78  _LOGGER.debug("Update coordinator: Getting locks from API")
79  # once every hours get all lock details, otherwise use the sync endpoint
80  if self._next_get_locks_next_get_locks <= time.time():
81  _LOGGER.debug("Updating through /my/lock endpoint")
82  await self._async_update_async_update(self.tedee_clienttedee_client.get_locks)
83  self._next_get_locks_next_get_locks = time.time() + GET_LOCKS_INTERVAL_SECONDS
84  else:
85  _LOGGER.debug("Updating through /sync endpoint")
86  await self._async_update_async_update(self.tedee_clienttedee_client.sync)
87 
88  _LOGGER.debug(
89  "available_locks: %s",
90  ", ".join(map(str, self.tedee_clienttedee_client.locks_dict.keys())),
91  )
92 
93  self._async_add_remove_locks_async_add_remove_locks()
94  return self.tedee_clienttedee_client.locks_dict
95 
96  async def _async_update(self, update_fn: Callable[[], Awaitable[None]]) -> None:
97  """Update locks based on update function."""
98  try:
99  await update_fn()
100  except TedeeLocalAuthException as ex:
101  raise ConfigEntryAuthFailed(
102  translation_domain=DOMAIN,
103  translation_key="authentification_failed",
104  ) from ex
105 
106  except TedeeDataUpdateException as ex:
107  _LOGGER.debug("Error while updating data: %s", str(ex))
108  raise UpdateFailed(
109  translation_domain=DOMAIN, translation_key="update_failed"
110  ) from ex
111  except (TedeeClientException, TimeoutError) as ex:
112  raise UpdateFailed(
113  translation_domain=DOMAIN, translation_key="api_error"
114  ) from ex
115 
116  def webhook_received(self, message: dict[str, Any]) -> None:
117  """Handle webhook message."""
118  self.tedee_clienttedee_client.parse_webhook_message(message)
119  self.async_set_updated_dataasync_set_updated_data(self.tedee_clienttedee_client.locks_dict)
120 
121  async def async_register_webhook(self, webhook_url: str) -> None:
122  """Register the webhook at the Tedee bridge."""
123  self.tedee_webhook_idtedee_webhook_id = await self.tedee_clienttedee_client.register_webhook(webhook_url)
124 
125  async def async_unregister_webhook(self) -> None:
126  """Unregister the webhook at the Tedee bridge."""
127  if self.tedee_webhook_idtedee_webhook_id is not None:
128  try:
129  await self.tedee_clienttedee_client.delete_webhook(self.tedee_webhook_idtedee_webhook_id)
130  except TedeeWebhookException:
131  _LOGGER.exception("Failed to unregister Tedee webhook from bridge")
132  else:
133  _LOGGER.debug("Unregistered Tedee webhook")
134 
135  def _async_add_remove_locks(self) -> None:
136  """Add new locks, remove non-existing locks."""
137  if not self._locks_last_update_locks_last_update:
138  self._locks_last_update_locks_last_update = set(self.tedee_clienttedee_client.locks_dict)
139 
140  if (
141  current_locks := set(self.tedee_clienttedee_client.locks_dict)
142  ) == self._locks_last_update_locks_last_update:
143  return
144 
145  # remove old locks
146  if removed_locks := self._locks_last_update_locks_last_update - current_locks:
147  _LOGGER.debug("Removed locks: %s", ", ".join(map(str, removed_locks)))
148  device_registry = dr.async_get(self.hasshass)
149  for lock_id in removed_locks:
150  if device := device_registry.async_get_device(
151  identifiers={(DOMAIN, str(lock_id))}
152  ):
153  device_registry.async_update_device(
154  device_id=device.id,
155  remove_config_entry_id=self.config_entryconfig_entry.entry_id,
156  )
157 
158  # add new locks
159  if new_locks := current_locks - self._locks_last_update_locks_last_update:
160  _LOGGER.debug("New locks found: %s", ", ".join(map(str, new_locks)))
161  for lock_id in new_locks:
162  for callback in self.new_lock_callbacks:
163  callback(lock_id)
164 
165  self._locks_last_update_locks_last_update = current_locks
None _async_update(self, Callable[[], Awaitable[None]] update_fn)
Definition: coordinator.py:96
None __init__(self, HomeAssistant hass, TedeeConfigEntry entry)
Definition: coordinator.py:45
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)