Home Assistant Unofficial Reference 2024.12.1
manager.py
Go to the documentation of this file.
1 """The bluetooth integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Iterable
6 from functools import partial
7 import itertools
8 import logging
9 
10 from bleak_retry_connector import BleakSlotManager
11 from bluetooth_adapters import BluetoothAdapters
12 from habluetooth import BaseHaRemoteScanner, BaseHaScanner, BluetoothManager
13 
14 from homeassistant import config_entries
15 from homeassistant.const import EVENT_HOMEASSISTANT_STOP, EVENT_LOGGING_CHANGED
16 from homeassistant.core import (
17  CALLBACK_TYPE,
18  Event,
19  HomeAssistant,
20  callback as hass_callback,
21 )
22 from homeassistant.helpers import discovery_flow
23 from homeassistant.helpers.dispatcher import async_dispatcher_connect
24 
25 from .const import DOMAIN
26 from .match import (
27  ADDRESS,
28  CALLBACK,
29  CONNECTABLE,
30  BluetoothCallbackMatcher,
31  BluetoothCallbackMatcherIndex,
32  BluetoothCallbackMatcherWithCallback,
33  IntegrationMatcher,
34  ble_device_matches,
35 )
36 from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak
37 from .storage import BluetoothStorage
38 from .util import async_load_history_from_system
39 
40 _LOGGER = logging.getLogger(__name__)
41 
42 
43 class HomeAssistantBluetoothManager(BluetoothManager):
44  """Manage Bluetooth for Home Assistant."""
45 
46  __slots__ = (
47  "hass",
48  "storage",
49  "_integration_matcher",
50  "_callback_index",
51  "_cancel_logging_listener",
52  )
53 
54  def __init__(
55  self,
56  hass: HomeAssistant,
57  integration_matcher: IntegrationMatcher,
58  bluetooth_adapters: BluetoothAdapters,
59  storage: BluetoothStorage,
60  slot_manager: BleakSlotManager,
61  ) -> None:
62  """Init bluetooth manager."""
63  self.hasshass = hass
64  self.storagestorage = storage
65  self._integration_matcher_integration_matcher = integration_matcher
67  self._cancel_logging_listener_cancel_logging_listener: CALLBACK_TYPE | None = None
68  super().__init__(bluetooth_adapters, slot_manager)
69  self._async_logging_changed_async_logging_changed()
70 
71  @hass_callback
72  def _async_logging_changed(self, event: Event | None = None) -> None:
73  """Handle logging change."""
74  self._debug_debug = _LOGGER.isEnabledFor(logging.DEBUG)
75 
77  self, service_info: BluetoothServiceInfoBleak
78  ) -> None:
79  """Trigger discovery for matching domains."""
80  discovery_key = discovery_flow.DiscoveryKey(
81  domain=DOMAIN,
82  key=service_info.address,
83  version=1,
84  )
85  for domain in self._integration_matcher_integration_matcher.match_domains(service_info):
86  discovery_flow.async_create_flow(
87  self.hasshass,
88  domain,
89  {"source": config_entries.SOURCE_BLUETOOTH},
90  service_info,
91  discovery_key=discovery_key,
92  )
93 
94  @hass_callback
95  def async_rediscover_address(self, address: str) -> None:
96  """Trigger discovery of devices which have already been seen."""
97  self._integration_matcher_integration_matcher.async_clear_address(address)
98  if service_info := self._connectable_history_connectable_history.get(address):
99  self._async_trigger_matching_discovery_async_trigger_matching_discovery(service_info)
100  return
101  if service_info := self._all_history.get(address):
102  self._async_trigger_matching_discovery_async_trigger_matching_discovery(service_info)
103 
104  def _discover_service_info(self, service_info: BluetoothServiceInfoBleak) -> None:
105  matched_domains = self._integration_matcher_integration_matcher.match_domains(service_info)
106  if self._debug_debug:
107  _LOGGER.debug(
108  "%s: %s match: %s",
109  self._async_describe_source(service_info),
110  service_info,
111  matched_domains,
112  )
113 
114  for match in self._callback_index_callback_index.match_callbacks(service_info):
115  callback = match[CALLBACK]
116  try:
117  callback(service_info, BluetoothChange.ADVERTISEMENT)
118  except Exception:
119  _LOGGER.exception("Error in bluetooth callback")
120 
121  if not matched_domains:
122  return # avoid creating DiscoveryKey if there are no matches
123 
124  discovery_key = discovery_flow.DiscoveryKey(
125  domain=DOMAIN,
126  key=service_info.address,
127  version=1,
128  )
129  for domain in matched_domains:
130  discovery_flow.async_create_flow(
131  self.hasshass,
132  domain,
133  {"source": config_entries.SOURCE_BLUETOOTH},
134  service_info,
135  discovery_key=discovery_key,
136  )
137 
138  def _address_disappeared(self, address: str) -> None:
139  """Dismiss all discoveries for the given address."""
140  self._integration_matcher_integration_matcher.async_clear_address(address)
141  for flow in self.hasshass.config_entries.flow.async_progress_by_init_data_type(
142  BluetoothServiceInfoBleak,
143  lambda service_info: bool(service_info.address == address),
144  ):
145  self.hasshass.config_entries.flow.async_abort(flow["flow_id"])
146 
147  async def async_setup(self) -> None:
148  """Set up the bluetooth manager."""
149  await super().async_setup()
150  self._all_history, self._connectable_history_connectable_history = async_load_history_from_system(
151  self._bluetooth_adapters, self.storagestorage
152  )
153  self._cancel_logging_listener_cancel_logging_listener = self.hasshass.bus.async_listen(
154  EVENT_LOGGING_CHANGED, self._async_logging_changed_async_logging_changed
155  )
156  self.hasshass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stopasync_stop)
157  seen: set[str] = set()
158  for address, service_info in itertools.chain(
159  self._connectable_history_connectable_history.items(), self._all_history.items()
160  ):
161  if address in seen:
162  continue
163  seen.add(address)
164  self._async_trigger_matching_discovery_async_trigger_matching_discovery(service_info)
166  self.hasshass,
167  config_entries.signal_discovered_config_entry_removed(DOMAIN),
168  self._handle_config_entry_removed_handle_config_entry_removed,
169  )
170 
172  self,
173  callback: BluetoothCallback,
174  matcher: BluetoothCallbackMatcher | None,
175  ) -> Callable[[], None]:
176  """Register a callback."""
177  callback_matcher = BluetoothCallbackMatcherWithCallback(callback=callback)
178  if not matcher:
179  callback_matcher[CONNECTABLE] = True
180  else:
181  # We could write out every item in the typed dict here
182  # but that would be a bit inefficient and verbose.
183  callback_matcher.update(matcher)
184  callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True)
185 
186  connectable = callback_matcher[CONNECTABLE]
187  self._callback_index_callback_index.add_callback_matcher(callback_matcher)
188 
189  def _async_remove_callback() -> None:
190  self._callback_index_callback_index.remove_callback_matcher(callback_matcher)
191 
192  # If we have history for the subscriber, we can trigger the callback
193  # immediately with the last packet so the subscriber can see the
194  # device.
195  history = self._connectable_history_connectable_history if connectable else self._all_history
196  service_infos: Iterable[BluetoothServiceInfoBleak] = []
197  if address := callback_matcher.get(ADDRESS):
198  if service_info := history.get(address):
199  service_infos = [service_info]
200  else:
201  service_infos = history.values()
202 
203  for service_info in service_infos:
204  if ble_device_matches(callback_matcher, service_info):
205  try:
206  callback(service_info, BluetoothChange.ADVERTISEMENT)
207  except Exception:
208  _LOGGER.exception("Error in bluetooth callback")
209 
210  return _async_remove_callback
211 
212  @hass_callback
213  def async_stop(self, event: Event | None = None) -> None:
214  """Stop the Bluetooth integration at shutdown."""
215  _LOGGER.debug("Stopping bluetooth manager")
216  self._async_save_scanner_histories_async_save_scanner_histories()
217  super().async_stop()
218  if self._cancel_logging_listener_cancel_logging_listener:
219  self._cancel_logging_listener_cancel_logging_listener()
220  self._cancel_logging_listener_cancel_logging_listener = None
221 
222  def _async_save_scanner_histories(self) -> None:
223  """Save the scanner histories."""
224  for scanner in itertools.chain(
225  self._connectable_scanners, self._non_connectable_scanners
226  ):
227  self._async_save_scanner_history_async_save_scanner_history(scanner)
228 
229  def _async_save_scanner_history(self, scanner: BaseHaScanner) -> None:
230  """Save the scanner history."""
231  if isinstance(scanner, BaseHaRemoteScanner):
232  self.storagestorage.async_set_advertisement_history(
233  scanner.source, scanner.serialize_discovered_devices()
234  )
235 
237  self, scanner: BaseHaScanner, unregister: CALLBACK_TYPE
238  ) -> None:
239  """Unregister a scanner."""
240  unregister()
241  self._async_save_scanner_history_async_save_scanner_history(scanner)
242 
244  self,
245  scanner: BaseHaScanner,
246  connection_slots: int | None = None,
247  ) -> CALLBACK_TYPE:
248  """Register a scanner."""
249  if isinstance(scanner, BaseHaRemoteScanner):
250  if history := self.storagestorage.async_get_advertisement_history(scanner.source):
251  scanner.restore_discovered_devices(history)
252 
253  unregister = super().async_register_scanner(scanner, connection_slots)
254  return partial(self._async_unregister_scanner_async_unregister_scanner, scanner, unregister)
255 
256  @hass_callback
258  self,
259  entry: config_entries.ConfigEntry,
260  ) -> None:
261  """Handle config entry changes."""
262  for discovery_key in entry.discovery_keys[DOMAIN]:
263  if discovery_key.version != 1 or not isinstance(discovery_key.key, str):
264  continue
265  address = discovery_key.key
266  _LOGGER.debug("Rediscover address %s", address)
267  self.async_rediscover_addressasync_rediscover_address(address)
Callable[[], None] async_register_callback(self, BluetoothCallback callback, BluetoothCallbackMatcher|None matcher)
Definition: manager.py:175
None _async_unregister_scanner(self, BaseHaScanner scanner, CALLBACK_TYPE unregister)
Definition: manager.py:238
None _handle_config_entry_removed(self, config_entries.ConfigEntry entry)
Definition: manager.py:260
None __init__(self, HomeAssistant hass, IntegrationMatcher integration_matcher, BluetoothAdapters bluetooth_adapters, BluetoothStorage storage, BleakSlotManager slot_manager)
Definition: manager.py:61
None _async_trigger_matching_discovery(self, BluetoothServiceInfoBleak service_info)
Definition: manager.py:78
CALLBACK_TYPE async_register_scanner(self, BaseHaScanner scanner, int|None connection_slots=None)
Definition: manager.py:247
None _discover_service_info(self, BluetoothServiceInfoBleak service_info)
Definition: manager.py:104
bool ble_device_matches(BluetoothMatcherOptional matcher, BluetoothServiceInfoBleak service_info)
Definition: match.py:396
tuple[dict[str, BluetoothServiceInfoBleak], dict[str, BluetoothServiceInfoBleak]] async_load_history_from_system(BluetoothAdapters adapters, BluetoothStorage storage)
Definition: util.py:24
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103