Tutorial: Zugriff auf Querx Messdaten mit Python

Die Querx Familie unterstützt seit Firmwareversion 2 den Export der gespeicherten Messdaten in Form von XML- und CSV-Dateien. Seit Version 2.1.2 können auch die gegenwärtigen Messwerte direkt im XML-Format abgefragt werden und so z.B. mit Hilfe von Scriptsprachen in eigene Projekte eingebunden werden.

In diesem Tutorial wird beispielhaft das Auslesen und Ausgeben der aktuellen Messwerte Ihres Geräts in Python 2.7 vorgestellt.

Querx Gerätevarianten

Dabei wird das Programm so auslegt, dass es mit allen Querx Varianten (TH, THP, PT - sowohl ohne als auch mit WLAN) funktioniert. Mehr dazu im Abschnitt: main()-Funktion.

Vorbereitung

Zunächst benötigen wir die IP-Adresse des Querx, mit dem wir kommunizieren wollen.

Die IP-Adresse Ihres Querx, können Sie beispielsweise über unser Tool Device Discoverer herausfinden.

Wenn Sie nun

http://[IP-Adresse]/tpl/document.cgi?tpl/j/live.tpl&msub=xml

in Ihrem Browser aufrufen, sollten Sie eine Antwort in folgender Form zurückerhalten, hier beispielhaft von einem Querx TH:

<querx ver="1.0">
  <live>
    <hostname>Tutorial-Querx TH</hostname>
    <ip>192.168.192.206</ip>
    <port>80</port>
    <date_local>06.01.2017</date_local>
    <time_local>16:02:20</time_local>
    <date_gmt>06.01.2017</date_gmt>
    <time_gmt>15:02:20</time_gmt>
    <timestamp>1483714940</timestamp>
    <temperature>
      <name>Temperature</name>
      <unit>C</unit>
      <value>24.9</value>
    </temperature>
    <humidity>
      <name>Humidity</name>
      <unit>%RH</unit>
      <value>17</value>
    </humidity>
    <dewpoint>
      <name>Dew point</name>
      <unit>C</unit>
      <value>-1.9</value>
    </dewpoint>
  </live>
</querx>

Dieser Textblock enthält sowohl die aktuellen Messdaten des Geräts, als auch einige nützliche Metadaten, wie die Uhrzeit zur Messung.

XML ist ein Format, in dem solche hierarchisch strukturierten Daten maschinenverständlich und relativ effizient ausgedrückt und ausgetauscht werden können.
Die an HTML-Tags erinnernden Objekte werden XML-Knoten (engl. nodes) genannt. Wie am Beispiel vom 'live'-Knoten zu sehen ist, können diese Knoten Unterknoten enthalten, um hierarchische Beziehungen zu modellieren.

Weitergehende Informationen unter: de.wikipedia.org/wiki/Extensible_Markup_Language

Da diese XML-Daten von einem Querx TH stammen, enthalten sie die Messwert-Knoten 'temperature' und 'humidity', die jeweils Unterknoten für Sensornamen, Einheit und den eigentlichen Messwert beinhalten. Die Sensornamen und Einheiten können Sie übrigens im Konfigurationsbereich Ihres Querx an Ihre Wünsche anpassen.

Im weiteren Verlauf des Tutorials gilt es, diese XML-Daten automatisiert vom Gerät abzurufen und zu interpretieren.

Fertiges Script

import sys
import httplib # Python 3.x: httplib => http.client
import xml.etree.ElementTree as ET

querx_address = "192.168.192.206"
querx_http_port = 80
xml_url = "/tpl/document.cgi?tpl/j/live.tpl&msub=xml"

def get_xml(address, port, url):
  try:
conn = httplib.HTTPConnection(address, port) # Python 3.x: httplib => http.client
conn.request("GET", url)
res = conn.getresponse()
xml = res.read()
return(xml) except Exception as e: print ("Error: " + str(e)) sys.exit(1) def get_value_unit(node): #value in node 'value' value = node.findtext('value') #unit in node 'unit' unit = node.findtext('unit') return (value, unit) def get_temperature(live): #temperature node: 'temperature' temp = live.find('temperature') return get_value_unit(temp) def get_humidity(live): #humidity node: 'humidity' humidity = live.find('humidity') return get_value_unit(humidity) def get_pressure(live): #pressure node: 'pressure' pressure = live.find('pressure') return get_value_unit(pressure) def main(): xml = get_xml(querx_address, querx_http_port, xml_url) print (xml) #root node: 'querx' root = ET.fromstring(xml) #sub node 'live' live = root.find('live') if live.find('temperature') != None: tvalue, tunit = get_temperature(live) print ("Temperature: " + tvalue + " " + tunit) if live.find('humidity') != None: hvalue, hunit = get_humidity(live) print ("Humidity: " + hvalue + " " + hunit) if live.find('pressure') != None: pvalue, punit = get_humidity(live) print ("Pressure: " + pvalue + " " + punit) main()

Info: Dieses Codebeispiel ist für Python 2.x geschrieben. Um es für Python 3.x lauffähig zu machen, müssen allerdings lediglich in den Zeilen 2 und 11 die Ausdrücke 'httplib' durch 'http.client' ersetzt werden.

Quelltext - Schritt für Schritt

Imports

Zunächst importieren wir einige Module aus der Python Standard Bibliothek, die wir benutzen werden.
  import sys
Das Modul 'sys' gibt uns Zugriff auf interpreternahe Funktionen. Es wird gebraucht, um im Fehlerfall das Programm explizit zu beenden.
  import httplib
'httplib' stellt eine simple Schnittstelle zum Zugriff auf HTTP-Ressourcen bereit. Wir werden mit Hilfe dieses Moduls die Messwerte vom HTTP-Server, den der Querx bereitstellt, anfragen und entgegennehmen. (Python 3.x: httplib => http.client, s.o.)
  import xml.etree.ElementTree as ET
Das Modul 'ElementTree' ist ein XML-Parser, mit dem wir den XML-String, den wir vom Querx HTTP-Server erhalten, in eine Baumstruktur umwandeln, aus der wir bequem die einzelnen Datenfelder herauslesen können. Durch 'as ET' steht uns das Modul im Folgenden kurz als 'ET' zur Verfügung.

Konfigurationsvariablen

  querx_address = "192.168.192.206"
querx_http_port = 80
xml_url = "/tpl/document.cgi?tpl/j/live.tpl&msub=xml"

Hier definieren wir die IP-Adresse, den TCP-Port (80 im Werkszustand) und den URL-Pfad der HTTP-Ressource, die uns die aktuellen Messwerte in XML-Form bereitstellt.

Es folgen eine Reihe von Hilfsfunktionen, welche jeweils eine bestimmte Funktionalität kapseln, um sie letztendlich in der main()-Funktion zusammenzuführen.

get_xml

def get_xml(address, port, url):
  try:
    conn = httplib.HTTPConnection(address, port)
    conn.request("GET", url)
    res = conn.getresponse()
    xml = res.read()
    return(xml)
  except Exception as e:
    print ("Error: " + str(e))
    sys.exit(1)

Wir definieren eine Funktion 'get_xml', die als Eingabeparameter die oben genannten Konfigurationsvariablen entgegennimmt.

Der Funktionskörper besteht aus einem try/except-Block, der sicherstellt, dass eventuelle zur Laufzeit auftretende Fehler im 'try'-Teil nicht zum Absturz, sondern zu einem im 'except'-Teil wohldefinierten Verhalten führen.

Zunächst wird mit der Funktion 'HTTPConnection' des 'httplib'-Moduls ein HTTP-Verbindungsobjekt zurückgegeben und in die Variable 'conn' geladen.
Anschließend wird mit '.request' eine HTTP-Anfrage an Querx über diese Verbindung getätigt, wobei mit dem Parameter 'GET' die HTTP-Methode bestimmt und durch 'url' der übergebene URL-Pfad weitergegeben wird.
Im Fehlerfall ('except'-Teil) wird der verursachende Fehler ausgegeben und mit der 'exit'-Methode des 'sys'-Moduls das Programm beendet.

get_value_unit

def get_value_unit(node):
  #value in node 'value'
  value = node.findtext('value')
  #unit in node 'unit'
  unit = node.findtext('unit')
  return (value, unit)

Als nächstes definieren wir eine Funktion 'get_value_unit'. Ihre Aufgabe ist es, aus dem Eingabeparameter 'node' den Rückgabewert (value, unit) zu erzeugen. 'node' steht in diesem Fall für einen einzelnen XML-Knoten, also ein bestimmtes Objekt der vom XML-Parser generierten Baumstruktur.

Die vom 'node'-Objekt bereitgestellte Methode 'findtext("value")', durchsucht den Knoten auf Unterknoten mit der Bezeichnung 'value' und gibt den Textinhalt des *ersten* gefundenen Unterknotens zurück.
Damit ist diese Funktion in der Lage Variablenpaare mit Wert und Einheit aus einem Knoten zu extrahieren, und zwar unabhängig davon, ob 'node' die Temperatur, die Luftfeuchtigkeit oder den Luftdruck beschreibt.

Beispiel:

<temperature>
  <name>Temperature</name>
  <unit>°C</unit>
  <value>20.3</value>
</temperature>

Wird dieser XML-Schnipsel in eine Baumstruktur geparst und als Argument 'node' an die Funktion get_value_unit(node) übergeben, gibt der Befehl 'node.findtext('value')' den String "20.3" zurück.

Die beiden Variablen 'value' und 'unit' werden mit den entprechenden Rückgabewerten der 'findtext'-Funktionsaufrufe gefüllt.

Der Rückgabewert von get_value_unit() ist das Tupel '(value, unit)', also eine aus diesen beiden Variablen bestehende Struktur. Der Aufrufer unserer Funktion kann damit aus einem einzigen Rückgabewert, beide Variablen für Wert und Einheit beziehen.

get_temperature

def get_temperature(live):
  #temperature node: 'temperature'
  temp = live.find('temperature')
  return get_value_unit(temp)

Aufgabe dieser Funktion ist es, wie der Name schon vermuten lässt, den Wert und die Einheit (als Tupel) für den Temperatur-Messwert auszugeben. Damit ist diese Funktion eine Stufe konkreter als 'get_value_unit()'.

Da der Eingabeparameter 'live', wieder ein vom Modul 'xml.etree.ElementTree' geparster XML-Knoten ist, stellt er eine Funktion 'find()' bereit, die einen ihr übergebenen Unterknoten zurückgibt.

Zur Erinnerung: 'findtext()' gibt den Textinhalt eines als Parameter übergebenen Knotens zurück, nicht den Knoten selbst.

Beispiel:

<live>
  <temperature>
    <name>Temperature</name>
    <unit>°C</unit>
    <value>20.3</value>
  </temperature>
</live>

Wird dieser XML-Schnipsel, nachdem er in eine Baumstruktur gewandelt wurde, also als Argument an 'get_temperature()' übergeben, wird darin nach einem Unterknoten namens "temperature" gesucht und, falls gefunden, in die Variable 'temp' geladen.
Anschließend, wird 'temp' an 'get_value_unit()' übergeben, um aus dem 'temperature'-Knoten das Tupel '(value, unit)' zu gewinnen.
Dieses wird direkt als Rückgabewert an den Aufrufer zurückgegeben.

get_humidity, get_pressure

Diese Funktionen verhalten sich vollständig analog zu 'get_temperature()'. Der einzige Unterschied ist, dass anstatt nach einem Unterknoten namens 'temperature', nach 'humidity' (Luftfeuchtigkeit), bzw. 'pressure' (Luftdruck) gesucht wird.

main()-Funktion

Fast geschafft!

Im letzten Schritt definieren wir die main()-Funktion, in der wir unsere vorbereiteten Hilfsfunktionen zu einem großen Ganzen zusammenfügen.

main()

def main():
  xml = get_xml(querx_address, querx_http_port, xml_url)
  print (xml)
  #root node: 'querx'
  root = ET.fromstring(xml)
  #sub node 'live'
  live = root.find('live')

  if live.find('temperature') != None:
    tvalue, tunit = get_temperature(live)
    print ("Temperature: " + tvalue + " " + tunit)
  if live.find('humidity') != None:
    hvalue, hunit = get_humidity(live)
    print ("Humidity: " + hvalue + " " + hunit)
if live.find('pressure') != None:
    pvalue, punit = get_humidity(live)
    print ("Pressure: " + pvalue + " " + punit)

Als Erstes wird 'get_xml()' mit unseren Konfigurationsvariablen als Parameter aufgerufen. Wir erhalten einen String mit den XML-formatierten Messwerten von Querx zurück und speichern das Ergebnis in der Variablen 'xml'.

Mit 'print(xml)' können wir den String in die Konsole ausgeben, was zur Fehlerbehebung hilfreich sein kann.

Anschließend führen wir die Funktion 'fromstring()' mit unserem XML-String als Parameter aus. Diese Methode wird vom 'ET'-Modul bereitgestellt und stellt den eigentlichen Parser dar:
Wir übergeben ihm einen XML-String und erhalten daraus eine Baumstruktur, genauer gesagt den obersten Knoten (engl. "root node") als ein 'Element'-Objekt, welches alle existierenden Unterknoten, sowie Methoden zum Erkunden der Struktur enthält.

Weitergehende Infos unter:
docs.python.org/2/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element

Das oberste Knoten-Element setzten wir als Variable "root".

Um darin den Unterknoten 'live' zu finden, der die augenblicklichen Messwerte enthält, rufen wir ein weiteres Mal die 'find()'-Methode mit dem Argument 'live' auf. Falls ein Unterknoten 'live' innerhalb von 'root' existiert, wird dieser dann in die Variable 'live' gesetzt.

Querx Gerätevarianten

egnite bietet verschiedene Querx Modelle für unterschiedliche Anforderungen an.

- Querx TH bzw. Querx WLAN TH verfügt über einen integrierten Temperatur- und Luftfeuchtesensor.

- Querx THP bzw. Querx WLAN THP verfügt über einen integrierten Temperatur-, einen Luftfeuchte- und einen Luftdrucksensor.

- Querx PT bzw. Querx WLAN PT misst Temperatur.

Da unser Programm mit sämtlichen Modellen funktionieren soll, ist es erforderlich zu überprüfen, welche Messwerte die XML-Datei, die wir von einem bestimmten Modell erhalten haben, tatsächlich enthält.

Dies geschieht mittels 3 'if'-Kontrollstrukturen:

if live.find('temperature') != None:
  tvalue, tunit = get_temperature(live)
  print ("Temperature: " + tvalue + " " + tunit)

Der 'live'-Knoten wird nach einem Unterknoten namens 'temperature' durchsucht.

Falls das Ergebnis der 'find()'-Funktion *nicht* None ist, wird der eingerückte Code-Teil ausgeführt:

Für die Temperaturmesswerte wird an die Funktion 'get_temperature()' der 'live'-Knoten übergeben und das Tupel aus dem Rückgabewert wird in zwei Variablen 'tvalue' (Temperatur-Wert) und 'tunit' (Temperatureinheit) gespeichert.

Anschließend werden diese Variablen mit der 'print'-Funktion in die Konsole ausgegeben.

Falls das Ergebnis der 'find()'-Funktion *allerdings None ist, wird der eingerückte Teil übersprungen. Es macht schließlich keinen Sinn zu versuchen, einen Teil der Baumstruktur auszulesen, die mangels entsprechendem Sensor gar nicht existiert.

Die entsprechenden Codeblöcke für Luftfeuchtigkeit und Luftdruck verhalten sich analog.

Herzlichen Glückwunsch. Sie haben es geschafft!

Um unser Programm zu starten, fehlt nur noch der Aufruf unserer Hauptfunktion:

  main();

Wenn Sie jetzt das Script mit

  python script.py

aufrufen, sollten Sie eine Ausgabe des kompletten XML-Dokuments, sowie nacheinander die Messwerte für die Sensoren Ihres Querx Geräts in der Form „Sensorname: Messwert Einheit“ sehen.