Initial import.
This commit is contained in:
commit
2e988a0df7
|
@ -0,0 +1 @@
|
||||||
|
Install https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/raspi-blinka.py first.
|
|
@ -0,0 +1,47 @@
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
def __init__(self, creds):
|
||||||
|
keys = ['host', 'port', 'name', 'user', 'pass']
|
||||||
|
for key in keys:
|
||||||
|
if not key in creds:
|
||||||
|
raise ValueError(f'missing cred key={key}')
|
||||||
|
self.creds = creds
|
||||||
|
self.conn = None
|
||||||
|
self.queued = []
|
||||||
|
|
||||||
|
def _connstr_(self):
|
||||||
|
connstr = f"host={self.creds['host']} port={self.creds['port']} "
|
||||||
|
connstr += f"dbname={self.creds['name']} user={self.creds['user']} "
|
||||||
|
connstr += f"password={self.creds['pass']}"
|
||||||
|
return connstr
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.conn:
|
||||||
|
self.conn.close()
|
||||||
|
self.conn = None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
if self.conn:
|
||||||
|
self.close()
|
||||||
|
self.conn = psycopg2.connect(self._connstr_())
|
||||||
|
|
||||||
|
def store(self, reading):
|
||||||
|
try:
|
||||||
|
if not self.conn:
|
||||||
|
self.connect()
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
|
||||||
|
cur.execute("""INSERT INTO readings
|
||||||
|
(created, temp, press, humid)
|
||||||
|
VALUES (%s, %s, %s, %s)
|
||||||
|
""", (reading.time.timestamp(), reading.temp, reading.press, reading.hum))
|
||||||
|
self.conn.commit()
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
if len(self.queued) > 0:
|
||||||
|
self.store(self.queued.pop())
|
||||||
|
except:
|
||||||
|
print('db error')
|
||||||
|
self.queued.append(reading)
|
|
@ -0,0 +1,82 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import pickle
|
||||||
|
import sys
|
||||||
|
from chime.database import Database
|
||||||
|
from chime.pubsub import Publisher
|
||||||
|
from chime.sensor import MS8607
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
|
def show_temperature(reading):
|
||||||
|
print(reading)
|
||||||
|
|
||||||
|
|
||||||
|
# !/usr/bin/env python3
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import pickle
|
||||||
|
import sys
|
||||||
|
from chime.database import Database
|
||||||
|
from chime.pubsub import Publisher
|
||||||
|
from chime.sensor import MS8607
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
|
def show_temperature(reading):
|
||||||
|
print(reading)
|
||||||
|
|
||||||
|
|
||||||
|
def write_reading(logfile, reading):
|
||||||
|
logfile.write(str(reading) + '\n')
|
||||||
|
logfile.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def main(credspath='/etc/chime/creds.dat', logpath='/var/lib/pht.csv'):
|
||||||
|
with open(credspath, 'rb') as credsfile:
|
||||||
|
creds = pickle.loads(credsfile.read())
|
||||||
|
db = Database(creds)
|
||||||
|
sensor = MS8607()
|
||||||
|
publisher = Publisher('tcp://*:4000')
|
||||||
|
|
||||||
|
logfile = open('/var/lib/pht.csv', 'at')
|
||||||
|
|
||||||
|
while True:
|
||||||
|
reading = sensor.reading()
|
||||||
|
show_temperature(reading)
|
||||||
|
write_reading(logfile, reading)
|
||||||
|
publisher.publish(reading.json())
|
||||||
|
db.store(reading)
|
||||||
|
sleep(120)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
def write_reading(logfile, reading):
|
||||||
|
logfile.write(str(reading) + '\n')
|
||||||
|
logfile.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def main(credspath='/etc/chime/creds.dat', logpath='/var/lib/pht.csv'):
|
||||||
|
with open(credspath, 'rb') as credsfile:
|
||||||
|
creds = pickle.loads(credsfile.read())
|
||||||
|
db = Database(creds)
|
||||||
|
sensor = MS8607()
|
||||||
|
publisher = Publisher('tcp://*:4000')
|
||||||
|
|
||||||
|
logfile = open('/var/lib/pht.csv', 'at')
|
||||||
|
|
||||||
|
while True:
|
||||||
|
reading = sensor.reading()
|
||||||
|
show_temperature(reading)
|
||||||
|
write_reading(logfile, reading)
|
||||||
|
publisher.publish(reading.json())
|
||||||
|
db.store(reading)
|
||||||
|
sleep(120)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,55 @@
|
||||||
|
import time
|
||||||
|
import zmq
|
||||||
|
import telemetry_pb2 as proto
|
||||||
|
|
||||||
|
|
||||||
|
class Subscriber:
|
||||||
|
def __init__(self, addr, topics=None, conflate=1):
|
||||||
|
self.ctx = zmq.Context().instance()
|
||||||
|
self.socket = self.ctx.socket(zmq.SUB)
|
||||||
|
self.socket.setsockopt(zmq.CONFLATE, conflate)
|
||||||
|
self._topics = set()
|
||||||
|
self.socket.connect(addr)
|
||||||
|
|
||||||
|
if topics is not None:
|
||||||
|
for topic in topics:
|
||||||
|
self.subscribe(topic)
|
||||||
|
|
||||||
|
def subscribe(self, topic):
|
||||||
|
if len(topic) == 0:
|
||||||
|
self.socket.subscribe('')
|
||||||
|
self._topics.add('')
|
||||||
|
return
|
||||||
|
|
||||||
|
if topic not in self._topics:
|
||||||
|
self._topics.add(topic)
|
||||||
|
topic_bytes = bytearray([10, len(topic)])
|
||||||
|
topic_bytes.extend(topic)
|
||||||
|
self.socket.subscribe(bytes(topic_bytes))
|
||||||
|
|
||||||
|
def topics(self):
|
||||||
|
return list(self._topics)
|
||||||
|
|
||||||
|
def receive(self):
|
||||||
|
return self.socket.recv()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
return self.socket.close()
|
||||||
|
|
||||||
|
|
||||||
|
class Publisher:
|
||||||
|
def __init__(self, addr, conflate=1):
|
||||||
|
self.ctx = zmq.Context().instance()
|
||||||
|
self.socket = self.ctx.socket(zmq.PUB)
|
||||||
|
self.socket.setsockopt(zmq.CONFLATE, conflate)
|
||||||
|
self.socket.bind(addr)
|
||||||
|
|
||||||
|
def publish(self, message, topic=b''):
|
||||||
|
message = proto.Packet(topic=topic, created=int(time.time()), payload=message)
|
||||||
|
return self.socket.send(message.SerializeToString())
|
||||||
|
|
||||||
|
def publish_json(self, message, topic=b''):
|
||||||
|
return self.publish(json.dumps(message).encode('UTF-8'), topic)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
return self.socket.close()
|
|
@ -0,0 +1,176 @@
|
||||||
|
import board
|
||||||
|
import busio
|
||||||
|
import json
|
||||||
|
import adafruit_ms8607 as ms8607
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
DELAY = 0.1
|
||||||
|
|
||||||
|
|
||||||
|
class Reading:
|
||||||
|
def __init__(self, t, p, h, normal=False):
|
||||||
|
if not normal:
|
||||||
|
if t < -40.0 or t > 85.0:
|
||||||
|
raise TemperatureError(t)
|
||||||
|
if h < 0.0 or h > 100.0:
|
||||||
|
raise HumidityError(h)
|
||||||
|
if p < 10 or p > 2000:
|
||||||
|
raise PressureError(p)
|
||||||
|
|
||||||
|
self.time = datetime.datetime.now()
|
||||||
|
self.temp = t
|
||||||
|
self.press = p
|
||||||
|
self.hum = h
|
||||||
|
self.normal = normal
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'Reading@{self.time.timestamp()}(t={self.temp},p={self.press},h={self.hum})'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.time.strftime("%F %T %Z")},{self.time.timestamp()},{self.temp},{self.press},{self.hum}'
|
||||||
|
|
||||||
|
def list(self):
|
||||||
|
return [
|
||||||
|
self.time.strftime("%F %T %Z"),
|
||||||
|
self.time.timestamp(),
|
||||||
|
self.temp,
|
||||||
|
self.press,
|
||||||
|
self.hum
|
||||||
|
]
|
||||||
|
|
||||||
|
def normalize(self):
|
||||||
|
"""
|
||||||
|
normal returns a normalised average of PHT.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.normal:
|
||||||
|
return self
|
||||||
|
return Reading(
|
||||||
|
normal_temp(self.temp),
|
||||||
|
normal_press(self.press),
|
||||||
|
normal_humidity(self.hum),
|
||||||
|
normal=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def average(self):
|
||||||
|
if not self.normal:
|
||||||
|
return self.normalize().average()
|
||||||
|
return average([self.temp, self.press, self.hum])
|
||||||
|
|
||||||
|
def json(self) -> bytes:
|
||||||
|
return bytes(json.dumps({
|
||||||
|
'timestamp': self.time.timestamp,
|
||||||
|
'temperature': self.temp,
|
||||||
|
'pressure': self.press,
|
||||||
|
'relative_humidity': self.hum,
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
class SensorError(Exception):
|
||||||
|
def __init__(self, component, value):
|
||||||
|
self.component = component
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'SensorError: {self.component} out of range: {self.value}'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self)
|
||||||
|
|
||||||
|
|
||||||
|
class TemperatureError(SensorError):
|
||||||
|
def __init__(self, value):
|
||||||
|
super().__init__('temperature', value)
|
||||||
|
|
||||||
|
|
||||||
|
class PressureError(SensorError):
|
||||||
|
def __init__(self, value):
|
||||||
|
super().__init__('pressure', value)
|
||||||
|
|
||||||
|
|
||||||
|
class HumidityError(SensorError):
|
||||||
|
def __init__(self, value):
|
||||||
|
super().__init__('humidity', value)
|
||||||
|
|
||||||
|
|
||||||
|
def clamp(val):
|
||||||
|
return min(1.0, max(0.0, val))
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(minval, maxval, val):
|
||||||
|
return clamp((val - minval) / maxval)
|
||||||
|
|
||||||
|
|
||||||
|
def normal_temp(temperature):
|
||||||
|
return normalize(0.0, 45.0, temperature)
|
||||||
|
|
||||||
|
|
||||||
|
def normal_press(pressure):
|
||||||
|
return normalize(1000.0, 1050.0, pressure)
|
||||||
|
|
||||||
|
|
||||||
|
def normal_humidity(hum):
|
||||||
|
return normalize(20, 90, hum)
|
||||||
|
|
||||||
|
|
||||||
|
def average(values):
|
||||||
|
return sum(values) / len(values)
|
||||||
|
|
||||||
|
|
||||||
|
class Sensor:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reading(self) -> Reading:
|
||||||
|
return Reading(0, 0, 0)
|
||||||
|
|
||||||
|
def json(self) -> bytes:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestSensor(Sensor):
|
||||||
|
def __init__(self, t, h, p):
|
||||||
|
super().__init__()
|
||||||
|
self.temperature = t
|
||||||
|
self.relative_humidity = h
|
||||||
|
self.pressure = p
|
||||||
|
|
||||||
|
def json(self) -> bytes:
|
||||||
|
return bytes(json.dumps({
|
||||||
|
'temperature': self.temperature,
|
||||||
|
'pressure': self.pressure,
|
||||||
|
'relative_humidity': self.relative_humidity,
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
class MS8607(Sensor):
|
||||||
|
|
||||||
|
def __init__(self, i2c=None):
|
||||||
|
super().__init__()
|
||||||
|
if i2c is None:
|
||||||
|
i2c = busio.I2C(board.SCL, board.SDA)
|
||||||
|
self.sensor = get_sensor(i2c)
|
||||||
|
|
||||||
|
def reading(self) -> Reading:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
reading = Reading(self.sensor.temperature, self.sensor.pressure, self.sensor.relative_humidity)
|
||||||
|
except (OSError, ValueError, SensorError) as err:
|
||||||
|
print(err)
|
||||||
|
sleep(DELAY)
|
||||||
|
else:
|
||||||
|
return reading
|
||||||
|
|
||||||
|
|
||||||
|
def get_sensor(i2c):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
sensor = ms8607.MS8607(i2c)
|
||||||
|
except OSError as os_error:
|
||||||
|
print(os_error)
|
||||||
|
sleep(DELAY)
|
||||||
|
except ValueError as i2c_error:
|
||||||
|
print(i2c_error)
|
||||||
|
sleep(DELAY)
|
||||||
|
else:
|
||||||
|
return sensor
|
|
@ -0,0 +1,80 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# source: telemetry.proto
|
||||||
|
|
||||||
|
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import message as _message
|
||||||
|
from google.protobuf import reflection as _reflection
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||||
|
name='telemetry.proto',
|
||||||
|
package='telemetrypb',
|
||||||
|
syntax='proto3',
|
||||||
|
serialized_options=b'Z\r.;telemetrypb',
|
||||||
|
create_key=_descriptor._internal_create_key,
|
||||||
|
serialized_pb=b'\n\x0ftelemetry.proto\x12\x0btelemetrypb\";\n\x06Packet\x12\r\n\x05topic\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x0f\n\x07payload\x18\x03 \x01(\x0c\x42\x0fZ\r.;telemetrypbb\x06proto3'
|
||||||
|
)
|
||||||
|
|
||||||
|
_PACKET = _descriptor.Descriptor(
|
||||||
|
name='Packet',
|
||||||
|
full_name='telemetrypb.Packet',
|
||||||
|
filename=None,
|
||||||
|
file=DESCRIPTOR,
|
||||||
|
containing_type=None,
|
||||||
|
create_key=_descriptor._internal_create_key,
|
||||||
|
fields=[
|
||||||
|
_descriptor.FieldDescriptor(
|
||||||
|
name='topic', full_name='telemetrypb.Packet.topic', index=0,
|
||||||
|
number=1, type=9, cpp_type=9, label=1,
|
||||||
|
has_default_value=False, default_value=b"".decode('utf-8'),
|
||||||
|
message_type=None, enum_type=None, containing_type=None,
|
||||||
|
is_extension=False, extension_scope=None,
|
||||||
|
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||||
|
_descriptor.FieldDescriptor(
|
||||||
|
name='timestamp', full_name='telemetrypb.Packet.timestamp', index=1,
|
||||||
|
number=2, type=4, cpp_type=4, label=1,
|
||||||
|
has_default_value=False, default_value=0,
|
||||||
|
message_type=None, enum_type=None, containing_type=None,
|
||||||
|
is_extension=False, extension_scope=None,
|
||||||
|
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||||
|
_descriptor.FieldDescriptor(
|
||||||
|
name='payload', full_name='telemetrypb.Packet.payload', index=2,
|
||||||
|
number=3, type=12, cpp_type=9, label=1,
|
||||||
|
has_default_value=False, default_value=b"",
|
||||||
|
message_type=None, enum_type=None, containing_type=None,
|
||||||
|
is_extension=False, extension_scope=None,
|
||||||
|
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||||
|
],
|
||||||
|
extensions=[
|
||||||
|
],
|
||||||
|
nested_types=[],
|
||||||
|
enum_types=[
|
||||||
|
],
|
||||||
|
serialized_options=None,
|
||||||
|
is_extendable=False,
|
||||||
|
syntax='proto3',
|
||||||
|
extension_ranges=[],
|
||||||
|
oneofs=[
|
||||||
|
],
|
||||||
|
serialized_start=32,
|
||||||
|
serialized_end=91,
|
||||||
|
)
|
||||||
|
|
||||||
|
DESCRIPTOR.message_types_by_name['Packet'] = _PACKET
|
||||||
|
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||||
|
|
||||||
|
Packet = _reflection.GeneratedProtocolMessageType('Packet', (_message.Message,), {
|
||||||
|
'DESCRIPTOR': _PACKET,
|
||||||
|
'__module__': 'telemetry_pb2'
|
||||||
|
# @@protoc_insertion_point(class_scope:telemetrypb.Packet)
|
||||||
|
})
|
||||||
|
_sym_db.RegisterMessage(Packet)
|
||||||
|
|
||||||
|
DESCRIPTOR._options = None
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
|
@ -0,0 +1,4 @@
|
||||||
|
adafruit-circuitpython-ms8607
|
||||||
|
protobuf
|
||||||
|
psycopg2
|
||||||
|
zmq
|
Loading…
Reference in New Issue