diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 8386178f459..e6b66ece490 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -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 diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index 3dffff9e153..0c847e41f1c 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -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, ) ) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 534bd636355..fa92ecdba95 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -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 diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 6e41523f33d..95098549dee 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -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" diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 09032715c74..1881c6ee041 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -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() diff --git a/homeassistant/components/bluetooth/storage.py b/homeassistant/components/bluetooth/storage.py new file mode 100644 index 00000000000..e3cb946ccee --- /dev/null +++ b/homeassistant/components/bluetooth/storage.py @@ -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) diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index c2336dd7af0..a7ae2e7daca 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -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: diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index b1b06d43e31..91482b2634e 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -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 diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 45c91a9815c..a34187c03a6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 37e6f66f687..16a8b3aed7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/requirements.txt b/requirements.txt index 6c878206b07..8831ce4cac4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index ad9c3726be4..f8d8d625ee8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eed6a539a9f..1694332b34e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/bluetooth/fixtures/bluetooth.remote_scanners b/tests/components/bluetooth/fixtures/bluetooth.remote_scanners new file mode 100644 index 00000000000..f503752151c --- /dev/null +++ b/tests/components/bluetooth/fixtures/bluetooth.remote_scanners @@ -0,0 +1,1384 @@ +{ + "version": 1, + "minor_version": 1, + "key": "bluetooth.remote_scanners", + "data": { + "atom-bluetooth-proxy-ceaac4": { + "connectable": true, + "expire_seconds": 195, + "discovered_device_advertisement_datas": { + "51:8E:5B:A8:44:8D": { + "device": { + "address": "51:8E:5B:A8:44:8D", + "name": "98E7829A01E23895E9", + "rssi": -80, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "98E7829A01E23895E9", + "manufacturer_data": { + "1447": "051001000000000000022200ca" + }, + "service_data": {}, + "service_uuids": [ + "0000fe07-0000-1000-8000-00805f9b34fb" + ], + "rssi": -80, + "tx_power": -127, + "platform_data": [] + } + }, + "16:9A:28:FC:D5:A8": { + "device": { + "address": "16:9A:28:FC:D5:A8", + "name": "", + "rssi": -77, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0906036dc0a86b7e130c0a59cff1970c9f010401030c" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -77, + "tx_power": -127, + "platform_data": [] + } + }, + "08:3A:F2:1E:32:69": { + "device": { + "address": "08:3A:F2:1E:32:69", + "name": "LOOKin_98F3320D", + "rssi": -91, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F3320D", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -91, + "tx_power": -127, + "platform_data": [] + } + }, + "34:AB:95:85:6E:F1": { + "device": { + "address": "34:AB:95:85:6E:F1", + "name": "LOOKin_98F330B5", + "rssi": -96, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F330B5", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -96, + "tx_power": -127, + "platform_data": [] + } + }, + "D8:2E:AD:CD:0D:85": { + "device": { + "address": "D8:2E:AD:CD:0D:85", + "name": "", + "rssi": -81, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "89": "d82eadcd0d85" + }, + "service_data": { + "00000d00-0000-1000-8000-00805f9b34fb": "481061" + }, + "service_uuids": [ + "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + ], + "rssi": -81, + "tx_power": -127, + "platform_data": [] + } + }, + "D4:B2:A1:7C:42:17": { + "device": { + "address": "D4:B2:A1:7C:42:17", + "name": "RZSS", + "rssi": -70, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "RZSS", + "manufacturer_data": { + "1033": "0c246232" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -70, + "tx_power": -127, + "platform_data": [] + } + }, + "D8:EF:2F:41:B1:34": { + "device": { + "address": "D8:EF:2F:41:B1:34", + "name": "RZSS", + "rssi": -64, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "RZSS", + "manufacturer_data": { + "1033": "0c246064" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -64, + "tx_power": -127, + "platform_data": [] + } + }, + "60:55:F9:29:FA:2A": { + "device": { + "address": "60:55:F9:29:FA:2A", + "name": "", + "rssi": -62, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "2409": "6055f929fa2a010000000000" + }, + "service_data": { + "0000fd3d-0000-1000-8000-00805f9b34fb": "6a0064" + }, + "service_uuids": [], + "rssi": -62, + "tx_power": -127, + "platform_data": [] + } + }, + "F3:33:CE:84:58:4A": { + "device": { + "address": "F3:33:CE:84:58:4A", + "name": "98E7822ED4F830AEE9", + "rssi": -78, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "98E7822ED4F830AEE9", + "manufacturer_data": { + "1447": "051001000000000000022200ca" + }, + "service_data": {}, + "service_uuids": [ + "0000fe07-0000-1000-8000-00805f9b34fb" + ], + "rssi": -78, + "tx_power": -127, + "platform_data": [] + } + }, + "CB:39:CD:C4:3D:46": { + "device": { + "address": "CB:39:CD:C4:3D:46", + "name": "", + "rssi": -84, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "2409": "cb39cdc43d46c02e5c55ffff84" + }, + "service_data": { + "0000fd3d-0000-1000-8000-00805f9b34fb": "64005a445c55ffff84" + }, + "service_uuids": [], + "rssi": -84, + "tx_power": -127, + "platform_data": [] + } + }, + "E2:4B:AB:57:4F:CC": { + "device": { + "address": "E2:4B:AB:57:4F:CC", + "name": "", + "rssi": -73, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "89": "e24bab574fcc" + }, + "service_data": { + "00000d00-0000-1000-8000-00805f9b34fb": "4810d7" + }, + "service_uuids": [ + "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + ], + "rssi": -73, + "tx_power": -127, + "platform_data": [] + } + }, + "7F:B3:F7:28:DD:5B": { + "device": { + "address": "7F:B3:F7:28:DD:5B", + "name": "", + "rssi": -80, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "1005229829e5ab" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -80, + "tx_power": -127, + "platform_data": [] + } + }, + "B4:E8:42:51:76:67": { + "device": { + "address": "B4:E8:42:51:76:67", + "name": "LEDnetWF010097517666", + "rssi": -86, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LEDnetWF010097517666", + "manufacturer_data": { + "23041": "5205b4e84251766600971603000000000000000000000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -86, + "tx_power": -127, + "platform_data": [] + } + }, + "EA:CF:16:0C:16:11": { + "device": { + "address": "EA:CF:16:0C:16:11", + "name": "", + "rssi": -79, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "12020003" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -79, + "tx_power": -127, + "platform_data": [] + } + }, + "08:3A:F2:1E:28:F1": { + "device": { + "address": "08:3A:F2:1E:28:F1", + "name": "LOOKin_98F33208", + "rssi": -86, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F33208", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -86, + "tx_power": -127, + "platform_data": [] + } + }, + "08:3A:F2:1E:2B:2D": { + "device": { + "address": "08:3A:F2:1E:2B:2D", + "name": "LOOKin_98F330F3", + "rssi": -80, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F330F3", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -80, + "tx_power": -127, + "platform_data": [] + } + }, + "3D:AF:40:AD:B3:8B": { + "device": { + "address": "3D:AF:40:AD:B3:8B", + "name": "", + "rssi": -88, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0906036fc0a86b3e130c0a98ce54cc75e4010401030c" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -88, + "tx_power": -127, + "platform_data": [] + } + }, + "61:25:DF:19:90:98": { + "device": { + "address": "61:25:DF:19:90:98", + "name": "", + "rssi": -74, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "1007321fcbeaadd248" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -74, + "tx_power": -127, + "platform_data": [] + } + }, + "34:AB:95:85:6E:F9": { + "device": { + "address": "34:AB:95:85:6E:F9", + "name": "LOOKin_98F330B4", + "rssi": -91, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F330B4", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -91, + "tx_power": -127, + "platform_data": [] + } + }, + "54:E6:1B:F0:20:97": { + "device": { + "address": "54:E6:1B:F0:20:97", + "name": "", + "rssi": -54, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0f08c00ad7f8b400440c10020100" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -54, + "tx_power": -127, + "platform_data": [] + } + }, + "41:CC:AF:67:18:EE": { + "device": { + "address": "41:CC:AF:67:18:EE", + "name": "", + "rssi": -80, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0c0e089c6a11d70cc17e3e52928b96b71006411d75808748" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -80, + "tx_power": -127, + "platform_data": [] + } + }, + "8C:EA:48:4D:93:3B": { + "device": { + "address": "8C:EA:48:4D:93:3B", + "name": "", + "rssi": -98, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "117": "42040180668cea484d933b8eea484d933a01000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -98, + "tx_power": -127, + "platform_data": [] + } + }, + "C5:2D:5E:AB:52:76": { + "device": { + "address": "C5:2D:5E:AB:52:76", + "name": "", + "rssi": -66, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "89": "c52d5eab5276" + }, + "service_data": { + "00000d00-0000-1000-8000-00805f9b34fb": "481063" + }, + "service_uuids": [ + "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + ], + "rssi": -66, + "tx_power": -127, + "platform_data": [] + } + }, + "C0:49:EF:8C:1F:86": { + "device": { + "address": "C0:49:EF:8C:1F:86", + "name": "ShellyPlugUS-C049EF8C1F84", + "rssi": -36, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "ShellyPlugUS-C049EF8C1F84", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [], + "rssi": -36, + "tx_power": -127, + "platform_data": [] + } + }, + "F8:04:2E:E1:71:9D": { + "device": { + "address": "F8:04:2E:E1:71:9D", + "name": "", + "rssi": -90, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "117": "42040180a0f8042ee1719dfa042ee1719c01000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -90, + "tx_power": -127, + "platform_data": [] + } + }, + "C6:D5:B5:C1:2A:22": { + "device": { + "address": "C6:D5:B5:C1:2A:22", + "name": "", + "rssi": -73, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "89": "c6d5b5c12a22" + }, + "service_data": { + "00000d00-0000-1000-8000-00805f9b34fb": "4810e1" + }, + "service_uuids": [ + "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + ], + "rssi": -73, + "tx_power": -127, + "platform_data": [] + } + }, + "31:FA:70:5F:9C:AC": { + "device": { + "address": "31:FA:70:5F:9C:AC", + "name": "", + "rssi": -78, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0906038ec0a86a5e" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -78, + "tx_power": -127, + "platform_data": [] + } + }, + "D5:C1:1D:1A:23:57": { + "device": { + "address": "D5:C1:1D:1A:23:57", + "name": "RZSS", + "rssi": -70, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "RZSS", + "manufacturer_data": { + "1033": "0c246200" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -70, + "tx_power": -127, + "platform_data": [] + } + }, + "2C:BF:C0:1C:D7:4B": { + "device": { + "address": "2C:BF:C0:1C:D7:4B", + "name": "", + "rssi": -89, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0906035cc0a86a9d" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -89, + "tx_power": -127, + "platform_data": [] + } + }, + "B4:E8:42:DB:12:85": { + "device": { + "address": "B4:E8:42:DB:12:85", + "name": "LEDnetWF010097DB1284", + "rssi": -66, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LEDnetWF010097DB1284", + "manufacturer_data": { + "23041": "5205b4e842db128400971603000000000000000000000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -66, + "tx_power": -127, + "platform_data": [] + } + }, + "6E:48:DA:52:2F:13": { + "device": { + "address": "6E:48:DA:52:2F:13", + "name": "", + "rssi": -92, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "10051a1c2e6061" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -92, + "tx_power": -127, + "platform_data": [] + } + }, + "34:AB:95:85:66:6D": { + "device": { + "address": "34:AB:95:85:66:6D", + "name": "LOOKin_98F33163", + "rssi": -91, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F33163", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -91, + "tx_power": -127, + "platform_data": [] + } + }, + "F8:04:2E:E1:9F:19": { + "device": { + "address": "F8:04:2E:E1:9F:19", + "name": "", + "rssi": -92, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "117": "42040180a0f8042ee19f19fa042ee19f1801000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -92, + "tx_power": -127, + "platform_data": [] + } + }, + "68:3C:69:9E:A1:18": { + "device": { + "address": "68:3C:69:9E:A1:18", + "name": "", + "rssi": -78, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0c0e089c6a11d70cc17e3e52928b96b71006411d75808748" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -78, + "tx_power": -127, + "platform_data": [] + } + }, + "07:68:27:D9:15:40": { + "device": { + "address": "07:68:27:D9:15:40", + "name": "", + "rssi": -74, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": {}, + "service_data": { + "0000fd6f-0000-1000-8000-00805f9b34fb": "1b8fb635708f1ef520edb36597fb0586b58f7214" + }, + "service_uuids": [ + "0000fd6f-0000-1000-8000-00805f9b34fb" + ], + "rssi": -74, + "tx_power": -127, + "platform_data": [] + } + }, + "F5:C7:51:3D:B7:33": { + "device": { + "address": "F5:C7:51:3D:B7:33", + "name": "RZSS", + "rssi": -81, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "RZSS", + "manufacturer_data": { + "1033": "0c246164" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -81, + "tx_power": -127, + "platform_data": [] + } + }, + "E0:1F:A6:3A:C5:66": { + "device": { + "address": "E0:1F:A6:3A:C5:66", + "name": "RZSS", + "rssi": -90, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "RZSS", + "manufacturer_data": { + "1033": "0c246200" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -90, + "tx_power": -127, + "platform_data": [] + } + }, + "78:9C:85:08:E6:2A": { + "device": { + "address": "78:9C:85:08:E6:2A", + "name": "M1029QJ", + "rssi": -73, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "M1029QJ", + "manufacturer_data": { + "76": "06310080e7146a373406009c2104023cb9eb0e" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -73, + "tx_power": -127, + "platform_data": [] + } + }, + "4F:EB:42:E2:57:4A": { + "device": { + "address": "4F:EB:42:E2:57:4A", + "name": "a25bd0d9", + "rssi": -86, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "a25bd0d9", + "manufacturer_data": { + "1015": "1a00000000004d617569204f776c" + }, + "service_data": {}, + "service_uuids": [ + "39d6b333-adad-45c8-b6ee-eac6c4cd0000" + ], + "rssi": -86, + "tx_power": -127, + "platform_data": [] + } + }, + "A8:51:AB:8A:5A:84": { + "device": { + "address": "A8:51:AB:8A:5A:84", + "name": "", + "rssi": -94, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "1005021484867e" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -94, + "tx_power": -127, + "platform_data": [] + } + }, + "5E:5C:AC:CF:CD:2D": { + "device": { + "address": "5E:5C:AC:CF:CD:2D", + "name": "", + "rssi": -82, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0f059000da640210022d04" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -82, + "tx_power": -127, + "platform_data": [] + } + }, + "0A:73:BD:22:FE:E2": { + "device": { + "address": "0A:73:BD:22:FE:E2", + "name": "", + "rssi": -92, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "09060318c0a86bef" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -92, + "tx_power": -127, + "platform_data": [] + } + }, + "B4:E8:42:9E:A4:F5": { + "device": { + "address": "B4:E8:42:9E:A4:F5", + "name": "LEDnetWF0100979EA4F4", + "rssi": -82, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LEDnetWF0100979EA4F4", + "manufacturer_data": { + "23041": "5202b4e8429ea4f400970c03010203040506070809a1a2a3a4a5a6" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -82, + "tx_power": -127, + "platform_data": [] + } + }, + "B4:E8:42:53:56:F9": { + "device": { + "address": "B4:E8:42:53:56:F9", + "name": "LEDnetWF0100975356F8", + "rssi": -98, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LEDnetWF0100975356F8", + "manufacturer_data": { + "23041": "5205b4e8425356f800971603000000000000000000000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -98, + "tx_power": -127, + "platform_data": [] + } + }, + "D4:2C:41:97:56:6E": { + "device": { + "address": "D4:2C:41:97:56:6E", + "name": "RZSS", + "rssi": -82, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "RZSS", + "manufacturer_data": { + "1033": "0c245f32" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -82, + "tx_power": -127, + "platform_data": [] + } + }, + "64:04:FF:2C:E3:55": { + "device": { + "address": "64:04:FF:2C:E3:55", + "name": "", + "rssi": -88, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0f0590007f5fd910022504" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -88, + "tx_power": -127, + "platform_data": [] + } + }, + "A4:C1:38:BD:91:BB": { + "device": { + "address": "A4:C1:38:BD:91:BB", + "name": "LYWSD03MMC", + "rssi": -78, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LYWSD03MMC", + "manufacturer_data": {}, + "service_data": { + "0000fe95-0000-1000-8000-00805f9b34fb": "30585b055bbb91bd38c1a408" + }, + "service_uuids": [], + "rssi": -78, + "tx_power": -127, + "platform_data": [] + } + }, + "B4:E8:42:DA:BA:23": { + "device": { + "address": "B4:E8:42:DA:BA:23", + "name": "LEDnetWF010097DABA22", + "rssi": -92, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LEDnetWF010097DABA22", + "manufacturer_data": { + "23041": "5205b4e842daba2200971603000000000000000000000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -92, + "tx_power": -127, + "platform_data": [] + } + }, + "E3:A5:63:3E:5E:23": { + "device": { + "address": "E3:A5:63:3E:5E:23", + "name": "", + "rssi": -70, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "12020003" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -70, + "tx_power": -127, + "platform_data": [] + } + }, + "68:E3:C2:4D:85:EE": { + "device": { + "address": "68:E3:C2:4D:85:EE", + "name": "", + "rssi": -97, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "10051a1c53afb1" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -97, + "tx_power": -127, + "platform_data": [] + } + }, + "B4:E8:42:52:4B:AD": { + "device": { + "address": "B4:E8:42:52:4B:AD", + "name": "LEDnetWF010097524BAC", + "rssi": -84, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LEDnetWF010097524BAC", + "manufacturer_data": { + "23041": "5205b4e842524bac00971603000000000000000000000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -84, + "tx_power": -127, + "platform_data": [] + } + }, + "58:2D:34:60:D5:CD": { + "device": { + "address": "58:2D:34:60:D5:CD", + "name": "Qingping Motion & Light", + "rssi": -83, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "Qingping Motion & Light", + "manufacturer_data": {}, + "service_data": { + "0000fe95-0000-1000-8000-00805f9b34fb": "3058830a02cdd560342d5808", + "0000fdcd-0000-1000-8000-00805f9b34fb": "4812cdd560342d580804010f00000f0166" + }, + "service_uuids": [], + "rssi": -83, + "tx_power": -127, + "platform_data": [] + } + }, + "72:90:B0:53:F1:15": { + "device": { + "address": "72:90:B0:53:F1:15", + "name": "", + "rssi": -94, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0f059000dc161910022d04" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -94, + "tx_power": -127, + "platform_data": [] + } + }, + "D5:1C:FB:39:78:56": { + "device": { + "address": "D5:1C:FB:39:78:56", + "name": "", + "rssi": -93, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "89": "d51cfb397856" + }, + "service_data": { + "00000d00-0000-1000-8000-00805f9b34fb": "4890d7" + }, + "service_uuids": [ + "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + ], + "rssi": -93, + "tx_power": -127, + "platform_data": [] + } + }, + "08:3A:F2:1E:2B:05": { + "device": { + "address": "08:3A:F2:1E:2B:05", + "name": "LOOKin_98F330C2", + "rssi": -98, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F330C2", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -98, + "tx_power": -127, + "platform_data": [] + } + }, + "FA:8F:F4:A8:9E:BC": { + "device": { + "address": "FA:8F:F4:A8:9E:BC", + "name": "", + "rssi": -92, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "12025400" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -92, + "tx_power": -127, + "platform_data": [] + } + }, + "EB:0B:36:35:6F:A4": { + "device": { + "address": "EB:0B:36:35:6F:A4", + "name": "", + "rssi": -90, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "12199021f9370af25a5ec7825f703a4ac47a7b76c08b5695e101ba" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -90, + "tx_power": -127, + "platform_data": [] + } + }, + "AC:67:B2:6A:18:2A": { + "device": { + "address": "AC:67:B2:6A:18:2A", + "name": "", + "rssi": -68, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "741": "ac67b26a182a" + }, + "service_data": { + "00000d00-0000-1000-8000-00805f9b34fb": "65000000613d7300" + }, + "service_uuids": [ + "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + ], + "rssi": -68, + "tx_power": -127, + "platform_data": [] + } + } + }, + "discovered_device_timestamps": { + "51:8E:5B:A8:44:8D": 1670722715.9919367, + "16:9A:28:FC:D5:A8": 1670722719.3087196, + "08:3A:F2:1E:32:69": 1670722719.1660142, + "34:AB:95:85:6E:F1": 1670722720.2908847, + "D8:2E:AD:CD:0D:85": 1670722720.2915154, + "D4:B2:A1:7C:42:17": 1670722719.0863516, + "D8:EF:2F:41:B1:34": 1670722718.7625487, + "60:55:F9:29:FA:2A": 1670722720.291449, + "F3:33:CE:84:58:4A": 1670722720.291406, + "CB:39:CD:C4:3D:46": 1670722715.599468, + "E2:4B:AB:57:4F:CC": 1670722720.291419, + "7F:B3:F7:28:DD:5B": 1670722718.9691222, + "B4:E8:42:51:76:67": 1670722710.6735754, + "EA:CF:16:0C:16:11": 1670722715.9919817, + "08:3A:F2:1E:28:F1": 1670722720.291503, + "08:3A:F2:1E:2B:2D": 1670722720.2914848, + "3D:AF:40:AD:B3:8B": 1670722718.85895, + "61:25:DF:19:90:98": 1670722717.941397, + "34:AB:95:85:6E:F9": 1670722718.7632113, + "54:E6:1B:F0:20:97": 1670722717.1185102, + "41:CC:AF:67:18:EE": 1670722720.2912233, + "8C:EA:48:4D:93:3B": 1670722720.2910836, + "C5:2D:5E:AB:52:76": 1670722718.558827, + "C0:49:EF:8C:1F:86": 1670722717.9688299, + "F8:04:2E:E1:71:9D": 1670722717.526199, + "C6:D5:B5:C1:2A:22": 1670722717.0176404, + "31:FA:70:5F:9C:AC": 1670722720.2911327, + "D5:C1:1D:1A:23:57": 1670722717.639859, + "2C:BF:C0:1C:D7:4B": 1670722720.2914364, + "B4:E8:42:DB:12:85": 1670722719.0764725, + "6E:48:DA:52:2F:13": 1670722720.2913177, + "34:AB:95:85:66:6D": 1670722720.2912607, + "F8:04:2E:E1:9F:19": 1670722720.2911522, + "68:3C:69:9E:A1:18": 1670722719.1658278, + "07:68:27:D9:15:40": 1670722720.2912369, + "F5:C7:51:3D:B7:33": 1670722720.2911801, + "E0:1F:A6:3A:C5:66": 1670722713.7567363, + "78:9C:85:08:E6:2A": 1670722712.5362735, + "4F:EB:42:E2:57:4A": 1670722716.5097647, + "A8:51:AB:8A:5A:84": 1670722711.9280567, + "5E:5C:AC:CF:CD:2D": 1670722716.2974086, + "0A:73:BD:22:FE:E2": 1670722720.2914622, + "B4:E8:42:9E:A4:F5": 1670722718.5590498, + "B4:E8:42:53:56:F9": 1670722712.4074357, + "D4:2C:41:97:56:6E": 1670722718.3550117, + "64:04:FF:2C:E3:55": 1670722718.1470506, + "A4:C1:38:BD:91:BB": 1670722712.7268498, + "B4:E8:42:DA:BA:23": 1670722720.2910104, + "E3:A5:63:3E:5E:23": 1670722713.8445594, + "68:E3:C2:4D:85:EE": 1670722713.8488224, + "B4:E8:42:52:4B:AD": 1670722714.0646405, + "58:2D:34:60:D5:CD": 1670722715.377663, + "72:90:B0:53:F1:15": 1670722718.1468875, + "D5:1C:FB:39:78:56": 1670722715.2835894, + "08:3A:F2:1E:2B:05": 1670722719.0629613, + "FA:8F:F4:A8:9E:BC": 1670722717.22681, + "EB:0B:36:35:6F:A4": 1670722717.6297317, + "AC:67:B2:6A:18:2A": 1670722719.086469 + } + } + } +} \ No newline at end of file diff --git a/tests/components/bluetooth/fixtures/bluetooth.remote_scanners.corrupt b/tests/components/bluetooth/fixtures/bluetooth.remote_scanners.corrupt new file mode 100644 index 00000000000..71d084d33a3 --- /dev/null +++ b/tests/components/bluetooth/fixtures/bluetooth.remote_scanners.corrupt @@ -0,0 +1,1380 @@ +{ + "version": 1, + "minor_version": 1, + "key": "bluetooth.remote_scanners", + "data": { + "atom-bluetooth-proxy-ceaac4": { + "connectable": true, + "expire_seconds": 195, + "discovered_device_advertisement_datas": { + "51:8E:5B:A8:44:8D": { + "device": { + "address": "51:8E:5B:A8:44:8D", + "name": "98E7829A01E23895E9", + "rssi": -80, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "98E7829A01E23895E9", + "manufacturer_data": { + "1447": "051001000000000000022200ca" + }, + "service_data": {}, + "service_uuids": [ + "0000fe07-0000-1000-8000-00805f9b34fb" + ], + "rssi": -80, + "tx_power": -127, + "platform_data": [] + } + }, + "16:9A:28:FC:D5:A8": { + "device": { + "address": "16:9A:28:FC:D5:A8", + "name": "", + "rssi": -77, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0906036dc0a86b7e130c0a59cff1970c9f010401030c" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -77, + "tx_power": -127, + "platform_data": [] + } + }, + "08:3A:F2:1E:32:69": { + "device": { + "address": "08:3A:F2:1E:32:69", + "name": "LOOKin_98F3320D", + "rssi": -91, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F3320D", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -91, + "tx_power": -127, + "platform_data": [] + } + }, + "34:AB:95:85:6E:F1": { + "device": { + "address": "34:AB:95:85:6E:F1", + "name": "LOOKin_98F330B5", + "rssi": -96, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F330B5", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -96, + "tx_power": -127, + "platform_data": [] + } + }, + "D8:2E:AD:CD:0D:85": { + "device": { + "address": "D8:2E:AD:CD:0D:85", + "name": "", + "rssi": -81, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "89": "d82eadcd0d85" + }, + "service_data": { + "00000d00-0000-1000-8000-00805f9b34fb": "481061" + }, + "service_uuids": [ + "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + ], + "rssi": -81, + "tx_power": -127, + "platform_data": [] + } + }, + "D4:B2:A1:7C:42:17": { + "device": { + "address": "D4:B2:A1:7C:42:17", + "name": "RZSS", + "rssi": -70, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "RZSS", + "manufacturer_data": { + "1033": "0c246232" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -70, + "tx_power": -127, + "platform_data": [] + } + }, + "D8:EF:2F:41:B1:34": { + "device": { + "address": "D8:EF:2F:41:B1:34", + "name": "RZSS", + "rssi": -64, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "RZSS", + "manufacturer_data": { + "1033": "0c246064" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -64, + "tx_power": -127, + "platform_data": [] + } + }, + "60:55:F9:29:FA:2A": { + "device": { + "address": "60:55:F9:29:FA:2A", + "name": "", + "rssi": -62, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "2409": "6055f929fa2a010000000000" + }, + "service_data": { + "0000fd3d-0000-1000-8000-00805f9b34fb": "6a0064" + }, + "service_uuids": [], + "rssi": -62, + "tx_power": -127, + "platform_data": [] + } + }, + "F3:33:CE:84:58:4A": { + "device": { + "address": "F3:33:CE:84:58:4A", + "name": "98E7822ED4F830AEE9", + "rssi": -78, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "98E7822ED4F830AEE9", + "manufacturer_data": { + "1447": "051001000000000000022200ca" + }, + "service_data": {}, + "service_uuids": [ + "0000fe07-0000-1000-8000-00805f9b34fb" + ], + "rssi": -78, + "tx_power": -127, + "platform_data": [] + } + }, + "CB:39:CD:C4:3D:46": { + "device": { + "address": "CB:39:CD:C4:3D:46", + "name": "", + "rssi": -84, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "2409": "cb39cdc43d46c02e5c55ffff84" + }, + "service_data": { + "0000fd3d-0000-1000-8000-00805f9b34fb": "64005a445c55ffff84" + }, + "service_uuids": [], + "rssi": -84, + "tx_power": -127, + "platform_data": [] + } + }, + "E2:4B:AB:57:4F:CC": { + "device": { + "address": "E2:4B:AB:57:4F:CC", + "name": "", + "rssi": -73, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "89": "e24bab574fcc" + }, + "service_data": { + "00000d00-0000-1000-8000-00805f9b34fb": "4810d7" + }, + "service_uuids": [ + "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + ], + "rssi": -73, + "tx_power": -127, + "platform_data": [] + } + }, + "7F:B3:F7:28:DD:5B": { + "device": { + "address": "7F:B3:F7:28:DD:5B", + "name": "", + "rssi": -80, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "1005229829e5ab" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -80, + "tx_power": -127, + "platform_data": [] + } + }, + "B4:E8:42:51:76:67": { + "device": { + "address": "B4:E8:42:51:76:67", + "name": "LEDnetWF010097517666", + "rssi": -86, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LEDnetWF010097517666", + "manufacturer_data": { + "23041": "5205b4e84251766600971603000000000000000000000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -86, + "tx_power": -127, + "platform_data": [] + } + }, + "EA:CF:16:0C:16:11": { + "device": { + "address": "EA:CF:16:0C:16:11", + "name": "", + "rssi": -79, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "12020003" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -79, + "tx_power": -127, + "platform_data": [] + } + }, + "08:3A:F2:1E:28:F1": { + "device": { + "address": "08:3A:F2:1E:28:F1", + "name": "LOOKin_98F33208", + "rssi": -86, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F33208", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -86, + "tx_power": -127, + "platform_data": [] + } + }, + "08:3A:F2:1E:2B:2D": { + "device": { + "address": "08:3A:F2:1E:2B:2D", + "name": "LOOKin_98F330F3", + "rssi": -80, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F330F3", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -80, + "tx_power": -127, + "platform_data": [] + } + }, + "3D:AF:40:AD:B3:8B": { + "device": { + "address": "3D:AF:40:AD:B3:8B", + "name": "", + "rssi": -88, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0906036fc0a86b3e130c0a98ce54cc75e4010401030c" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -88, + "tx_power": -127, + "platform_data": [] + } + }, + "61:25:DF:19:90:98": { + "device": { + "address": "61:25:DF:19:90:98", + "name": "", + "rssi": -74, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "1007321fcbeaadd248" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -74, + "tx_power": -127, + "platform_data": [] + } + }, + "34:AB:95:85:6E:F9": { + "device": { + "address": "34:AB:95:85:6E:F9", + "name": "LOOKin_98F330B4", + "rssi": -91, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F330B4", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -91, + "tx_power": -127, + "platform_data": [] + } + }, + "54:E6:1B:F0:20:97": { + "device": { + "address": "54:E6:1B:F0:20:97", + "name": "", + "rssi": -54, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0f08c00ad7f8b400440c10020100" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -54, + "tx_power": -127, + "platform_data": [] + } + }, + "41:CC:AF:67:18:EE": { + "device": { + "address": "41:CC:AF:67:18:EE", + "name": "", + "rssi": -80, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0c0e089c6a11d70cc17e3e52928b96b71006411d75808748" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -80, + "tx_power": -127, + "platform_data": [] + } + }, + "8C:EA:48:4D:93:3B": { + "device": { + "address": "8C:EA:48:4D:93:3B", + "name": "", + "rssi": -98, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "117": "42040180668cea484d933b8eea484d933a01000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -98, + "tx_power": -127, + "platform_data": [] + } + }, + "C5:2D:5E:AB:52:76": { + "device": { + "address": "C5:2D:5E:AB:52:76", + "name": "", + "rssi": -66, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "89": "c52d5eab5276" + }, + "service_data": { + "00000d00-0000-1000-8000-00805f9b34fb": "481063" + }, + "service_uuids": [ + "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + ], + "rssi": -66, + "tx_power": -127, + "platform_data": [] + } + }, + "C0:49:EF:8C:1F:86": { + "device": { + "address": "C0:49:EF:8C:1F:86", + "name": "ShellyPlugUS-C049EF8C1F84", + "rssi": -36, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "ShellyPlugUS-C049EF8C1F84", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [], + "rssi": -36, + "tx_power": -127, + "platform_data": [] + } + }, + "F8:04:2E:E1:71:9D": { + "device": { + "address": "F8:04:2E:E1:71:9D", + "name": "", + "rssi": -90, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "117": "42040180a0f8042ee1719dfa042ee1719c01000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -90, + "tx_power": -127, + "platform_data": [] + } + }, + "C6:D5:B5:C1:2A:22": { + "device": { + "address": "C6:D5:B5:C1:2A:22", + "name": "", + "rssi": -73, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "89": "c6d5b5c12a22" + }, + "service_data": { + "00000d00-0000-1000-8000-00805f9b34fb": "4810e1" + }, + "service_uuids": [ + "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + ], + "rssi": -73, + "tx_power": -127, + "platform_data": [] + } + }, + "31:FA:70:5F:9C:AC": { + "device": { + "address": "31:FA:70:5F:9C:AC", + "name": "", + "rssi": -78, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0906038ec0a86a5e" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -78, + "tx_power": -127, + "platform_data": [] + } + }, + "D5:C1:1D:1A:23:57": { + "device": { + "address": "D5:C1:1D:1A:23:57", + "name": "RZSS", + "rssi": -70, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "RZSS", + "manufacturer_data": { + "1033": "0c246200" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -70, + "tx_power": -127, + "platform_data": [] + } + }, + "2C:BF:C0:1C:D7:4B": { + "device": { + "address": "2C:BF:C0:1C:D7:4B", + "name": "", + "rssi": -89, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0906035cc0a86a9d" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -89, + "tx_power": -127, + "platform_data": [] + } + }, + "B4:E8:42:DB:12:85": { + "device": { + "address": "B4:E8:42:DB:12:85", + "name": "LEDnetWF010097DB1284", + "rssi": -66, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LEDnetWF010097DB1284", + "manufacturer_data": { + "23041": "5205b4e842db128400971603000000000000000000000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -66, + "tx_power": -127, + "platform_data": [] + } + }, + "6E:48:DA:52:2F:13": { + "device": { + "address": "6E:48:DA:52:2F:13", + "name": "", + "rssi": -92, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "10051a1c2e6061" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -92, + "tx_power": -127, + "platform_data": [] + } + }, + "34:AB:95:85:66:6D": { + "device": { + "address": "34:AB:95:85:66:6D", + "name": "LOOKin_98F33163", + "rssi": -91, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F33163", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -91, + "tx_power": -127, + "platform_data": [] + } + }, + "F8:04:2E:E1:9F:19": { + "device": { + "address": "F8:04:2E:E1:9F:19", + "name": "", + "rssi": -92, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "117": "42040180a0f8042ee19f19fa042ee19f1801000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -92, + "tx_power": -127, + "platform_data": [] + } + }, + "68:3C:69:9E:A1:18": { + "device": { + "address": "68:3C:69:9E:A1:18", + "name": "", + "rssi": -78, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0c0e089c6a11d70cc17e3e52928b96b71006411d75808748" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -78, + "tx_power": -127, + "platform_data": [] + } + }, + "07:68:27:D9:15:40": { + "device": { + "address": "07:68:27:D9:15:40", + "name": "", + "rssi": -74, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": {}, + "service_data": { + "0000fd6f-0000-1000-8000-00805f9b34fb": "1b8fb635708f1ef520edb36597fb0586b58f7214" + }, + "service_uuids": [ + "0000fd6f-0000-1000-8000-00805f9b34fb" + ], + "rssi": -74, + "tx_power": -127, + "platform_data": [] + } + }, + "F5:C7:51:3D:B7:33": { + "device": { + "address": "F5:C7:51:3D:B7:33", + "name": "RZSS", + "rssi": -81, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "RZSS", + "manufacturer_data": { + "1033": "0c246164" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -81, + "tx_power": -127, + "platform_data": [] + } + }, + "E0:1F:A6:3A:C5:66": { + "device": { + "address": "E0:1F:A6:3A:C5:66", + "name": "RZSS", + "rssi": -90, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "RZSS", + "manufacturer_data": { + "1033": "0c246200" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -90, + "tx_power": -127, + "platform_data": [] + } + }, + "78:9C:85:08:E6:2A": { + "device": { + "address": "78:9C:85:08:E6:2A", + "name": "M1029QJ", + "rssi": -73, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "M1029QJ", + "manufacturer_data": { + "76": "06310080e7146a373406009c2104023cb9eb0e" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -73, + "tx_power": -127, + "platform_data": [] + } + }, + "4F:EB:42:E2:57:4A": { + "device": { + "address": "4F:EB:42:E2:57:4A", + "name": "a25bd0d9", + "rssi": -86, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "a25bd0d9", + "manufacturer_data": { + "1015": "1a00000000004d617569204f776c" + }, + "service_data": {}, + "service_uuids": [ + "39d6b333-adad-45c8-b6ee-eac6c4cd0000" + ], + "rssi": -86, + "tx_power": -127, + "platform_data": [] + } + }, + "A8:51:AB:8A:5A:84": { + "device": { + "address": "A8:51:AB:8A:5A:84", + "name": "", + "rssi": -94, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "1005021484867e" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -94, + "tx_power": -127, + "platform_data": [] + } + }, + "5E:5C:AC:CF:CD:2D": { + "device": { + "address": "5E:5C:AC:CF:CD:2D", + "name": "", + "rssi": -82, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0f059000da640210022d04" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -82, + "tx_power": -127, + "platform_data": [] + } + }, + "0A:73:BD:22:FE:E2": { + "device": { + "address": "0A:73:BD:22:FE:E2", + "name": "", + "rssi": -92, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "09060318c0a86bef" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -92, + "tx_power": -127, + "platform_data": [] + } + }, + "B4:E8:42:9E:A4:F5": { + "device": { + "address": "B4:E8:42:9E:A4:F5", + "name": "LEDnetWF0100979EA4F4", + "rssi": -82, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LEDnetWF0100979EA4F4", + "manufacturer_data": { + "23041": "5202b4e8429ea4f400970c03010203040506070809a1a2a3a4a5a6" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -82, + "tx_power": -127, + "platform_data": [] + } + }, + "B4:E8:42:53:56:F9": { + "device": { + "address": "B4:E8:42:53:56:F9", + "name": "LEDnetWF0100975356F8", + "rssi": -98, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LEDnetWF0100975356F8", + "manufacturer_data": { + "23041": "5205b4e8425356f800971603000000000000000000000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -98, + "tx_power": -127, + "platform_data": [] + } + }, + "D4:2C:41:97:56:6E": { + "device": { + "address": "D4:2C:41:97:56:6E", + "name": "RZSS", + "rssi": -82, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": "RZSS", + "manufacturer_data": { + "1033": "0c245f32" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -82, + "tx_power": -127, + "platform_data": [] + } + }, + "64:04:FF:2C:E3:55": { + "device": { + "address": "64:04:FF:2C:E3:55", + "name": "", + "rssi": -88, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0f0590007f5fd910022504" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -88, + "tx_power": -127, + "platform_data": [] + } + }, + "A4:C1:38:BD:91:BB": { + "device": { + "address": "A4:C1:38:BD:91:BB", + "name": "LYWSD03MMC", + "rssi": -78, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LYWSD03MMC", + "manufacturer_data": {}, + "service_data": { + "0000fe95-0000-1000-8000-00805f9b34fb": "30585b055bbb91bd38c1a408" + }, + "service_uuids": [], + "rssi": -78, + "tx_power": -127, + "platform_data": [] + } + }, + "B4:E8:42:DA:BA:23": { + "device": { + "address": "B4:E8:42:DA:BA:23", + "name": "LEDnetWF010097DABA22", + "rssi": -92, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LEDnetWF010097DABA22", + "manufacturer_data": { + "23041": "5205b4e842daba2200971603000000000000000000000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -92, + "tx_power": -127, + "platform_data": [] + } + }, + "E3:A5:63:3E:5E:23": { + "device": { + "address": "E3:A5:63:3E:5E:23", + "name": "", + "rssi": -70, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "12020003" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -70, + "tx_power": -127, + "platform_data": [] + } + }, + "68:E3:C2:4D:85:EE": { + "device": { + "address": "68:E3:C2:4D:85:EE", + "name": "", + "rssi": -97, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "10051a1c53afb1" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -97, + "tx_power": -127, + "platform_data": [] + } + }, + "B4:E8:42:52:4B:AD": { + "device": { + "address": "B4:E8:42:52:4B:AD", + "name": "LEDnetWF010097524BAC", + "rssi": -84, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LEDnetWF010097524BAC", + "manufacturer_data": { + "23041": "5205b4e842524bac00971603000000000000000000000000000000" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -84, + "tx_power": -127, + "platform_data": [] + } + }, + "58:2D:34:60:D5:CD": { + "device": { + "address": "58:2D:34:60:D5:CD", + "name": "Qingping Motion & Light", + "rssi": -83, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "Qingping Motion & Light", + "manufacturer_data": {}, + "service_data": { + "0000fe95-0000-1000-8000-00805f9b34fb": "3058830a02cdd560342d5808", + "0000fdcd-0000-1000-8000-00805f9b34fb": "4812cdd560342d580804010f00000f0166" + }, + "service_uuids": [], + "rssi": -83, + "tx_power": -127, + "platform_data": [] + } + }, + "72:90:B0:53:F1:15": { + "device": { + "address": "72:90:B0:53:F1:15", + "name": "", + "rssi": -94, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "0f059000dc161910022d04" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -94, + "tx_power": -127, + "platform_data": [] + } + }, + "D5:1C:FB:39:78:56": { + "device": { + "address": "D5:1C:FB:39:78:56", + "name": "", + "rssi": -93, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "89": "d51cfb397856" + }, + "service_data": { + "00000d00-0000-1000-8000-00805f9b34fb": "4890d7" + }, + "service_uuids": [ + "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + ], + "rssi": -93, + "tx_power": -127, + "platform_data": [] + } + }, + "08:3A:F2:1E:2B:05": { + "device": { + "address": "08:3A:F2:1E:2B:05", + "name": "LOOKin_98F330C2", + "rssi": -98, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": "LOOKin_98F330C2", + "manufacturer_data": {}, + "service_data": {}, + "service_uuids": [ + "00001812-0000-1000-8000-00805f9b34fb" + ], + "rssi": -98, + "tx_power": -127, + "platform_data": [] + } + }, + "FA:8F:F4:A8:9E:BC": { + "device": { + "address": "FA:8F:F4:A8:9E:BC", + "rssi": -92, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "12025400" + }, + "service_data": {}, + "platform_data": [] + } + }, + "EB:0B:36:35:6F:A4": { + "device": { + "address": "EB:0B:36:35:6F:A4", + "name": "", + "rssi": -90, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 1 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "76": "12199021f9370af25a5ec7825f703a4ac47a7b76c08b5695e101ba" + }, + "service_data": {}, + "service_uuids": [], + "rssi": -90, + "tx_power": -127, + "platform_data": [] + } + }, + "AC:67:B2:6A:18:2A": { + "device": { + "address": "AC:67:B2:6A:18:2A", + "name": "", + "rssi": -68, + "details": { + "source": "atom-bluetooth-proxy-ceaac4", + "address_type": 0 + } + }, + "advertisement_data": { + "local_name": null, + "manufacturer_data": { + "741": "ac67b26a182a" + }, + "service_data": { + "00000d00-0000-1000-8000-00805f9b34fb": "65000000613d7300" + }, + "service_uuids": [ + "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + ], + "rssi": -68, + "tx_power": -127, + "platform_data": [] + } + } + }, + "discovered_device_timestamps": { + "51:8E:5B:A8:44:8D": 1670722715.9919367, + "16:9A:28:FC:D5:A8": 1670722719.3087196, + "08:3A:F2:1E:32:69": 1670722719.1660142, + "34:AB:95:85:6E:F1": 1670722720.2908847, + "D8:2E:AD:CD:0D:85": 1670722720.2915154, + "D4:B2:A1:7C:42:17": 1670722719.0863516, + "D8:EF:2F:41:B1:34": 1670722718.7625487, + "60:55:F9:29:FA:2A": 1670722720.291449, + "F3:33:CE:84:58:4A": 1670722720.291406, + "CB:39:CD:C4:3D:46": 1670722715.599468, + "E2:4B:AB:57:4F:CC": 1670722720.291419, + "7F:B3:F7:28:DD:5B": 1670722718.9691222, + "B4:E8:42:51:76:67": 1670722710.6735754, + "EA:CF:16:0C:16:11": 1670722715.9919817, + "08:3A:F2:1E:28:F1": 1670722720.291503, + "08:3A:F2:1E:2B:2D": 1670722720.2914848, + "3D:AF:40:AD:B3:8B": 1670722718.85895, + "61:25:DF:19:90:98": 1670722717.941397, + "34:AB:95:85:6E:F9": 1670722718.7632113, + "54:E6:1B:F0:20:97": 1670722717.1185102, + "41:CC:AF:67:18:EE": 1670722720.2912233, + "8C:EA:48:4D:93:3B": 1670722720.2910836, + "C5:2D:5E:AB:52:76": 1670722718.558827, + "C0:49:EF:8C:1F:86": 1670722717.9688299, + "F8:04:2E:E1:71:9D": 1670722717.526199, + "C6:D5:B5:C1:2A:22": 1670722717.0176404, + "31:FA:70:5F:9C:AC": 1670722720.2911327, + "D5:C1:1D:1A:23:57": 1670722717.639859, + "2C:BF:C0:1C:D7:4B": 1670722720.2914364, + "B4:E8:42:DB:12:85": 1670722719.0764725, + "6E:48:DA:52:2F:13": 1670722720.2913177, + "34:AB:95:85:66:6D": 1670722720.2912607, + "F8:04:2E:E1:9F:19": 1670722720.2911522, + "68:3C:69:9E:A1:18": 1670722719.1658278, + "07:68:27:D9:15:40": 1670722720.2912369, + "F5:C7:51:3D:B7:33": 1670722720.2911801, + "E0:1F:A6:3A:C5:66": 1670722713.7567363, + "78:9C:85:08:E6:2A": 1670722712.5362735, + "4F:EB:42:E2:57:4A": 1670722716.5097647, + "A8:51:AB:8A:5A:84": 1670722711.9280567, + "5E:5C:AC:CF:CD:2D": 1670722716.2974086, + "0A:73:BD:22:FE:E2": 1670722720.2914622, + "B4:E8:42:9E:A4:F5": 1670722718.5590498, + "B4:E8:42:53:56:F9": 1670722712.4074357, + "D4:2C:41:97:56:6E": 1670722718.3550117, + "64:04:FF:2C:E3:55": 1670722718.1470506, + "A4:C1:38:BD:91:BB": 1670722712.7268498, + "B4:E8:42:DA:BA:23": 1670722720.2910104, + "E3:A5:63:3E:5E:23": 1670722713.8445594, + "68:E3:C2:4D:85:EE": 1670722713.8488224, + "B4:E8:42:52:4B:AD": 1670722714.0646405, + "58:2D:34:60:D5:CD": 1670722715.377663, + "72:90:B0:53:F1:15": 1670722718.1468875, + "D5:1C:FB:39:78:56": 1670722715.2835894, + "08:3A:F2:1E:2B:05": 1670722719.0629613, + "FA:8F:F4:A8:9E:BC": 1670722717.22681, + "EB:0B:36:35:6F:A4": 1670722717.6297317, + "AC:67:B2:6A:18:2A": 1670722719.086469 + } + } + } +} diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index 19c47361a6a..53c2716b0bd 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -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() diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index bb403ee0409..6569d4a148b 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -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 ): diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py index 2b401f32c01..b653e1f5de3 100644 --- a/tests/components/bluetooth/test_models.py +++ b/tests/components/bluetooth/test_models.py @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index 9eb830950fa..dda31934f7b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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")