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.rst
platypush/backend/bluetooth.fileserver.rst platypush/backend/bluetooth.fileserver.rst
platypush/backend/bluetooth.pushserver.rst platypush/backend/bluetooth.pushserver.rst
platypush/backend/bluetooth.scanner.rst
platypush/backend/bluetooth.scanner.ble.rst
platypush/backend/button.flic.rst platypush/backend/button.flic.rst
platypush/backend/camera.pi.rst platypush/backend/camera.pi.rst
platypush/backend/chat.telegram.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): except (TypeError, ValueError):
pass 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 ret[k] = v
return ret return ret
@ -171,6 +173,10 @@ class SensorBackend(Backend):
if plugin and hasattr(plugin, 'close'): if plugin and hasattr(plugin, 'close'):
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): def run(self):
super().run() super().run()
self.logger.info('Initialized {} sensor backend'.format(self.__class__.__name__)) self.logger.info('Initialized {} sensor backend'.format(self.__class__.__name__))
@ -179,9 +185,7 @@ class SensorBackend(Backend):
try: try:
data = self.get_measurement() data = self.get_measurement()
new_data = self.get_new_data(data) new_data = self.get_new_data(data)
self.process_data(data, new_data)
if new_data:
self.bus.post(SensorDataChangeEvent(data=new_data))
data_below_threshold = {} data_below_threshold = {}
data_above_threshold = {} data_above_threshold = {}

View file

@ -1,3 +1,5 @@
from typing import Optional
from platypush.message.event import Event from platypush.message.event import Event
@ -5,6 +7,22 @@ class BluetoothEvent(Event):
pass 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): class BluetoothDeviceConnectedEvent(Event):
""" """
Event triggered on bluetooth device connection Event triggered on bluetooth device connection

View file

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

View file

@ -3,12 +3,16 @@ import os
import re import re
import select 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, \ from platypush.message.response.bluetooth import BluetoothScanResponse, \
BluetoothLookupNameResponse, BluetoothLookupServiceResponse, BluetoothResponse BluetoothLookupNameResponse, BluetoothLookupServiceResponse, BluetoothResponse
class BluetoothPlugin(Plugin): class BluetoothPlugin(SensorPlugin):
""" """
Bluetooth plugin Bluetooth plugin
@ -67,7 +71,7 @@ class BluetoothPlugin(Plugin):
return self.lookup_address(device).output['addr'] return self.lookup_address(device).output['addr']
@action @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 Scan for nearby bluetooth devices
@ -79,7 +83,7 @@ class BluetoothPlugin(Plugin):
if device_id is None: if device_id is None:
device_id = self.device_id 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)) device_id, duration))
devices = discover_devices(duration=duration, lookup_names=True, lookup_class=True, device_id=device_id, 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')} self._devices_by_name = {dev['name']: dev for dev in self._devices if dev.get('name')}
return response 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 @action
def lookup_name(self, addr: str, timeout: int = 10) -> BluetoothLookupNameResponse: 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) sock = self._get_sock(device=device, port=port, service_uuid=service_uuid, service_name=service_name)
if not sock: 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 return
try: try: