Restore passive bluetooth entity data at startup (#97462)

This commit is contained in:
J. Nick Koston 2023-08-06 20:25:18 -10:00 committed by GitHub
parent 1adfa6bbeb
commit 56257b7a38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 560 additions and 17 deletions

View File

@ -38,7 +38,7 @@ from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import async_delete_issue
from homeassistant.loader import async_get_bluetooth
from . import models
from . import models, passive_update_processor
from .api import (
_get_manager,
async_address_present,
@ -125,6 +125,7 @@ async def _async_get_adapter_from_address(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the bluetooth integration."""
await passive_update_processor.async_setup(hass)
integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass))
integration_matcher.async_setup()
bluetooth_adapters = get_adapters()

View File

@ -2,12 +2,31 @@
from __future__ import annotations
import dataclasses
from datetime import timedelta
from functools import cache
import logging
from typing import TYPE_CHECKING, Any, Generic, TypeVar
from typing import TYPE_CHECKING, Any, Generic, TypedDict, TypeVar, cast
from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
from homeassistant import config_entries
from homeassistant.const import (
ATTR_IDENTIFIERS,
ATTR_NAME,
CONF_ENTITY_CATEGORY,
EVENT_HOMEASSISTANT_STOP,
EntityCategory,
)
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.helpers.entity import (
DeviceInfo,
Entity,
EntityDescription,
)
from homeassistant.helpers.entity_platform import (
async_get_current_platform,
)
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.storage import Store
from homeassistant.util.enum import try_parse_enum
from .const import DOMAIN
from .update_coordinator import BasePassiveBluetoothCoordinator
@ -23,6 +42,12 @@ if TYPE_CHECKING:
BluetoothServiceInfoBleak,
)
STORAGE_KEY = "bluetooth.passive_update_processor"
STORAGE_VERSION = 1
STORAGE_SAVE_INTERVAL = timedelta(minutes=15)
PASSIVE_UPDATE_PROCESSOR = "passive_update_processor"
_T = TypeVar("_T")
@dataclasses.dataclass(slots=True, frozen=True)
class PassiveBluetoothEntityKey:
@ -36,8 +61,67 @@ class PassiveBluetoothEntityKey:
key: str
device_id: str | None
def to_string(self) -> str:
"""Convert the key to a string which can be used as JSON key."""
return f"{self.key}___{self.device_id or ''}"
_T = TypeVar("_T")
@classmethod
def from_string(cls, key: str) -> PassiveBluetoothEntityKey:
"""Convert a string (from JSON) to a key."""
key, device_id = key.split("___")
return cls(key, device_id or None)
@dataclasses.dataclass(slots=True, frozen=False)
class PassiveBluetoothProcessorData:
"""Data for the passive bluetooth processor."""
coordinators: set[PassiveBluetoothProcessorCoordinator]
all_restore_data: dict[str, dict[str, RestoredPassiveBluetoothDataUpdate]]
class RestoredPassiveBluetoothDataUpdate(TypedDict):
"""Restored PassiveBluetoothDataUpdate."""
devices: dict[str, DeviceInfo]
entity_descriptions: dict[str, dict[str, Any]]
entity_names: dict[str, str | None]
entity_data: dict[str, Any]
# Fields do not change so we can cache the result
# of calling fields() on the dataclass
cached_fields = cache(dataclasses.fields)
def deserialize_entity_description(
descriptions_class: type[EntityDescription], data: dict[str, Any]
) -> EntityDescription:
"""Deserialize an entity description."""
result: dict[str, Any] = {}
for field in cached_fields(descriptions_class): # type: ignore[arg-type]
field_name = field.name
# It would be nice if field.type returned the actual
# type instead of a str so we could avoid writing this
# out, but it doesn't. If we end up using this in more
# places we can add a `as_dict` and a `from_dict`
# method to these classes
if field_name == CONF_ENTITY_CATEGORY:
value = try_parse_enum(EntityCategory, data.get(field_name))
else:
value = data.get(field_name)
result[field_name] = value
return descriptions_class(**result)
def serialize_entity_description(description: EntityDescription) -> dict[str, Any]:
"""Serialize an entity description."""
as_dict = dataclasses.asdict(description)
return {
field.name: as_dict[field.name]
for field in cached_fields(type(description)) # type: ignore[arg-type]
if field.default != as_dict.get(field.name)
}
@dataclasses.dataclass(slots=True, frozen=True)
@ -62,6 +146,114 @@ class PassiveBluetoothDataUpdate(Generic[_T]):
self.entity_data.update(new_data.entity_data)
self.entity_names.update(new_data.entity_names)
def async_get_restore_data(self) -> RestoredPassiveBluetoothDataUpdate:
"""Serialize restore data to storage."""
return {
"devices": {
key or "": device_info for key, device_info in self.devices.items()
},
"entity_descriptions": {
key.to_string(): serialize_entity_description(description)
for key, description in self.entity_descriptions.items()
},
"entity_names": {
key.to_string(): name for key, name in self.entity_names.items()
},
"entity_data": {
key.to_string(): data for key, data in self.entity_data.items()
},
}
@callback
def async_set_restore_data(
self,
restore_data: RestoredPassiveBluetoothDataUpdate,
entity_description_class: type[EntityDescription],
) -> None:
"""Set the restored data from storage."""
self.devices.update(
{
key or None: device_info
for key, device_info in restore_data["devices"].items()
}
)
self.entity_descriptions.update(
{
PassiveBluetoothEntityKey.from_string(
key
): deserialize_entity_description(entity_description_class, description)
for key, description in restore_data["entity_descriptions"].items()
if description
}
)
self.entity_names.update(
{
PassiveBluetoothEntityKey.from_string(key): name
for key, name in restore_data["entity_names"].items()
}
)
self.entity_data.update(
{
PassiveBluetoothEntityKey.from_string(key): cast(_T, data)
for key, data in restore_data["entity_data"].items()
}
)
def async_register_coordinator_for_restore(
hass: HomeAssistant, coordinator: PassiveBluetoothProcessorCoordinator
) -> CALLBACK_TYPE:
"""Register a coordinator to have its processors data restored."""
data: PassiveBluetoothProcessorData = hass.data[PASSIVE_UPDATE_PROCESSOR]
coordinators = data.coordinators
coordinators.add(coordinator)
if restore_key := coordinator.restore_key:
coordinator.restore_data = data.all_restore_data.setdefault(restore_key, {})
@callback
def _unregister_coordinator_for_restore() -> None:
"""Unregister a coordinator."""
coordinators.remove(coordinator)
return _unregister_coordinator_for_restore
async def async_setup(hass: HomeAssistant) -> None:
"""Set up the passive update processor coordinators."""
storage: Store[dict[str, dict[str, RestoredPassiveBluetoothDataUpdate]]] = Store(
hass, STORAGE_VERSION, STORAGE_KEY
)
coordinators: set[PassiveBluetoothProcessorCoordinator] = set()
all_restore_data: dict[str, dict[str, RestoredPassiveBluetoothDataUpdate]] = (
await storage.async_load() or {}
)
hass.data[PASSIVE_UPDATE_PROCESSOR] = PassiveBluetoothProcessorData(
coordinators, all_restore_data
)
async def _async_save_processor_data(_: Any) -> None:
"""Save the processor data."""
await storage.async_save(
{
coordinator.restore_key: coordinator.async_get_restore_data()
for coordinator in coordinators
if coordinator.restore_key
}
)
cancel_interval = async_track_time_interval(
hass, _async_save_processor_data, STORAGE_SAVE_INTERVAL
)
async def _async_save_processor_data_at_stop(_event: Event) -> None:
"""Save the processor data at shutdown."""
cancel_interval()
await _async_save_processor_data(None)
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _async_save_processor_data_at_stop
)
class PassiveBluetoothProcessorCoordinator(
Generic[_T], BasePassiveBluetoothCoordinator
@ -90,23 +282,49 @@ class PassiveBluetoothProcessorCoordinator(
self._processors: list[PassiveBluetoothDataProcessor] = []
self._update_method = update_method
self.last_update_success = True
self.restore_data: dict[str, RestoredPassiveBluetoothDataUpdate] = {}
self.restore_key = None
if config_entry := config_entries.current_entry.get():
self.restore_key = config_entry.entry_id
self._on_stop.append(async_register_coordinator_for_restore(self.hass, self))
@property
def available(self) -> bool:
"""Return if the device is available."""
return super().available and self.last_update_success
@callback
def async_get_restore_data(
self,
) -> dict[str, RestoredPassiveBluetoothDataUpdate]:
"""Generate the restore data."""
return {
processor.restore_key: processor.data.async_get_restore_data()
for processor in self._processors
if processor.restore_key
}
@callback
def async_register_processor(
self,
processor: PassiveBluetoothDataProcessor,
entity_description_class: type[EntityDescription] | None = None,
) -> Callable[[], None]:
"""Register a processor that subscribes to updates."""
processor.async_register_coordinator(self)
# entity_description_class will become mandatory
# in the future, but is optional for now to allow
# for a transition period.
processor.async_register_coordinator(self, entity_description_class)
@callback
def remove_processor() -> None:
"""Remove a processor."""
# Save the data before removing the processor
# so if they reload its still there
if restore_key := processor.restore_key:
self.restore_data[restore_key] = processor.data.async_get_restore_data()
self._processors.remove(processor)
self._processors.append(processor)
@ -182,12 +400,18 @@ class PassiveBluetoothDataProcessor(Generic[_T]):
entity_data: dict[PassiveBluetoothEntityKey, _T]
entity_descriptions: dict[PassiveBluetoothEntityKey, EntityDescription]
devices: dict[str | None, DeviceInfo]
restore_key: str | None
def __init__(
self,
update_method: Callable[[_T], PassiveBluetoothDataUpdate[_T]],
restore_key: str | None = None,
) -> None:
"""Initialize the coordinator."""
try:
self.restore_key = restore_key or async_get_current_platform().domain
except RuntimeError:
self.restore_key = None
self._listeners: list[
Callable[[PassiveBluetoothDataUpdate[_T] | None], None]
] = []
@ -202,15 +426,29 @@ class PassiveBluetoothDataProcessor(Generic[_T]):
def async_register_coordinator(
self,
coordinator: PassiveBluetoothProcessorCoordinator,
entity_description_class: type[EntityDescription] | None,
) -> None:
"""Register a coordinator."""
self.coordinator = coordinator
self.data = PassiveBluetoothDataUpdate()
data = self.data
# These attributes to access the data in
# self.data are for backwards compatibility.
self.entity_names = data.entity_names
self.entity_data = data.entity_data
self.entity_descriptions = data.entity_descriptions
self.devices = data.devices
if (
entity_description_class
and (restore_key := self.restore_key)
and (restore_data := coordinator.restore_data)
and (restored_processor_data := restore_data.get(restore_key))
):
data.async_set_restore_data(
restored_processor_data,
entity_description_class,
)
self.async_update_listeners(data)
@property
def available(self) -> bool:

View File

@ -1,15 +1,18 @@
"""Tests for the Bluetooth integration."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
import time
from typing import Any
from unittest.mock import MagicMock, patch
from home_assistant_bluetooth import BluetoothServiceInfo
import pytest
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
BinarySensorEntityDescription,
)
@ -22,13 +25,19 @@ from homeassistant.components.bluetooth import (
)
from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS
from homeassistant.components.bluetooth.passive_update_processor import (
STORAGE_KEY,
PassiveBluetoothDataProcessor,
PassiveBluetoothDataUpdate,
PassiveBluetoothEntityKey,
PassiveBluetoothProcessorCoordinator,
PassiveBluetoothProcessorEntity,
)
from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
SensorDeviceClass,
SensorEntityDescription,
)
from homeassistant.config_entries import current_entry
from homeassistant.const import UnitOfTemperature
from homeassistant.core import CoreState, HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo
@ -41,7 +50,12 @@ from . import (
patch_all_discovered_devices,
)
from tests.common import MockEntityPlatform, async_fire_time_changed
from tests.common import (
MockConfigEntry,
MockEntityPlatform,
async_fire_time_changed,
async_test_home_assistant,
)
_LOGGER = logging.getLogger(__name__)
@ -1092,6 +1106,18 @@ BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate(
)
DEVICE_ONLY_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate(
devices={
None: DeviceInfo(
name="Test Device", model="Test Model", manufacturer="Test Manufacturer"
),
},
entity_data={},
entity_names={},
entity_descriptions={},
)
async def test_integration_multiple_entity_platforms(
hass: HomeAssistant,
mock_bleak_scanner_start: MagicMock,
@ -1118,21 +1144,21 @@ async def test_integration_multiple_entity_platforms(
binary_sensor_processor = PassiveBluetoothDataProcessor(
lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE
)
sesnor_processor = PassiveBluetoothDataProcessor(
sensor_processor = PassiveBluetoothDataProcessor(
lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE
)
coordinator.async_register_processor(binary_sensor_processor)
coordinator.async_register_processor(sesnor_processor)
coordinator.async_register_processor(sensor_processor)
cancel_coordinator = coordinator.async_start()
binary_sensor_processor.async_add_listener(MagicMock())
sesnor_processor.async_add_listener(MagicMock())
sensor_processor.async_add_listener(MagicMock())
mock_add_sensor_entities = MagicMock()
mock_add_binary_sensor_entities = MagicMock()
sesnor_processor.async_add_entities_listener(
sensor_processor.async_add_entities_listener(
PassiveBluetoothProcessorEntity,
mock_add_sensor_entities,
)
@ -1146,14 +1172,14 @@ async def test_integration_multiple_entity_platforms(
assert len(mock_add_binary_sensor_entities.mock_calls) == 1
assert len(mock_add_sensor_entities.mock_calls) == 1
binary_sesnor_entities = [
binary_sensor_entities = [
*mock_add_binary_sensor_entities.mock_calls[0][1][0],
]
sesnor_entities = [
sensor_entities = [
*mock_add_sensor_entities.mock_calls[0][1][0],
]
sensor_entity_one: PassiveBluetoothProcessorEntity = sesnor_entities[0]
sensor_entity_one: PassiveBluetoothProcessorEntity = sensor_entities[0]
sensor_entity_one.hass = hass
assert sensor_entity_one.available is True
assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure"
@ -1167,7 +1193,7 @@ async def test_integration_multiple_entity_platforms(
key="pressure", device_id=None
)
binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sesnor_entities[
binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sensor_entities[
0
]
binary_sensor_entity_one.hass = hass
@ -1242,3 +1268,281 @@ async def test_exception_from_coordinator_update_method(
assert processor.available is True
unregister_processor()
cancel_coordinator()
async def test_integration_multiple_entity_platforms_with_reload_and_restart(
hass: HomeAssistant,
mock_bleak_scanner_start: MagicMock,
mock_bluetooth_adapters: None,
hass_storage: dict[str, Any],
) -> None:
"""Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms with reload."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
entry = MockConfigEntry(domain=DOMAIN, data={})
@callback
def _mock_update_method(
service_info: BluetoothServiceInfo,
) -> dict[str, str]:
return {"test": "data"}
current_entry.set(entry)
coordinator = PassiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
"aa:bb:cc:dd:ee:ff",
BluetoothScanningMode.ACTIVE,
_mock_update_method,
)
assert coordinator.available is False # no data yet
binary_sensor_processor = PassiveBluetoothDataProcessor(
lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE,
BINARY_SENSOR_DOMAIN,
)
sensor_processor = PassiveBluetoothDataProcessor(
lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE, SENSOR_DOMAIN
)
unregister_binary_sensor_processor = coordinator.async_register_processor(
binary_sensor_processor, BinarySensorEntityDescription
)
unregister_sensor_processor = coordinator.async_register_processor(
sensor_processor, SensorEntityDescription
)
cancel_coordinator = coordinator.async_start()
binary_sensor_processor.async_add_listener(MagicMock())
sensor_processor.async_add_listener(MagicMock())
mock_add_sensor_entities = MagicMock()
mock_add_binary_sensor_entities = MagicMock()
sensor_processor.async_add_entities_listener(
PassiveBluetoothProcessorEntity,
mock_add_sensor_entities,
)
binary_sensor_processor.async_add_entities_listener(
PassiveBluetoothProcessorEntity,
mock_add_binary_sensor_entities,
)
inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO)
# First call with just the remote sensor entities results in them being added
assert len(mock_add_binary_sensor_entities.mock_calls) == 1
assert len(mock_add_sensor_entities.mock_calls) == 1
binary_sensor_entities = [
*mock_add_binary_sensor_entities.mock_calls[0][1][0],
]
sensor_entities = [
*mock_add_sensor_entities.mock_calls[0][1][0],
]
sensor_entity_one: PassiveBluetoothProcessorEntity = sensor_entities[0]
sensor_entity_one.hass = hass
assert sensor_entity_one.available is True
assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure"
assert sensor_entity_one.device_info == {
"identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")},
"manufacturer": "Test Manufacturer",
"model": "Test Model",
"name": "Test Device",
}
assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey(
key="pressure", device_id=None
)
binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sensor_entities[
0
]
binary_sensor_entity_one.hass = hass
assert binary_sensor_entity_one.available is True
assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion"
assert binary_sensor_entity_one.device_info == {
"identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")},
"manufacturer": "Test Manufacturer",
"model": "Test Model",
"name": "Test Device",
}
assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey(
key="motion", device_id=None
)
cancel_coordinator()
unregister_binary_sensor_processor()
unregister_sensor_processor()
mock_add_sensor_entities = MagicMock()
mock_add_binary_sensor_entities = MagicMock()
current_entry.set(entry)
coordinator = PassiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
"aa:bb:cc:dd:ee:ff",
BluetoothScanningMode.ACTIVE,
_mock_update_method,
)
binary_sensor_processor = PassiveBluetoothDataProcessor(
lambda service_info: DEVICE_ONLY_PASSIVE_BLUETOOTH_DATA_UPDATE,
BINARY_SENSOR_DOMAIN,
)
sensor_processor = PassiveBluetoothDataProcessor(
lambda service_info: DEVICE_ONLY_PASSIVE_BLUETOOTH_DATA_UPDATE,
SENSOR_DOMAIN,
)
sensor_processor.async_add_entities_listener(
PassiveBluetoothProcessorEntity,
mock_add_sensor_entities,
)
binary_sensor_processor.async_add_entities_listener(
PassiveBluetoothProcessorEntity,
mock_add_binary_sensor_entities,
)
unregister_binary_sensor_processor = coordinator.async_register_processor(
binary_sensor_processor, BinarySensorEntityDescription
)
unregister_sensor_processor = coordinator.async_register_processor(
sensor_processor, SensorEntityDescription
)
cancel_coordinator = coordinator.async_start()
assert len(mock_add_binary_sensor_entities.mock_calls) == 1
assert len(mock_add_sensor_entities.mock_calls) == 1
binary_sensor_entities = [
*mock_add_binary_sensor_entities.mock_calls[0][1][0],
]
sensor_entities = [
*mock_add_sensor_entities.mock_calls[0][1][0],
]
sensor_entity_one: PassiveBluetoothProcessorEntity = sensor_entities[0]
sensor_entity_one.hass = hass
assert sensor_entity_one.available is True
assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure"
assert sensor_entity_one.device_info == {
"identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")},
"manufacturer": "Test Manufacturer",
"model": "Test Model",
"name": "Test Device",
}
assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey(
key="pressure", device_id=None
)
binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sensor_entities[
0
]
binary_sensor_entity_one.hass = hass
assert binary_sensor_entity_one.available is True
assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion"
assert binary_sensor_entity_one.device_info == {
"identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")},
"manufacturer": "Test Manufacturer",
"model": "Test Model",
"name": "Test Device",
}
assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey(
key="motion", device_id=None
)
await hass.async_stop()
await hass.async_block_till_done()
assert SENSOR_DOMAIN in hass_storage[STORAGE_KEY]["data"][entry.entry_id]
assert BINARY_SENSOR_DOMAIN in hass_storage[STORAGE_KEY]["data"][entry.entry_id]
# We don't normally cancel or unregister these at stop,
# but since we are mocking a restart we need to cleanup
cancel_coordinator()
unregister_binary_sensor_processor()
unregister_sensor_processor()
hass = await async_test_home_assistant(asyncio.get_running_loop())
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
current_entry.set(entry)
coordinator = PassiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
"aa:bb:cc:dd:ee:ff",
BluetoothScanningMode.ACTIVE,
_mock_update_method,
)
assert coordinator.available is False # no data yet
mock_add_sensor_entities = MagicMock()
mock_add_binary_sensor_entities = MagicMock()
binary_sensor_processor = PassiveBluetoothDataProcessor(
lambda service_info: DEVICE_ONLY_PASSIVE_BLUETOOTH_DATA_UPDATE,
BINARY_SENSOR_DOMAIN,
)
sensor_processor = PassiveBluetoothDataProcessor(
lambda service_info: DEVICE_ONLY_PASSIVE_BLUETOOTH_DATA_UPDATE,
SENSOR_DOMAIN,
)
sensor_processor.async_add_entities_listener(
PassiveBluetoothProcessorEntity,
mock_add_sensor_entities,
)
binary_sensor_processor.async_add_entities_listener(
PassiveBluetoothProcessorEntity,
mock_add_binary_sensor_entities,
)
unregister_binary_sensor_processor = coordinator.async_register_processor(
binary_sensor_processor, BinarySensorEntityDescription
)
unregister_sensor_processor = coordinator.async_register_processor(
sensor_processor, SensorEntityDescription
)
cancel_coordinator = coordinator.async_start()
assert len(mock_add_binary_sensor_entities.mock_calls) == 1
assert len(mock_add_sensor_entities.mock_calls) == 1
binary_sensor_entities = [
*mock_add_binary_sensor_entities.mock_calls[0][1][0],
]
sensor_entities = [
*mock_add_sensor_entities.mock_calls[0][1][0],
]
sensor_entity_one: PassiveBluetoothProcessorEntity = sensor_entities[0]
sensor_entity_one.hass = hass
assert sensor_entity_one.available is False # service data not injected
assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure"
assert sensor_entity_one.device_info == {
"identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")},
"manufacturer": "Test Manufacturer",
"model": "Test Model",
"name": "Test Device",
}
assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey(
key="pressure", device_id=None
)
binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sensor_entities[
0
]
binary_sensor_entity_one.hass = hass
assert binary_sensor_entity_one.available is False # service data not injected
assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion"
assert binary_sensor_entity_one.device_info == {
"identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")},
"manufacturer": "Test Manufacturer",
"model": "Test Model",
"name": "Test Device",
}
assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey(
key="motion", device_id=None
)
cancel_coordinator()
unregister_binary_sensor_processor()
unregister_sensor_processor()
await hass.async_stop()