mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 05:37:44 +00:00
Restore remote discovered devices between remote scanner restarts (#83699)
This commit is contained in:
parent
fbab7413a5
commit
9008006ac8
@ -71,9 +71,14 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .manager import BluetoothManager
|
from .manager import BluetoothManager
|
||||||
from .match import BluetoothCallbackMatcher, IntegrationMatcher
|
from .match import BluetoothCallbackMatcher, IntegrationMatcher
|
||||||
from .models import BluetoothCallback, BluetoothChange, BluetoothScanningMode
|
from .models import (
|
||||||
|
BluetoothCallback,
|
||||||
|
BluetoothChange,
|
||||||
|
BluetoothScanningMode,
|
||||||
|
HaBluetoothConnector,
|
||||||
|
)
|
||||||
from .scanner import HaScanner, ScannerStartError
|
from .scanner import HaScanner, ScannerStartError
|
||||||
from .wrappers import HaBluetoothConnector
|
from .storage import BluetoothStorage
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
@ -158,7 +163,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass))
|
integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass))
|
||||||
integration_matcher.async_setup()
|
integration_matcher.async_setup()
|
||||||
bluetooth_adapters = get_adapters()
|
bluetooth_adapters = get_adapters()
|
||||||
manager = BluetoothManager(hass, integration_matcher, bluetooth_adapters)
|
bluetooth_storage = BluetoothStorage(hass)
|
||||||
|
await bluetooth_storage.async_setup()
|
||||||
|
manager = BluetoothManager(
|
||||||
|
hass, integration_matcher, bluetooth_adapters, bluetooth_storage
|
||||||
|
)
|
||||||
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, manager.async_stop)
|
||||||
hass.data[DATA_MANAGER] = models.MANAGER = manager
|
hass.data[DATA_MANAGER] = models.MANAGER = manager
|
||||||
|
@ -11,13 +11,21 @@ from typing import Any, Final
|
|||||||
from bleak.backends.device import BLEDevice
|
from bleak.backends.device import BLEDevice
|
||||||
from bleak.backends.scanner import AdvertisementData
|
from bleak.backends.scanner import AdvertisementData
|
||||||
from bleak_retry_connector import NO_RSSI_VALUE
|
from bleak_retry_connector import NO_RSSI_VALUE
|
||||||
from bluetooth_adapters import adapter_human_name
|
from bluetooth_adapters import DiscoveredDeviceAdvertisementData, adapter_human_name
|
||||||
from home_assistant_bluetooth import BluetoothServiceInfoBleak
|
from home_assistant_bluetooth import BluetoothServiceInfoBleak
|
||||||
|
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
|
from homeassistant.core import (
|
||||||
|
CALLBACK_TYPE,
|
||||||
|
Event,
|
||||||
|
HomeAssistant,
|
||||||
|
callback as hass_callback,
|
||||||
|
)
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.util.dt import monotonic_time_coarse
|
from homeassistant.util.dt import monotonic_time_coarse
|
||||||
|
|
||||||
|
from . import models
|
||||||
from .const import (
|
from .const import (
|
||||||
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||||
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||||
@ -30,12 +38,22 @@ MONOTONIC_TIME: Final = monotonic_time_coarse
|
|||||||
class BaseHaScanner(ABC):
|
class BaseHaScanner(ABC):
|
||||||
"""Base class for Ha Scanners."""
|
"""Base class for Ha Scanners."""
|
||||||
|
|
||||||
__slots__ = ("hass", "source", "_connecting", "name", "scanning")
|
__slots__ = (
|
||||||
|
"hass",
|
||||||
|
"connectable",
|
||||||
|
"source",
|
||||||
|
"connector",
|
||||||
|
"_connecting",
|
||||||
|
"name",
|
||||||
|
"scanning",
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, source: str, adapter: str) -> None:
|
def __init__(self, hass: HomeAssistant, source: str, adapter: str) -> None:
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
self.connectable = False
|
||||||
self.source = source
|
self.source = source
|
||||||
|
self.connector: HaBluetoothConnector | None = None
|
||||||
self._connecting = 0
|
self._connecting = 0
|
||||||
self.name = adapter_human_name(adapter, source) if adapter != source else source
|
self.name = adapter_human_name(adapter, source) if adapter != source else source
|
||||||
self.scanning = True
|
self.scanning = True
|
||||||
@ -87,10 +105,9 @@ class BaseHaRemoteScanner(BaseHaScanner):
|
|||||||
"_new_info_callback",
|
"_new_info_callback",
|
||||||
"_discovered_device_advertisement_datas",
|
"_discovered_device_advertisement_datas",
|
||||||
"_discovered_device_timestamps",
|
"_discovered_device_timestamps",
|
||||||
"_connector",
|
|
||||||
"_connectable",
|
|
||||||
"_details",
|
"_details",
|
||||||
"_expire_seconds",
|
"_expire_seconds",
|
||||||
|
"_storage",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -109,12 +126,13 @@ class BaseHaRemoteScanner(BaseHaScanner):
|
|||||||
str, tuple[BLEDevice, AdvertisementData]
|
str, tuple[BLEDevice, AdvertisementData]
|
||||||
] = {}
|
] = {}
|
||||||
self._discovered_device_timestamps: dict[str, float] = {}
|
self._discovered_device_timestamps: dict[str, float] = {}
|
||||||
self._connector = connector
|
self.connectable = connectable
|
||||||
self._connectable = connectable
|
self.connector = connector
|
||||||
self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id}
|
self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id}
|
||||||
self._expire_seconds = FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
self._expire_seconds = FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
||||||
|
assert models.MANAGER is not None
|
||||||
|
self._storage = models.MANAGER.storage
|
||||||
if connectable:
|
if connectable:
|
||||||
self._details["connector"] = connector
|
|
||||||
self._expire_seconds = (
|
self._expire_seconds = (
|
||||||
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
||||||
)
|
)
|
||||||
@ -122,9 +140,40 @@ class BaseHaRemoteScanner(BaseHaScanner):
|
|||||||
@hass_callback
|
@hass_callback
|
||||||
def async_setup(self) -> CALLBACK_TYPE:
|
def async_setup(self) -> CALLBACK_TYPE:
|
||||||
"""Set up the scanner."""
|
"""Set up the scanner."""
|
||||||
return async_track_time_interval(
|
if history := self._storage.async_get_advertisement_history(self.source):
|
||||||
|
self._discovered_device_advertisement_datas = (
|
||||||
|
history.discovered_device_advertisement_datas
|
||||||
|
)
|
||||||
|
self._discovered_device_timestamps = history.discovered_device_timestamps
|
||||||
|
# Expire anything that is too old
|
||||||
|
self._async_expire_devices(dt_util.utcnow())
|
||||||
|
|
||||||
|
cancel_track = async_track_time_interval(
|
||||||
self.hass, self._async_expire_devices, timedelta(seconds=30)
|
self.hass, self._async_expire_devices, timedelta(seconds=30)
|
||||||
)
|
)
|
||||||
|
cancel_stop = self.hass.bus.async_listen(
|
||||||
|
EVENT_HOMEASSISTANT_STOP, self._save_history
|
||||||
|
)
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def _cancel() -> None:
|
||||||
|
self._save_history()
|
||||||
|
cancel_track()
|
||||||
|
cancel_stop()
|
||||||
|
|
||||||
|
return _cancel
|
||||||
|
|
||||||
|
def _save_history(self, event: Event | None = None) -> None:
|
||||||
|
"""Save the history."""
|
||||||
|
self._storage.async_set_advertisement_history(
|
||||||
|
self.source,
|
||||||
|
DiscoveredDeviceAdvertisementData(
|
||||||
|
self.connectable,
|
||||||
|
self._expire_seconds,
|
||||||
|
self._discovered_device_advertisement_datas,
|
||||||
|
self._discovered_device_timestamps,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def _async_expire_devices(self, _datetime: datetime.datetime) -> None:
|
def _async_expire_devices(self, _datetime: datetime.datetime) -> None:
|
||||||
"""Expire old devices."""
|
"""Expire old devices."""
|
||||||
@ -222,7 +271,7 @@ class BaseHaRemoteScanner(BaseHaScanner):
|
|||||||
source=self.source,
|
source=self.source,
|
||||||
device=device,
|
device=device,
|
||||||
advertisement=advertisement_data,
|
advertisement=advertisement_data,
|
||||||
connectable=self._connectable,
|
connectable=self.connectable,
|
||||||
time=now,
|
time=now,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -45,6 +45,7 @@ from .match import (
|
|||||||
ble_device_matches,
|
ble_device_matches,
|
||||||
)
|
)
|
||||||
from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak
|
from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak
|
||||||
|
from .storage import BluetoothStorage
|
||||||
from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher
|
from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher
|
||||||
from .util import async_load_history_from_system
|
from .util import async_load_history_from_system
|
||||||
|
|
||||||
@ -102,6 +103,7 @@ class BluetoothManager:
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
integration_matcher: IntegrationMatcher,
|
integration_matcher: IntegrationMatcher,
|
||||||
bluetooth_adapters: BluetoothAdapters,
|
bluetooth_adapters: BluetoothAdapters,
|
||||||
|
storage: BluetoothStorage,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init bluetooth manager."""
|
"""Init bluetooth manager."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
@ -128,6 +130,7 @@ class BluetoothManager:
|
|||||||
self._adapters: dict[str, AdapterDetails] = {}
|
self._adapters: dict[str, AdapterDetails] = {}
|
||||||
self._sources: dict[str, BaseHaScanner] = {}
|
self._sources: dict[str, BaseHaScanner] = {}
|
||||||
self._bluetooth_adapters = bluetooth_adapters
|
self._bluetooth_adapters = bluetooth_adapters
|
||||||
|
self.storage = storage
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supports_passive_scan(self) -> bool:
|
def supports_passive_scan(self) -> bool:
|
||||||
@ -196,12 +199,9 @@ class BluetoothManager:
|
|||||||
"""Set up the bluetooth manager."""
|
"""Set up the bluetooth manager."""
|
||||||
await self._bluetooth_adapters.refresh()
|
await self._bluetooth_adapters.refresh()
|
||||||
install_multiple_bleak_catcher()
|
install_multiple_bleak_catcher()
|
||||||
history = async_load_history_from_system(self._bluetooth_adapters)
|
self._all_history, self._connectable_history = async_load_history_from_system(
|
||||||
# Everything is connectable so it fall into both
|
self._bluetooth_adapters, self.storage
|
||||||
# buckets since the host system can only provide
|
)
|
||||||
# connectable devices
|
|
||||||
self._all_history = history.copy()
|
|
||||||
self._connectable_history = history.copy()
|
|
||||||
self.async_setup_unavailable_tracking()
|
self.async_setup_unavailable_tracking()
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
"requirements": [
|
"requirements": [
|
||||||
"bleak==0.19.2",
|
"bleak==0.19.2",
|
||||||
"bleak-retry-connector==2.10.1",
|
"bleak-retry-connector==2.10.1",
|
||||||
"bluetooth-adapters==0.12.0",
|
"bluetooth-adapters==0.14.1",
|
||||||
"bluetooth-auto-recovery==0.5.5",
|
"bluetooth-auto-recovery==0.5.5",
|
||||||
"bluetooth-data-tools==0.3.0",
|
"bluetooth-data-tools==0.3.0",
|
||||||
"dbus-fast==1.82.0"
|
"dbus-fast==1.82.0"
|
||||||
|
@ -132,6 +132,7 @@ class HaScanner(BaseHaScanner):
|
|||||||
"""Init bluetooth discovery."""
|
"""Init bluetooth discovery."""
|
||||||
source = address if address != DEFAULT_ADDRESS else adapter or SOURCE_LOCAL
|
source = address if address != DEFAULT_ADDRESS else adapter or SOURCE_LOCAL
|
||||||
super().__init__(hass, source, adapter)
|
super().__init__(hass, source, adapter)
|
||||||
|
self.connectable = True
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.adapter = adapter
|
self.adapter = adapter
|
||||||
self._start_stop_lock = asyncio.Lock()
|
self._start_stop_lock = asyncio.Lock()
|
||||||
|
59
homeassistant/components/bluetooth/storage.py
Normal file
59
homeassistant/components/bluetooth/storage.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"""Storage for remote scanners."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from bluetooth_adapters import (
|
||||||
|
DiscoveredDeviceAdvertisementData,
|
||||||
|
DiscoveryStorageType,
|
||||||
|
discovered_device_advertisement_data_from_dict,
|
||||||
|
discovered_device_advertisement_data_to_dict,
|
||||||
|
expire_stale_scanner_discovered_device_advertisement_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.storage import Store
|
||||||
|
|
||||||
|
REMOTE_SCANNER_STORAGE_VERSION = 1
|
||||||
|
REMOTE_SCANNER_STORAGE_KEY = "bluetooth.remote_scanners"
|
||||||
|
SCANNER_SAVE_DELAY = 5
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothStorage:
|
||||||
|
"""Storage for remote scanners."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize the storage."""
|
||||||
|
self._store: Store[DiscoveryStorageType] = Store(
|
||||||
|
hass, REMOTE_SCANNER_STORAGE_VERSION, REMOTE_SCANNER_STORAGE_KEY
|
||||||
|
)
|
||||||
|
self._data: DiscoveryStorageType = {}
|
||||||
|
|
||||||
|
async def async_setup(self) -> None:
|
||||||
|
"""Set up the storage."""
|
||||||
|
self._data = await self._store.async_load() or {}
|
||||||
|
expire_stale_scanner_discovered_device_advertisement_data(self._data)
|
||||||
|
|
||||||
|
def scanners(self) -> list[str]:
|
||||||
|
"""Get all scanners."""
|
||||||
|
return list(self._data.keys())
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_advertisement_history(
|
||||||
|
self, scanner: str
|
||||||
|
) -> DiscoveredDeviceAdvertisementData | None:
|
||||||
|
"""Get discovered devices by scanner."""
|
||||||
|
if not (scanner_data := self._data.get(scanner)):
|
||||||
|
return None
|
||||||
|
return discovered_device_advertisement_data_from_dict(scanner_data)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_get_data(self) -> DiscoveryStorageType:
|
||||||
|
"""Get data to save to disk."""
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_set_advertisement_history(
|
||||||
|
self, scanner: str, data: DiscoveredDeviceAdvertisementData
|
||||||
|
) -> None:
|
||||||
|
"""Set discovered devices by scanner."""
|
||||||
|
self._data[scanner] = discovered_device_advertisement_data_to_dict(data)
|
||||||
|
self._store.async_delay_save(self._async_get_data, SCANNER_SAVE_DELAY)
|
@ -8,32 +8,64 @@ from homeassistant.core import callback
|
|||||||
from homeassistant.util.dt import monotonic_time_coarse
|
from homeassistant.util.dt import monotonic_time_coarse
|
||||||
|
|
||||||
from .models import BluetoothServiceInfoBleak
|
from .models import BluetoothServiceInfoBleak
|
||||||
|
from .storage import BluetoothStorage
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_load_history_from_system(
|
def async_load_history_from_system(
|
||||||
adapters: BluetoothAdapters,
|
adapters: BluetoothAdapters, storage: BluetoothStorage
|
||||||
) -> dict[str, BluetoothServiceInfoBleak]:
|
) -> tuple[dict[str, BluetoothServiceInfoBleak], dict[str, BluetoothServiceInfoBleak]]:
|
||||||
"""Load the device and advertisement_data history if available on the current system."""
|
"""Load the device and advertisement_data history if available on the current system."""
|
||||||
now = monotonic_time_coarse()
|
now_monotonic = monotonic_time_coarse()
|
||||||
return {
|
connectable_loaded_history: dict[str, BluetoothServiceInfoBleak] = {}
|
||||||
address: BluetoothServiceInfoBleak(
|
all_loaded_history: dict[str, BluetoothServiceInfoBleak] = {}
|
||||||
name=history.advertisement_data.local_name
|
|
||||||
or history.device.name
|
# Restore local adapters
|
||||||
or history.device.address,
|
for address, history in adapters.history.items():
|
||||||
address=history.device.address,
|
if (
|
||||||
rssi=history.advertisement_data.rssi,
|
not (existing_all := connectable_loaded_history.get(address))
|
||||||
manufacturer_data=history.advertisement_data.manufacturer_data,
|
or history.advertisement_data.rssi > existing_all.rssi
|
||||||
service_data=history.advertisement_data.service_data,
|
):
|
||||||
service_uuids=history.advertisement_data.service_uuids,
|
connectable_loaded_history[address] = all_loaded_history[
|
||||||
source=history.source,
|
address
|
||||||
device=history.device,
|
] = BluetoothServiceInfoBleak.from_device_and_advertisement_data(
|
||||||
advertisement=history.advertisement_data,
|
history.device,
|
||||||
connectable=False,
|
history.advertisement_data,
|
||||||
time=now,
|
history.source,
|
||||||
)
|
now_monotonic,
|
||||||
for address, history in adapters.history.items()
|
True,
|
||||||
}
|
)
|
||||||
|
|
||||||
|
# Restore remote adapters
|
||||||
|
for scanner in storage.scanners():
|
||||||
|
if not (adv_history := storage.async_get_advertisement_history(scanner)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
connectable = adv_history.connectable
|
||||||
|
discovered_device_timestamps = adv_history.discovered_device_timestamps
|
||||||
|
for (
|
||||||
|
address,
|
||||||
|
(device, advertisement_data),
|
||||||
|
) in adv_history.discovered_device_advertisement_datas.items():
|
||||||
|
service_info = BluetoothServiceInfoBleak.from_device_and_advertisement_data(
|
||||||
|
device,
|
||||||
|
advertisement_data,
|
||||||
|
scanner,
|
||||||
|
discovered_device_timestamps[address],
|
||||||
|
connectable,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not (existing_all := all_loaded_history.get(address))
|
||||||
|
or service_info.rssi > existing_all.rssi
|
||||||
|
):
|
||||||
|
all_loaded_history[address] = service_info
|
||||||
|
if connectable and (
|
||||||
|
not (existing_connectable := connectable_loaded_history.get(address))
|
||||||
|
or service_info.rssi > existing_connectable.rssi
|
||||||
|
):
|
||||||
|
connectable_loaded_history[address] = service_info
|
||||||
|
|
||||||
|
return all_loaded_history, connectable_loaded_history
|
||||||
|
|
||||||
|
|
||||||
async def async_reset_adapter(adapter: str | None) -> bool | None:
|
async def async_reset_adapter(adapter: str | None) -> bool | None:
|
||||||
|
@ -6,7 +6,7 @@ from collections.abc import Callable
|
|||||||
import contextlib
|
import contextlib
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Final
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
from bleak import BleakClient, BleakError
|
from bleak import BleakClient, BleakError
|
||||||
from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type
|
from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type
|
||||||
@ -18,12 +18,15 @@ from homeassistant.core import CALLBACK_TYPE, callback as hass_callback
|
|||||||
from homeassistant.helpers.frame import report
|
from homeassistant.helpers.frame import report
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from .models import HaBluetoothConnector
|
|
||||||
|
|
||||||
FILTER_UUIDS: Final = "UUIDs"
|
FILTER_UUIDS: Final = "UUIDs"
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .manager import BluetoothManager
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _HaWrappedBleakBackend:
|
class _HaWrappedBleakBackend:
|
||||||
"""Wrap bleak backend to make it usable by Home Assistant."""
|
"""Wrap bleak backend to make it usable by Home Assistant."""
|
||||||
@ -207,23 +210,27 @@ class HaBleakClientWrapper(BleakClient):
|
|||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
def _async_get_backend_for_ble_device(
|
def _async_get_backend_for_ble_device(
|
||||||
self, ble_device: BLEDevice
|
self, manager: BluetoothManager, ble_device: BLEDevice
|
||||||
) -> _HaWrappedBleakBackend | None:
|
) -> _HaWrappedBleakBackend | None:
|
||||||
"""Get the backend for a BLEDevice."""
|
"""Get the backend for a BLEDevice."""
|
||||||
details = ble_device.details
|
details = ble_device.details
|
||||||
if not isinstance(details, dict) or "connector" not in details:
|
if not isinstance(details, dict) or "source" not in details:
|
||||||
# If client is not defined in details
|
# If client is not defined in details
|
||||||
# its the client for this platform
|
# its the client for this platform
|
||||||
cls = get_platform_client_backend_type()
|
cls = get_platform_client_backend_type()
|
||||||
return _HaWrappedBleakBackend(ble_device, cls)
|
return _HaWrappedBleakBackend(ble_device, cls)
|
||||||
|
|
||||||
connector: HaBluetoothConnector = details["connector"]
|
source: str = details["source"]
|
||||||
# Make sure the backend can connect to the device
|
# Make sure the backend can connect to the device
|
||||||
# as some backends have connection limits
|
# as some backends have connection limits
|
||||||
if not connector.can_connect():
|
if (
|
||||||
|
not (scanner := manager.async_scanner_by_source(source))
|
||||||
|
or not scanner.connector
|
||||||
|
or not scanner.connector.can_connect()
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return _HaWrappedBleakBackend(ble_device, connector.client)
|
return _HaWrappedBleakBackend(ble_device, scanner.connector.client)
|
||||||
|
|
||||||
@hass_callback
|
@hass_callback
|
||||||
def _async_get_best_available_backend_and_device(
|
def _async_get_best_available_backend_and_device(
|
||||||
@ -246,7 +253,7 @@ class HaBleakClientWrapper(BleakClient):
|
|||||||
reverse=True,
|
reverse=True,
|
||||||
):
|
):
|
||||||
if backend := self._async_get_backend_for_ble_device(
|
if backend := self._async_get_backend_for_ble_device(
|
||||||
device_advertisement_data[0]
|
models.MANAGER, device_advertisement_data[0]
|
||||||
):
|
):
|
||||||
return backend
|
return backend
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ awesomeversion==22.9.0
|
|||||||
bcrypt==3.1.7
|
bcrypt==3.1.7
|
||||||
bleak-retry-connector==2.10.1
|
bleak-retry-connector==2.10.1
|
||||||
bleak==0.19.2
|
bleak==0.19.2
|
||||||
bluetooth-adapters==0.12.0
|
bluetooth-adapters==0.14.1
|
||||||
bluetooth-auto-recovery==0.5.5
|
bluetooth-auto-recovery==0.5.5
|
||||||
bluetooth-data-tools==0.3.0
|
bluetooth-data-tools==0.3.0
|
||||||
certifi>=2021.5.30
|
certifi>=2021.5.30
|
||||||
@ -21,7 +21,7 @@ cryptography==38.0.3
|
|||||||
dbus-fast==1.82.0
|
dbus-fast==1.82.0
|
||||||
fnvhash==0.1.0
|
fnvhash==0.1.0
|
||||||
hass-nabucasa==0.61.0
|
hass-nabucasa==0.61.0
|
||||||
home-assistant-bluetooth==1.8.1
|
home-assistant-bluetooth==1.9.0
|
||||||
home-assistant-frontend==20221208.0
|
home-assistant-frontend==20221208.0
|
||||||
httpx==0.23.1
|
httpx==0.23.1
|
||||||
ifaddr==0.1.7
|
ifaddr==0.1.7
|
||||||
|
@ -36,7 +36,7 @@ dependencies = [
|
|||||||
# When bumping httpx, please check the version pins of
|
# When bumping httpx, please check the version pins of
|
||||||
# httpcore, anyio, and h11 in gen_requirements_all
|
# httpcore, anyio, and h11 in gen_requirements_all
|
||||||
"httpx==0.23.1",
|
"httpx==0.23.1",
|
||||||
"home-assistant-bluetooth==1.8.1",
|
"home-assistant-bluetooth==1.9.0",
|
||||||
"ifaddr==0.1.7",
|
"ifaddr==0.1.7",
|
||||||
"jinja2==3.1.2",
|
"jinja2==3.1.2",
|
||||||
"lru-dict==1.1.8",
|
"lru-dict==1.1.8",
|
||||||
|
@ -11,7 +11,7 @@ bcrypt==3.1.7
|
|||||||
certifi>=2021.5.30
|
certifi>=2021.5.30
|
||||||
ciso8601==2.2.0
|
ciso8601==2.2.0
|
||||||
httpx==0.23.1
|
httpx==0.23.1
|
||||||
home-assistant-bluetooth==1.8.1
|
home-assistant-bluetooth==1.9.0
|
||||||
ifaddr==0.1.7
|
ifaddr==0.1.7
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
lru-dict==1.1.8
|
lru-dict==1.1.8
|
||||||
|
@ -450,7 +450,7 @@ bluemaestro-ble==0.2.0
|
|||||||
# bluepy==1.3.0
|
# bluepy==1.3.0
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-adapters==0.12.0
|
bluetooth-adapters==0.14.1
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-auto-recovery==0.5.5
|
bluetooth-auto-recovery==0.5.5
|
||||||
|
@ -364,7 +364,7 @@ blinkpy==0.19.2
|
|||||||
bluemaestro-ble==0.2.0
|
bluemaestro-ble==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-adapters==0.12.0
|
bluetooth-adapters==0.14.1
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-auto-recovery==0.5.5
|
bluetooth-auto-recovery==0.5.5
|
||||||
|
1384
tests/components/bluetooth/fixtures/bluetooth.remote_scanners
Normal file
1384
tests/components/bluetooth/fixtures/bluetooth.remote_scanners
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -8,16 +8,23 @@ from unittest.mock import patch
|
|||||||
from bleak.backends.device import BLEDevice
|
from bleak.backends.device import BLEDevice
|
||||||
from bleak.backends.scanner import AdvertisementData
|
from bleak.backends.scanner import AdvertisementData
|
||||||
|
|
||||||
from homeassistant.components.bluetooth import BaseHaRemoteScanner, HaBluetoothConnector
|
from homeassistant.components import bluetooth
|
||||||
|
from homeassistant.components.bluetooth import (
|
||||||
|
BaseHaRemoteScanner,
|
||||||
|
HaBluetoothConnector,
|
||||||
|
storage,
|
||||||
|
)
|
||||||
from homeassistant.components.bluetooth.const import (
|
from homeassistant.components.bluetooth.const import (
|
||||||
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||||
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.json import json_loads
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from . import MockBleakClient, _get_manager, generate_advertisement_data
|
from . import MockBleakClient, _get_manager, generate_advertisement_data
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
from tests.common import async_fire_time_changed, load_fixture
|
||||||
|
|
||||||
|
|
||||||
async def test_remote_scanner(hass, enable_bluetooth):
|
async def test_remote_scanner(hass, enable_bluetooth):
|
||||||
@ -72,7 +79,7 @@ async def test_remote_scanner(hass, enable_bluetooth):
|
|||||||
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
||||||
)
|
)
|
||||||
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True)
|
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True)
|
||||||
scanner.async_setup()
|
unsetup = scanner.async_setup()
|
||||||
cancel = manager.async_register_scanner(scanner, True)
|
cancel = manager.async_register_scanner(scanner, True)
|
||||||
|
|
||||||
scanner.inject_advertisement(switchbot_device, switchbot_device_adv)
|
scanner.inject_advertisement(switchbot_device, switchbot_device_adv)
|
||||||
@ -103,6 +110,7 @@ async def test_remote_scanner(hass, enable_bluetooth):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
unsetup()
|
||||||
|
|
||||||
|
|
||||||
async def test_remote_scanner_expires_connectable(hass, enable_bluetooth):
|
async def test_remote_scanner_expires_connectable(hass, enable_bluetooth):
|
||||||
@ -143,7 +151,7 @@ async def test_remote_scanner_expires_connectable(hass, enable_bluetooth):
|
|||||||
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
||||||
)
|
)
|
||||||
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True)
|
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True)
|
||||||
scanner.async_setup()
|
unsetup = scanner.async_setup()
|
||||||
cancel = manager.async_register_scanner(scanner, True)
|
cancel = manager.async_register_scanner(scanner, True)
|
||||||
|
|
||||||
start_time_monotonic = time.monotonic()
|
start_time_monotonic = time.monotonic()
|
||||||
@ -174,6 +182,7 @@ async def test_remote_scanner_expires_connectable(hass, enable_bluetooth):
|
|||||||
assert len(scanner.discovered_devices_and_advertisement_data) == 0
|
assert len(scanner.discovered_devices_and_advertisement_data) == 0
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
unsetup()
|
||||||
|
|
||||||
|
|
||||||
async def test_remote_scanner_expires_non_connectable(hass, enable_bluetooth):
|
async def test_remote_scanner_expires_non_connectable(hass, enable_bluetooth):
|
||||||
@ -214,7 +223,7 @@ async def test_remote_scanner_expires_non_connectable(hass, enable_bluetooth):
|
|||||||
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
||||||
)
|
)
|
||||||
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False)
|
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False)
|
||||||
scanner.async_setup()
|
unsetup = scanner.async_setup()
|
||||||
cancel = manager.async_register_scanner(scanner, True)
|
cancel = manager.async_register_scanner(scanner, True)
|
||||||
|
|
||||||
start_time_monotonic = time.monotonic()
|
start_time_monotonic = time.monotonic()
|
||||||
@ -268,6 +277,7 @@ async def test_remote_scanner_expires_non_connectable(hass, enable_bluetooth):
|
|||||||
assert len(scanner.discovered_devices_and_advertisement_data) == 0
|
assert len(scanner.discovered_devices_and_advertisement_data) == 0
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
unsetup()
|
||||||
|
|
||||||
|
|
||||||
async def test_base_scanner_connecting_behavior(hass, enable_bluetooth):
|
async def test_base_scanner_connecting_behavior(hass, enable_bluetooth):
|
||||||
@ -308,7 +318,7 @@ async def test_base_scanner_connecting_behavior(hass, enable_bluetooth):
|
|||||||
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
||||||
)
|
)
|
||||||
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False)
|
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False)
|
||||||
scanner.async_setup()
|
unsetup = scanner.async_setup()
|
||||||
cancel = manager.async_register_scanner(scanner, True)
|
cancel = manager.async_register_scanner(scanner, True)
|
||||||
|
|
||||||
with scanner.connecting():
|
with scanner.connecting():
|
||||||
@ -327,3 +337,60 @@ async def test_base_scanner_connecting_behavior(hass, enable_bluetooth):
|
|||||||
assert devices[0].name == "wohand"
|
assert devices[0].name == "wohand"
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
unsetup()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_restore_history_remote_adapter(hass, hass_storage):
|
||||||
|
"""Test we can restore history for a remote adapter."""
|
||||||
|
|
||||||
|
data = hass_storage[storage.REMOTE_SCANNER_STORAGE_KEY] = json_loads(
|
||||||
|
load_fixture("bluetooth.remote_scanners", bluetooth.DOMAIN)
|
||||||
|
)
|
||||||
|
now = time.time()
|
||||||
|
timestamps = data["data"]["atom-bluetooth-proxy-ceaac4"][
|
||||||
|
"discovered_device_timestamps"
|
||||||
|
]
|
||||||
|
for address in timestamps:
|
||||||
|
if address != "E3:A5:63:3E:5E:23":
|
||||||
|
timestamps[address] = now
|
||||||
|
|
||||||
|
with patch("bluetooth_adapters.systems.linux.LinuxAdapters.history", {},), patch(
|
||||||
|
"bluetooth_adapters.systems.linux.LinuxAdapters.refresh",
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
connector = (
|
||||||
|
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
||||||
|
)
|
||||||
|
scanner = BaseHaRemoteScanner(
|
||||||
|
hass,
|
||||||
|
"atom-bluetooth-proxy-ceaac4",
|
||||||
|
"atom-bluetooth-proxy-ceaac4",
|
||||||
|
lambda adv: None,
|
||||||
|
connector,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
unsetup = scanner.async_setup()
|
||||||
|
cancel = _get_manager().async_register_scanner(scanner, True)
|
||||||
|
|
||||||
|
assert "EB:0B:36:35:6F:A4" in scanner.discovered_devices_and_advertisement_data
|
||||||
|
assert "E3:A5:63:3E:5E:23" not in scanner.discovered_devices_and_advertisement_data
|
||||||
|
cancel()
|
||||||
|
unsetup()
|
||||||
|
|
||||||
|
scanner = BaseHaRemoteScanner(
|
||||||
|
hass,
|
||||||
|
"atom-bluetooth-proxy-ceaac4",
|
||||||
|
"atom-bluetooth-proxy-ceaac4",
|
||||||
|
lambda adv: None,
|
||||||
|
connector,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
unsetup = scanner.async_setup()
|
||||||
|
cancel = _get_manager().async_register_scanner(scanner, True)
|
||||||
|
assert "EB:0B:36:35:6F:A4" in scanner.discovered_devices_and_advertisement_data
|
||||||
|
assert "E3:A5:63:3E:5E:23" not in scanner.discovered_devices_and_advertisement_data
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
unsetup()
|
||||||
|
@ -8,10 +8,12 @@ from bluetooth_adapters import AdvertisementHistory
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import bluetooth
|
from homeassistant.components import bluetooth
|
||||||
|
from homeassistant.components.bluetooth import storage
|
||||||
from homeassistant.components.bluetooth.manager import (
|
from homeassistant.components.bluetooth.manager import (
|
||||||
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.json import json_loads
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
@ -22,6 +24,8 @@ from . import (
|
|||||||
inject_advertisement_with_time_and_source_connectable,
|
inject_advertisement_with_time_and_source_connectable,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from tests.common import load_fixture
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def register_hci0_scanner(hass: HomeAssistant) -> None:
|
def register_hci0_scanner(hass: HomeAssistant) -> None:
|
||||||
@ -282,6 +286,76 @@ async def test_restore_history_from_dbus(hass, one_adapter):
|
|||||||
assert bluetooth.async_ble_device_from_address(hass, address) is ble_device
|
assert bluetooth.async_ble_device_from_address(hass, address) is ble_device
|
||||||
|
|
||||||
|
|
||||||
|
async def test_restore_history_from_dbus_and_remote_adapters(
|
||||||
|
hass, one_adapter, hass_storage
|
||||||
|
):
|
||||||
|
"""Test we can restore history from dbus along with remote adapters."""
|
||||||
|
address = "AA:BB:CC:CC:CC:FF"
|
||||||
|
|
||||||
|
data = hass_storage[storage.REMOTE_SCANNER_STORAGE_KEY] = json_loads(
|
||||||
|
load_fixture("bluetooth.remote_scanners", bluetooth.DOMAIN)
|
||||||
|
)
|
||||||
|
now = time.time()
|
||||||
|
timestamps = data["data"]["atom-bluetooth-proxy-ceaac4"][
|
||||||
|
"discovered_device_timestamps"
|
||||||
|
]
|
||||||
|
for address in timestamps:
|
||||||
|
timestamps[address] = now
|
||||||
|
|
||||||
|
ble_device = BLEDevice(address, "name")
|
||||||
|
history = {
|
||||||
|
address: AdvertisementHistory(
|
||||||
|
ble_device, generate_advertisement_data(local_name="name"), "hci0"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"bluetooth_adapters.systems.linux.LinuxAdapters.history",
|
||||||
|
history,
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert bluetooth.async_ble_device_from_address(hass, address) is not None
|
||||||
|
assert (
|
||||||
|
bluetooth.async_ble_device_from_address(hass, "EB:0B:36:35:6F:A4") is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_restore_history_from_dbus_and_corrupted_remote_adapters(
|
||||||
|
hass, one_adapter, hass_storage
|
||||||
|
):
|
||||||
|
"""Test we can restore history from dbus when the remote adapters data is corrupted."""
|
||||||
|
address = "AA:BB:CC:CC:CC:FF"
|
||||||
|
|
||||||
|
data = hass_storage[storage.REMOTE_SCANNER_STORAGE_KEY] = json_loads(
|
||||||
|
load_fixture("bluetooth.remote_scanners.corrupt", bluetooth.DOMAIN)
|
||||||
|
)
|
||||||
|
now = time.time()
|
||||||
|
timestamps = data["data"]["atom-bluetooth-proxy-ceaac4"][
|
||||||
|
"discovered_device_timestamps"
|
||||||
|
]
|
||||||
|
for address in timestamps:
|
||||||
|
timestamps[address] = now
|
||||||
|
|
||||||
|
ble_device = BLEDevice(address, "name")
|
||||||
|
history = {
|
||||||
|
address: AdvertisementHistory(
|
||||||
|
ble_device, generate_advertisement_data(local_name="name"), "hci0"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"bluetooth_adapters.systems.linux.LinuxAdapters.history",
|
||||||
|
history,
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert bluetooth.async_ble_device_from_address(hass, address) is not None
|
||||||
|
assert bluetooth.async_ble_device_from_address(hass, "EB:0B:36:35:6F:A4") is None
|
||||||
|
|
||||||
|
|
||||||
async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable(
|
async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable(
|
||||||
hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner
|
hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner
|
||||||
):
|
):
|
||||||
|
@ -9,7 +9,11 @@ from bleak.backends.device import BLEDevice
|
|||||||
from bleak.backends.scanner import AdvertisementData
|
from bleak.backends.scanner import AdvertisementData
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.bluetooth import BaseHaScanner, HaBluetoothConnector
|
from homeassistant.components.bluetooth import (
|
||||||
|
BaseHaRemoteScanner,
|
||||||
|
BaseHaScanner,
|
||||||
|
HaBluetoothConnector,
|
||||||
|
)
|
||||||
from homeassistant.components.bluetooth.wrappers import (
|
from homeassistant.components.bluetooth.wrappers import (
|
||||||
HaBleakClientWrapper,
|
HaBleakClientWrapper,
|
||||||
HaBleakScannerWrapper,
|
HaBleakScannerWrapper,
|
||||||
@ -57,6 +61,67 @@ async def test_wrapped_bleak_client_set_disconnected_callback_before_connected(
|
|||||||
client.set_disconnected_callback(lambda client: None)
|
client.set_disconnected_callback(lambda client: None)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_wrapped_bleak_client_local_adapter_only(
|
||||||
|
hass, enable_bluetooth, one_adapter
|
||||||
|
):
|
||||||
|
"""Test wrapped bleak client with only a local adapter."""
|
||||||
|
manager = _get_manager()
|
||||||
|
|
||||||
|
switchbot_device = BLEDevice(
|
||||||
|
"44:44:33:11:23:45",
|
||||||
|
"wohand",
|
||||||
|
{"path": "/org/bluez/hci0/dev_44_44_33_11_23_45"},
|
||||||
|
)
|
||||||
|
switchbot_adv = generate_advertisement_data(
|
||||||
|
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}, rssi=-100
|
||||||
|
)
|
||||||
|
|
||||||
|
class FakeScanner(BaseHaScanner):
|
||||||
|
@property
|
||||||
|
def discovered_devices(self) -> list[BLEDevice]:
|
||||||
|
"""Return a list of discovered devices."""
|
||||||
|
return []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def discovered_devices_and_advertisement_data(
|
||||||
|
self,
|
||||||
|
) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
|
||||||
|
"""Return a list of discovered devices."""
|
||||||
|
return {
|
||||||
|
switchbot_device.address: (
|
||||||
|
switchbot_device,
|
||||||
|
switchbot_adv,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_get_device_by_address(self, address: str) -> BLEDevice | None:
|
||||||
|
"""Return a list of discovered devices."""
|
||||||
|
if address == switchbot_device.address:
|
||||||
|
return switchbot_adv
|
||||||
|
return None
|
||||||
|
|
||||||
|
scanner = FakeScanner(
|
||||||
|
hass,
|
||||||
|
"00:00:00:00:00:01",
|
||||||
|
"hci0",
|
||||||
|
)
|
||||||
|
cancel = manager.async_register_scanner(scanner, True)
|
||||||
|
inject_advertisement_with_source(
|
||||||
|
hass, switchbot_device, switchbot_adv, "00:00:00:00:00:01"
|
||||||
|
)
|
||||||
|
|
||||||
|
client = HaBleakClientWrapper(switchbot_device)
|
||||||
|
with patch(
|
||||||
|
"bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect",
|
||||||
|
return_value=True,
|
||||||
|
), patch("bleak.backends.bluezdbus.client.BleakClientBlueZDBus.is_connected", True):
|
||||||
|
assert await client.connect() is True
|
||||||
|
assert client.is_connected is True
|
||||||
|
client.set_disconnected_callback(lambda client: None)
|
||||||
|
await client.disconnect()
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
|
||||||
async def test_wrapped_bleak_client_set_disconnected_callback_after_connected(
|
async def test_wrapped_bleak_client_set_disconnected_callback_after_connected(
|
||||||
hass, enable_bluetooth, one_adapter
|
hass, enable_bluetooth, one_adapter
|
||||||
):
|
):
|
||||||
@ -67,9 +132,7 @@ async def test_wrapped_bleak_client_set_disconnected_callback_after_connected(
|
|||||||
"44:44:33:11:23:45",
|
"44:44:33:11:23:45",
|
||||||
"wohand",
|
"wohand",
|
||||||
{
|
{
|
||||||
"connector": HaBluetoothConnector(
|
"source": "esp32_has_connection_slot",
|
||||||
MockBleakClient, "mock_bleak_client", lambda: True
|
|
||||||
),
|
|
||||||
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||||
},
|
},
|
||||||
rssi=-40,
|
rssi=-40,
|
||||||
@ -89,17 +152,7 @@ async def test_wrapped_bleak_client_set_disconnected_callback_after_connected(
|
|||||||
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}, rssi=-100
|
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}, rssi=-100
|
||||||
)
|
)
|
||||||
|
|
||||||
inject_advertisement_with_source(
|
class FakeScanner(BaseHaRemoteScanner):
|
||||||
hass, switchbot_device, switchbot_adv, "00:00:00:00:00:01"
|
|
||||||
)
|
|
||||||
inject_advertisement_with_source(
|
|
||||||
hass,
|
|
||||||
switchbot_proxy_device_has_connection_slot,
|
|
||||||
switchbot_proxy_device_adv_has_connection_slot,
|
|
||||||
"esp32_has_connection_slot",
|
|
||||||
)
|
|
||||||
|
|
||||||
class FakeScanner(BaseHaScanner):
|
|
||||||
@property
|
@property
|
||||||
def discovered_devices(self) -> list[BLEDevice]:
|
def discovered_devices(self) -> list[BLEDevice]:
|
||||||
"""Return a list of discovered devices."""
|
"""Return a list of discovered devices."""
|
||||||
@ -123,31 +176,50 @@ async def test_wrapped_bleak_client_set_disconnected_callback_after_connected(
|
|||||||
return switchbot_proxy_device_has_connection_slot
|
return switchbot_proxy_device_has_connection_slot
|
||||||
return None
|
return None
|
||||||
|
|
||||||
scanner = FakeScanner(hass, "esp32", "esp32")
|
connector = HaBluetoothConnector(
|
||||||
|
MockBleakClient, "esp32_has_connection_slot", lambda: True
|
||||||
|
)
|
||||||
|
scanner = FakeScanner(
|
||||||
|
hass,
|
||||||
|
"esp32_has_connection_slot",
|
||||||
|
"esp32_has_connection_slot",
|
||||||
|
lambda info: None,
|
||||||
|
connector,
|
||||||
|
True,
|
||||||
|
)
|
||||||
cancel = manager.async_register_scanner(scanner, True)
|
cancel = manager.async_register_scanner(scanner, True)
|
||||||
|
inject_advertisement_with_source(
|
||||||
|
hass, switchbot_device, switchbot_adv, "00:00:00:00:00:01"
|
||||||
|
)
|
||||||
|
inject_advertisement_with_source(
|
||||||
|
hass,
|
||||||
|
switchbot_proxy_device_has_connection_slot,
|
||||||
|
switchbot_proxy_device_adv_has_connection_slot,
|
||||||
|
"esp32_has_connection_slot",
|
||||||
|
)
|
||||||
client = HaBleakClientWrapper(switchbot_proxy_device_has_connection_slot)
|
client = HaBleakClientWrapper(switchbot_proxy_device_has_connection_slot)
|
||||||
with patch("bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect"):
|
with patch(
|
||||||
await client.connect()
|
"bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect",
|
||||||
assert client.is_connected is True
|
return_value=True,
|
||||||
|
), patch("bleak.backends.bluezdbus.client.BleakClientBlueZDBus.is_connected", True):
|
||||||
|
assert await client.connect() is True
|
||||||
|
assert client.is_connected is True
|
||||||
client.set_disconnected_callback(lambda client: None)
|
client.set_disconnected_callback(lambda client: None)
|
||||||
await client.disconnect()
|
await client.disconnect()
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
|
||||||
async def test_ble_device_with_proxy_client_out_of_connections(
|
async def test_ble_device_with_proxy_client_out_of_connections_no_scanners(
|
||||||
hass, enable_bluetooth, one_adapter
|
hass, enable_bluetooth, one_adapter
|
||||||
):
|
):
|
||||||
"""Test we switch to the next available proxy when one runs out of connections."""
|
"""Test we switch to the next available proxy when one runs out of connections with no scanners."""
|
||||||
manager = _get_manager()
|
manager = _get_manager()
|
||||||
|
|
||||||
switchbot_proxy_device_no_connection_slot = BLEDevice(
|
switchbot_proxy_device_no_connection_slot = BLEDevice(
|
||||||
"44:44:33:11:23:45",
|
"44:44:33:11:23:45",
|
||||||
"wohand",
|
"wohand",
|
||||||
{
|
{
|
||||||
"connector": HaBluetoothConnector(
|
"source": "esp32",
|
||||||
MockBleakClient, "mock_bleak_client", lambda: False
|
|
||||||
),
|
|
||||||
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||||
},
|
},
|
||||||
rssi=-30,
|
rssi=-30,
|
||||||
@ -174,17 +246,17 @@ async def test_ble_device_with_proxy_client_out_of_connections(
|
|||||||
await client.disconnect()
|
await client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
async def test_ble_device_with_proxy_clear_cache(hass, enable_bluetooth, one_adapter):
|
async def test_ble_device_with_proxy_client_out_of_connections(
|
||||||
"""Test we can clear cache on the proxy."""
|
hass, enable_bluetooth, one_adapter
|
||||||
|
):
|
||||||
|
"""Test handling all scanners are out of connection slots."""
|
||||||
manager = _get_manager()
|
manager = _get_manager()
|
||||||
|
|
||||||
switchbot_proxy_device_with_connection_slot = BLEDevice(
|
switchbot_proxy_device_no_connection_slot = BLEDevice(
|
||||||
"44:44:33:11:23:45",
|
"44:44:33:11:23:45",
|
||||||
"wohand",
|
"wohand",
|
||||||
{
|
{
|
||||||
"connector": HaBluetoothConnector(
|
"source": "esp32",
|
||||||
MockBleakClient, "mock_bleak_client", lambda: True
|
|
||||||
),
|
|
||||||
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||||
},
|
},
|
||||||
rssi=-30,
|
rssi=-30,
|
||||||
@ -193,7 +265,70 @@ async def test_ble_device_with_proxy_clear_cache(hass, enable_bluetooth, one_ada
|
|||||||
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
|
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
|
||||||
)
|
)
|
||||||
|
|
||||||
class FakeScanner(BaseHaScanner):
|
class FakeScanner(BaseHaRemoteScanner):
|
||||||
|
@property
|
||||||
|
def discovered_devices(self) -> list[BLEDevice]:
|
||||||
|
"""Return a list of discovered devices."""
|
||||||
|
return []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def discovered_devices_and_advertisement_data(
|
||||||
|
self,
|
||||||
|
) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
|
||||||
|
"""Return a list of discovered devices."""
|
||||||
|
return {
|
||||||
|
switchbot_proxy_device_no_connection_slot.address: (
|
||||||
|
switchbot_proxy_device_no_connection_slot,
|
||||||
|
switchbot_adv,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_get_device_by_address(self, address: str) -> BLEDevice | None:
|
||||||
|
"""Return a list of discovered devices."""
|
||||||
|
if address == switchbot_proxy_device_no_connection_slot.address:
|
||||||
|
return switchbot_adv
|
||||||
|
return None
|
||||||
|
|
||||||
|
connector = HaBluetoothConnector(MockBleakClient, "esp32", lambda: False)
|
||||||
|
scanner = FakeScanner(hass, "esp32", "esp32", lambda info: None, connector, True)
|
||||||
|
cancel = manager.async_register_scanner(scanner, True)
|
||||||
|
inject_advertisement_with_source(
|
||||||
|
hass, switchbot_proxy_device_no_connection_slot, switchbot_adv, "esp32"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert manager.async_discovered_devices(True) == [
|
||||||
|
switchbot_proxy_device_no_connection_slot
|
||||||
|
]
|
||||||
|
|
||||||
|
client = HaBleakClientWrapper(switchbot_proxy_device_no_connection_slot)
|
||||||
|
with patch(
|
||||||
|
"bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect"
|
||||||
|
), pytest.raises(BleakError):
|
||||||
|
await client.connect()
|
||||||
|
assert client.is_connected is False
|
||||||
|
client.set_disconnected_callback(lambda client: None)
|
||||||
|
await client.disconnect()
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ble_device_with_proxy_clear_cache(hass, enable_bluetooth, one_adapter):
|
||||||
|
"""Test we can clear cache on the proxy."""
|
||||||
|
manager = _get_manager()
|
||||||
|
|
||||||
|
switchbot_proxy_device_with_connection_slot = BLEDevice(
|
||||||
|
"44:44:33:11:23:45",
|
||||||
|
"wohand",
|
||||||
|
{
|
||||||
|
"source": "esp32",
|
||||||
|
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||||
|
},
|
||||||
|
rssi=-30,
|
||||||
|
)
|
||||||
|
switchbot_adv = generate_advertisement_data(
|
||||||
|
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
|
||||||
|
)
|
||||||
|
|
||||||
|
class FakeScanner(BaseHaRemoteScanner):
|
||||||
@property
|
@property
|
||||||
def discovered_devices(self) -> list[BLEDevice]:
|
def discovered_devices(self) -> list[BLEDevice]:
|
||||||
"""Return a list of discovered devices."""
|
"""Return a list of discovered devices."""
|
||||||
@ -217,7 +352,8 @@ async def test_ble_device_with_proxy_clear_cache(hass, enable_bluetooth, one_ada
|
|||||||
return switchbot_adv
|
return switchbot_adv
|
||||||
return None
|
return None
|
||||||
|
|
||||||
scanner = FakeScanner(hass, "esp32", "esp32")
|
connector = HaBluetoothConnector(MockBleakClient, "esp32", lambda: True)
|
||||||
|
scanner = FakeScanner(hass, "esp32", "esp32", lambda info: None, connector, True)
|
||||||
cancel = manager.async_register_scanner(scanner, True)
|
cancel = manager.async_register_scanner(scanner, True)
|
||||||
inject_advertisement_with_source(
|
inject_advertisement_with_source(
|
||||||
hass, switchbot_proxy_device_with_connection_slot, switchbot_adv, "esp32"
|
hass, switchbot_proxy_device_with_connection_slot, switchbot_adv, "esp32"
|
||||||
@ -245,9 +381,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
|
|||||||
"44:44:33:11:23:45",
|
"44:44:33:11:23:45",
|
||||||
"wohand",
|
"wohand",
|
||||||
{
|
{
|
||||||
"connector": HaBluetoothConnector(
|
"source": "esp32_no_connection_slot",
|
||||||
MockBleakClient, "mock_bleak_client", lambda: False
|
|
||||||
),
|
|
||||||
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -261,9 +395,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
|
|||||||
"44:44:33:11:23:45",
|
"44:44:33:11:23:45",
|
||||||
"wohand",
|
"wohand",
|
||||||
{
|
{
|
||||||
"connector": HaBluetoothConnector(
|
"source": "esp32_has_connection_slot",
|
||||||
MockBleakClient, "mock_bleak_client", lambda: True
|
|
||||||
),
|
|
||||||
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||||
},
|
},
|
||||||
rssi=-40,
|
rssi=-40,
|
||||||
@ -299,7 +431,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
|
|||||||
"esp32_no_connection_slot",
|
"esp32_no_connection_slot",
|
||||||
)
|
)
|
||||||
|
|
||||||
class FakeScanner(BaseHaScanner):
|
class FakeScanner(BaseHaRemoteScanner):
|
||||||
@property
|
@property
|
||||||
def discovered_devices(self) -> list[BLEDevice]:
|
def discovered_devices(self) -> list[BLEDevice]:
|
||||||
"""Return a list of discovered devices."""
|
"""Return a list of discovered devices."""
|
||||||
@ -323,7 +455,17 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
|
|||||||
return switchbot_proxy_device_has_connection_slot
|
return switchbot_proxy_device_has_connection_slot
|
||||||
return None
|
return None
|
||||||
|
|
||||||
scanner = FakeScanner(hass, "esp32", "esp32")
|
connector = HaBluetoothConnector(
|
||||||
|
MockBleakClient, "esp32_has_connection_slot", lambda: True
|
||||||
|
)
|
||||||
|
scanner = FakeScanner(
|
||||||
|
hass,
|
||||||
|
"esp32_has_connection_slot",
|
||||||
|
"esp32_has_connection_slot",
|
||||||
|
lambda info: None,
|
||||||
|
connector,
|
||||||
|
True,
|
||||||
|
)
|
||||||
cancel = manager.async_register_scanner(scanner, True)
|
cancel = manager.async_register_scanner(scanner, True)
|
||||||
assert manager.async_discovered_devices(True) == [
|
assert manager.async_discovered_devices(True) == [
|
||||||
switchbot_proxy_device_no_connection_slot
|
switchbot_proxy_device_no_connection_slot
|
||||||
@ -348,9 +490,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
|
|||||||
"44:44:33:11:23:45",
|
"44:44:33:11:23:45",
|
||||||
"wohand_no_connection_slot",
|
"wohand_no_connection_slot",
|
||||||
{
|
{
|
||||||
"connector": HaBluetoothConnector(
|
"source": "esp32_no_connection_slot",
|
||||||
MockBleakClient, "mock_bleak_client", lambda: False
|
|
||||||
),
|
|
||||||
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||||
},
|
},
|
||||||
rssi=-30,
|
rssi=-30,
|
||||||
@ -366,9 +506,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
|
|||||||
"44:44:33:11:23:45",
|
"44:44:33:11:23:45",
|
||||||
"wohand_has_connection_slot",
|
"wohand_has_connection_slot",
|
||||||
{
|
{
|
||||||
"connector": HaBluetoothConnector(
|
"source": "esp32_has_connection_slot",
|
||||||
MockBleakClient, "mock_bleak_client", lambda: True
|
|
||||||
),
|
|
||||||
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -410,7 +548,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
|
|||||||
"esp32_no_connection_slot",
|
"esp32_no_connection_slot",
|
||||||
)
|
)
|
||||||
|
|
||||||
class FakeScanner(BaseHaScanner):
|
class FakeScanner(BaseHaRemoteScanner):
|
||||||
@property
|
@property
|
||||||
def discovered_devices(self) -> list[BLEDevice]:
|
def discovered_devices(self) -> list[BLEDevice]:
|
||||||
"""Return a list of discovered devices."""
|
"""Return a list of discovered devices."""
|
||||||
@ -434,7 +572,17 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
|
|||||||
return switchbot_proxy_device_has_connection_slot
|
return switchbot_proxy_device_has_connection_slot
|
||||||
return None
|
return None
|
||||||
|
|
||||||
scanner = FakeScanner(hass, "esp32", "esp32")
|
connector = HaBluetoothConnector(
|
||||||
|
MockBleakClient, "esp32_has_connection_slot", lambda: True
|
||||||
|
)
|
||||||
|
scanner = FakeScanner(
|
||||||
|
hass,
|
||||||
|
"esp32_has_connection_slot",
|
||||||
|
"esp32_has_connection_slot",
|
||||||
|
lambda info: None,
|
||||||
|
connector,
|
||||||
|
True,
|
||||||
|
)
|
||||||
cancel = manager.async_register_scanner(scanner, True)
|
cancel = manager.async_register_scanner(scanner, True)
|
||||||
assert manager.async_discovered_devices(True) == [
|
assert manager.async_discovered_devices(True) == [
|
||||||
switchbot_proxy_device_no_connection_slot
|
switchbot_proxy_device_no_connection_slot
|
||||||
|
@ -1092,6 +1092,9 @@ async def mock_enable_bluetooth(
|
|||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
yield
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="mock_bluetooth_adapters")
|
@pytest.fixture(name="mock_bluetooth_adapters")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user