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.
|
"""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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue