Temperatuurmonitoring met de Raspberry Pi

Introductie

Voor een horecaproject wilde ik een simpele webpagina maken die een log van de temperaturen van een aantal koelkasten en vriezers weergeeft. Daarvoor had ik een eenvoudig systeem nodig dat een aantal temperatuursensoren uitleest en naar een database stuurt voor verdere verwerking. Ik had nog een Raspberry Pi liggen die ik niet gebruikte, dus heb ik een simpele setup gemaakt met die Pi en meerdere DS18B20-temperatuursensoren.

Bedrading

De nuttigste tutorial over de Raspberry Pi in combinatie met temperatuurmonitoring die ik ben tegengekomen is de tutorial van Matthew Kirk. Volgens deze tutorial is de beste enkeldraadse temperatuursensor de DS18B20, die direct aangesloten kan worden op de GPIO4-poort. Het enige wat ik zelf nog moest doen was het toevoegen van een 4K7 Ohm-weerstand tussen de GPIO4-poort en de 3.3V-aansluiting.

Het schema ziet er als volgt uit:
DS18B20 Raspberry Pi scheme
Omdat ik meerdere sensoren wilde uitlezen, hoopte ik dat ik de andere GPIO-poorten van de Raspberry Pi op dezelfde manier kon gebruiken als de GPIO4-poort. De huidige Wheezy-kernel van de Raspberry Pi ondersteunt de enkeldraadse DS18B20-temperatuursensor echter alleen via de GPIO4-poort. Het leek er dus op dat één sensor het maximum per Raspberry Pi zou zijn. Gelukkig heeft de DS18B20 nog een prettige eigenschap, die overigens nergens wordt vermeld (of ik heb het nog niet gevonden): de DS18B20 is een van de weinige enkeldraadse temperatuursensoren die parallel kunnen worden geschakeld! Dit zou moeten betekenen dat we zo veel sensoren kunnen toevoegen als we willen en delen van de kabel tussen de sensoren en de Raspberry kunnen hergebruiken. Bovendien heb je nog steeds maar één 4K7 Ohm-weerstand nodig als je meerdere DS18B20-sensoren gebruikt.

De bedrading is dus heel simpel en ziet er (als je, net als ik, de waterbestendige versie van de sensoren hebt) zo uit:
Parallelle bedrading van de DS18B20

GPIO- en Therm-kernelmodules

In de eerdergenoemde tutorial staat dat we de kernelmodule voor de GPIO-pinnen en de thermometermodule kunnen inschakelen door middel van de volgende opdrachten:
sudo modprobe w1-gpio
sudo modprobe w1-therm
We willen dat natuurlijk niet elke keer handmatig doen als de Raspberry opnieuw opstart, dus moeten we ervoor zorgen dat de modules automatisch starten. Dit kunnen we doen door in het bestand /etc/modules de volgende regels aan te passen:
w1-gpio
w1-therm

Handmatig uitlezen

Ik heb meerdere temperatuursensoren, die nu, met verschillende ID's, in sys/bus/w1/devices/ zullen verschijnen. We kunnen een temperatuur uitlezen door het volgende uit te voeren:
cat /sys/bus/w1/devices/<ID>/w1_slave

Uitlezen met Python

In Python kunnen de devices worden geopend als een bestand. Ik heb een loop gemaakt die een bepaalde groep sensoren uitleest, gebaseerd op de Python-code uit de tutorial.
import time
 
sensorids = ["28-000004eb4e02", "28-000004eb6805"]
avgtemperatures = []
for sensor in range(len(sensorids)):
        temperatures = []
        for polltime in range(0,5):
                tfile = open("/sys/bus/w1/devices/"+ sensorids[sensor] +"/w1_slave")
                # Read all of the text in the file.
                text = tfile.read()
                # Close the file now that the text has been read.
                tfile.close()
                # Split the text with new lines (\n) and select the second line.
                secondline = text.split("\n")[1]
                # Split the line into words, referring to the spaces, and select the 10th word (counting from 0).
                temperaturedata = secondline.split(" ")[9]
                # The first two characters are "t=", so get rid of those and convert the temperature from a string to a number.
                temperature = float(temperaturedata[2:])
                # Put the decimal point in the right place and display it.
                temperatures.append(temperature / 1000)
                time.sleep(1)
        temperatures = sorted(temperatures)
        del temperatures[6]
        del temperatures[0]
        avgtemperatures.append(sum(temperatures) / float(len(temperatures)))
Update 1-7-2013: Zoals iemand in een reactie aangaf is het niet nodig om meerdere waarden van de temperatuursensor op te halen, de hoogste en laagste waarden weg te gooien en van de overblijvende waarden een gemiddelde te nemen met als doel foutieve waarden te filteren. De temperatuursensor berekend namelijk een CRC checksum over het antwoord en de kernel controleert deze voor je. In de eerste regel wordt aangegeven of de waarde valide is met "YES" of "NO". Hier kunnen we gewoon op matchen. Vandaar dat ik de code nu zo heb aangepast dat er net zo lang een nieuwe meting wordt gedaan tot de CRC checksum klopt. Let er op dat ik dit nog steeds drie keer doe omdat we dan een stabielere meting (over een langere tijdsperiode) verkrijgen.
import time
 
sensorids = ["28-000004eb4e02", "28-000004eb6805"]
avgtemperatures = []
for sensor in range(len(sensorids)):
	temperatures = []
	for polltime in range(0,3):
			text = '';
			while text.split("\n")[0].find("YES") == -1:
					tfile = open("/sys/bus/w1/devices/"+ sensorids[sensor] +"/w1_slave")
					text = tfile.read()
					tfile.close()
					time.sleep(1)
 
			secondline = text.split("\n")[1]
			temperaturedata = secondline.split(" ")[9]
			temperature = float(temperaturedata[2:])
			temperatures.append(temperature / 1000)
 
	avgtemperatures.append(sum(temperatures) / float(len(temperatures)))
Nu staan de temperaturen in de lijst avgtemperatures. Ik lees de sensoren vijf keer uit, gevolgd door een pauze van een seconde. Dit doe ik omdat ik heb gemerkt dat de sensoren niet altijd stabiele waarden geven, ook als ik het apparaat open met cat. Af en toe geven de sensoren een heel hoge of juist heel lage waarde aan. Ik probeer die uitschieters eruit te filteren door vijf metingen uit te voeren, de resultaten te sorteren van laag naar hoog, de laagste en hoogste waarde te verwijderen en het gemiddelde te berekenen van de overgebleven waarden. Dit vermindert het effect van leesfouten, die een onrealistische daling zouden veroorzaken in de temperatuurgrafieken.

HTTP-posts naar de webserver

Je zou nu een MySQL-verbinding kunnen maken in hetzelfde Python-script en de waarden op die manier kunnen toevoegen aan de database. Helaas ondersteunt mijn hostingprovider geen externe databaseverbindingen, dus moest ik door middel van wat extra code de waarden naar de webserver sturen met een HTTP-request. Om ervoor te zorgen dat niemand met de waarden kan rommelen heb ik ook een eenvoudige wachtwoordbeveiliging toegevoegd.
# Start a session because the server needs to be able to link the nonce request and the actual data post request.
session = requests.Session()
 
# Getting a fresh nonce which we will use in the authentication step.
nonce = session.get(url='url_to_server_side_script?step=nonce').text
 
# Hashing the nonce, the password and the temperature values (to provide some integrity).
response = hashlib.sha256(nonce + 'PASSWORD' + str(avgtemperatures[0]) + str(avgtemperatures[1])).hexdigest()
 
# Post data of the two temperature values and the authentication response.
post_data = {'response':response, 'temp1':avgtemperatures[0], 'temp2': avgtemperatures[1]}
 
post_request = session.post(url='url_to_server_side_script', data=post_data)
Let erop dat je vanaf nu requests en hashlib moet importeren. Daarnaast kan het nodig zijn om de requests-module voor Python te installeren.

Een cronjob toevoegen

Om het Python-script om de paar minuten uit te voeren, zul je een crontab-entry moeten toevoegen (door crontab -e uit te voeren). Ik lees de sensoren elke twee minuten uit met:
# m h  dom mon dow   command
*/2 * * * * /usr/bin/python /path/to/python_temperature_polling_script.py

Server-side code

Aan de serverkant moeten we een nonce geven, de authenticatie controleren en de waarde aan de databasetabel toevoegen.
<?php
 
define("PASSWORD","password");
 
session_start();
 
if(isset($_GET['step']) && $_GET['step'] == 'nonce') {
	getNonce();
} else if (isset($_POST['response']) && isset($_POST['temp1']) && isset($_POST['temp2'])) {
	checkAuthenticationResponce();
	processEntry();
}
 
function checkAuthenticationResponce() {
	if(!isset($_SESSION['tempNonce']) || hash('sha256', $_SESSION['tempNonce'] . PASSWORD . $_POST['temp1'] . $_POST['temp2']) != $_POST['response']) {
		header("HTTP/1.0 401 Authorization Required");
		exit;
	} else {
		unset($_SESSION['tempNonce']);
	}
}
 
function getNonce() {
	$_SESSION['tempNonce'] = hash('sha256', 'some secret nonsense to make sure nobody can predict this nonce' . time());
	echo $_SESSION['tempNonce'];
}
 
function processEntry() {
	$mysqli = new mysqli("database_connection_information");
 
	/* check connection */
	if ($mysqli->connect_errno) {
		header("HTTP/1.0 500 Internal Server Error");
		exit();
	}
 
	/* Create table doesn't return a resultset */
	$stmt = $mysqli->prepare("INSERT INTO temps (temp1, temp2) VALUES(?,?)");
	$stmt->bind_param('dd', floatval($_POST['temp1']), floatval($_POST['temp2']));
 
	if ($stmt->execute() === true) {
		echo "added";
	} else {
		header("HTTP/1.0 500 Internal Server Error");
	}
 
	$mysqli->close();
}
?>
Vervolgens worden de waarden toegevoegd aan de database. Zoals je aan de laatste stappen kunt zien was ik te lui om de code erg uitbreidbaar te maken. Ik zou de code zo hebben kunnen aanpassen dat het Python-script alle waarden in de avgtemperatures-lijst aan de postdata zou toevoegen, zodat je alleen maar de ID van de sensor zou hoeven toevoegen aan de lijst met sensor-ID's om een nieuwe sensor toe te voegen. Ook zou de database op de server kunnen worden aangepast zodat de ID van de sensor met de temperatuurwaarde wordt opgeslagen. Op dit moment gaat het er echter om dat het werkt, en dat doet het.

De waarden weergeven

Nu de waarden in de database zijn opgeslagen kun je de dashboards maken die de temperatuurstatistieken weergeven. Om je daar een beetje mee op weg te helpen: ik gebruik de CSS3-thermometerstijl van Daniel Stancu in combinatie met een temperatuurgrafiek die ik heb gemaakt met de Amchart Library.
Temperature dash
Bijlagen: 

Reactie toevoegen

Reacties

afbeelding van Christiaan

Ik vermoed dat dit iets te maken kan hebben met je MySQL datatype van de velden waar je de waarden in opslaat. Kijk bijvoorbeeld eens naar: http://www.tutorialspoint.com/mysql/mysql-data-types.htm

Hallo

Het lijkt allemaal te werken. Ik gebruik alleen php om de temperatuur uit te lezen, maar de hardware heb ik opgezet zoals beschreven: twee sensoren. Echter ze lijken elkaar te beinvloeden. Als ik 1 van de 2 sensoren in de hand houd, zie ik ze beiden oplopen. Doe ik iets verkeerd?

afbeelding van Christiaan

Hoi Michael, ik denk niet dat de sensoren zelf elkaar kunnen beinvloeden, ookal zijn ze via dezelfde verbinding aangesloten op de raspberry pi. Het zijn digitale sensoren die zelf een digitale temperatuurwaarde opsturen naar de pi wanneer daar om gevraagd wordt. Die kunnen elkaars waarde dus niet beïnvloeden. Ik zou eerder kijken naar de code waarmee je de sensoren uitleest. Misschien pak je per ongeluk dezelfde sensor voor beide waarden, maar tel je er toch iets (een string bij een integer optellen kan in php zelfs) bij op zodat de waarden niet exact hetzelfde zijn? Als je de code plaatst kan ik er ook wel even naar kijken.

Succes!

Hoi Christiaan

Je hebt helemaal gelijk: ik las dezelfde sensor 2X uit!
Bedankt voor je reactie!

Geachte,
Ziet er goed uit heb een deel werkend tot de update mysql.
Heb je mogelijk een hint hoe te gegevens temp1 en temp2 lokaal toegevoegd kunnen worden aan
Mysql db met db "temperatuur" en table "openweather" met o.a. velden "temp1" en "temp2".
ben niet zo thuis in python.
mvg
Henry

afbeelding van Christiaan

Hoi, volgens een artikel op http://stackoverflow.com/questions/372885/how-do-i-connect-to-a-mysql-da... kun je verbinden met mySQL via de volgende code;

import MySQLdb
 
db = MySQLdb.connect(host="localhost", # your host, usually localhost
                      user="henry", # your username
                      passwd="not_henry", # your password
                      db="temperatuur") # name of the data base
 
# you must create a Cursor object. It will let
#  you execute all the query you need
cur = db.cursor() 
 
# Use all the SQL you like
cur.execute("INSERT INTO openwheather (temp1, temp2) VALUES(%f, %f)", temp1, temp2)

Ik heb gegokt dat een variabele voor een float in een geparameteriseerde functie een %f is, als dit niet werkt zou ik dit als eerste gaan nazoeken.

Veel succes!

http://datasheets.maximintegrated.com/en/ds/DS18B20.pdf
Each DS18B20 has a unique 64-bit serial code, which allows multiple DS18B20s to function on the same 1-Wire bus. Thus, it is simple to use one microprocessor to control many DS18B20s distributed over a large area. Applications that can benefit from this feature include HVAC environmental controls, temperature monitoring systems inside buildings, equipment, or machinery, and process monitoring and control systems.

Pagina's