mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Split bluetooth coordinator into two classes (#75675)
This commit is contained in:
parent
19f82e5201
commit
da131beced
@ -1,65 +1,23 @@
|
|||||||
"""The Bluetooth integration."""
|
"""Passive update coordinator for the Bluetooth integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable, Mapping
|
from collections.abc import Callable, Generator
|
||||||
import dataclasses
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
from typing import Any
|
||||||
from typing import Any, Generic, TypeVar
|
|
||||||
|
|
||||||
from home_assistant_bluetooth import BluetoothServiceInfo
|
|
||||||
|
|
||||||
from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME
|
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
|
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import (
|
from . import BluetoothChange
|
||||||
BluetoothCallbackMatcher,
|
from .update_coordinator import BasePassiveBluetoothCoordinator
|
||||||
BluetoothChange,
|
|
||||||
async_register_callback,
|
|
||||||
async_track_unavailable,
|
|
||||||
)
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator):
|
||||||
class PassiveBluetoothEntityKey:
|
"""Class to manage passive bluetooth advertisements.
|
||||||
"""Key for a passive bluetooth entity.
|
|
||||||
|
|
||||||
Example:
|
This coordinator is responsible for dispatching the bluetooth data
|
||||||
key: temperature
|
and tracking devices.
|
||||||
device_id: outdoor_sensor_1
|
|
||||||
"""
|
|
||||||
|
|
||||||
key: str
|
|
||||||
device_id: str | None
|
|
||||||
|
|
||||||
|
|
||||||
_T = TypeVar("_T")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
|
||||||
class PassiveBluetoothDataUpdate(Generic[_T]):
|
|
||||||
"""Generic bluetooth data."""
|
|
||||||
|
|
||||||
devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict)
|
|
||||||
entity_descriptions: Mapping[
|
|
||||||
PassiveBluetoothEntityKey, EntityDescription
|
|
||||||
] = dataclasses.field(default_factory=dict)
|
|
||||||
entity_names: Mapping[PassiveBluetoothEntityKey, str | None] = dataclasses.field(
|
|
||||||
default_factory=dict
|
|
||||||
)
|
|
||||||
entity_data: Mapping[PassiveBluetoothEntityKey, _T] = dataclasses.field(
|
|
||||||
default_factory=dict
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PassiveBluetoothDataUpdateCoordinator:
|
|
||||||
"""Passive bluetooth data update coordinator for bluetooth advertisements.
|
|
||||||
|
|
||||||
The coordinator is responsible for dispatching the bluetooth data,
|
|
||||||
to each processor, and tracking devices.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -68,78 +26,52 @@ class PassiveBluetoothDataUpdateCoordinator:
|
|||||||
logger: logging.Logger,
|
logger: logging.Logger,
|
||||||
address: str,
|
address: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the coordinator."""
|
"""Initialize PassiveBluetoothDataUpdateCoordinator."""
|
||||||
self.hass = hass
|
super().__init__(hass, logger, address)
|
||||||
self.logger = logger
|
self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {}
|
||||||
self.name: str | None = None
|
|
||||||
self.address = address
|
|
||||||
self._processors: list[PassiveBluetoothDataProcessor] = []
|
|
||||||
self._cancel_track_unavailable: CALLBACK_TYPE | None = None
|
|
||||||
self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None
|
|
||||||
self._present = False
|
|
||||||
self.last_seen = 0.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if the device is available."""
|
|
||||||
return self._present
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_start(self) -> None:
|
def async_update_listeners(self) -> None:
|
||||||
"""Start the callbacks."""
|
"""Update all registered listeners."""
|
||||||
self._cancel_bluetooth_advertisements = async_register_callback(
|
for update_callback, _ in list(self._listeners.values()):
|
||||||
self.hass,
|
update_callback()
|
||||||
self._async_handle_bluetooth_event,
|
|
||||||
BluetoothCallbackMatcher(address=self.address),
|
|
||||||
)
|
|
||||||
self._cancel_track_unavailable = async_track_unavailable(
|
|
||||||
self.hass,
|
|
||||||
self._async_handle_unavailable,
|
|
||||||
self.address,
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_stop(self) -> None:
|
def _async_handle_unavailable(self, address: str) -> None:
|
||||||
"""Stop the callbacks."""
|
"""Handle the device going unavailable."""
|
||||||
if self._cancel_bluetooth_advertisements is not None:
|
super()._async_handle_unavailable(address)
|
||||||
self._cancel_bluetooth_advertisements()
|
self.async_update_listeners()
|
||||||
self._cancel_bluetooth_advertisements = None
|
|
||||||
if self._cancel_track_unavailable is not None:
|
|
||||||
self._cancel_track_unavailable()
|
|
||||||
self._cancel_track_unavailable = None
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_register_processor(
|
def async_start(self) -> CALLBACK_TYPE:
|
||||||
self, processor: PassiveBluetoothDataProcessor
|
"""Start the data updater."""
|
||||||
) -> Callable[[], None]:
|
self._async_start()
|
||||||
"""Register a processor that subscribes to updates."""
|
|
||||||
processor.coordinator = self
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def remove_processor() -> None:
|
def _async_cancel() -> 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()
|
self._async_stop()
|
||||||
elif not running and self._processors:
|
|
||||||
self._async_start()
|
return _async_cancel
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_handle_unavailable(self, _address: str) -> None:
|
def async_add_listener(
|
||||||
"""Handle the device going unavailable."""
|
self, update_callback: CALLBACK_TYPE, context: Any = None
|
||||||
self._present = False
|
) -> Callable[[], None]:
|
||||||
for processor in self._processors:
|
"""Listen for data updates."""
|
||||||
processor.async_handle_unavailable()
|
|
||||||
|
@callback
|
||||||
|
def remove_listener() -> None:
|
||||||
|
"""Remove update listener."""
|
||||||
|
self._listeners.pop(remove_listener)
|
||||||
|
|
||||||
|
self._listeners[remove_listener] = (update_callback, context)
|
||||||
|
return remove_listener
|
||||||
|
|
||||||
|
def async_contexts(self) -> Generator[Any, None, None]:
|
||||||
|
"""Return all registered contexts."""
|
||||||
|
yield from (
|
||||||
|
context for _, context in self._listeners.values() if context is not None
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_handle_bluetooth_event(
|
def _async_handle_bluetooth_event(
|
||||||
@ -148,242 +80,19 @@ class PassiveBluetoothDataUpdateCoordinator:
|
|||||||
change: BluetoothChange,
|
change: BluetoothChange,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle a Bluetooth event."""
|
"""Handle a Bluetooth event."""
|
||||||
self.last_seen = time.monotonic()
|
super()._async_handle_bluetooth_event(service_info, change)
|
||||||
self.name = service_info.name
|
self.async_update_listeners()
|
||||||
self._present = True
|
|
||||||
if self.hass.is_stopping:
|
|
||||||
return
|
|
||||||
for processor in self._processors:
|
|
||||||
processor.async_handle_bluetooth_event(service_info, change)
|
|
||||||
|
|
||||||
|
|
||||||
_PassiveBluetoothDataProcessorT = TypeVar(
|
class PassiveBluetoothCoordinatorEntity(CoordinatorEntity):
|
||||||
"_PassiveBluetoothDataProcessorT",
|
"""A class for entities using DataUpdateCoordinator."""
|
||||||
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
|
coordinator: PassiveBluetoothDataUpdateCoordinator
|
||||||
|
|
||||||
def __init__(
|
async def async_update(self) -> None:
|
||||||
self,
|
"""All updates are passive."""
|
||||||
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[PassiveBluetoothProcessorEntity],
|
|
||||||
async_add_entites: AddEntitiesCallback,
|
|
||||||
) -> Callable[[], None]:
|
|
||||||
"""Add a listener for new entities."""
|
|
||||||
created: set[PassiveBluetoothEntityKey] = set()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_add_or_update_entities(
|
|
||||||
data: PassiveBluetoothDataUpdate[_T] | None,
|
|
||||||
) -> None:
|
|
||||||
"""Listen for new entities."""
|
|
||||||
if data is None:
|
|
||||||
return
|
|
||||||
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))
|
|
||||||
created.add(entity_key)
|
|
||||||
if entities:
|
|
||||||
async_add_entites(entities)
|
|
||||||
|
|
||||||
return self.async_add_listener(_async_add_or_update_entities)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_add_listener(
|
|
||||||
self,
|
|
||||||
update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None],
|
|
||||||
) -> Callable[[], None]:
|
|
||||||
"""Listen for all updates."""
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def remove_listener() -> None:
|
|
||||||
"""Remove update listener."""
|
|
||||||
self._listeners.remove(update_callback)
|
|
||||||
|
|
||||||
self._listeners.append(update_callback)
|
|
||||||
return remove_listener
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_add_entity_key_listener(
|
|
||||||
self,
|
|
||||||
update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None],
|
|
||||||
entity_key: PassiveBluetoothEntityKey,
|
|
||||||
) -> Callable[[], None]:
|
|
||||||
"""Listen for updates by device key."""
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def remove_listener() -> None:
|
|
||||||
"""Remove update listener."""
|
|
||||||
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._entity_key_listeners.setdefault(entity_key, []).append(update_callback)
|
|
||||||
return remove_listener
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_update_listeners(
|
|
||||||
self, data: PassiveBluetoothDataUpdate[_T] | None
|
|
||||||
) -> None:
|
|
||||||
"""Update all registered listeners."""
|
|
||||||
# Dispatch to listeners without a filter key
|
|
||||||
for update_callback in self._listeners:
|
|
||||||
update_callback(data)
|
|
||||||
|
|
||||||
# Dispatch to listeners with a filter key
|
|
||||||
for listeners in self._entity_key_listeners.values():
|
|
||||||
for update_callback in listeners:
|
|
||||||
update_callback(data)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_handle_bluetooth_event(
|
|
||||||
self,
|
|
||||||
service_info: BluetoothServiceInfo,
|
|
||||||
change: BluetoothChange,
|
|
||||||
) -> None:
|
|
||||||
"""Handle a Bluetooth event."""
|
|
||||||
try:
|
|
||||||
new_data = self.update_method(service_info)
|
|
||||||
except Exception as err: # pylint: disable=broad-except
|
|
||||||
self.last_update_success = False
|
|
||||||
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.coordinator.name} returned {new_data} instead of a PassiveBluetoothDataUpdate"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.last_update_success:
|
|
||||||
self.last_update_success = True
|
|
||||||
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)
|
|
||||||
self.entity_data.update(new_data.entity_data)
|
|
||||||
self.entity_names.update(new_data.entity_names)
|
|
||||||
self.async_update_listeners(new_data)
|
|
||||||
|
|
||||||
|
|
||||||
class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProcessorT]):
|
|
||||||
"""A class for entities using PassiveBluetoothDataProcessor."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_should_poll = False
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
processor: _PassiveBluetoothDataProcessorT,
|
|
||||||
entity_key: PassiveBluetoothEntityKey,
|
|
||||||
description: EntityDescription,
|
|
||||||
context: Any = None,
|
|
||||||
) -> None:
|
|
||||||
"""Create the entity with a PassiveBluetoothDataProcessor."""
|
|
||||||
self.entity_description = description
|
|
||||||
self.entity_key = entity_key
|
|
||||||
self.processor = processor
|
|
||||||
self.processor_context = context
|
|
||||||
address = processor.coordinator.address
|
|
||||||
device_id = entity_key.device_id
|
|
||||||
devices = processor.devices
|
|
||||||
key = entity_key.key
|
|
||||||
if device_id in devices:
|
|
||||||
base_device_info = devices[device_id]
|
|
||||||
else:
|
|
||||||
base_device_info = DeviceInfo({})
|
|
||||||
if device_id:
|
|
||||||
self._attr_device_info = base_device_info | DeviceInfo(
|
|
||||||
{ATTR_IDENTIFIERS: {(DOMAIN, f"{address}-{device_id}")}}
|
|
||||||
)
|
|
||||||
self._attr_unique_id = f"{address}-{key}-{device_id}"
|
|
||||||
else:
|
|
||||||
self._attr_device_info = base_device_info | DeviceInfo(
|
|
||||||
{ATTR_IDENTIFIERS: {(DOMAIN, address)}}
|
|
||||||
)
|
|
||||||
self._attr_unique_id = f"{address}-{key}"
|
|
||||||
if ATTR_NAME not in self._attr_device_info:
|
|
||||||
self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name
|
|
||||||
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.processor.available
|
return self.coordinator.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.processor.async_add_entity_key_listener(
|
|
||||||
self._handle_processor_update, self.entity_key
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _handle_processor_update(
|
|
||||||
self, new_data: PassiveBluetoothDataUpdate | None
|
|
||||||
) -> None:
|
|
||||||
"""Handle updated data from the processor."""
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
346
homeassistant/components/bluetooth/passive_update_processor.py
Normal file
346
homeassistant/components/bluetooth/passive_update_processor.py
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
"""Passive update processors for the Bluetooth integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Mapping
|
||||||
|
import dataclasses
|
||||||
|
import logging
|
||||||
|
from typing import Any, Generic, TypeVar
|
||||||
|
|
||||||
|
from home_assistant_bluetooth import BluetoothServiceInfo
|
||||||
|
|
||||||
|
from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import BluetoothChange
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .update_coordinator import BasePassiveBluetoothCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class PassiveBluetoothEntityKey:
|
||||||
|
"""Key for a passive bluetooth entity.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
key: temperature
|
||||||
|
device_id: outdoor_sensor_1
|
||||||
|
"""
|
||||||
|
|
||||||
|
key: str
|
||||||
|
device_id: str | None
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class PassiveBluetoothDataUpdate(Generic[_T]):
|
||||||
|
"""Generic bluetooth data."""
|
||||||
|
|
||||||
|
devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict)
|
||||||
|
entity_descriptions: Mapping[
|
||||||
|
PassiveBluetoothEntityKey, EntityDescription
|
||||||
|
] = dataclasses.field(default_factory=dict)
|
||||||
|
entity_names: Mapping[PassiveBluetoothEntityKey, str | None] = dataclasses.field(
|
||||||
|
default_factory=dict
|
||||||
|
)
|
||||||
|
entity_data: Mapping[PassiveBluetoothEntityKey, _T] = dataclasses.field(
|
||||||
|
default_factory=dict
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator):
|
||||||
|
"""Passive bluetooth processor coordinator for bluetooth advertisements.
|
||||||
|
|
||||||
|
The coordinator is responsible for dispatching the bluetooth data,
|
||||||
|
to each processor, and tracking devices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
logger: logging.Logger,
|
||||||
|
address: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
super().__init__(hass, logger, address)
|
||||||
|
self._processors: list[PassiveBluetoothDataProcessor] = []
|
||||||
|
|
||||||
|
@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."""
|
||||||
|
super()._async_handle_unavailable(address)
|
||||||
|
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."""
|
||||||
|
super()._async_handle_bluetooth_event(service_info, change)
|
||||||
|
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: PassiveBluetoothProcessorCoordinator
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
self.coordinator: PassiveBluetoothProcessorCoordinator
|
||||||
|
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[PassiveBluetoothProcessorEntity],
|
||||||
|
async_add_entites: AddEntitiesCallback,
|
||||||
|
) -> Callable[[], None]:
|
||||||
|
"""Add a listener for new entities."""
|
||||||
|
created: set[PassiveBluetoothEntityKey] = set()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_add_or_update_entities(
|
||||||
|
data: PassiveBluetoothDataUpdate[_T] | None,
|
||||||
|
) -> None:
|
||||||
|
"""Listen for new entities."""
|
||||||
|
if data is None:
|
||||||
|
return
|
||||||
|
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))
|
||||||
|
created.add(entity_key)
|
||||||
|
if entities:
|
||||||
|
async_add_entites(entities)
|
||||||
|
|
||||||
|
return self.async_add_listener(_async_add_or_update_entities)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_listener(
|
||||||
|
self,
|
||||||
|
update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None],
|
||||||
|
) -> Callable[[], None]:
|
||||||
|
"""Listen for all updates."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def remove_listener() -> None:
|
||||||
|
"""Remove update listener."""
|
||||||
|
self._listeners.remove(update_callback)
|
||||||
|
|
||||||
|
self._listeners.append(update_callback)
|
||||||
|
return remove_listener
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_entity_key_listener(
|
||||||
|
self,
|
||||||
|
update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None],
|
||||||
|
entity_key: PassiveBluetoothEntityKey,
|
||||||
|
) -> Callable[[], None]:
|
||||||
|
"""Listen for updates by device key."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def remove_listener() -> None:
|
||||||
|
"""Remove update listener."""
|
||||||
|
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._entity_key_listeners.setdefault(entity_key, []).append(update_callback)
|
||||||
|
return remove_listener
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_listeners(
|
||||||
|
self, data: PassiveBluetoothDataUpdate[_T] | None
|
||||||
|
) -> None:
|
||||||
|
"""Update all registered listeners."""
|
||||||
|
# Dispatch to listeners without a filter key
|
||||||
|
for update_callback in self._listeners:
|
||||||
|
update_callback(data)
|
||||||
|
|
||||||
|
# Dispatch to listeners with a filter key
|
||||||
|
for listeners in self._entity_key_listeners.values():
|
||||||
|
for update_callback in listeners:
|
||||||
|
update_callback(data)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_handle_bluetooth_event(
|
||||||
|
self,
|
||||||
|
service_info: BluetoothServiceInfo,
|
||||||
|
change: BluetoothChange,
|
||||||
|
) -> None:
|
||||||
|
"""Handle a Bluetooth event."""
|
||||||
|
try:
|
||||||
|
new_data = self.update_method(service_info)
|
||||||
|
except Exception as err: # pylint: disable=broad-except
|
||||||
|
self.last_update_success = False
|
||||||
|
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.coordinator.name} returned {new_data} instead of a PassiveBluetoothDataUpdate"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.last_update_success:
|
||||||
|
self.last_update_success = True
|
||||||
|
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)
|
||||||
|
self.entity_data.update(new_data.entity_data)
|
||||||
|
self.entity_names.update(new_data.entity_names)
|
||||||
|
self.async_update_listeners(new_data)
|
||||||
|
|
||||||
|
|
||||||
|
class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProcessorT]):
|
||||||
|
"""A class for entities using PassiveBluetoothDataProcessor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
processor: _PassiveBluetoothDataProcessorT,
|
||||||
|
entity_key: PassiveBluetoothEntityKey,
|
||||||
|
description: EntityDescription,
|
||||||
|
context: Any = None,
|
||||||
|
) -> None:
|
||||||
|
"""Create the entity with a PassiveBluetoothDataProcessor."""
|
||||||
|
self.entity_description = description
|
||||||
|
self.entity_key = entity_key
|
||||||
|
self.processor = processor
|
||||||
|
self.processor_context = context
|
||||||
|
address = processor.coordinator.address
|
||||||
|
device_id = entity_key.device_id
|
||||||
|
devices = processor.devices
|
||||||
|
key = entity_key.key
|
||||||
|
if device_id in devices:
|
||||||
|
base_device_info = devices[device_id]
|
||||||
|
else:
|
||||||
|
base_device_info = DeviceInfo({})
|
||||||
|
if device_id:
|
||||||
|
self._attr_device_info = base_device_info | DeviceInfo(
|
||||||
|
{ATTR_IDENTIFIERS: {(DOMAIN, f"{address}-{device_id}")}}
|
||||||
|
)
|
||||||
|
self._attr_unique_id = f"{address}-{key}-{device_id}"
|
||||||
|
else:
|
||||||
|
self._attr_device_info = base_device_info | DeviceInfo(
|
||||||
|
{ATTR_IDENTIFIERS: {(DOMAIN, address)}}
|
||||||
|
)
|
||||||
|
self._attr_unique_id = f"{address}-{key}"
|
||||||
|
if ATTR_NAME not in self._attr_device_info:
|
||||||
|
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.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.processor.async_add_entity_key_listener(
|
||||||
|
self._handle_processor_update, self.entity_key
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_processor_update(
|
||||||
|
self, new_data: PassiveBluetoothDataUpdate | None
|
||||||
|
) -> None:
|
||||||
|
"""Handle updated data from the processor."""
|
||||||
|
self.async_write_ha_state()
|
84
homeassistant/components/bluetooth/update_coordinator.py
Normal file
84
homeassistant/components/bluetooth/update_coordinator.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
"""Update coordinator for the Bluetooth integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from home_assistant_bluetooth import BluetoothServiceInfo
|
||||||
|
|
||||||
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
BluetoothCallbackMatcher,
|
||||||
|
BluetoothChange,
|
||||||
|
async_register_callback,
|
||||||
|
async_track_unavailable,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BasePassiveBluetoothCoordinator:
|
||||||
|
"""Base class for passive bluetooth coordinator for bluetooth advertisements.
|
||||||
|
|
||||||
|
The coordinator is responsible for tracking devices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
logger: logging.Logger,
|
||||||
|
address: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
self.hass = hass
|
||||||
|
self.logger = logger
|
||||||
|
self.name: str | None = None
|
||||||
|
self.address = address
|
||||||
|
self._cancel_track_unavailable: CALLBACK_TYPE | None = None
|
||||||
|
self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None
|
||||||
|
self._present = False
|
||||||
|
self.last_seen = 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if the device is available."""
|
||||||
|
return self._present
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_start(self) -> None:
|
||||||
|
"""Start the callbacks."""
|
||||||
|
self._cancel_bluetooth_advertisements = async_register_callback(
|
||||||
|
self.hass,
|
||||||
|
self._async_handle_bluetooth_event,
|
||||||
|
BluetoothCallbackMatcher(address=self.address),
|
||||||
|
)
|
||||||
|
self._cancel_track_unavailable = async_track_unavailable(
|
||||||
|
self.hass,
|
||||||
|
self._async_handle_unavailable,
|
||||||
|
self.address,
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_stop(self) -> None:
|
||||||
|
"""Stop the callbacks."""
|
||||||
|
if self._cancel_bluetooth_advertisements is not None:
|
||||||
|
self._cancel_bluetooth_advertisements()
|
||||||
|
self._cancel_bluetooth_advertisements = None
|
||||||
|
if self._cancel_track_unavailable is not None:
|
||||||
|
self._cancel_track_unavailable()
|
||||||
|
self._cancel_track_unavailable = None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_handle_unavailable(self, address: str) -> None:
|
||||||
|
"""Handle the device going unavailable."""
|
||||||
|
self._present = False
|
||||||
|
|
||||||
|
@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
|
@ -3,8 +3,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
PassiveBluetoothProcessorCoordinator,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
assert address is not None
|
assert address is not None
|
||||||
hass.data.setdefault(DOMAIN, {})[
|
hass.data.setdefault(DOMAIN, {})[
|
||||||
entry.entry_id
|
entry.entry_id
|
||||||
] = PassiveBluetoothDataUpdateCoordinator(
|
] = PassiveBluetoothProcessorCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
address=address,
|
address=address,
|
||||||
|
@ -13,11 +13,11 @@ from inkbird_ble import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||||
PassiveBluetoothDataProcessor,
|
PassiveBluetoothDataProcessor,
|
||||||
PassiveBluetoothDataUpdate,
|
PassiveBluetoothDataUpdate,
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
|
||||||
PassiveBluetoothEntityKey,
|
PassiveBluetoothEntityKey,
|
||||||
|
PassiveBluetoothProcessorCoordinator,
|
||||||
PassiveBluetoothProcessorEntity,
|
PassiveBluetoothProcessorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
@ -126,7 +126,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the INKBIRD BLE sensors."""
|
"""Set up the INKBIRD BLE sensors."""
|
||||||
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][
|
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||||
entry.entry_id
|
entry.entry_id
|
||||||
]
|
]
|
||||||
data = INKBIRDBluetoothDeviceData()
|
data = INKBIRDBluetoothDeviceData()
|
||||||
|
@ -3,8 +3,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
PassiveBluetoothProcessorCoordinator,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
assert address is not None
|
assert address is not None
|
||||||
hass.data.setdefault(DOMAIN, {})[
|
hass.data.setdefault(DOMAIN, {})[
|
||||||
entry.entry_id
|
entry.entry_id
|
||||||
] = PassiveBluetoothDataUpdateCoordinator(
|
] = PassiveBluetoothProcessorCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
address=address,
|
address=address,
|
||||||
|
@ -13,11 +13,11 @@ from sensorpush_ble import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||||
PassiveBluetoothDataProcessor,
|
PassiveBluetoothDataProcessor,
|
||||||
PassiveBluetoothDataUpdate,
|
PassiveBluetoothDataUpdate,
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
|
||||||
PassiveBluetoothEntityKey,
|
PassiveBluetoothEntityKey,
|
||||||
|
PassiveBluetoothProcessorCoordinator,
|
||||||
PassiveBluetoothProcessorEntity,
|
PassiveBluetoothProcessorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
@ -127,7 +127,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the SensorPush BLE sensors."""
|
"""Set up the SensorPush BLE sensors."""
|
||||||
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][
|
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||||
entry.entry_id
|
entry.entry_id
|
||||||
]
|
]
|
||||||
data = SensorPushBluetoothDeviceData()
|
data = SensorPushBluetoothDeviceData()
|
||||||
|
@ -3,8 +3,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
PassiveBluetoothProcessorCoordinator,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
assert address is not None
|
assert address is not None
|
||||||
hass.data.setdefault(DOMAIN, {})[
|
hass.data.setdefault(DOMAIN, {})[
|
||||||
entry.entry_id
|
entry.entry_id
|
||||||
] = PassiveBluetoothDataUpdateCoordinator(
|
] = PassiveBluetoothProcessorCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
address=address,
|
address=address,
|
||||||
|
@ -13,11 +13,11 @@ from xiaomi_ble import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||||
PassiveBluetoothDataProcessor,
|
PassiveBluetoothDataProcessor,
|
||||||
PassiveBluetoothDataUpdate,
|
PassiveBluetoothDataUpdate,
|
||||||
PassiveBluetoothDataUpdateCoordinator,
|
|
||||||
PassiveBluetoothEntityKey,
|
PassiveBluetoothEntityKey,
|
||||||
|
PassiveBluetoothProcessorCoordinator,
|
||||||
PassiveBluetoothProcessorEntity,
|
PassiveBluetoothProcessorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
@ -155,7 +155,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Xiaomi BLE sensors."""
|
"""Set up the Xiaomi BLE sensors."""
|
||||||
coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][
|
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||||
entry.entry_id
|
entry.entry_id
|
||||||
]
|
]
|
||||||
data = XiaomiBluetoothDeviceData()
|
data = XiaomiBluetoothDeviceData()
|
||||||
|
File diff suppressed because it is too large
Load Diff
1058
tests/components/bluetooth/test_passive_update_processor.py
Normal file
1058
tests/components/bluetooth/test_passive_update_processor.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -28,7 +28,7 @@ async def test_sensors(hass):
|
|||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
"homeassistant.components.bluetooth.update_coordinator.async_register_callback",
|
||||||
_async_register_callback,
|
_async_register_callback,
|
||||||
):
|
):
|
||||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
@ -28,7 +28,7 @@ async def test_sensors(hass):
|
|||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
"homeassistant.components.bluetooth.update_coordinator.async_register_callback",
|
||||||
_async_register_callback,
|
_async_register_callback,
|
||||||
):
|
):
|
||||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
@ -28,7 +28,7 @@ async def test_sensors(hass):
|
|||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
"homeassistant.components.bluetooth.update_coordinator.async_register_callback",
|
||||||
_async_register_callback,
|
_async_register_callback,
|
||||||
):
|
):
|
||||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
@ -66,7 +66,7 @@ async def test_xiaomi_HHCCJCY01(hass):
|
|||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback",
|
"homeassistant.components.bluetooth.update_coordinator.async_register_callback",
|
||||||
_async_register_callback,
|
_async_register_callback,
|
||||||
):
|
):
|
||||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user