Use the TheengsDecoder to parse Bluetooth packets and map services to native entities.
This commit is contained in:
parent
73bf2446bd
commit
aa0b909fff
4 changed files with 334 additions and 79 deletions
|
@ -298,6 +298,7 @@ autodoc_mock_imports = [
|
||||||
'async_lru',
|
'async_lru',
|
||||||
'bleak',
|
'bleak',
|
||||||
'bluetooth_numbers',
|
'bluetooth_numbers',
|
||||||
|
'TheengsGateway',
|
||||||
]
|
]
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
|
|
|
@ -19,8 +19,6 @@ 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.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
|
||||||
|
@ -44,6 +42,8 @@ from platypush.message.event.bluetooth import (
|
||||||
)
|
)
|
||||||
from platypush.plugins import AsyncRunnablePlugin, action
|
from platypush.plugins import AsyncRunnablePlugin, action
|
||||||
|
|
||||||
|
from ._mappers import device_to_entity, parse_device_args
|
||||||
|
|
||||||
UUIDType = Union[str, UUID]
|
UUIDType = Union[str, UUID]
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
|
|
||||||
* **bleak** (``pip install bleak``)
|
* **bleak** (``pip install bleak``)
|
||||||
* **bluetooth-numbers** (``pip install bluetooth-numbers``)
|
* **bluetooth-numbers** (``pip install bluetooth-numbers``)
|
||||||
|
* **TheengsGateway** (``pip install git+https://github.com/BlackLight/TheengsGateway``)
|
||||||
|
|
||||||
Triggers:
|
Triggers:
|
||||||
|
|
||||||
|
@ -129,6 +130,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
self._connections: Dict[str, BleakClient] = {}
|
self._connections: Dict[str, BleakClient] = {}
|
||||||
self._connection_locks: Dict[str, Lock] = {}
|
self._connection_locks: Dict[str, Lock] = {}
|
||||||
self._devices: Dict[str, BLEDevice] = {}
|
self._devices: Dict[str, BLEDevice] = {}
|
||||||
|
self._entities: Dict[str, BluetoothDevice] = {}
|
||||||
self._device_last_updated_at: Dict[str, float] = {}
|
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 = {
|
||||||
|
@ -156,104 +158,68 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
assert dev is not None, f'Unknown device: "{device}"'
|
assert dev is not None, f'Unknown device: "{device}"'
|
||||||
return dev
|
return dev
|
||||||
|
|
||||||
def _get_device_name(self, device: BLEDevice) -> str:
|
|
||||||
return (
|
|
||||||
self._device_name_by_addr.get(device.address)
|
|
||||||
or device.name
|
|
||||||
or device.address
|
|
||||||
)
|
|
||||||
|
|
||||||
def _post_event(
|
def _post_event(
|
||||||
self, event_type: Type[BluetoothDeviceEvent], device: BLEDevice, **kwargs
|
self, event_type: Type[BluetoothDeviceEvent], device: BLEDevice, **kwargs
|
||||||
):
|
):
|
||||||
get_bus().post(
|
get_bus().post(
|
||||||
event_type(
|
event_type(address=device.address, **parse_device_args(device), **kwargs)
|
||||||
address=device.address, **self._parse_device_args(device), **kwargs
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _parse_device_args(self, device: BLEDevice) -> Dict[str, Any]:
|
def _on_device_event(self, device: BLEDevice, data: AdvertisementData):
|
||||||
props = device.details.get('props', {})
|
"""
|
||||||
return {
|
Device advertisement packet callback handler.
|
||||||
'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
|
1. It generates the relevant
|
||||||
def _parse_manufacturer_data(device: BLEDevice) -> Dict[int, str]:
|
:class:`platypush.message.event.bluetooth.BluetoothDeviceEvent` if the
|
||||||
return {
|
state of the device has changed.
|
||||||
manufacturer_id: ':'.join([f'{x:02x}' for x in value])
|
|
||||||
for manufacturer_id, value in device.metadata.get(
|
|
||||||
'manufacturer_data', {}
|
|
||||||
).items()
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
2. It builds the relevant
|
||||||
def _parse_service_data(device: BLEDevice) -> Dict[str, str]:
|
:class:`platypush.entity.bluetooth.BluetoothDevice` entity object
|
||||||
return {
|
populated with children entities that contain the supported
|
||||||
service_uuid: ':'.join([f'{x:02x}' for x in value])
|
properties.
|
||||||
for service_uuid, value in device.details.get('props', {})
|
|
||||||
.get('ServiceData', {})
|
:param device: The Bluetooth device.
|
||||||
.items()
|
:param data: The advertisement data.
|
||||||
}
|
"""
|
||||||
|
|
||||||
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_entity = self._entities.get(device.address)
|
||||||
|
entity = device_to_entity(device, data)
|
||||||
|
|
||||||
if existing_device:
|
if existing_entity:
|
||||||
old_props = existing_device.details.get('props', {})
|
if existing_entity.paired != entity.paired:
|
||||||
new_props = device.details.get('props', {})
|
|
||||||
|
|
||||||
if old_props.get('Paired') != new_props.get('Paired'):
|
|
||||||
event_types.append(
|
event_types.append(
|
||||||
BluetoothDevicePairedEvent
|
BluetoothDevicePairedEvent
|
||||||
if new_props.get('Paired')
|
if entity.paired
|
||||||
else BluetoothDeviceUnpairedEvent
|
else BluetoothDeviceUnpairedEvent
|
||||||
)
|
)
|
||||||
|
|
||||||
if old_props.get('Connected') != new_props.get('Connected'):
|
if existing_entity.connected != entity.connected:
|
||||||
event_types.append(
|
event_types.append(
|
||||||
BluetoothDeviceConnectedEvent
|
BluetoothDeviceConnectedEvent
|
||||||
if new_props.get('Connected')
|
if entity.connected
|
||||||
else BluetoothDeviceDisconnectedEvent
|
else BluetoothDeviceDisconnectedEvent
|
||||||
)
|
)
|
||||||
|
|
||||||
if old_props.get('Blocked') != new_props.get('Blocked'):
|
if existing_entity.blocked != entity.blocked:
|
||||||
event_types.append(
|
event_types.append(
|
||||||
BluetoothDeviceBlockedEvent
|
BluetoothDeviceBlockedEvent
|
||||||
if new_props.get('Blocked')
|
if entity.blocked
|
||||||
else BluetoothDeviceUnblockedEvent
|
else BluetoothDeviceUnblockedEvent
|
||||||
)
|
)
|
||||||
|
|
||||||
if old_props.get('Trusted') != new_props.get('Trusted'):
|
if existing_entity.trusted != entity.trusted:
|
||||||
event_types.append(
|
event_types.append(
|
||||||
BluetoothDeviceTrustedEvent
|
BluetoothDeviceTrustedEvent
|
||||||
if new_props.get('Trusted')
|
if entity.trusted
|
||||||
else BluetoothDeviceUntrustedEvent
|
else BluetoothDeviceUntrustedEvent
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
time() - self._device_last_updated_at.get(device.address, 0)
|
time() - self._device_last_updated_at.get(device.address, 0)
|
||||||
) >= self._rssi_update_interval and (
|
) >= self._rssi_update_interval and (
|
||||||
existing_device.rssi != device.rssi
|
existing_entity.rssi != device.rssi
|
||||||
or old_props.get('TxPower') != new_props.get('TxPower')
|
or existing_entity.tx_power != entity.tx_power
|
||||||
):
|
):
|
||||||
event_types.append(BluetoothDeviceSignalUpdateEvent)
|
event_types.append(BluetoothDeviceSignalUpdateEvent)
|
||||||
else:
|
else:
|
||||||
|
@ -267,9 +233,32 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
if event_types:
|
if event_types:
|
||||||
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._device_last_updated_at[device.address] = time()
|
self._device_last_updated_at[device.address] = time()
|
||||||
|
|
||||||
|
for child in entity.children:
|
||||||
|
child.parent = entity
|
||||||
|
|
||||||
|
self.publish_entities([entity])
|
||||||
|
|
||||||
|
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'}
|
||||||
|
)
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def _connect(
|
async def _connect(
|
||||||
self,
|
self,
|
||||||
|
@ -318,7 +307,6 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
self,
|
self,
|
||||||
duration: Optional[float] = None,
|
duration: Optional[float] = None,
|
||||||
uuids: Optional[Collection[UUIDType]] = None,
|
uuids: Optional[Collection[UUIDType]] = None,
|
||||||
publish_entities: bool = False,
|
|
||||||
) -> Collection[Entity]:
|
) -> Collection[Entity]:
|
||||||
with self._scan_lock:
|
with self._scan_lock:
|
||||||
timeout = duration or self.poll_interval or 5
|
timeout = duration or self.poll_interval or 5
|
||||||
|
@ -330,11 +318,14 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
)
|
)
|
||||||
|
|
||||||
self._devices.update({dev.address: dev for dev in devices})
|
self._devices.update({dev.address: dev for dev in devices})
|
||||||
return (
|
addresses = {dev.address.lower() for dev in devices}
|
||||||
self.publish_entities(devices)
|
return [
|
||||||
if publish_entities
|
dev
|
||||||
else self.transform_entities(devices)
|
for addr, dev in self._entities.items()
|
||||||
)
|
if isinstance(dev, BluetoothDevice)
|
||||||
|
and addr.lower() in addresses
|
||||||
|
and dev.reachable
|
||||||
|
]
|
||||||
|
|
||||||
async def _scan_state_set(self, state: bool, duration: Optional[float] = None):
|
async def _scan_state_set(self, state: bool, duration: Optional[float] = None):
|
||||||
def timer_callback():
|
def timer_callback():
|
||||||
|
@ -397,9 +388,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
:param uuids: 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, uuids))
|
||||||
self._scan(duration, uuids, publish_entities=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def read(
|
def read(
|
||||||
|
@ -460,15 +449,25 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
"""
|
"""
|
||||||
return self.scan().output
|
return self.scan().output
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def transform_entities(
|
def transform_entities(
|
||||||
self, entities: Collection[BLEDevice]
|
self, entities: Collection[Union[BLEDevice, BluetoothDevice]]
|
||||||
) -> Collection[BluetoothDevice]:
|
) -> Collection[BluetoothDevice]:
|
||||||
return [
|
return [
|
||||||
BluetoothDevice(
|
BluetoothDevice(
|
||||||
id=dev.address,
|
id=dev.address,
|
||||||
**self._parse_device_args(dev),
|
**parse_device_args(dev),
|
||||||
)
|
)
|
||||||
|
if isinstance(dev, BLEDevice)
|
||||||
|
else dev
|
||||||
for dev in entities
|
for dev in entities
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -480,7 +479,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
await self._scan_enabled.wait()
|
await self._scan_enabled.wait()
|
||||||
entities = await self._scan()
|
entities = await self._scan()
|
||||||
|
|
||||||
new_device_addresses = {e.id for e in entities}
|
new_device_addresses = {e.external_id for e in entities}
|
||||||
missing_device_addresses = device_addresses - new_device_addresses
|
missing_device_addresses = device_addresses - new_device_addresses
|
||||||
missing_devices = [
|
missing_devices = [
|
||||||
dev
|
dev
|
||||||
|
@ -491,6 +490,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
|
||||||
for dev in missing_devices:
|
for dev in missing_devices:
|
||||||
self._post_event(BluetoothDeviceLostEvent, dev)
|
self._post_event(BluetoothDeviceLostEvent, dev)
|
||||||
self._devices.pop(dev.address, None)
|
self._devices.pop(dev.address, None)
|
||||||
|
self._entities.pop(dev.address, None)
|
||||||
|
|
||||||
device_addresses = new_device_addresses
|
device_addresses = new_device_addresses
|
||||||
|
|
||||||
|
|
253
platypush/plugins/bluetooth/ble/_mappers.py
Normal file
253
platypush/plugins/bluetooth/ble/_mappers.py
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
import json
|
||||||
|
import struct
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Callable, Dict, Optional
|
||||||
|
|
||||||
|
from bleak.backends.device import BLEDevice
|
||||||
|
from bleak.backends.scanner import AdvertisementData
|
||||||
|
from bleak.uuids import uuidstr_to_str
|
||||||
|
from bluetooth_numbers import company
|
||||||
|
|
||||||
|
# pylint: disable=no-name-in-module
|
||||||
|
from TheengsGateway._decoder import decodeBLE, getAttribute, getProperties
|
||||||
|
|
||||||
|
from platypush.entities import Entity
|
||||||
|
from platypush.entities.batteries import Battery
|
||||||
|
from platypush.entities.bluetooth import BluetoothDevice
|
||||||
|
from platypush.entities.electricity import (
|
||||||
|
CurrentSensor,
|
||||||
|
EnergySensor,
|
||||||
|
PowerSensor,
|
||||||
|
VoltageSensor,
|
||||||
|
)
|
||||||
|
from platypush.entities.humidity import HumiditySensor
|
||||||
|
from platypush.entities.illuminance import IlluminanceSensor
|
||||||
|
from platypush.entities.motion import MotionSensor
|
||||||
|
from platypush.entities.sensors import BinarySensor, NumericSensor, RawSensor
|
||||||
|
from platypush.entities.temperature import TemperatureSensor
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TheengsEntity:
|
||||||
|
"""
|
||||||
|
Utility class to store the data parsed from the Theengs library.
|
||||||
|
"""
|
||||||
|
|
||||||
|
data: dict = field(default_factory=dict)
|
||||||
|
properties: dict = field(default_factory=dict)
|
||||||
|
brand: Optional[str] = None
|
||||||
|
model: Optional[str] = None
|
||||||
|
model_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# Maps property names to transformer methods (first mapper choice).
|
||||||
|
_property_to_entity: Dict[str, Callable[[Any, Dict[str, Any]], Entity]] = {
|
||||||
|
'battery': lambda value, conf: Battery(
|
||||||
|
value=value,
|
||||||
|
unit=conf.get('unit', '%'),
|
||||||
|
min=conf.get('min', 0),
|
||||||
|
max=conf.get('min', 100),
|
||||||
|
),
|
||||||
|
'current': lambda value, conf: CurrentSensor(
|
||||||
|
value=value,
|
||||||
|
unit=conf.get('unit', 'A'),
|
||||||
|
),
|
||||||
|
'energy': lambda value, conf: EnergySensor(
|
||||||
|
value=value,
|
||||||
|
unit=conf.get('unit', 'kWh'),
|
||||||
|
),
|
||||||
|
'humidity': lambda value, conf: HumiditySensor(
|
||||||
|
value=value,
|
||||||
|
unit=conf.get('unit', '%'),
|
||||||
|
min=conf.get('min', 0),
|
||||||
|
max=conf.get('min', 100),
|
||||||
|
),
|
||||||
|
'light level': lambda value, _: IlluminanceSensor(value=value),
|
||||||
|
'power': lambda value, conf: PowerSensor(
|
||||||
|
value=value,
|
||||||
|
unit=conf.get('unit', 'W'),
|
||||||
|
),
|
||||||
|
'motion': lambda value, _: MotionSensor(value=value),
|
||||||
|
'temperature': lambda value, conf: TemperatureSensor(
|
||||||
|
value=value,
|
||||||
|
unit=conf.get('unit', 'C'),
|
||||||
|
),
|
||||||
|
'voltage': lambda value, conf: VoltageSensor(
|
||||||
|
value=value,
|
||||||
|
unit=conf.get('unit', 'V'),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Maps reported units to transformer methods (second mapper choice).
|
||||||
|
_unit_to_entity: Dict[str, Callable[[Any, Dict[str, Any]], Entity]] = {
|
||||||
|
'status': lambda value, _: BinarySensor(value=value),
|
||||||
|
'int': lambda value, _: NumericSensor(value=value),
|
||||||
|
'%': lambda value, conf: NumericSensor(
|
||||||
|
value=value,
|
||||||
|
unit='%',
|
||||||
|
min=conf.get('min', 0),
|
||||||
|
max=conf.get('min', 100),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Maps value types to transformer methods (third mapper choice).
|
||||||
|
_value_type_to_entity: Dict[type, Callable[[Any, Dict[str, Any]], Entity]] = {
|
||||||
|
bool: lambda value, _: BinarySensor(value=value),
|
||||||
|
int: lambda value, _: NumericSensor(value=value),
|
||||||
|
float: lambda value, _: NumericSensor(value=value),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def device_to_entity(device: BLEDevice, data: AdvertisementData) -> BluetoothDevice:
|
||||||
|
"""
|
||||||
|
Convert the data received from a Bluetooth advertisement packet into a
|
||||||
|
compatible Platypush :class:`platypush.entity.bluetooth.BluetoothDevice`
|
||||||
|
entity, with the discovered services and characteristics exposed as children
|
||||||
|
entities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
theengs_entity = _parse_advertisement_data(data)
|
||||||
|
parent_entity = BluetoothDevice(
|
||||||
|
id=device.address,
|
||||||
|
model=theengs_entity.model,
|
||||||
|
brand=theengs_entity.brand,
|
||||||
|
reachable=True,
|
||||||
|
**parse_device_args(device),
|
||||||
|
)
|
||||||
|
|
||||||
|
parsed_entities = {
|
||||||
|
# Check if we can infer an entity mapper from the property name.
|
||||||
|
conf.get('name', name): _property_to_entity.get(
|
||||||
|
conf.get('name'),
|
||||||
|
# If not, check if we can infer an entity mapper from the reported unit.
|
||||||
|
_unit_to_entity.get(
|
||||||
|
conf.get('unit'),
|
||||||
|
# If not, check if we can infer an entity mapper from the value type.
|
||||||
|
_value_type_to_entity.get(
|
||||||
|
type(theengs_entity.data.get(name)),
|
||||||
|
# If not, default to a raw sensor.
|
||||||
|
lambda value, _: RawSensor(value=value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)(theengs_entity.data.get(name), conf)
|
||||||
|
for name, conf in theengs_entity.properties.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
for prop, entity in parsed_entities.items():
|
||||||
|
entity.id = f'{parent_entity.id}:{prop}'
|
||||||
|
entity.name = prop
|
||||||
|
parent_entity.children.append(entity)
|
||||||
|
|
||||||
|
return parent_entity
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_advertisement_data(data: AdvertisementData) -> TheengsEntity:
|
||||||
|
"""
|
||||||
|
:param data: The data received from a Bluetooth advertisement packet.
|
||||||
|
:return: A :class:`platypush.entity.bluetooth.TheengsEntity` instance that
|
||||||
|
maps the parsed attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
entity_args, properties, brand, model, model_id = ({}, {}, None, None, None)
|
||||||
|
|
||||||
|
if data.service_data:
|
||||||
|
parsed_data = list(data.service_data.keys())[0]
|
||||||
|
# TheengsDecoder only accepts 16 bit uuid's, this converts the 128 bit uuid to 16 bit.
|
||||||
|
entity_args['servicedatauuid'] = parsed_data[4:8]
|
||||||
|
parsed_data = str(list(data.service_data.values())[0].hex())
|
||||||
|
entity_args['servicedata'] = parsed_data
|
||||||
|
|
||||||
|
if data.manufacturer_data:
|
||||||
|
parsed_data = str(
|
||||||
|
struct.pack('<H', list(data.manufacturer_data.keys())[0]).hex()
|
||||||
|
)
|
||||||
|
parsed_data += str(list(data.manufacturer_data.values())[0].hex())
|
||||||
|
entity_args['manufacturerdata'] = parsed_data
|
||||||
|
|
||||||
|
if data.local_name:
|
||||||
|
entity_args['name'] = data.local_name
|
||||||
|
|
||||||
|
if entity_args:
|
||||||
|
# print('==== DECODING: ====')
|
||||||
|
# print(entity_args)
|
||||||
|
encoded_ret = decodeBLE(json.dumps(entity_args))
|
||||||
|
# print('==== DECODED! ====')
|
||||||
|
|
||||||
|
if encoded_ret:
|
||||||
|
entity_args = json.loads(encoded_ret)
|
||||||
|
|
||||||
|
if entity_args.get('model_id'):
|
||||||
|
properties = json.loads(getProperties(entity_args['model_id'])).get(
|
||||||
|
'properties', {}
|
||||||
|
)
|
||||||
|
model = getAttribute(entity_args['model_id'], 'model')
|
||||||
|
|
||||||
|
model_id = entity_args.pop('model_id', None)
|
||||||
|
|
||||||
|
return TheengsEntity(
|
||||||
|
data=entity_args,
|
||||||
|
properties=properties,
|
||||||
|
brand=brand,
|
||||||
|
model=model,
|
||||||
|
model_id=model_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_device_args(device: BLEDevice) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
:param device: The device to parse.
|
||||||
|
:return: The mapped device arguments required to initialize a
|
||||||
|
:class:`platypush.entity.bluetooth.BluetoothDevice` or
|
||||||
|
:class:`platypush.message.event.bluetooth.BluetoothDeviceEvent`
|
||||||
|
object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
props = device.details.get('props', {})
|
||||||
|
return {
|
||||||
|
'name': device.name or device.address,
|
||||||
|
'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': _parse_manufacturer_data(device),
|
||||||
|
'service_data': _parse_service_data(device),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_manufacturer_data(device: BLEDevice) -> Dict[int, str]:
|
||||||
|
"""
|
||||||
|
:param device: The device to parse.
|
||||||
|
:return: The manufacturer data as a ``manufacturer_id -> hex_string``
|
||||||
|
mapping.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
manufacturer_id: ''.join([f'{x:02x}' for x in value])
|
||||||
|
for manufacturer_id, value in device.metadata.get(
|
||||||
|
'manufacturer_data', {}
|
||||||
|
).items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_service_data(device: BLEDevice) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
:param device: The device to parse.
|
||||||
|
:return: The service data as a ``service_uuid -> hex_string`` mapping.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
service_uuid: ''.join([f'{x:02x}' for x in value])
|
||||||
|
for service_uuid, value in device.details.get('props', {})
|
||||||
|
.get('ServiceData', {})
|
||||||
|
.items()
|
||||||
|
}
|
|
@ -16,5 +16,6 @@ manifest:
|
||||||
pip:
|
pip:
|
||||||
- bleak
|
- bleak
|
||||||
- bluetooth-numbers
|
- bluetooth-numbers
|
||||||
|
- git+https://github.com/BlackLight/TheengsGateway
|
||||||
package: platypush.plugins.bluetooth.ble
|
package: platypush.plugins.bluetooth.ble
|
||||||
type: plugin
|
type: plugin
|
||||||
|
|
Loading…
Reference in a new issue