Ein Web Interface ist besonders hilfreich, wenn man seinen Pi von unterwegs steuern möchte. Ich habe hier eine sehr einfach gehaltene Lösung für meinen Anwendungsfall IoT Heizungssteuerung (LCD Panel) erstellt.
Diese Lösung lässt sich beliebig anpassen. Was allerdings noch fehlt, ist ein responsive Design, das auch auf Smartphones und Tablets ohne Zoomen gut aussieht.
Damit das funktioniert, müsst ihr einen Webserver sowie php installieren. Welchen Webserver ihr verwendet ist eigentlich egal. Ich verwende Lighttpd. Er ist relativ schlank und trotzdem leistungsfähig. Anleitungen zur Installation gibts im Web zuhauf.
Ich habe mich an diese hier gehalten: http://www.penguintutor.com/linux/light-webserver. Die Anleitung enthält auch die Schritte zur Installation von PHP. Mysql braucht ihr nicht zu installieren. Eine sehr gute Anleitung auf Deutsch gibt es auch bei Kampis Elektroecke.
PHP mag ich nicht besonders. Zumal das Debuggen nicht ganz einfach ist. Hat man einen schwerwiegenderen Fehler gemacht (und sei es nur ein vergessenes Semicolon am Ende einer Programmzeile) gibt es einfach ein weißes Browserfenster.
Wem das Editieren mit einem der im Pi eingebauten Editoren (nano, vi) oder auf dem Desktop z.B. mit Idle zu mühsam ist, dem empfehle ich diesen Artikel hier.
Inhalt
Programmstruktur
Okay, ich gebs zu, meine html Skripte sind nicht besonders schön. Außerdem habe ich aus Bequemlichkeit teilweise mit Frames (gelten als veraltet) gearbeitet und auch noch iFrames dazugemischt.
Im Prinzip gibt es 5 einzelne Seiten, die miteinander über die Frame Definitionsseite index.html oder das PHP Formular verbunden sind:
- index.html enthält die Frame Definitions
- top Frame.html einen beliebigen Seitenkopf
- leftFrame.php das Menü und die Buttons inklusive einem iFrame, der ein Log der Ein- und ausschaltvorgänge anzeigt
- log.php zum Auslesen und anzeigen der vergangenen Schaltvorgänge in oben erwähntem iFrame
- set_temp.php zum Wegspeichern der Werte nachdem der "Speichern" Button geklickt wurde.
Index.html
Das Ganze ist wie folgt aufgebaut, zuersteinmal kommt die Datei index.html zum Zug, die den Seitenaufbau im Bereich <frameset ….> bis </frameset> definiert. Oben wird ein schöner Überschriftsbereich namens topFrame.html definiert, darunter kommt der eigentliche Eingabebereich leftFrame.php und dann noch ein Ausgabebereich set_temp.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <META HTTP-EQUIV="Pragma" CONTENT="no-cache"> <META HTTP-EQUIV="Expires" CONTENT="-1"> <title>Rustimation Heizung</title> </head> <frameset rows ="80,525,*" cols="220" framespacing="0" frameborder="NO" border="0" > <frame src="topFrame.html" name="header" scrolling="NO" title="topFrame" /> <frame src="leftFrame.php" name="combos" scrolling="NO" noresize title="leftFrame" /> <frame src="set_temp.php" name="settemp" scrolling="no" noresize title="Temperature" /> </frameset> <body> </body></noframes> </html> |
topFrame.html
Diese Seite hat lediglich eine gestalterische Funktion und kann nach eigenen Wünschen angepasst werden.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>Rustimation</title> <style type="text/css"> body,td,th { font-size: 18px; font-weight: bold; color: #FFFFFF; } body { background-color: #003366; font-family:Verdana,Arial,Helvetica,sans-serif; } </style> </head> <body> <div align="left"> <strong>Rustimation Heizung</strong><br /> <p><strong>served by RaspberryZwei</strong></p></div> </body> </html> |
LeftFrame.php
Die wesentlichen Punkte sind unterhalb des Programmlistings erklärt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>Raspberry Pi Heizungssteuerung</title> <style type="text/css"> body { background-color: #A6A6A6; font-family:Verdana,Arial,Helvetica,sans-serif; font-size:12px } </style> </head> <body> <?php //debug setting error_reporting (E_ALL | E_STRICT); ini_set ('display_errors' , 1); // Ende Debug $filename = "settings.dat"; if (file_exists($filename)) { $f=fopen($filename, "r"); for($i=0;$i<5;$i++) { $preset[$i]=fgets($f); } fclose($f); $presetnightstart=trim($preset[0]); $presetdaystart=trim($preset[1]); $presetdaytemp=trim($preset[2]); $presetnighttemp=trim($preset[3]); } else { $presetnightstart="22:00"; $presetdaystart= "06:00"; $presetdaytemp= "21.0"; $presetnighttemp= "19.0"; echo "using defaults"; } ?> <?php $act_temp=shell_exec("sudo /home/pi/heizung/get_temperature.py"); $status = shell_exec("cat /tmp/status.dat"); $timestamp = time(); $datum = date("d.m.Y",$timestamp); $uhrzeit = date("H:i:s",$timestamp); ?> <h3> <?php echo $datum." ".$uhrzeit; ?><br> Aktuelle Temperatur: <?php echo $act_temp; ?> °C<br> Pumpe ist <?php echo $status; ?><br> </h3> <form action="set_temp.php" target="settemp" method="get" enctype="text/plain"> <table width="220" border="0"> <tr> <td><label for="nightstart">Nachtabsenkung von</label></td> <td> <select name="nightstart" style="width:75px"> <option <?php if ($presetnightstart == "20:00") echo "selected"; ?>>20:00</option> <option <?php if ($presetnightstart == "20:15") echo "selected"; ?>>20:15</option> <option <?php if ($presetnightstart == "20:30") echo "selected"; ?>>20:30</option> <option <?php if ($presetnightstart == "20:45") echo "selected"; ?>>20:45</option> <option <?php if ($presetnightstart == "21:00") echo "selected"; ?>>21:00</option> <option <?php if ($presetnightstart == "21:15") echo "selected"; ?>>21:15</option> <option <?php if ($presetnightstart == "21:30") echo "selected"; ?>>21:30</option> <option <?php if ($presetnightstart == "21:45") echo "selected"; ?>>21:45</option> <option <?php if ($presetnightstart == "22:00") echo "selected"; ?>>22:00</option> <option <?php if ($presetnightstart == "22:15") echo "selected"; ?>>22:15</option> <option <?php if ($presetnightstart == "22:30") echo "selected"; ?>>22:30</option> </select></td> </tr> <tr> <td><label for="daystart">Nachtabsenkung bis</label></td> <td> <select name="daystart" style="width:75px"> <option <?php if ($presetdaystart == "05:00") echo "selected"; ?>>05:00</option> <option <?php if ($presetdaystart == "05:15") echo "selected"; ?>>05:15</option> <option <?php if ($presetdaystart == "05:30") echo "selected"; ?>>05:30</option> <option <?php if ($presetdaystart == "05:45") echo "selected"; ?>>05:45</option> <option <?php if ($presetdaystart == "06:00") echo "selected"; ?>>06:00</option> <option <?php if ($presetdaystart == "06:15") echo "selected"; ?>>06:15</option> <option <?php if ($presetdaystart == "06:30") echo "selected"; ?>>06:30</option> <option <?php if ($presetdaystart == "06:45") echo "selected"; ?>>06:45</option> <option <?php if ($presetdaystart == "07:00") echo "selected"; ?>>07:00</option> <option <?php if ($presetdaystart == "07:15") echo "selected"; ?>>07:15</option> <option <?php if ($presetdaystart == "07:30") echo "selected"; ?>>07:30</option> <option <?php if ($presetdaystart == "07:45") echo "selected"; ?>>07:45</option> <option <?php if ($presetdaystart == "08:00") echo "selected"; ?>>08:00</option> </select></td> </tr> <tr> <td><label for="nighttemp">Nachttemperatur</label></td> <td> <select name="nighttemp" style="width:75px"> <option <?php if ($presetnighttemp == "15.0") echo "selected"; ?>>15.0</option> <option <?php if ($presetnighttemp == "15.5") echo "selected"; ?>>15.5</option> <option <?php if ($presetnighttemp == "16.0") echo "selected"; ?>>16.0</option> <option <?php if ($presetnighttemp == "16.5") echo "selected"; ?>>16.5</option> <option <?php if ($presetnighttemp == "17.0") echo "selected"; ?>>17.0</option> <option <?php if ($presetnighttemp == "17.5") echo "selected"; ?>>17.5</option> <option <?php if ($presetnighttemp == "18.0") echo "selected"; ?>>18.0</option> <option <?php if ($presetnighttemp == "18.5") echo "selected"; ?>>18.5</option> <option <?php if ($presetnighttemp == "19.0") echo "selected"; ?>>19.0</option> <option <?php if ($presetnighttemp == "19.5") echo "selected"; ?>>19.5</option> <option <?php if ($presetnighttemp == "20.0") echo "selected"; ?>>20.0</option> <option <?php if ($presetnighttemp == "20.5") echo "selected"; ?>>20.5</option> <option <?php if ($presetnighttemp == "20.0") echo "selected"; ?>>20.0</option> </select></td> </tr> <tr><td>---------------------<br></td></tr> <tr> <td><label for="daytemp">Tagestemperatur</label></td> <td> <select name="daytemp" style="width:75px"> <option <?php if ($presetdaytemp == "15.0") echo "selected"; ?>>15.0</option> <option <?php if ($presetdaytemp == "15.5") echo "selected"; ?>>15.5</option> <option <?php if ($presetdaytemp == "16.0") echo "selected"; ?>>16.0</option> <option <?php if ($presetdaytemp == "16.5") echo "selected"; ?>>16.5</option> <option <?php if ($presetdaytemp == "17.0") echo "selected"; ?>>17.0</option> <option <?php if ($presetdaytemp == "17.5") echo "selected"; ?>>17.5</option> <option <?php if ($presetdaytemp == "18.0") echo "selected"; ?>>18.0</option> <option <?php if ($presetdaytemp == "18.5") echo "selected"; ?>>18.5</option> <option <?php if ($presetdaytemp == "19.0") echo "selected"; ?>>19.0</option> <option <?php if ($presetdaytemp == "19.5") echo "selected"; ?>>19.5</option> <option <?php if ($presetdaytemp == "20.0") echo "selected"; ?>>20.0</option> <option <?php if ($presetdaytemp == "20.5") echo "selected"; ?>>20.5</option> <option <?php if ($presetdaytemp == "21.0") echo "selected"; ?>>21.0</option> <option <?php if ($presetdaytemp == "21.5") echo "selected"; ?>>21.5</option> <option <?php if ($presetdaytemp == "22.0") echo "selected"; ?>>22.0</option> <option <?php if ($presetdaytemp == "22.5") echo "selected"; ?>>22.5</option> <option <?php if ($presetdaytemp == "23.0") echo "selected"; ?>>23.0</option> <option <?php if ($presetdaytemp == "23.5") echo "selected"; ?>>23.5</option> <option <?php if ($presetdaytemp == "24.0") echo "selected"; ?>>24.0</option> <option <?php if ($presetdaytemp == "24.5") echo "selected"; ?>>24.5</option> </select></td> </tr> <tr><td>---------------------<br></td></tr> </table> <br> <div align="left"> <input type="Submit" name="settemp" value="Einstellung speichern"> <br><br> <input type="Reset" <br><br> </div> <br> </form> <iframe src="log.php" height="200" width="300" name="Historie" frameborder="0"></iframe> </body> </html> |
Was passiert da?
Nach den Headerinformationen über Schriftart und so weiter folgt ein PHP Abschnitt mit einem kurzen Debug Snippet, das wenigstens die gröbsten Fehler auswirft.
Dann wird die Datei settings.dat geöffnet und ausgelesen. Ist diese nicht vorhanden, werden Default Werte genommen. Anschließend werden aktuelle Temperatur und aktueller Status ausgelesen.
Wenn ihr mit Raspbian Wheezy arbeitet, muss das Python Skript get_temperature.py als sudo aufgerufen werden. Das geht aber nicht so ohne weiteres, weil der Webserver-User www-data aus Sicherheitsgründen nicht als sudo arbeiten darf. Deshalb müsst ihr eine Ausnahme einstellen. Das geschieht mit dem Befehl
sudo visudo
.
Dort tragt ihr als Letztes folgende Zeile ein:
www-data ALL=(ALL) NOPASSWD: /home/pi/heizung/get_temperature.py
(natürlich müsst ihr den Pfad eintragen, der zu eurem Python Programm führt.)
Visudo ist nicht ohne. Wenn ihr hier einen Fehler macht, und diesen wegspeichert, kann es sein, dass ihr den Superuser getötet habt und so auch keinen sudo mehr verwenden könnt. Visudo ist aber so nett und warnt einen, dass die Datei so nicht funktionieren wird. In diesem Fall lieber abbrechen, kontrollieren und neu eingeben. Mit Strg-X speichert ihr eure Eingabe und kommt auf den Prompt zurück.
All das ist bei Raspbian Jessie nicht mehr nötig, der Sensor kann ohne sudo abgefragt werden..
Dann kommt der Formularbereich mit den Drop Down Feldern für Temperatur und Uhrzeit. Die vorbelegten bzw. ausgelesenen Parameter für Temperatur- und Nachtzeiteinstellung werden dabei als Voreinstellung übernommen. Das Ganze ist etwas mühsam und mit viel Schreibarbeit verbunden.
In das Skript ist noch ein iFrame zur Anzeige des Logs eingebettet, welches wir über das folgende Skript erzeugen:
log.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<html> <style type="text/css"> body { background-color: #A6A6A6; font-family:Verdana,Arial,Helvetica,sans-serif; font-size:12px } </style> <body> <script type="text/javascript"> window.onload=toBottom; function toBottom() { window.scrollTo(0, document.body.scrollHeight); } </script> <?php $loglist = shell_exec("tail -n250 /var/log/heizung.log"); $loglist=str_replace(chr(10),"<br>",$loglist); echo $loglist; ?> </body> </html> |
Im PHP Teil werden über den Shell Befehl tail die letzten 250 Zeilen des vom Python Programm erzeugte Logs eingelesen. Da normale Zeilenumbrüche chr(10) in html nicht funktionieren, werden sie noch über die Funktion str_replace in den html Zeilenumbruch <br> umgewandelt.
Über den Javascript Teil wird erreicht, dass im Iframe ganz nach unten gescrollt wird.
set_temp.php
Dieses Skript wird nach dem Klicken auf "Einstellungen speichern" erneut aufgerufen und speichert dann die in den Drop Down Feldern (auch Comboboxen genannt) eingestellten Werte in der Datei settings.dat. Diese Steuerdatei wird wiederum von der Python Routine verwendet um den Temperatur Soll-/Istvergleich vorzunehmen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>Raspberry Pi Heizungssteuerung Result Page</title> <style type="text/css"> body { background-color: #A6A6A6; font-family:Verdana,Arial,Helvetica,sans-serif; font-size:12px } </style> </head> <body> <?php if (isset($_GET["nightstart"])) //save parameters only if they have been set { $nightstart=$_GET["nightstart"]; $daystart=$_GET["daystart"]; $daytemp=$_GET["daytemp"]; $nighttemp=$_GET["nighttemp"]; $settings = $nightstart."\n".$daystart."\n".$daytemp."\n".$nighttemp; echo "Einstellung wird innerhalb 60 Sekunden übernommen"; $datei = fopen("settings.dat","w"); fwrite($datei, $settings); fclose($datei); } ?> </body> </html> |
Die Werte werden nur gespeichert, wenn der Parameter "nightstart" vom aufrufenden Programm gesetzt wurde. Beim ersten Aufruf der Website wird set_temp.php zwar auch geladen, nightstart ist jedoch leer und es findet deshalb keine Speicherung und keine Anzeige statt.
Das wars eigentlich schon. Ganz schön viel Holz für so eine kleine Anwendung!
Abschluss und Optimierung
Autostart
Das Python Programm – bei mir heißt es
lcd_menu_integrated.py
– muss jetzt noch beim Systemstart automatisch hochgefahren werden. Das geschieht über die Crontab welche ihr mit
sudo crontab -e
aufruft und dann am Ende folgende Zeile eintragt:
Mit Strg-X wird gespeichert und ihr verlasst den Editor.
Wie immer müsste ihr den korrekten Pfad und Skriptnamen verwenden.
SD Kartenverschleiß vermeiden
Da auch hier wieder laufend Flags und Statusinformationen (z.B. die Datei settings.dat) weggeschrieben werden, empfiehlt es sich diese in eine RAMdisk auszulagern., um einem vorzeitigen Tod der SD-Karte vorzubeugen. Wie das geht, steht in diesem Beitrag: SD-Karten Verschleiß vermeiden
Passwortschutz für das Webinterface
Wenn ihr bis hierher gekommen seid, liegt der Gedanke nahe, das Webinterface auch von unterwegs aufzurufen. Dazu müsst ihr euren Router so konfigurieren, dass eine von euch gewählte Portadresse auf den Pi umgeleitet wird. Wie das mit einer Fritzbox funktioniert, habe ich im Kapitel Portfreischaltung meines Artikels "Dynamische IP Adresse mit Bordmitteln" beschrieben. Weiter unten in demselben Artikel steht auch, wie man das Verzeichnis eures Webinterfaces mit einem Passwortschutz versieht. Ihr wollt ja sicher nicht, dass irgend welche bösen Buben eure Heizung manipulieren.
Eine weitere, sehr gute Möglichkeit einzelne Webseiten Verzeichnisse mit einem verschlüsselten Passort zu sichern, ist im nachfolgenden Link beschrieben: http://jacobsalmela.com/password-protect-a-lighttpd-web-server-on-a-raspberry-pi-using-mod-auth/
Viel Spaß und gutes Gelingen!