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