2019-12-13 02:08:43 +01:00
|
|
|
import base64
|
2023-02-19 23:11:19 +01:00
|
|
|
from asyncio import Event, Lock, ensure_future
|
2023-02-10 17:40:20 +01:00
|
|
|
from contextlib import asynccontextmanager
|
2023-02-13 23:12:25 +01:00
|
|
|
from threading import RLock, Timer
|
2023-02-18 01:15:10 +01:00
|
|
|
from time import time
|
|
|
|
from typing import (
|
|
|
|
Any,
|
|
|
|
AsyncGenerator,
|
|
|
|
Collection,
|
|
|
|
Final,
|
|
|
|
List,
|
|
|
|
Optional,
|
|
|
|
Dict,
|
|
|
|
Type,
|
|
|
|
Union,
|
|
|
|
)
|
2023-02-10 17:40:20 +01:00
|
|
|
from uuid import UUID
|
|
|
|
|
|
|
|
from bleak import BleakClient, BleakScanner
|
|
|
|
from bleak.backends.device import BLEDevice
|
2023-02-18 01:15:10 +01:00
|
|
|
from bleak.backends.scanner import AdvertisementData
|
2023-02-10 17:40:20 +01:00
|
|
|
from typing_extensions import override
|
|
|
|
|
|
|
|
from platypush.context import get_bus, get_or_create_event_loop
|
|
|
|
from platypush.entities import Entity, EntityManager
|
2023-02-13 23:12:25 +01:00
|
|
|
from platypush.entities.bluetooth import BluetoothDevice
|
2023-02-18 01:15:10 +01:00
|
|
|
from platypush.message.event.bluetooth import (
|
2023-02-10 17:40:20 +01:00
|
|
|
BluetoothDeviceBlockedEvent,
|
|
|
|
BluetoothDeviceConnectedEvent,
|
|
|
|
BluetoothDeviceDisconnectedEvent,
|
|
|
|
BluetoothDeviceFoundEvent,
|
|
|
|
BluetoothDeviceLostEvent,
|
2023-02-22 02:23:11 +01:00
|
|
|
BluetoothDeviceNewDataEvent,
|
2023-02-10 17:40:20 +01:00
|
|
|
BluetoothDevicePairedEvent,
|
2023-02-18 01:15:10 +01:00
|
|
|
BluetoothDeviceSignalUpdateEvent,
|
2023-02-10 17:40:20 +01:00
|
|
|
BluetoothDeviceTrustedEvent,
|
|
|
|
BluetoothDeviceUnblockedEvent,
|
|
|
|
BluetoothDeviceUnpairedEvent,
|
|
|
|
BluetoothDeviceUntrustedEvent,
|
2023-02-13 23:12:25 +01:00
|
|
|
BluetoothDeviceEvent,
|
|
|
|
BluetoothScanPausedEvent,
|
|
|
|
BluetoothScanResumedEvent,
|
2023-02-10 17:40:20 +01:00
|
|
|
)
|
|
|
|
from platypush.plugins import AsyncRunnablePlugin, action
|
|
|
|
|
2023-02-20 20:27:17 +01:00
|
|
|
from ._mappers import device_to_entity, parse_device_args
|
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
UUIDType = Union[str, UUID]
|
|
|
|
|
|
|
|
|
|
|
|
class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
2019-12-13 02:08:43 +01:00
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
Plugin to interact with BLE (Bluetooth Low-Energy) devices.
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-22 02:23:11 +01:00
|
|
|
This plugin uses `_Bleak_ <https://github.com/hbldh/bleak>`_ to interact
|
|
|
|
with the Bluetooth stack and `_Theengs_ <https://github.com/theengs/decoder>`_
|
|
|
|
to map the services exposed by the devices into native entities.
|
|
|
|
|
|
|
|
The full list of devices natively supported can be found
|
|
|
|
`here <https://decoder.theengs.io/devices/devices_by_brand.html>`_.
|
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
Note that the support for Bluetooth low-energy devices requires a Bluetooth
|
|
|
|
adapter compatible with the Bluetooth 5.0 specification or higher.
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
Requires:
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
* **bleak** (``pip install bleak``)
|
2023-02-18 01:15:10 +01:00
|
|
|
* **bluetooth-numbers** (``pip install bluetooth-numbers``)
|
2023-02-20 20:27:17 +01:00
|
|
|
* **TheengsGateway** (``pip install git+https://github.com/BlackLight/TheengsGateway``)
|
2023-02-18 01:15:10 +01:00
|
|
|
|
|
|
|
Triggers:
|
|
|
|
|
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothDeviceBlockedEvent`
|
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothDeviceConnectedEvent`
|
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothDeviceDisconnectedEvent`
|
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothDeviceFoundEvent`
|
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothDeviceLostEvent`
|
2023-02-22 02:23:11 +01:00
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothDeviceNewDataEvent`
|
2023-02-18 01:15:10 +01:00
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothDevicePairedEvent`
|
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothDeviceTrustedEvent`
|
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothDeviceUnblockedEvent`
|
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothDeviceUnpairedEvent`
|
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothDeviceUntrustedEvent`
|
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothScanPausedEvent`
|
|
|
|
* :class:`platypush.message.event.bluetooth.BluetoothScanResumedEvent`
|
2019-12-13 02:08:43 +01:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
2023-02-18 01:15:10 +01:00
|
|
|
_default_connect_timeout: Final[int] = 5
|
|
|
|
""" Default connection timeout (in seconds) """
|
|
|
|
|
|
|
|
_rssi_update_interval: Final[int] = 30
|
|
|
|
"""
|
|
|
|
How long we should wait before triggering an update event upon a new
|
|
|
|
RSSI update, in seconds.
|
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
interface: Optional[str] = None,
|
|
|
|
connect_timeout: float = _default_connect_timeout,
|
|
|
|
device_names: Optional[Dict[str, str]] = None,
|
2023-02-18 01:15:10 +01:00
|
|
|
uuids: Optional[Collection[UUIDType]] = None,
|
2023-02-13 23:12:25 +01:00
|
|
|
scan_paused_on_start: bool = False,
|
2023-02-10 17:40:20 +01:00
|
|
|
**kwargs,
|
|
|
|
):
|
2019-12-13 02:08:43 +01:00
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
:param interface: Name of the Bluetooth interface to use (e.g. ``hci0``
|
|
|
|
on Linux). Default: first available interface.
|
|
|
|
:param connect_timeout: Timeout in seconds for the connection to a
|
|
|
|
Bluetooth device. Default: 5 seconds.
|
2023-02-18 01:15:10 +01:00
|
|
|
:param uuids: List of service/characteristic UUIDs to discover.
|
|
|
|
Default: all.
|
2023-02-10 17:40:20 +01:00
|
|
|
:param device_names: Bluetooth address -> device name mapping. If not
|
|
|
|
specified, the device's advertised name will be used, or its
|
|
|
|
Bluetooth address. Example:
|
|
|
|
|
|
|
|
.. code-block:: json
|
|
|
|
|
|
|
|
{
|
|
|
|
"00:11:22:33:44:55": "Switchbot",
|
|
|
|
"00:11:22:33:44:56": "Headphones",
|
|
|
|
"00:11:22:33:44:57": "Button"
|
|
|
|
}
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-13 23:12:25 +01:00
|
|
|
:param scan_paused_on_start: If ``True``, the plugin will not the
|
|
|
|
scanning thread until :meth:`.scan_resume` is called (default:
|
|
|
|
``False``).
|
|
|
|
|
2020-03-08 23:50:23 +01:00
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
super().__init__(**kwargs)
|
2020-03-08 23:50:23 +01:00
|
|
|
|
2023-02-18 01:15:10 +01:00
|
|
|
self._interface: Optional[str] = interface
|
|
|
|
self._connect_timeout: float = connect_timeout
|
|
|
|
self._uuids: Collection[Union[str, UUID]] = uuids or []
|
2023-02-10 17:40:20 +01:00
|
|
|
self._scan_lock = RLock()
|
2023-02-13 23:12:25 +01:00
|
|
|
self._scan_enabled = Event()
|
|
|
|
self._scan_controller_timer: Optional[Timer] = None
|
2023-02-10 17:40:20 +01:00
|
|
|
self._connections: Dict[str, BleakClient] = {}
|
2023-02-19 23:11:19 +01:00
|
|
|
self._connection_locks: Dict[str, Lock] = {}
|
2023-02-10 17:40:20 +01:00
|
|
|
self._devices: Dict[str, BLEDevice] = {}
|
2023-02-20 20:27:17 +01:00
|
|
|
self._entities: Dict[str, BluetoothDevice] = {}
|
2023-02-18 01:15:10 +01:00
|
|
|
self._device_last_updated_at: Dict[str, float] = {}
|
2023-02-10 17:40:20 +01:00
|
|
|
self._device_name_by_addr = device_names or {}
|
|
|
|
self._device_addr_by_name = {
|
|
|
|
name: addr for addr, name in self._device_name_by_addr.items()
|
|
|
|
}
|
|
|
|
|
2023-02-13 23:12:25 +01:00
|
|
|
if not scan_paused_on_start:
|
|
|
|
self._scan_enabled.set()
|
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
async def _get_device(self, device: str) -> BLEDevice:
|
2019-12-13 02:08:43 +01:00
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
Utility method to get a device by name or address.
|
2019-12-13 02:08:43 +01:00
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
addr = (
|
|
|
|
self._device_addr_by_name[device]
|
|
|
|
if device in self._device_addr_by_name
|
|
|
|
else device
|
|
|
|
)
|
|
|
|
|
|
|
|
if addr not in self._devices:
|
|
|
|
self.logger.info('Scanning for unknown device "%s"', device)
|
|
|
|
await self._scan()
|
|
|
|
|
|
|
|
dev = self._devices.get(addr)
|
|
|
|
assert dev is not None, f'Unknown device: "{device}"'
|
|
|
|
return dev
|
|
|
|
|
|
|
|
def _post_event(
|
2023-02-13 23:12:25 +01:00
|
|
|
self, event_type: Type[BluetoothDeviceEvent], device: BLEDevice, **kwargs
|
2023-02-10 17:40:20 +01:00
|
|
|
):
|
|
|
|
get_bus().post(
|
2023-02-20 20:27:17 +01:00
|
|
|
event_type(address=device.address, **parse_device_args(device), **kwargs)
|
2023-02-10 17:40:20 +01:00
|
|
|
)
|
|
|
|
|
2023-02-20 20:27:17 +01:00
|
|
|
def _on_device_event(self, device: BLEDevice, data: AdvertisementData):
|
|
|
|
"""
|
|
|
|
Device advertisement packet callback handler.
|
2023-02-18 01:15:10 +01:00
|
|
|
|
2023-02-20 20:27:17 +01:00
|
|
|
1. It generates the relevant
|
|
|
|
:class:`platypush.message.event.bluetooth.BluetoothDeviceEvent` if the
|
|
|
|
state of the device has changed.
|
2023-02-18 01:15:10 +01:00
|
|
|
|
2023-02-20 20:27:17 +01:00
|
|
|
2. It builds the relevant
|
|
|
|
:class:`platypush.entity.bluetooth.BluetoothDevice` entity object
|
|
|
|
populated with children entities that contain the supported
|
|
|
|
properties.
|
2023-02-18 01:15:10 +01:00
|
|
|
|
2023-02-20 20:27:17 +01:00
|
|
|
:param device: The Bluetooth device.
|
|
|
|
:param data: The advertisement data.
|
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
|
2023-02-20 20:27:17 +01:00
|
|
|
event_types: List[Type[BluetoothDeviceEvent]] = []
|
|
|
|
entity = device_to_entity(device, data)
|
2023-02-22 02:23:11 +01:00
|
|
|
existing_entity = self._entities.get(device.address)
|
|
|
|
existing_device = self._devices.get(device.address)
|
2023-02-10 17:40:20 +01:00
|
|
|
|
2023-02-22 02:23:11 +01:00
|
|
|
if existing_entity and existing_device:
|
2023-02-20 20:27:17 +01:00
|
|
|
if existing_entity.paired != entity.paired:
|
2023-02-10 17:40:20 +01:00
|
|
|
event_types.append(
|
|
|
|
BluetoothDevicePairedEvent
|
2023-02-20 20:27:17 +01:00
|
|
|
if entity.paired
|
2023-02-10 17:40:20 +01:00
|
|
|
else BluetoothDeviceUnpairedEvent
|
|
|
|
)
|
|
|
|
|
2023-02-20 20:27:17 +01:00
|
|
|
if existing_entity.connected != entity.connected:
|
2023-02-10 17:40:20 +01:00
|
|
|
event_types.append(
|
|
|
|
BluetoothDeviceConnectedEvent
|
2023-02-20 20:27:17 +01:00
|
|
|
if entity.connected
|
2023-02-10 17:40:20 +01:00
|
|
|
else BluetoothDeviceDisconnectedEvent
|
|
|
|
)
|
|
|
|
|
2023-02-20 20:27:17 +01:00
|
|
|
if existing_entity.blocked != entity.blocked:
|
2023-02-10 17:40:20 +01:00
|
|
|
event_types.append(
|
|
|
|
BluetoothDeviceBlockedEvent
|
2023-02-20 20:27:17 +01:00
|
|
|
if entity.blocked
|
2023-02-10 17:40:20 +01:00
|
|
|
else BluetoothDeviceUnblockedEvent
|
|
|
|
)
|
|
|
|
|
2023-02-20 20:27:17 +01:00
|
|
|
if existing_entity.trusted != entity.trusted:
|
2023-02-10 17:40:20 +01:00
|
|
|
event_types.append(
|
|
|
|
BluetoothDeviceTrustedEvent
|
2023-02-20 20:27:17 +01:00
|
|
|
if entity.trusted
|
2023-02-10 17:40:20 +01:00
|
|
|
else BluetoothDeviceUntrustedEvent
|
|
|
|
)
|
2023-02-18 01:15:10 +01:00
|
|
|
|
|
|
|
if (
|
|
|
|
time() - self._device_last_updated_at.get(device.address, 0)
|
|
|
|
) >= self._rssi_update_interval and (
|
2023-02-20 20:27:17 +01:00
|
|
|
existing_entity.rssi != device.rssi
|
|
|
|
or existing_entity.tx_power != entity.tx_power
|
2023-02-18 01:15:10 +01:00
|
|
|
):
|
|
|
|
event_types.append(BluetoothDeviceSignalUpdateEvent)
|
2023-02-22 02:23:11 +01:00
|
|
|
|
|
|
|
if (
|
|
|
|
existing_device.metadata.get('manufacturer_data', {})
|
|
|
|
!= device.metadata.get('manufacturer_data', {})
|
|
|
|
) or (
|
|
|
|
existing_device.details.get('props', {}).get('ServiceData', {})
|
|
|
|
!= device.details.get('props', {}).get('ServiceData', {})
|
|
|
|
):
|
|
|
|
event_types.append(BluetoothDeviceNewDataEvent)
|
2019-12-13 02:08:43 +01:00
|
|
|
else:
|
2023-02-10 17:40:20 +01:00
|
|
|
event_types.append(BluetoothDeviceFoundEvent)
|
|
|
|
|
|
|
|
self._devices[device.address] = device
|
2023-02-13 23:12:25 +01:00
|
|
|
if device.name:
|
|
|
|
self._device_name_by_addr[device.address] = device.name
|
|
|
|
self._device_addr_by_name[device.name] = device.address
|
2023-02-10 17:40:20 +01:00
|
|
|
|
|
|
|
if event_types:
|
|
|
|
for event_type in event_types:
|
|
|
|
self._post_event(event_type, device)
|
2023-02-18 01:15:10 +01:00
|
|
|
self._device_last_updated_at[device.address] = time()
|
2023-02-10 17:40:20 +01:00
|
|
|
|
2023-02-22 02:23:11 +01:00
|
|
|
for child in entity.children:
|
|
|
|
child.parent = entity
|
2023-02-20 20:27:17 +01:00
|
|
|
|
2023-02-22 02:23:11 +01:00
|
|
|
self.publish_entities([entity])
|
2023-02-20 20:27:17 +01:00
|
|
|
|
|
|
|
def _has_changed(self, entity: BluetoothDevice) -> bool:
|
|
|
|
existing_entity = self._entities.get(entity.id or entity.external_id)
|
|
|
|
|
|
|
|
# If the entity didn't exist before, it's a new device.
|
|
|
|
if not existing_entity:
|
|
|
|
return True
|
|
|
|
|
|
|
|
entity_dict = entity.to_json()
|
|
|
|
existing_entity_dict = entity.to_json()
|
|
|
|
|
|
|
|
# Check if any of the root attributes changed, excluding those that are
|
|
|
|
# managed by the entities engine).
|
|
|
|
return any(
|
|
|
|
attr
|
|
|
|
for attr, value in entity_dict.items()
|
|
|
|
if value != existing_entity_dict.get(attr)
|
|
|
|
and attr not in {'id', 'external_id', 'plugin', 'updated_at'}
|
|
|
|
)
|
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
@asynccontextmanager
|
|
|
|
async def _connect(
|
|
|
|
self,
|
|
|
|
device: str,
|
|
|
|
interface: Optional[str] = None,
|
|
|
|
timeout: Optional[float] = None,
|
|
|
|
) -> AsyncGenerator[BleakClient, None]:
|
|
|
|
dev = await self._get_device(device)
|
2023-02-19 23:11:19 +01:00
|
|
|
|
|
|
|
async with self._connection_locks.get(dev.address, Lock()) as lock:
|
|
|
|
self._connection_locks[dev.address] = lock or Lock()
|
|
|
|
|
|
|
|
async with BleakClient(
|
|
|
|
dev.address,
|
|
|
|
adapter=interface or self._interface,
|
|
|
|
timeout=timeout or self._connect_timeout,
|
|
|
|
) as client:
|
|
|
|
self._connections[dev.address] = client
|
|
|
|
yield client
|
|
|
|
self._connections.pop(dev.address, None)
|
2023-02-10 17:40:20 +01:00
|
|
|
|
|
|
|
async def _read(
|
|
|
|
self,
|
|
|
|
device: str,
|
|
|
|
service_uuid: UUIDType,
|
|
|
|
interface: Optional[str] = None,
|
|
|
|
connect_timeout: Optional[float] = None,
|
|
|
|
) -> bytearray:
|
|
|
|
async with self._connect(device, interface, connect_timeout) as client:
|
|
|
|
data = await client.read_gatt_char(service_uuid)
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
return data
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
async def _write(
|
|
|
|
self,
|
|
|
|
device: str,
|
|
|
|
data: bytes,
|
|
|
|
service_uuid: UUIDType,
|
|
|
|
interface: Optional[str] = None,
|
|
|
|
connect_timeout: Optional[float] = None,
|
|
|
|
):
|
|
|
|
async with self._connect(device, interface, connect_timeout) as client:
|
|
|
|
await client.write_gatt_char(service_uuid, data)
|
|
|
|
|
|
|
|
async def _scan(
|
|
|
|
self,
|
|
|
|
duration: Optional[float] = None,
|
2023-02-18 01:15:10 +01:00
|
|
|
uuids: Optional[Collection[UUIDType]] = None,
|
2023-02-10 17:40:20 +01:00
|
|
|
) -> Collection[Entity]:
|
|
|
|
with self._scan_lock:
|
|
|
|
timeout = duration or self.poll_interval or 5
|
|
|
|
devices = await BleakScanner.discover(
|
|
|
|
adapter=self._interface,
|
|
|
|
timeout=timeout,
|
2023-02-18 01:15:10 +01:00
|
|
|
service_uuids=list(map(str, uuids or self._uuids or [])),
|
2023-02-10 17:40:20 +01:00
|
|
|
detection_callback=self._on_device_event,
|
|
|
|
)
|
|
|
|
|
|
|
|
self._devices.update({dev.address: dev for dev in devices})
|
2023-02-20 20:27:17 +01:00
|
|
|
addresses = {dev.address.lower() for dev in devices}
|
|
|
|
return [
|
|
|
|
dev
|
|
|
|
for addr, dev in self._entities.items()
|
|
|
|
if isinstance(dev, BluetoothDevice)
|
|
|
|
and addr.lower() in addresses
|
|
|
|
and dev.reachable
|
|
|
|
]
|
2023-02-13 23:12:25 +01:00
|
|
|
|
|
|
|
async def _scan_state_set(self, state: bool, duration: Optional[float] = None):
|
|
|
|
def timer_callback():
|
|
|
|
if state:
|
|
|
|
self.scan_pause()
|
|
|
|
else:
|
|
|
|
self.scan_resume()
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-13 23:12:25 +01:00
|
|
|
self._scan_controller_timer = None
|
|
|
|
|
|
|
|
with self._scan_lock:
|
|
|
|
if not state and self._scan_enabled.is_set():
|
|
|
|
get_bus().post(BluetoothScanPausedEvent(duration=duration))
|
|
|
|
elif state and not self._scan_enabled.is_set():
|
|
|
|
get_bus().post(BluetoothScanResumedEvent(duration=duration))
|
|
|
|
|
|
|
|
if state:
|
|
|
|
self._scan_enabled.set()
|
|
|
|
else:
|
|
|
|
self._scan_enabled.clear()
|
|
|
|
|
|
|
|
if duration and not self._scan_controller_timer:
|
|
|
|
self._scan_controller_timer = Timer(duration, timer_callback)
|
|
|
|
self._scan_controller_timer.start()
|
|
|
|
|
|
|
|
@action
|
|
|
|
def scan_pause(self, duration: Optional[float] = None):
|
|
|
|
"""
|
|
|
|
Pause the scanning thread.
|
|
|
|
|
|
|
|
:param duration: For how long the scanning thread should be paused
|
|
|
|
(default: null = indefinitely).
|
|
|
|
"""
|
|
|
|
if self._loop:
|
|
|
|
ensure_future(self._scan_state_set(False, duration), loop=self._loop)
|
|
|
|
|
|
|
|
@action
|
|
|
|
def scan_resume(self, duration: Optional[float] = None):
|
|
|
|
"""
|
|
|
|
Resume the scanning thread, if inactive.
|
|
|
|
|
|
|
|
:param duration: For how long the scanning thread should be running
|
|
|
|
(default: null = indefinitely).
|
|
|
|
"""
|
|
|
|
if self._loop:
|
|
|
|
ensure_future(self._scan_state_set(True, duration), loop=self._loop)
|
2019-12-13 02:08:43 +01:00
|
|
|
|
|
|
|
@action
|
2023-02-10 17:40:20 +01:00
|
|
|
def scan(
|
|
|
|
self,
|
|
|
|
duration: Optional[float] = None,
|
2023-02-18 01:15:10 +01:00
|
|
|
uuids: Optional[Collection[UUIDType]] = None,
|
2023-02-10 17:40:20 +01:00
|
|
|
):
|
2019-12-13 02:08:43 +01:00
|
|
|
"""
|
2023-02-13 23:12:25 +01:00
|
|
|
Scan for Bluetooth devices nearby and return the results as a list of
|
|
|
|
entities.
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
:param duration: Scan duration in seconds (default: same as the plugin's
|
|
|
|
`poll_interval` configuration parameter)
|
2023-02-18 01:15:10 +01:00
|
|
|
:param uuids: List of characteristic UUIDs to discover. Default: all.
|
2019-12-13 02:08:43 +01:00
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
loop = get_or_create_event_loop()
|
2023-02-20 20:27:17 +01:00
|
|
|
return loop.run_until_complete(self._scan(duration, uuids))
|
2019-12-13 02:08:43 +01:00
|
|
|
|
|
|
|
@action
|
2023-02-10 17:40:20 +01:00
|
|
|
def read(
|
|
|
|
self,
|
|
|
|
device: str,
|
|
|
|
service_uuid: UUIDType,
|
|
|
|
interface: Optional[str] = None,
|
|
|
|
connect_timeout: Optional[float] = None,
|
|
|
|
) -> str:
|
2019-12-13 02:08:43 +01:00
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
Read a message from a device.
|
|
|
|
|
|
|
|
:param device: Name or address of the device to read from.
|
|
|
|
:param service_uuid: Service UUID.
|
|
|
|
:param interface: Bluetooth adapter name to use (default configured if None).
|
|
|
|
:param connect_timeout: Connection timeout in seconds (default: same as the
|
|
|
|
configured `connect_timeout`).
|
|
|
|
:return: The base64-encoded response received from the device.
|
2019-12-13 02:08:43 +01:00
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
loop = get_or_create_event_loop()
|
|
|
|
data = loop.run_until_complete(
|
|
|
|
self._read(device, service_uuid, interface, connect_timeout)
|
|
|
|
)
|
|
|
|
return base64.b64encode(data).decode()
|
2019-12-13 02:08:43 +01:00
|
|
|
|
|
|
|
@action
|
2023-02-10 17:40:20 +01:00
|
|
|
def write(
|
|
|
|
self,
|
|
|
|
device: str,
|
|
|
|
data: Union[str, bytes],
|
|
|
|
service_uuid: UUIDType,
|
|
|
|
interface: Optional[str] = None,
|
|
|
|
connect_timeout: Optional[float] = None,
|
|
|
|
):
|
2019-12-13 02:08:43 +01:00
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
Writes data to a device
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
:param device: Name or address of the device to read from.
|
|
|
|
:param data: Data to be written, either as bytes or as a base64-encoded string.
|
|
|
|
:param service_uuid: Service UUID.
|
2019-12-13 02:08:43 +01:00
|
|
|
:param interface: Bluetooth adapter name to use (default configured if None)
|
2023-02-10 17:40:20 +01:00
|
|
|
:param connect_timeout: Connection timeout in seconds (default: same as the
|
|
|
|
configured `connect_timeout`).
|
2019-12-13 02:08:43 +01:00
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
loop = get_or_create_event_loop()
|
|
|
|
if isinstance(data, str):
|
|
|
|
data = base64.b64decode(data.encode())
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
loop.run_until_complete(
|
|
|
|
self._write(device, data, service_uuid, interface, connect_timeout)
|
|
|
|
)
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
@override
|
2019-12-13 02:08:43 +01:00
|
|
|
@action
|
2023-02-10 17:40:20 +01:00
|
|
|
def status(self, *_, **__) -> Collection[Entity]:
|
2019-12-13 02:08:43 +01:00
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
Alias for :meth:`.scan`.
|
2019-12-13 02:08:43 +01:00
|
|
|
"""
|
2023-02-10 17:40:20 +01:00
|
|
|
return self.scan().output
|
|
|
|
|
2023-02-20 20:27:17 +01:00
|
|
|
@override
|
|
|
|
def publish_entities(
|
|
|
|
self, entities: Optional[Collection[Any]]
|
|
|
|
) -> Collection[Entity]:
|
|
|
|
self._entities.update({entity.id: entity for entity in (entities or [])})
|
|
|
|
|
|
|
|
return super().publish_entities(entities)
|
|
|
|
|
2023-02-10 17:40:20 +01:00
|
|
|
@override
|
2023-02-13 23:12:25 +01:00
|
|
|
def transform_entities(
|
2023-02-20 20:27:17 +01:00
|
|
|
self, entities: Collection[Union[BLEDevice, BluetoothDevice]]
|
2023-02-13 23:12:25 +01:00
|
|
|
) -> Collection[BluetoothDevice]:
|
2023-02-10 17:40:20 +01:00
|
|
|
return [
|
2023-02-13 23:12:25 +01:00
|
|
|
BluetoothDevice(
|
2023-02-10 17:40:20 +01:00
|
|
|
id=dev.address,
|
2023-02-20 20:27:17 +01:00
|
|
|
**parse_device_args(dev),
|
2023-02-10 17:40:20 +01:00
|
|
|
)
|
2023-02-20 20:27:17 +01:00
|
|
|
if isinstance(dev, BLEDevice)
|
|
|
|
else dev
|
2023-02-10 17:40:20 +01:00
|
|
|
for dev in entities
|
|
|
|
]
|
|
|
|
|
|
|
|
@override
|
|
|
|
async def listen(self):
|
|
|
|
device_addresses = set()
|
|
|
|
|
|
|
|
while True:
|
2023-02-13 23:12:25 +01:00
|
|
|
await self._scan_enabled.wait()
|
2023-02-22 03:35:05 +01:00
|
|
|
entities = await self._scan(uuids=self._uuids)
|
2023-02-13 23:12:25 +01:00
|
|
|
|
2023-02-20 20:27:17 +01:00
|
|
|
new_device_addresses = {e.external_id for e in entities}
|
2023-02-10 17:40:20 +01:00
|
|
|
missing_device_addresses = device_addresses - new_device_addresses
|
|
|
|
missing_devices = [
|
|
|
|
dev
|
|
|
|
for addr, dev in self._devices.items()
|
|
|
|
if addr in missing_device_addresses
|
|
|
|
]
|
|
|
|
|
|
|
|
for dev in missing_devices:
|
|
|
|
self._post_event(BluetoothDeviceLostEvent, dev)
|
|
|
|
self._devices.pop(dev.address, None)
|
2023-02-20 20:27:17 +01:00
|
|
|
self._entities.pop(dev.address, None)
|
2023-02-10 17:40:20 +01:00
|
|
|
|
|
|
|
device_addresses = new_device_addresses
|
2019-12-13 02:08:43 +01:00
|
|
|
|
2023-02-13 23:12:25 +01:00
|
|
|
@override
|
|
|
|
def stop(self):
|
|
|
|
if self._scan_controller_timer:
|
|
|
|
self._scan_controller_timer.cancel()
|
|
|
|
|
|
|
|
super().stop()
|
|
|
|
|
2019-12-13 02:08:43 +01:00
|
|
|
|
|
|
|
# vim:sw=4:ts=4:et:
|