In meinen bisherigen Projekten habe ich für die Visualisierung der Daten bisher immer verschiedene lokale Libraries verwendet mit dem Effekt das diese entweder sehr kompliziert waren oder einfach nicht den Umfang anbieten den ich mir gewünscht habe. Diese Problemstellung hatte ich auch in meinem Temperatur Sensor Projekt.
Endlich habe ich eine passende Lösung für mich gefunden und zwar die Google Chart Tools, diese haben folgende Vorteile:
- Wir brauchen am Webserver des Raspberry Pi keine Libraries mehr, es reicht PHP und eine Datenbank als Datenquelle
- Die Formatierung der Daten wird bei den richtigen Datentypen von Google übernommen
- Es gibt etliche Diagramme, Vorlagen und Beispiele
- Die Charts sind interaktiv und fast unbegrenzt anpassbar
- Wir müssen nicht immer einzeln Tages, Wochen, Monats und Jahres Diagramme erstellen
Allerdings sei gesagt das man auch hier Zeit und Entwicklungserfahrung benötigt um die API zu verstehen und das gewünschte Ergebnis zu erzielen. Erwähnt sei natürlich noch die nicht unumstrittene Daten sammelnde Krake Google , es sollte aber jeder für sich selbst entscheiden welche Tools er verwenden. Bei diesen beiden Beispielen werden die Daten NICHT an Google übertragen, die Charts werden in eurem Browser generiert, selbst wenn Daten übertragen werden versichert Google diese direkt nach der Generierung zu löschen und nicht weiter zu verwenden.
Ausgabe als normales Liniendiagramm
Ausgabe als Diagram vom Typ AnnotatedTimeLine
Wie funktioniert das ganze?
Wie ich bereits erwähnt habe basieren die Diagramme auf den Google Charts. Wir sammeln auf unserem Raspberry Pi aus der Datenbank die benötigten Werte zusammen, in meinem Fall ist es eine Sqlite Datenbank, bringen Sie in das in der Google API definierte DataTable Format und generieren mit den passenden JavaScript Libraries im Browser (nicht auf dem Server) die Diagramme. Ich habe mich anstelle von JSON für das Data Table Format entschieden da mit diesem eine automatische Skalierung der X-Achse möglich ist.
Der Code zum generieren der Diagramme
Die passenden Informationen zum Setup des Webservers und zur Einrichtung des Perl Scripts zur Datenübermittlung findet ihr in meinem Raspberry Pi Projekt Funksensoren selbst bauen.
Die Dateien könnt ihr auch direkt herunterladen www-sensor.
index.php
Die index.php enthält neben den notwendigen SQL Abfragen und dem Auswahl Dialog den notwendigen Java Script Code zum generieren des Diagramm. Es wird eine Listbox mit möglichen Sensoren angezeigt für welche wir dann ein Diagramm generieren.
<?php //------------------- Config und Funktionen einbinden -------------------------- require_once("config.php"); require_once("functions.php"); //------------------------ PHP Settings ---------------------------------------- ini_set('track_errors', 1); ini_set('display_errors', 1); ini_set('log_errors', 1); ini_set("memory_limit","64M"); ini_set("max_execution_time","30"); @ob_implicit_flush(true); @ob_end_flush(); $_SELF=$_SERVER['PHP_SELF']; if (!file_exists($DBfile)) { echo "<center>Database missing!</center>\n"; _exit(); } $SelectedNodes = isset($_POST["nodeID"]) ? $_POST["nodeID"] : ""; //------------------------ Daten für Grafik holen ---------------------------------------- //Aus selektierter Node DataTable für Chart generieren if (!empty($SelectedNodes)) { $SQL="SELECT supplyV,temp,hum,datetime(time,'unixepoch') AS timestamp FROM werte WHERE nodeID='".$SelectedNodes."'"; $db = db_con($DBfile); $q = db_query($SQL); //Zeilen Header definieren, werden im Chart angezeigt $data = "var data = new google.visualization.DataTable();\n" ."data.addColumn('datetime', 'Timestamp');\n" ."data.addColumn('number', 'Volt');\n" ."data.addColumn('number', 'Feuchtigkeit');\n" ."data.addColumn('number', 'Temperatur');\n\n" ."data.addRows([\n"; while ($res = $q->fetch(PDO::FETCH_ASSOC)) { $temp = (int)$res['temp'] / 100; $hum = (int)$res['hum'] / 100; $pwr = (int)$res['supplyV'] / 1000; $timestamp = $res['timestamp']; $data = $data." [new Date('".$timestamp."'), ".$pwr.", ".$hum.", ".$temp."],\n"; } $data = $data."[new Date('2014-11-22 16:01:29'), 4.434, 59, 19.79]"; $data = $data."]);\n"; } ?> <!-- HTML & Java Script für das Chart --> <html> <head> <title>http://raspberry.tips - Temperatur Ausgabe</title> <link href="style.css" rel="stylesheet" type="text/css" /> <!-- Load jQuery --> <script language="javascript" type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script> <!-- Load Google JSAPI --> <script type="text/javascript" src="https://www.google.com/jsapi"></script> <script type="text/javascript"> google.load("visualization", "1", { packages: ["annotatedtimeline"] }); google.setOnLoadCallback(drawChart); function drawChart() { <!-- Generierte Data Table integieren --> <?php echo $data; ?> <!-- Chart Optionen --> var options = { title: 'Raspberry.Tips Daten', backgroundColor: {stroke:'black', fill:'#f2f2f2', strokeSize: 0}, displayZoomButtons: true, hAxis: { format: 'dd.MM. HH:mm' } }; <!-- Chart vom Typ AnnotatedTimeLine generieren--> var chart = new google.visualization.AnnotatedTimeLine(document.getElementById('chart_div')); chart.draw(data, options); } </script> </head> <body> <?php //----------------- Aktuelle Werte Ausgeben ------------------------------------ echo "<center>\n"; echo "<table border='0'>\n<tr>"; $db = db_con($DBfile); $q = db_query("SELECT temp,place FROM werte WHERE id IN (SELECT MAX(id) FROM werte GROUP BY nodeID)"); while ($res = $q->fetch(PDO::FETCH_ASSOC)) { $temp = $res['temp'] / 100; $arrayTemp = explode(".", $temp); echo "<td>\n<div align='center'>\n"; echo " <h3>".$res['place']."</h3>\n"; echo " <div class='container'>\n"; echo " <div class='de'>\n"; echo " <div class='den'>\n"; echo " <div class='dene'>\n"; echo " <div class='denem'>\n"; echo " <div class='deneme'>\n"; echo " ".$arrayTemp[0]."<span>".$arrayTemp[1]."</span>"; echo " </div>\n"; echo " </div>\n"; echo " </div>\n"; echo " </div>\n"; echo " </div>\n"; echo " </div>\n"; echo "</div>\n</td>\n" ; } unset($res); echo "</tr>\n</table>\n"; echo "</center>\n"; //----------------------------------------------------------------------------------- //----------------- Knoten zur Auswahl holen ------------------------------------ echo "<center>\n"; echo "<br/>\n"; echo "<br/>\n"; echo "<h3>Sensordaten anzeigen</h3>\n"; echo "<form action='' name='sensor' method='POST'>\n"; echo "<select name='nodeID'>"; $i=0; $s=" "; $MAXROW=10; $db = db_con($DBfile); $q = db_query("SELECT nodeID,place FROM werte WHERE 1 GROUP BY nodeID ORDER BY nodeID ASC"); while ($res = $q->fetch(PDO::FETCH_ASSOC)) { echo "<option value='".$res['nodeID']."'>".$res['place']."</option>'"; } unset($FoundChecked); unset($node_id); unset($res); echo "</select>\n<br><br>\n<input type='submit' value='Anzeigen'>\n</form>"; echo "</center>\n"; ?> <center> <!-- Chart DIV --> <div id="chart_div" style="width: 900px; height: 500px;"></div> </center> </body> </html>
functions.php
Das Functions Script enthält wiederverwendbare Funktionen, in meinem Fall die Methoden zur Abfrage der SQL-Lite Datenbank
<?php function _exit() { echo "</body>"; echo "</html>"; exit(); } // DB connect function db_con($DBfile) { if (!$db = new PDO("sqlite:$DBfile")) { $e="font-size:23px; text-align:left; color:firebrick; font-weight:bold;"; echo "<b style='".$e."'>Fehler beim öffnen der Datenbank:</b><br/>"; echo "<b style='".$e."'>".$db->errorInfo()."</b><br/>"; die; } $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); return $db; } // DB Query function db_query($sql) { global $db; $result = $db->query($sql) OR db_error($sql,$db->errorInfo()); return $result; } //Function to handle database errors function db_error($sql,$error) { die('<small><font color="#ff0000"><b>[DB ERROR]</b></font></small><br/><br/><font color="#800000"><b>'.$error.'</b><br/><br/>'.$sql.'</font>'); } ?>
config.php
Dieses PHP Script definiert globale Variablen wie zum Beispiel den Pfad zur SQLite Datenbank. Der Pfad zur Datenbankdatei muss sicherlich auf eure Umgebung angepasst werden. Der Security Key und die Namensdefinitionen werden im Script add.php verwendet welches als Schnittelle zwischen Sensor und Datenbank fungiert und die Temperaturwerte in die Datenbank einträgt.
<?php //------------------------------------------------------------------------------ $DBfile = "/var/www/Sensor/.db.sensors"; $SECURITYKEY = "23338d373027ce83b1f81b9e9563b629"; //------------------------------------------------------------------------------ // $Sensor[<nodeID>] = "<Place>"; $Sensor['19'] = "Wohnzimmer"; $Sensor['20'] = "Garage / Schuppen"; $Sensor['21'] = "Garten"; ?>
add.php
Dieses Script Bilded die Schnittstelle zwischen der UART Ausgabe der Temperatur Sensoren am Raspberry Pi und dem Webserver bzw. der Datenbank für den Fall das ihr den Webserver und die Datenbank nicht direkt bzw. auf dem selben RasPi speichern möchtet. Gefüttert wird die Schnittstelle von einem Perl Script welches auf der UART Schnittelle alle vom Empfänger übertragenen Temperaturen, Spannungen und Daten zur Luftfeuchtigkeit an dieses Script versendet. Details und das Perl Script findet ihr im ursprünglichen Artikel.
<?php //------------------------------------------------------------------------------ error_reporting(E_ALL); ini_set('track_errors', 1); ini_set('display_errors', 1); ini_set('log_errors', 1); //------------------------------------------------------------------------------ require_once("config.php"); require_once("functions.php"); //------------------------------------------------------------------------------ // check if sqlite db file exists else create it.. if (!file_exists($DBfile)) { $db = db_con($DBfile); $SQL = "CREATE TABLE IF NOT EXISTS werte (id INTEGER PRIMARY KEY,time INT,nodeID INT,place TEXT,supplyV TEXT,temp TEXT,hum TEXT)"; $create = db_query($SQL); } if (!empty($_GET)) { $ValidKey = false; foreach ($_GET AS $arg => $var) { if ($arg == "key" AND $var == $SECURITYKEY) { $ValidKey=true; } if ($arg == "node") { $nodeID = $var; } if ($arg == "v") { $supplyV = $var; } if ($arg == "t") { $temp = $var; } if ($arg == "h") { $hum = $var; } } if (!$ValidKey) { echo "Invalid Key!"; exit(); } if ( isset($nodeID) AND isset($supplyV) AND (isset($temp) OR isset($hum)) ) { if (!isset($hum)) { $SQL = "INSERT INTO werte (time,nodeID,place,supplyV,temp) VALUES ('".time()."','".$nodeID."','".$Sensor[$nodeID]."','".$supplyV."','".$temp."')"; } else { $SQL = "INSERT INTO werte (time,nodeID,place,supplyV,temp,hum) VALUES ('".time()."','".$nodeID."','".$Sensor[$nodeID]."','".$supplyV."','".$temp."','".$hum."')"; } $db = db_con($DBfile); $insert = db_query($SQL); } } ?>
style.css
Styles für die Seite als extra css
@import url(http://fonts.googleapis.com/css?family=Dosis:200,400,500,600); html, body { height: 100%; } body { background: #f2f2f2; font-size: 8pt; color: black; font-family: Verdana,arial,helvetica,serif; margin: 0 0 0 0; } .style1 { color: #999999; font-weight: bold; } .container { width: 200px; margin: 10px auto 0; } .de .den, .de .dene, .de .denem, .de .deneme { position: absolute; left: 50%; top: 50%; } .de { position: relative; width: 240px; height: 240px; border-radius: 100%; box-shadow: 0 0 10px rgba(0, 0, 0, .1); background-color: transparent; } .den { position: relative; width: 210px; height: 210px; margin: -105px 0 0 -105px; border-radius: 100%; box-shadow: inset 0 2px 10px rgba(0, 0, 0, .5), 0 2px 20px rgba(255, 255, 255, 1); background: #df3341; background: -moz-linear-gradient(left, #df3341 0%, #d4f355 50%, #61c0ec 100%); background: -webkit-gradient(linear, left top, right top, color-stop(0%,#df3341), color-stop(50%,#d4f355), color-stop(100%,#61c0ec)); background: -webkit-linear-gradient(left, #df3341 0%,#d4f355 50%,#61c0ec 100%); background: linear-gradient(to right, #df3341 0%,#d4f355 50%,#61c0ec 100%); position:relative; } .dene { width: 180px; height: 180px; margin: -90px 0 0 -90px; border-radius: 100%; box-shadow: inset 0 2px 2px rgba(255, 255, 255, .4), 0 3px 13px rgba(0, 0, 0, .85); background: #f2f6f5; background: -moz-linear-gradient(top, #f2f6f5 0%, #cbd5d6 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #f2f6f5), color-stop(100%, #cbd5d6)); background: -webkit-linear-gradient(top, #f2f6f5 0%, #cbd5d6 100%); background: -o-linear-gradient(top, #f2f6f5 0%, #cbd5d6 100%); } .denem { width: 160px; height: 160px; margin: -80px 0 0 -80px; border-radius: 100%; background: #cbd5d6; background: -moz-linear-gradient(top, #cbd5d6 0%, #f2f6f5 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #cbd5d6), color-stop(100%, #f2f6f5)); background: -webkit-linear-gradient(top, #cbd5d6 0%, #f2f6f5 100%); } .deneme { padding: 3px 10px 0 10px; width: 120px; height: 137px; display: inline-block; margin: -70px 0 0 -70px; color: #555; text-shadow: 1px 1px 1px white; font-family: 'Dosis'; font-size: 100px; font-weight: 400; text-align: center; } .deneme span { font-size: 30px; font-weight: 200; } .deneme strong { position: absolute; right: 10px; top: 25px; font-size: 34px; }