Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The bluetooth integration."""
2 
3 from __future__ import annotations
4 
5 import datetime
6 import logging
7 import platform
8 from typing import TYPE_CHECKING
9 
10 from bleak_retry_connector import BleakSlotManager
11 from bluetooth_adapters import (
12  ADAPTER_ADDRESS,
13  ADAPTER_CONNECTION_SLOTS,
14  ADAPTER_HW_VERSION,
15  ADAPTER_MANUFACTURER,
16  ADAPTER_SW_VERSION,
17  DEFAULT_ADDRESS,
18  DEFAULT_CONNECTION_SLOTS,
19  AdapterDetails,
20  BluetoothAdapters,
21  adapter_human_name,
22  adapter_model,
23  adapter_unique_name,
24  get_adapters,
25 )
26 from bluetooth_data_tools import monotonic_time_coarse as MONOTONIC_TIME
27 from habluetooth import (
28  BaseHaRemoteScanner,
29  BaseHaScanner,
30  BluetoothScannerDevice,
31  BluetoothScanningMode,
32  HaBluetoothConnector,
33  HaScanner,
34  ScannerStartError,
35  set_manager,
36 )
37 from home_assistant_bluetooth import BluetoothServiceInfo, BluetoothServiceInfoBleak
38 
39 from homeassistant.components import usb
40 from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry
41 from homeassistant.const import EVENT_HOMEASSISTANT_STOP
42 from homeassistant.core import Event, HassJob, HomeAssistant, callback as hass_callback
43 from homeassistant.exceptions import ConfigEntryNotReady
44 from homeassistant.helpers import (
45  config_validation as cv,
46  device_registry as dr,
47  discovery_flow,
48 )
49 from homeassistant.helpers.debounce import Debouncer
50 from homeassistant.helpers.event import async_call_later
51 from homeassistant.helpers.issue_registry import async_delete_issue
52 from homeassistant.loader import async_get_bluetooth
53 
54 from . import passive_update_processor
55 from .api import (
56  _get_manager,
57  async_address_present,
58  async_ble_device_from_address,
59  async_discovered_service_info,
60  async_get_advertisement_callback,
61  async_get_fallback_availability_interval,
62  async_get_learned_advertising_interval,
63  async_get_scanner,
64  async_last_service_info,
65  async_process_advertisements,
66  async_rediscover_address,
67  async_register_callback,
68  async_register_scanner,
69  async_scanner_by_source,
70  async_scanner_count,
71  async_scanner_devices_by_address,
72  async_set_fallback_availability_interval,
73  async_track_unavailable,
74 )
75 from .const import (
76  BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
77  CONF_ADAPTER,
78  CONF_DETAILS,
79  CONF_PASSIVE,
80  DOMAIN,
81  FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
82  LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
83  SOURCE_LOCAL,
84 )
85 from .manager import HomeAssistantBluetoothManager
86 from .match import BluetoothCallbackMatcher, IntegrationMatcher
87 from .models import BluetoothCallback, BluetoothChange
88 from .storage import BluetoothStorage
89 from .util import adapter_title
90 
91 if TYPE_CHECKING:
92  from homeassistant.helpers.typing import ConfigType
93 
94 __all__ = [
95  "async_address_present",
96  "async_ble_device_from_address",
97  "async_discovered_service_info",
98  "async_get_fallback_availability_interval",
99  "async_get_learned_advertising_interval",
100  "async_get_scanner",
101  "async_last_service_info",
102  "async_process_advertisements",
103  "async_rediscover_address",
104  "async_register_callback",
105  "async_register_scanner",
106  "async_set_fallback_availability_interval",
107  "async_track_unavailable",
108  "async_scanner_by_source",
109  "async_scanner_count",
110  "async_scanner_devices_by_address",
111  "async_get_advertisement_callback",
112  "BaseHaScanner",
113  "HomeAssistantRemoteScanner",
114  "BluetoothCallbackMatcher",
115  "BluetoothChange",
116  "BluetoothServiceInfo",
117  "BluetoothServiceInfoBleak",
118  "BluetoothScanningMode",
119  "BluetoothCallback",
120  "BluetoothScannerDevice",
121  "HaBluetoothConnector",
122  "BaseHaRemoteScanner",
123  "SOURCE_LOCAL",
124  "FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS",
125  "MONOTONIC_TIME",
126 ]
127 
128 _LOGGER = logging.getLogger(__name__)
129 
130 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
131 
132 
134  hass: HomeAssistant,
135  manager: HomeAssistantBluetoothManager,
136  bluetooth_adapters: BluetoothAdapters,
137 ) -> None:
138  """Start adapter discovery."""
139  adapters = await manager.async_get_bluetooth_adapters()
140  async_migrate_entries(hass, adapters, bluetooth_adapters.default_adapter)
141  await async_discover_adapters(hass, adapters)
142 
143  async def _async_rediscover_adapters() -> None:
144  """Rediscover adapters when a new one may be available."""
145  discovered_adapters = await manager.async_get_bluetooth_adapters(cached=False)
146  _LOGGER.debug("Rediscovered adapters: %s", discovered_adapters)
147  await async_discover_adapters(hass, discovered_adapters)
148 
149  discovery_debouncer = Debouncer(
150  hass,
151  _LOGGER,
152  cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
153  immediate=False,
154  function=_async_rediscover_adapters,
155  background=True,
156  )
157 
158  @hass_callback
159  def _async_shutdown_debouncer(_: Event) -> None:
160  """Shutdown debouncer."""
161  discovery_debouncer.async_shutdown()
162 
163  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown_debouncer)
164 
165  async def _async_call_debouncer(now: datetime.datetime) -> None:
166  """Call the debouncer at a later time."""
167  await discovery_debouncer.async_call()
168 
169  call_debouncer_job = HassJob(_async_call_debouncer, cancel_on_shutdown=True)
170 
171  def _async_trigger_discovery() -> None:
172  # There are so many bluetooth adapter models that
173  # we check the bus whenever a usb device is plugged in
174  # to see if it is a bluetooth adapter since we can't
175  # tell if the device is a bluetooth adapter or if its
176  # actually supported unless we ask DBus if its now
177  # present.
178  _LOGGER.debug("Triggering bluetooth usb discovery")
179  hass.async_create_task(discovery_debouncer.async_call())
180  # Because it can take 120s for the firmware loader
181  # fallback to timeout we need to wait that plus
182  # the debounce time to ensure we do not miss the
183  # adapter becoming available to DBus since otherwise
184  # we will never see the new adapter until
185  # Home Assistant is restarted
187  hass,
188  BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
189  call_debouncer_job,
190  )
191 
192  cancel = usb.async_register_scan_request_callback(hass, _async_trigger_discovery)
193  hass.bus.async_listen_once(
194  EVENT_HOMEASSISTANT_STOP,
195  hass_callback(lambda event: cancel()),
196  )
197 
198 
199 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
200  """Set up the bluetooth integration."""
201  if platform.system() == "Linux":
202  # Remove any config entries that are using the default address
203  # that were created from discovering adapters in a crashed state
204  #
205  # DEFAULT_ADDRESS is perfectly valid on MacOS but on
206  # Linux it means the adapter is not yet configured
207  # or crashed
208  for entry in list(hass.config_entries.async_entries(DOMAIN)):
209  if entry.unique_id == DEFAULT_ADDRESS:
210  await hass.config_entries.async_remove(entry.entry_id)
211 
212  bluetooth_adapters = get_adapters()
213  bluetooth_storage = BluetoothStorage(hass)
214  slot_manager = BleakSlotManager()
215  integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass))
216 
217  slot_manager_setup_task = hass.async_create_task(
218  slot_manager.async_setup(), "slot_manager setup", eager_start=True
219  )
220  processor_setup_task = hass.async_create_task(
221  passive_update_processor.async_setup(hass),
222  "passive_update_processor setup",
223  eager_start=True,
224  )
225  storage_setup_task = hass.async_create_task(
226  bluetooth_storage.async_setup(), "bluetooth storage setup", eager_start=True
227  )
228  integration_matcher.async_setup()
230  hass, integration_matcher, bluetooth_adapters, bluetooth_storage, slot_manager
231  )
232  set_manager(manager)
233  await storage_setup_task
234  await manager.async_setup()
235 
236  hass.async_create_background_task(
237  _async_start_adapter_discovery(hass, manager, bluetooth_adapters),
238  "start_adapter_discovery",
239  )
240  await slot_manager_setup_task
241  async_delete_issue(hass, DOMAIN, "haos_outdated")
242  await processor_setup_task
243  return True
244 
245 
246 @hass_callback
248  hass: HomeAssistant, adapters: dict[str, AdapterDetails], default_adapter: str
249 ) -> None:
250  """Migrate config entries to support multiple."""
251  current_entries = hass.config_entries.async_entries(DOMAIN)
252 
253  for entry in current_entries:
254  if entry.unique_id:
255  continue
256 
257  address = DEFAULT_ADDRESS
258  adapter = entry.options.get(CONF_ADAPTER, default_adapter)
259  if adapter in adapters:
260  address = adapters[adapter][ADAPTER_ADDRESS]
261  hass.config_entries.async_update_entry(
262  entry, title=adapter_unique_name(adapter, address), unique_id=address
263  )
264 
265 
267  hass: HomeAssistant,
268  adapters: dict[str, AdapterDetails],
269 ) -> None:
270  """Discover adapters and start flows."""
271  system = platform.system()
272  if system == "Windows":
273  # We currently do not have a good way to detect if a bluetooth device is
274  # available on Windows. We will just assume that it is not unless they
275  # actively add it.
276  return
277 
278  for adapter, details in adapters.items():
279  if system == "Linux" and details[ADAPTER_ADDRESS] == DEFAULT_ADDRESS:
280  # DEFAULT_ADDRESS is perfectly valid on MacOS but on
281  # Linux it means the adapter is not yet configured
282  # or crashed so we should not try to start a flow for it.
283  continue
284  discovery_flow.async_create_flow(
285  hass,
286  DOMAIN,
287  context={"source": SOURCE_INTEGRATION_DISCOVERY},
288  data={CONF_ADAPTER: adapter, CONF_DETAILS: details},
289  )
290 
291 
293  hass: HomeAssistant, entry: ConfigEntry, adapter: str, details: AdapterDetails
294 ) -> None:
295  """Update device registry entry.
296 
297  The physical adapter can change from hci0/hci1 on reboot
298  or if the user moves around the usb sticks so we need to
299  update the device with the new location so they can
300  figure out where the adapter is.
301  """
302  dr.async_get(hass).async_get_or_create(
303  config_entry_id=entry.entry_id,
304  name=adapter_human_name(adapter, details[ADAPTER_ADDRESS]),
305  connections={(dr.CONNECTION_BLUETOOTH, details[ADAPTER_ADDRESS])},
306  manufacturer=details[ADAPTER_MANUFACTURER],
307  model=adapter_model(details),
308  sw_version=details.get(ADAPTER_SW_VERSION),
309  hw_version=details.get(ADAPTER_HW_VERSION),
310  )
311 
312 
313 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
314  """Set up a config entry for a bluetooth scanner."""
315  manager = _get_manager(hass)
316  address = entry.unique_id
317  assert address is not None
318  adapter = await manager.async_get_adapter_from_address_or_recover(address)
319  if adapter is None:
320  raise ConfigEntryNotReady(
321  f"Bluetooth adapter {adapter} with address {address} not found"
322  )
323  passive = entry.options.get(CONF_PASSIVE)
324  mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE
325  scanner = HaScanner(mode, adapter, address)
326  scanner.async_setup()
327  try:
328  await scanner.async_start()
329  except (RuntimeError, ScannerStartError) as err:
330  raise ConfigEntryNotReady(
331  f"{adapter_human_name(adapter, address)}: {err}"
332  ) from err
333  adapters = await manager.async_get_bluetooth_adapters()
334  details = adapters[adapter]
335  if entry.title == address:
336  hass.config_entries.async_update_entry(
337  entry, title=adapter_title(adapter, details)
338  )
339  slots: int = details.get(ADAPTER_CONNECTION_SLOTS) or DEFAULT_CONNECTION_SLOTS
340  entry.async_on_unload(async_register_scanner(hass, scanner, connection_slots=slots))
341  await async_update_device(hass, entry, adapter, details)
342  entry.async_on_unload(entry.add_update_listener(async_update_listener))
343  entry.async_on_unload(scanner.async_stop)
344  return True
345 
346 
347 async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
348  """Handle options update."""
349  await hass.config_entries.async_reload(entry.entry_id)
350 
351 
352 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
353  """Unload a config entry."""
354  return True
CALLBACK_TYPE async_register_scanner(HomeAssistant hass, BaseHaScanner scanner, int|None connection_slots=None)
Definition: api.py:181
HomeAssistantBluetoothManager _get_manager(HomeAssistant hass)
Definition: api.py:35
str adapter_title(str adapter, AdapterDetails details)
Definition: util.py:82
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:199
None async_update_device(HomeAssistant hass, ConfigEntry entry, str adapter, AdapterDetails details)
Definition: __init__.py:294
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:313
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:352
None async_discover_adapters(HomeAssistant hass, dict[str, AdapterDetails] adapters)
Definition: __init__.py:269
None async_update_listener(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:347
None _async_start_adapter_discovery(HomeAssistant hass, HomeAssistantBluetoothManager manager, BluetoothAdapters bluetooth_adapters)
Definition: __init__.py:137
None async_migrate_entries(HomeAssistant hass, dict[str, AdapterDetails] adapters, str default_adapter)
Definition: __init__.py:249
None async_delete_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:85
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597
list[BluetoothMatcher] async_get_bluetooth(HomeAssistant hass)
Definition: loader.py:515