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

He Sander,
Had hier het zelfde probleem.
Zie hieronder de oplossing. Heb gelijk ook de boel omgebouwd om gebruik te maken van: https://github.com/timofurrer/w1thermsensor hierdoor is het systeem wat sneller.
import requests
import hashlib
import time
import sys
reload(sys) # Reload does the trick!
sys.setdefaultencoding('UTF8')

#Dont forget to fill in PASSWORD and URL TO saveTemp (twice) in this file

sensorids = ["28-000005d339a2", "28-000005d36d06"]

from w1thermsensor import W1ThermSensor

sensor1 = W1ThermSensor(W1ThermSensor.THERM_SENSOR_DS18B20, "000005d339a2")
sensor2 = W1ThermSensor(W1ThermSensor.THERM_SENSOR_DS18B20, "000005d36d06")
temp_sensor1_in_celsius = sensor1.get_temperature()
temp_sensor2_in_celsius = sensor2.get_temperature()

print temp_sensor1_in_celsius
print temp_sensor2_in_celsius

session = requests.Session()
nonce = session.get(url='http://xxx.xxx.xxx/saveTemp.php?step=nonce').text

response = hashlib.sha256(nonce + 'JE_WACHWOORD' + str(temp_sensor1_in_celsius) + str(temp_sensor2_in_celsius)).hexdigest()

post_data = {'response':response, 'temp1':temp_sensor1_in_celsius, 'temp2': temp_sensor2_in_celsius}

post_request = session.post(url='http://xxx.xxx.xxx/saveTemp.php', data=post_data)

if post_request.status_code == 200 :
print post_request.text

Hoi Christiaan ,
Bedankt voor je reactie. Helaas weet ik er nog weinig van af dus vandaar mijn vraag of iemand mijn handje kan beetje houden en begeleiden welke stappen ik moet ondernemen om dit tot een werkende geheel te krijgen.

Bedankt, mooi voorbeeld. De web-pagina heb ik ook gebruikt.
Ik zie alleen dat er ingebakken een standaard verschil zit tussen UTC en hier (NL) van 2 uur. En net nu de wintertijd weer begonnen is, is dat 1 uur, de tijden uit de grafiek zitten er een uur naast. Ik heb dat veranderd door in php de offset uit te rekenen tussen de lokale timezone en UTC, en die op de plaats van het getal 2 in de aanroep van calcTime te zetten:

(variabele $offset bevat de offset van de lokale timezone)

{date:calcTime(<?php print strtotime($row['timestamp']) * 1000; ?>,<?php print $offset ?>),

Ik gebruik dit script nu al maanden .. echter soms heb ik vreemde spikes in mijn grafiek ... http://temp.on4top.be/index.php?w=shed
Geen idee hoe dit komt ..

afbeelding van Christiaan

Beste On4TOP, Ik heb je site bekeken en het ziet er goed uit. Ik zie inderdaad de spikes in je grafiek als ik de 30 dagen filter aanklik. Kun je je code eens nakijken of je de CRC checksum over de temperatuurwaarde controleert? Een andere mogelijkheid is het gebruik van de nieuwe Python library voor de DS18B20: https://github.com/timofurrer/w1thermsensor. Deze doet de checksum controle volgens mij voor je.

is het ook mogelijk om in het phyton gedeelte gelijk naar de database te schrijven als je de webserver ook lokaal draait?
ik ben met deze code bezig en krijg het maar niet voor elkaar om het in de database te krijgen.
wellicht dat ik iets verkeerd doe.. is dit zo goed?
nonce = session.get(url='http:\\127.0.0.1\saveTemp.php?step=nonce').text
response = hashlib.sha256(nonce + 'deze moet veranderd worden toch?' + str(avgtemperatures[0]) + str(avgtemperatures[1])).hexdigest()
post_data = {'response':response, 'temp1':avgtemperatures[0], 'temp2': avgtemperatures[1]}
post_request = session.post(url='http:\\127.0.0.1\saveTemp.php', data=post_data)

en in de save temp.php
$mysqli = new mysqli("localhost", "temps", "het wachtwoord", "de gebruiker");

moet deze ook gewijzigd worden?
define("PASSWORD","PASSWORD");

ik weet namelijk niet of het in de .py mis gaat of in de php...

Er is ook een python library voor de DS18b20, zie: https://github.com/timofurrer/ds18b20
Te gebruiken als volgt:
from ds18b20 import DS18B20
sensor = DS18B20()
sensor.get_temperature()

beste, Naar aanleiding van uw voorbeeld alles werkend gekregen, inclusief opslag in een SQLdatabase. 1 ding lukt me echter niet. Ik heb 2 pi's met sensoren, welke de data in 2 tabellen opslaan in dezelfde database. Ik wil graag inhoud van deze twee tabellen in 1 grafiek weergeven, maar kom hier helaas niet uit. M.I. zal er in het volgende stuk code een extra table aangesproken moeten worden, maar wat ik ook probeer; de grafiek blijft blank of een internal server error... heb je misschien enige tips?
	var lineChartData = [
		<?php foreach($last24hourValues as $row) { ?>
			{
				date: calcTime(<?php print strtotime($row['created_at']) * 1000; ?>, 2),
				temp1: <?php print number_format($row['temp1'], 2); ?>,
				temp2: <?php print number_format($row['temp2'], 2); ?>,
			},
		<?php } ?>
	];
afbeelding van Christiaan

Hoi Roy,
Bovengenoemde code lijkt maar uit een databasetabel te lezen. De code die jij hierboven aangeeft lijkt sterk op wat ik had. Hierbij doe ik het volgende:

  • Ik zet in $last24hourValues de databaserijen van de temps tabel van de afgelopen 24 uur, dmv een SQL query.
  • Zolang als er resultaten zijn stop ik de waarden van elke databaserij als associatieve array in de array $last24hourValues ik behoud hierbij de kolomnamen temp1, temp2 en created_at
  • Wanneer ik de waarden wil gaan printen voor de javascript array, het punt waar jij bovenstaande code van hebt, maak ik voor elk van deze rijen een javascript object (aangegeven met {}), met zowel temp1 als temp2 waarde.

Je zult dus denk ik de functie last24hourValues() aan moeten passen zodat hij met twee SQL queries achter elkaar beide tabellen ophaalt en deze waarden vervolgens beiden in $last24hourValues stopt. Hierbij moet je er wel op letten dat de created_at wel overeenkomt.

aanvulling hierop, maar ook iets wat ik absoluut niet begrijp; indien de waarden in de SQLdatabase groter zijn dan 1000, is de grafiek geheel blanco... bijv. bij het meten van luchtdruk komt dit naar boven...

alle mogelijke aanwijzingen worden zeer gewaardeerd..!!

Pagina's