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 Querx Hub 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:

    Tutorial-Querx TH
    192.168.192.206
    80
    06.01.2017
    16:02:20
    06.01.2017
    15:02:20
    1483714940
    
      Temperature
      C
      24.9
    
    
      Humidity
      %RH
      17
    
    
      Dew point
      C
      -1.9

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: https://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_pressure(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
°C
20.3

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:

Temperature
°C
20.3

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_pressure(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.