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 (39)
...@@ -2,4 +2,7 @@ ...@@ -2,4 +2,7 @@
*.swo *.swo
__pycache__ __pycache__
*.pyc *.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
[Unit] [Unit]
Description=SSN temperature management service Description=SSN temperature monitoring service
After=network.target After=network.target
[Service] [Service]
ExecStart=/usr/local/bin/temperature-deamon/temperdaemon.py ExecStart=/usr/bin/python3 -m tempermonitor
Restart=on-failure Restart=on-failure
[Install] [Install]
......
[general]
plugins=prometheus,mail,warnings
[serial]
port=/tmp/temperature_pts
baudrate=115200
timeout=100
[collectd]
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
"""
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 python2.7
# encoding: utf-8
# Changelog
#
# 200X Created
# 2017 JW -- Gitted & Updated libtemperusb and changed the interfaces accordingly
import os
import sys
sys.path.append("../temper")
os.environ["USB_DEVFS_PATH"] = "/dev/bus/usb/temper"
import socket
from temperusb import temper
import threading
import time
import Queue
import smtplib
import ow
import logging
from collections import deque, namedtuple
from email.mime.text import MIMEText
from email.Utils import formatdate
logging.basicConfig(format='%(asctime)s:%(name)s:%(levelname)s:%(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
READINGINTERVAL = 10
UNIX_SOCKET = "/var/run/collectd-unixsock"
HOSTNAME = "hugin.stusta.mhn.de"
FLOOR_LIMIT = 25
CEILING_LIMIT = 30
CEILING_LIMIT_EMR = 42
FLOOR_LIMIT_EMR = 38
MAX_OUTDOOR_DIFF = 15
MAILINTERVAL = 3600
# Read the BUS and ADDRESS from a lsusb:
#
# $ lsusb
# ...
# Bus 005 Device 008: ID 0c45:7401 Microdia TEMPer Temperature Sensor
# ...
#
# meaning: use the pair (5, 8) as bus, address tuple.
DEVICEMAPPING_USB = {
#(BUS, ADDRESS): (identifier, calibration)
(5, 8): ("floor", 5.00),
(5, 7): ("ceiling", 3.00),
(1, 4): ("outdoor", 3.30),
}
DEVICEMAPPING_SERIAL = [
('floorserial', '10.C238A5010800'),
('ceilserial', '10.0C33A5010800')
]
SPAM = False
#SPAM = True
class TempReader(threading.Thread):
def __init__(self, export_queue, mail_queue):
threading.Thread.__init__(self)
self.export_queue = export_queue
self.mail_queue = mail_queue
self.last_mail = 0
self.iteration = 0
def queue_mail(self, floor, ceiling, outdoor):
logger.info("write mail ...")
body = u'''DON'T PANIC!
... aber die Temperatur im Serverraum beträgt:
Boden: {:.2f}
Decke: {:.2f}
Outdoor: {:.2f}
Der Temperator
--
oooo$$$$$$$$$$$$oooo
oo$$$$$$$$$$$$$$$$$$$$$$$$o
oo$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o o$ $$ o$
o $ oo o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o $$ $$ $$o$
oo $ $ "$ o$$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$o $$$o$$o$
"$$$$$$o$ o$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$o $$$$$$$$
$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$
$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$$$$$$ """$$$
"$$$""""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$
$$$ o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$o
o$$" $$$$$$$$$$$$$$$$$$$$$$$$S$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$o
$$$ $$$$$$$$$$$$$$$$$$$$$$$$SS$$$$$$$$$$$$$$$$$$$" "$$$$$$ooooo$$$$o
o$$$oooo$$$$$ $$$$$$$$$$$$$$$$SSSS$$$$$$$$$$$$$$$$$ o$$$$$$$$$$$$$$$$$
$$$$$$$$"$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$""""""""
"""" $$$$ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$" o$$$
"$$$o """$$$$$$$$$$$$$$$$$$"$$" $$$
$$$o "$$""$$$$$$"""" o$$$
$$$$o o$$$"
"$$$$o o$$$$$$o"$$$$o o$$$$
"$$$$$oo ""$$$$o$$$$$o o$$$$""
""$$$$$oooo "$$$o$$$$$$$$$"""
""$$$$$$$oo $$$$$$$$$$
""""$$$$$$$$$$$
$$$$$$$$$$$$
$$$$$$$$$$"
"$$$""""
'''
msg = MIMEText(body.format(floor, ceiling, outdoor), _charset="UTF-8")
src = "Temperator <root@hugin.stusta.mhn.de>"
if (ceiling>CEILING_LIMIT_EMR)or(floor>FLOOR_LIMIT_EMR):
msg['Subject'] = u"TEMPERATURNOTFALL Serverraum!"
else:
msg['Subject'] = u"Temperaturalarm Serverraum"
msg['From'] = src
if SPAM:
dst = ["jw@stusta.net"]
else:
dst = ["admins@stustanet.de"]
msg['To'] = ", ".join(dst)
msg['Date'] = formatdate(localtime=True)
# Commented out for debugging reasons to not concern the admins
self.mail_queue.put((src, dst, msg.as_string()))
def run(self):
while True:
logger.debug("reading loop")
start = time.time()
try:
th = temper.TemperHandler()
# TODO naming things is hard!
for usb_device in th.get_devices():
busadress = (usb_device._device.bus, usb_device._device.address)
temp_sensor = temperature_sensors.get(busadress)
if temp_sensor:
try:
usb_device.set_calibration_data(scale=1, offset=temp_sensor.calibration)
temp_c = usb_device.get_temperature()
record = temp_sensor.update(temp_c)
self.export_queue.put(record)
except Exception as e:
logger.debug(e)
#logger.exception(e)
except Exception as e:
logger.exception(e)
try:
ow.init( 'localhost:4304' )
for a, owid in iter(temperature_sensors):
if a == 'serial':
temp_sensor = temperature_sensors.get((a, owid))
try:
temp_c = float(ow.Sensor( '/uncached/'+ owid ).temperature)
if temp_c != float('85'):
record = temp_sensor.update(temp_c)
self.export_queue.put(record)
except Exception as e:
logger.debug(e)
# memory leak?
#ow.finish()
except Exception as e:
logger.exception(e)
self.iteration += 1
now = time.time()
floor, floor_last, floor_avg = temperature_sensors_usb_by_name['floor'].get_current_and_average()
ceiling, ceiling_last, ceiling_avg = temperature_sensors_usb_by_name['ceiling'].get_current_and_average()
outdoor, outdoor_last, outdoor_avg = temperature_sensors_usb_by_name['outdoor'].get_current_and_average()
falling = False
if (floor is not None and floor_avg is not None and floor <= floor_avg and
ceiling is not None and ceiling_avg is not None and ceiling <= ceiling_avg and
outdoor is not None and outdoor_avg is not None and outdoor <= outdoor_avg):
falling = True
# error case: some sensor is not working!
error = False
if floor is None or (floor_last is not None and floor_last + MAILINTERVAL < now):
error = True
floor = 9000
if ceiling is None or (ceiling_last is not None and ceiling_last + MAILINTERVAL < now):
error = True
ceiling = 9001
if outdoor is None or (outdoor_last is not None and outdoor_last + MAILINTERVAL < now):
# for the difference ...
error = True
outdoor = 0
if (self.iteration > 1 and self.last_mail + MAILINTERVAL < now and
(error or (not falling and
((floor > FLOOR_LIMIT and floor - MAX_OUTDOOR_DIFF > outdoor) or
(ceiling > CEILING_LIMIT and ceiling - MAX_OUTDOOR_DIFF > outdoor))))):
self.last_mail = now
self.queue_mail(floor, ceiling, outdoor)
if (self.iteration > 1 and self.last_mail + 300 < now and (ceiling > CEILING_LIMIT_EMR or floor > FLOOR_LIMIT_EMR)):
self.last_mail = now
self.queue_mail(floor, ceiling, outdoor)
logger.info("--------------EMERGENCY------------")
sys.stdout.flush()
wait = start + READINGINTERVAL - time.time()
if wait > 0:
time.sleep(wait)
class Mail0r(threading.Thread):
def __init__(self, mail_queue):
threading.Thread.__init__(self)
self.mail_queue = mail_queue
def run(self):
while True:
try:
sender, recipient, msg = self.mail_queue.get(True, 3600)
except Queue.Empty as e:
logger.debug(e)
continue
try:
#s = smtplib.SMTP("localhost")
#we don't have a local mta anymore
s = smtplib.SMTP("mail.stusta.mhn.de")
s.sendmail(sender, recipient, msg)
s.quit()
except Exception as e:
logger.error("----ERROR---MAIL----")
logger.error(e)
CollectdRecord = namedtuple("CollectdRecord", ["hostname", "path", "interval", "epoch", "value"])
class Exp0rt0r(threading.Thread):
def __init__(self, export_queue):
threading.Thread.__init__(self)
self.socket = None
# CollectdRecords
self.export_queue = export_queue
def run(self):
while True:
record = None
try:
record = self.export_queue.get(True, 3600)
except Queue.Empty as e:
continue
if not self.socket:
s = socket.socket(socket.AF_UNIX)
try:
s.connect(UNIX_SOCKET)
s.setblocking(False)
self.socket = s
except Exception as e:
logger.error(e)
time.sleep(1)
continue
data = "PUTVAL \"%s/%s\" interval=%i %i:%s\n" % \
(record.hostname, record.path, int(record.interval), int(record.epoch), record.value)
try:
logger.debug("socket write: %s", data.strip())
self.socket.send(data)
recv = []
while True:
try:
recv = self.socket.recv(1<<12)
except socket.error as e:
if e.errno == 11: #EAGAIN
break
raise e
recv = ''.join(recv)
logger.debug("socket read: %s", recv.strip())
except Exception as e:
logger.debug(e)
try:
self.socket.close()
except:
pass
self.socket = None
class TempSensor(object):
def __init__(self, name):
self.name = name
self._path = "tail-temperature/temperature-%s" % (name, )
self._interval = READINGINTERVAL
self.last_updated = None
self.temperature = None
self.history = deque()
self._lock = threading.RLock()
def update(self, temperature):
with self._lock:
now = time.time()
while self.history and self.history[0][1] < (now - MAILINTERVAL):
self.history.popleft()
if self.last_updated is not None and self.temperature is not None:
self.history.append((self.temperature, self.last_updated))
self.temperature = temperature
self.last_updated = now
logger.info("update %s", self)
return CollectdRecord(HOSTNAME, self._path, int(self._interval),
int(self.last_updated), "%f" % (self.temperature,))
def get_current_and_average(self):
with self._lock:
avg = None
if self.history:
avg = float(sum(i[0] for i in self.history))/len(self.history)
return self.temperature, self.last_updated, avg
def __str__(self):
with self._lock:
return "%s temperature: %s, last_updated:%s" % \
(self.name, self.temperature, self.last_updated)
class TempSensorUSB(TempSensor):
def __init__(self, name, calibration, bus, device):
super(TempSensorUSB, self).__init__(name)
self.calibration = calibration
self.bus = bus
self.device = device
@property
def usb_id(self):
return self.bus, self.device
def __str__(self):
with self._lock:
return "%s (bus:%s, device:%s, calibration:%i) temperature: %s, last_updated:%s" % \
(self.name, self.bus, self.device, self.calibration, self.temperature, self.last_updated)
class TempSensorSerial(TempSensor):
def __init__(self, name, owid):
super(TempSensorSerial, self).__init__(name)
self.owid = owid
def __str__(self):
with self._lock:
return "%s (owid:%s) temperature: %s, last_updated:%s" % \
(self.name, self.owid, self.temperature, self.last_updated)
temperature_sensors = {}
#TODO:Type in key, only one dict?
temperature_sensors_usb_by_name = {}
temperature_sensors_serial_by_name = {}
for busadress, descriptor in DEVICEMAPPING_USB.items():
temperature_sensors[busadress] = TempSensorUSB(
descriptor[0],
descriptor[1],
busadress[0],
busadress[1])
temperature_sensors_usb_by_name[descriptor[0]] = temperature_sensors[busadress]
from pprint import pprint
pprint(temperature_sensors_usb_by_name)
for name, owid in DEVICEMAPPING_SERIAL:
temperature_sensors[('serial', owid)] = TempSensorSerial(name, owid)
temperature_sensors_serial_by_name[name] = temperature_sensors[('serial', owid)]
if __name__ == "__main__":
mail_queue = Queue.Queue()
export_queue = Queue.Queue()
readerfred = TempReader(export_queue, mail_queue)
readerfred.setDaemon(True)
readerfred.start()
exp0rtfred = Exp0rt0r(export_queue)
exp0rtfred.setDaemon(True)
exp0rtfred.start()
mail0r = Mail0r(mail_queue)
mail0r.setDaemon(True)
mail0r.start()
while True:
time.sleep(3600)
from tempermonitor.tempermonitor import main
main()