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

Hoi!
Wat een toffe post! Heb er heel veel aan gehad tijdens mijn eigen bouw proces. Heb het nu allemaal draaien, inclusief de website die netjes de waardes weergeeft in een mooi grafiekje!
Super veel dank voor deze praktische uitleg over hoe e.e.a. in elkaar geklust moet worden :)
Groet,
Arno

ik probeer in 1 python script/programma om 2 temperaturen te monitoren; met behulp van sensor DS18B20 (temp1)en een Thermokoppel(temp2) mbv met MAX31855….In 1 script omdat de temps naar SQL weggeschreven worden, naar 1 table die coherent is.
Echter, bij run van het script krijg ik het niet voor elkaar om beide temperaturen uit le laten lezen…de thermokoppelsensor geeft steeds 0 aan.
Ik ben niet ervaren met python, maar het moet toch mogelijk zijn beide sensoren in een script uit te lezen met 1 datastamp?!
Heeft iemand een tip hoe ik dit op kan lossen. Ik heb er al aardig wat tijd in zitten. Ik heb oa van de volgende site voorbeelden gepakt (http://iada.nl/blog/artikel/temperatuurmonitoring-met-de-raspberry-pi) en wil de daarin weergegeven display verkrijgen. Indien ik de scripts (voor sensor en thermocouple) afzonderlijk via crontab draai, krijg ik wel de beide temps maar niet de grafiek (index.php), ik denk omdat de datestamps uit twee afzonderlijke tables nooit het zelfde zijn?

Beste Christiaan,

Super bedankt voor deze uitgebreide tutorial.
Ik loop echter tegen een probleem aan. Ik heb een python script draaien zodat elke 5 minuten de temperatuur + tijd in een Mysql database wordt opgeslagen. Ik zou graag de temperatuur willen uitlezen doormiddel van je PHP file (index.php)
Ik heb de inloggegevens en de query aangepast, maar de website wil niet werken. Ik krijg helaas ook geen foutmeldingen, maar een blank scherm.

Heb je misschien een idee wat er niet goed is aan de php file?

<?php
// Make sure you set the database connection on the next line:
$mysqli = new mysqli("localhost", "root", "Passwoord", "temperatuur");

/* check connection */
if ($mysqli->connect_errno) {
header("HTTP/1.0 500 Internal Server Error");
exit();
}

$lastTemps = getLastTemps($mysqli);
$lastAVGTemps = getAVGTemps($mysqli);
$last24hourValues = last24hourValues($mysqli);

$mysqli->close();

if($_GET['format'] == json) {
header('Content-Type: application/json');
print json_encode($lastTemps);
exit;
}

header('Refresh: 150; url=index.php');

function getLastTemps($mysqli) {
$stmt = $mysqli->prepare("SELECT temperatuur created_at FROM temp_4 ORDER BY created_at DESC LIMIT 1");
$stmt->execute();
$stmt->bind_result($res['temp1'], $res['created_at']);
$stmt->fetch();
return $res;
}

function getAVGTemps($mysqli) {
$stmt = $mysqli->prepare("SELECT AVG(temperatuur) FROM temp_4 WHERE created_at >= SYSDATE() - INTERVAL 1 DAY");
$stmt->execute();
$stmt->bind_result($res['temp1']);
$stmt->fetch();
return $res;
}

function last24hourValues($mysqli) {
$stmt = $mysqli->prepare("SELECT temperatuur created_at FROM temp_4 WHERE created_at >= SYSDATE() - INTERVAL 1 DAY");
$stmt->execute();
$stmt->bind_result($res['temp1'], $res['created_at']);
$rows = array();

$i = 0;
while($stmt->fetch()) {
$rows[$i] = array();
foreach($res as $k=>$v)
$rows[$i][$k] = $v;
$i++;
}
return $rows;
}

function tempParts($temp, $index) {
$parts = explode('.', number_format($temp, 1));
return $parts[$index];
}

?>
<html>
<head>
<title>Temperatures</title>
<link rel="stylesheet" type="text/css" href="./css/style.css" />
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="./amcharts/amcharts.js" type="text/javascript"></script>
<script type="text/javascript" src="./js/common.js"></script>
<script type="text/javascript">

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); ?>,
},
<?php } ?>
];

function calcTime(unixTime, offset) {

// create Date object for current location
d = new Date(unixTime);

// convert to msec
// add local time zone offset
// get UTC time in msec
utc = d.getTime() + (d.getTimezoneOffset() * 60000);

// create new Date object for different city
// using supplied offset
nd = new Date(utc + (3600000*offset));
return nd;

}

function formatLabel(value, valueString, axis){
// let's say we dont' want minus sign next to negative numbers
if(value < 0)
{
valueString = valueString.substr(1);
}

// and we also want a letter C to be added next to all labels (you can do it with unit, but anyway)
valueString = valueString + "° C";
return valueString;
}

AmCharts.ready(function () {
var chart = new AmCharts.AmSerialChart();
chart.dataProvider = lineChartData;
chart.pathToImages = "../amcharts/images/";
chart.categoryField = "date";
chart.balloon.bulletSize = 5;

// sometimes we need to set margins manually
// autoMargins should be set to false in order chart to use custom margin values
chart.marginLeft = 0;
chart.marginBottom = 0;
chart.marginTop = 0;

// AXES
// category
var categoryAxis = chart.categoryAxis;
categoryAxis.parseDates = true; // as our data is date-based, we set parseDates to true
categoryAxis.minPeriod = "ss"; // our data is daily, so we set minPeriod to DD
categoryAxis.inside = true;
categoryAxis.gridAlpha = 0;
categoryAxis.tickLength = 0;
categoryAxis.axisAlpha = 0;

// value
var valueAxis = new AmCharts.ValueAxis();
valueAxis.dashLength = 1;
valueAxis.axisAlpha = 0;
//set label function which will format values
valueAxis.labelFunction = formatLabel;

chart.addValueAxis(valueAxis);

// GRAPH
var graph = new AmCharts.AmGraph();
graph.type = "line";
graph.valueField = "temp1";
graph.lineColor = "#5fb503";
graph.negativeLineColor = "#efcc26";
//graph.fillAlphas = 0.3; // setting fillAlphas to > 0 value makes it area graph
graph.bulletSize = 3; // bullet image should be a rectangle (width = height)
chart.addGraph(graph);

// CURSOR
var chartCursor = new AmCharts.ChartCursor();
chartCursor.cursorPosition = "mouse";
chartCursor.categoryBalloonDateFormat = "JJ:NN, DD MMMM";
chart.addChartCursor(chartCursor);

// WRITE
chart.write("chartdiv");
});
</script>

</head>
<body>
<div class="content">
<div class="thermometers">
<div class="label">Sensor Binnen</div><div class="label">Sensor Buiten</div>
<div class="de">
<div class="den">
<div class="dene">
<div class="denem">
<div class="deneme">
<?php print tempParts($lastTemps['temp1'], 0); ?><span>.<?php print tempParts($lastTemps['temp1'], 1); ?></span><strong>&deg;</strong>
</div>
</div>
</div>
</div>
</div>
<div class="de">
<div class="den">
<div class="dene">
<div class="denem">
<div class="deneme">
<?php print tempParts($lastTemps['temp2'], 0); ?><span>.<?php print tempParts($lastTemps['temp2'], 1); ?></span><strong>&deg;</strong>
</div>
</div>
</div>
</div>
</div>
<div class="details">Gem laatste 24u<br /><?php print number_format($lastAVGTemps['temp1'], 1); ?>&deg;</div><div class="details">Gem laatste 24u<br /><?php print number_format($lastAVGTemps['temp2'], 1); ?>&deg;</div>
<div class="last-update"><?php print date('d-m-Y H:i:s', strtotime($lastTemps['created_at'])); ?></div>
</div>
<div id="chartdiv" style="width:100%; height:400px;"></div>
</div>
</body>
</html>

Hoi Christiaan,

Bedankt voor dit artikel! Ik heb nu ook een Raspberry met 1 sensor aan de gang, en met jouw code had ik zo het website-je draaien met de temperatuur erop.

Groeten,
Gerrelt.

Beste Gerrelt ,
Na veel proberen lukt het mij maar niet om de tempertuur logger aan de praat te krijgen.
Het Raspberry gedeelte ging prima maar het python en server gedeelte niet.
Ik heb een database aangemaakt op mijn webserver en de index aangepast maar ik krijg het niet werkend.
Kan jij mij hier mee helpen?

Hoi Gerrelt,
Mag ik je wellicht vragen of je me wilt helpen met een stappenplan die ik moet doorlopen om dit werkende te krijgen?
Mag via de mail als je er zin in hebt. Mocht je het niet willen dan houdt het uiteraard op ;-)
Gr, Sander
email = goes_sander(at)hotmail.com

Hoi Allemaal,

Ik wil heel graag gebruik maken van de source files.
Ik heb ze reeds geplaatst in me apache html folder op de raspberry met de nodige aanpassing.

Alleen lijkt mij het geheel niet plug en play uiteraard

Wil iemand mij wellicht vertellen wat ik echter nog meer moet doen om het geheel werkende te krijgen? Ik gok het creëren van een database op de raspberry pi ? Wellicht wil er iemand even wat tijd voor maken om mij te vertellen wat ik nog meer moet doen.

Ik wil graag de temperaturen gaan meten in me aquarium :D . Ik hoop dat iemand zin in heeft ;)
Alvast bedankt
Gr. Sander

afbeelding van Christiaan

Hoi Sander,

Het geheel is ook nooit bedoeld als een plug-and-play oplossing. Ik heb slechts mijn bevindingen geblogd over het communiceren met de DS18B20 en ik heb een paar, niet erg afgewerkte, scriptjes voor het uitlezen en opslaan als bijlage toegevoegd. Dit met de bedoeling dat iemand dit als bouwsteen voor zijn eigen systeem kan gaan gebruiken.

Er zijn al een aantal mensen die een opstelling hebben gemaakt waarbij de database met gegevens ook op de raspberry pi draait. Heb je de comments doorgekeken van de Nederlandstalige en Engelstalige versie van deze blog? Hier staan waarschijnlijk nog handige tips voor je in.

Voor het aanmaken van een database op de Raspberry Pi kun je MySQL gebruiken: http://raspberrywebserver.com/sql-databases/using-mysql-on-a-raspberry-p...

Op dit moment kom je niet echt met specifieke problemen naar deze blog toe, dus ik kan ook niet zo goed inschatten hoe ver je al bent met je project en wat je kennisniveau is van de dingen die hierbij komen kijken.

Succes,

Christiaan

Hoi Christiaan,

Inmiddels ben ik aan het proberen gegaan. En heb de python script op de raspberry pi en probeer het te runnen.
Hierbij krijg ik wel een error code : Traceback (most recent call last):
File "/usr/lib/cgi-bin/pollSensors.py", line 47, in <module>
response = hashlib.sha256(nonce + 'password?' + str(avgtemperatures[0]) + str(avgtemperatures[1])).hexdigest()
UnicodeEncodeError: 'ascii' codec can't encode character u'\ufeff' in position 0: ordinal not in range(128)

Ook heb ik de index.php in de /var/www/html folder geplaatst met de nodige info maar krijg daarbij de volgende error :
Fatal error: Call to a member function execute() on boolean in /var/www/html/index.php on line 30

Weet jij toevallig hier iets meer over ?

Hoi sander,

Hoe heb je het uiteindelijk opgelost, ik kreeg het met de oplossing die er aangedragen werd niet gefixt.

Pagina's