Optimierung der Sensorwerteverwaltung


21.02.2017  |  Smarthome Server, Tutorial

In zwei der früheren Tutorials habe ich dir gezeigt, wie du Sensorwerte in der Datenbank speichern und archivierte Sensorwerte abfragen kannst. Vor ein paar Tagen ist mir bei den Vorbereitungen für ein kommendes Tutorial der Smarthome App ein Problem mit dem Skript "getGraphData.php" aufgefallen, bei dem das Skript "getGraphData.php" für die Abfrage von größeren Zeiträumen unverhältnismäßig lange braucht oder sogar abbricht. Daher und weil das Smarthome-System etwas umstrukturiert wird, um die Integration von verschiedenen Komponenten zu ermöglichen, werden im Folgenden die Skripte "getSensorData.php", "getGraphData.php", "getRoomData.php", "api.php" und "saveSensorData.php" ein wenig angepasst. Im Beitrag "Struktur der Datenbank optimieren" wurde bereits die Tabelle "SENSOR_DATA" angepasst und geleert. Jetzt müssen nur noch die Skripte an die neue Struktur angepasst werden. Dazu verbindest du dich per FileZilla mit deinem Pi und öffnest die Datei "saveSensorData.py" im Ordner "python". Als erstes wird die Methode "getData()" entfernt, da am Ende des Skriptes für jeden einzelnen Sensortypen die Sensorwerte abgefragt werden. Anschließend wird am Ende des Skriptes die Schleife bearbeitet. In der Methode "saveSensorData()" werden die Parameter der HTTP-Request bearbeitet (Zeile 40), damit sie zur Systemstruktur passen. Dazu wird der Parameter "room" mit "type" und der Parameter "sensor" mit "id" ersetzt, da zur Identifizierung eines Sensors nun nicht der Raum und die Sensorart, sondern der Typ und die ID des Sensors verwendet werden. Die Zeile "uhrzeit = now.strftime("%d.%m.%y %H:%M")" wird in "uhrzeit = now.strftime("%Y-%m-%d %H:%M")" (Zeile 49) geändert, damit SQLite besser mit dem Datumsformat umgehen kann und die SQL-Abfrage auf die neue Tabellenstruktur angepasst (Zeile 53).

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sqlite3 as lite		#Für die Datenbankverbindung
import datetime				#Um Datum und Uhrzeit abzufragen
import urllib				#Um die URL aufzurufen
import httplib				#Um die URL aufzurufen
import sys					#Um Kommandozeilenargumente zu lesen

#Um die IP-Adresse zu ermitteln
import socket
import fcntl
import struct

now = datetime.datetime.now()

con = lite.connect('/var/www/html/database/data.sqlite')

#Nutzerdaten, da die API im weiteren Verlauf der Tutorials noch geschützt wird
username = 'client'
password = 'clientpassword'

#IP-Adresse des Servers feststellen und zurückgeben
#Parameter: ifname - 'wlan0' falls per WLAN verbunden und 'eth0' falls per LAN verbunden
def get_ip_address(ifname):
	s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
	return socket.inet_ntoa(fcntl.ioctl(
	s.fileno(),
	0x8915,
	struct.pack('256s', ifname[:15])
	)[20:24])
		
#Sensorwerte abfragen und Wert in Datenbank schreiben
#Parameter:
	#room: Raum, in dem der Wert gesucht werden soll
	#sensor: Art des zu suchenden Sensors
def saveSensorData(room, type, post_id, ip):
	#entsprechenden Sensorwert abfragen
	
	params = urllib.urlencode({'action': 'getsensordata', 'room': room, 'type': type, 'id': post_id, 'username': username, 'password': password})
	headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain"}
	conn = httplib.HTTPConnection(ip)
	conn.request("POST", "/api.php", params, headers)
	response = conn.getresponse()
	sensorwert = response.read()
	conn.close
	
	#Datum und Uhrzeit ermitteln
	uhrzeit = now.strftime("%Y-%m-%d %H:%M") #Datum und Uhrzeit im Format JJJJ-MM-TT HH:MM
	
	#Daten in Datenbank schreiben
	cur = con.cursor()
	cur.execute("INSERT INTO SENSOR_DATA (DEVICE_ID, DEVICE_TYPE, DATETIME, VALUE) VALUES ('"+id+"', '"+type+"', '"+uhrzeit+"', '"+sensorwert+"')")
	con.commit()

#Hier wurde die Schleife angepasst
with con:
	cur = con.cursor()
	cur.execute("SELECT * FROM ZWAVE_SENSOREN")
	
	for data in cur.fetchall():
		if(data[4] == "true"): #data[4] legt fest, ob der Sensorwerteverlauf dieses Sensors gepseichert werden soll
			saveSensorData(data[0], "Z-Wave Sensor", data[2], get_ip_address(sys.argv[1])) #data[0] enthält den KEY des Raumes und data[2] die ID des Sensors
Nachdem du die Änderungen vorgenommen hast, speicherst du das Skript ab und lädst die Änderungen auf den Server hoch.

Optimierung von getSensorData.php

Auch "getSensorData.php" muss an die neue Struktur angepasst werden. Als erstes werden die Methodenparameter angepasst (Zeile 3). Wenn die Variable "room" leer ist, wird "error" ausgegegeben (Zeile 6). Eine if-Abfrage prüft, ob "type" und "id" einen Wert haben und unterscheidet so zwischen den beiden Teilen der Methode (Zeile 8). Anschließend wird noch die Integration neuer Sensortypen erleichtert. Dazu wurden die zwei Teile der Methode (Einzelabfrage eines Wertes und Abfrage der Werte eine bestimmten Raumes bzw. aller Räume) getrennt. Im Teil der Einzelabfrage eines Wertes wird nun mit einem switch-Block zwischen den verschiedenen Sensortypen unterschieden (Zeile 10). So kann für jeden Sensortypen eine eigene Art der Wertabfrage implementiert werden. Das System wird in Zukunft um weitere Sensortypen erweitert. Dazu zählen auch selbstgebaute Sensoren (z.B. mit dem NodeMcu ESP8266). Bei der Sammel-Abfrage aller Sensoren eines Raumes bzw. jeden Raumes wird für jeden Sensortypen eine eigene Schleife angelegt, die die Tabelle dieses Sensortyps durchläuft und die Sensordaten in das Ausgabearray schreibt (Zeile 76 - 82). Somit kann auch die Sammelabfrage leicht um neue Sensortypen erweitert werden (Zeile 84 & 85). Ersetze den vorhandenen Code mit dem folgenden:
<?php

function getSensorData($room, $type, $id, $show_einheit, $db){
	$server_ip = $_SERVER['SERVER_ADDR'];
	
	if($room === "") return "error";
	
	if($type !== "" && $id !== ""){
		switch($type){
			case "Z-Wave Sensor":
				if(!hasPermission($room, $db)){
					return "nopermission";
				}
				
				if($id !== null){
					//Z-Wave API aufrufen
					$link = "http://".$server_ip."/ZAutomation/api/v1/devices/".$id;
					
					//curl mit URL initialisieren
					$curl = curl_init($link);
					
					//Port setzen
					curl_setopt($curl, CURLOPT_PORT, 8083);
					
					//Ausgabe einstellen
					curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
					
					//Abfrage ausf&uuml;hren
					$result = curl_exec($curl);
					
					//Wert je nach Wunsch mit/ohne Einheit ausgeben
					$array = json_decode($result, true);
					
					$return = $array['data']['metrics']['level'];
					
					if($show_einheit == "1"){
						$return = $return." ".$array['data']['metrics']['scaleTitle'];
					}
					
					return $return;
				}
				else return "N/A";
			//Case f&uuml;r weitere Sensorarten
			//...
		}
	}
	else{
		//R&auml;ume laden
		if($room == "all"){
			$results = $db->prepare("SELECT * FROM 'ROOMS'");
			$results->execute();
		}
		else{
			$results = $db->prepare("SELECT * FROM 'ROOMS' WHERE LOCATION == :room");
			$results->execute(array('room' => $room));
		}
		
		//ausgabearray erzeugen
		$values = array();
		
		//Alle R&auml;ume durchlaufen
		foreach($results->fetchAll(PDO::FETCH_ASSOC) as $row){
			//R&auml;ume &uuml;berspringen, zu denen der Nutzer keine Berechtigung hat
			if(!hasPermission($room, $db)){
				continue;
			}
		
			//Wertearray erzeugen
			$value_array = array();
			
			//Alle Z-Wave Sensoren im aktuellen Raum laden
			$type = "Z-Wave Sensor";
			$ergebnisse = $db->prepare("SELECT * FROM 'ZWAVE_SENSOREN' WHERE RAUM == :location");
			$ergebnisse->execute(array('location' => $row['LOCATION']));
			
			//Alle Z-Wave Sensoren im Raum durchlaufen
			foreach($ergebnisse->fetchAll(PDO::FETCH_ASSOC) as $reihe){
				//Wert f&uuml;r jeden Sensor zusammen mit Sensorart in Wertearray schreiben
				$value = array('shortform'=> $reihe['SHORTFORM'], 'id' => $reihe['ID'], 'type' => $type, "icon" => $reihe['ICON'],
				'wert' => getSensorData($row['LOCATION'], $type, $reihe['ID'], 1, $db));
				array_push($value_array, $value);
			}
			
			//Abfrage f&uuml;r weitere Sensorarten
			//...
			
			//Daten f&uuml;r aktuellen Raum in Ausgabearray schreiben
			$value_item = array('name' => $row['NAME'], 'location' => $row['LOCATION'], 'value_array' => $value_array);
			array_push($values, $value_item);
		}
		
		//JSON-Objekt zur&uuml;ckgeben
		return json_encode(array('values' => $values));
	}
}

?>
Anschließend speicherst du die Änderungen und lädst sie auf den Server hoch.

getRoomData.php optimieren

Damit auch die Ausgabe der Raumdaten weiterhin funktioniert, wird auch die Datei "getRoomData.php" etwas bearbeitet. Dazu wird die Schleife, in der die Sensordaten in das Ausgabearray geschrieben werden, angepasst (Zeilen 23-26).
<?php
 
function getRoomData($room, $db){
	
	if(!hasPermission($room, $db)){
		return "nopermission";
	}
	
    $roomdata = array();
     
    $switchdata = json_decode(getModes($room, "", $db), true);
    foreach($switchdata['modi'] as $switch){
        if($switch['mode'] === "1"){
            $mode = true;
        }
        else{
            $mode = false;
        }
        array_push($roomdata, array('name' => $switch['name'], 'device' => $switch['device'], 'icon' => $switch['icon'], 'type' => "switch", 'value' => ($mode) ? 'true' : 'false'));
    }
     
    $sensordata = json_decode(getSensorData($room, "", "", "", $db), true);
    foreach($sensordata['values'][0]['value_array'] as $sensor){
        array_push($roomdata, array('name' => $sensor['shortform'], 'device' => $sensor['id'], 'type' => $sensor['type'],
		'icon' => $sensor['icon'], 'type' => "value", 'device_type' => $sensor['type'], 'value' => $sensor['wert']));
    }
     
    $scenedata = json_decode(getScenes($room, $db), true);
    if(sizeOf($scenedata['scenes']) > 0){
        array_push($roomdata, array('name' => "Szenen", 'device' => "scenes", 'icon' => "scenes", 'type' => "scenes", 'value' => ""));
    }
     
    //Heizungs-Item implementieren
     
    return json_encode(array('roomdata' => $roomdata));
}
 
?>

Optimierung von getGraphData.php

Als vorletztes wird das Skript zur Abfrage der Graph-Daten angepasst. Dabei werden nicht die Daten jedes einzelnen Tages abgefragt und dann die Extremwerte gesucht, sondern die gesamte Abfrage mit SQL realisiert. Das reduziert den Code und die Laufzeit um einiges. Dazu öffnest du die Datei "getGraphData.php" und ersetzt den vorhandenen Code mit dem folgenden Code. Dort wurden die Parameter der Funktionen angepasst (Zeile 3, 20, 40 und 56) und außer Anpassungen an der SQL-Abfrage der Sensorwerte (Zeilen 27-29) wurde die Methode getDayData() entfernt, die Methode getEinheit() hinzugefügt und die Funktionen an die neue Datenbankstruktur angepasst.
<?php
 
function getGraphData($type, $id, $von, $bis, $db){
     
    if(!hasPermission($type, $db)){
        return "nopermission";
    }
     
    if($von == $bis){
        //Datum formattieren
        $datum = date("Y-m-d", strtotime($von));
         
        return getDayData($type, $id, $datum, $db);
    }
    else{
        return getDayMinMax($type, $id, $von, $bis, $db);
    }
}
 
function getDayMinMax($type, $id, $start, $ende, $db){   
    $start = date("Y-m-d", strtotime($start));
	
	$ende = date("Y-m-d", strtotime($ende));
     
    $values = array();
     
    $query = $db->query("SELECT MIN(VALUE) as MIN, MAX(VALUE) as MAX, strftime(\"%d.%m.%Y\", DATETIME) as DATE FROM SENSOR_DATA WHERE DEVICE_TYPE == :type
	AND DEVICE_ID == :id AND DATETIME >= :start AND DATETIME <= :ende GROUP BY DATE ORDER BY DATETIME ASC");
	$query->execute(array('type' => $type, 'id' => $id, 'start' => "$start 00:00", 'ende' => "$ende 23:59"));
	
	print_r(array('type' => $type, 'id' => $id, 'start' => "$start 00:00", 'ende' => "$ende 23:59"));
	
	foreach($query->fetchAll(PDO::FETCH_ASSOC) as $row){
		array_push($values, array('date' => $row['DATE'], 'min' => $row['MIN'], 'max' => $row['MAX']));
	}
	
    return json_encode(array('values' => $values, 'einheit' => getEinheit($type, $id, $db)));
}

function getEinheit($type, $id, $db){
	switch($type){
		case "Z-Wave Sensor":
			$query = $db->query("SELECT * FROM 'ZWAVE_SENSOREN' WHERE ID == :id");
			$query->execute(array('id' => $id));
			break;
	}
     
    if($result = $query->fetch(PDO::FETCH_ASSOC)){
        return $result['EINHEIT'];
    }
	else{
		return "";
	}
}

function getDayData($type, $id, $datum, $db){    
    $values = array();
     
    $query = $db->query("SELECT * FROM 'SENSOR_DATA' WHERE DEVICE_TYPE == :type AND DEVICE_ID == :id AND DATETIME >= :start AND DATETIME < :ende ORDER BY DATETIME ASC");
    $query->execute(array('type' => $type, 'id' => $id, 'start' => $datum." 00:00", 'ende'=> $datum." 23:59"));
     
    foreach($query->fetchAll(PDO::FETCH_ASSOC) as $row){
        $value_item = array('value' => floatval($row['VALUE']), 'time' => str_replace($datum." ", "", $row['DATETIME']));
        array_push($values, $value_item);
    }
    
	switch($type){
		case "Z-Wave Sensor":
			$query = $db->query("SELECT * FROM 'ZWAVE_SENSOREN' WHERE ID == :id");
			$query->execute(array('id' => $id));
			break;
	}
     
    if($result = $query->fetch(PDO::FETCH_ASSOC)){
        $einheit = $result['EINHEIT'];
    }
     
    return json_encode(array('values' => $values, 'einheit' => getEinheit($type, $id, $db)));
}
 
?>
Anschließend speicherst du die Datei und lädst die Änderungen auf den Server hoch.

Schnittstelle anpassen

Da in diesem Beitrag einige Änderungen an den Funktionen vorgenommen wurden, muss folglich auch die Schnittstelle an diese Änderungen angepasst werden. Öffne dazu die Datei "api.php". Die betroffenen Stellen sind die Zeilen 49 - 52 und die Zeilen 62 - 65. Dort wurden lediglich die Parameter der aufgerufenen Funktionen angepasst.
<?php
//Datenbankverbindung herstellen
$SQLITEdb = "database/data.sqlite";
$db = new PDO("sqlite:".$SQLITEdb);

//Funksteckdosen
include "getModes.php";
include "setModes.php";
  
//Andere
include "getRooms.php";
include "getSensorData.php";
include "getSystemInfo.php";
include "getRoomData.php";
include "getGraphData.php";
include "events.php";
include "permissions.php";

//Szenen
include "scenes/getScenes.php";
include "scenes/runScene.php";
include "scenes/createScene.php";

$validUser = validateUser($_POST['username'], $_POST['password'], $db);
 
if($validUser){
    switch($_POST['action']){
        case "getrooms":
            echo getRooms($db);
            break;
		case "getevents":
			echo getEvents($_POST['username'], $_POST['type'], $_POST['limit'], $_POST['offset'], $db);
			break;
		case "geteventtypes":
			echo getEventTypes($db);
			break;
		case "addevent":
			echo addEvent($_POST['type'], $_POST['text'], $db);
			break;
		case "getunseenevents":
			echo getUnseenEvents($_POST['username'], $db);
			break;
		case "getpermissions":
			echo getPermissions($_POST['user'], $db);
			break;
		case "setpermissions":
			echo setPermissions($_POST['user'], $_POST['permissions'], $db);
			break;
		//Dieser Block wurde bearbeitet
        case "getgraphdata":
            echo getGraphData($_POST['type'], $_POST['id'], $_POST['von'], $_POST['bis'], $db);
            break;
        case "getroomdata":
            echo getRoomData($_POST['room'], $db);
            break;
        case "getmodes":
            echo getModes($_POST['room'], $_POST['device'], $db);
            break;
        case "setmodes":
            echo setModes($_POST['room'], $_POST['device'], $_POST['zustand'], $db);
            break;
		//Dieser Block wurde auch bearbeitet
        case "getsensordata":
            echo getSensorData($_POST['room'], $_POST['type'], $_POST['id'], $_POST['showeinheit'], $db);
            break;
        case "runscene":
            echo runScene($_POST['room'], $_POST['name'], $db);
            break;
        case "createscene":
            echo createScene($_POST['devices'], $_POST['rooms'], $_POST['types'], $_POST['values'], $_POST['conditions'], $_POST['room'], $_POST['name'], $db);
            break;
        case "getscenes":
            echo getScenes($_POST['room'], $db);
            break;
        case "getsysteminfo":
            echo getSystemInfo();
            break;
    }
}

function validateUser($username, $password, $db){
    //wird noch implementiert
      
    return true;
}

function hasPermission($action, $db){
	$permissions = getPermissions($_POST['username'], $db);
	$permissions = json_decode($permissions, true)['permissions'];
	
	return (in_array($action, $permissions) || in_array("admin", $permissions));
}

?>
Speichere auch dieses Skript und lade die Änderungen hoch. Im nächsten Tutorial wird auch die Steuerung der Funksteckdosen an die neue Datenbankstruktur angepasst. Bei Fragen oder Probleme kannst du mir gerne einen Kommentar hinterlassen.

Über den Autor


Sascha Huber

Hallo, ich bin Sascha, der Gründer von Smarthome Blogger.

Mit einer Leidenschaft für Technologie und einem Hintergrund als Software Engineer habe ich 2016 Smarthome Blogger gegründet. Mein Ziel war es schon immer, innovative Lösungen zu entdecken, die unser Leben einfacher und intelligenter gestalten können. In meinem beruflichen Leben arbeite ich täglich mit Software und Technik, aber auch in meiner Freizeit bin ich stets auf der Suche nach neuen technischen Spielereien und Möglichkeiten, mein Zuhause zu automatisieren und zu verbessern.

Auf Smarthome Blogger teile ich mein Wissen, meine Erfahrungen und meine Begeisterung für alles rund um das Thema Smarthome.



Dieser Beitrag hat dir gefallen?

Dann abonniere doch unseren Newsletter!