From 757e0ff9bfed116071ef492c2939b85147273292 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 14 Aug 2019 19:49:19 +0200 Subject: [PATCH] Refactored SensorBackend and derived classes and added BME280 sensor plugin and backend --- platypush/backend/sensor/__init__.py | 118 ++++++++++++++++++---- platypush/backend/sensor/accelerometer.py | 8 +- platypush/backend/sensor/bme280.py | 30 ++++++ platypush/backend/sensor/envirophat.py | 24 ++--- platypush/backend/sensor/mcp3008.py | 7 +- platypush/backend/sensor/serial.py | 7 +- platypush/plugins/gpio/sensor/__init__.py | 4 +- platypush/plugins/gpio/sensor/bme280.py | 50 +++++++++ 8 files changed, 195 insertions(+), 53 deletions(-) create mode 100644 platypush/backend/sensor/bme280.py create mode 100644 platypush/plugins/gpio/sensor/bme280.py diff --git a/platypush/backend/sensor/__init__.py b/platypush/backend/sensor/__init__.py index f9a92a2c..4ad2729c 100644 --- a/platypush/backend/sensor/__init__.py +++ b/platypush/backend/sensor/__init__.py @@ -1,6 +1,7 @@ import time from platypush.backend import Backend +from platypush.context import get_plugin from platypush.message.event.sensor import SensorDataChangeEvent, \ SensorDataAboveThresholdEvent, SensorDataBelowThresholdEvent @@ -18,62 +19,145 @@ class SensorBackend(Backend): gone below a configured threshold """ - def __init__(self, thresholds=None, poll_seconds=None, **kwargs): + default_tolerance = 1e-7 + + def __init__(self, plugin=None, plugin_args=None, thresholds=None, tolerance=default_tolerance, poll_seconds=None, + enabled_sensors=None, **kwargs): """ + :param plugin: If set, then this plugin instance, referenced by plugin id, will be polled + through ``get_plugin()``. Example: ``'gpio.sensor.bme280'`` or ``'gpio.sensor.envirophat'``. + :type plugin: str + + :param plugin_args: If plugin is set and its ``get_measurement()`` method accepts optional arguments, then you + can pass those arguments through ``plugin_args``. + :type plugin_args: dict + :param thresholds: Thresholds can be either a scalar value or a dictionary (e.g. ``{"temperature": 20.0}``). Sensor threshold events will be fired when measurements get above or below these values. Set it as a scalar if your get_measurement() code returns a scalar, as a dictionary if it returns a - dictionary of values. + dictionary of values. For instance, if your sensor code returns both humidity and temperature in a format + like ``{'humidity':60.0, 'temperature': 25.0}``, you'll want to set up a threshold on temperature with a + syntax like ``{'temperature':20.0}`` to trigger events when the temperature goes above/below 20 degrees. - For instance, if your sensor code returns both humidity and - temperature in a format like ``{'humidity':60.0, 'temperature': 25.0}``, - you'll want to set up a threshold on temperature with a syntax like - ``{'temperature':20.0}`` to trigger events when the temperature goes - above/below 20 degrees. + :param tolerance: If set, then the sensor change events will be triggered only if the difference between + the new value and the previous value is higher than the specified tolerance. Example:: + + { + "temperature": 0.01, # Tolerance on the 2nd decimal digit + "humidity": 0.1 # Tolerance on the 1st decimal digit + } + + :type tolerance: dict or float :param poll_seconds: If set, the thread will wait for the specified number of seconds between a read and the next one. :type poll_seconds: float + + :param enabled_sensors: If ``get_measurement()`` returns data in dict form, then ``enabled_sensors`` selects + which keys should be taken into account when monitoring for new events (e.g. "temperature" or "humidity"). + :type enabled_sensors: dict (in the form ``name -> [True/False]``), set or list """ super().__init__(**kwargs) self.data = None + self.plugin = plugin + self.plugin_args = plugin_args or {} self.thresholds = thresholds + self.tolerance = tolerance or {} self.poll_seconds = poll_seconds + if isinstance(enabled_sensors, list): + enabled_sensors = set(enabled_sensors) + if isinstance(enabled_sensors, set): + enabled_sensors = {k: True for k in enabled_sensors} + + self.enabled_sensors = enabled_sensors or {} + def get_measurement(self): - """ To be implemented by derived classes """ - raise NotImplementedError('To be implemented in a derived class') + """ + Wrapper around ``plugin.get_measurement()`` that can filter events on specified enabled sensors data or on + specified tolerance values. It can be overridden by derived classes. + """ + if not self.plugin: + raise NotImplementedError('No plugin specified') + + plugin = get_plugin(self.plugin) + data = plugin.get_data(**self.plugin_args).output + + if self.enabled_sensors: + data = { + sensor: data[sensor] + for sensor, enabled in self.enabled_sensors.items() + if enabled and sensor in data + } + + return data + + def get_new_data(self, new_data): + if self.data is None or new_data is None: + return new_data + + # noinspection PyBroadException + try: + # Scalar data case + new_data = float(new_data) + return new_data if abs(new_data - self.data) >= self.tolerance else None + except: + # If it's not a scalar then it should be a dict + assert isinstance(new_data, dict) + + ret = {} + for k, v in new_data.items(): + if (v is None and self.data.get(k) is not None) \ + or k not in self.data \ + or self.tolerance is None: + ret[k] = v + continue + + if isinstance(self.tolerance, dict): + tolerance = self.tolerance.get(k, self.default_tolerance) + else: + try: + tolerance = float(self.tolerance) + except (TypeError, ValueError): + continue + + if abs(v - self.data.get(k)) >= tolerance: + ret[k] = v + + return ret def run(self): super().run() self.logger.info('Initialized {} sensor backend'.format(self.__class__.__name__)) while not self.should_stop(): - new_data = self.get_measurement() - if self.data is None or self.data != new_data: + data = self.get_measurement() + new_data = self.get_new_data(data) + + if new_data: self.bus.post(SensorDataChangeEvent(data=new_data)) data_below_threshold = {} data_above_threshold = {} if self.thresholds: - if isinstance(self.thresholds, dict) and isinstance(new_data, dict): + if isinstance(self.thresholds, dict) and isinstance(data, dict): for (measure, thresholds) in self.thresholds.items(): - if measure not in new_data: + if measure not in data: continue if not isinstance(thresholds, list): thresholds = [thresholds] for threshold in thresholds: - if new_data[measure] > threshold and (self.data is None or ( + if data[measure] > threshold and (self.data is None or ( measure in self.data and self.data[measure] <= threshold)): - data_above_threshold[measure] = new_data[measure] - elif new_data[measure] < threshold and (self.data is None or ( + data_above_threshold[measure] = data[measure] + elif data[measure] < threshold and (self.data is None or ( measure in self.data and self.data[measure] >= threshold)): - data_below_threshold[measure] = new_data[measure] + data_below_threshold[measure] = data[measure] if data_below_threshold: self.bus.post(SensorDataBelowThresholdEvent(data=data_below_threshold)) diff --git a/platypush/backend/sensor/accelerometer.py b/platypush/backend/sensor/accelerometer.py index ee9a0592..55125d5c 100644 --- a/platypush/backend/sensor/accelerometer.py +++ b/platypush/backend/sensor/accelerometer.py @@ -1,5 +1,4 @@ from platypush.backend.sensor import SensorBackend -from platypush.context import get_plugin class SensorAccelerometerBackend(SensorBackend): @@ -12,11 +11,8 @@ class SensorAccelerometerBackend(SensorBackend): * The :mod:`platypush.plugins.gpio.sensor.accelerometer` plugin configured """ - def get_measurement(self): - """ get_measurement implementation """ - plugin = get_plugin('gpio.sensor.accelerometer') - return plugin.get_data().output + def __init__(self, **kwargs): + super().__init__(plugin='gpio.sensor.accelerometer', **kwargs) # vim:sw=4:ts=4:et: - diff --git a/platypush/backend/sensor/bme280.py b/platypush/backend/sensor/bme280.py new file mode 100644 index 00000000..aa171f62 --- /dev/null +++ b/platypush/backend/sensor/bme280.py @@ -0,0 +1,30 @@ +from platypush.backend.sensor import SensorBackend + + +class SensorBme280Backend(SensorBackend): + """ + Backend to poll analog sensor values from a `BME280 `_ + environment sensor + + Requires: + + * ``pimoroni-bme280`` (``pip install pimoroni-bme280``) + """ + + def __init__(self, temperature=True, pressure=True, humidity=True, **kwargs): + """ + :param temperature: Enable temperature sensor polling + :param pressure: Enable pressure sensor polling + :param humidity: Enable humidity sensor polling + """ + + enabled_sensors = { + 'temperature': temperature, + 'pressure': pressure, + 'humidity': humidity, + } + + super().__init__(plugin='gpio.sensor.bme280', enabled_sensors=enabled_sensors, **kwargs) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/backend/sensor/envirophat.py b/platypush/backend/sensor/envirophat.py index ea51520b..764c91fb 100644 --- a/platypush/backend/sensor/envirophat.py +++ b/platypush/backend/sensor/envirophat.py @@ -1,11 +1,10 @@ from platypush.backend.sensor import SensorBackend -from platypush.context import get_plugin class SensorEnvirophatBackend(SensorBackend): """ - Backend to poll analog sensor values from an MCP3008 chipset - (https://learn.adafruit.com/raspberry-pi-analog-to-digital-converters/mcp3008) + Backend to poll analog sensor values from an enviroPHAT sensor pHAT + (https://shop.pimoroni.com/products/enviro-phat) Requires: @@ -24,11 +23,8 @@ class SensorEnvirophatBackend(SensorBackend): :param magnetometer: Enable magnetometer polling :param qnh: Base reference for your sea level pressure (for altitude sensor) """ - super().__init__(self, **kwargs) - self.qnh = qnh - self._last_read = {} - self.enabled_sensors = { + enabled_sensors = { 'temperature': temperature, 'pressure': pressure, 'altitude': altitude, @@ -38,17 +34,9 @@ class SensorEnvirophatBackend(SensorBackend): 'magnetometer': magnetometer, } - def get_measurement(self): - plugin = get_plugin('gpio.sensor.envirophat') - sensors = plugin.get_data(qnh=self.qnh).output - ret = { - sensor: sensors[sensor] - for sensor, enabled in self.enabled_sensors.items() - if enabled and sensor in sensors and sensors[sensor] != self._last_read.get(sensor) - } - - self._last_read = sensors - return ret + super().__init__(plugin='gpio.sensor.envirophat', + plugin_args={'qnh': qnh}, + enabled_sensors=enabled_sensors, **kwargs) # vim:sw=4:ts=4:et: diff --git a/platypush/backend/sensor/mcp3008.py b/platypush/backend/sensor/mcp3008.py index dc08ef4a..2a0327e3 100644 --- a/platypush/backend/sensor/mcp3008.py +++ b/platypush/backend/sensor/mcp3008.py @@ -1,5 +1,4 @@ from platypush.backend.sensor import SensorBackend -from platypush.context import get_plugin class SensorMcp3008Backend(SensorBackend): @@ -13,10 +12,8 @@ class SensorMcp3008Backend(SensorBackend): * The :mod:`platypush.plugins.gpio.sensor.mcp3008` plugin configured """ - def get_measurement(self): - """ get_measurement implementation """ - plugin = get_plugin('gpio.sensor.mcp3008') - return plugin.get_data().output + def __init__(self, **kwargs): + super().__init__(plugin='gpio.sensor.mcp3008', **kwargs) # vim:sw=4:ts=4:et: diff --git a/platypush/backend/sensor/serial.py b/platypush/backend/sensor/serial.py index 722f8f6d..acc988d9 100644 --- a/platypush/backend/sensor/serial.py +++ b/platypush/backend/sensor/serial.py @@ -1,5 +1,4 @@ from platypush.backend.sensor import SensorBackend -from platypush.context import get_plugin class SensorSerialBackend(SensorBackend): @@ -12,10 +11,8 @@ class SensorSerialBackend(SensorBackend): * The :mod:`platypush.plugins.serial` plugin configured """ - def get_measurement(self): - """ Implementation of ``get_measurement`` """ - plugin = get_plugin('serial') - return plugin.get_data().output + def __init__(self, **kwargs): + super().__init__(plugin='gpio.sensor.serial', **kwargs) # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/gpio/sensor/__init__.py b/platypush/plugins/gpio/sensor/__init__.py index 30789d59..025819f1 100644 --- a/platypush/plugins/gpio/sensor/__init__.py +++ b/platypush/plugins/gpio/sensor/__init__.py @@ -7,8 +7,8 @@ class GpioSensorPlugin(Plugin): should implement this class (and the get_measurement() method) """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, **kwargs): + super().__init__(**kwargs) @action def get_measurement(self, *args, **kwargs): diff --git a/platypush/plugins/gpio/sensor/bme280.py b/platypush/plugins/gpio/sensor/bme280.py new file mode 100644 index 00000000..7a2cb507 --- /dev/null +++ b/platypush/plugins/gpio/sensor/bme280.py @@ -0,0 +1,50 @@ +from platypush.plugins import action +from platypush.plugins.gpio.sensor import GpioSensorPlugin + + +class GpioSensorBme280Plugin(GpioSensorPlugin): + """ + Plugin to interact with a `BME280 `_ environment sensor for + temperature, humidity and pressure measurements over I2C interface + + Requires: + + * ``pimoroni-bme280`` (``pip install pimoroni-bme280``) + """ + + def __init__(self, port=1, **kwargs): + """ + :param port: I2C port. 0 = /dev/i2c-0 (port I2C0), 1 = /dev/i2c-1 (port I2C1) + """ + + super().__init__(**kwargs) + self.port = port + + # noinspection PyPackageRequirements + # noinspection PyUnresolvedReferences + @action + def get_measurement(self): + """ + :returns: dict. Example:: + + output = { + "temperature": 21.0, # Celsius + "pressure": 101555.08, # Pascals + "humidity": 23.543, # percentage + } + + """ + + from smbus import SMBus + from bme280 import BME280 + + bus = SMBus(self.port) + device = BME280(i2c_dev=bus) + return { + 'temperature': device.get_temperature(), + 'pressure': device.get_pressure()*100, + 'humidity': device.get_humidity(), + } + + +# vim:sw=4:ts=4:et: