Added bluetooth scanner backends [closes #112]

This commit is contained in:
Fabio Manganiello 2020-03-08 23:37:57 +01:00
parent 535b2ec083
commit 59e3f81202
9 changed files with 145 additions and 14 deletions

View file

@ -14,6 +14,8 @@ Backends
platypush/backend/bluetooth.rst
platypush/backend/bluetooth.fileserver.rst
platypush/backend/bluetooth.pushserver.rst
platypush/backend/bluetooth.scanner.rst
platypush/backend/bluetooth.scanner.ble.rst
platypush/backend/button.flic.rst
platypush/backend/camera.pi.rst
platypush/backend/chat.telegram.rst

View file

@ -0,0 +1,5 @@
``platypush.backend.bluetooth.scanner.ble``
===========================================
.. automodule:: platypush.backend.bluetooth.scanner.ble
:members:

View file

@ -0,0 +1,5 @@
``platypush.backend.bluetooth.scanner``
=======================================
.. automodule:: platypush.backend.bluetooth.scanner
:members:

View file

@ -0,0 +1,47 @@
from typing import Dict, Optional
from platypush.backend.sensor import SensorBackend
from platypush.message.event.bluetooth import BluetoothDeviceFoundEvent, BluetoothDeviceLostEvent
class BluetoothScannerBackend(SensorBackend):
"""
This backend periodically scans for available bluetooth devices and returns events when a devices enter or exits
the range.
Triggers:
* :class:`platypush.message.event.bluetooth.BluetoothDeviceFoundEvent` when a new bluetooth device is found.
* :class:`platypush.message.event.bluetooth.BluetoothDeviceLostEvent` when a bluetooth device is lost.
Requires:
* The :class:`platypush.plugins.bluetooth.BluetoothPlugin` plugin working.
"""
def __init__(self, device_id: Optional[int] = None, scan_interval: int = 10, **kwargs):
"""
:param device_id: Bluetooth adapter ID to use (default configured on the ``bluetooth`` plugin if None).
:param scan_interval: How long the scan should run (default: 10 seconds).
"""
super().__init__(plugin='bluetooth', plugin_args={
'device_id': device_id,
'duration': scan_interval,
}, **kwargs)
self._last_seen_devices = {}
def process_data(self, data: Dict[str, dict], new_data: Dict[str, dict]):
for addr, dev in data.items():
if addr not in self._last_seen_devices:
self.bus.post(BluetoothDeviceFoundEvent(address=dev.pop('addr'), **dev))
self._last_seen_devices[addr] = {'addr': addr, **dev}
for addr, dev in self._last_seen_devices.copy().items():
if addr not in data:
self.bus.post(BluetoothDeviceLostEvent(address=dev.pop('addr'), **dev))
del self._last_seen_devices[addr]
# vim:sw=4:ts=4:et:

View file

@ -0,0 +1,33 @@
from typing import Optional
from platypush.backend.bluetooth.scanner import BluetoothScannerBackend
class BluetoothBleScannerBackend(BluetoothScannerBackend):
"""
This backend periodically scans for available bluetooth low-energy devices and returns events when a devices enter
or exits the range.
Triggers:
* :class:`platypush.message.event.bluetooth.BluetoothDeviceFoundEvent` when a new bluetooth device is found.
* :class:`platypush.message.event.bluetooth.BluetoothDeviceLostEvent` when a bluetooth device is lost.
Requires:
* The :class:`platypush.plugins.bluetooth.BluetoothBlePlugin` plugin working.
"""
def __init__(self, interface: Optional[int] = None, scan_interval: int = 10, **kwargs):
"""
:param interface: Bluetooth adapter name to use (default configured on the ``bluetooth.ble`` plugin if None).
:param scan_interval: How long the scan should run (default: 10 seconds).
"""
super().__init__(plugin='bluetooth.ble', plugin_args={
'interface': interface,
'duration': scan_interval,
}, **kwargs)
# vim:sw=4:ts=4:et:

View file

@ -158,7 +158,9 @@ class SensorBackend(Backend):
except (TypeError, ValueError):
pass
if tolerance is None or is_nan or abs(v - old_v) >= tolerance:
if tolerance is None or abs(v - old_v) >= tolerance:
ret[k] = v
elif k not in self.data or self.data[k] != v:
ret[k] = v
return ret
@ -171,6 +173,10 @@ class SensorBackend(Backend):
if plugin and hasattr(plugin, 'close'):
plugin.close()
def process_data(self, data, new_data):
if new_data:
self.bus.post(SensorDataChangeEvent(data=data, source=self.plugin or self.__class__.__name__))
def run(self):
super().run()
self.logger.info('Initialized {} sensor backend'.format(self.__class__.__name__))
@ -179,9 +185,7 @@ class SensorBackend(Backend):
try:
data = self.get_measurement()
new_data = self.get_new_data(data)
if new_data:
self.bus.post(SensorDataChangeEvent(data=new_data))
self.process_data(data, new_data)
data_below_threshold = {}
data_above_threshold = {}

View file

@ -1,3 +1,5 @@
from typing import Optional
from platypush.message.event import Event
@ -5,6 +7,22 @@ class BluetoothEvent(Event):
pass
class BluetoothDeviceFoundEvent(Event):
"""
Event triggered when a bluetooth device is found during a scan.
"""
def __init__(self, address: str, name: Optional[str] = None, *args, **kwargs):
super().__init__(*args, address=address, name=name, **kwargs)
class BluetoothDeviceLostEvent(Event):
"""
Event triggered when a bluetooth device previously scanned is lost.
"""
def __init__(self, address: str, name: Optional[str] = None, *args, **kwargs):
super().__init__(*args, address=address, name=name, **kwargs)
class BluetoothDeviceConnectedEvent(Event):
"""
Event triggered on bluetooth device connection

View file

@ -1,3 +1,5 @@
from typing import Optional
from platypush.message.event import Event
@ -6,14 +8,14 @@ class SensorDataChangeEvent(Event):
Event triggered when a sensor has new data
"""
def __init__(self, data, *args, **kwargs):
def __init__(self, data, source: Optional[str] = None, *args, **kwargs):
"""
:param data: Sensor data
:type data: object
"""
super().__init__(data=data, *args, **kwargs)
super().__init__(data=data, source=source, *args, **kwargs)
self.data = data
self.source = source
class SensorDataAboveThresholdEvent(Event):
@ -24,7 +26,6 @@ class SensorDataAboveThresholdEvent(Event):
def __init__(self, data, *args, **kwargs):
"""
:param data: Sensor data
:type data: object
"""
super().__init__(data=data, *args, **kwargs)
@ -39,7 +40,6 @@ class SensorDataBelowThresholdEvent(Event):
def __init__(self, data, *args, **kwargs):
"""
:param data: Sensor data
:type data: object
"""
super().__init__(data=data, *args, **kwargs)

View file

@ -3,12 +3,16 @@ import os
import re
import select
from platypush.plugins import Plugin, action
from typing import Dict, Optional
from platypush.plugins.sensor import SensorPlugin
from platypush.plugins import action
from platypush.message.response.bluetooth import BluetoothScanResponse, \
BluetoothLookupNameResponse, BluetoothLookupServiceResponse, BluetoothResponse
class BluetoothPlugin(Plugin):
class BluetoothPlugin(SensorPlugin):
"""
Bluetooth plugin
@ -67,7 +71,7 @@ class BluetoothPlugin(Plugin):
return self.lookup_address(device).output['addr']
@action
def scan(self, device_id: int = None, duration: int = 10) -> BluetoothScanResponse:
def scan(self, device_id: Optional[int] = None, duration: int = 10) -> BluetoothScanResponse:
"""
Scan for nearby bluetooth devices
@ -79,7 +83,7 @@ class BluetoothPlugin(Plugin):
if device_id is None:
device_id = self.device_id
self.logger.info('Discovering devices on adapter {}, duration: {} seconds'.format(
self.logger.debug('Discovering devices on adapter {}, duration: {} seconds'.format(
device_id, duration))
devices = discover_devices(duration=duration, lookup_names=True, lookup_class=True, device_id=device_id,
@ -91,6 +95,19 @@ class BluetoothPlugin(Plugin):
self._devices_by_name = {dev['name']: dev for dev in self._devices if dev.get('name')}
return response
@action
def get_measurement(self, device_id: Optional[int] = None, duration: Optional[int] = 10, *args, **kwargs) \
-> Dict[str, dict]:
"""
Wrapper for ``scan`` that returns bluetooth devices in a format usable by sensor backends.
:param device_id: Bluetooth adapter ID to use (default configured if None)
:param duration: Scan duration in seconds
:return: Device address -> info map.
"""
devices = self.scan(device_id=device_id, duration=duration).output
return {device['addr']: device for device in devices}
@action
def lookup_name(self, addr: str, timeout: int = 10) -> BluetoothLookupNameResponse:
"""
@ -280,7 +297,7 @@ class BluetoothPlugin(Plugin):
sock = self._get_sock(device=device, port=port, service_uuid=service_uuid, service_name=service_name)
if not sock:
self.logger.info('Close on device {}({}) that is not connected'.format(device, port))
self.logger.debug('Close on device {}({}) that is not connected'.format(device, port))
return
try: