mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
Split bluetooth manager so it can be extracted into the habluetooth lib (#105015)
This commit is contained in:
parent
94d168e20e
commit
3c635fdbf2
@ -81,7 +81,7 @@ from .const import (
|
|||||||
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
|
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
|
||||||
SOURCE_LOCAL,
|
SOURCE_LOCAL,
|
||||||
)
|
)
|
||||||
from .manager import MONOTONIC_TIME, BluetoothManager
|
from .manager import MONOTONIC_TIME, HomeAssistantBluetoothManager
|
||||||
from .match import BluetoothCallbackMatcher, IntegrationMatcher
|
from .match import BluetoothCallbackMatcher, IntegrationMatcher
|
||||||
from .models import BluetoothCallback, BluetoothChange
|
from .models import BluetoothCallback, BluetoothChange
|
||||||
from .storage import BluetoothStorage
|
from .storage import BluetoothStorage
|
||||||
@ -143,11 +143,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
await bluetooth_storage.async_setup()
|
await bluetooth_storage.async_setup()
|
||||||
slot_manager = BleakSlotManager()
|
slot_manager = BleakSlotManager()
|
||||||
await slot_manager.async_setup()
|
await slot_manager.async_setup()
|
||||||
manager = BluetoothManager(
|
manager = HomeAssistantBluetoothManager(
|
||||||
hass, integration_matcher, bluetooth_adapters, bluetooth_storage, slot_manager
|
hass, integration_matcher, bluetooth_adapters, bluetooth_storage, slot_manager
|
||||||
)
|
)
|
||||||
await manager.async_setup()
|
await manager.async_setup()
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop)
|
hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_STOP, lambda event: manager.async_stop()
|
||||||
|
)
|
||||||
hass.data[DATA_MANAGER] = models.MANAGER = manager
|
hass.data[DATA_MANAGER] = models.MANAGER = manager
|
||||||
adapters = await manager.async_get_bluetooth_adapters()
|
adapters = await manager.async_get_bluetooth_adapters()
|
||||||
|
|
||||||
@ -284,7 +286,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
passive = entry.options.get(CONF_PASSIVE)
|
passive = entry.options.get(CONF_PASSIVE)
|
||||||
mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE
|
mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE
|
||||||
new_info_callback = async_get_advertisement_callback(hass)
|
new_info_callback = async_get_advertisement_callback(hass)
|
||||||
manager: BluetoothManager = hass.data[DATA_MANAGER]
|
manager: HomeAssistantBluetoothManager = hass.data[DATA_MANAGER]
|
||||||
scanner = HaScanner(mode, adapter, address, new_info_callback)
|
scanner = HaScanner(mode, adapter, address, new_info_callback)
|
||||||
try:
|
try:
|
||||||
scanner.async_setup()
|
scanner.async_setup()
|
||||||
|
@ -16,7 +16,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_ca
|
|||||||
|
|
||||||
from .base_scanner import BaseHaScanner, BluetoothScannerDevice
|
from .base_scanner import BaseHaScanner, BluetoothScannerDevice
|
||||||
from .const import DATA_MANAGER
|
from .const import DATA_MANAGER
|
||||||
from .manager import BluetoothManager
|
from .manager import HomeAssistantBluetoothManager
|
||||||
from .match import BluetoothCallbackMatcher
|
from .match import BluetoothCallbackMatcher
|
||||||
from .models import BluetoothCallback, BluetoothChange, ProcessAdvertisementCallback
|
from .models import BluetoothCallback, BluetoothChange, ProcessAdvertisementCallback
|
||||||
from .wrappers import HaBleakScannerWrapper
|
from .wrappers import HaBleakScannerWrapper
|
||||||
@ -25,9 +25,9 @@ if TYPE_CHECKING:
|
|||||||
from bleak.backends.device import BLEDevice
|
from bleak.backends.device import BLEDevice
|
||||||
|
|
||||||
|
|
||||||
def _get_manager(hass: HomeAssistant) -> BluetoothManager:
|
def _get_manager(hass: HomeAssistant) -> HomeAssistantBluetoothManager:
|
||||||
"""Get the bluetooth manager."""
|
"""Get the bluetooth manager."""
|
||||||
return cast(BluetoothManager, hass.data[DATA_MANAGER])
|
return cast(HomeAssistantBluetoothManager, hass.data[DATA_MANAGER])
|
||||||
|
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Final
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
@ -28,7 +27,6 @@ from homeassistant.core import (
|
|||||||
callback as hass_callback,
|
callback as hass_callback,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import discovery_flow
|
from homeassistant.helpers import discovery_flow
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
|
||||||
|
|
||||||
from .base_scanner import BaseHaScanner, BluetoothScannerDevice
|
from .base_scanner import BaseHaScanner, BluetoothScannerDevice
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -100,16 +98,12 @@ class BluetoothManager:
|
|||||||
"""Manage Bluetooth."""
|
"""Manage Bluetooth."""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"hass",
|
|
||||||
"_integration_matcher",
|
|
||||||
"_cancel_unavailable_tracking",
|
"_cancel_unavailable_tracking",
|
||||||
"_cancel_logging_listener",
|
|
||||||
"_advertisement_tracker",
|
"_advertisement_tracker",
|
||||||
"_fallback_intervals",
|
"_fallback_intervals",
|
||||||
"_intervals",
|
"_intervals",
|
||||||
"_unavailable_callbacks",
|
"_unavailable_callbacks",
|
||||||
"_connectable_unavailable_callbacks",
|
"_connectable_unavailable_callbacks",
|
||||||
"_callback_index",
|
|
||||||
"_bleak_callbacks",
|
"_bleak_callbacks",
|
||||||
"_all_history",
|
"_all_history",
|
||||||
"_connectable_history",
|
"_connectable_history",
|
||||||
@ -122,21 +116,17 @@ class BluetoothManager:
|
|||||||
"slot_manager",
|
"slot_manager",
|
||||||
"_debug",
|
"_debug",
|
||||||
"shutdown",
|
"shutdown",
|
||||||
|
"_loop",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
|
||||||
integration_matcher: IntegrationMatcher,
|
|
||||||
bluetooth_adapters: BluetoothAdapters,
|
bluetooth_adapters: BluetoothAdapters,
|
||||||
storage: BluetoothStorage,
|
storage: BluetoothStorage,
|
||||||
slot_manager: BleakSlotManager,
|
slot_manager: BleakSlotManager,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init bluetooth manager."""
|
"""Init bluetooth manager."""
|
||||||
self.hass = hass
|
self._cancel_unavailable_tracking: asyncio.TimerHandle | None = None
|
||||||
self._integration_matcher = integration_matcher
|
|
||||||
self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None
|
|
||||||
self._cancel_logging_listener: CALLBACK_TYPE | None = None
|
|
||||||
|
|
||||||
self._advertisement_tracker = AdvertisementTracker()
|
self._advertisement_tracker = AdvertisementTracker()
|
||||||
self._fallback_intervals = self._advertisement_tracker.fallback_intervals
|
self._fallback_intervals = self._advertisement_tracker.fallback_intervals
|
||||||
@ -149,7 +139,6 @@ class BluetoothManager:
|
|||||||
str, list[Callable[[BluetoothServiceInfoBleak], None]]
|
str, list[Callable[[BluetoothServiceInfoBleak], None]]
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
self._callback_index = BluetoothCallbackMatcherIndex()
|
|
||||||
self._bleak_callbacks: list[
|
self._bleak_callbacks: list[
|
||||||
tuple[AdvertisementDataCallback, dict[str, set[str]]]
|
tuple[AdvertisementDataCallback, dict[str, set[str]]]
|
||||||
] = []
|
] = []
|
||||||
@ -164,6 +153,7 @@ class BluetoothManager:
|
|||||||
self.slot_manager = slot_manager
|
self.slot_manager = slot_manager
|
||||||
self._debug = _LOGGER.isEnabledFor(logging.DEBUG)
|
self._debug = _LOGGER.isEnabledFor(logging.DEBUG)
|
||||||
self.shutdown = False
|
self.shutdown = False
|
||||||
|
self._loop: asyncio.AbstractEventLoop | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supports_passive_scan(self) -> bool:
|
def supports_passive_scan(self) -> bool:
|
||||||
@ -206,7 +196,6 @@ class BluetoothManager:
|
|||||||
return adapter
|
return adapter
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_scanner_by_source(self, source: str) -> BaseHaScanner | None:
|
def async_scanner_by_source(self, source: str) -> BaseHaScanner | None:
|
||||||
"""Return the scanner for a source."""
|
"""Return the scanner for a source."""
|
||||||
return self._sources.get(source)
|
return self._sources.get(source)
|
||||||
@ -229,45 +218,22 @@ class BluetoothManager:
|
|||||||
self._adapters = self._bluetooth_adapters.adapters
|
self._adapters = self._bluetooth_adapters.adapters
|
||||||
return self._find_adapter_by_address(address)
|
return self._find_adapter_by_address(address)
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def _async_logging_changed(self, event: Event) -> None:
|
|
||||||
"""Handle logging change."""
|
|
||||||
self._debug = _LOGGER.isEnabledFor(logging.DEBUG)
|
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
async def async_setup(self) -> None:
|
||||||
"""Set up the bluetooth manager."""
|
"""Set up the bluetooth manager."""
|
||||||
|
self._loop = asyncio.get_running_loop()
|
||||||
await self._bluetooth_adapters.refresh()
|
await self._bluetooth_adapters.refresh()
|
||||||
install_multiple_bleak_catcher()
|
install_multiple_bleak_catcher()
|
||||||
self._all_history, self._connectable_history = async_load_history_from_system(
|
|
||||||
self._bluetooth_adapters, self.storage
|
|
||||||
)
|
|
||||||
self._cancel_logging_listener = self.hass.bus.async_listen(
|
|
||||||
EVENT_LOGGING_CHANGED, self._async_logging_changed
|
|
||||||
)
|
|
||||||
self.async_setup_unavailable_tracking()
|
self.async_setup_unavailable_tracking()
|
||||||
seen: set[str] = set()
|
|
||||||
for address, service_info in itertools.chain(
|
|
||||||
self._connectable_history.items(), self._all_history.items()
|
|
||||||
):
|
|
||||||
if address in seen:
|
|
||||||
continue
|
|
||||||
seen.add(address)
|
|
||||||
self._async_trigger_matching_discovery(service_info)
|
|
||||||
|
|
||||||
@hass_callback
|
def async_stop(self) -> None:
|
||||||
def async_stop(self, event: Event) -> None:
|
|
||||||
"""Stop the Bluetooth integration at shutdown."""
|
"""Stop the Bluetooth integration at shutdown."""
|
||||||
_LOGGER.debug("Stopping bluetooth manager")
|
_LOGGER.debug("Stopping bluetooth manager")
|
||||||
self.shutdown = True
|
self.shutdown = True
|
||||||
if self._cancel_unavailable_tracking:
|
if self._cancel_unavailable_tracking:
|
||||||
self._cancel_unavailable_tracking()
|
self._cancel_unavailable_tracking.cancel()
|
||||||
self._cancel_unavailable_tracking = None
|
self._cancel_unavailable_tracking = None
|
||||||
if self._cancel_logging_listener:
|
|
||||||
self._cancel_logging_listener()
|
|
||||||
self._cancel_logging_listener = None
|
|
||||||
uninstall_multiple_bleak_catcher()
|
uninstall_multiple_bleak_catcher()
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_scanner_devices_by_address(
|
def async_scanner_devices_by_address(
|
||||||
self, address: str, connectable: bool
|
self, address: str, connectable: bool
|
||||||
) -> list[BluetoothScannerDevice]:
|
) -> list[BluetoothScannerDevice]:
|
||||||
@ -288,7 +254,6 @@ class BluetoothManager:
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def _async_all_discovered_addresses(self, connectable: bool) -> Iterable[str]:
|
def _async_all_discovered_addresses(self, connectable: bool) -> Iterable[str]:
|
||||||
"""Return all of discovered addresses.
|
"""Return all of discovered addresses.
|
||||||
|
|
||||||
@ -304,24 +269,25 @@ class BluetoothManager:
|
|||||||
for scanner in self._non_connectable_scanners
|
for scanner in self._non_connectable_scanners
|
||||||
)
|
)
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_discovered_devices(self, connectable: bool) -> list[BLEDevice]:
|
def async_discovered_devices(self, connectable: bool) -> list[BLEDevice]:
|
||||||
"""Return all of combined best path to discovered from all the scanners."""
|
"""Return all of combined best path to discovered from all the scanners."""
|
||||||
histories = self._connectable_history if connectable else self._all_history
|
histories = self._connectable_history if connectable else self._all_history
|
||||||
return [history.device for history in histories.values()]
|
return [history.device for history in histories.values()]
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_setup_unavailable_tracking(self) -> None:
|
def async_setup_unavailable_tracking(self) -> None:
|
||||||
"""Set up the unavailable tracking."""
|
"""Set up the unavailable tracking."""
|
||||||
self._cancel_unavailable_tracking = async_track_time_interval(
|
self._schedule_unavailable_tracking()
|
||||||
self.hass,
|
|
||||||
self._async_check_unavailable,
|
def _schedule_unavailable_tracking(self) -> None:
|
||||||
timedelta(seconds=UNAVAILABLE_TRACK_SECONDS),
|
"""Schedule the unavailable tracking."""
|
||||||
name="Bluetooth manager unavailable tracking",
|
if TYPE_CHECKING:
|
||||||
|
assert self._loop is not None
|
||||||
|
loop = self._loop
|
||||||
|
self._cancel_unavailable_tracking = loop.call_at(
|
||||||
|
loop.time() + UNAVAILABLE_TRACK_SECONDS, self._async_check_unavailable
|
||||||
)
|
)
|
||||||
|
|
||||||
@hass_callback
|
def _async_check_unavailable(self) -> None:
|
||||||
def _async_check_unavailable(self, now: datetime) -> None:
|
|
||||||
"""Watch for unavailable devices and cleanup state history."""
|
"""Watch for unavailable devices and cleanup state history."""
|
||||||
monotonic_now = MONOTONIC_TIME()
|
monotonic_now = MONOTONIC_TIME()
|
||||||
connectable_history = self._connectable_history
|
connectable_history = self._connectable_history
|
||||||
@ -363,8 +329,7 @@ class BluetoothManager:
|
|||||||
# available for both connectable and non-connectable
|
# available for both connectable and non-connectable
|
||||||
tracker.async_remove_fallback_interval(address)
|
tracker.async_remove_fallback_interval(address)
|
||||||
tracker.async_remove_address(address)
|
tracker.async_remove_address(address)
|
||||||
self._integration_matcher.async_clear_address(address)
|
self._address_disappeared(address)
|
||||||
self._async_dismiss_discoveries(address)
|
|
||||||
|
|
||||||
service_info = history.pop(address)
|
service_info = history.pop(address)
|
||||||
|
|
||||||
@ -377,13 +342,13 @@ class BluetoothManager:
|
|||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("Error in unavailable callback")
|
_LOGGER.exception("Error in unavailable callback")
|
||||||
|
|
||||||
def _async_dismiss_discoveries(self, address: str) -> None:
|
self._schedule_unavailable_tracking()
|
||||||
"""Dismiss all discoveries for the given address."""
|
|
||||||
for flow in self.hass.config_entries.flow.async_progress_by_init_data_type(
|
def _address_disappeared(self, address: str) -> None:
|
||||||
BluetoothServiceInfoBleak,
|
"""Call when an address disappears from the stack.
|
||||||
lambda service_info: bool(service_info.address == address),
|
|
||||||
):
|
This method is intended to be overridden by subclasses.
|
||||||
self.hass.config_entries.flow.async_abort(flow["flow_id"])
|
"""
|
||||||
|
|
||||||
def _prefer_previous_adv_from_different_source(
|
def _prefer_previous_adv_from_different_source(
|
||||||
self,
|
self,
|
||||||
@ -436,7 +401,6 @@ class BluetoothManager:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def scanner_adv_received(self, service_info: BluetoothServiceInfoBleak) -> None:
|
def scanner_adv_received(self, service_info: BluetoothServiceInfoBleak) -> None:
|
||||||
"""Handle a new advertisement from any scanner.
|
"""Handle a new advertisement from any scanner.
|
||||||
|
|
||||||
@ -567,16 +531,6 @@ class BluetoothManager:
|
|||||||
time=service_info.time,
|
time=service_info.time,
|
||||||
)
|
)
|
||||||
|
|
||||||
matched_domains = self._integration_matcher.match_domains(service_info)
|
|
||||||
if self._debug:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"%s: %s %s match: %s",
|
|
||||||
self._async_describe_source(service_info),
|
|
||||||
address,
|
|
||||||
service_info.advertisement,
|
|
||||||
matched_domains,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (connectable or old_connectable_service_info) and (
|
if (connectable or old_connectable_service_info) and (
|
||||||
bleak_callbacks := self._bleak_callbacks
|
bleak_callbacks := self._bleak_callbacks
|
||||||
):
|
):
|
||||||
@ -586,22 +540,14 @@ class BluetoothManager:
|
|||||||
for callback_filters in bleak_callbacks:
|
for callback_filters in bleak_callbacks:
|
||||||
_dispatch_bleak_callback(*callback_filters, device, advertisement_data)
|
_dispatch_bleak_callback(*callback_filters, device, advertisement_data)
|
||||||
|
|
||||||
for match in self._callback_index.match_callbacks(service_info):
|
self._discover_service_info(service_info)
|
||||||
callback = match[CALLBACK]
|
|
||||||
try:
|
|
||||||
callback(service_info, BluetoothChange.ADVERTISEMENT)
|
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
_LOGGER.exception("Error in bluetooth callback")
|
|
||||||
|
|
||||||
for domain in matched_domains:
|
def _discover_service_info(self, service_info: BluetoothServiceInfoBleak) -> None:
|
||||||
discovery_flow.async_create_flow(
|
"""Discover a new service info.
|
||||||
self.hass,
|
|
||||||
domain,
|
This method is intended to be overridden by subclasses.
|
||||||
{"source": config_entries.SOURCE_BLUETOOTH},
|
"""
|
||||||
service_info,
|
|
||||||
)
|
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def _async_describe_source(self, service_info: BluetoothServiceInfoBleak) -> str:
|
def _async_describe_source(self, service_info: BluetoothServiceInfoBleak) -> str:
|
||||||
"""Describe a source."""
|
"""Describe a source."""
|
||||||
if scanner := self._sources.get(service_info.source):
|
if scanner := self._sources.get(service_info.source):
|
||||||
@ -612,7 +558,6 @@ class BluetoothManager:
|
|||||||
description += " [connectable]"
|
description += " [connectable]"
|
||||||
return description
|
return description
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_track_unavailable(
|
def async_track_unavailable(
|
||||||
self,
|
self,
|
||||||
callback: Callable[[BluetoothServiceInfoBleak], None],
|
callback: Callable[[BluetoothServiceInfoBleak], None],
|
||||||
@ -626,7 +571,6 @@ class BluetoothManager:
|
|||||||
unavailable_callbacks = self._unavailable_callbacks
|
unavailable_callbacks = self._unavailable_callbacks
|
||||||
unavailable_callbacks.setdefault(address, []).append(callback)
|
unavailable_callbacks.setdefault(address, []).append(callback)
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def _async_remove_callback() -> None:
|
def _async_remove_callback() -> None:
|
||||||
unavailable_callbacks[address].remove(callback)
|
unavailable_callbacks[address].remove(callback)
|
||||||
if not unavailable_callbacks[address]:
|
if not unavailable_callbacks[address]:
|
||||||
@ -634,50 +578,6 @@ class BluetoothManager:
|
|||||||
|
|
||||||
return _async_remove_callback
|
return _async_remove_callback
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_register_callback(
|
|
||||||
self,
|
|
||||||
callback: BluetoothCallback,
|
|
||||||
matcher: BluetoothCallbackMatcher | None,
|
|
||||||
) -> Callable[[], None]:
|
|
||||||
"""Register a callback."""
|
|
||||||
callback_matcher = BluetoothCallbackMatcherWithCallback(callback=callback)
|
|
||||||
if not matcher:
|
|
||||||
callback_matcher[CONNECTABLE] = True
|
|
||||||
else:
|
|
||||||
# We could write out every item in the typed dict here
|
|
||||||
# but that would be a bit inefficient and verbose.
|
|
||||||
callback_matcher.update(matcher)
|
|
||||||
callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True)
|
|
||||||
|
|
||||||
connectable = callback_matcher[CONNECTABLE]
|
|
||||||
self._callback_index.add_callback_matcher(callback_matcher)
|
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def _async_remove_callback() -> None:
|
|
||||||
self._callback_index.remove_callback_matcher(callback_matcher)
|
|
||||||
|
|
||||||
# If we have history for the subscriber, we can trigger the callback
|
|
||||||
# immediately with the last packet so the subscriber can see the
|
|
||||||
# device.
|
|
||||||
history = self._connectable_history if connectable else self._all_history
|
|
||||||
service_infos: Iterable[BluetoothServiceInfoBleak] = []
|
|
||||||
if address := callback_matcher.get(ADDRESS):
|
|
||||||
if service_info := history.get(address):
|
|
||||||
service_infos = [service_info]
|
|
||||||
else:
|
|
||||||
service_infos = history.values()
|
|
||||||
|
|
||||||
for service_info in service_infos:
|
|
||||||
if ble_device_matches(callback_matcher, service_info):
|
|
||||||
try:
|
|
||||||
callback(service_info, BluetoothChange.ADVERTISEMENT)
|
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
_LOGGER.exception("Error in bluetooth callback")
|
|
||||||
|
|
||||||
return _async_remove_callback
|
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_ble_device_from_address(
|
def async_ble_device_from_address(
|
||||||
self, address: str, connectable: bool
|
self, address: str, connectable: bool
|
||||||
) -> BLEDevice | None:
|
) -> BLEDevice | None:
|
||||||
@ -687,13 +587,11 @@ class BluetoothManager:
|
|||||||
return history.device
|
return history.device
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_address_present(self, address: str, connectable: bool) -> bool:
|
def async_address_present(self, address: str, connectable: bool) -> bool:
|
||||||
"""Return if the address is present."""
|
"""Return if the address is present."""
|
||||||
histories = self._connectable_history if connectable else self._all_history
|
histories = self._connectable_history if connectable else self._all_history
|
||||||
return address in histories
|
return address in histories
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_discovered_service_info(
|
def async_discovered_service_info(
|
||||||
self, connectable: bool
|
self, connectable: bool
|
||||||
) -> Iterable[BluetoothServiceInfoBleak]:
|
) -> Iterable[BluetoothServiceInfoBleak]:
|
||||||
@ -701,7 +599,6 @@ class BluetoothManager:
|
|||||||
histories = self._connectable_history if connectable else self._all_history
|
histories = self._connectable_history if connectable else self._all_history
|
||||||
return histories.values()
|
return histories.values()
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_last_service_info(
|
def async_last_service_info(
|
||||||
self, address: str, connectable: bool
|
self, address: str, connectable: bool
|
||||||
) -> BluetoothServiceInfoBleak | None:
|
) -> BluetoothServiceInfoBleak | None:
|
||||||
@ -709,28 +606,6 @@ class BluetoothManager:
|
|||||||
histories = self._connectable_history if connectable else self._all_history
|
histories = self._connectable_history if connectable else self._all_history
|
||||||
return histories.get(address)
|
return histories.get(address)
|
||||||
|
|
||||||
def _async_trigger_matching_discovery(
|
|
||||||
self, service_info: BluetoothServiceInfoBleak
|
|
||||||
) -> None:
|
|
||||||
"""Trigger discovery for matching domains."""
|
|
||||||
for domain in self._integration_matcher.match_domains(service_info):
|
|
||||||
discovery_flow.async_create_flow(
|
|
||||||
self.hass,
|
|
||||||
domain,
|
|
||||||
{"source": config_entries.SOURCE_BLUETOOTH},
|
|
||||||
service_info,
|
|
||||||
)
|
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_rediscover_address(self, address: str) -> None:
|
|
||||||
"""Trigger discovery of devices which have already been seen."""
|
|
||||||
self._integration_matcher.async_clear_address(address)
|
|
||||||
if service_info := self._connectable_history.get(address):
|
|
||||||
self._async_trigger_matching_discovery(service_info)
|
|
||||||
return
|
|
||||||
if service_info := self._all_history.get(address):
|
|
||||||
self._async_trigger_matching_discovery(service_info)
|
|
||||||
|
|
||||||
def async_register_scanner(
|
def async_register_scanner(
|
||||||
self,
|
self,
|
||||||
scanner: BaseHaScanner,
|
scanner: BaseHaScanner,
|
||||||
@ -758,7 +633,6 @@ class BluetoothManager:
|
|||||||
self.slot_manager.register_adapter(scanner.adapter, connection_slots)
|
self.slot_manager.register_adapter(scanner.adapter, connection_slots)
|
||||||
return _unregister_scanner
|
return _unregister_scanner
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_register_bleak_callback(
|
def async_register_bleak_callback(
|
||||||
self, callback: AdvertisementDataCallback, filters: dict[str, set[str]]
|
self, callback: AdvertisementDataCallback, filters: dict[str, set[str]]
|
||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
@ -766,7 +640,6 @@ class BluetoothManager:
|
|||||||
callback_entry = (callback, filters)
|
callback_entry = (callback, filters)
|
||||||
self._bleak_callbacks.append(callback_entry)
|
self._bleak_callbacks.append(callback_entry)
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def _remove_callback() -> None:
|
def _remove_callback() -> None:
|
||||||
self._bleak_callbacks.remove(callback_entry)
|
self._bleak_callbacks.remove(callback_entry)
|
||||||
|
|
||||||
@ -780,29 +653,180 @@ class BluetoothManager:
|
|||||||
|
|
||||||
return _remove_callback
|
return _remove_callback
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_release_connection_slot(self, device: BLEDevice) -> None:
|
def async_release_connection_slot(self, device: BLEDevice) -> None:
|
||||||
"""Release a connection slot."""
|
"""Release a connection slot."""
|
||||||
self.slot_manager.release_slot(device)
|
self.slot_manager.release_slot(device)
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_allocate_connection_slot(self, device: BLEDevice) -> bool:
|
def async_allocate_connection_slot(self, device: BLEDevice) -> bool:
|
||||||
"""Allocate a connection slot."""
|
"""Allocate a connection slot."""
|
||||||
return self.slot_manager.allocate_slot(device)
|
return self.slot_manager.allocate_slot(device)
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_get_learned_advertising_interval(self, address: str) -> float | None:
|
def async_get_learned_advertising_interval(self, address: str) -> float | None:
|
||||||
"""Get the learned advertising interval for a MAC address."""
|
"""Get the learned advertising interval for a MAC address."""
|
||||||
return self._intervals.get(address)
|
return self._intervals.get(address)
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_get_fallback_availability_interval(self, address: str) -> float | None:
|
def async_get_fallback_availability_interval(self, address: str) -> float | None:
|
||||||
"""Get the fallback availability timeout for a MAC address."""
|
"""Get the fallback availability timeout for a MAC address."""
|
||||||
return self._fallback_intervals.get(address)
|
return self._fallback_intervals.get(address)
|
||||||
|
|
||||||
@hass_callback
|
|
||||||
def async_set_fallback_availability_interval(
|
def async_set_fallback_availability_interval(
|
||||||
self, address: str, interval: float
|
self, address: str, interval: float
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Override the fallback availability timeout for a MAC address."""
|
"""Override the fallback availability timeout for a MAC address."""
|
||||||
self._fallback_intervals[address] = interval
|
self._fallback_intervals[address] = interval
|
||||||
|
|
||||||
|
|
||||||
|
class HomeAssistantBluetoothManager(BluetoothManager):
|
||||||
|
"""Manage Bluetooth for Home Assistant."""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"hass",
|
||||||
|
"_integration_matcher",
|
||||||
|
"_callback_index",
|
||||||
|
"_cancel_logging_listener",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
integration_matcher: IntegrationMatcher,
|
||||||
|
bluetooth_adapters: BluetoothAdapters,
|
||||||
|
storage: BluetoothStorage,
|
||||||
|
slot_manager: BleakSlotManager,
|
||||||
|
) -> None:
|
||||||
|
"""Init bluetooth manager."""
|
||||||
|
self.hass = hass
|
||||||
|
self._integration_matcher = integration_matcher
|
||||||
|
self._callback_index = BluetoothCallbackMatcherIndex()
|
||||||
|
self._cancel_logging_listener: CALLBACK_TYPE | None = None
|
||||||
|
super().__init__(bluetooth_adapters, storage, slot_manager)
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def _async_logging_changed(self, event: Event) -> None:
|
||||||
|
"""Handle logging change."""
|
||||||
|
self._debug = _LOGGER.isEnabledFor(logging.DEBUG)
|
||||||
|
|
||||||
|
def _async_trigger_matching_discovery(
|
||||||
|
self, service_info: BluetoothServiceInfoBleak
|
||||||
|
) -> None:
|
||||||
|
"""Trigger discovery for matching domains."""
|
||||||
|
for domain in self._integration_matcher.match_domains(service_info):
|
||||||
|
discovery_flow.async_create_flow(
|
||||||
|
self.hass,
|
||||||
|
domain,
|
||||||
|
{"source": config_entries.SOURCE_BLUETOOTH},
|
||||||
|
service_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def async_rediscover_address(self, address: str) -> None:
|
||||||
|
"""Trigger discovery of devices which have already been seen."""
|
||||||
|
self._integration_matcher.async_clear_address(address)
|
||||||
|
if service_info := self._connectable_history.get(address):
|
||||||
|
self._async_trigger_matching_discovery(service_info)
|
||||||
|
return
|
||||||
|
if service_info := self._all_history.get(address):
|
||||||
|
self._async_trigger_matching_discovery(service_info)
|
||||||
|
|
||||||
|
def _discover_service_info(self, service_info: BluetoothServiceInfoBleak) -> None:
|
||||||
|
matched_domains = self._integration_matcher.match_domains(service_info)
|
||||||
|
if self._debug:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s: %s %s match: %s",
|
||||||
|
self._async_describe_source(service_info),
|
||||||
|
service_info.address,
|
||||||
|
service_info.advertisement,
|
||||||
|
matched_domains,
|
||||||
|
)
|
||||||
|
|
||||||
|
for match in self._callback_index.match_callbacks(service_info):
|
||||||
|
callback = match[CALLBACK]
|
||||||
|
try:
|
||||||
|
callback(service_info, BluetoothChange.ADVERTISEMENT)
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Error in bluetooth callback")
|
||||||
|
|
||||||
|
for domain in matched_domains:
|
||||||
|
discovery_flow.async_create_flow(
|
||||||
|
self.hass,
|
||||||
|
domain,
|
||||||
|
{"source": config_entries.SOURCE_BLUETOOTH},
|
||||||
|
service_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _address_disappeared(self, address: str) -> None:
|
||||||
|
"""Dismiss all discoveries for the given address."""
|
||||||
|
self._integration_matcher.async_clear_address(address)
|
||||||
|
for flow in self.hass.config_entries.flow.async_progress_by_init_data_type(
|
||||||
|
BluetoothServiceInfoBleak,
|
||||||
|
lambda service_info: bool(service_info.address == address),
|
||||||
|
):
|
||||||
|
self.hass.config_entries.flow.async_abort(flow["flow_id"])
|
||||||
|
|
||||||
|
async def async_setup(self) -> None:
|
||||||
|
"""Set up the bluetooth manager."""
|
||||||
|
await super().async_setup()
|
||||||
|
self._all_history, self._connectable_history = async_load_history_from_system(
|
||||||
|
self._bluetooth_adapters, self.storage
|
||||||
|
)
|
||||||
|
self._cancel_logging_listener = self.hass.bus.async_listen(
|
||||||
|
EVENT_LOGGING_CHANGED, self._async_logging_changed
|
||||||
|
)
|
||||||
|
seen: set[str] = set()
|
||||||
|
for address, service_info in itertools.chain(
|
||||||
|
self._connectable_history.items(), self._all_history.items()
|
||||||
|
):
|
||||||
|
if address in seen:
|
||||||
|
continue
|
||||||
|
seen.add(address)
|
||||||
|
self._async_trigger_matching_discovery(service_info)
|
||||||
|
|
||||||
|
def async_register_callback(
|
||||||
|
self,
|
||||||
|
callback: BluetoothCallback,
|
||||||
|
matcher: BluetoothCallbackMatcher | None,
|
||||||
|
) -> Callable[[], None]:
|
||||||
|
"""Register a callback."""
|
||||||
|
callback_matcher = BluetoothCallbackMatcherWithCallback(callback=callback)
|
||||||
|
if not matcher:
|
||||||
|
callback_matcher[CONNECTABLE] = True
|
||||||
|
else:
|
||||||
|
# We could write out every item in the typed dict here
|
||||||
|
# but that would be a bit inefficient and verbose.
|
||||||
|
callback_matcher.update(matcher)
|
||||||
|
callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True)
|
||||||
|
|
||||||
|
connectable = callback_matcher[CONNECTABLE]
|
||||||
|
self._callback_index.add_callback_matcher(callback_matcher)
|
||||||
|
|
||||||
|
def _async_remove_callback() -> None:
|
||||||
|
self._callback_index.remove_callback_matcher(callback_matcher)
|
||||||
|
|
||||||
|
# If we have history for the subscriber, we can trigger the callback
|
||||||
|
# immediately with the last packet so the subscriber can see the
|
||||||
|
# device.
|
||||||
|
history = self._connectable_history if connectable else self._all_history
|
||||||
|
service_infos: Iterable[BluetoothServiceInfoBleak] = []
|
||||||
|
if address := callback_matcher.get(ADDRESS):
|
||||||
|
if service_info := history.get(address):
|
||||||
|
service_infos = [service_info]
|
||||||
|
else:
|
||||||
|
service_infos = history.values()
|
||||||
|
|
||||||
|
for service_info in service_infos:
|
||||||
|
if ble_device_matches(callback_matcher, service_info):
|
||||||
|
try:
|
||||||
|
callback(service_info, BluetoothChange.ADVERTISEMENT)
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Error in bluetooth callback")
|
||||||
|
|
||||||
|
return _async_remove_callback
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def async_stop(self) -> None:
|
||||||
|
"""Stop the Bluetooth integration at shutdown."""
|
||||||
|
_LOGGER.debug("Stopping bluetooth manager")
|
||||||
|
super().async_stop()
|
||||||
|
if self._cancel_logging_listener:
|
||||||
|
self._cancel_logging_listener()
|
||||||
|
self._cancel_logging_listener = None
|
||||||
|
@ -283,7 +283,6 @@ class HaBleakClientWrapper(BleakClient):
|
|||||||
self.__disconnected_callback
|
self.__disconnected_callback
|
||||||
),
|
),
|
||||||
timeout=self.__timeout,
|
timeout=self.__timeout,
|
||||||
hass=manager.hass,
|
|
||||||
)
|
)
|
||||||
if debug_logging:
|
if debug_logging:
|
||||||
# Only lookup the description if we are going to log it
|
# Only lookup the description if we are going to log it
|
||||||
|
Loading…
x
Reference in New Issue
Block a user