Allow parsing to happen in PassiveBluetoothProcessorCoordinator (#76384)

This commit is contained in:
Jc2k 2022-08-09 06:36:39 +01:00 committed by GitHub
parent 12721da063
commit 7d427ddbd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 288 additions and 130 deletions

View file

@ -52,11 +52,17 @@ class PassiveBluetoothDataUpdate(Generic[_T]):
) )
class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): class PassiveBluetoothProcessorCoordinator(
Generic[_T], BasePassiveBluetoothCoordinator
):
"""Passive bluetooth processor coordinator for bluetooth advertisements. """Passive bluetooth processor coordinator for bluetooth advertisements.
The coordinator is responsible for dispatching the bluetooth data, The coordinator is responsible for dispatching the bluetooth data,
to each processor, and tracking devices. 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__( def __init__(
@ -65,10 +71,18 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator):
logger: logging.Logger, logger: logging.Logger,
address: str, address: str,
mode: BluetoothScanningMode, mode: BluetoothScanningMode,
update_method: Callable[[BluetoothServiceInfoBleak], _T],
) -> None: ) -> None:
"""Initialize the coordinator.""" """Initialize the coordinator."""
super().__init__(hass, logger, address, mode) super().__init__(hass, logger, address, mode)
self._processors: list[PassiveBluetoothDataProcessor] = [] 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 @callback
def async_register_processor( def async_register_processor(
@ -102,8 +116,22 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator):
super()._async_handle_bluetooth_event(service_info, change) super()._async_handle_bluetooth_event(service_info, change)
if self.hass.is_stopping: if self.hass.is_stopping:
return 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: for processor in self._processors:
processor.async_handle_bluetooth_event(service_info, change) processor.async_handle_update(update)
_PassiveBluetoothDataProcessorT = TypeVar( _PassiveBluetoothDataProcessorT = TypeVar(
@ -123,9 +151,8 @@ class PassiveBluetoothDataProcessor(Generic[_T]):
the appropriate format. the appropriate format.
The processor will call the update_method every time the bluetooth device The processor will call the update_method every time the bluetooth device
receives a new advertisement data from the coordinator with the following signature: receives a new advertisement data from the coordinator with the data
returned by he update_method of the coordinator.
update_method(service_info: BluetoothServiceInfoBleak) -> PassiveBluetoothDataUpdate
As the size of each advertisement is limited, the update_method should As the size of each advertisement is limited, the update_method should
return a PassiveBluetoothDataUpdate object that contains only data that return a PassiveBluetoothDataUpdate object that contains only data that
@ -138,9 +165,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]):
def __init__( def __init__(
self, self,
update_method: Callable[ update_method: Callable[[_T], PassiveBluetoothDataUpdate[_T]],
[BluetoothServiceInfoBleak], PassiveBluetoothDataUpdate[_T]
],
) -> None: ) -> None:
"""Initialize the coordinator.""" """Initialize the coordinator."""
self.coordinator: PassiveBluetoothProcessorCoordinator self.coordinator: PassiveBluetoothProcessorCoordinator
@ -244,14 +269,10 @@ class PassiveBluetoothDataProcessor(Generic[_T]):
update_callback(data) update_callback(data)
@callback @callback
def async_handle_bluetooth_event( def async_handle_update(self, update: _T) -> None:
self,
service_info: BluetoothServiceInfoBleak,
change: BluetoothChange,
) -> None:
"""Handle a Bluetooth event.""" """Handle a Bluetooth event."""
try: try:
new_data = self.update_method(service_info) new_data = self.update_method(update)
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.coordinator.logger.exception( self.coordinator.logger.exception(

View file

@ -3,6 +3,8 @@ from __future__ import annotations
import logging import logging
from govee_ble import GoveeBluetoothDeviceData
from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth import BluetoothScanningMode
from homeassistant.components.bluetooth.passive_update_processor import ( from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorCoordinator,
@ -22,6 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Govee BLE device from a config entry.""" """Set up Govee 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 = GoveeBluetoothDeviceData()
coordinator = hass.data.setdefault(DOMAIN, {})[ coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id entry.entry_id
] = PassiveBluetoothProcessorCoordinator( ] = PassiveBluetoothProcessorCoordinator(
@ -29,6 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER, _LOGGER,
address=address, address=address,
mode=BluetoothScanningMode.ACTIVE, mode=BluetoothScanningMode.ACTIVE,
update_method=data.update,
) )
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload( entry.async_on_unload(

View file

@ -3,14 +3,7 @@ from __future__ import annotations
from typing import Optional, Union from typing import Optional, Union
from govee_ble import ( from govee_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
DeviceClass,
DeviceKey,
GoveeBluetoothDeviceData,
SensorDeviceInfo,
SensorUpdate,
Units,
)
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.bluetooth.passive_update_processor import ( from homeassistant.components.bluetooth.passive_update_processor import (
@ -129,12 +122,7 @@ async def async_setup_entry(
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
entry.entry_id entry.entry_id
] ]
data = GoveeBluetoothDeviceData() processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
processor = PassiveBluetoothDataProcessor(
lambda service_info: sensor_update_to_bluetooth_data_update(
data.update(service_info)
)
)
entry.async_on_unload( entry.async_on_unload(
processor.async_add_entities_listener( processor.async_add_entities_listener(
GoveeBluetoothSensorEntity, async_add_entities GoveeBluetoothSensorEntity, async_add_entities

View file

@ -3,6 +3,8 @@ from __future__ import annotations
import logging import logging
from inkbird_ble import INKBIRDBluetoothDeviceData
from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth import BluetoothScanningMode
from homeassistant.components.bluetooth.passive_update_processor import ( from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorCoordinator,
@ -22,10 +24,15 @@ 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()
coordinator = hass.data.setdefault(DOMAIN, {})[ coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id entry.entry_id
] = PassiveBluetoothProcessorCoordinator( ] = 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) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload( entry.async_on_unload(

View file

@ -3,14 +3,7 @@ from __future__ import annotations
from typing import Optional, Union from typing import Optional, Union
from inkbird_ble import ( from inkbird_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
DeviceClass,
DeviceKey,
INKBIRDBluetoothDeviceData,
SensorDeviceInfo,
SensorUpdate,
Units,
)
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.bluetooth.passive_update_processor import ( from homeassistant.components.bluetooth.passive_update_processor import (
@ -129,12 +122,7 @@ async def async_setup_entry(
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
entry.entry_id entry.entry_id
] ]
data = INKBIRDBluetoothDeviceData() processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
processor = PassiveBluetoothDataProcessor(
lambda service_info: sensor_update_to_bluetooth_data_update(
data.update(service_info)
)
)
entry.async_on_unload( entry.async_on_unload(
processor.async_add_entities_listener( processor.async_add_entities_listener(
INKBIRDBluetoothSensorEntity, async_add_entities INKBIRDBluetoothSensorEntity, async_add_entities

View file

@ -3,6 +3,8 @@ from __future__ import annotations
import logging import logging
from moat_ble import MoatBluetoothDeviceData
from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth import BluetoothScanningMode
from homeassistant.components.bluetooth.passive_update_processor import ( from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorCoordinator,
@ -22,10 +24,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Moat BLE device from a config entry.""" """Set up Moat 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 = MoatBluetoothDeviceData()
coordinator = hass.data.setdefault(DOMAIN, {})[ coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id entry.entry_id
] = PassiveBluetoothProcessorCoordinator( ] = 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) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload( entry.async_on_unload(

View file

@ -3,14 +3,7 @@ from __future__ import annotations
from typing import Optional, Union from typing import Optional, Union
from moat_ble import ( from moat_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
DeviceClass,
DeviceKey,
MoatBluetoothDeviceData,
SensorDeviceInfo,
SensorUpdate,
Units,
)
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.bluetooth.passive_update_processor import ( from homeassistant.components.bluetooth.passive_update_processor import (
@ -136,12 +129,7 @@ async def async_setup_entry(
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
entry.entry_id entry.entry_id
] ]
data = MoatBluetoothDeviceData() processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
processor = PassiveBluetoothDataProcessor(
lambda service_info: sensor_update_to_bluetooth_data_update(
data.update(service_info)
)
)
entry.async_on_unload( entry.async_on_unload(
processor.async_add_entities_listener( processor.async_add_entities_listener(
MoatBluetoothSensorEntity, async_add_entities MoatBluetoothSensorEntity, async_add_entities

View file

@ -3,6 +3,8 @@ from __future__ import annotations
import logging import logging
from sensorpush_ble import SensorPushBluetoothDeviceData
from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth import BluetoothScanningMode
from homeassistant.components.bluetooth.passive_update_processor import ( from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorCoordinator,
@ -22,10 +24,15 @@ 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()
coordinator = hass.data.setdefault(DOMAIN, {})[ coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id entry.entry_id
] = PassiveBluetoothProcessorCoordinator( ] = 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) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload( entry.async_on_unload(

View file

@ -3,14 +3,7 @@ from __future__ import annotations
from typing import Optional, Union from typing import Optional, Union
from sensorpush_ble import ( from sensorpush_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
DeviceClass,
DeviceKey,
SensorDeviceInfo,
SensorPushBluetoothDeviceData,
SensorUpdate,
Units,
)
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.bluetooth.passive_update_processor import ( from homeassistant.components.bluetooth.passive_update_processor import (
@ -130,12 +123,7 @@ async def async_setup_entry(
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
entry.entry_id entry.entry_id
] ]
data = SensorPushBluetoothDeviceData() processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
processor = PassiveBluetoothDataProcessor(
lambda service_info: sensor_update_to_bluetooth_data_update(
data.update(service_info)
)
)
entry.async_on_unload( entry.async_on_unload(
processor.async_add_entities_listener( processor.async_add_entities_listener(
SensorPushBluetoothSensorEntity, async_add_entities SensorPushBluetoothSensorEntity, async_add_entities

View file

@ -3,7 +3,14 @@ from __future__ import annotations
import logging 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 ( from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorCoordinator,
) )
@ -18,14 +25,47 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__) _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: 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
kwargs = {}
if bindkey := entry.data.get("bindkey"):
kwargs["bindkey"] = bytes.fromhex(bindkey)
data = XiaomiBluetoothDeviceData(**kwargs)
coordinator = hass.data.setdefault(DOMAIN, {})[ coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id entry.entry_id
] = PassiveBluetoothProcessorCoordinator( ] = 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) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload( entry.async_on_unload(

View file

@ -3,18 +3,9 @@ from __future__ import annotations
from typing import Optional, Union from typing import Optional, Union
from xiaomi_ble import ( from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units
DeviceClass,
DeviceKey,
SensorDeviceInfo,
SensorUpdate,
Units,
XiaomiBluetoothDeviceData,
)
from xiaomi_ble.parser import EncryptionScheme
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.bluetooth.passive_update_processor import ( from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothDataProcessor, PassiveBluetoothDataProcessor,
PassiveBluetoothDataUpdate, 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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: config_entries.ConfigEntry, entry: config_entries.ConfigEntry,
@ -195,13 +165,7 @@ async def async_setup_entry(
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
entry.entry_id entry.entry_id
] ]
kwargs = {} processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
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)
)
entry.async_on_unload( entry.async_on_unload(
processor.async_add_entities_listener( processor.async_add_entities_listener(
XiaomiBluetoothSensorEntity, async_add_entities XiaomiBluetoothSensorEntity, async_add_entities

View file

@ -84,14 +84,25 @@ async def test_basic_usage(hass, mock_bleak_scanner_start):
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@callback @callback
def _async_generate_mock_data( def _mock_update_method(
service_info: BluetoothServiceInfo, service_info: BluetoothServiceInfo,
) -> dict[str, str]:
return {"test": "data"}
@callback
def _async_generate_mock_data(
data: dict[str, str],
) -> PassiveBluetoothDataUpdate: ) -> PassiveBluetoothDataUpdate:
"""Generate mock data.""" """Generate mock data."""
assert data == {"test": "data"}
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
coordinator = PassiveBluetoothProcessorCoordinator( 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 assert coordinator.available is False # no data yet
saved_callback = None 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() await hass.async_block_till_done()
@callback @callback
def _async_generate_mock_data( def _mock_update_method(
service_info: BluetoothServiceInfo, service_info: BluetoothServiceInfo,
) -> dict[str, str]:
return {"test": "data"}
@callback
def _async_generate_mock_data(
data: dict[str, str],
) -> PassiveBluetoothDataUpdate: ) -> PassiveBluetoothDataUpdate:
"""Generate mock data.""" """Generate mock data."""
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
coordinator = PassiveBluetoothProcessorCoordinator( 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 assert coordinator.available is False # no data yet
saved_callback = None 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: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@callback @callback
def _async_generate_mock_data( def _mock_update_method(
service_info: BluetoothServiceInfo, service_info: BluetoothServiceInfo,
) -> dict[str, str]:
return {"test": "data"}
@callback
def _async_generate_mock_data(
data: dict[str, str],
) -> PassiveBluetoothDataUpdate: ) -> PassiveBluetoothDataUpdate:
"""Generate mock data.""" """Generate mock data."""
return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
coordinator = PassiveBluetoothProcessorCoordinator( 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 assert coordinator.available is False # no data yet
saved_callback = None saved_callback = None
@ -326,8 +357,14 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta
run_count = 0 run_count = 0
@callback @callback
def _async_generate_mock_data( def _mock_update_method(
service_info: BluetoothServiceInfo, service_info: BluetoothServiceInfo,
) -> dict[str, str]:
return {"test": "data"}
@callback
def _async_generate_mock_data(
data: dict[str, str],
) -> PassiveBluetoothDataUpdate: ) -> PassiveBluetoothDataUpdate:
"""Generate mock data.""" """Generate mock data."""
nonlocal run_count 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 return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
coordinator = PassiveBluetoothProcessorCoordinator( 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 assert coordinator.available is False # no data yet
saved_callback = None saved_callback = None
@ -379,8 +420,14 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start):
run_count = 0 run_count = 0
@callback @callback
def _async_generate_mock_data( def _mock_update_method(
service_info: BluetoothServiceInfo, service_info: BluetoothServiceInfo,
) -> dict[str, str]:
return {"test": "data"}
@callback
def _async_generate_mock_data(
data: dict[str, str],
) -> PassiveBluetoothDataUpdate: ) -> PassiveBluetoothDataUpdate:
"""Generate mock data.""" """Generate mock data."""
nonlocal run_count 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 return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE
coordinator = PassiveBluetoothProcessorCoordinator( 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 assert coordinator.available is False # no data yet
saved_callback = None saved_callback = None
@ -721,8 +772,14 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start):
update_count = 0 update_count = 0
@callback @callback
def _async_generate_mock_data( def _mock_update_method(
service_info: BluetoothServiceInfo, service_info: BluetoothServiceInfo,
) -> dict[str, str]:
return {"test": "data"}
@callback
def _async_generate_mock_data(
data: dict[str, str],
) -> PassiveBluetoothDataUpdate: ) -> PassiveBluetoothDataUpdate:
"""Generate mock data.""" """Generate mock data."""
nonlocal update_count 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 return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE
coordinator = PassiveBluetoothProcessorCoordinator( 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 assert coordinator.available is False # no data yet
saved_callback = None 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: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@callback @callback
def _async_generate_mock_data( def _mock_update_method(
service_info: BluetoothServiceInfo, service_info: BluetoothServiceInfo,
) -> dict[str, str]:
return {"test": "data"}
@callback
def _async_generate_mock_data(
data: dict[str, str],
) -> PassiveBluetoothDataUpdate: ) -> PassiveBluetoothDataUpdate:
"""Generate mock data.""" """Generate mock data."""
return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE
coordinator = PassiveBluetoothProcessorCoordinator( 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 assert coordinator.available is False # no data yet
saved_callback = None saved_callback = None
@ -899,14 +970,24 @@ async def test_passive_bluetooth_entity_with_entity_platform(
entity_platform = MockEntityPlatform(hass) entity_platform = MockEntityPlatform(hass)
@callback @callback
def _async_generate_mock_data( def _mock_update_method(
service_info: BluetoothServiceInfo, service_info: BluetoothServiceInfo,
) -> dict[str, str]:
return {"test": "data"}
@callback
def _async_generate_mock_data(
data: dict[str, str],
) -> PassiveBluetoothDataUpdate: ) -> PassiveBluetoothDataUpdate:
"""Generate mock data.""" """Generate mock data."""
return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE
coordinator = PassiveBluetoothProcessorCoordinator( 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 assert coordinator.available is False # no data yet
saved_callback = None 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.""" """Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
@callback
def _mock_update_method(
service_info: BluetoothServiceInfo,
) -> dict[str, str]:
return {"test": "data"}
coordinator = PassiveBluetoothProcessorCoordinator( 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 assert coordinator.available is False # no data yet
saved_callback = None saved_callback = None
@ -1075,3 +1166,68 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st
key="motion", device_id=None key="motion", device_id=None
) )
cancel_coordinator() 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()