Refactored SensorBackend and derived classes and added BME280 sensor

plugin and backend
This commit is contained in:
Fabio Manganiello 2019-08-14 19:49:19 +02:00
parent d4800b5c55
commit 757e0ff9bf
8 changed files with 195 additions and 53 deletions

View file

@ -1,6 +1,7 @@
import time import time
from platypush.backend import Backend from platypush.backend import Backend
from platypush.context import get_plugin
from platypush.message.event.sensor import SensorDataChangeEvent, \ from platypush.message.event.sensor import SensorDataChangeEvent, \
SensorDataAboveThresholdEvent, SensorDataBelowThresholdEvent SensorDataAboveThresholdEvent, SensorDataBelowThresholdEvent
@ -18,62 +19,145 @@ class SensorBackend(Backend):
gone below a configured threshold 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}``). :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. 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 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 :param tolerance: If set, then the sensor change events will be triggered only if the difference between
temperature in a format like ``{'humidity':60.0, 'temperature': 25.0}``, the new value and the previous value is higher than the specified tolerance. Example::
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. "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 :param poll_seconds: If set, the thread will wait for the specified number of seconds between a read and the
next one. next one.
:type poll_seconds: float :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) super().__init__(**kwargs)
self.data = None self.data = None
self.plugin = plugin
self.plugin_args = plugin_args or {}
self.thresholds = thresholds self.thresholds = thresholds
self.tolerance = tolerance or {}
self.poll_seconds = poll_seconds 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): 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): 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__))
while not self.should_stop(): while not self.should_stop():
new_data = self.get_measurement() data = self.get_measurement()
if self.data is None or self.data != new_data: new_data = self.get_new_data(data)
if new_data:
self.bus.post(SensorDataChangeEvent(data=new_data)) self.bus.post(SensorDataChangeEvent(data=new_data))
data_below_threshold = {} data_below_threshold = {}
data_above_threshold = {} data_above_threshold = {}
if self.thresholds: 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(): for (measure, thresholds) in self.thresholds.items():
if measure not in new_data: if measure not in data:
continue continue
if not isinstance(thresholds, list): if not isinstance(thresholds, list):
thresholds = [thresholds] thresholds = [thresholds]
for threshold in 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)): measure in self.data and self.data[measure] <= threshold)):
data_above_threshold[measure] = new_data[measure] data_above_threshold[measure] = data[measure]
elif new_data[measure] < threshold and (self.data is None or ( elif data[measure] < threshold and (self.data is None or (
measure in self.data and self.data[measure] >= threshold)): 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: if data_below_threshold:
self.bus.post(SensorDataBelowThresholdEvent(data=data_below_threshold)) self.bus.post(SensorDataBelowThresholdEvent(data=data_below_threshold))

View file

@ -1,5 +1,4 @@
from platypush.backend.sensor import SensorBackend from platypush.backend.sensor import SensorBackend
from platypush.context import get_plugin
class SensorAccelerometerBackend(SensorBackend): class SensorAccelerometerBackend(SensorBackend):
@ -12,11 +11,8 @@ class SensorAccelerometerBackend(SensorBackend):
* The :mod:`platypush.plugins.gpio.sensor.accelerometer` plugin configured * The :mod:`platypush.plugins.gpio.sensor.accelerometer` plugin configured
""" """
def get_measurement(self): def __init__(self, **kwargs):
""" get_measurement implementation """ super().__init__(plugin='gpio.sensor.accelerometer', **kwargs)
plugin = get_plugin('gpio.sensor.accelerometer')
return plugin.get_data().output
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -0,0 +1,30 @@
from platypush.backend.sensor import SensorBackend
class SensorBme280Backend(SensorBackend):
"""
Backend to poll analog sensor values from a `BME280 <https://shop.pimoroni.com/products/bme280-breakout>`_
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:

View file

@ -1,11 +1,10 @@
from platypush.backend.sensor import SensorBackend from platypush.backend.sensor import SensorBackend
from platypush.context import get_plugin
class SensorEnvirophatBackend(SensorBackend): class SensorEnvirophatBackend(SensorBackend):
""" """
Backend to poll analog sensor values from an MCP3008 chipset Backend to poll analog sensor values from an enviroPHAT sensor pHAT
(https://learn.adafruit.com/raspberry-pi-analog-to-digital-converters/mcp3008) (https://shop.pimoroni.com/products/enviro-phat)
Requires: Requires:
@ -24,11 +23,8 @@ class SensorEnvirophatBackend(SensorBackend):
:param magnetometer: Enable magnetometer polling :param magnetometer: Enable magnetometer polling
:param qnh: Base reference for your sea level pressure (for altitude sensor) :param qnh: Base reference for your sea level pressure (for altitude sensor)
""" """
super().__init__(self, **kwargs)
self.qnh = qnh enabled_sensors = {
self._last_read = {}
self.enabled_sensors = {
'temperature': temperature, 'temperature': temperature,
'pressure': pressure, 'pressure': pressure,
'altitude': altitude, 'altitude': altitude,
@ -38,17 +34,9 @@ class SensorEnvirophatBackend(SensorBackend):
'magnetometer': magnetometer, 'magnetometer': magnetometer,
} }
def get_measurement(self): super().__init__(plugin='gpio.sensor.envirophat',
plugin = get_plugin('gpio.sensor.envirophat') plugin_args={'qnh': qnh},
sensors = plugin.get_data(qnh=self.qnh).output enabled_sensors=enabled_sensors, **kwargs)
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
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,5 +1,4 @@
from platypush.backend.sensor import SensorBackend from platypush.backend.sensor import SensorBackend
from platypush.context import get_plugin
class SensorMcp3008Backend(SensorBackend): class SensorMcp3008Backend(SensorBackend):
@ -13,10 +12,8 @@ class SensorMcp3008Backend(SensorBackend):
* The :mod:`platypush.plugins.gpio.sensor.mcp3008` plugin configured * The :mod:`platypush.plugins.gpio.sensor.mcp3008` plugin configured
""" """
def get_measurement(self): def __init__(self, **kwargs):
""" get_measurement implementation """ super().__init__(plugin='gpio.sensor.mcp3008', **kwargs)
plugin = get_plugin('gpio.sensor.mcp3008')
return plugin.get_data().output
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,5 +1,4 @@
from platypush.backend.sensor import SensorBackend from platypush.backend.sensor import SensorBackend
from platypush.context import get_plugin
class SensorSerialBackend(SensorBackend): class SensorSerialBackend(SensorBackend):
@ -12,10 +11,8 @@ class SensorSerialBackend(SensorBackend):
* The :mod:`platypush.plugins.serial` plugin configured * The :mod:`platypush.plugins.serial` plugin configured
""" """
def get_measurement(self): def __init__(self, **kwargs):
""" Implementation of ``get_measurement`` """ super().__init__(plugin='gpio.sensor.serial', **kwargs)
plugin = get_plugin('serial')
return plugin.get_data().output
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -7,8 +7,8 @@ class GpioSensorPlugin(Plugin):
should implement this class (and the get_measurement() method) should implement this class (and the get_measurement() method)
""" """
def __init__(self, *args, **kwargs): def __init__(self, **kwargs):
super().__init__(*args, **kwargs) super().__init__(**kwargs)
@action @action
def get_measurement(self, *args, **kwargs): def get_measurement(self, *args, **kwargs):

View file

@ -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 <https://shop.pimoroni.com/products/bme280-breakout>`_ 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: