Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • stustanet/temperature-daemon
  • roman/temperature-daemon
  • 007638/temperature-daemon
3 results
Show changes
Commits on Source (33)
......@@ -2,4 +2,7 @@
*.swo
__pycache__
*.pyc
/venv/
/.mypy_cache
/.idea
/*.egg-info
default:
image: debian-build-python:bullseye
# Is performed before the scripts in the stages step
before_script:
- source /etc/profile
# Load the ssh private key from the gitlab build variables to enable dupload
# to connect to the repo via scp
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan repo.stusta.mhn.de >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
# Defines stages which are to be executed
stages:
- build_bullseye
- upload_to_repo
# Stage "build_bullseye"
build_bullseye:
stage: build_bullseye
script:
- apt update
- apt install -y python3-stdeb python-all
- dpkg-buildpackage -us -uc
- mkdir -p build/
- mv ../tempermonitor*.deb build/
- mv ../tempermonitor*.changes build/
- mv ../tempermonitor*.tar.gz build/
- mv ../tempermonitor*.dsc build/
- mv ../tempermonitor*.buildinfo build/
# The files which are to be made available in GitLab
artifacts:
paths:
- build/*
upload_to_repo:
stage: upload_to_repo
script:
- echo "Uploading debian package to ssn repository"
- ssh repo@repo.stusta.mhn.de "echo SSH-Connection working"
- dupload -f -t ssn_repo build/tempermonitor*.changes
when: manual
only:
- master
This is the StuStaNet Temperature Monitoring System.
# Hardware Setup and Protocol
The temperature sensors are ds18x20 sensors connected via the onewire protocol
to an esp32. The esp is connected via usb-serial to the host computer.
This sends roughly every second a measurement value from one of the sensors.
After a complete round it sends an empty line.
# Dependencies
pyserial-asyncio. And >=python3.5.
# Architecture
The communications is done within the temperature_daemon.py file as well as
error handling for the sensor measurements.
It will then call functions in plugins to do the majority of the work.
The plugins are located in the `plugins` folder.
A plugin has to include a `def init(temperaturemonitor):` initializer which
returns the plugin-class. See `plugins/warnings.py` for reference.
To see which plugin functions are called with what arguments search for
`call_plugin` in the whole tree.
A plugin function can either be either async or not, both versions will be
executed properly.
Plugins can also call other plugins.
# Configuration
The system is configured via the `tempermon.ini` file, but the path can be changed
by supplying a single argument to the main executable.
It includes a bunch of default sections:
* **serial**: settings for the serial connection
* **\<pluginname>**: plugin specific settings
* **\<one-wire-id>**: every other section is interpreted as a sensor configuration
section. The configured sensor name is used for the collectd graphs, so if a
sensor is replaced, also change its name.
If a sensor is missing from this list, it will generate warning mails, as well
as for extra sensors. Only leave the sensors commented in, that are actually
used.
# Testing
In `/tests` the testing architecture is set up.
`run_tests.sh` creates a testing socket as well as a emulated collectd socket.
Now testing can be started using the default configfile.
# Existing Plugins
If you create another plugin please add it to this list.
## Collectd
Store values into collectd when new sensor values are available as well as expose
a generic graph-storing for other plugins
## Mail
Contains the emailing system as well as all email templates.
Reacts to most `err_*` and `warn_` plugin calls and sends emails for them to the
configured clients
## Warnings
Analyse all available sensors, create statistics and analsye them and create
warnings, if required.
Here we can adopt new warning strategies.
#!/usr/bin/env sh
rm -rf deb_dist tempermonitor.egg-info dist
python3 setup.py --command-packages=stdeb.command bdist_deb
tempermonitor (2.1.1) bullseye; urgency=medium
* fix for python 3.11
-- Wolfgang Walter <wolfgang.walter@stusta.de> Sun, 06 Aug 2023 00:21:00 +0200
tempermonitor (2.1.0) bullseye; urgency=medium
* upgrade to bullseye
-- Michael Loipführer <ml@stusta.de> Fri, 12 Nov 2021 18:40:18 +0200
tempermonitor (2.0.6) buster; urgency=medium
* fix missing module
-- Michael Loipführer <ml@stusta.de> Thu, 24 Apr 2020 18:40:18 +0200
tempermonitor (2.0.5) buster; urgency=medium
* fix missing module
-- Michael Loipführer <ml@stusta.de> Thu, 23 Apr 2020 23:40:18 +0200
tempermonitor (2.0.4) buster; urgency=medium
* improve plugin handling
-- Michael Loipführer <ml@stusta.de> Thu, 23 Apr 2020 23:30:18 +0200
tempermonitor (2.0.3) buster; urgency=medium
* rename config file
-- Michael Loipführer <ml@stusta.de> Thu, 23 Apr 2020 23:10:18 +0200
tempermonitor (2.0.2) buster; urgency=medium
* change main function
-- Michael Loipführer <ml@stusta.de> Thu, 23 Apr 2020 23:00:18 +0200
tempermonitor (2.0.1) buster; urgency=medium
* package renaming
-- Michael Loipführer <ml@stusta.de> Thu, 23 Apr 2020 22:55:18 +0200
tempermonitor (2.0.0) buster; urgency=medium
* Initial debian release
-- Michael Loipführer <ml@stusta.de> Thu, 23 Apr 2020 19:15:18 +0200
11
Source: tempermonitor
Maintainer: Michael Loipführer <ml@stusta.de>
Section: python
Priority: optional
Build-Depends: python3-setuptools, python3-all, debhelper (>= 11)
Standards-Version: 4.2.1
Package: tempermonitor
Homepage: https://gitlab.stusta.de/stustanet/temperature-daemon
Vcs-Browser: https://gitlab.stusta.de/stustanet/temperature-daemon
Vcs-Git: https://gitlab.stusta.de/stustanet/temperature-daemon.git
Architecture: any
Depends: ${misc:Depends}, ${python3:Depends}
Description: Tempermonitor sensor temperature reading deamon
This is the StuStaNet Temperature Monitoring System.
.
# Hardware Setup and Protocol
.
The temperature sensors are ds18x20 sensors connected via the onewire protocol
to an esp32. The esp is connected via usb-serial to the host computer.
This sends roughly every second a measurement value from one of the sensors.
After a complete round it sends an empty line.
.
# Dependencies
.
pyserial-asyncio. And >=python3.5.
.
# Architecture
The communications is done within the temperature_daemon.py file as well as
error handling for the sensor measurements.
It will then call functions in plugins to do the majority of the work.
.
The plugins are located in the `plugins` folder.
.
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: tempermonitor
Source: gitlab.stusta.de/stustanet/temperature-daemon
License: GPL
etc/* /etc/
\ No newline at end of file
#!/usr/bin/make -f
# This file was automatically generated by stdeb 0.9.0 at
# Thu, 23 Apr 2020 19:15:18 +0200
%:
dh $@ --with python3 --buildsystem=python_distutils
override_dh_auto_clean:
python3 setup.py clean -a
find . -name \*.pyc -exec rm {} \;
override_dh_auto_build:
python3 setup.py build --force
override_dh_auto_install:
python3 setup.py install --force --root=debian/tempermonitor --no-compile -O0 --install-layout=deb
override_dh_python2:
dh_python2 --no-guessing-versions
extend-diff-ignore="\.egg-info$"
\ No newline at end of file
......@@ -3,8 +3,7 @@ Description=SSN temperature monitoring service
After=network.target
[Service]
WorkingDirectory=/usr/local/bin/temperature-daemonv2/
ExecStart=/usr/local/bin/temperature-daemonv2/tempermonitor.py
ExecStart=/usr/bin/python3 -m tempermonitor
Restart=on-failure
[Install]
......
[general]
plugins=prometheus,mail,warnings
[serial]
port=/dev/ttyUSB0
port=/tmp/temperature_pts
baudrate=115200
timeout=100
[collectd]
socketpath=/var/run/collectd-unixsock
socketpath=/tmp/collectd_sock
hostname=hugin
interval=1
[prometheus]
sensor_metric_name=ssn_container_temperature
aggregated_metric_name=ssn_container_temperature_agg
address=localhost
port=9199
[mail]
from=Temperman <root@temperator.stusta.de>
to=jw@stusta.de,markus.hefele@stusta.de
to_urgent=jw@stusta.de,markus.hefele@stusta.de
min_delay_between_messages=3600
[warning]
floor_sensors=Test
ceiling_sensors=Test2
min_ceiling_warning=35
floor_ceiling_diff=15
ceiling_warning_level=40
[testsensor]
name=Test
calibration=5
"""
This code is designed to run on an ESP32 for grabbing temperature data from
ds18x20 sensors via the onewire protocol and sending them via serial to the
connected host. It should be trivial to port it to different platforms.
The Protocol is dead simple:
ONEWIRE-ID1 TEMPERATURE1
ONEWIRE-ID2 TEMPERATURE2
...
<Empty Line>
When a sensor has problems reading, it sends as temperature 9001.
New sensors are only detected upon powerup, so you have to reboot in order to
extend the sensor network.
The sensors have a parasitic-power-mode which is NOT TO BE USED here.
Please connect all three pins, and multiplex as you please.
"""
import machine
import time
import onewire, ds18x20
import ubinascii
class reader:
def __init__(self):
self.di = machine.Pin(13)
self.ds = ds18x20.DS18X20(onewire.OneWire(self.di))
#scan for sensors
self.roms = self.ds.scan()
def run(self):
while 1:
time.sleep_ms(240)
self.ds.convert_temp()
time.sleep_ms(750)
for rom in self.roms:
print(ubinascii.hexlify(rom).decode('utf-8'), end=' ')
try:
print(self.ds.read_temp(rom))
except:
print(9001)
print()
prometheus-client==0.7.1
pyserial==3.4
pyserial-asyncio==0.4
\ No newline at end of file
from setuptools import setup
VERSION = '2.0.6'
def readme():
with open('README.md') as f:
return f.read()
setup(
name='tempermonitor',
version=VERSION,
description='Tempermonitor sensor temperature reading deamon',
long_description=readme(),
url='http://gitlab.stusta.de/stustanet/temperature-daemon',
classifiers=[
'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.7',
'Operating System :: POSIX :: Linux'
],
install_requires=[
'asyncio',
'pyserial-asyncio',
'prometheus_client'
],
license='MIT',
packages=[
'tempermonitor',
'tempermonitor.plugins'
],
include_package_data=True,
zip_safe=False
)
[tempermonitor]
Depends: python3-serial-asyncio python3-prometheus-client
Suite: buster
#!/usr/bin/env python3
import asyncio
import configparser
import time
from datetime import datetime
from email.mime.text import MIMEText
from email.utils import formatdate
import smtplib
import serial_asyncio
UNKNOWN_SENSOR_HEADER = "WARNING: Unknown Sensor ID"
UNKNOWN_SENSOR_BODY = """Hello Guys,
An unknown Sensor has been connected to the Temperature monitoring service.
Please add the sensor to the list of known sensors: {config}.
The SensorID is {owid}
Its current Temperature is {temp}
Regards, Temperature"""
SENSOR_MEASUREMENT_MISSED = "WARNING: Sensor Measurement was missed"
SENSOR_MEASUREMENT_MISSED = """Hello Guys,
A Sensor measurement was missed from the temperature monitoring.
The sensor in question is {owid}, named {name}.
Please go check it!
Regards, Temperature"""
class Sensor:
"""
One instance as sensor posing as measurementproxy
"""
def __init__(self, config, owid):
self.temperature = None
self.last_update = 0
self.calibration = 0
try:
if owid in config:
self.name = config[owid]['name']
self.calibration = config[owid]['calibration']
except KeyError as exc:
print("Invalid Config: ", exc)
raise
def update(self, temperature):
"""
Store a new measurement, and remember the time it was taken
"""
self.temperature = temperature
self.last_update = time.time()
class Collectd:
def __init__(self, loop, config):
self.loop = loop or asyncio.get_event_loop()
self.config = config
self._reader, self._writer = self.loop.run_until_complete(
asyncio.open_unix_connection(
path=self.config['collectd']['socketpath'],
loop=self.loop
))
async def send(self, sensor):
"""
Store the temperature to collectd for fancy graphs
"""
data = "PUTVAL \"{}/{}\" interval={} {}:{}\n".format(
self.config['collectd']['hostname'],
sensor.name,
int(self.config['collectd']['interval']),
int(sensor.last_update),
sensor.temperature)
self._writer.write(data)
await self._writer.drain()
class TempMonitor:
"""
Interact with the esp-one-wire interface that sends:
one-wire-id1 temperature
one-wire-id1 temperature
one-wire-id1 temperature
followed by an empty line as data packet
"""
def __init__(self, loop, configfile):
self.loop = loop or asyncio.get_event_loop()
self._configname = configfile
self.config = configparser.ConfigParser()
self.config.read(configfile)
self._collectd = Collectd(self.loop, self.config)
self._reader, self._writer = self.loop.run_until_complete(
serial_asyncio.open_serial_connection(
url=self.config['serial']['port'],
baudrate=self.config['serial']['baudrate'],
loop=self.loop
))
self._known_sensors = {}
self._last_store = 0
# Test if all necessary config fields, that are not part of the normal
# startup
configtest = [
self.config['mail']['from'],
self.config['mail']['to'],
self.config['mail']['to_urgent'],
]
del configtest
predefined_sections = ['serial', 'collectd', 'mail']
for owid in self.config:
if owid in predefined_sections:
continue
self._known_sensors[owid] = Sensor(self.config, owid)
self._run_task = loop.create_task(self.run())
async def run(self):
"""
Read the protocol, update the sensors or trigger a collectd update
"""
# This is just a hack to drop the micropython startup
# The parameter has to be tuned
await asyncio.sleep(0.1)
self._reader.drain()
firstrun = True
while True:
line = self._reader.readline()
try:
line = line.decode('ascii')
except UnicodeError:
continue
if line == '':
# Block has ended
await self.store_sensors()
firstrun = False
elif firstrun:
# We start recording after we have seen the first empty line
# else our first package might be incomplete
pass
else:
try:
owid, temp = line.split(' ')
except ValueError as exc:
# TODO upon startup we only see garbage. (micropython starting up)
# maybe there is an efficient way of dropping those?
# like waiting for ~10 seconds in the beginning?
print("Invaid line received: {}\n{}".format(line, exc))
continue
if owid not in self._known_sensors:
await self.send_mail(
UNKNOWN_SENSOR_HEADER,
UNKNOWN_SENSOR_BODY.format(
configparser=self._configname,
owid=owid,
temp=temp))
else:
self._known_sensors[owid].update(temp)
async def teardown(self):
""" Terminate all started tasks """
self._run_task.cancel()
try:
await self._run_task
except asyncio.CancelledError:
pass
async def store_sensors(self):
"""
Prepare the sensors to be stored and maybe send an email
"""
for owid, sensor in self._known_sensors.items():
if sensor.last_update < self._last_store:
isotime = datetime.utcfromtimestamp(sensor.last_update).isoformat()
self.send_mail(
SENSOR_MEASUREMENT_MISSED,
SENSOR_MEASUREMENT_MISSED.format(
owid=owid,
last_update=isotime))
else:
await self._collectd.send(sensor)
self._last_store = time.time()
async def send_mail(self, subject, body, urgent=False):
"""
Send a mail to the configured recipients
"""
msg = MIMEText(body, _charset="UTF-8")
msg['Subject'] = subject
msg['From'] = self.config['mail']['from']
if urgent:
msg['To'] = self.config['mail']['to_urgent']
else:
msg['To'] = self.config['mail']['to']
msg['Date'] = formatdate(localtime=True)
# Commented out for debugging reasons to not concern the admins
smtp = smtplib.SMTP("mail.stusta.mhn.de")
smtp.sendmail(msg['From'], msg['To'], msg.as_string())
smtp.quit()
def main():
"""
Start the tempmonitor
"""
loop = asyncio.get_event_loop()
monitor = TempMonitor(loop, "./tempermon.ini")
loop.run_forever()
loop.run_until_complete(monitor.teardown())
if __name__ == "__init__":
main()
from tempermonitor.tempermonitor import main
main()