From 59e3f8120206ebfcefd89002cd866b4e5671c556 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 8 Mar 2020 23:37:57 +0100 Subject: [PATCH] Added bluetooth scanner backends [closes #112] --- docs/source/backends.rst | 2 + .../backend/bluetooth.scanner.ble.rst | 5 ++ .../platypush/backend/bluetooth.scanner.rst | 5 ++ .../backend/bluetooth/scanner/__init__.py | 47 +++++++++++++++++++ platypush/backend/bluetooth/scanner/ble.py | 33 +++++++++++++ platypush/backend/sensor/__init__.py | 12 +++-- platypush/message/event/bluetooth.py | 18 +++++++ platypush/message/event/sensor/__init__.py | 10 ++-- platypush/plugins/bluetooth/__init__.py | 27 +++++++++-- 9 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 docs/source/platypush/backend/bluetooth.scanner.ble.rst create mode 100644 docs/source/platypush/backend/bluetooth.scanner.rst create mode 100644 platypush/backend/bluetooth/scanner/__init__.py create mode 100644 platypush/backend/bluetooth/scanner/ble.py diff --git a/docs/source/backends.rst b/docs/source/backends.rst index 1d83b38d..eb26189d 100644 --- a/docs/source/backends.rst +++ b/docs/source/backends.rst @@ -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 diff --git a/docs/source/platypush/backend/bluetooth.scanner.ble.rst b/docs/source/platypush/backend/bluetooth.scanner.ble.rst new file mode 100644 index 00000000..f75c6413 --- /dev/null +++ b/docs/source/platypush/backend/bluetooth.scanner.ble.rst @@ -0,0 +1,5 @@ +``platypush.backend.bluetooth.scanner.ble`` +=========================================== + +.. automodule:: platypush.backend.bluetooth.scanner.ble + :members: diff --git a/docs/source/platypush/backend/bluetooth.scanner.rst b/docs/source/platypush/backend/bluetooth.scanner.rst new file mode 100644 index 00000000..ef00ee42 --- /dev/null +++ b/docs/source/platypush/backend/bluetooth.scanner.rst @@ -0,0 +1,5 @@ +``platypush.backend.bluetooth.scanner`` +======================================= + +.. automodule:: platypush.backend.bluetooth.scanner + :members: diff --git a/platypush/backend/bluetooth/scanner/__init__.py b/platypush/backend/bluetooth/scanner/__init__.py new file mode 100644 index 00000000..77821abb --- /dev/null +++ b/platypush/backend/bluetooth/scanner/__init__.py @@ -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: diff --git a/platypush/backend/bluetooth/scanner/ble.py b/platypush/backend/bluetooth/scanner/ble.py new file mode 100644 index 00000000..3a66e41f --- /dev/null +++ b/platypush/backend/bluetooth/scanner/ble.py @@ -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: diff --git a/platypush/backend/sensor/__init__.py b/platypush/backend/sensor/__init__.py index 0a2bf887..966176bd 100644 --- a/platypush/backend/sensor/__init__.py +++ b/platypush/backend/sensor/__init__.py @@ -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 = {} diff --git a/platypush/message/event/bluetooth.py b/platypush/message/event/bluetooth.py index dd594078..b70f6848 100644 --- a/platypush/message/event/bluetooth.py +++ b/platypush/message/event/bluetooth.py @@ -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 diff --git a/platypush/message/event/sensor/__init__.py b/platypush/message/event/sensor/__init__.py index c145e075..b10672ee 100644 --- a/platypush/message/event/sensor/__init__.py +++ b/platypush/message/event/sensor/__init__.py @@ -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) diff --git a/platypush/plugins/bluetooth/__init__.py b/platypush/plugins/bluetooth/__init__.py index 16495d4c..1d4e3ba2 100644 --- a/platypush/plugins/bluetooth/__init__.py +++ b/platypush/plugins/bluetooth/__init__.py @@ -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: