Home Assistant Unofficial Reference 2024.12.1
active_update_coordinator.py
Go to the documentation of this file.
1 """A Bluetooth passive coordinator.
2 
3 Receives data from advertisements but can also poll.
4 """
5 
6 from __future__ import annotations
7 
8 from collections.abc import Callable, Coroutine
9 import logging
10 from typing import Any
11 
12 from bleak import BleakError
13 from bluetooth_data_tools import monotonic_time_coarse
14 
15 from homeassistant.core import HomeAssistant, callback
16 from homeassistant.helpers.debounce import Debouncer
17 
18 from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak
19 from .passive_update_coordinator import PassiveBluetoothDataUpdateCoordinator
20 
21 POLL_DEFAULT_COOLDOWN = 10
22 POLL_DEFAULT_IMMEDIATE = True
23 
24 
25 class ActiveBluetoothDataUpdateCoordinator[_T](PassiveBluetoothDataUpdateCoordinator):
26  """A coordinator that receives passive data from advertisements but can also poll.
27 
28  Unlike the passive processor coordinator, this coordinator does call a parser
29  method to parse the data from the advertisement.
30 
31  Every time an advertisement is received, needs_poll_method is called to work
32  out if a poll is needed. This should return True if it is and False if it is
33  not needed.
34 
35  def needs_poll_method(
36  svc_info: BluetoothServiceInfoBleak,
37  last_poll: float | None
38  ) -> bool:
39  return True
40 
41  If there has been no poll since HA started, `last_poll` will be None.
42  Otherwise it is the number of seconds since one was last attempted.
43 
44  If a poll is needed, the coordinator will call poll_method. This is a coroutine.
45  It should return the same type of data as your update_method. The expectation is
46  that data from advertisements and from polling are being parsed and fed into
47  a shared object that represents the current state of the device.
48 
49  async def poll_method(svc_info: BluetoothServiceInfoBleak) -> YourDataType:
50  return YourDataType(....)
51 
52  BluetoothServiceInfoBleak.device contains a BLEDevice. You should use this in
53  your poll function, as it is the most efficient way to get a BleakClient.
54 
55  Once the poll is complete, the coordinator will call _async_handle_bluetooth_poll
56  which needs to be implemented in the subclass.
57  """
58 
59  def __init__(
60  self,
61  hass: HomeAssistant,
62  logger: logging.Logger,
63  *,
64  address: str,
65  mode: BluetoothScanningMode,
66  needs_poll_method: Callable[[BluetoothServiceInfoBleak, float | None], bool],
67  poll_method: Callable[
68  [BluetoothServiceInfoBleak],
69  Coroutine[Any, Any, _T],
70  ]
71  | None = None,
72  poll_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None,
73  connectable: bool = True,
74  ) -> None:
75  """Initialize the coordinator."""
76  super().__init__(hass, logger, address, mode, connectable)
77  # It's None before the first successful update.
78  # Set type to just T to remove annoying checks that data is not None
79  # when it was already checked during setup.
80  self.datadata: _T = None # type: ignore[assignment]
81 
82  self._needs_poll_method_needs_poll_method = needs_poll_method
83  self._poll_method_poll_method = poll_method
84  self._last_poll_last_poll: float | None = None
85  self.last_poll_successfullast_poll_successful = True
86 
87  # We keep the last service info in case the poller needs to refer to
88  # e.g. its BLEDevice
89  self._last_service_info_last_service_info: BluetoothServiceInfoBleak | None = None
90 
91  if poll_debouncer is None:
92  poll_debouncer = Debouncer(
93  hass,
94  logger,
95  cooldown=POLL_DEFAULT_COOLDOWN,
96  immediate=POLL_DEFAULT_IMMEDIATE,
97  function=self._async_poll_async_poll,
98  background=True,
99  )
100  else:
101  poll_debouncer.function = self._async_poll_async_poll
102 
103  self._debounced_poll_debounced_poll = poll_debouncer
104 
105  def needs_poll(self, service_info: BluetoothServiceInfoBleak) -> bool:
106  """Return true if time to try and poll."""
107  if self.hasshass.is_stopping:
108  return False
109  poll_age: float | None = None
110  if self._last_poll_last_poll:
111  poll_age = service_info.time - self._last_poll_last_poll
112  return self._needs_poll_method_needs_poll_method(service_info, poll_age)
113 
114  async def _async_poll_data(
115  self, last_service_info: BluetoothServiceInfoBleak
116  ) -> _T:
117  """Fetch the latest data from the source."""
118  if self._poll_method_poll_method is None:
119  raise NotImplementedError("Poll method not implemented")
120  return await self._poll_method_poll_method(last_service_info)
121 
122  async def _async_poll(self) -> None:
123  """Poll the device to retrieve any extra data."""
124  assert self._last_service_info_last_service_info
125 
126  try:
127  self.datadata = await self._async_poll_data_async_poll_data(self._last_service_info_last_service_info)
128  except BleakError as exc:
129  if self.last_poll_successfullast_poll_successful:
130  self.loggerlogger.error(
131  "%s: Bluetooth error whilst polling: %s", self.addressaddress, str(exc)
132  )
133  self.last_poll_successfullast_poll_successful = False
134  return
135  except Exception: # noqa: BLE001
136  if self.last_poll_successfullast_poll_successful:
137  self.loggerlogger.exception("%s: Failure while polling", self.addressaddress)
138  self.last_poll_successfullast_poll_successful = False
139  return
140  finally:
141  self._last_poll_last_poll = monotonic_time_coarse()
142 
143  if not self.last_poll_successfullast_poll_successful:
144  self.loggerlogger.debug("%s: Polling recovered", self.addressaddress)
145  self.last_poll_successfullast_poll_successful = True
146 
147  self._async_handle_bluetooth_poll_async_handle_bluetooth_poll()
148 
149  @callback
150  def _async_handle_bluetooth_poll(self) -> None:
151  """Handle a poll event."""
152  self.async_update_listenersasync_update_listeners()
153 
154  @callback
156  self,
157  service_info: BluetoothServiceInfoBleak,
158  change: BluetoothChange,
159  ) -> None:
160  """Handle a Bluetooth event."""
161  super()._async_handle_bluetooth_event(service_info, change)
162 
163  self._last_service_info_last_service_info = service_info
164 
165  # See if its time to poll
166  # We use bluetooth events to trigger the poll so that we scan as soon as
167  # possible after a device comes online or back in range, if a poll is due
168  if self.needs_pollneeds_poll(service_info):
169  self._debounced_poll_debounced_poll.async_schedule_call()
170 
171  @callback
172  def _async_stop(self) -> None:
173  """Cancel debouncer and stop the callbacks."""
174  self._debounced_poll_debounced_poll.async_cancel()
175  super()._async_stop()
None _async_handle_bluetooth_event(self, BluetoothServiceInfoBleak service_info, BluetoothChange change)
None __init__(self, HomeAssistant hass, logging.Logger logger, *str address, BluetoothScanningMode mode, Callable[[BluetoothServiceInfoBleak, float|None], bool] needs_poll_method, Callable[[BluetoothServiceInfoBleak], Coroutine[Any, Any, _T],]|None poll_method=None, Debouncer[Coroutine[Any, Any, None]]|None poll_debouncer=None, bool connectable=True)