Generisches Modul zum Abfragen von Readings per HTTP bzw. Webservice

Begonnen von StefanStrobel, 22 Dezember 2013, 11:49:49

Vorheriges Thema - Nächstes Thema

StefanStrobel

Hallo,

gibt es eigentlich schon ein Modul, mit dem man Werte vom Webinterface oder Webservice eines Geräts auslesen kann?

Ich habe einen Poolmanager, der Wassertemperatur, Chlor und PH verwaltet. Die Werte kann ich mit dem richtigen Post-Request und passenden Headern bekommen.

Meine erste Idee war, das in einem eigenen Poolmanager-Modul zu realisieren, aber eigentlich könnte man das auch generisch machen, so dass beim define oder mit zusätzlichen sets die URL, Header, Anfragedaten und Polling-Interval definiert werden, dazu Regexes, mit denen man die gewünschten Readings aus dem reply extrahieren kann.

Wenn es sowas noch nicht gibt, würde ich das mal angehen.
Nur wenn es das doch schon gibt, natürlich nicht ;-)

Was meint Ihr?

Gruss
   Stefan

Loredo

Bist du hier schon weiter gekommen?
Ich frage wegen http://forum.fhem.de/index.php/topic,17951.0.html


Ich hätte demnach einen ersten Anwendungsfall für ein solches Modul wie mir scheint ;-)




Gruß
Julian
Hat meine Arbeit dir geholfen? ⟹ https://paypal.me/pools/c/8gDLrIWrG9

Maintainer:
FHEM-Docker Image, https://github.com/fhem, Astro(Co-Maintainer), ENIGMA2, GEOFANCY, GUEST, HP1000, Installer, LaMetric2, MSG, msgConfig, npmjs, PET, PHTV, Pushover, RESIDENTS, ROOMMATE, search, THINKINGCLEANER

StefanStrobel

Für den Poolmanager funktioniert der Prototyp inzwischen. Ich bin jetzt dabei das ganze noch etwas allgemeiner zu machen. In den nächsten Tagen sollte es soweit sein, dass ich eine erste Version posten kann.

Meine derzeitige Idee ist dass man nach einem "define" mit URl und "Interval" per "set" Paare mit Reading-Name und Regex setzen kann, so dass das Modul dann per Regex mit $1 das entsprechende Reading aus der HTTP-Antwort holen kann.
Ebenso sollten benötigte HTTP-Header und Post-Daten für die Abfrage per "set" definiert werden können, sonst würde der "define" zu komplex und Leerstellen verwirrend.

Da ich für die Abfrage einen HTTP-Header setzen muss, war auch ein kleiner Patch an HttpUtils nötig - alternativ hätte ich eine eigene HTTP-Routine schreiben müssen. Aber im Kontext langsamer Abfragen wäre das evt. sowieso noch ein Thema. Hat jemand schon in einem Modul die HTTP-Abfragen asynchron implementiert?

Hier schonmal die Änderung an HttpUtils, wie ich sie bei mir gemacht habe.

Boris: liest Du mit? Macht das aus Deiner Sicht so Sinn?

*** HttpUtils.pm   2013-12-25 12:11:00.000000000 -0800
--- HttpUtils-Orig.pm   2013-12-25 11:30:44.000000000 -0800
***************
*** 60,66 ****
  sub
  CustomGetFileFromURL($$@)
  {
!   my ($quiet, $url, $timeout, $data, $noshutdown, $loglevel, $reqheader) = @_;
    $timeout = 4.0 if(!defined($timeout));
    $loglevel = 1 if(!$loglevel);
   
--- 60,66 ----
  sub
  CustomGetFileFromURL($$@)
  {
!   my ($quiet, $url, $timeout, $data, $noshutdown, $loglevel) = @_;
    $timeout = 4.0 if(!defined($timeout));
    $loglevel = 1 if(!$loglevel);
   
***************
*** 114,127 ****
    if(defined($authstring)) {
      $hdr .= "Authorization: Basic $auth64\r\n";
    }
-   if(defined($reqheader)) {
-     $hdr .= $reqheader."\r\n";
-   }
    if(defined($data)) {
      $hdr .= "Content-Length: ".length($data)."\r\n";
!    if ($hdr !~ "Content-Type:") {
!       $hdr .= "Content-Type: application/x-www-form-urlencoded";
!    }
    }
    $hdr .= "\r\n\r\n";
    syswrite $conn, $hdr;
--- 114,122 ----
    if(defined($authstring)) {
      $hdr .= "Authorization: Basic $auth64\r\n";
    }
    if(defined($data)) {
      $hdr .= "Content-Length: ".length($data)."\r\n";
!     $hdr .= "Content-Type: application/x-www-form-urlencoded";
    }
    $hdr .= "\r\n\r\n";
    syswrite $conn, $hdr;

rudolfkoenig

Da zunehmend mehr Geraete per HTTP bedient werden wollen, wollte ich eine nicht blockierende Variante zu Verfuegung stellen, insb. ein generisches HTTP Modul muss es verwenden. Es ist nicht ganz nicht-blockierend: DNS-Lookup und SSL Negotiation sind weiterhin blockierend, aber immmerhin connect und read nicht. Nachteil: das Ergebnis wird zeitversetzt zugestellt, nach JavaScript-Manier durch Aufruf einer Funktion, was man als Parameter uebergibt.

Beispiel:
# Parameters in the hash:
#  mandatory:url, callback
#  optional:hideurl,timeout,data,noshutdown,loglevel
HttpUtils_NonblockingGet({
    url=>"http://192.168.178.112:8888/fhem",
    myParam=>7,
    callback=>sub($$$){ Log 1,"$_[0]->{myParam} ERR:$_[1] DATA:$_[2]" }
})


Da ich dafuer das komplette HttpUtils.pm umgebaut habe und einiges "zu Fuss" machen muss, will ich es nicht sofort einchecken, sondern haenge ich es hier an, bitte gibt mir Feedback. Ich habe es zwar unter OSX, Linux, FritzBox und Windows getestet, jeweils mit und ohne HTTPS, redirection, DNS und connect Problemen, aber wer weiss was ich vergessen habe. Achtung: ein neues fhem.pl von gerade eben ist Voraussetzung!

@Autoren der 17_EGPM2LAN.pm, 30_HUEBridge.pm, 59_WWO.pm, 70_ENIGMA2.pm, 71_YAMAHA_AVR.pm: Bitte statt CustomGetFileFromURL auf GetFileFromURL bzw. GetFileFromURLQuiet ausweichen (== erstes Parameter von CustomGetFileFromURL weglassen), da ich weniger Wrapperfunktionen haben will. Ihr duerft natuerlich gerne HttpUtils_NonblockingGet verwenden :)


@StefanStrobel: ich kann die Aenderungen gerne uebernehmen, aber ich brauche ein diff -u oder svn diff. Am liebsten gegen die neue Version :)

StefanStrobel

Anbei ist meine erste Version eines generischen Moduls um Geräte per HTTP-Interface abzufragen.
Die nächste Version werde ich für das neue NonBlockingGet umbauen.

@Rudolf: nach genau so einem Non-Blocking Get habe ich gesucht! ich werde es gleich testen und dann bei Bedarf auch den Patch anpassen. Für den Poolmanager muss der HTTP Post z.B. einen Content-Type Header mit application/json;charset=UTF-8 enthalten.


Zum Modul:
Ich verwende es für den Poolmanager folgendermassen:

define PM HTTPMOD http://192.168.x.y/cgi-bin/webgui.fcgi 60
set PM AddRequestHeader Content-Type: application/json;charset=UTF-8
set PM AddRequestHeader Accept: */*
set PM RequestData {"get" :["34.4001.value" ,"34.4008.value" ,"34.4033.value"]}
set PM AddMatchPair PH 34.4001.value":[ \t]+"([\d\.]+)",
set PM AddMatchPair CL 34.4008.value":[ \t]+"([\d\.]+)",
set PM AddMatchPair TEMP 34.4033.value":[ \t]+"([\d\.]+)"

attr PM room Pool
attr PM stateFormat {sprintf("%.1f Grad, PH %.1f, %.1f mg/l Chlor", ReadingsVal($name,"TEMP",0), ReadingsVal($name,"PH",0), ReadingsVal($name,"CL",0))}

define FileLog_PM FileLog ./log/PM-%Y.log PM
attr FileLog_PM room Pool


Im Define gibt man URL und Abfrage-Intervall an.
Mit den Sets danach definiert man eventuell benötigte zusätzliche HTTP-Header oder Post Inhalte für die Abfrage.
Die Antwort wird gegen einen Hash ausgewertet. Darin stehen Paare mit je einem Reading-Namen und einer Regex, die aus dem HTTP-Response den passenden Wert holt.
Mit set AddMatchPair kann man den Hash aufbauen.


StefanStrobel

Anbei eine erste Version eines generischen Moduls zur Abfrage von Geräten mit Web-Interface, das HttpUtils_NonblockingGet aufruft.
Ich hoffe ich habe die Verwendung der Http_Utils richtig verstanden. Bei mir scheint es zu funktionieren.

Ebenfalls beigefügt ist ein diff -u um eigene Header an HttpUtils_NonblockingGet übergeben zu können.

rudolfkoenig

#6
ZitatIch hoffe ich habe die Verwendung der Http_Utils richtig verstanden.
Ja, so war das auch gemeint.

Bei der kurzen Uebersicht des Moduls ist mir folgendes aufgefallen:

- ich finde AddMatchPair als Name nicht intuitiv, ich wuerde es in readingsRegexp oder vergleichbares aendern. 

- die Parameter per set zu setzen ist nicht "FHEM-like", "richtig" sind Attribute, und ja, es gibt ein AttrFn.

- mit Attributen hat man das Problem der Listen, dazu gibt es Workarounds:
a) eine Liste von Attribute zulassen: readingsRegexp1,readingsRegexp2...readingsRegexp9
b) Attribute mit Wildcard zulassen, z.Bsp. readingsRegexp.*. Nachteil: in FHEMWEB ist sowas z.Zt. nur ueber Kommandozeile definierbar oder aendernbar, ich waere aber geneigt das zu aendern :)
c) alles als ein Attribut setzen, mit Leerzeichen als Trenner. Es gibt eine Hilfsfunktion attrSplit, der nach / oder , trennt, falls der Attributwert mit diesem Zeichen beginnt, sonst mit Leerzeichen. Sowas ist z.Zt. zwar in FHEM ueblich, aber nicht schoen fuer lange Listen, und verwirrend fuer weiter strukturierte Daten.

- bitte das regexp entweder beim Ausfuehren, oder noch besser beim definieren (AttrFn) per eval pruefen, sonst stuerzt fhem beim ersten  kaputten regexp ab.

StefanStrobel

Dann werd ich das nochmal umbauen.

Ein Problem ist mir auch noch aufgefallen:
Seit ich das neue HttpUtils verwende, und die Readings in der Callback-Funktion setze, wird kein Filelog mehr dafür geschrieben, der Status nicht aktualisiert und das GUI scheint auch nichts mehr von Änderungen an den Readings mitzubekommen.
Muss ich in der Callback-Funktion irgendwelche Notifications anschalten oder ist das noch ein Problem in Fhem.pl?

rudolfkoenig

Ich kann keinen Grund vorstellen.
Kannst Du mir einen Beispiel bauen, den ich auch ohne speziellen Hardware testen kann?

Markus Bloch

Zitat von: rudolfkoenig am 29 Dezember 2013, 19:19:54
@Autoren der 17_EGPM2LAN.pm, 30_HUEBridge.pm, 59_WWO.pm, 70_ENIGMA2.pm, 71_YAMAHA_AVR.pm: Bitte statt CustomGetFileFromURL auf GetFileFromURL bzw. GetFileFromURLQuiet ausweichen (== erstes Parameter von CustomGetFileFromURL weglassen), da ich weniger Wrapperfunktionen haben will. Ihr duerft natuerlich gerne HttpUtils_NonblockingGet verwenden :)

Hi Rudi,

mach ich gerne, aber ist das nicht eben das, was du nicht willst? Du schlägst vor, das wir auf die Wrapper-Funktionen von CustomGetFileFromURL ausweichen. Ich denke du willst diese Wrapperfunktionen nicht oder habe ich das falsch verstanden?

Die HttpUtils_NonBlocking klingt sehr interessant, da mich bezgl. der Blockierung bei einem nicht erreichbaren Server bereits verschiedene Leute fragend kontaktiert hatten.

Viele Grüße

Markus
Developer für Module: YAMAHA_AVR, YAMAHA_BD, FB_CALLMONITOR, FB_CALLLIST, PRESENCE, Pushsafer, LGTV_IP12, version

aktives Mitglied des FHEM e.V. (Technik)

rudolfkoenig

Ich wollte ja nicht so radikal sein, aber ihr koennt natuerlich auf alle .*GetFileFromURL.* Funktionen verzichten :)
GetHttpFile werde ich aber behalten, man braucht auch was einfaches.

Ich habe HttpUtils.pm eingecheckt, damit es jetzt endlich getestet wird.

StefanStrobel

Anbei ein Entwurf, der mit attr statt mit set arbeitet.
Das ganze klappt bei mir jedoch nur wenn ich Zeile 136 in HttpUtils auskommentiere.

$hash->{NAME} = ""; # Delete might check it

Ansonsten fehlt im hash des Geräts später der Name. Oder habe ich da noch was falsch verstanden?
Ohne die Zeile 136 klappt es auch mit dem Filelog wieder.

Die Attribute habe ich mit readingsName.*, readingsRegex.* etc. in die AttrList gestellt und dann später in der Callback-Funktion mit
foreach my $a (sort (grep (/readingsName/, keys %{$attr{$name}}))) {
die tatsächlich gesetzten Attribute verarbeitet.
Macht das so Sinn in fhem oder gibt es da einen besseren Weg?

Die Beispiel-Konfiguration für einen Poolmanager sieht folgendermassen aus:

define PM HTTPMOD http://192.168.70.90/cgi-bin/webgui.fcgi 60

attr PM requestHeader1 Content-Type: application/json
attr PM requestHeader2 Accept: */*
attr PM requestData {"get" :["34.4001.value" ,"34.4008.value" ,"34.4033.value"]}

attr PM readingsName1 PH
attr PM readingsRegex1 34.4001.value":[ \t]+"([\d\.]+)",

attr PM readingsName2 CL
attr PM readingsRegex2 34.4008.value":[ \t]+"([\d\.]+)",

attr PM readingsName3 TEMP
attr PM readingsRegex3 34.4033.value":[ \t]+"([\d\.]+)"

attr PM room Pool
attr PM stateFormat {sprintf("%.1f Grad, PH %.1f, %.1f mg/l Chlor", ReadingsVal($name,"TEMP",0), ReadingsVal($name,"PH",0), ReadingsVal($name,"CL",0))}

define FileLog_PM FileLog ./log/PM-%Y.log PM
attr FileLog_PM room Pool


rudolfkoenig

ZitatDas ganze klappt bei mir jedoch nur wenn ich Zeile 136 in HttpUtils auskommentiere.

Sorry, hab nur mit einem leeren $hash experimentiert, deswegen ist es mir nicht aufgefallen.
Habs jetzt gefixed und eingecheckt.

ZitatMacht das so Sinn in fhem oder gibt es da einen besseren Weg?
Ich finde es ok, und es faellt mir nichts besseres ein.

Jetzt fehlt noch testen und Doku mit einfachen Beispielen, und dann sollte es eingecheckt werden.

StefanStrobel

Hier eine neue Version mit einer verbesserten Fehlerbehandlung und etwas Doku (ich hoffe das Format ist ok so - habe es von einem anderen Modul übernommen).
Es würde mich freuen wenn jemand andere Anwendungsfälle hat und das Modul testen kann.

UliM

Hi Stefan,
ich find die Idee klasse und bin sicher, das auch selbst nutzen zu können und damit ein paar eigene Schmalspur-Parser sersetzen zu können.

Wäre es möglich, dass Du noch 1-2 erklärte Beispiel-Konfigs im Wiki hinterlegst?
In dem o.g. Beispiel ist für mich nicht klar, ob "34.4008.value" irgendwo im html-code der angeforderten Seite auftaucht oder ein beliebiger Name ist oder die 34.4008 irgendeine steuernde Funktion hat.
Auch ist mir nicht klar, was es mit den Header-Attributen auf sich hat, denn die Header-Info kommt doch mit der angeforderten Seite zurück?

Beste Grüße,
Uli
RPi4/Raspbian, CUL V3 (ca. 30 HomeMatic-devices), LAN (HarmonyHub, alexa etc.).  Fördermitglied des FHEM e.V.