Restore remote discovered devices between remote scanner restarts (#83699)
parent
fbab7413a5
commit
9008006ac8
|
@ -71,9 +71,14 @@ from .const import (
|
|||
)
|
||||
from .manager import BluetoothManager
|
||||
from .match import BluetoothCallbackMatcher, IntegrationMatcher
|
||||
from .models import BluetoothCallback, BluetoothChange, BluetoothScanningMode
|
||||
from .models import (
|
||||
BluetoothCallback,
|
||||
BluetoothChange,
|
||||
BluetoothScanningMode,
|
||||
HaBluetoothConnector,
|
||||
)
|
||||
from .scanner import HaScanner, ScannerStartError
|
||||
from .wrappers import HaBluetoothConnector
|
||||
from .storage import BluetoothStorage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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.async_setup()
|
||||
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()
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop)
|
||||
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.scanner import AdvertisementData
|
||||
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 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
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.dt import monotonic_time_coarse
|
||||
|
||||
from . import models
|
||||
from .const import (
|
||||
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||
|
@ -30,12 +38,22 @@ MONOTONIC_TIME: Final = monotonic_time_coarse
|
|||
class BaseHaScanner(ABC):
|
||||
"""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:
|
||||
"""Initialize the scanner."""
|
||||
self.hass = hass
|
||||
self.connectable = False
|
||||
self.source = source
|
||||
self.connector: HaBluetoothConnector | None = None
|
||||
self._connecting = 0
|
||||
self.name = adapter_human_name(adapter, source) if adapter != source else source
|
||||
self.scanning = True
|
||||
|
@ -87,10 +105,9 @@ class BaseHaRemoteScanner(BaseHaScanner):
|
|||
"_new_info_callback",
|
||||
"_discovered_device_advertisement_datas",
|
||||
"_discovered_device_timestamps",
|
||||
"_connector",
|
||||
"_connectable",
|
||||
"_details",
|
||||
"_expire_seconds",
|
||||
"_storage",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
|
@ -109,12 +126,13 @@ class BaseHaRemoteScanner(BaseHaScanner):
|
|||
str, tuple[BLEDevice, AdvertisementData]
|
||||
] = {}
|
||||
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._expire_seconds = FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
||||
assert models.MANAGER is not None
|
||||
self._storage = models.MANAGER.storage
|
||||
if connectable:
|
||||
self._details["connector"] = connector
|
||||
self._expire_seconds = (
|
||||
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
||||
)
|
||||
|
@ -122,9 +140,40 @@ class BaseHaRemoteScanner(BaseHaScanner):
|
|||
@hass_callback
|
||||
def async_setup(self) -> CALLBACK_TYPE:
|
||||
"""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)
|
||||
)
|
||||
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:
|
||||
"""Expire old devices."""
|
||||
|
@ -222,7 +271,7 @@ class BaseHaRemoteScanner(BaseHaScanner):
|
|||
source=self.source,
|
||||
device=device,
|
||||
advertisement=advertisement_data,
|
||||
connectable=self._connectable,
|
||||
connectable=self.connectable,
|
||||
time=now,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -45,6 +45,7 @@ from .match import (
|
|||
ble_device_matches,
|
||||
)
|
||||
from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak
|
||||
from .storage import BluetoothStorage
|
||||
from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher
|
||||
from .util import async_load_history_from_system
|
||||
|
||||
|
@ -102,6 +103,7 @@ class BluetoothManager:
|
|||
hass: HomeAssistant,
|
||||
integration_matcher: IntegrationMatcher,
|
||||
bluetooth_adapters: BluetoothAdapters,
|
||||
storage: BluetoothStorage,
|
||||
) -> None:
|
||||
"""Init bluetooth manager."""
|
||||
self.hass = hass
|
||||
|
@ -128,6 +130,7 @@ class BluetoothManager:
|
|||
self._adapters: dict[str, AdapterDetails] = {}
|
||||
self._sources: dict[str, BaseHaScanner] = {}
|
||||
self._bluetooth_adapters = bluetooth_adapters
|
||||
self.storage = storage
|
||||
|
||||
@property
|
||||
def supports_passive_scan(self) -> bool:
|
||||
|
@ -196,12 +199,9 @@ class BluetoothManager:
|
|||
"""Set up the bluetooth manager."""
|
||||
await self._bluetooth_adapters.refresh()
|
||||
install_multiple_bleak_catcher()
|
||||
history = async_load_history_from_system(self._bluetooth_adapters)
|
||||
# Everything is connectable so it fall into both
|
||||
# buckets since the host system can only provide
|
||||
# connectable devices
|
||||
self._all_history = history.copy()
|
||||
self._connectable_history = history.copy()
|
||||
self._all_history, self._connectable_history = async_load_history_from_system(
|
||||
self._bluetooth_adapters, self.storage
|
||||
)
|
||||
self.async_setup_unavailable_tracking()
|
||||
|
||||
@hass_callback
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"requirements": [
|
||||
"bleak==0.19.2",
|
||||
"bleak-retry-connector==2.10.1",
|
||||
"bluetooth-adapters==0.12.0",
|
||||
"bluetooth-adapters==0.14.1",
|
||||
"bluetooth-auto-recovery==0.5.5",
|
||||
"bluetooth-data-tools==0.3.0",
|
||||
"dbus-fast==1.82.0"
|
||||
|
|
|
@ -132,6 +132,7 @@ class HaScanner(BaseHaScanner):
|
|||
"""Init bluetooth discovery."""
|
||||
source = address if address != DEFAULT_ADDRESS else adapter or SOURCE_LOCAL
|
||||
super().__init__(hass, source, adapter)
|
||||
self.connectable = True
|
||||
self.mode = mode
|
||||
self.adapter = adapter
|
||||
self._start_stop_lock = asyncio.Lock()
|
||||
|
|
|
@ -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 .models import BluetoothServiceInfoBleak
|
||||
from .storage import BluetoothStorage
|
||||
|
||||
|
||||
@callback
|
||||
def async_load_history_from_system(
|
||||
adapters: BluetoothAdapters,
|
||||
) -> dict[str, BluetoothServiceInfoBleak]:
|
||||
adapters: BluetoothAdapters, storage: BluetoothStorage
|
||||
) -> tuple[dict[str, BluetoothServiceInfoBleak], dict[str, BluetoothServiceInfoBleak]]:
|
||||
"""Load the device and advertisement_data history if available on the current system."""
|
||||
now = monotonic_time_coarse()
|
||||
return {
|
||||
address: BluetoothServiceInfoBleak(
|
||||
name=history.advertisement_data.local_name
|
||||
or history.device.name
|
||||
or history.device.address,
|
||||
address=history.device.address,
|
||||
rssi=history.advertisement_data.rssi,
|
||||
manufacturer_data=history.advertisement_data.manufacturer_data,
|
||||
service_data=history.advertisement_data.service_data,
|
||||
service_uuids=history.advertisement_data.service_uuids,
|
||||
source=history.source,
|
||||
device=history.device,
|
||||
advertisement=history.advertisement_data,
|
||||
connectable=False,
|
||||
time=now,
|
||||
)
|
||||
for address, history in adapters.history.items()
|
||||
}
|
||||
now_monotonic = monotonic_time_coarse()
|
||||
connectable_loaded_history: dict[str, BluetoothServiceInfoBleak] = {}
|
||||
all_loaded_history: dict[str, BluetoothServiceInfoBleak] = {}
|
||||
|
||||
# Restore local adapters
|
||||
for address, history in adapters.history.items():
|
||||
if (
|
||||
not (existing_all := connectable_loaded_history.get(address))
|
||||
or history.advertisement_data.rssi > existing_all.rssi
|
||||
):
|
||||
connectable_loaded_history[address] = all_loaded_history[
|
||||
address
|
||||
] = BluetoothServiceInfoBleak.from_device_and_advertisement_data(
|
||||
history.device,
|
||||
history.advertisement_data,
|
||||
history.source,
|
||||
now_monotonic,
|
||||
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:
|
||||
|
|
|
@ -6,7 +6,7 @@ from collections.abc import Callable
|
|||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Final
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from bleak import BleakClient, BleakError
|
||||
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 . import models
|
||||
from .models import HaBluetoothConnector
|
||||
|
||||
FILTER_UUIDS: Final = "UUIDs"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import BluetoothManager
|
||||
|
||||
|
||||
@dataclass
|
||||
class _HaWrappedBleakBackend:
|
||||
"""Wrap bleak backend to make it usable by Home Assistant."""
|
||||
|
@ -207,23 +210,27 @@ class HaBleakClientWrapper(BleakClient):
|
|||
|
||||
@hass_callback
|
||||
def _async_get_backend_for_ble_device(
|
||||
self, ble_device: BLEDevice
|
||||
self, manager: BluetoothManager, ble_device: BLEDevice
|
||||
) -> _HaWrappedBleakBackend | None:
|
||||
"""Get the backend for a BLEDevice."""
|
||||
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
|
||||
# its the client for this platform
|
||||
cls = get_platform_client_backend_type()
|
||||
return _HaWrappedBleakBackend(ble_device, cls)
|
||||
|
||||
connector: HaBluetoothConnector = details["connector"]
|
||||
source: str = details["source"]
|
||||
# Make sure the backend can connect to the device
|
||||
# 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 _HaWrappedBleakBackend(ble_device, connector.client)
|
||||
return _HaWrappedBleakBackend(ble_device, scanner.connector.client)
|
||||
|
||||
@hass_callback
|
||||
def _async_get_best_available_backend_and_device(
|
||||
|
@ -246,7 +253,7 @@ class HaBleakClientWrapper(BleakClient):
|
|||
reverse=True,
|
||||
):
|
||||
if backend := self._async_get_backend_for_ble_device(
|
||||
device_advertisement_data[0]
|
||||
models.MANAGER, device_advertisement_data[0]
|
||||
):
|
||||
return backend
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ awesomeversion==22.9.0
|
|||
bcrypt==3.1.7
|
||||
bleak-retry-connector==2.10.1
|
||||
bleak==0.19.2
|
||||
bluetooth-adapters==0.12.0
|
||||
bluetooth-adapters==0.14.1
|
||||
bluetooth-auto-recovery==0.5.5
|
||||
bluetooth-data-tools==0.3.0
|
||||
certifi>=2021.5.30
|
||||
|
@ -21,7 +21,7 @@ cryptography==38.0.3
|
|||
dbus-fast==1.82.0
|
||||
fnvhash==0.1.0
|
||||
hass-nabucasa==0.61.0
|
||||
home-assistant-bluetooth==1.8.1
|
||||
home-assistant-bluetooth==1.9.0
|
||||
home-assistant-frontend==20221208.0
|
||||
httpx==0.23.1
|
||||
ifaddr==0.1.7
|
||||
|
|
|
@ -36,7 +36,7 @@ dependencies = [
|
|||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.23.1",
|
||||
"home-assistant-bluetooth==1.8.1",
|
||||
"home-assistant-bluetooth==1.9.0",
|
||||
"ifaddr==0.1.7",
|
||||
"jinja2==3.1.2",
|
||||
"lru-dict==1.1.8",
|
||||
|
|
|
@ -11,7 +11,7 @@ bcrypt==3.1.7
|
|||
certifi>=2021.5.30
|
||||
ciso8601==2.2.0
|
||||
httpx==0.23.1
|
||||
home-assistant-bluetooth==1.8.1
|
||||
home-assistant-bluetooth==1.9.0
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.1.2
|
||||
lru-dict==1.1.8
|
||||
|
|
|
@ -450,7 +450,7 @@ bluemaestro-ble==0.2.0
|
|||
# bluepy==1.3.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.12.0
|
||||
bluetooth-adapters==0.14.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==0.5.5
|
||||
|
|
|
@ -364,7 +364,7 @@ blinkpy==0.19.2
|
|||
bluemaestro-ble==0.2.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.12.0
|
||||
bluetooth-adapters==0.14.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==0.5.5
|
||||
|
|
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.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 (
|
||||
CONNECTABLE_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
|
||||
|
||||
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):
|
||||
|
@ -72,7 +79,7 @@ async def test_remote_scanner(hass, enable_bluetooth):
|
|||
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
||||
)
|
||||
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True)
|
||||
scanner.async_setup()
|
||||
unsetup = scanner.async_setup()
|
||||
cancel = manager.async_register_scanner(scanner, True)
|
||||
|
||||
scanner.inject_advertisement(switchbot_device, switchbot_device_adv)
|
||||
|
@ -103,6 +110,7 @@ async def test_remote_scanner(hass, enable_bluetooth):
|
|||
}
|
||||
|
||||
cancel()
|
||||
unsetup()
|
||||
|
||||
|
||||
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),
|
||||
)
|
||||
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True)
|
||||
scanner.async_setup()
|
||||
unsetup = scanner.async_setup()
|
||||
cancel = manager.async_register_scanner(scanner, True)
|
||||
|
||||
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
|
||||
|
||||
cancel()
|
||||
unsetup()
|
||||
|
||||
|
||||
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),
|
||||
)
|
||||
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False)
|
||||
scanner.async_setup()
|
||||
unsetup = scanner.async_setup()
|
||||
cancel = manager.async_register_scanner(scanner, True)
|
||||
|
||||
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
|
||||
|
||||
cancel()
|
||||
unsetup()
|
||||
|
||||
|
||||
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),
|
||||
)
|
||||
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False)
|
||||
scanner.async_setup()
|
||||
unsetup = scanner.async_setup()
|
||||
cancel = manager.async_register_scanner(scanner, True)
|
||||
|
||||
with scanner.connecting():
|
||||
|
@ -327,3 +337,60 @@ async def test_base_scanner_connecting_behavior(hass, enable_bluetooth):
|
|||
assert devices[0].name == "wohand"
|
||||
|
||||
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
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth import storage
|
||||
from homeassistant.components.bluetooth.manager import (
|
||||
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.json import json_loads
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import (
|
||||
|
@ -22,6 +24,8 @@ from . import (
|
|||
inject_advertisement_with_time_and_source_connectable,
|
||||
)
|
||||
|
||||
from tests.common import load_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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
|
||||
|
||||
|
||||
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(
|
||||
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
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bluetooth import BaseHaScanner, HaBluetoothConnector
|
||||
from homeassistant.components.bluetooth import (
|
||||
BaseHaRemoteScanner,
|
||||
BaseHaScanner,
|
||||
HaBluetoothConnector,
|
||||
)
|
||||
from homeassistant.components.bluetooth.wrappers import (
|
||||
HaBleakClientWrapper,
|
||||
HaBleakScannerWrapper,
|
||||
|
@ -57,6 +61,67 @@ async def test_wrapped_bleak_client_set_disconnected_callback_before_connected(
|
|||
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(
|
||||
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",
|
||||
"wohand",
|
||||
{
|
||||
"connector": HaBluetoothConnector(
|
||||
MockBleakClient, "mock_bleak_client", lambda: True
|
||||
),
|
||||
"source": "esp32_has_connection_slot",
|
||||
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||
},
|
||||
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
|
||||
)
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
class FakeScanner(BaseHaScanner):
|
||||
class FakeScanner(BaseHaRemoteScanner):
|
||||
@property
|
||||
def discovered_devices(self) -> list[BLEDevice]:
|
||||
"""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 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)
|
||||
|
||||
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)
|
||||
with patch("bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect"):
|
||||
await client.connect()
|
||||
assert client.is_connected is True
|
||||
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_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
|
||||
):
|
||||
"""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()
|
||||
|
||||
switchbot_proxy_device_no_connection_slot = BLEDevice(
|
||||
"44:44:33:11:23:45",
|
||||
"wohand",
|
||||
{
|
||||
"connector": HaBluetoothConnector(
|
||||
MockBleakClient, "mock_bleak_client", lambda: False
|
||||
),
|
||||
"source": "esp32",
|
||||
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||
},
|
||||
rssi=-30,
|
||||
|
@ -174,17 +246,17 @@ async def test_ble_device_with_proxy_client_out_of_connections(
|
|||
await client.disconnect()
|
||||
|
||||
|
||||
async def test_ble_device_with_proxy_clear_cache(hass, enable_bluetooth, one_adapter):
|
||||
"""Test we can clear cache on the proxy."""
|
||||
async def test_ble_device_with_proxy_client_out_of_connections(
|
||||
hass, enable_bluetooth, one_adapter
|
||||
):
|
||||
"""Test handling all scanners are out of connection slots."""
|
||||
manager = _get_manager()
|
||||
|
||||
switchbot_proxy_device_with_connection_slot = BLEDevice(
|
||||
switchbot_proxy_device_no_connection_slot = BLEDevice(
|
||||
"44:44:33:11:23:45",
|
||||
"wohand",
|
||||
{
|
||||
"connector": HaBluetoothConnector(
|
||||
MockBleakClient, "mock_bleak_client", lambda: True
|
||||
),
|
||||
"source": "esp32",
|
||||
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||
},
|
||||
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"}
|
||||
)
|
||||
|
||||
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
|
||||
def discovered_devices(self) -> list[BLEDevice]:
|
||||
"""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 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)
|
||||
inject_advertisement_with_source(
|
||||
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",
|
||||
"wohand",
|
||||
{
|
||||
"connector": HaBluetoothConnector(
|
||||
MockBleakClient, "mock_bleak_client", lambda: False
|
||||
),
|
||||
"source": "esp32_no_connection_slot",
|
||||
"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",
|
||||
"wohand",
|
||||
{
|
||||
"connector": HaBluetoothConnector(
|
||||
MockBleakClient, "mock_bleak_client", lambda: True
|
||||
),
|
||||
"source": "esp32_has_connection_slot",
|
||||
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||
},
|
||||
rssi=-40,
|
||||
|
@ -299,7 +431,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
|
|||
"esp32_no_connection_slot",
|
||||
)
|
||||
|
||||
class FakeScanner(BaseHaScanner):
|
||||
class FakeScanner(BaseHaRemoteScanner):
|
||||
@property
|
||||
def discovered_devices(self) -> list[BLEDevice]:
|
||||
"""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 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)
|
||||
assert manager.async_discovered_devices(True) == [
|
||||
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",
|
||||
"wohand_no_connection_slot",
|
||||
{
|
||||
"connector": HaBluetoothConnector(
|
||||
MockBleakClient, "mock_bleak_client", lambda: False
|
||||
),
|
||||
"source": "esp32_no_connection_slot",
|
||||
"path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
|
||||
},
|
||||
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",
|
||||
"wohand_has_connection_slot",
|
||||
{
|
||||
"connector": HaBluetoothConnector(
|
||||
MockBleakClient, "mock_bleak_client", lambda: True
|
||||
),
|
||||
"source": "esp32_has_connection_slot",
|
||||
"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",
|
||||
)
|
||||
|
||||
class FakeScanner(BaseHaScanner):
|
||||
class FakeScanner(BaseHaRemoteScanner):
|
||||
@property
|
||||
def discovered_devices(self) -> list[BLEDevice]:
|
||||
"""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 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)
|
||||
assert manager.async_discovered_devices(True) == [
|
||||
switchbot_proxy_device_no_connection_slot
|
||||
|
|
|
@ -1092,6 +1092,9 @@ async def mock_enable_bluetooth(
|
|||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
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")
|
||||
|
|
Loading…
Reference in New Issue