Refactor PassiveBluetoothDataUpdateCoordinator to support multiple platforms (#75642)
parent
8300d5b89e
commit
c5afaa2e6a
|
@ -55,32 +55,11 @@ class PassiveBluetoothDataUpdate(Generic[_T]):
|
|||
)
|
||||
|
||||
|
||||
_PassiveBluetoothDataUpdateCoordinatorT = TypeVar(
|
||||
"_PassiveBluetoothDataUpdateCoordinatorT",
|
||||
bound="PassiveBluetoothDataUpdateCoordinator[Any]",
|
||||
)
|
||||
|
||||
|
||||
class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
||||
class PassiveBluetoothDataUpdateCoordinator:
|
||||
"""Passive bluetooth data update coordinator for bluetooth advertisements.
|
||||
|
||||
The coordinator is responsible for keeping track of the bluetooth data,
|
||||
updating subscribers, and device availability.
|
||||
|
||||
The update_method must return a PassiveBluetoothDataUpdate object. Callers
|
||||
are responsible for formatting the data returned from their parser into
|
||||
the appropriate format.
|
||||
|
||||
The coordinator will call the update_method every time the bluetooth device
|
||||
receives a new advertisement with the following signature:
|
||||
|
||||
update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate
|
||||
|
||||
As the size of each advertisement is limited, the update_method should
|
||||
return a PassiveBluetoothDataUpdate object that contains only data that
|
||||
should be updated. The coordinator will then dispatch subscribers based
|
||||
on the data in the PassiveBluetoothDataUpdate object. The accumulated data
|
||||
is available in the devices, entity_data, and entity_descriptions attributes.
|
||||
The coordinator is responsible for dispatching the bluetooth data,
|
||||
to each processor, and tracking devices.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
@ -88,45 +67,22 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
|||
hass: HomeAssistant,
|
||||
logger: logging.Logger,
|
||||
address: str,
|
||||
update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]],
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.hass = hass
|
||||
self.logger = logger
|
||||
self.name: str | None = None
|
||||
self.address = address
|
||||
self._listeners: list[
|
||||
Callable[[PassiveBluetoothDataUpdate[_T] | None], None]
|
||||
] = []
|
||||
self._entity_key_listeners: dict[
|
||||
PassiveBluetoothEntityKey,
|
||||
list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]],
|
||||
] = {}
|
||||
self.update_method = update_method
|
||||
|
||||
self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {}
|
||||
self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {}
|
||||
self.entity_descriptions: dict[
|
||||
PassiveBluetoothEntityKey, EntityDescription
|
||||
] = {}
|
||||
self.devices: dict[str | None, DeviceInfo] = {}
|
||||
|
||||
self.last_update_success = True
|
||||
self._processors: list[PassiveBluetoothDataProcessor] = []
|
||||
self._cancel_track_unavailable: CALLBACK_TYPE | None = None
|
||||
self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None
|
||||
self.present = False
|
||||
self._present = False
|
||||
self.last_seen = 0.0
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the device is available."""
|
||||
return self.present and self.last_update_success
|
||||
|
||||
@callback
|
||||
def _async_handle_unavailable(self, _address: str) -> None:
|
||||
"""Handle the device going unavailable."""
|
||||
self.present = False
|
||||
self.async_update_listeners(None)
|
||||
return self._present
|
||||
|
||||
@callback
|
||||
def _async_start(self) -> None:
|
||||
|
@ -152,10 +108,121 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
|||
self._cancel_track_unavailable()
|
||||
self._cancel_track_unavailable = None
|
||||
|
||||
@callback
|
||||
def async_register_processor(
|
||||
self, processor: PassiveBluetoothDataProcessor
|
||||
) -> Callable[[], None]:
|
||||
"""Register a processor that subscribes to updates."""
|
||||
processor.coordinator = self
|
||||
|
||||
@callback
|
||||
def remove_processor() -> None:
|
||||
"""Remove a processor."""
|
||||
self._processors.remove(processor)
|
||||
self._async_handle_processors_changed()
|
||||
|
||||
self._processors.append(processor)
|
||||
self._async_handle_processors_changed()
|
||||
return remove_processor
|
||||
|
||||
@callback
|
||||
def _async_handle_processors_changed(self) -> None:
|
||||
"""Handle processors changed."""
|
||||
running = bool(self._cancel_bluetooth_advertisements)
|
||||
if running and not self._processors:
|
||||
self._async_stop()
|
||||
elif not running and self._processors:
|
||||
self._async_start()
|
||||
|
||||
@callback
|
||||
def _async_handle_unavailable(self, _address: str) -> None:
|
||||
"""Handle the device going unavailable."""
|
||||
self._present = False
|
||||
for processor in self._processors:
|
||||
processor.async_handle_unavailable()
|
||||
|
||||
@callback
|
||||
def _async_handle_bluetooth_event(
|
||||
self,
|
||||
service_info: BluetoothServiceInfo,
|
||||
change: BluetoothChange,
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
self.last_seen = time.monotonic()
|
||||
self.name = service_info.name
|
||||
self._present = True
|
||||
if self.hass.is_stopping:
|
||||
return
|
||||
for processor in self._processors:
|
||||
processor.async_handle_bluetooth_event(service_info, change)
|
||||
|
||||
|
||||
_PassiveBluetoothDataProcessorT = TypeVar(
|
||||
"_PassiveBluetoothDataProcessorT",
|
||||
bound="PassiveBluetoothDataProcessor[Any]",
|
||||
)
|
||||
|
||||
|
||||
class PassiveBluetoothDataProcessor(Generic[_T]):
|
||||
"""Passive bluetooth data processor for bluetooth advertisements.
|
||||
|
||||
The processor is responsible for keeping track of the bluetooth data
|
||||
and updating subscribers.
|
||||
|
||||
The update_method must return a PassiveBluetoothDataUpdate object. Callers
|
||||
are responsible for formatting the data returned from their parser into
|
||||
the appropriate format.
|
||||
|
||||
The processor will call the update_method every time the bluetooth device
|
||||
receives a new advertisement data from the coordinator with the following signature:
|
||||
|
||||
update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate
|
||||
|
||||
As the size of each advertisement is limited, the update_method should
|
||||
return a PassiveBluetoothDataUpdate object that contains only data that
|
||||
should be updated. The coordinator will then dispatch subscribers based
|
||||
on the data in the PassiveBluetoothDataUpdate object. The accumulated data
|
||||
is available in the devices, entity_data, and entity_descriptions attributes.
|
||||
"""
|
||||
|
||||
coordinator: PassiveBluetoothDataUpdateCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]],
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.coordinator: PassiveBluetoothDataUpdateCoordinator
|
||||
self._listeners: list[
|
||||
Callable[[PassiveBluetoothDataUpdate[_T] | None], None]
|
||||
] = []
|
||||
self._entity_key_listeners: dict[
|
||||
PassiveBluetoothEntityKey,
|
||||
list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]],
|
||||
] = {}
|
||||
self.update_method = update_method
|
||||
self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {}
|
||||
self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {}
|
||||
self.entity_descriptions: dict[
|
||||
PassiveBluetoothEntityKey, EntityDescription
|
||||
] = {}
|
||||
self.devices: dict[str | None, DeviceInfo] = {}
|
||||
self.last_update_success = True
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the device is available."""
|
||||
return self.coordinator.available and self.last_update_success
|
||||
|
||||
@callback
|
||||
def async_handle_unavailable(self) -> None:
|
||||
"""Handle the device going unavailable."""
|
||||
self.async_update_listeners(None)
|
||||
|
||||
@callback
|
||||
def async_add_entities_listener(
|
||||
self,
|
||||
entity_class: type[PassiveBluetoothCoordinatorEntity],
|
||||
entity_class: type[PassiveBluetoothProcessorEntity],
|
||||
async_add_entites: AddEntitiesCallback,
|
||||
) -> Callable[[], None]:
|
||||
"""Add a listener for new entities."""
|
||||
|
@ -168,7 +235,7 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
|||
"""Listen for new entities."""
|
||||
if data is None:
|
||||
return
|
||||
entities: list[PassiveBluetoothCoordinatorEntity] = []
|
||||
entities: list[PassiveBluetoothProcessorEntity] = []
|
||||
for entity_key, description in data.entity_descriptions.items():
|
||||
if entity_key not in created:
|
||||
entities.append(entity_class(self, entity_key, description))
|
||||
|
@ -189,22 +256,10 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
|||
def remove_listener() -> None:
|
||||
"""Remove update listener."""
|
||||
self._listeners.remove(update_callback)
|
||||
self._async_handle_listeners_changed()
|
||||
|
||||
self._listeners.append(update_callback)
|
||||
self._async_handle_listeners_changed()
|
||||
return remove_listener
|
||||
|
||||
@callback
|
||||
def _async_handle_listeners_changed(self) -> None:
|
||||
"""Handle listeners changed."""
|
||||
has_listeners = self._listeners or self._entity_key_listeners
|
||||
running = bool(self._cancel_bluetooth_advertisements)
|
||||
if running and not has_listeners:
|
||||
self._async_stop()
|
||||
elif not running and has_listeners:
|
||||
self._async_start()
|
||||
|
||||
@callback
|
||||
def async_add_entity_key_listener(
|
||||
self,
|
||||
|
@ -219,10 +274,8 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
|||
self._entity_key_listeners[entity_key].remove(update_callback)
|
||||
if not self._entity_key_listeners[entity_key]:
|
||||
del self._entity_key_listeners[entity_key]
|
||||
self._async_handle_listeners_changed()
|
||||
|
||||
self._entity_key_listeners.setdefault(entity_key, []).append(update_callback)
|
||||
self._async_handle_listeners_changed()
|
||||
return remove_listener
|
||||
|
||||
@callback
|
||||
|
@ -240,36 +293,32 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
|||
update_callback(data)
|
||||
|
||||
@callback
|
||||
def _async_handle_bluetooth_event(
|
||||
def async_handle_bluetooth_event(
|
||||
self,
|
||||
service_info: BluetoothServiceInfo,
|
||||
change: BluetoothChange,
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
self.last_seen = time.monotonic()
|
||||
self.name = service_info.name
|
||||
self.present = True
|
||||
if self.hass.is_stopping:
|
||||
return
|
||||
|
||||
try:
|
||||
new_data = self.update_method(service_info)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
self.last_update_success = False
|
||||
self.logger.exception(
|
||||
"Unexpected error updating %s data: %s", self.name, err
|
||||
self.coordinator.logger.exception(
|
||||
"Unexpected error updating %s data: %s", self.coordinator.name, err
|
||||
)
|
||||
return
|
||||
|
||||
if not isinstance(new_data, PassiveBluetoothDataUpdate):
|
||||
self.last_update_success = False # type: ignore[unreachable]
|
||||
raise ValueError(
|
||||
f"The update_method for {self.name} returned {new_data} instead of a PassiveBluetoothDataUpdate"
|
||||
f"The update_method for {self.coordinator.name} returned {new_data} instead of a PassiveBluetoothDataUpdate"
|
||||
)
|
||||
|
||||
if not self.last_update_success:
|
||||
self.last_update_success = True
|
||||
self.logger.info("Processing %s data recovered", self.name)
|
||||
self.coordinator.logger.info(
|
||||
"Processing %s data recovered", self.coordinator.name
|
||||
)
|
||||
|
||||
self.devices.update(new_data.devices)
|
||||
self.entity_descriptions.update(new_data.entity_descriptions)
|
||||
|
@ -278,29 +327,27 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
|||
self.async_update_listeners(new_data)
|
||||
|
||||
|
||||
class PassiveBluetoothCoordinatorEntity(
|
||||
Entity, Generic[_PassiveBluetoothDataUpdateCoordinatorT]
|
||||
):
|
||||
"""A class for entities using PassiveBluetoothDataUpdateCoordinator."""
|
||||
class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProcessorT]):
|
||||
"""A class for entities using PassiveBluetoothDataProcessor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: _PassiveBluetoothDataUpdateCoordinatorT,
|
||||
processor: _PassiveBluetoothDataProcessorT,
|
||||
entity_key: PassiveBluetoothEntityKey,
|
||||
description: EntityDescription,
|
||||
context: Any = None,
|
||||
) -> None:
|
||||
"""Create the entity with a PassiveBluetoothDataUpdateCoordinator."""
|
||||
"""Create the entity with a PassiveBluetoothDataProcessor."""
|
||||
self.entity_description = description
|
||||
self.entity_key = entity_key
|
||||
self.coordinator = coordinator
|
||||
self.coordinator_context = context
|
||||
address = coordinator.address
|
||||
self.processor = processor
|
||||
self.processor_context = context
|
||||
address = processor.coordinator.address
|
||||
device_id = entity_key.device_id
|
||||
devices = coordinator.devices
|
||||
devices = processor.devices
|
||||
key = entity_key.key
|
||||
if device_id in devices:
|
||||
base_device_info = devices[device_id]
|
||||
|
@ -317,26 +364,26 @@ class PassiveBluetoothCoordinatorEntity(
|
|||
)
|
||||
self._attr_unique_id = f"{address}-{key}"
|
||||
if ATTR_NAME not in self._attr_device_info:
|
||||
self._attr_device_info[ATTR_NAME] = self.coordinator.name
|
||||
self._attr_name = coordinator.entity_names.get(entity_key)
|
||||
self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name
|
||||
self._attr_name = processor.entity_names.get(entity_key)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return self.coordinator.available
|
||||
return self.processor.available
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self.coordinator.async_add_entity_key_listener(
|
||||
self._handle_coordinator_update, self.entity_key
|
||||
self.processor.async_add_entity_key_listener(
|
||||
self._handle_processor_update, self.entity_key
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(
|
||||
def _handle_processor_update(
|
||||
self, new_data: PassiveBluetoothDataUpdate | None
|
||||
) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
"""Handle updated data from the processor."""
|
||||
self.async_write_ha_state()
|
||||
|
|
|
@ -3,19 +3,14 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
from inkbird_ble import INKBIRDBluetoothDeviceData
|
||||
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothDataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .sensor import sensor_update_to_bluetooth_data_update
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
@ -26,22 +21,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
"""Set up INKBIRD BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
|
||||
data = INKBIRDBluetoothDeviceData()
|
||||
|
||||
@callback
|
||||
def _async_update_data(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Update data from INKBIRD Bluetooth."""
|
||||
return sensor_update_to_bluetooth_data_update(data.update(service_info))
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[
|
||||
entry.entry_id
|
||||
] = PassiveBluetoothDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
update_method=_async_update_data,
|
||||
address=address,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
|
|
@ -3,14 +3,22 @@ from __future__ import annotations
|
|||
|
||||
from typing import Optional, Union
|
||||
|
||||
from inkbird_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
|
||||
from inkbird_ble import (
|
||||
DeviceClass,
|
||||
DeviceKey,
|
||||
INKBIRDBluetoothDeviceData,
|
||||
SensorDeviceInfo,
|
||||
SensorUpdate,
|
||||
Units,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
PassiveBluetoothCoordinatorEntity,
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothDataUpdateCoordinator,
|
||||
PassiveBluetoothEntityKey,
|
||||
PassiveBluetoothProcessorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
|
@ -121,16 +129,23 @@ async def async_setup_entry(
|
|||
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
data = INKBIRDBluetoothDeviceData()
|
||||
processor = PassiveBluetoothDataProcessor(
|
||||
lambda service_info: sensor_update_to_bluetooth_data_update(
|
||||
data.update(service_info)
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
entry.async_on_unload(
|
||||
coordinator.async_add_entities_listener(
|
||||
processor.async_add_entities_listener(
|
||||
INKBIRDBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class INKBIRDBluetoothSensorEntity(
|
||||
PassiveBluetoothCoordinatorEntity[
|
||||
PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]]
|
||||
PassiveBluetoothProcessorEntity[
|
||||
PassiveBluetoothDataProcessor[Optional[Union[float, int]]]
|
||||
],
|
||||
SensorEntity,
|
||||
):
|
||||
|
@ -139,4 +154,4 @@ class INKBIRDBluetoothSensorEntity(
|
|||
@property
|
||||
def native_value(self) -> int | float | None:
|
||||
"""Return the native value."""
|
||||
return self.coordinator.entity_data.get(self.entity_key)
|
||||
return self.processor.entity_data.get(self.entity_key)
|
||||
|
|
|
@ -3,19 +3,14 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
from sensorpush_ble import SensorPushBluetoothDeviceData
|
||||
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothDataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .sensor import sensor_update_to_bluetooth_data_update
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
@ -26,22 +21,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
"""Set up SensorPush BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
|
||||
data = SensorPushBluetoothDeviceData()
|
||||
|
||||
@callback
|
||||
def _async_update_data(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Update data from SensorPush Bluetooth."""
|
||||
return sensor_update_to_bluetooth_data_update(data.update(service_info))
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[
|
||||
entry.entry_id
|
||||
] = PassiveBluetoothDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
update_method=_async_update_data,
|
||||
address=address,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
|
|
@ -3,14 +3,22 @@ from __future__ import annotations
|
|||
|
||||
from typing import Optional, Union
|
||||
|
||||
from sensorpush_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
|
||||
from sensorpush_ble import (
|
||||
DeviceClass,
|
||||
DeviceKey,
|
||||
SensorDeviceInfo,
|
||||
SensorPushBluetoothDeviceData,
|
||||
SensorUpdate,
|
||||
Units,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
PassiveBluetoothCoordinatorEntity,
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothDataUpdateCoordinator,
|
||||
PassiveBluetoothEntityKey,
|
||||
PassiveBluetoothProcessorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
|
@ -122,16 +130,23 @@ async def async_setup_entry(
|
|||
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
data = SensorPushBluetoothDeviceData()
|
||||
processor = PassiveBluetoothDataProcessor(
|
||||
lambda service_info: sensor_update_to_bluetooth_data_update(
|
||||
data.update(service_info)
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
entry.async_on_unload(
|
||||
coordinator.async_add_entities_listener(
|
||||
processor.async_add_entities_listener(
|
||||
SensorPushBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class SensorPushBluetoothSensorEntity(
|
||||
PassiveBluetoothCoordinatorEntity[
|
||||
PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]]
|
||||
PassiveBluetoothProcessorEntity[
|
||||
PassiveBluetoothDataProcessor[Optional[Union[float, int]]]
|
||||
],
|
||||
SensorEntity,
|
||||
):
|
||||
|
@ -140,4 +155,4 @@ class SensorPushBluetoothSensorEntity(
|
|||
@property
|
||||
def native_value(self) -> int | float | None:
|
||||
"""Return the native value."""
|
||||
return self.coordinator.entity_data.get(self.entity_key)
|
||||
return self.processor.entity_data.get(self.entity_key)
|
||||
|
|
|
@ -3,19 +3,14 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
from xiaomi_ble import XiaomiBluetoothDeviceData
|
||||
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothDataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .sensor import sensor_update_to_bluetooth_data_update
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
@ -26,22 +21,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
"""Set up Xiaomi BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
|
||||
data = XiaomiBluetoothDeviceData()
|
||||
|
||||
@callback
|
||||
def _async_update_data(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Update data from Xiaomi Bluetooth."""
|
||||
return sensor_update_to_bluetooth_data_update(data.update(service_info))
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[
|
||||
entry.entry_id
|
||||
] = PassiveBluetoothDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
update_method=_async_update_data,
|
||||
address=address,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
|
|
@ -3,14 +3,22 @@ from __future__ import annotations
|
|||
|
||||
from typing import Optional, Union
|
||||
|
||||
from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
|
||||
from xiaomi_ble import (
|
||||
DeviceClass,
|
||||
DeviceKey,
|
||||
SensorDeviceInfo,
|
||||
SensorUpdate,
|
||||
Units,
|
||||
XiaomiBluetoothDeviceData,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
PassiveBluetoothCoordinatorEntity,
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothDataUpdateCoordinator,
|
||||
PassiveBluetoothEntityKey,
|
||||
PassiveBluetoothProcessorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
|
@ -150,16 +158,23 @@ async def async_setup_entry(
|
|||
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
data = XiaomiBluetoothDeviceData()
|
||||
processor = PassiveBluetoothDataProcessor(
|
||||
lambda service_info: sensor_update_to_bluetooth_data_update(
|
||||
data.update(service_info)
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
entry.async_on_unload(
|
||||
coordinator.async_add_entities_listener(
|
||||
processor.async_add_entities_listener(
|
||||
XiaomiBluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class XiaomiBluetoothSensorEntity(
|
||||
PassiveBluetoothCoordinatorEntity[
|
||||
PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]]
|
||||
PassiveBluetoothProcessorEntity[
|
||||
PassiveBluetoothDataProcessor[Optional[Union[float, int]]]
|
||||
],
|
||||
SensorEntity,
|
||||
):
|
||||
|
@ -168,4 +183,4 @@ class XiaomiBluetoothSensorEntity(
|
|||
@property
|
||||
def native_value(self) -> int | float | None:
|
||||
"""Return the native value."""
|
||||
return self.coordinator.entity_data.get(self.entity_key)
|
||||
return self.processor.entity_data.get(self.entity_key)
|
||||
|
|
|
@ -8,16 +8,21 @@ from unittest.mock import MagicMock, patch
|
|||
from home_assistant_bluetooth import BluetoothServiceInfo
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.components.bluetooth import (
|
||||
DOMAIN,
|
||||
UNAVAILABLE_TRACK_SECONDS,
|
||||
BluetoothChange,
|
||||
)
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
PassiveBluetoothCoordinatorEntity,
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothDataUpdateCoordinator,
|
||||
PassiveBluetoothEntityKey,
|
||||
PassiveBluetoothProcessorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
|
@ -85,7 +90,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
|
|||
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff"
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -95,10 +100,13 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
|
|||
saved_callback = _callback
|
||||
return lambda: None
|
||||
|
||||
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||
_async_register_callback,
|
||||
):
|
||||
unregister_processor = coordinator.async_register_processor(processor)
|
||||
|
||||
entity_key = PassiveBluetoothEntityKey("temperature", None)
|
||||
entity_key_events = []
|
||||
|
@ -110,22 +118,20 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
|
|||
"""Mock entity key listener."""
|
||||
entity_key_events.append(data)
|
||||
|
||||
cancel_async_add_entity_key_listener = (
|
||||
coordinator.async_add_entity_key_listener(
|
||||
cancel_async_add_entity_key_listener = processor.async_add_entity_key_listener(
|
||||
_async_entity_key_listener,
|
||||
entity_key,
|
||||
)
|
||||
)
|
||||
|
||||
def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None:
|
||||
"""Mock an all listener."""
|
||||
all_events.append(data)
|
||||
|
||||
cancel_listener = coordinator.async_add_listener(
|
||||
cancel_listener = processor.async_add_listener(
|
||||
_all_listener,
|
||||
)
|
||||
|
||||
cancel_async_add_entities_listener = coordinator.async_add_entities_listener(
|
||||
cancel_async_add_entities_listener = processor.async_add_entities_listener(
|
||||
mock_entity,
|
||||
mock_add_entities,
|
||||
)
|
||||
|
@ -164,6 +170,8 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
|
|||
assert len(mock_entity.mock_calls) == 2
|
||||
assert coordinator.available is True
|
||||
|
||||
unregister_processor()
|
||||
|
||||
|
||||
async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
||||
"""Test that the coordinator is unavailable after no data for a while."""
|
||||
|
@ -182,7 +190,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
|||
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff"
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -192,23 +200,27 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
|||
saved_callback = _callback
|
||||
return lambda: None
|
||||
|
||||
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||
_async_register_callback,
|
||||
):
|
||||
unregister_processor = coordinator.async_register_processor(processor)
|
||||
|
||||
mock_entity = MagicMock()
|
||||
mock_add_entities = MagicMock()
|
||||
coordinator.async_add_entities_listener(
|
||||
processor.async_add_entities_listener(
|
||||
mock_entity,
|
||||
mock_add_entities,
|
||||
)
|
||||
|
||||
assert coordinator.available is False
|
||||
assert processor.available is False
|
||||
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
assert len(mock_add_entities.mock_calls) == 1
|
||||
assert coordinator.available is True
|
||||
assert processor.available is True
|
||||
scanner = _get_underlying_scanner()
|
||||
|
||||
with patch(
|
||||
|
@ -224,10 +236,12 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
assert coordinator.available is False
|
||||
assert processor.available is False
|
||||
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
assert len(mock_add_entities.mock_calls) == 1
|
||||
assert coordinator.available is True
|
||||
assert processor.available is True
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices",
|
||||
|
@ -242,6 +256,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
assert coordinator.available is False
|
||||
assert processor.available is False
|
||||
|
||||
unregister_processor()
|
||||
|
||||
|
||||
async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
|
||||
|
@ -256,7 +273,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
|
|||
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff"
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -266,10 +283,13 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
|
|||
saved_callback = _callback
|
||||
return lambda: None
|
||||
|
||||
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||
_async_register_callback,
|
||||
):
|
||||
unregister_processor = coordinator.async_register_processor(processor)
|
||||
|
||||
all_events = []
|
||||
|
||||
|
@ -277,7 +297,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
|
|||
"""Mock an all listener."""
|
||||
all_events.append(data)
|
||||
|
||||
coordinator.async_add_listener(
|
||||
processor.async_add_listener(
|
||||
_all_listener,
|
||||
)
|
||||
|
||||
|
@ -289,6 +309,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
|
|||
# We should stop processing events once hass is stopping
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
assert len(all_events) == 1
|
||||
unregister_processor()
|
||||
|
||||
|
||||
async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start):
|
||||
|
@ -309,7 +330,7 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta
|
|||
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff"
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -319,23 +340,27 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta
|
|||
saved_callback = _callback
|
||||
return lambda: None
|
||||
|
||||
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||
_async_register_callback,
|
||||
):
|
||||
coordinator.async_add_listener(MagicMock())
|
||||
unregister_processor = coordinator.async_register_processor(processor)
|
||||
|
||||
processor.async_add_listener(MagicMock())
|
||||
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
assert coordinator.available is True
|
||||
assert processor.available is True
|
||||
|
||||
# We should go unavailable once we get an exception
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
assert "Test exception" in caplog.text
|
||||
assert coordinator.available is False
|
||||
assert processor.available is False
|
||||
|
||||
# We should go available again once we get data again
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
assert coordinator.available is True
|
||||
assert processor.available is True
|
||||
unregister_processor()
|
||||
|
||||
|
||||
async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start):
|
||||
|
@ -356,7 +381,7 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start):
|
|||
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff"
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -366,24 +391,28 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start):
|
|||
saved_callback = _callback
|
||||
return lambda: None
|
||||
|
||||
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||
_async_register_callback,
|
||||
):
|
||||
coordinator.async_add_listener(MagicMock())
|
||||
unregister_processor = coordinator.async_register_processor(processor)
|
||||
|
||||
processor.async_add_listener(MagicMock())
|
||||
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
assert coordinator.available is True
|
||||
assert processor.available is True
|
||||
|
||||
# We should go unavailable once we get bad data
|
||||
with pytest.raises(ValueError):
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
|
||||
assert coordinator.available is False
|
||||
assert processor.available is False
|
||||
|
||||
# We should go available again once we get good data again
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
assert coordinator.available is True
|
||||
assert processor.available is True
|
||||
unregister_processor()
|
||||
|
||||
|
||||
GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo(
|
||||
|
@ -692,7 +721,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start):
|
|||
return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff"
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -702,16 +731,19 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start):
|
|||
saved_callback = _callback
|
||||
return lambda: None
|
||||
|
||||
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||
_async_register_callback,
|
||||
):
|
||||
coordinator.async_add_listener(MagicMock())
|
||||
coordinator.async_register_processor(processor)
|
||||
|
||||
processor.async_add_listener(MagicMock())
|
||||
|
||||
mock_add_entities = MagicMock()
|
||||
|
||||
coordinator.async_add_entities_listener(
|
||||
PassiveBluetoothCoordinatorEntity,
|
||||
processor.async_add_entities_listener(
|
||||
PassiveBluetoothProcessorEntity,
|
||||
mock_add_entities,
|
||||
)
|
||||
|
||||
|
@ -736,7 +768,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start):
|
|||
*mock_add_entities.mock_calls[1][1][0],
|
||||
]
|
||||
|
||||
entity_one: PassiveBluetoothCoordinatorEntity = entities[0]
|
||||
entity_one: PassiveBluetoothProcessorEntity = entities[0]
|
||||
entity_one.hass = hass
|
||||
assert entity_one.available is True
|
||||
assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature-remote"
|
||||
|
@ -797,7 +829,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner
|
|||
return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff"
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -807,15 +839,17 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner
|
|||
saved_callback = _callback
|
||||
return lambda: None
|
||||
|
||||
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||
_async_register_callback,
|
||||
):
|
||||
coordinator.async_register_processor(processor)
|
||||
|
||||
mock_add_entities = MagicMock()
|
||||
|
||||
coordinator.async_add_entities_listener(
|
||||
PassiveBluetoothCoordinatorEntity,
|
||||
processor.async_add_entities_listener(
|
||||
PassiveBluetoothProcessorEntity,
|
||||
mock_add_entities,
|
||||
)
|
||||
|
||||
|
@ -828,7 +862,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner
|
|||
assert len(mock_add_entities.mock_calls) == 1
|
||||
|
||||
entities = mock_add_entities.mock_calls[0][1][0]
|
||||
entity_one: PassiveBluetoothCoordinatorEntity = entities[0]
|
||||
entity_one: PassiveBluetoothProcessorEntity = entities[0]
|
||||
entity_one.hass = hass
|
||||
assert entity_one.available is True
|
||||
assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature"
|
||||
|
@ -857,7 +891,7 @@ async def test_passive_bluetooth_entity_with_entity_platform(
|
|||
return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff"
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -867,18 +901,19 @@ async def test_passive_bluetooth_entity_with_entity_platform(
|
|||
saved_callback = _callback
|
||||
return lambda: None
|
||||
|
||||
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||
_async_register_callback,
|
||||
):
|
||||
coordinator.async_register_processor(processor)
|
||||
|
||||
coordinator.async_add_entities_listener(
|
||||
PassiveBluetoothCoordinatorEntity,
|
||||
processor.async_add_entities_listener(
|
||||
PassiveBluetoothProcessorEntity,
|
||||
lambda entities: hass.async_create_task(
|
||||
entity_platform.async_add_entities(entities)
|
||||
),
|
||||
)
|
||||
|
||||
saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
await hass.async_block_till_done()
|
||||
saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
|
@ -891,3 +926,133 @@ async def test_passive_bluetooth_entity_with_entity_platform(
|
|||
hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure")
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate(
|
||||
devices={
|
||||
None: DeviceInfo(
|
||||
name="Test Device", model="Test Model", manufacturer="Test Manufacturer"
|
||||
),
|
||||
},
|
||||
entity_data={
|
||||
PassiveBluetoothEntityKey("pressure", None): 1234,
|
||||
},
|
||||
entity_names={
|
||||
PassiveBluetoothEntityKey("pressure", None): "Pressure",
|
||||
},
|
||||
entity_descriptions={
|
||||
PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription(
|
||||
key="pressure",
|
||||
native_unit_of_measurement="hPa",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate(
|
||||
devices={
|
||||
None: DeviceInfo(
|
||||
name="Test Device", model="Test Model", manufacturer="Test Manufacturer"
|
||||
),
|
||||
},
|
||||
entity_data={
|
||||
PassiveBluetoothEntityKey("motion", None): True,
|
||||
},
|
||||
entity_names={
|
||||
PassiveBluetoothEntityKey("motion", None): "Motion",
|
||||
},
|
||||
entity_descriptions={
|
||||
PassiveBluetoothEntityKey("motion", None): BinarySensorEntityDescription(
|
||||
key="motion",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_start):
|
||||
"""Test integration of PassiveBluetoothDataUpdateCoordinator with multiple platforms."""
|
||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
|
||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff"
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
||||
def _async_register_callback(_hass, _callback, _matcher):
|
||||
nonlocal saved_callback
|
||||
saved_callback = _callback
|
||||
return lambda: None
|
||||
|
||||
binary_sensor_processor = PassiveBluetoothDataProcessor(
|
||||
lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
)
|
||||
sesnor_processor = PassiveBluetoothDataProcessor(
|
||||
lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||
_async_register_callback,
|
||||
):
|
||||
coordinator.async_register_processor(binary_sensor_processor)
|
||||
coordinator.async_register_processor(sesnor_processor)
|
||||
|
||||
binary_sensor_processor.async_add_listener(MagicMock())
|
||||
sesnor_processor.async_add_listener(MagicMock())
|
||||
|
||||
mock_add_sensor_entities = MagicMock()
|
||||
mock_add_binary_sensor_entities = MagicMock()
|
||||
|
||||
sesnor_processor.async_add_entities_listener(
|
||||
PassiveBluetoothProcessorEntity,
|
||||
mock_add_sensor_entities,
|
||||
)
|
||||
binary_sensor_processor.async_add_entities_listener(
|
||||
PassiveBluetoothProcessorEntity,
|
||||
mock_add_binary_sensor_entities,
|
||||
)
|
||||
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
# 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_sesnor_entities = [
|
||||
*mock_add_binary_sensor_entities.mock_calls[0][1][0],
|
||||
]
|
||||
sesnor_entities = [
|
||||
*mock_add_sensor_entities.mock_calls[0][1][0],
|
||||
]
|
||||
|
||||
sensor_entity_one: PassiveBluetoothProcessorEntity = sesnor_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_sesnor_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
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue