Initial commit
This commit is contained in:
commit
f94c1d1cf1
|
@ -0,0 +1,3 @@
|
||||||
|
*.pyc
|
||||||
|
__pycache__
|
||||||
|
*.swp
|
|
@ -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"}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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()
|
|
@ -0,0 +1,8 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.implementation == 'micropython':
|
||||||
|
from esp import MQTTClient
|
||||||
|
else:
|
||||||
|
from .paho_mqtt import MQTTClient
|
||||||
|
|
||||||
|
__all__ = [MQTTClient]
|
|
@ -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")
|
|
@ -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))
|
|
@ -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 *
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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<temp>\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])
|
|
@ -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'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
Loading…
Reference in New Issue