mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Restore passive bluetooth entity data at startup (#97462)
This commit is contained in:
parent
1adfa6bbeb
commit
56257b7a38
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user