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