Initial import.

This commit is contained in:
Kyle Isom 2022-02-26 18:31:17 -08:00
commit 2e988a0df7
9 changed files with 455 additions and 0 deletions

1
README.txt Normal file
View File

@ -0,0 +1 @@
Install https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/raspi-blinka.py first.

0
chime/__init__.py Normal file
View File

47
chime/database.py Normal file
View File

@ -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)

82
chime/main.py Normal file
View File

@ -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()

55
chime/pubsub.py Normal file
View File

@ -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()

176
chime/sensor.py Normal file
View File

@ -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

80
chime/telemetry_pb2.py Normal file
View File

@ -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)

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
adafruit-circuitpython-ms8607
protobuf
psycopg2
zmq

10
setup.py Normal file
View File

@ -0,0 +1,10 @@
from setuptools import setup, find_packages
setup(
name='chime',
version='0.1.0',
packages=find_packages(include=['chime', 'chime.*']),
entry_points={
'console_scripts': ['my-command=exampleproject.example:main.py']
}
)