Allow parsing to happen in PassiveBluetoothProcessorCoordinator (#76384)
This commit is contained in:
parent
12721da063
commit
7d427ddbd4
12 changed files with 288 additions and 130 deletions
|
@ -52,11 +52,17 @@ class PassiveBluetoothDataUpdate(Generic[_T]):
|
|||
)
|
||||
|
||||
|
||||
class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator):
|
||||
class PassiveBluetoothProcessorCoordinator(
|
||||
Generic[_T], BasePassiveBluetoothCoordinator
|
||||
):
|
||||
"""Passive bluetooth processor coordinator for bluetooth advertisements.
|
||||
|
||||
The coordinator is responsible for dispatching the bluetooth data,
|
||||
to each processor, and tracking devices.
|
||||
|
||||
The update_method should return the data that is dispatched to each processor.
|
||||
This is normally a parsed form of the data, but you can just forward the
|
||||
BluetoothServiceInfoBleak if needed.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
@ -65,10 +71,18 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator):
|
|||
logger: logging.Logger,
|
||||
address: str,
|
||||
mode: BluetoothScanningMode,
|
||||
update_method: Callable[[BluetoothServiceInfoBleak], _T],
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(hass, logger, address, mode)
|
||||
self._processors: list[PassiveBluetoothDataProcessor] = []
|
||||
self._update_method = update_method
|
||||
self.last_update_success = True
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the device is available."""
|
||||
return super().available and self.last_update_success
|
||||
|
||||
@callback
|
||||
def async_register_processor(
|
||||
|
@ -102,8 +116,22 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator):
|
|||
super()._async_handle_bluetooth_event(service_info, change)
|
||||
if self.hass.is_stopping:
|
||||
return
|
||||
|
||||
try:
|
||||
update = self._update_method(service_info)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
self.last_update_success = False
|
||||
self.logger.exception(
|
||||
"Unexpected error updating %s data: %s", self.name, err
|
||||
)
|
||||
return
|
||||
|
||||
if not self.last_update_success:
|
||||
self.last_update_success = True
|
||||
self.logger.info("Coordinator %s recovered", self.name)
|
||||
|
||||
for processor in self._processors:
|
||||
processor.async_handle_bluetooth_event(service_info, change)
|
||||
processor.async_handle_update(update)
|
||||
|
||||
|
||||
_PassiveBluetoothDataProcessorT = TypeVar(
|
||||
|
@ -123,9 +151,8 @@ class PassiveBluetoothDataProcessor(Generic[_T]):
|
|||
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: BluetoothServiceInfoBleak) -> PassiveBluetoothDataUpdate
|
||||
receives a new advertisement data from the coordinator with the data
|
||||
returned by he update_method of the coordinator.
|
||||
|
||||
As the size of each advertisement is limited, the update_method should
|
||||
return a PassiveBluetoothDataUpdate object that contains only data that
|
||||
|
@ -138,9 +165,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
update_method: Callable[
|
||||
[BluetoothServiceInfoBleak], PassiveBluetoothDataUpdate[_T]
|
||||
],
|
||||
update_method: Callable[[_T], PassiveBluetoothDataUpdate[_T]],
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.coordinator: PassiveBluetoothProcessorCoordinator
|
||||
|
@ -244,14 +269,10 @@ class PassiveBluetoothDataProcessor(Generic[_T]):
|
|||
update_callback(data)
|
||||
|
||||
@callback
|
||||
def async_handle_bluetooth_event(
|
||||
self,
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
change: BluetoothChange,
|
||||
) -> None:
|
||||
def async_handle_update(self, update: _T) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
try:
|
||||
new_data = self.update_method(service_info)
|
||||
new_data = self.update_method(update)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
self.last_update_success = False
|
||||
self.coordinator.logger.exception(
|
||||
|
|
|
@ -3,6 +3,8 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
from govee_ble import GoveeBluetoothDeviceData
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
|
@ -22,6 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
"""Set up Govee BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
data = GoveeBluetoothDeviceData()
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[
|
||||
entry.entry_id
|
||||
] = PassiveBluetoothProcessorCoordinator(
|
||||
|
@ -29,6 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.ACTIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
|
|
|
@ -3,14 +3,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import Optional, Union
|
||||
|
||||
from govee_ble import (
|
||||
DeviceClass,
|
||||
DeviceKey,
|
||||
GoveeBluetoothDeviceData,
|
||||
SensorDeviceInfo,
|
||||
SensorUpdate,
|
||||
Units,
|
||||
)
|
||||
from govee_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
|
@ -129,12 +122,7 @@ async def async_setup_entry(
|
|||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
data = GoveeBluetoothDeviceData()
|
||||
processor = PassiveBluetoothDataProcessor(
|
||||
lambda service_info: sensor_update_to_bluetooth_data_update(
|
||||
data.update(service_info)
|
||||
)
|
||||
)
|
||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||
entry.async_on_unload(
|
||||
processor.async_add_entities_listener(
|
||||
GoveeBluetoothSensorEntity, async_add_entities
|
||||
|
|
|
@ -3,6 +3,8 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
from inkbird_ble import INKBIRDBluetoothDeviceData
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
|
@ -22,10 +24,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
"""Set up INKBIRD BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
data = INKBIRDBluetoothDeviceData()
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[
|
||||
entry.entry_id
|
||||
] = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, address=address, mode=BluetoothScanningMode.ACTIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.ACTIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
|
|
|
@ -3,14 +3,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import Optional, Union
|
||||
|
||||
from inkbird_ble import (
|
||||
DeviceClass,
|
||||
DeviceKey,
|
||||
INKBIRDBluetoothDeviceData,
|
||||
SensorDeviceInfo,
|
||||
SensorUpdate,
|
||||
Units,
|
||||
)
|
||||
from inkbird_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
|
@ -129,12 +122,7 @@ async def async_setup_entry(
|
|||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
data = INKBIRDBluetoothDeviceData()
|
||||
processor = PassiveBluetoothDataProcessor(
|
||||
lambda service_info: sensor_update_to_bluetooth_data_update(
|
||||
data.update(service_info)
|
||||
)
|
||||
)
|
||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||
entry.async_on_unload(
|
||||
processor.async_add_entities_listener(
|
||||
INKBIRDBluetoothSensorEntity, async_add_entities
|
||||
|
|
|
@ -3,6 +3,8 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
from moat_ble import MoatBluetoothDeviceData
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
|
@ -22,10 +24,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
"""Set up Moat BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
data = MoatBluetoothDeviceData()
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[
|
||||
entry.entry_id
|
||||
] = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
|
|
|
@ -3,14 +3,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import Optional, Union
|
||||
|
||||
from moat_ble import (
|
||||
DeviceClass,
|
||||
DeviceKey,
|
||||
MoatBluetoothDeviceData,
|
||||
SensorDeviceInfo,
|
||||
SensorUpdate,
|
||||
Units,
|
||||
)
|
||||
from moat_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
|
@ -136,12 +129,7 @@ async def async_setup_entry(
|
|||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
data = MoatBluetoothDeviceData()
|
||||
processor = PassiveBluetoothDataProcessor(
|
||||
lambda service_info: sensor_update_to_bluetooth_data_update(
|
||||
data.update(service_info)
|
||||
)
|
||||
)
|
||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||
entry.async_on_unload(
|
||||
processor.async_add_entities_listener(
|
||||
MoatBluetoothSensorEntity, async_add_entities
|
||||
|
|
|
@ -3,6 +3,8 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
from sensorpush_ble import SensorPushBluetoothDeviceData
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
|
@ -22,10 +24,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
"""Set up SensorPush BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
data = SensorPushBluetoothDeviceData()
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[
|
||||
entry.entry_id
|
||||
] = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
|
|
|
@ -3,14 +3,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import Optional, Union
|
||||
|
||||
from sensorpush_ble import (
|
||||
DeviceClass,
|
||||
DeviceKey,
|
||||
SensorDeviceInfo,
|
||||
SensorPushBluetoothDeviceData,
|
||||
SensorUpdate,
|
||||
Units,
|
||||
)
|
||||
from sensorpush_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
|
@ -130,12 +123,7 @@ async def async_setup_entry(
|
|||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
data = SensorPushBluetoothDeviceData()
|
||||
processor = PassiveBluetoothDataProcessor(
|
||||
lambda service_info: sensor_update_to_bluetooth_data_update(
|
||||
data.update(service_info)
|
||||
)
|
||||
)
|
||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||
entry.async_on_unload(
|
||||
processor.async_add_entities_listener(
|
||||
SensorPushBluetoothSensorEntity, async_add_entities
|
||||
|
|
|
@ -3,7 +3,14 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from xiaomi_ble import SensorUpdate, XiaomiBluetoothDeviceData
|
||||
from xiaomi_ble.parser import EncryptionScheme
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothScanningMode,
|
||||
BluetoothServiceInfoBleak,
|
||||
)
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
)
|
||||
|
@ -18,14 +25,47 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def process_service_info(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
data: XiaomiBluetoothDeviceData,
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
) -> SensorUpdate:
|
||||
"""Process a BluetoothServiceInfoBleak, running side effects and returning sensor data."""
|
||||
update = data.update(service_info)
|
||||
|
||||
# If device isn't pending we know it has seen at least one broadcast with a payload
|
||||
# If that payload was encrypted and the bindkey was not verified then we need to reauth
|
||||
if (
|
||||
not data.pending
|
||||
and data.encryption_scheme != EncryptionScheme.NONE
|
||||
and not data.bindkey_verified
|
||||
):
|
||||
entry.async_start_reauth(hass, data={"device": data})
|
||||
|
||||
return update
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Xiaomi BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
|
||||
kwargs = {}
|
||||
if bindkey := entry.data.get("bindkey"):
|
||||
kwargs["bindkey"] = bytes.fromhex(bindkey)
|
||||
data = XiaomiBluetoothDeviceData(**kwargs)
|
||||
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[
|
||||
entry.entry_id
|
||||
] = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=lambda service_info: process_service_info(
|
||||
hass, entry, data, service_info
|
||||
),
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
|
|
|
@ -3,18 +3,9 @@ from __future__ import annotations
|
|||
|
||||
from typing import Optional, Union
|
||||
|
||||
from xiaomi_ble import (
|
||||
DeviceClass,
|
||||
DeviceKey,
|
||||
SensorDeviceInfo,
|
||||
SensorUpdate,
|
||||
Units,
|
||||
XiaomiBluetoothDeviceData,
|
||||
)
|
||||
from xiaomi_ble.parser import EncryptionScheme
|
||||
from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
|
@ -165,27 +156,6 @@ def sensor_update_to_bluetooth_data_update(
|
|||
)
|
||||
|
||||
|
||||
def process_service_info(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
data: XiaomiBluetoothDeviceData,
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Process a BluetoothServiceInfoBleak, running side effects and returning sensor data."""
|
||||
update = data.update(service_info)
|
||||
|
||||
# If device isn't pending we know it has seen at least one broadcast with a payload
|
||||
# If that payload was encrypted and the bindkey was not verified then we need to reauth
|
||||
if (
|
||||
not data.pending
|
||||
and data.encryption_scheme != EncryptionScheme.NONE
|
||||
and not data.bindkey_verified
|
||||
):
|
||||
entry.async_start_reauth(hass, data={"device": data})
|
||||
|
||||
return sensor_update_to_bluetooth_data_update(update)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
|
@ -195,13 +165,7 @@ async def async_setup_entry(
|
|||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
kwargs = {}
|
||||
if bindkey := entry.data.get("bindkey"):
|
||||
kwargs["bindkey"] = bytes.fromhex(bindkey)
|
||||
data = XiaomiBluetoothDeviceData(**kwargs)
|
||||
processor = PassiveBluetoothDataProcessor(
|
||||
lambda service_info: process_service_info(hass, entry, data, service_info)
|
||||
)
|
||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||
entry.async_on_unload(
|
||||
processor.async_add_entities_listener(
|
||||
XiaomiBluetoothSensorEntity, async_add_entities
|
||||
|
|
|
@ -84,14 +84,25 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
|
|||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
def _mock_update_method(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> dict[str, str]:
|
||||
return {"test": "data"}
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
data: dict[str, str],
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Generate mock data."""
|
||||
assert data == {"test": "data"}
|
||||
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
"aa:bb:cc:dd:ee:ff",
|
||||
BluetoothScanningMode.ACTIVE,
|
||||
_mock_update_method,
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -186,14 +197,24 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start):
|
|||
await hass.async_block_till_done()
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
def _mock_update_method(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> dict[str, str]:
|
||||
return {"test": "data"}
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
data: dict[str, str],
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Generate mock data."""
|
||||
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
"aa:bb:cc:dd:ee:ff",
|
||||
BluetoothScanningMode.ACTIVE,
|
||||
_mock_update_method,
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -271,14 +292,24 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start):
|
|||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
def _mock_update_method(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> dict[str, str]:
|
||||
return {"test": "data"}
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
data: dict[str, str],
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Generate mock data."""
|
||||
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
"aa:bb:cc:dd:ee:ff",
|
||||
BluetoothScanningMode.ACTIVE,
|
||||
_mock_update_method,
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -326,8 +357,14 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta
|
|||
run_count = 0
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
def _mock_update_method(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> dict[str, str]:
|
||||
return {"test": "data"}
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
data: dict[str, str],
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Generate mock data."""
|
||||
nonlocal run_count
|
||||
|
@ -337,7 +374,11 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta
|
|||
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
"aa:bb:cc:dd:ee:ff",
|
||||
BluetoothScanningMode.ACTIVE,
|
||||
_mock_update_method,
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -379,8 +420,14 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start):
|
|||
run_count = 0
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
def _mock_update_method(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> dict[str, str]:
|
||||
return {"test": "data"}
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
data: dict[str, str],
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Generate mock data."""
|
||||
nonlocal run_count
|
||||
|
@ -390,7 +437,11 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start):
|
|||
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
"aa:bb:cc:dd:ee:ff",
|
||||
BluetoothScanningMode.ACTIVE,
|
||||
_mock_update_method,
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -721,8 +772,14 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start):
|
|||
update_count = 0
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
def _mock_update_method(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> dict[str, str]:
|
||||
return {"test": "data"}
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
data: dict[str, str],
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Generate mock data."""
|
||||
nonlocal update_count
|
||||
|
@ -732,7 +789,11 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start):
|
|||
return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
"aa:bb:cc:dd:ee:ff",
|
||||
BluetoothScanningMode.ACTIVE,
|
||||
_mock_update_method,
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -835,14 +896,24 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner
|
|||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
def _mock_update_method(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> dict[str, str]:
|
||||
return {"test": "data"}
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
data: dict[str, str],
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Generate mock data."""
|
||||
return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
"aa:bb:cc:dd:ee:ff",
|
||||
BluetoothScanningMode.ACTIVE,
|
||||
_mock_update_method,
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -899,14 +970,24 @@ async def test_passive_bluetooth_entity_with_entity_platform(
|
|||
entity_platform = MockEntityPlatform(hass)
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
def _mock_update_method(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> dict[str, str]:
|
||||
return {"test": "data"}
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
data: dict[str, str],
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Generate mock data."""
|
||||
return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
"aa:bb:cc:dd:ee:ff",
|
||||
BluetoothScanningMode.ACTIVE,
|
||||
_mock_update_method,
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -992,8 +1073,18 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st
|
|||
"""Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms."""
|
||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
|
||||
@callback
|
||||
def _mock_update_method(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> dict[str, str]:
|
||||
return {"test": "data"}
|
||||
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE
|
||||
hass,
|
||||
_LOGGER,
|
||||
"aa:bb:cc:dd:ee:ff",
|
||||
BluetoothScanningMode.ACTIVE,
|
||||
_mock_update_method,
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
@ -1075,3 +1166,68 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st
|
|||
key="motion", device_id=None
|
||||
)
|
||||
cancel_coordinator()
|
||||
|
||||
|
||||
async def test_exception_from_coordinator_update_method(
|
||||
hass, caplog, mock_bleak_scanner_start
|
||||
):
|
||||
"""Test we handle exceptions from the update method."""
|
||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
|
||||
run_count = 0
|
||||
|
||||
@callback
|
||||
def _mock_update_method(
|
||||
service_info: BluetoothServiceInfo,
|
||||
) -> dict[str, str]:
|
||||
nonlocal run_count
|
||||
run_count += 1
|
||||
if run_count == 2:
|
||||
raise Exception("Test exception")
|
||||
return {"test": "data"}
|
||||
|
||||
@callback
|
||||
def _async_generate_mock_data(
|
||||
data: dict[str, str],
|
||||
) -> PassiveBluetoothDataUpdate:
|
||||
"""Generate mock data."""
|
||||
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
|
||||
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
"aa:bb:cc:dd:ee:ff",
|
||||
BluetoothScanningMode.ACTIVE,
|
||||
_mock_update_method,
|
||||
)
|
||||
assert coordinator.available is False # no data yet
|
||||
saved_callback = None
|
||||
|
||||
def _async_register_callback(_hass, _callback, _matcher, _mode):
|
||||
nonlocal saved_callback
|
||||
saved_callback = _callback
|
||||
return lambda: None
|
||||
|
||||
processor = PassiveBluetoothDataProcessor(_async_generate_mock_data)
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.update_coordinator.async_register_callback",
|
||||
_async_register_callback,
|
||||
):
|
||||
unregister_processor = coordinator.async_register_processor(processor)
|
||||
cancel_coordinator = coordinator.async_start()
|
||||
|
||||
processor.async_add_listener(MagicMock())
|
||||
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
assert processor.available is True
|
||||
|
||||
# We should go unavailable once we get an exception
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
assert "Test exception" in caplog.text
|
||||
assert processor.available is False
|
||||
|
||||
# We should go available again once we get data again
|
||||
saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT)
|
||||
assert processor.available is True
|
||||
unregister_processor()
|
||||
cancel_coordinator()
|
||||
|
|
Loading…
Add table
Reference in a new issue