Refactor PassiveBluetoothDataUpdateCoordinator to support multiple platforms (#75642)
This commit is contained in:
parent
8300d5b89e
commit
c5afaa2e6a
8 changed files with 443 additions and 234 deletions
|
@ -55,32 +55,11 @@ class PassiveBluetoothDataUpdate(Generic[_T]):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
_PassiveBluetoothDataUpdateCoordinatorT = TypeVar(
|
class PassiveBluetoothDataUpdateCoordinator:
|
||||||
"_PassiveBluetoothDataUpdateCoordinatorT",
|
|
||||||
bound="PassiveBluetoothDataUpdateCoordinator[Any]",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
|
||||||
"""Passive bluetooth data update coordinator for bluetooth advertisements.
|
"""Passive bluetooth data update coordinator for bluetooth advertisements.
|
||||||
|
|
||||||
The coordinator is responsible for keeping track of the bluetooth data,
|
The coordinator is responsible for dispatching the bluetooth data,
|
||||||
updating subscribers, and device availability.
|
to each processor, and tracking devices.
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -88,45 +67,22 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
logger: logging.Logger,
|
logger: logging.Logger,
|
||||||
address: str,
|
address: str,
|
||||||
update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the coordinator."""
|
"""Initialize the coordinator."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.name: str | None = None
|
self.name: str | None = None
|
||||||
self.address = address
|
self.address = address
|
||||||
self._listeners: list[
|
self._processors: list[PassiveBluetoothDataProcessor] = []
|
||||||
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._cancel_track_unavailable: CALLBACK_TYPE | None = None
|
self._cancel_track_unavailable: CALLBACK_TYPE | None = None
|
||||||
self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None
|
self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None
|
||||||
self.present = False
|
self._present = False
|
||||||
self.last_seen = 0.0
|
self.last_seen = 0.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return if the device is available."""
|
"""Return if the device is available."""
|
||||||
return self.present and self.last_update_success
|
return self._present
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_handle_unavailable(self, _address: str) -> None:
|
|
||||||
"""Handle the device going unavailable."""
|
|
||||||
self.present = False
|
|
||||||
self.async_update_listeners(None)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_start(self) -> None:
|
def _async_start(self) -> None:
|
||||||
|
@ -152,10 +108,121 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
||||||
self._cancel_track_unavailable()
|
self._cancel_track_unavailable()
|
||||||
self._cancel_track_unavailable = None
|
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
|
@callback
|
||||||
def async_add_entities_listener(
|
def async_add_entities_listener(
|
||||||
self,
|
self,
|
||||||
entity_class: type[PassiveBluetoothCoordinatorEntity],
|
entity_class: type[PassiveBluetoothProcessorEntity],
|
||||||
async_add_entites: AddEntitiesCallback,
|
async_add_entites: AddEntitiesCallback,
|
||||||
) -> Callable[[], None]:
|
) -> Callable[[], None]:
|
||||||
"""Add a listener for new entities."""
|
"""Add a listener for new entities."""
|
||||||
|
@ -168,7 +235,7 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
||||||
"""Listen for new entities."""
|
"""Listen for new entities."""
|
||||||
if data is None:
|
if data is None:
|
||||||
return
|
return
|
||||||
entities: list[PassiveBluetoothCoordinatorEntity] = []
|
entities: list[PassiveBluetoothProcessorEntity] = []
|
||||||
for entity_key, description in data.entity_descriptions.items():
|
for entity_key, description in data.entity_descriptions.items():
|
||||||
if entity_key not in created:
|
if entity_key not in created:
|
||||||
entities.append(entity_class(self, entity_key, description))
|
entities.append(entity_class(self, entity_key, description))
|
||||||
|
@ -189,22 +256,10 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
||||||
def remove_listener() -> None:
|
def remove_listener() -> None:
|
||||||
"""Remove update listener."""
|
"""Remove update listener."""
|
||||||
self._listeners.remove(update_callback)
|
self._listeners.remove(update_callback)
|
||||||
self._async_handle_listeners_changed()
|
|
||||||
|
|
||||||
self._listeners.append(update_callback)
|
self._listeners.append(update_callback)
|
||||||
self._async_handle_listeners_changed()
|
|
||||||
return remove_listener
|
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
|
@callback
|
||||||
def async_add_entity_key_listener(
|
def async_add_entity_key_listener(
|
||||||
self,
|
self,
|
||||||
|
@ -219,10 +274,8 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
||||||
self._entity_key_listeners[entity_key].remove(update_callback)
|
self._entity_key_listeners[entity_key].remove(update_callback)
|
||||||
if not self._entity_key_listeners[entity_key]:
|
if not self._entity_key_listeners[entity_key]:
|
||||||
del 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._entity_key_listeners.setdefault(entity_key, []).append(update_callback)
|
||||||
self._async_handle_listeners_changed()
|
|
||||||
return remove_listener
|
return remove_listener
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -240,36 +293,32 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
||||||
update_callback(data)
|
update_callback(data)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_handle_bluetooth_event(
|
def async_handle_bluetooth_event(
|
||||||
self,
|
self,
|
||||||
service_info: BluetoothServiceInfo,
|
service_info: BluetoothServiceInfo,
|
||||||
change: BluetoothChange,
|
change: BluetoothChange,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle a Bluetooth event."""
|
"""Handle a Bluetooth event."""
|
||||||
self.last_seen = time.monotonic()
|
|
||||||
self.name = service_info.name
|
|
||||||
self.present = True
|
|
||||||
if self.hass.is_stopping:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_data = self.update_method(service_info)
|
new_data = self.update_method(service_info)
|
||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
self.last_update_success = False
|
self.last_update_success = False
|
||||||
self.logger.exception(
|
self.coordinator.logger.exception(
|
||||||
"Unexpected error updating %s data: %s", self.name, err
|
"Unexpected error updating %s data: %s", self.coordinator.name, err
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(new_data, PassiveBluetoothDataUpdate):
|
if not isinstance(new_data, PassiveBluetoothDataUpdate):
|
||||||
self.last_update_success = False # type: ignore[unreachable]
|
self.last_update_success = False # type: ignore[unreachable]
|
||||||
raise ValueError(
|
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:
|
if not self.last_update_success:
|
||||||
self.last_update_success = True
|
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.devices.update(new_data.devices)
|
||||||
self.entity_descriptions.update(new_data.entity_descriptions)
|
self.entity_descriptions.update(new_data.entity_descriptions)
|
||||||
|
@ -278,29 +327,27 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]):
|
||||||
self.async_update_listeners(new_data)
|
self.async_update_listeners(new_data)
|
||||||
|
|
||||||
|
|
||||||
class PassiveBluetoothCoordinatorEntity(
|
class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProcessorT]):
|
||||||
Entity, Generic[_PassiveBluetoothDataUpdateCoordinatorT]
|
"""A class for entities using PassiveBluetoothDataProcessor."""
|
||||||
):
|
|
||||||
"""A class for entities using PassiveBluetoothDataUpdateCoordinator."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: _PassiveBluetoothDataUpdateCoordinatorT,
|
processor: _PassiveBluetoothDataProcessorT,
|
||||||
entity_key: PassiveBluetoothEntityKey,
|
entity_key: PassiveBluetoothEntityKey,
|
||||||
description: EntityDescription,
|
description: EntityDescription,
|
||||||
context: Any = None,
|
context: Any = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create the entity with a PassiveBluetoothDataUpdateCoordinator."""
|
"""Create the entity with a PassiveBluetoothDataProcessor."""
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self.entity_key = entity_key
|
self.entity_key = entity_key
|
||||||
self.coordinator = coordinator
|
self.processor = processor
|
||||||
self.coordinator_context = context
|
self.processor_context = context
|
||||||
address = coordinator.address
|
address = processor.coordinator.address
|
||||||
device_id = entity_key.device_id
|
device_id = entity_key.device_id
|
||||||
devices = coordinator.devices
|
devices = processor.devices
|
||||||
key = entity_key.key
|
key = entity_key.key
|
||||||
if device_id in devices:
|
if device_id in devices:
|
||||||
base_device_info = devices[device_id]
|
base_device_info = devices[device_id]
|
||||||
|
@ -317,26 +364,26 @@ class PassiveBluetoothCoordinatorEntity(
|
||||||
)
|
)
|
||||||
self._attr_unique_id = f"{address}-{key}"
|
self._attr_unique_id = f"{address}-{key}"
|
||||||
if ATTR_NAME not in self._attr_device_info:
|
if ATTR_NAME not in self._attr_device_info:
|
||||||
self._attr_device_info[ATTR_NAME] = self.coordinator.name
|
self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name
|
||||||
self._attr_name = coordinator.entity_names.get(entity_key)
|
self._attr_name = processor.entity_names.get(entity_key)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return if entity is available."""
|
"""Return if entity is available."""
|
||||||
return self.coordinator.available
|
return self.processor.available
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""When entity is added to hass."""
|
"""When entity is added to hass."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
self.coordinator.async_add_entity_key_listener(
|
self.processor.async_add_entity_key_listener(
|
||||||
self._handle_coordinator_update, self.entity_key
|
self._handle_processor_update, self.entity_key
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(
|
def _handle_processor_update(
|
||||||
self, new_data: PassiveBluetoothDataUpdate | None
|
self, new_data: PassiveBluetoothDataUpdate | None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle updated data from the coordinator."""
|
"""Handle updated data from the processor."""
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
|
@ -3,19 +3,14 @@ from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from inkbird_ble import INKBIRDBluetoothDeviceData
|
|
||||||
|
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||||
PassiveBluetoothDataUpdate,
|
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
PassiveBluetoothDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .sensor import sensor_update_to_bluetooth_data_update
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
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."""
|
"""Set up INKBIRD BLE device from a config entry."""
|
||||||
address = entry.unique_id
|
address = entry.unique_id
|
||||||
assert address is not None
|
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, {})[
|
hass.data.setdefault(DOMAIN, {})[
|
||||||
entry.entry_id
|
entry.entry_id
|
||||||
] = PassiveBluetoothDataUpdateCoordinator(
|
] = PassiveBluetoothDataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
update_method=_async_update_data,
|
|
||||||
address=address,
|
address=address,
|
||||||
)
|
)
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
|
@ -3,14 +3,22 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional, Union
|
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 import config_entries
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||||
PassiveBluetoothCoordinatorEntity,
|
PassiveBluetoothDataProcessor,
|
||||||
PassiveBluetoothDataUpdate,
|
PassiveBluetoothDataUpdate,
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
PassiveBluetoothDataUpdateCoordinator,
|
||||||
PassiveBluetoothEntityKey,
|
PassiveBluetoothEntityKey,
|
||||||
|
PassiveBluetoothProcessorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
|
@ -121,16 +129,23 @@ async def async_setup_entry(
|
||||||
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][
|
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][
|
||||||
entry.entry_id
|
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(
|
entry.async_on_unload(
|
||||||
coordinator.async_add_entities_listener(
|
processor.async_add_entities_listener(
|
||||||
INKBIRDBluetoothSensorEntity, async_add_entities
|
INKBIRDBluetoothSensorEntity, async_add_entities
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class INKBIRDBluetoothSensorEntity(
|
class INKBIRDBluetoothSensorEntity(
|
||||||
PassiveBluetoothCoordinatorEntity[
|
PassiveBluetoothProcessorEntity[
|
||||||
PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]]
|
PassiveBluetoothDataProcessor[Optional[Union[float, int]]]
|
||||||
],
|
],
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
):
|
):
|
||||||
|
@ -139,4 +154,4 @@ class INKBIRDBluetoothSensorEntity(
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> int | float | None:
|
def native_value(self) -> int | float | None:
|
||||||
"""Return the native value."""
|
"""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
|
import logging
|
||||||
|
|
||||||
from sensorpush_ble import SensorPushBluetoothDeviceData
|
|
||||||
|
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||||
PassiveBluetoothDataUpdate,
|
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
PassiveBluetoothDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .sensor import sensor_update_to_bluetooth_data_update
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
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."""
|
"""Set up SensorPush BLE device from a config entry."""
|
||||||
address = entry.unique_id
|
address = entry.unique_id
|
||||||
assert address is not None
|
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, {})[
|
hass.data.setdefault(DOMAIN, {})[
|
||||||
entry.entry_id
|
entry.entry_id
|
||||||
] = PassiveBluetoothDataUpdateCoordinator(
|
] = PassiveBluetoothDataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
update_method=_async_update_data,
|
|
||||||
address=address,
|
address=address,
|
||||||
)
|
)
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
|
@ -3,14 +3,22 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional, Union
|
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 import config_entries
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||||
PassiveBluetoothCoordinatorEntity,
|
PassiveBluetoothDataProcessor,
|
||||||
PassiveBluetoothDataUpdate,
|
PassiveBluetoothDataUpdate,
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
PassiveBluetoothDataUpdateCoordinator,
|
||||||
PassiveBluetoothEntityKey,
|
PassiveBluetoothEntityKey,
|
||||||
|
PassiveBluetoothProcessorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
|
@ -122,16 +130,23 @@ async def async_setup_entry(
|
||||||
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][
|
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][
|
||||||
entry.entry_id
|
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(
|
entry.async_on_unload(
|
||||||
coordinator.async_add_entities_listener(
|
processor.async_add_entities_listener(
|
||||||
SensorPushBluetoothSensorEntity, async_add_entities
|
SensorPushBluetoothSensorEntity, async_add_entities
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SensorPushBluetoothSensorEntity(
|
class SensorPushBluetoothSensorEntity(
|
||||||
PassiveBluetoothCoordinatorEntity[
|
PassiveBluetoothProcessorEntity[
|
||||||
PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]]
|
PassiveBluetoothDataProcessor[Optional[Union[float, int]]]
|
||||||
],
|
],
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
):
|
):
|
||||||
|
@ -140,4 +155,4 @@ class SensorPushBluetoothSensorEntity(
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> int | float | None:
|
def native_value(self) -> int | float | None:
|
||||||
"""Return the native value."""
|
"""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
|
import logging
|
||||||
|
|
||||||
from xiaomi_ble import XiaomiBluetoothDeviceData
|
|
||||||
|
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||||
PassiveBluetoothDataUpdate,
|
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
PassiveBluetoothDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .sensor import sensor_update_to_bluetooth_data_update
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
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."""
|
"""Set up Xiaomi BLE device from a config entry."""
|
||||||
address = entry.unique_id
|
address = entry.unique_id
|
||||||
assert address is not None
|
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, {})[
|
hass.data.setdefault(DOMAIN, {})[
|
||||||
entry.entry_id
|
entry.entry_id
|
||||||
] = PassiveBluetoothDataUpdateCoordinator(
|
] = PassiveBluetoothDataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
update_method=_async_update_data,
|
|
||||||
address=address,
|
address=address,
|
||||||
)
|
)
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
|
@ -3,14 +3,22 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional, Union
|
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 import config_entries
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||||
PassiveBluetoothCoordinatorEntity,
|
PassiveBluetoothDataProcessor,
|
||||||
PassiveBluetoothDataUpdate,
|
PassiveBluetoothDataUpdate,
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
PassiveBluetoothDataUpdateCoordinator,
|
||||||
PassiveBluetoothEntityKey,
|
PassiveBluetoothEntityKey,
|
||||||
|
PassiveBluetoothProcessorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
|
@ -150,16 +158,23 @@ async def async_setup_entry(
|
||||||
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][
|
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][
|
||||||
entry.entry_id
|
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(
|
entry.async_on_unload(
|
||||||
coordinator.async_add_entities_listener(
|
processor.async_add_entities_listener(
|
||||||
XiaomiBluetoothSensorEntity, async_add_entities
|
XiaomiBluetoothSensorEntity, async_add_entities
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class XiaomiBluetoothSensorEntity(
|
class XiaomiBluetoothSensorEntity(
|
||||||
PassiveBluetoothCoordinatorEntity[
|
PassiveBluetoothProcessorEntity[
|
||||||
PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]]
|
PassiveBluetoothDataProcessor[Optional[Union[float, int]]]
|
||||||
],
|
],
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
):
|
):
|
||||||
|
@ -168,4 +183,4 @@ class XiaomiBluetoothSensorEntity(
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> int | float | None:
|
def native_value(self) -> int | float | None:
|
||||||
"""Return the native value."""
|
"""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
|
from home_assistant_bluetooth import BluetoothServiceInfo
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
BinarySensorDeviceClass,
|
||||||
|
BinarySensorEntityDescription,
|
||||||
|
)
|
||||||
from homeassistant.components.bluetooth import (
|
from homeassistant.components.bluetooth import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
UNAVAILABLE_TRACK_SECONDS,
|
UNAVAILABLE_TRACK_SECONDS,
|
||||||
BluetoothChange,
|
BluetoothChange,
|
||||||
)
|
)
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||||
PassiveBluetoothCoordinatorEntity,
|
PassiveBluetoothDataProcessor,
|
||||||
PassiveBluetoothDataUpdate,
|
PassiveBluetoothDataUpdate,
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
PassiveBluetoothDataUpdateCoordinator,
|
||||||
PassiveBluetoothEntityKey,
|
PassiveBluetoothEntityKey,
|
||||||
|
PassiveBluetoothProcessorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription
|
from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription
|
||||||
from homeassistant.const import TEMP_CELSIUS
|
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
|
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||||
|
|
||||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
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
|
assert coordinator.available is False # no data yet
|
||||||
saved_callback = None
|
saved_callback = None
|
||||||
|
@ -95,10 +100,13 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
|
||||||
saved_callback = _callback
|
saved_callback = _callback
|
||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
||||||
|
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||||
_async_register_callback,
|
_async_register_callback,
|
||||||
):
|
):
|
||||||
|
unregister_processor = coordinator.async_register_processor(processor)
|
||||||
|
|
||||||
entity_key = PassiveBluetoothEntityKey("temperature", None)
|
entity_key = PassiveBluetoothEntityKey("temperature", None)
|
||||||
entity_key_events = []
|
entity_key_events = []
|
||||||
|
@ -110,22 +118,20 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
|
||||||
"""Mock entity key listener."""
|
"""Mock entity key listener."""
|
||||||
entity_key_events.append(data)
|
entity_key_events.append(data)
|
||||||
|
|
||||||
cancel_async_add_entity_key_listener = (
|
cancel_async_add_entity_key_listener = processor.async_add_entity_key_listener(
|
||||||
coordinator.async_add_entity_key_listener(
|
|
||||||
_async_entity_key_listener,
|
_async_entity_key_listener,
|
||||||
entity_key,
|
entity_key,
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None:
|
def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None:
|
||||||
"""Mock an all listener."""
|
"""Mock an all listener."""
|
||||||
all_events.append(data)
|
all_events.append(data)
|
||||||
|
|
||||||
cancel_listener = coordinator.async_add_listener(
|
cancel_listener = processor.async_add_listener(
|
||||||
_all_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_entity,
|
||||||
mock_add_entities,
|
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 len(mock_entity.mock_calls) == 2
|
||||||
assert coordinator.available is True
|
assert coordinator.available is True
|
||||||
|
|
||||||
|
unregister_processor()
|
||||||
|
|
||||||
|
|
||||||
async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
||||||
"""Test that the coordinator is unavailable after no data for a while."""
|
"""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
|
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||||
|
|
||||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
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
|
assert coordinator.available is False # no data yet
|
||||||
saved_callback = None
|
saved_callback = None
|
||||||
|
@ -192,23 +200,27 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
||||||
saved_callback = _callback
|
saved_callback = _callback
|
||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
||||||
|
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||||
_async_register_callback,
|
_async_register_callback,
|
||||||
):
|
):
|
||||||
|
unregister_processor = coordinator.async_register_processor(processor)
|
||||||
|
|
||||||
mock_entity = MagicMock()
|
mock_entity = MagicMock()
|
||||||
mock_add_entities = MagicMock()
|
mock_add_entities = MagicMock()
|
||||||
coordinator.async_add_entities_listener(
|
processor.async_add_entities_listener(
|
||||||
mock_entity,
|
mock_entity,
|
||||||
mock_add_entities,
|
mock_add_entities,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert coordinator.available is False
|
assert coordinator.available is False
|
||||||
|
assert processor.available is False
|
||||||
|
|
||||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||||
assert len(mock_add_entities.mock_calls) == 1
|
assert len(mock_add_entities.mock_calls) == 1
|
||||||
assert coordinator.available is True
|
assert coordinator.available is True
|
||||||
|
assert processor.available is True
|
||||||
scanner = _get_underlying_scanner()
|
scanner = _get_underlying_scanner()
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
|
@ -224,10 +236,12 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert coordinator.available is False
|
assert coordinator.available is False
|
||||||
|
assert processor.available is False
|
||||||
|
|
||||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||||
assert len(mock_add_entities.mock_calls) == 1
|
assert len(mock_add_entities.mock_calls) == 1
|
||||||
assert coordinator.available is True
|
assert coordinator.available is True
|
||||||
|
assert processor.available is True
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices",
|
"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()
|
await hass.async_block_till_done()
|
||||||
assert coordinator.available is False
|
assert coordinator.available is False
|
||||||
|
assert processor.available is False
|
||||||
|
|
||||||
|
unregister_processor()
|
||||||
|
|
||||||
|
|
||||||
async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
|
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
|
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||||
|
|
||||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
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
|
assert coordinator.available is False # no data yet
|
||||||
saved_callback = None
|
saved_callback = None
|
||||||
|
@ -266,10 +283,13 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
|
||||||
saved_callback = _callback
|
saved_callback = _callback
|
||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
||||||
|
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||||
_async_register_callback,
|
_async_register_callback,
|
||||||
):
|
):
|
||||||
|
unregister_processor = coordinator.async_register_processor(processor)
|
||||||
|
|
||||||
all_events = []
|
all_events = []
|
||||||
|
|
||||||
|
@ -277,7 +297,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
|
||||||
"""Mock an all listener."""
|
"""Mock an all listener."""
|
||||||
all_events.append(data)
|
all_events.append(data)
|
||||||
|
|
||||||
coordinator.async_add_listener(
|
processor.async_add_listener(
|
||||||
_all_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
|
# We should stop processing events once hass is stopping
|
||||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||||
assert len(all_events) == 1
|
assert len(all_events) == 1
|
||||||
|
unregister_processor()
|
||||||
|
|
||||||
|
|
||||||
async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start):
|
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
|
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||||
|
|
||||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
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
|
assert coordinator.available is False # no data yet
|
||||||
saved_callback = None
|
saved_callback = None
|
||||||
|
@ -319,23 +340,27 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta
|
||||||
saved_callback = _callback
|
saved_callback = _callback
|
||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
||||||
|
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||||
_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)
|
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
|
# We should go unavailable once we get an exception
|
||||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||||
assert "Test exception" in caplog.text
|
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
|
# We should go available again once we get data again
|
||||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
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):
|
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
|
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||||
|
|
||||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
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
|
assert coordinator.available is False # no data yet
|
||||||
saved_callback = None
|
saved_callback = None
|
||||||
|
@ -366,24 +391,28 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start):
|
||||||
saved_callback = _callback
|
saved_callback = _callback
|
||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
||||||
|
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||||
_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)
|
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
|
# We should go unavailable once we get bad data
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
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
|
# We should go available again once we get good data again
|
||||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
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(
|
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
|
return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||||
|
|
||||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
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
|
assert coordinator.available is False # no data yet
|
||||||
saved_callback = None
|
saved_callback = None
|
||||||
|
@ -702,16 +731,19 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start):
|
||||||
saved_callback = _callback
|
saved_callback = _callback
|
||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
||||||
|
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||||
_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()
|
mock_add_entities = MagicMock()
|
||||||
|
|
||||||
coordinator.async_add_entities_listener(
|
processor.async_add_entities_listener(
|
||||||
PassiveBluetoothCoordinatorEntity,
|
PassiveBluetoothProcessorEntity,
|
||||||
mock_add_entities,
|
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],
|
*mock_add_entities.mock_calls[1][1][0],
|
||||||
]
|
]
|
||||||
|
|
||||||
entity_one: PassiveBluetoothCoordinatorEntity = entities[0]
|
entity_one: PassiveBluetoothProcessorEntity = entities[0]
|
||||||
entity_one.hass = hass
|
entity_one.hass = hass
|
||||||
assert entity_one.available is True
|
assert entity_one.available is True
|
||||||
assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature-remote"
|
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
|
return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||||
|
|
||||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
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
|
assert coordinator.available is False # no data yet
|
||||||
saved_callback = None
|
saved_callback = None
|
||||||
|
@ -807,15 +839,17 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner
|
||||||
saved_callback = _callback
|
saved_callback = _callback
|
||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
||||||
|
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||||
_async_register_callback,
|
_async_register_callback,
|
||||||
):
|
):
|
||||||
|
coordinator.async_register_processor(processor)
|
||||||
|
|
||||||
mock_add_entities = MagicMock()
|
mock_add_entities = MagicMock()
|
||||||
|
|
||||||
coordinator.async_add_entities_listener(
|
processor.async_add_entities_listener(
|
||||||
PassiveBluetoothCoordinatorEntity,
|
PassiveBluetoothProcessorEntity,
|
||||||
mock_add_entities,
|
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
|
assert len(mock_add_entities.mock_calls) == 1
|
||||||
|
|
||||||
entities = mock_add_entities.mock_calls[0][1][0]
|
entities = mock_add_entities.mock_calls[0][1][0]
|
||||||
entity_one: PassiveBluetoothCoordinatorEntity = entities[0]
|
entity_one: PassiveBluetoothProcessorEntity = entities[0]
|
||||||
entity_one.hass = hass
|
entity_one.hass = hass
|
||||||
assert entity_one.available is True
|
assert entity_one.available is True
|
||||||
assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature"
|
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
|
return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||||
|
|
||||||
coordinator = PassiveBluetoothDataUpdateCoordinator(
|
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
|
assert coordinator.available is False # no data yet
|
||||||
saved_callback = None
|
saved_callback = None
|
||||||
|
@ -867,18 +901,19 @@ async def test_passive_bluetooth_entity_with_entity_platform(
|
||||||
saved_callback = _callback
|
saved_callback = _callback
|
||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
||||||
|
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
||||||
_async_register_callback,
|
_async_register_callback,
|
||||||
):
|
):
|
||||||
|
coordinator.async_register_processor(processor)
|
||||||
|
|
||||||
coordinator.async_add_entities_listener(
|
processor.async_add_entities_listener(
|
||||||
PassiveBluetoothCoordinatorEntity,
|
PassiveBluetoothProcessorEntity,
|
||||||
lambda entities: hass.async_create_task(
|
lambda entities: hass.async_create_task(
|
||||||
entity_platform.async_add_entities(entities)
|
entity_platform.async_add_entities(entities)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
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")
|
hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure")
|
||||||
is not None
|
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…
Add table
Add a link
Reference in a new issue