forked from platypush/platypush
Refactored SensorBackend and derived classes and added BME280 sensor
plugin and backend
This commit is contained in:
parent
d4800b5c55
commit
757e0ff9bf
8 changed files with 195 additions and 53 deletions
|
@ -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))
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
30
platypush/backend/sensor/bme280.py
Normal file
30
platypush/backend/sensor/bme280.py
Normal 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:
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
50
platypush/plugins/gpio/sensor/bme280.py
Normal file
50
platypush/plugins/gpio/sensor/bme280.py
Normal 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:
|
Loading…
Reference in a new issue