platypush/platypush/plugins/bluetooth/_ble/_event_handler.py

136 lines
4.1 KiB
Python

from datetime import datetime, timedelta
from queue import Queue
from typing import Callable, Dict, Final, List, Optional, Type
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from platypush.entities.bluetooth import BluetoothDevice
from platypush.message.event.bluetooth import (
BluetoothDeviceConnectedEvent,
BluetoothDeviceDisconnectedEvent,
BluetoothDeviceFoundEvent,
BluetoothDeviceSignalUpdateEvent,
BluetoothDeviceEvent,
)
from platypush.context import get_bus
from .._cache import EntityCache
from ._cache import DeviceCache
from ._mappers import device_to_entity
_rssi_update_interval: Final[float] = 30.0
""" How often to trigger RSSI update events for a device. """
def _has_changed(
old: Optional[BluetoothDevice], new: BluetoothDevice, attr: str
) -> bool:
"""
Returns True if the given attribute has changed on the device.
"""
if old is None:
return False # No previous value
old_value = getattr(old, attr)
new_value = getattr(new, attr)
return old_value != new_value
def _has_been_set(
old: Optional[BluetoothDevice], new: BluetoothDevice, attr: str, value: bool
) -> bool:
"""
Returns True if the given attribute has changed and its new value matches
the given value.
"""
if not _has_changed(old, new, attr):
return False
new_value = getattr(new, attr)
return new_value == value
event_matchers: Dict[
Type[BluetoothDeviceEvent],
Callable[[Optional[BluetoothDevice], BluetoothDevice], bool],
] = {
BluetoothDeviceConnectedEvent: lambda old, new: _has_been_set(
old, new, 'connected', True
),
BluetoothDeviceDisconnectedEvent: lambda old, new: old is not None
and old.connected
and _has_been_set(old, new, 'connected', False),
BluetoothDeviceFoundEvent: lambda old, new: old is None
or (old.reachable is False and new.reachable is True),
BluetoothDeviceSignalUpdateEvent: lambda old, new: (
(new.rssi is not None or new.tx_power is not None)
and (_has_changed(old, new, 'rssi') or _has_changed(old, new, 'tx_power'))
and (
not (old and old.updated_at)
or datetime.utcnow() - old.updated_at
>= timedelta(seconds=_rssi_update_interval)
)
),
}
""" A static ``BluetoothDeviceEvent -> MatchCallback`` mapping. """
# pylint: disable=too-few-public-methods
class EventHandler:
"""
Event handler for BLE devices.
"""
def __init__(
self,
device_queue: Queue,
device_cache: DeviceCache,
entity_cache: EntityCache,
):
"""
:param device_queue: Queue used to publish updated devices upstream.
:param device_cache: Device cache.
:param entity_cache: Entity cache.
"""
self._device_queue = device_queue
self._device_cache = device_cache
self._entity_cache = entity_cache
def __call__(self, device: BLEDevice, data: AdvertisementData):
"""
Handler for Bluetooth device advertisement packets.
1. It generates the relevant
:class:`platypush.message.event.bluetooth.BluetoothDeviceEvent` if the
state of the device has changed.
2. It builds the relevant
:class:`platypush.entity.bluetooth.BluetoothDevice` entity object
populated with children entities that contain the supported
properties.
3. Publishes the updated entity to the upstream queue.
:param device: The Bluetooth device.
:param data: The advertised data.
"""
events: List[BluetoothDeviceEvent] = []
existing_entity = self._entity_cache.get(device.address)
new_entity = device_to_entity(device, data)
events += [
event_type.from_device(new_entity)
for event_type, matcher in event_matchers.items()
if matcher(existing_entity, new_entity)
]
self._device_cache.add(device)
for event in events:
get_bus().post(event)
if events:
self._device_queue.put_nowait(new_entity)