forked from platypush/platypush
Extended number of supported events and data fields in Bluetooth integration.
This commit is contained in:
parent
7adae272a4
commit
613e32e7c1
7 changed files with 301 additions and 177 deletions
|
@ -297,6 +297,7 @@ autodoc_mock_imports = [
|
||||||
'aiofiles.os',
|
'aiofiles.os',
|
||||||
'async_lru',
|
'async_lru',
|
||||||
'bleak',
|
'bleak',
|
||||||
|
'bluetooth_numbers',
|
||||||
]
|
]
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
from sqlalchemy import Column, Integer, Boolean, ForeignKey
|
from sqlalchemy import (
|
||||||
|
Boolean,
|
||||||
|
Column,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
JSON,
|
||||||
|
)
|
||||||
|
|
||||||
from platypush.common.db import Base
|
from platypush.common.db import Base
|
||||||
|
|
||||||
|
@ -17,7 +23,46 @@ if 'bluetooth_device' not in Base.metadata:
|
||||||
id = Column(
|
id = Column(
|
||||||
Integer, ForeignKey(Device.id, ondelete='CASCADE'), primary_key=True
|
Integer, ForeignKey(Device.id, ondelete='CASCADE'), primary_key=True
|
||||||
)
|
)
|
||||||
|
|
||||||
connected = Column(Boolean, default=False)
|
connected = Column(Boolean, default=False)
|
||||||
|
""" Whether the device is connected. """
|
||||||
|
|
||||||
|
paired = Column(Boolean, default=False)
|
||||||
|
""" Whether the device is paired. """
|
||||||
|
|
||||||
|
trusted = Column(Boolean, default=False)
|
||||||
|
""" Whether the device is trusted. """
|
||||||
|
|
||||||
|
blocked = Column(Boolean, default=False)
|
||||||
|
""" Whether the device is blocked. """
|
||||||
|
|
||||||
|
rssi = Column(Integer, default=None)
|
||||||
|
""" Received Signal Strength Indicator. """
|
||||||
|
|
||||||
|
tx_power = Column(Integer, default=None)
|
||||||
|
""" Reported transmission power. """
|
||||||
|
|
||||||
|
manufacturers = Column(JSON)
|
||||||
|
""" Registered manufacturers for the device, as an ID -> Name map. """
|
||||||
|
|
||||||
|
uuids = Column(JSON)
|
||||||
|
"""
|
||||||
|
Service/characteristic UUIDs exposed by the device, as a
|
||||||
|
UUID -> Name map.
|
||||||
|
"""
|
||||||
|
|
||||||
|
manufacturer_data = Column(JSON)
|
||||||
|
"""
|
||||||
|
Latest manufacturer data published by the device, as a
|
||||||
|
``manufacturer_id -> data`` map, where ``data`` is a hexadecimal
|
||||||
|
string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
service_data = Column(JSON)
|
||||||
|
"""
|
||||||
|
Latest service data published by the device, as a ``service_uuid ->
|
||||||
|
data`` map, where ``data`` is a hexadecimal string.
|
||||||
|
"""
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {
|
||||||
'polymorphic_identity': __tablename__,
|
'polymorphic_identity': __tablename__,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from platypush.message.event import Event
|
from platypush.message.event import Event
|
||||||
|
|
||||||
|
@ -8,44 +8,162 @@ class BluetoothEvent(Event):
|
||||||
Base class for Bluetooth events.
|
Base class for Bluetooth events.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, address: str, *args, name: Optional[str] = None, **kwargs):
|
|
||||||
super().__init__(*args, address=address, name=name, **kwargs)
|
|
||||||
|
|
||||||
|
class BluetoothScanPausedEvent(BluetoothEvent):
|
||||||
class BluetoothWithPortEvent(BluetoothEvent):
|
|
||||||
"""
|
"""
|
||||||
Base class for Bluetooth events that include a communication port.
|
Event triggered when the Bluetooth scan is paused.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, duration: Optional[float] = None, **kwargs):
|
||||||
|
super().__init__(*args, duration=duration, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothScanResumedEvent(BluetoothEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when the Bluetooth scan is resumed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, duration: Optional[float] = None, **kwargs):
|
||||||
|
super().__init__(*args, duration=duration, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothWithPortEvent(Event):
|
||||||
|
"""
|
||||||
|
Base class for Bluetooth events with an associated port.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, port: Optional[str] = None, **kwargs):
|
def __init__(self, *args, port: Optional[str] = None, **kwargs):
|
||||||
|
"""
|
||||||
|
:param port: The communication port of the device.
|
||||||
|
"""
|
||||||
super().__init__(*args, port=port, **kwargs)
|
super().__init__(*args, port=port, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceFoundEvent(BluetoothEvent):
|
class BluetoothDeviceEvent(BluetoothWithPortEvent):
|
||||||
"""
|
"""
|
||||||
Event triggered when a Bluetooth device is found during a scan.
|
Base class for Bluetooth device events.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*args,
|
||||||
|
address: str,
|
||||||
|
connected: bool,
|
||||||
|
paired: bool,
|
||||||
|
trusted: bool,
|
||||||
|
blocked: bool,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
uuids: Optional[Dict[str, str]] = None,
|
||||||
|
rssi: Optional[int] = None,
|
||||||
|
tx_power: Optional[int] = None,
|
||||||
|
manufacturers: Optional[Dict[int, str]] = None,
|
||||||
|
manufacturer_data: Optional[Dict[int, str]] = None,
|
||||||
|
service_data: Optional[Dict[str, str]] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param address: The Bluetooth address of the device.
|
||||||
|
:param connected: Whether the device is connected.
|
||||||
|
:param paired: Whether the device is paired.
|
||||||
|
:param trusted: Whether the device is trusted.
|
||||||
|
:param blocked: Whether the device is blocked.
|
||||||
|
:param name: The name of the device.
|
||||||
|
:param uuids: The UUIDs of the services exposed by the device.
|
||||||
|
:param rssi: Received Signal Strength Indicator.
|
||||||
|
:param tx_power: Transmission power.
|
||||||
|
:param manufacturers: The manufacturers published by the device, as a
|
||||||
|
``manufacturer_id -> registered_name`` map.
|
||||||
|
:param manufacturer_data: The manufacturer data published by the
|
||||||
|
device, as a ``manufacturer_id -> data`` map, where ``data`` is a
|
||||||
|
hexadecimal string.
|
||||||
|
:param service_data: The service data published by the device, as a
|
||||||
|
``service_uuid -> data`` map, where ``data`` is a hexadecimal string.
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
*args,
|
||||||
|
address=address,
|
||||||
|
name=name,
|
||||||
|
connected=connected,
|
||||||
|
paired=paired,
|
||||||
|
blocked=blocked,
|
||||||
|
trusted=trusted,
|
||||||
|
uuids=uuids or {},
|
||||||
|
rssi=rssi,
|
||||||
|
tx_power=tx_power,
|
||||||
|
manufacturers=manufacturers or {},
|
||||||
|
manufacturer_data=manufacturer_data or {},
|
||||||
|
service_data=service_data or {},
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothDeviceFoundEvent(BluetoothDeviceEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a Bluetooth device is discovered during a scan.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceLostEvent(BluetoothEvent):
|
class BluetoothDeviceLostEvent(BluetoothDeviceEvent):
|
||||||
"""
|
"""
|
||||||
Event triggered when a Bluetooth device previously scanned is lost.
|
Event triggered when a previously discovered Bluetooth device is lost.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceConnectedEvent(BluetoothWithPortEvent):
|
class BluetoothDeviceConnectedEvent(BluetoothDeviceEvent):
|
||||||
"""
|
"""
|
||||||
Event triggered when a Bluetooth device is connected.
|
Event triggered when a Bluetooth device is connected.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceDisconnectedEvent(BluetoothWithPortEvent):
|
class BluetoothDeviceDisconnectedEvent(BluetoothDeviceEvent):
|
||||||
"""
|
"""
|
||||||
Event triggered when a Bluetooth device is disconnected.
|
Event triggered when a Bluetooth device is disconnected.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class BluetoothConnectionRejectedEvent(BluetoothWithPortEvent):
|
class BluetoothDevicePairedEvent(BluetoothDeviceEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a Bluetooth device is paired.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothDeviceUnpairedEvent(BluetoothDeviceEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a Bluetooth device is unpaired.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothDeviceBlockedEvent(BluetoothDeviceEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a Bluetooth device is blocked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothDeviceUnblockedEvent(BluetoothDeviceEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a Bluetooth device is unblocked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothDeviceTrustedEvent(BluetoothDeviceEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a Bluetooth device is trusted.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothDeviceSignalUpdateEvent(BluetoothDeviceEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when the RSSI/TX power of a Bluetooth device is updated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothDeviceUntrustedEvent(BluetoothDeviceEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a Bluetooth device is untrusted.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothConnectionRejectedEvent(BluetoothDeviceEvent):
|
||||||
"""
|
"""
|
||||||
Event triggered when a Bluetooth connection is rejected.
|
Event triggered when a Bluetooth connection is rejected.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
from typing import Collection, Optional
|
|
||||||
|
|
||||||
from platypush.message.event import Event
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothEvent(Event):
|
|
||||||
"""
|
|
||||||
Base class for Bluetooth events.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothScanPausedEvent(BluetoothEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when the Bluetooth scan is paused.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, duration: Optional[float] = None, **kwargs):
|
|
||||||
super().__init__(*args, duration=duration, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothScanResumedEvent(BluetoothEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when the Bluetooth scan is resumed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, duration: Optional[float] = None, **kwargs):
|
|
||||||
super().__init__(*args, duration=duration, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceEvent(BluetoothEvent):
|
|
||||||
"""
|
|
||||||
Base class for Bluetooth device events.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*args,
|
|
||||||
address: str,
|
|
||||||
connected: bool,
|
|
||||||
paired: bool,
|
|
||||||
trusted: bool,
|
|
||||||
blocked: bool,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
characteristics: Optional[Collection[str]] = None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
:param address: The Bluetooth address of the device.
|
|
||||||
:param connected: Whether the device is connected.
|
|
||||||
:param paired: Whether the device is paired.
|
|
||||||
:param trusted: Whether the device is trusted.
|
|
||||||
:param blocked: Whether the device is blocked.
|
|
||||||
:param name: The name of the device.
|
|
||||||
:param characteristics: The UUIDs of the characteristics exposed by the
|
|
||||||
device.
|
|
||||||
"""
|
|
||||||
super().__init__(
|
|
||||||
*args,
|
|
||||||
address=address,
|
|
||||||
name=name,
|
|
||||||
connected=connected,
|
|
||||||
paired=paired,
|
|
||||||
blocked=blocked,
|
|
||||||
trusted=trusted,
|
|
||||||
characteristics=characteristics or [],
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceFoundEvent(BluetoothDeviceEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when a Bluetooth device is discovered during a scan.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceLostEvent(BluetoothDeviceEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when a previously discovered Bluetooth device is lost.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceConnectedEvent(BluetoothDeviceEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when a Bluetooth device is connected.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceDisconnectedEvent(BluetoothDeviceEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when a Bluetooth device is disconnected.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDevicePairedEvent(BluetoothDeviceEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when a Bluetooth device is paired.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceUnpairedEvent(BluetoothDeviceEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when a Bluetooth device is unpaired.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceBlockedEvent(BluetoothDeviceEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when a Bluetooth device is blocked.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceUnblockedEvent(BluetoothDeviceEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when a Bluetooth device is unblocked.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceTrustedEvent(BluetoothDeviceEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when a Bluetooth device is trusted.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceUntrustedEvent(BluetoothDeviceEvent):
|
|
||||||
"""
|
|
||||||
Event triggered when a Bluetooth device is untrusted.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -2,23 +2,38 @@ import base64
|
||||||
from asyncio import Event, ensure_future
|
from asyncio import Event, ensure_future
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from threading import RLock, Timer
|
from threading import RLock, Timer
|
||||||
from typing import AsyncGenerator, Collection, List, Optional, Dict, Type, Union
|
from time import time
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
AsyncGenerator,
|
||||||
|
Collection,
|
||||||
|
Final,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Dict,
|
||||||
|
Type,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from bleak import BleakClient, BleakScanner
|
from bleak import BleakClient, BleakScanner
|
||||||
from bleak.backends.device import BLEDevice
|
from bleak.backends.device import BLEDevice
|
||||||
|
from bleak.backends.scanner import AdvertisementData
|
||||||
|
from bleak.uuids import uuidstr_to_str
|
||||||
|
from bluetooth_numbers import company
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from platypush.context import get_bus, get_or_create_event_loop
|
from platypush.context import get_bus, get_or_create_event_loop
|
||||||
from platypush.entities import Entity, EntityManager
|
from platypush.entities import Entity, EntityManager
|
||||||
from platypush.entities.bluetooth import BluetoothDevice
|
from platypush.entities.bluetooth import BluetoothDevice
|
||||||
from platypush.message.event.bluetooth.ble import (
|
from platypush.message.event.bluetooth import (
|
||||||
BluetoothDeviceBlockedEvent,
|
BluetoothDeviceBlockedEvent,
|
||||||
BluetoothDeviceConnectedEvent,
|
BluetoothDeviceConnectedEvent,
|
||||||
BluetoothDeviceDisconnectedEvent,
|
BluetoothDeviceDisconnectedEvent,
|
||||||
BluetoothDeviceFoundEvent,
|
BluetoothDeviceFoundEvent,
|
||||||
BluetoothDeviceLostEvent,
|
BluetoothDeviceLostEvent,
|
||||||
BluetoothDevicePairedEvent,
|
BluetoothDevicePairedEvent,
|
||||||
|
BluetoothDeviceSignalUpdateEvent,
|
||||||
BluetoothDeviceTrustedEvent,
|
BluetoothDeviceTrustedEvent,
|
||||||
BluetoothDeviceUnblockedEvent,
|
BluetoothDeviceUnblockedEvent,
|
||||||
BluetoothDeviceUnpairedEvent,
|
BluetoothDeviceUnpairedEvent,
|
||||||
|
@ -42,20 +57,40 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* **bleak** (``pip install bleak``)
|
* **bleak** (``pip install bleak``)
|
||||||
|
* **bluetooth-numbers** (``pip install bluetooth-numbers``)
|
||||||
|
|
||||||
TODO: Write supported events.
|
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`
|
||||||
|
* :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`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Default connection timeout (in seconds)
|
_default_connect_timeout: Final[int] = 5
|
||||||
_default_connect_timeout = 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.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
interface: Optional[str] = None,
|
interface: Optional[str] = None,
|
||||||
connect_timeout: float = _default_connect_timeout,
|
connect_timeout: float = _default_connect_timeout,
|
||||||
device_names: Optional[Dict[str, str]] = None,
|
device_names: Optional[Dict[str, str]] = None,
|
||||||
characteristics: Optional[Collection[UUIDType]] = None,
|
uuids: Optional[Collection[UUIDType]] = None,
|
||||||
scan_paused_on_start: bool = False,
|
scan_paused_on_start: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
@ -64,8 +99,8 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
on Linux). Default: first available interface.
|
on Linux). Default: first available interface.
|
||||||
:param connect_timeout: Timeout in seconds for the connection to a
|
:param connect_timeout: Timeout in seconds for the connection to a
|
||||||
Bluetooth device. Default: 5 seconds.
|
Bluetooth device. Default: 5 seconds.
|
||||||
:param characteristics: List of service/characteristic UUIDs to
|
:param uuids: List of service/characteristic UUIDs to discover.
|
||||||
discover. Default: all.
|
Default: all.
|
||||||
:param device_names: Bluetooth address -> device name mapping. If not
|
:param device_names: Bluetooth address -> device name mapping. If not
|
||||||
specified, the device's advertised name will be used, or its
|
specified, the device's advertised name will be used, or its
|
||||||
Bluetooth address. Example:
|
Bluetooth address. Example:
|
||||||
|
@ -85,14 +120,15 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self._interface = interface
|
self._interface: Optional[str] = interface
|
||||||
self._connect_timeout = connect_timeout
|
self._connect_timeout: float = connect_timeout
|
||||||
self._characteristics = characteristics
|
self._uuids: Collection[Union[str, UUID]] = uuids or []
|
||||||
self._scan_lock = RLock()
|
self._scan_lock = RLock()
|
||||||
self._scan_enabled = Event()
|
self._scan_enabled = Event()
|
||||||
self._scan_controller_timer: Optional[Timer] = None
|
self._scan_controller_timer: Optional[Timer] = None
|
||||||
self._connections: Dict[str, BleakClient] = {}
|
self._connections: Dict[str, BleakClient] = {}
|
||||||
self._devices: Dict[str, BLEDevice] = {}
|
self._devices: Dict[str, BLEDevice] = {}
|
||||||
|
self._device_last_updated_at: Dict[str, float] = {}
|
||||||
self._device_name_by_addr = device_names or {}
|
self._device_name_by_addr = device_names or {}
|
||||||
self._device_addr_by_name = {
|
self._device_addr_by_name = {
|
||||||
name: addr for addr, name in self._device_name_by_addr.items()
|
name: addr for addr, name in self._device_name_by_addr.items()
|
||||||
|
@ -129,21 +165,54 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
def _post_event(
|
def _post_event(
|
||||||
self, event_type: Type[BluetoothDeviceEvent], device: BLEDevice, **kwargs
|
self, event_type: Type[BluetoothDeviceEvent], device: BLEDevice, **kwargs
|
||||||
):
|
):
|
||||||
props = device.details.get('props', {})
|
|
||||||
get_bus().post(
|
get_bus().post(
|
||||||
event_type(
|
event_type(
|
||||||
address=device.address,
|
address=device.address, **self._parse_device_args(device), **kwargs
|
||||||
name=self._get_device_name(device),
|
|
||||||
connected=props.get('Connected', False),
|
|
||||||
paired=props.get('Paired', False),
|
|
||||||
blocked=props.get('Blocked', False),
|
|
||||||
trusted=props.get('Trusted', False),
|
|
||||||
characteristics=device.metadata.get('uuids', []),
|
|
||||||
**kwargs,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _on_device_event(self, device: BLEDevice, _):
|
def _parse_device_args(self, device: BLEDevice) -> Dict[str, Any]:
|
||||||
|
props = device.details.get('props', {})
|
||||||
|
return {
|
||||||
|
'name': self._get_device_name(device),
|
||||||
|
'connected': props.get('Connected', False),
|
||||||
|
'paired': props.get('Paired', False),
|
||||||
|
'blocked': props.get('Blocked', False),
|
||||||
|
'trusted': props.get('Trusted', False),
|
||||||
|
'rssi': device.rssi,
|
||||||
|
'tx_power': props.get('TxPower'),
|
||||||
|
'uuids': {
|
||||||
|
uuid: uuidstr_to_str(uuid) for uuid in device.metadata.get('uuids', [])
|
||||||
|
},
|
||||||
|
'manufacturers': {
|
||||||
|
manufacturer_id: company.get(manufacturer_id, 'Unknown')
|
||||||
|
for manufacturer_id in sorted(
|
||||||
|
device.metadata.get('manufacturer_data', {}).keys()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
'manufacturer_data': self._parse_manufacturer_data(device),
|
||||||
|
'service_data': self._parse_service_data(device),
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_manufacturer_data(device: BLEDevice) -> Dict[int, str]:
|
||||||
|
return {
|
||||||
|
manufacturer_id: ':'.join([f'{x:02x}' for x in value])
|
||||||
|
for manufacturer_id, value in device.metadata.get(
|
||||||
|
'manufacturer_data', {}
|
||||||
|
).items()
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_service_data(device: BLEDevice) -> Dict[str, str]:
|
||||||
|
return {
|
||||||
|
service_uuid: ':'.join([f'{x:02x}' for x in value])
|
||||||
|
for service_uuid, value in device.details.get('props', {})
|
||||||
|
.get('ServiceData', {})
|
||||||
|
.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def _on_device_event(self, device: BLEDevice, _: AdvertisementData):
|
||||||
event_types: List[Type[BluetoothDeviceEvent]] = []
|
event_types: List[Type[BluetoothDeviceEvent]] = []
|
||||||
existing_device = self._devices.get(device.address)
|
existing_device = self._devices.get(device.address)
|
||||||
|
|
||||||
|
@ -178,6 +247,15 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
if new_props.get('Trusted')
|
if new_props.get('Trusted')
|
||||||
else BluetoothDeviceUntrustedEvent
|
else BluetoothDeviceUntrustedEvent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
time() - self._device_last_updated_at.get(device.address, 0)
|
||||||
|
) >= self._rssi_update_interval and (
|
||||||
|
existing_device.rssi != device.rssi
|
||||||
|
or old_props.get('TxPower') != new_props.get('TxPower')
|
||||||
|
):
|
||||||
|
event_types.append(BluetoothDeviceSignalUpdateEvent)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
event_types.append(BluetoothDeviceFoundEvent)
|
event_types.append(BluetoothDeviceFoundEvent)
|
||||||
|
|
||||||
|
@ -190,6 +268,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
for event_type in event_types:
|
for event_type in event_types:
|
||||||
self._post_event(event_type, device)
|
self._post_event(event_type, device)
|
||||||
self.publish_entities([device])
|
self.publish_entities([device])
|
||||||
|
self._device_last_updated_at[device.address] = time()
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def _connect(
|
async def _connect(
|
||||||
|
@ -234,7 +313,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
async def _scan(
|
async def _scan(
|
||||||
self,
|
self,
|
||||||
duration: Optional[float] = None,
|
duration: Optional[float] = None,
|
||||||
characteristics: Optional[Collection[UUIDType]] = None,
|
uuids: Optional[Collection[UUIDType]] = None,
|
||||||
publish_entities: bool = False,
|
publish_entities: bool = False,
|
||||||
) -> Collection[Entity]:
|
) -> Collection[Entity]:
|
||||||
with self._scan_lock:
|
with self._scan_lock:
|
||||||
|
@ -242,14 +321,10 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
devices = await BleakScanner.discover(
|
devices = await BleakScanner.discover(
|
||||||
adapter=self._interface,
|
adapter=self._interface,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
service_uuids=list(
|
service_uuids=list(map(str, uuids or self._uuids or [])),
|
||||||
map(str, characteristics or self._characteristics or [])
|
|
||||||
),
|
|
||||||
detection_callback=self._on_device_event,
|
detection_callback=self._on_device_event,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO Infer type from device.metadata['manufacturer_data']
|
|
||||||
|
|
||||||
self._devices.update({dev.address: dev for dev in devices})
|
self._devices.update({dev.address: dev for dev in devices})
|
||||||
return (
|
return (
|
||||||
self.publish_entities(devices)
|
self.publish_entities(devices)
|
||||||
|
@ -307,7 +382,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
def scan(
|
def scan(
|
||||||
self,
|
self,
|
||||||
duration: Optional[float] = None,
|
duration: Optional[float] = None,
|
||||||
characteristics: Optional[Collection[UUIDType]] = None,
|
uuids: Optional[Collection[UUIDType]] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Scan for Bluetooth devices nearby and return the results as a list of
|
Scan for Bluetooth devices nearby and return the results as a list of
|
||||||
|
@ -315,11 +390,11 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
|
|
||||||
:param duration: Scan duration in seconds (default: same as the plugin's
|
:param duration: Scan duration in seconds (default: same as the plugin's
|
||||||
`poll_interval` configuration parameter)
|
`poll_interval` configuration parameter)
|
||||||
:param characteristics: List of characteristic UUIDs to discover. Default: all.
|
:param uuids: List of characteristic UUIDs to discover. Default: all.
|
||||||
"""
|
"""
|
||||||
loop = get_or_create_event_loop()
|
loop = get_or_create_event_loop()
|
||||||
return loop.run_until_complete(
|
return loop.run_until_complete(
|
||||||
self._scan(duration, characteristics, publish_entities=True)
|
self._scan(duration, uuids, publish_entities=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -388,7 +463,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
return [
|
return [
|
||||||
BluetoothDevice(
|
BluetoothDevice(
|
||||||
id=dev.address,
|
id=dev.address,
|
||||||
name=self._get_device_name(dev),
|
**self._parse_device_args(dev),
|
||||||
)
|
)
|
||||||
for dev in entities
|
for dev in entities
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
manifest:
|
manifest:
|
||||||
events: {}
|
events:
|
||||||
|
platypush.message.event.bluetooth.BluetoothDeviceBlockedEvent:
|
||||||
|
platypush.message.event.bluetooth.BluetoothDeviceConnectedEvent:
|
||||||
|
platypush.message.event.bluetooth.BluetoothDeviceDisconnectedEvent:
|
||||||
|
platypush.message.event.bluetooth.BluetoothDeviceFoundEvent:
|
||||||
|
platypush.message.event.bluetooth.BluetoothDeviceLostEvent:
|
||||||
|
platypush.message.event.bluetooth.BluetoothDevicePairedEvent:
|
||||||
|
platypush.message.event.bluetooth.BluetoothDeviceTrustedEvent:
|
||||||
|
platypush.message.event.bluetooth.BluetoothDeviceUnblockedEvent:
|
||||||
|
platypush.message.event.bluetooth.BluetoothDeviceUnpairedEvent:
|
||||||
|
platypush.message.event.bluetooth.BluetoothDeviceUntrustedEvent:
|
||||||
|
platypush.message.event.bluetooth.BluetoothScanPausedEvent:
|
||||||
|
platypush.message.event.bluetooth.BluetoothScanResumedEvent:
|
||||||
install:
|
install:
|
||||||
pip:
|
pip:
|
||||||
- bleak
|
- bleak
|
||||||
|
- bluetooth-numbers
|
||||||
package: platypush.plugins.bluetooth.ble
|
package: platypush.plugins.bluetooth.ble
|
||||||
type: plugin
|
type: plugin
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -173,9 +173,11 @@ setup(
|
||||||
],
|
],
|
||||||
# Support for Alexa/Echo plugin
|
# Support for Alexa/Echo plugin
|
||||||
'alexa': ['avs @ https://github.com/BlackLight/avs/tarball/master'],
|
'alexa': ['avs @ https://github.com/BlackLight/avs/tarball/master'],
|
||||||
# Support for bluetooth devices
|
# Support for Bluetooth devices
|
||||||
'bluetooth': [
|
'bluetooth': [
|
||||||
'bleak',
|
'bleak',
|
||||||
|
'bluetooth-numbers',
|
||||||
|
'pybluez',
|
||||||
'pyobex @ https://github.com/BlackLight/PyOBEX/tarball/master',
|
'pyobex @ https://github.com/BlackLight/PyOBEX/tarball/master',
|
||||||
],
|
],
|
||||||
# Support for TP-Link devices
|
# Support for TP-Link devices
|
||||||
|
|
Loading…
Reference in a new issue