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.
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(

View file

@ -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(

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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()