Refactor all Bluetooth scanners to inherit from BaseHaRemoteScanner (#105523)

This commit is contained in:
J. Nick Koston 2023-12-12 10:28:43 -10:00 committed by GitHub
parent 5bd0833f49
commit f002a6a732
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 83 additions and 165 deletions

View File

@ -23,6 +23,7 @@ from bluetooth_adapters import (
)
from bluetooth_data_tools import monotonic_time_coarse as MONOTONIC_TIME
from habluetooth import (
BaseHaRemoteScanner,
BaseHaScanner,
BluetoothScannerDevice,
BluetoothScanningMode,
@ -69,7 +70,6 @@ from .api import (
async_set_fallback_availability_interval,
async_track_unavailable,
)
from .base_scanner import HomeAssistantRemoteScanner
from .const import (
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
CONF_ADAPTER,
@ -116,6 +116,7 @@ __all__ = [
"BluetoothCallback",
"BluetoothScannerDevice",
"HaBluetoothConnector",
"BaseHaRemoteScanner",
"SOURCE_LOCAL",
"FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS",
"MONOTONIC_TIME",

View File

@ -1,95 +0,0 @@
"""Base classes for HA Bluetooth scanners for bluetooth."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from bluetooth_adapters import DiscoveredDeviceAdvertisementData
from habluetooth import BaseHaRemoteScanner, HaBluetoothConnector
from home_assistant_bluetooth import BluetoothServiceInfoBleak
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import (
CALLBACK_TYPE,
Event,
HomeAssistant,
callback as hass_callback,
)
from . import models
class HomeAssistantRemoteScanner(BaseHaRemoteScanner):
"""Home Assistant remote BLE scanner.
This is the only object that should know about
the hass object.
"""
__slots__ = (
"hass",
"_storage",
"_cancel_stop",
)
def __init__(
self,
hass: HomeAssistant,
scanner_id: str,
name: str,
new_info_callback: Callable[[BluetoothServiceInfoBleak], None],
connector: HaBluetoothConnector | None,
connectable: bool,
) -> None:
"""Initialize the scanner."""
self.hass = hass
assert models.MANAGER is not None
self._storage = models.MANAGER.storage
self._cancel_stop: CALLBACK_TYPE | None = None
super().__init__(scanner_id, name, new_info_callback, connector, connectable)
@hass_callback
def async_setup(self) -> CALLBACK_TYPE:
"""Set up the scanner."""
super().async_setup()
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()
self._cancel_stop = self.hass.bus.async_listen(
EVENT_HOMEASSISTANT_STOP, self._async_save_history
)
return self._unsetup
@hass_callback
def _unsetup(self) -> None:
super()._unsetup()
self._async_save_history()
if self._cancel_stop:
self._cancel_stop()
self._cancel_stop = None
@hass_callback
def _async_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,
),
)
async def async_diagnostics(self) -> dict[str, Any]:
"""Return diagnostic information about the scanner."""
diag = await super().async_diagnostics()
diag["storage"] = self._storage.async_get_advertisement_history_as_dict(
self.source
)
return diag

View File

@ -2,12 +2,13 @@
from __future__ import annotations
from collections.abc import Callable, Iterable
from functools import partial
import itertools
import logging
from bleak_retry_connector import BleakSlotManager
from bluetooth_adapters import BluetoothAdapters
from habluetooth import BluetoothManager
from habluetooth import BaseHaRemoteScanner, BaseHaScanner, BluetoothManager
from homeassistant import config_entries
from homeassistant.const import EVENT_LOGGING_CHANGED
@ -189,7 +190,45 @@ class HomeAssistantBluetoothManager(BluetoothManager):
def async_stop(self) -> None:
"""Stop the Bluetooth integration at shutdown."""
_LOGGER.debug("Stopping bluetooth manager")
self._async_save_scanner_histories()
super().async_stop()
if self._cancel_logging_listener:
self._cancel_logging_listener()
self._cancel_logging_listener = None
def _async_save_scanner_histories(self) -> None:
"""Save the scanner histories."""
for scanner in itertools.chain(
self._connectable_scanners, self._non_connectable_scanners
):
self._async_save_scanner_history(scanner)
def _async_save_scanner_history(self, scanner: BaseHaScanner) -> None:
"""Save the scanner history."""
if isinstance(scanner, BaseHaRemoteScanner):
self.storage.async_set_advertisement_history(
scanner.source, scanner.serialize_discovered_devices()
)
def _async_unregister_scanner(
self, scanner: BaseHaScanner, unregister: CALLBACK_TYPE
) -> None:
"""Unregister a scanner."""
unregister()
self._async_save_scanner_history(scanner)
def async_register_scanner(
self,
scanner: BaseHaScanner,
connectable: bool,
connection_slots: int | None = None,
) -> CALLBACK_TYPE:
"""Register a scanner."""
if isinstance(scanner, BaseHaRemoteScanner):
if history := self.storage.async_get_advertisement_history(scanner.source):
scanner.restore_discovered_devices(history)
unregister = super().async_register_scanner(
scanner, connectable, connection_slots
)
return partial(self._async_unregister_scanner, scanner, unregister)

View File

@ -99,7 +99,7 @@ async def async_connect_scanner(
),
)
scanner = ESPHomeScanner(
hass, source, entry.title, new_info_callback, connector, connectable
source, entry.title, new_info_callback, connector, connectable
)
client_data.scanner = scanner
coros: list[Coroutine[Any, Any, CALLBACK_TYPE]] = []

View File

@ -7,14 +7,11 @@ from bluetooth_data_tools import (
parse_advertisement_data_tuple,
)
from homeassistant.components.bluetooth import (
MONOTONIC_TIME,
HomeAssistantRemoteScanner,
)
from homeassistant.components.bluetooth import MONOTONIC_TIME, BaseHaRemoteScanner
from homeassistant.core import callback
class ESPHomeScanner(HomeAssistantRemoteScanner):
class ESPHomeScanner(BaseHaRemoteScanner):
"""Scanner for esphome."""
__slots__ = ()

View File

@ -10,7 +10,7 @@ from home_assistant_bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.bluetooth import (
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
MONOTONIC_TIME,
HomeAssistantRemoteScanner,
BaseHaRemoteScanner,
async_get_advertisement_callback,
async_register_scanner,
)
@ -22,12 +22,11 @@ from .coordinator import RuuviGatewayUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
class RuuviGatewayScanner(HomeAssistantRemoteScanner):
class RuuviGatewayScanner(BaseHaRemoteScanner):
"""Scanner for Ruuvi Gateway."""
def __init__(
self,
hass: HomeAssistant,
scanner_id: str,
name: str,
new_info_callback: Callable[[BluetoothServiceInfoBleak], None],
@ -36,7 +35,6 @@ class RuuviGatewayScanner(HomeAssistantRemoteScanner):
) -> None:
"""Initialize the scanner, using the given update coordinator as data source."""
super().__init__(
hass,
scanner_id,
name,
new_info_callback,
@ -87,7 +85,6 @@ def async_connect_scanner(
source,
)
scanner = RuuviGatewayScanner(
hass=hass,
scanner_id=source,
name=entry.title,
new_info_callback=async_get_advertisement_callback(hass),

View File

@ -43,9 +43,7 @@ async def async_connect_scanner(
source=source,
can_connect=lambda: False,
)
scanner = ShellyBLEScanner(
hass, source, entry.title, new_info_callback, connector, False
)
scanner = ShellyBLEScanner(source, entry.title, new_info_callback, connector, False)
unload_callbacks = [
async_register_scanner(hass, scanner, False),
scanner.async_setup(),

View File

@ -6,16 +6,13 @@ from typing import Any
from aioshelly.ble import parse_ble_scan_result_event
from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT, BLE_SCAN_RESULT_VERSION
from homeassistant.components.bluetooth import (
MONOTONIC_TIME,
HomeAssistantRemoteScanner,
)
from homeassistant.components.bluetooth import MONOTONIC_TIME, BaseHaRemoteScanner
from homeassistant.core import callback
from ..const import LOGGER
class ShellyBLEScanner(HomeAssistantRemoteScanner):
class ShellyBLEScanner(BaseHaRemoteScanner):
"""Scanner for shelly."""
@callback

View File

@ -7,9 +7,9 @@ import pytest
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
MONOTONIC_TIME,
BaseHaRemoteScanner,
BaseHaScanner,
HaBluetoothConnector,
HomeAssistantRemoteScanner,
async_scanner_by_source,
async_scanner_devices_by_address,
)
@ -46,7 +46,7 @@ async def test_async_scanner_devices_by_address_connectable(
"""Test getting scanner devices by address with connectable devices."""
manager = _get_manager()
class FakeInjectableScanner(HomeAssistantRemoteScanner):
class FakeInjectableScanner(BaseHaRemoteScanner):
def inject_advertisement(
self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
@ -68,7 +68,7 @@ async def test_async_scanner_devices_by_address_connectable(
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
scanner = FakeInjectableScanner(
hass, "esp32", "esp32", new_info_callback, connector, False
"esp32", "esp32", new_info_callback, connector, False
)
unsetup = scanner.async_setup()
cancel = manager.async_register_scanner(scanner, True)

View File

@ -14,8 +14,8 @@ import pytest
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
MONOTONIC_TIME,
BaseHaRemoteScanner,
HaBluetoothConnector,
HomeAssistantRemoteScanner,
storage,
)
from homeassistant.components.bluetooth.const import (
@ -41,7 +41,7 @@ from . import (
from tests.common import async_fire_time_changed, load_fixture
class FakeScanner(HomeAssistantRemoteScanner):
class FakeScanner(BaseHaRemoteScanner):
"""Fake scanner."""
def inject_advertisement(
@ -115,7 +115,7 @@ async def test_remote_scanner(
connector = (
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True)
scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, True)
unsetup = scanner.async_setup()
cancel = manager.async_register_scanner(scanner, True)
@ -182,7 +182,7 @@ async def test_remote_scanner_expires_connectable(
connector = (
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True)
scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, True)
unsetup = scanner.async_setup()
cancel = manager.async_register_scanner(scanner, True)
@ -237,7 +237,7 @@ async def test_remote_scanner_expires_non_connectable(
connector = (
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False)
scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False)
unsetup = scanner.async_setup()
cancel = manager.async_register_scanner(scanner, True)
@ -312,7 +312,7 @@ async def test_base_scanner_connecting_behavior(
connector = (
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False)
scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False)
unsetup = scanner.async_setup()
cancel = manager.async_register_scanner(scanner, True)
@ -363,8 +363,7 @@ async def test_restore_history_remote_adapter(
connector = (
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
scanner = HomeAssistantRemoteScanner(
hass,
scanner = BaseHaRemoteScanner(
"atom-bluetooth-proxy-ceaac4",
"atom-bluetooth-proxy-ceaac4",
lambda adv: None,
@ -379,8 +378,7 @@ async def test_restore_history_remote_adapter(
cancel()
unsetup()
scanner = HomeAssistantRemoteScanner(
hass,
scanner = BaseHaRemoteScanner(
"atom-bluetooth-proxy-ceaac4",
"atom-bluetooth-proxy-ceaac4",
lambda adv: None,
@ -419,7 +417,7 @@ async def test_device_with_ten_minute_advertising_interval(
connector = (
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False)
scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False)
unsetup = scanner.async_setup()
cancel = manager.async_register_scanner(scanner, True)
@ -511,7 +509,7 @@ async def test_scanner_stops_responding(
connector = (
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False)
scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False)
unsetup = scanner.async_setup()
cancel = manager.async_register_scanner(scanner, True)

View File

@ -8,8 +8,8 @@ from habluetooth import HaScanner
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
MONOTONIC_TIME,
BaseHaRemoteScanner,
HaBluetoothConnector,
HomeAssistantRemoteScanner,
)
from homeassistant.core import HomeAssistant
@ -423,7 +423,7 @@ async def test_diagnostics_remote_adapter(
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
)
class FakeScanner(HomeAssistantRemoteScanner):
class FakeScanner(BaseHaRemoteScanner):
def inject_advertisement(
self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
@ -458,9 +458,7 @@ async def test_diagnostics_remote_adapter(
connector = (
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
scanner = FakeScanner(
hass, "esp32", "esp32", new_info_callback, connector, False
)
scanner = FakeScanner("esp32", "esp32", new_info_callback, connector, False)
unsetup = scanner.async_setup()
cancel = manager.async_register_scanner(scanner, True)
@ -631,7 +629,6 @@ async def test_diagnostics_remote_adapter(
"scanning": True,
"source": "esp32",
"start_time": ANY,
"storage": None,
"time_since_last_device_detection": {"44:44:33:11:23:45": ANY},
"type": "FakeScanner",
},

View File

@ -13,12 +13,12 @@ import pytest
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
MONOTONIC_TIME,
BaseHaRemoteScanner,
BluetoothChange,
BluetoothScanningMode,
BluetoothServiceInfo,
BluetoothServiceInfoBleak,
HaBluetoothConnector,
HomeAssistantRemoteScanner,
async_ble_device_from_address,
async_get_advertisement_callback,
async_get_fallback_availability_interval,
@ -703,7 +703,7 @@ async def test_goes_unavailable_connectable_only_and_recovers(
BluetoothScanningMode.ACTIVE,
)
class FakeScanner(HomeAssistantRemoteScanner):
class FakeScanner(BaseHaRemoteScanner):
def inject_advertisement(
self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
@ -725,7 +725,6 @@ async def test_goes_unavailable_connectable_only_and_recovers(
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
connectable_scanner = FakeScanner(
hass,
"connectable",
"connectable",
new_info_callback,
@ -749,7 +748,6 @@ async def test_goes_unavailable_connectable_only_and_recovers(
)
not_connectable_scanner = FakeScanner(
hass,
"not_connectable",
"not_connectable",
new_info_callback,
@ -800,7 +798,6 @@ async def test_goes_unavailable_connectable_only_and_recovers(
cancel_unavailable()
connectable_scanner_2 = FakeScanner(
hass,
"connectable",
"connectable",
new_info_callback,
@ -876,7 +873,7 @@ async def test_goes_unavailable_dismisses_discovery_and_makes_discoverable(
BluetoothScanningMode.ACTIVE,
)
class FakeScanner(HomeAssistantRemoteScanner):
class FakeScanner(BaseHaRemoteScanner):
def inject_advertisement(
self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
@ -904,7 +901,6 @@ async def test_goes_unavailable_dismisses_discovery_and_makes_discoverable(
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
non_connectable_scanner = FakeScanner(
hass,
"connectable",
"connectable",
new_info_callback,

View File

@ -11,9 +11,9 @@ from habluetooth.wrappers import HaBleakClientWrapper, HaBleakScannerWrapper
import pytest
from homeassistant.components.bluetooth import (
BaseHaRemoteScanner,
BaseHaScanner,
HaBluetoothConnector,
HomeAssistantRemoteScanner,
)
from homeassistant.core import HomeAssistant
@ -154,7 +154,7 @@ async def test_wrapped_bleak_client_set_disconnected_callback_after_connected(
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}, rssi=-100
)
class FakeScanner(HomeAssistantRemoteScanner):
class FakeScanner(BaseHaRemoteScanner):
@property
def discovered_devices(self) -> list[BLEDevice]:
"""Return a list of discovered devices."""
@ -182,7 +182,6 @@ async def test_wrapped_bleak_client_set_disconnected_callback_after_connected(
MockBleakClient, "esp32_has_connection_slot", lambda: True
)
scanner = FakeScanner(
hass,
"esp32_has_connection_slot",
"esp32_has_connection_slot",
lambda info: None,
@ -267,7 +266,7 @@ async def test_ble_device_with_proxy_client_out_of_connections(
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
)
class FakeScanner(HomeAssistantRemoteScanner):
class FakeScanner(BaseHaRemoteScanner):
@property
def discovered_devices(self) -> list[BLEDevice]:
"""Return a list of discovered devices."""
@ -292,7 +291,7 @@ async def test_ble_device_with_proxy_client_out_of_connections(
return None
connector = HaBluetoothConnector(MockBleakClient, "esp32", lambda: False)
scanner = FakeScanner(hass, "esp32", "esp32", lambda info: None, connector, True)
scanner = FakeScanner("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"
@ -332,7 +331,7 @@ async def test_ble_device_with_proxy_clear_cache(
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
)
class FakeScanner(HomeAssistantRemoteScanner):
class FakeScanner(BaseHaRemoteScanner):
@property
def discovered_devices(self) -> list[BLEDevice]:
"""Return a list of discovered devices."""
@ -357,7 +356,7 @@ async def test_ble_device_with_proxy_clear_cache(
return None
connector = HaBluetoothConnector(MockBleakClient, "esp32", lambda: True)
scanner = FakeScanner(hass, "esp32", "esp32", lambda info: None, connector, True)
scanner = FakeScanner("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"
@ -435,7 +434,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
"esp32_no_connection_slot",
)
class FakeScanner(HomeAssistantRemoteScanner):
class FakeScanner(BaseHaRemoteScanner):
@property
def discovered_devices(self) -> list[BLEDevice]:
"""Return a list of discovered devices."""
@ -463,7 +462,6 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
MockBleakClient, "esp32_has_connection_slot", lambda: True
)
scanner = FakeScanner(
hass,
"esp32_has_connection_slot",
"esp32_has_connection_slot",
lambda info: None,
@ -549,7 +547,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
"esp32_no_connection_slot",
)
class FakeScanner(HomeAssistantRemoteScanner):
class FakeScanner(BaseHaRemoteScanner):
@property
def discovered_devices(self) -> list[BLEDevice]:
"""Return a list of discovered devices."""
@ -577,7 +575,6 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
MockBleakClient, "esp32_has_connection_slot", lambda: True
)
scanner = FakeScanner(
hass,
"esp32_has_connection_slot",
"esp32_has_connection_slot",
lambda info: None,

View File

@ -17,10 +17,10 @@ import pytest
from homeassistant.components.bluetooth import (
MONOTONIC_TIME,
BaseHaRemoteScanner,
BluetoothServiceInfoBleak,
HaBluetoothConnector,
HomeAssistantBluetoothManager,
HomeAssistantRemoteScanner,
async_get_advertisement_callback,
)
from homeassistant.core import HomeAssistant
@ -36,12 +36,11 @@ def mock_shutdown(manager: HomeAssistantBluetoothManager) -> None:
manager.shutdown = False
class FakeScanner(HomeAssistantRemoteScanner):
class FakeScanner(BaseHaRemoteScanner):
"""Fake scanner."""
def __init__(
self,
hass: HomeAssistant,
scanner_id: str,
name: str,
new_info_callback: Callable[[BluetoothServiceInfoBleak], None],
@ -49,9 +48,7 @@ class FakeScanner(HomeAssistantRemoteScanner):
connectable: bool,
) -> None:
"""Initialize the scanner."""
super().__init__(
hass, scanner_id, name, new_info_callback, connector, connectable
)
super().__init__(scanner_id, name, new_info_callback, connector, connectable)
self._details: dict[str, str | HaBluetoothConnector] = {}
def __repr__(self) -> str:
@ -187,10 +184,10 @@ def _generate_scanners_with_fake_devices(hass):
new_info_callback = async_get_advertisement_callback(hass)
scanner_hci0 = FakeScanner(
hass, "00:00:00:00:00:01", "hci0", new_info_callback, None, True
"00:00:00:00:00:01", "hci0", new_info_callback, None, True
)
scanner_hci1 = FakeScanner(
hass, "00:00:00:00:00:02", "hci1", new_info_callback, None, True
"00:00:00:00:00:02", "hci1", new_info_callback, None, True
)
for device, adv_data in hci0_device_advs.values():

View File

@ -44,7 +44,7 @@ async def client_data_fixture(
api_version=APIVersion(1, 9),
title=ESP_NAME,
scanner=ESPHomeScanner(
hass, ESP_MAC_ADDRESS, ESP_NAME, lambda info: None, connector, True
ESP_MAC_ADDRESS, ESP_NAME, lambda info: None, connector, True
),
)

View File

@ -130,7 +130,6 @@ async def test_rpc_config_entry_diagnostics(
"scanning": True,
"start_time": ANY,
"source": "12:34:56:78:9A:BC",
"storage": None,
"time_since_last_device_detection": {"AA:BB:CC:DD:EE:FF": ANY},
"type": "ShellyBLEScanner",
}