From f94c1d1cf1e940fa8720aeb570acfe675ea15d2b Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Tue, 9 Jul 2019 22:01:39 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + mqtt-sensord.conf.example | 14 +++ mqtt_sensord/__init__.py | 0 mqtt_sensord/mqtt_sensord.py | 45 ++++++++ mqtt_sensord/mqttlib/__init__.py | 8 ++ mqtt_sensord/mqttlib/base.py | 14 +++ mqtt_sensord/mqttlib/paho_mqtt.py | 15 +++ mqtt_sensord/sensorlib/__init__.py | 11 ++ mqtt_sensord/sensorlib/base.py | 47 +++++++++ mqtt_sensord/sensorlib/config.py | 11 ++ mqtt_sensord/sensorlib/esp_sensors.py | 43 ++++++++ mqtt_sensord/sensorlib/misc_sensors.py | 140 +++++++++++++++++++++++++ mqtt_sensord/sensorlib/pi_sensors.py | 64 +++++++++++ setup.py | 22 ++++ 14 files changed, 437 insertions(+) create mode 100644 .gitignore create mode 100644 mqtt-sensord.conf.example create mode 100644 mqtt_sensord/__init__.py create mode 100755 mqtt_sensord/mqtt_sensord.py create mode 100644 mqtt_sensord/mqttlib/__init__.py create mode 100644 mqtt_sensord/mqttlib/base.py create mode 100644 mqtt_sensord/mqttlib/paho_mqtt.py create mode 100644 mqtt_sensord/sensorlib/__init__.py create mode 100644 mqtt_sensord/sensorlib/base.py create mode 100644 mqtt_sensord/sensorlib/config.py create mode 100644 mqtt_sensord/sensorlib/esp_sensors.py create mode 100644 mqtt_sensord/sensorlib/misc_sensors.py create mode 100644 mqtt_sensord/sensorlib/pi_sensors.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96eac8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +__pycache__ +*.swp diff --git a/mqtt-sensord.conf.example b/mqtt-sensord.conf.example new file mode 100644 index 0000000..a923e64 --- /dev/null +++ b/mqtt-sensord.conf.example @@ -0,0 +1,14 @@ +{ + "host": { + "name": "dev-pi", + "interval": 10 + }, + "mqtt": { + "host": "127.0.0.1", + "user": "sensornode", + "password": "password" + }, + "sensors": [ + {"id": 1, "name": "Pi Humidity 1", "type": "pi-dht22", "description": "nope"} + ] +} diff --git a/mqtt_sensord/__init__.py b/mqtt_sensord/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mqtt_sensord/mqtt_sensord.py b/mqtt_sensord/mqtt_sensord.py new file mode 100755 index 0000000..95e0bd1 --- /dev/null +++ b/mqtt_sensord/mqtt_sensord.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import argparse +import sys +import time + +from . import sensorlib +from .mqttlib import MQTTClient + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-c", "--config", default="/etc/mqtt-sensord.conf") + args = parser.parse_args() + + config = sensorlib.load_config(args.config) + sensorlib.Sensor.configure(config['host']['name'], sys.platform) + + mcfg = config['mqtt'] + if 'client_id' in config['mqtt']: + client_id = config['mqtt']['client_id'] + else: + client_id = config['host']['name'] + + mqtt = MQTTClient(mcfg['host'], mcfg['user'], mcfg['password'], client_id) + mqtt_topic = mcfg['sensor_topic'] + print("Connected to", mqtt.host, "with topic", mqtt_topic) + + sensors = [] + for scfg in config['sensors']: + s = sensorlib.Sensor.make_sensor(**scfg) + sensors.append(s) + + last_measurement = time.time() + while True: + print("Getting values from sensors...") + for sensor in sensors: + data = sensor.gen_datapoint() + print(mqtt_topic, data) + mqtt.send_data(mqtt_topic, data) + time.sleep(max(0.0, time.time() + config['host']['interval'] - last_measurement)) + last_measurement = time.time() + + +if __name__ == '__main__': + main() diff --git a/mqtt_sensord/mqttlib/__init__.py b/mqtt_sensord/mqttlib/__init__.py new file mode 100644 index 0000000..7e6fc64 --- /dev/null +++ b/mqtt_sensord/mqttlib/__init__.py @@ -0,0 +1,8 @@ +import sys + +if sys.implementation == 'micropython': + from esp import MQTTClient +else: + from .paho_mqtt import MQTTClient + +__all__ = [MQTTClient] diff --git a/mqtt_sensord/mqttlib/base.py b/mqtt_sensord/mqttlib/base.py new file mode 100644 index 0000000..b176de6 --- /dev/null +++ b/mqtt_sensord/mqttlib/base.py @@ -0,0 +1,14 @@ +class MQTTClientBase: + def __init__(self, host, user, password, client_id): + self.host = host + self.user = user + self.password = password + self.client_id = client_id + + self._connect() + + def send_data(self, topic, data): + raise NotImplementedError("Send not implemented") + + def _connect(self): + raise NotImplementedError("Connect not implemented") diff --git a/mqtt_sensord/mqttlib/paho_mqtt.py b/mqtt_sensord/mqttlib/paho_mqtt.py new file mode 100644 index 0000000..e92e7d1 --- /dev/null +++ b/mqtt_sensord/mqttlib/paho_mqtt.py @@ -0,0 +1,15 @@ +import json + +import paho.mqtt.client as mqtt + +from .base import MQTTClientBase + + +class MQTTClient(MQTTClientBase): + def _connect(self): + self._mqtt = mqtt.Client(self.client_id) + self._mqtt.username_pw_set(self.user, self.password) + self._mqtt.connect(self.host) + + def send_data(self, topic, data): + self._mqtt.publish(topic, json.dumps(data)) diff --git a/mqtt_sensord/sensorlib/__init__.py b/mqtt_sensord/sensorlib/__init__.py new file mode 100644 index 0000000..59cb366 --- /dev/null +++ b/mqtt_sensord/sensorlib/__init__.py @@ -0,0 +1,11 @@ +import sys + +from .base import Sensor +from .config import load_config + +if sys.implementation == 'micropython': + from .esp_sensors import * +elif getattr(sys.implementation, '_multiarch') == 'arm-linux-gnueabihf': + from .pi_sensors import * +from .misc_sensors import * + diff --git a/mqtt_sensord/sensorlib/base.py b/mqtt_sensord/sensorlib/base.py new file mode 100644 index 0000000..54d048c --- /dev/null +++ b/mqtt_sensord/sensorlib/base.py @@ -0,0 +1,47 @@ +def sensor(cls): + Sensor.register(cls) + + return cls + + +class Sensor(object): + hostname = None + platform = None + _sensor_classes = {} + + def __init__(self, id, name, type, pin, description, **sensor_conf): + self.id = id + self.name = name + self.type = type + self.pin = pin + self.description = description + + @classmethod + def configure(cls, hostname, platform): + cls.hostname = hostname + cls.platform = platform + + @classmethod + def register(cls, new_cls): + cls._sensor_classes[new_cls.sensor_class] = new_cls + + @classmethod + def make_sensor(cls, **kwargs): + sensor_cls = cls._sensor_classes[kwargs['type']] + return sensor_cls(**kwargs) + + def get_data(self): + raise NotImplementedError("You missed a spot!") + + def gen_datapoint(self): + return { + 'type': 'measurement', + 'tags': { + 'name': self.name, + 'sub-id': self.id, + 'sensor': self.type, + 'hostname': self.hostname, + 'platform': self.platform, + }, + 'fields': self.get_data() + } diff --git a/mqtt_sensord/sensorlib/config.py b/mqtt_sensord/sensorlib/config.py new file mode 100644 index 0000000..b95a0a5 --- /dev/null +++ b/mqtt_sensord/sensorlib/config.py @@ -0,0 +1,11 @@ +import sys + +if sys.implementation == 'micropython': + import ujson as json +else: + import json + + +def load_config(path): + with open(path) as f: + return json.load(f) diff --git a/mqtt_sensord/sensorlib/esp_sensors.py b/mqtt_sensord/sensorlib/esp_sensors.py new file mode 100644 index 0000000..59393c7 --- /dev/null +++ b/mqtt_sensord/sensorlib/esp_sensors.py @@ -0,0 +1,43 @@ +import dht +import ds18x20 +import machine +import onewire +import time + +from .base import Sensor + + +class DS18B20(Sensor): + sensor_class = 'esp-ds18b20' + + def __init__(self, **sensor_conf): + super(DS18B20, self).__init__(**sensor_conf) + + self.o = onewire.OneWire(machine.Pin(self.pin)) + self.ds = ds18x20.DS18X20(self.o) + self.sID = self.ds.scan()[0] + + def get_data(self): + self.ds.convert_temp() + time.sleep(0.1) + return {"temp": self.ds.read_temp(self.sID)} +Sensor.register(DS18B20) + + +class DHT(Sensor): + sensor_class = 'esp-dht' + + def __init__(self, **sensor_conf): + super(DHT, self).__init__(**sensor_conf) + if sensor_conf["type"] == "dht11": + self.dht = dht.DHT11(machine.Pin(self.pin)) + elif sensor_conf["type"] == "dht22": + self.dht = dht.DHT22(machine.Pin(self.pin)) + else: + raise ValueError("Unknown type") + + def get_data(self): + self.dht.measure() + time.sleep(0.1) + return {"temp": self.dht.temperature(), "humidity": self.dht.humidity()} +Sensor.register(DHT) diff --git a/mqtt_sensord/sensorlib/misc_sensors.py b/mqtt_sensord/sensorlib/misc_sensors.py new file mode 100644 index 0000000..e92b296 --- /dev/null +++ b/mqtt_sensord/sensorlib/misc_sensors.py @@ -0,0 +1,140 @@ +import fcntl +import random +import threading +import time + +from .base import Sensor, sensor + + +@sensor +class FakeSensor(Sensor): + sensor_class = 'fake' + + def __init__(self, **sensor_conf): + super(FakeSensor, self).__init__(**sensor_conf) + + def get_data(self): + return {'random': random.randint(0, 20)} + + +@sensor +class CO2Meter(Sensor): + sensor_class = 'co2meter' + HIDIOCSFEATURE_9 = 0xC0094806 + TEMP = 0x42 + CO2 = 0x50 + HUM = 0x44 + + def __init__(self, dev_path, **sensor_conf): + super(CO2Meter, self).__init__(**sensor_conf) + + self.dev_path = dev_path + self.key = b'\x11\x11\x11\x11\x11\x11\x11\x11' + self._dev = None + self.values = {} + + self.init_meter() + self.start() + + def init_meter(self): + self._dev = open(self.dev_path, "a+b", 0) + set_report = b'\x00' + self.key + fcntl.ioctl(self._dev, self.HIDIOCSFEATURE_9, set_report) + + def decrypt(self, data): + cstate = [0x48, 0x74, 0x65, 0x6D, 0x70, 0x39, 0x39, 0x65] + shuffle = [2, 4, 0, 7, 1, 6, 5, 3] + + phase1 = bytearray(8) + for i, o in enumerate(shuffle): + phase1[o] = data[i] + + phase2 = bytearray(8) + for i in range(8): + phase2[i] = phase1[i] ^ self.key[i] + + phase3 = bytearray(8) + for i in range(8): + phase3[i] = (phase2[i] >> 3 | phase2[i - 1] << 5) & 0xff + + ctmp = bytearray(8) + for i in range(8): + ctmp[i] = (cstate[i] >> 4 | cstate[i] << 4) & 0xff + + out = bytearray(8) + for i in range(8): + out[i] = (0x100 + phase3[i] - ctmp[i]) & 0xff + + return out + + def start(self): + self._stop = False + self._worker_thread = threading.Thread(target=self._worker, daemon=True) + self._worker_thread.start() + + def stop(self): + self._stop = True + self._worker_thread = None + + def _worker(self): + while not self._stop: + try: + data = self._dev.read(8) + decrypted = self.decrypt(data) + if decrypted[4] == 0x0d and sum(decrypted[:3]) & 0xff == decrypted[3]: + op = decrypted[0] + val = decrypted[1] << 8 | decrypted[2] + self.values[op] = val + else: + print("Error decrypting data:", data, '==>', decrypted) + except (IOError, OSError) as e: + print("Device error: {} - trying to reopen".format(e)) + self.values = {} + try: + self._dev.close() + except Exception: + pass + while self._dev.closed: + time.sleep(1) + try: + self.init_meter() + except (IOError, OSError) as e: + print("Device error: {} - trying to reopen".format(e)) + + + def get_co2(self): + if 0x50 in self.values: + return self.values[0x50] + return None + + def get_temp(self): + if 0x42 in self.values: + return self.values[0x42] / 16.0 - 273.15 + return None + + def get_humidity(self): + if 0x44 in self.values: + return self.values[0x44] / 100.0 + return None + + def get_data(self): + return dict(temp=self.get_temp(), co2=self.get_co2()) + + +if __name__ == '__main__': + import argparse + import time + + parser = argparse.ArgumentParser() + parser.add_argument("device", help="Device do read from") + args = parser.parse_args() + + co2 = CO2Meter(args.device) + co2.start() + try: + while True: + if co2.get_temp() and co2.get_co2(): + print("{:2.4}°C {:4} PPM CO2".format(co2.get_temp(), co2.get_co2())) + time.sleep(2) + except KeyboardInterrupt: + pass diff --git a/mqtt_sensord/sensorlib/pi_sensors.py b/mqtt_sensord/sensorlib/pi_sensors.py new file mode 100644 index 0000000..2467ed8 --- /dev/null +++ b/mqtt_sensord/sensorlib/pi_sensors.py @@ -0,0 +1,64 @@ +import os +import re + +import Adafruit_DHT + +from .base import Sensor, sensor + + +@sensor +class DS18B20(Sensor): + sensor_class = 'pi-ds18b20' + + def __init__(self, **sensor_conf): + super(DS18B20, self).__init__(**sensor_conf) + + self._sensor_id = None + if 'hw_id' in sensor_conf: + self._sensor_id = sensor_conf['hw_id'] + else: + self._find_sensor() + + def _find_sensor(self): + basepath = "/sys/bus/w1/devices" + for d in os.listdir(basepath): + if 'master' not in d and '-' in d and \ + os.path.exists(os.path.join(basepath, d, 'w1_slave')): + self._sensor_id = d + break + else: + raise ValueError("No sensor found in filesystem") + + def get_data(self): + temp = None + try: + path = "/sys/bus/w1/devices/{}/w1_slave".format(self._sensor_id) + with open(path) as f: + data = f.read() + m = re.search(r".* t=(?P\d+)$", data) + if m: + temp = int(m.group('temp')) / 1000.0 + except IOError: + # log? + pass + + return dict(temp=temp) + + +@sensor +class DHT(Sensor): + sensor_class = 'pi-dht' + + def __init__(self, **sensor_conf): + super(DHT, self).__init__(**sensor_conf) + + if sensor_conf['dht_type'] == 'dht11': + self._dht_type = Adafruit_DHT.DHT11 + elif sensor_conf['dht_type'] == 'dht22': + self._dht_type = Adafruit_DHT.DHT22 + else: + raise ValueError("Unknown DHT") + + def get_data(self): + data = Adafruit_DHT.read_retry(self._dht_type, self.pin) + return dict(humidity=data[0], temp=data[1]) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..420eeb3 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages + +setup(name='mqtt-sensord', + version='0.1.0', + description='MQTT sensor reader daemon', + author='Sebastian Lohff', + author_email='seba@someserver.de', + url='https://git.someserver.de/seba/mqtt-sensord/', + python_requires='>=3.5', + packages=find_packages(), + install_requires=[ + 'paho-mqtt', + "Adafruit_DHT ; platform_machine=='armv6l' or platform_machine=='armv7l'" + ], + entry_points={ + 'console_scripts': [ + 'mqtt-sensord = mqtt_sensord.mqtt_sensord:main' + ] + }, + )