From 81c0f7f8bc70fe0ac6d54c5ee36f293ac914d70b Mon Sep 17 00:00:00 2001 From: ststrobel <> Date: Sat, 5 Mar 2016 19:39:06 +0000 Subject: [PATCH] 98_HTTPMOD.pm: many enhancements - see announcement in the forum git-svn-id: https://svn.fhem.de/fhem/trunk@11002 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/98_HTTPMOD.pm | 3176 +++++++++++++++++++++++++++++---------- 1 file changed, 2388 insertions(+), 788 deletions(-) diff --git a/fhem/FHEM/98_HTTPMOD.pm b/fhem/FHEM/98_HTTPMOD.pm index e15f2fff4..452ae7260 100755 --- a/fhem/FHEM/98_HTTPMOD.pm +++ b/fhem/FHEM/98_HTTPMOD.pm @@ -1,7 +1,6 @@ ######################################################################### # $Id$ # fhem Modul f�r Ger�te mit Web-Oberfl�che -# wie z.B. Poolmanager Pro von Bayrol (PM5) # # This file is part of fhem. # @@ -45,17 +44,62 @@ # 2015-04-27 Integrated modification of jowiemann partially # settings: interval, reread, stop, start # DEVSTATE was not implemented because "disabled" is visible as attribute -# and stopped / started is visible as TRIGGERTIME. +# and stopped / started is visible as TRIGGERTIME. # also the attribute disabled will not touch the internal timer. # 2015-05-10 Integrated xpath extension as suggested in the forum # 2015-06-22 added set[0-9]*NoArg and get[0-9]*URLExpr, get[0-9]*HeaderExpr and get[0-9]*DataExpr # 2015-07-30 added set[0-9]*TextArg, Encode and Decode # 2015-08-03 added get[0-9]*PullToFile (not fully implemented yet and not yet documented) # 2015-08-24 corrected bug when handling sidIdRegex for step <> 1 +# 2015-09-14 implemented parseFunction1 and 2, modified to not return a value if successful +# 2015-10-10 major restructuring, new xpath, xpath-strict and json parsing implementation +# 2015-11-08 fixed bug which caused a recursion when reading from file:// urls +# fixed xpath handling (so far ...) +# 2015-11-19 MaxAge, aligned type and context for some functions +# 2015-11-23 fixed map handling to allow spaces in names and convert them for fhemweb +# 2015-12-03 Max age finalized +# 2015-12-05 fixed error when loading Libs inside eval{} (should have been eval"") and added documentation for showError +# 2015-12-07 fixed syntax to work with Perl older than 5.14 in a few places +# added RecombineExpr and a few performance optimisations +# 2015-12-10 fixed a bug in JSON parsing and corrected extractAllJSON to start with lower case +# 2015-12-22 fixed missing error handling for JSON parser call +# 2015-12-28 added SetParseResponse +# 2016-01-01 fixed bug where httpheader was not handled, added cookie handling +# 2016-01-09 fixed a bug which caused only one replacement per string to happen +# 2016-01-10 fixed a bug where only the first word of text passed to set is used, +# added sid extraction and reAuth detection with JSON and XPath +# 2016-01-11 modified automatic $val replacement for set values to pass the value through the request queue and +# do the actual replacement just before sending just like user definable replacements +# so they can be done by replacement attributes with other placeholders instead +# 2016-01-16 added TextArg to get and optimized creating the hint list for get / set ? +# 2016-01-21 added documentation +# added RegOpt (still needs more testing), Replacement mode delete +# 2016-01-23 changed MATCHED_READINGS to contain automatically created subreadings (-num) +# added AutoNumLen for automatic sub-reading names (multiple matches) so the number has leading zeros and a fixed length +# added new attribute upgrading mechanism (e.g. for sidIDRegex to sidIdRegex) +# 2016-01-25 modified the way attributes are added to userattr - now includes :hints for fhemweb and old entries are replaced +# 2016-02-02 added more checks to JsonFlatter (if defined ...), fixed auth to be added in the front of the queue, added clearSIdBeforeAuth, authRetries +# 2016-02-04 added a feature to name a reading "unnamed-XX" if Name attribute is missing instead of ignoring everything related +# 2016-02-05 fixed a warning caused by missing initialisation of .setList internal +# 2016-02-07 allowed more regular expression modifiers in RegOpt, added IMap / OMap / IExpr / OExpr +# 2016-02-13 enable sslVersion attribute f�r HttpUtils and httpVersion +# 2016-02-14 add sslArgs attribute - e.g. as attr myDevice sslArgs SSL_verify_mode,SSL_VERIFY_NONE +# Log old attrs and offer set upgradeAttributes +# 2016-02-15 added replacement type key and set storeKeyValue +# 2016-02-20 set $XML::XPath::SafeMode = 1 to avoid memory leak in XML parser lib # # Todo: +# replacement scope attribute +# Implement IMap und IExpr for get +# +# doku der wichtigsten internen Strukturen (z.B. Request auch f�r Replacements und f�r Parse-Funktionen +# make axtracting the sid after a get / update an attribute / option +# # multi page log extraction -# generic cookie handling? +# Profiling von Modbus �bernehmen? +# +# extend httpmod to support simple tcp connections aver devio instead of HttpUtils +# extend devio for non blocking connect like httputils # # @@ -75,7 +119,9 @@ sub HTTPMOD_Get($@); sub HTTPMOD_Attr(@); sub HTTPMOD_GetUpdate($); sub HTTPMOD_Read($$$); -sub HTTPMOD_AddToQueue($$$$$;$$$); +sub HTTPMOD_AddToQueue($$$$$;$$$$); +sub HTTPMOD_JsonFlatter($$;$); +sub HTTPMOD_ExtractReading($$$$); # # FHEM module intitialisation @@ -91,76 +137,114 @@ sub HTTPMOD_Initialize($) $hash->{GetFn} = "HTTPMOD_Get"; $hash->{AttrFn} = "HTTPMOD_Attr"; $hash->{AttrList} = - "reading[0-9]+Name " . # new syntax for readings - "reading[0-9]+Regex " . - "reading[0-9]*Expr " . - "reading[0-9]*Map " . # new feature - "reading[0-9]*Format " . # new feature - "reading[0-9]*Decode " . # new feature - "reading[0-9]*Encode " . # new feature + "(reading|get|set)[0-9]+(-[0-9]+)?Name " . - "readingsName.* " . # old syntax - "readingsRegex.* " . - "readingsExpr.* " . + "(reading|get|set)[0-9]*(-[0-9]+)?Expr " . + "(reading|get|set)[0-9]*(-[0-9]+)?Map " . + "(reading|get|set)[0-9]*(-[0-9]+)?OExpr " . + "(reading|get|set)[0-9]*(-[0-9]+)?OMap " . + "(get|set)[0-9]*(-[0-9]+)?IExpr " . + "(get|set)[0-9]*(-[0-9]+)?IMap " . + + "(reading|get|set)[0-9]*(-[0-9]+)?Format " . + "(reading|get|set)[0-9]*(-[0-9]+)?Decode " . + "(reading|get|set)[0-9]*(-[0-9]+)?Encode " . + + "(reading|get)[0-9]*(-[0-9]+)?MaxAge " . + "(reading|get)[0-9]*(-[0-9]+)?MaxAgeReplacementMode:text,expression,delete " . + "(reading|get)[0-9]*(-[0-9]+)?MaxAgeReplacement " . + + "(reading|get|set)[0-9]+Regex " . + "(reading|get|set)[0-9]+RegOpt " . # see http://perldoc.perl.org/perlre.html#Modifiers + "(reading|get|set)[0-9]+XPath " . + "(reading|get|set)[0-9]+XPath-Strict " . + "(reading|get|set)[0-9]+JSON " . + "(reading|get|set)[0-9]*RecombineExpr " . + "(reading|get|set)[0-9]*AutoNumLen " . + "extractAllJSON " . + + "readingsName.* " . # old + "readingsRegex.* " . # old + "readingsExpr.* " . # old "requestHeader.* " . "requestData.* " . - "reAuthRegex " . - "noShutdown:0,1 " . - + "noShutdown:0,1 " . + "httpVersion " . + "sslVersion " . + "sslArgs " . "timeout " . "queueDelay " . "queueMax " . "minSendDelay " . "showMatched:0,1 " . - - "sid[0-9]*URL " . - "sid[0-9]*IDRegex " . - "sid[0-9]*Data.* " . - "sid[0-9]*Header.* " . - "sid[0-9]*IgnoreRedirects " . + "showError:0,1 " . - "set[0-9]+Name " . - "set[0-9]*URL " . - "set[0-9]*Data.* " . - "set[0-9]*Header.* " . - "set[0-9]+Min " . - "set[0-9]+Max " . - "set[0-9]+Map " . # Umwandlung von Codes f�r das Ger�t zu sprechenden Namen, z.B. "0:mittig, 1:oberhalb, 2:unterhalb" - "set[0-9]+Hint " . # Direkte Fhem-spezifische Syntax f�r's GUI, z.B. "6,10,14" bzw. slider etc. - "set[0-9]+Expr " . - "set[0-9]*ReAuthRegex " . - "set[0-9]*NoArg " . # don't expect a value - for set on / off and similar. - "set[0-9]*TextArg " . # just pass on a raw text value without validation / further conversion + "parseFunction1 " . + "parseFunction2 " . + + "[gs]et[0-9]*URL " . + "[gs]et[0-9]*Data.* " . + "[gs]et[0-9]*NoData.* " . # make sure it is an HTTP GET without data - even if a more generic data is defined + "[gs]et[0-9]*Header.* " . + "[gs]et[0-9]*CheckAllReadings:0,1 " . + "[gs]et[0-9]*ExtractAllJSON:0,1 " . - "get[0-9]+Name " . - "get[0-9]*URL " . - "get[0-9]*Data.* " . - "get[0-9]*Header.* " . + "[gs]et[0-9]*URLExpr " . # old + "[gs]et[0-9]*DatExpr " . # old + "[gs]et[0-9]*HdrExpr " . # old - "get[0-9]*URLExpr " . - "get[0-9]*DatExpr " . - "get[0-9]*HdrExpr " . - - "get[0-9]+Poll " . # Todo: warum geht bei wildcards kein :0,1 Anhang ? -> in fhem.pl nachsehen + "get[0-9]+Poll:0,1 " . "get[0-9]+PollDelay " . - "get[0-9]*Regex " . - "get[0-9]*Expr " . - "get[0-9]*Map " . - "get[0-9]*Format " . - "get[0-9]*Decode " . - "get[0-9]*Encode " . - "get[0-9]*CheckAllReadings " . "get[0-9]*PullToFile " . "get[0-9]*PullIterate " . - "get[0-9]*RecombineExpr " . + + "set[0-9]+Min " . # todo: min, max und hint auch f�r get, Schreibweise der Liste auf (get|set) vereinheitlichen + "set[0-9]+Max " . + "set[0-9]+Hint " . # Direkte Fhem-spezifische Syntax f�r's GUI, z.B. "6,10,14" bzw. slider etc. + "set[0-9]*NoArg:0,1 " . # don't expect a value - for set on / off and similar. (default for get) + "[gs]et[0-9]*TextArg:0,1 " . # just pass on a raw text value without validation / further conversion + "set[0-9]*ParseResponse:0,1 " . # parse response to set as if it was a get + + "reAuthRegex " . + "reAuthJSON " . + "reAuthXPath " . + "reAuthXPath-Strict " . + "[gs]et[0-9]*ReAuthRegex " . + "[gs]et[0-9]*ReAuthJSON " . + "[gs]et[0-9]*ReAuthXPath " . + "[gs]et[0-9]*ReAuthXPath-Strict " . + + "idRegex " . + "idJSON " . + "idXPath " . + "idXPath-Strict " . + "(get|set|sid)[0-9]*IDRegex " . # old + "(get|set|sid)[0-9]*IdRegex " . + "(get|set|sid)[0-9]*IdJSON " . + "(get|set|sid)[0-9]*IdXPath " . + "(get|set|sid)[0-9]*IdXPath-Strict " . + + "sid[0-9]*URL " . + "sid[0-9]*Header.* " . + "sid[0-9]*Data.* " . + "sid[0-9]*IgnoreRedirects:0,1 " . + "sid[0-9]*ParseResponse:0,1 " . # parse response as if it was a get + "clearSIdBeforeAuth:0,1 " . + "authRetries " . + + "replacement[0-9]*Regex " . + "replacement[0-9]*Mode:reading,internal,text,expression,key " . # defaults to text + "replacement[0-9]*Value " . # device:reading, device:internal, text, replacement expression + "[gs]et[0-9]*Replacement[0-9]*Value " . # can overwrite a global replacement value - todo: auch f�r auth? "do_not_notify:1,0 " . "disable:0,1 " . "enableControlSet:0,1 " . - "enableXPath:0,1 " . - "enableXPath-Strict:0,1 " . + "enableCookies:0,1 " . + "enableXPath:0,1 " . # old + "enableXPath-Strict:0,1 " . # old $readingFnAttributes; } @@ -171,8 +255,8 @@ sub HTTPMOD_Initialize($) ######################################################################### sub HTTPMOD_Define($$) { - my ( $hash, $def ) = @_; - my @a = split( "[ \t][ \t]*", $def ); + my ($hash, $def) = @_; + my @a = split( "[ \t]+", $def ); return "wrong syntax: define <name> HTTPMOD URL interval" if ( @a < 3 ); @@ -186,6 +270,7 @@ sub HTTPMOD_Define($$) } if(int(@a) > 3) { + # interval specified if ($a[3] > 0) { if ($a[3] >= 5) { $hash->{Interval} = $a[3]; @@ -197,10 +282,13 @@ sub HTTPMOD_Define($$) $hash->{Interval} = 0; } } else { + # default if no interval specified $hash->{Interval} = 300; } - Log3 $name, 3, "$name: Defined with URL $hash->{MainURL} and interval $hash->{Interval}"; + Log3 $name, 3, "$name: Defined " . + ($hash->{MainURL} ? "with URL $hash->{MainURL}" : "without URL") . + ($hash->{Interval} ? " and interval $hash->{Interval}" : ""); # Initial request after 2 secs, for further updates the timer will be set according to interval. # but only if URL is specified and interval > 0 @@ -212,18 +300,21 @@ sub HTTPMOD_Define($$) InternalTimer($firstTrigger, "HTTPMOD_GetUpdate", "update:$name", 0); Log3 $name, 5, "$name: InternalTimer set to call GetUpdate in 2 seconds for the first time"; } else { - $hash->{TRIGGERTIME} = 0; + $hash->{TRIGGERTIME} = 0; $hash->{TRIGGERTIME_FMT} = ""; } + $hash->{".getList"} = ""; + $hash->{".setList"} = ""; return undef; } + # # undefine command when device is deleted ######################################################################### sub HTTPMOD_Undef($$) { - my ( $hash, $arg ) = @_; + my ($hash, $arg) = @_; my $name = $hash->{NAME}; RemoveInternalTimer ("timeout:$name"); RemoveInternalTimer ("queue:$name"); @@ -232,144 +323,780 @@ sub HTTPMOD_Undef($$) } +######################################################################### +sub HTTPMOD_LogOldAttr($$;$) +{ + my ($hash, $old, $new) = @_; + my $name = $hash->{NAME}; + Log3 $name, 3, "$name: the attribute $old should no longer be used." . ($new ? " Please use $new instead" : ""); + Log3 $name, 3, "$name: For most old attributes you can specify enableControlSet and then set device upgradeAttributes to automatically modify the configuration"; +} + + # # Attr command ######################################################################### -sub -HTTPMOD_Attr(@) +sub HTTPMOD_Attr(@) { my ($cmd,$name,$aName,$aVal) = @_; - my $hash = $defs{$name}; # might be needed inside a URLExpr + my $hash = $defs{$name}; + my $modHash = $modules{$hash->{TYPE}}; my ($sid, $old); # might be needed inside a URLExpr + # $cmd can be "del" or "set" # $name is device name - # aName and aVal are Attribute name and value + # aName and aVal are attribute name and attribute value # simple attributes like requestHeader and requestData need no special treatment here # readingsExpr, readingsRegex.* or reAuthRegex need validation though. + # if validation fails, return something so CommandAttr in fhem.pl doesn't assign a value to $attr if ($cmd eq "set") { - if ($aName =~ "Regex") { # catch all Regex like attributes - eval { qr/$aVal/ }; + if ($aName =~ /Regex/) { # catch all Regex like attributes + eval {qr/$aVal/}; if ($@) { Log3 $name, 3, "$name: Attr with invalid regex in attr $name $aName $aVal: $@"; return "Invalid Regex $aVal"; } - } elsif ($aName =~ "Expr") { # validate all Expressions - my $val = 1; + if ($aName =~ /([gs]et[0-9]*)?[Rr]eplacement[0-9]*Regex$/) { + $hash->{ReplacementEnabled} = 1; + } + + # conversions for legacy things + if ($aName =~ /(.+)IDRegex$/) { + HTTPMOD_LogOldAttr($hash, $aName, "${1}IdRegex"); + } + if ($aName =~ /readingsRegex.*/) { + HTTPMOD_LogOldAttr($hash, $aName, "reading01Regex syntax"); + } + } elsif ($aName =~ /readingsName.*/) { + HTTPMOD_LogOldAttr($hash, $aName, "reading01Name syntax"); + } elsif ($aName =~ /RegOpt$/) { + if ($aVal !~ /^[msxdualsig]*$/) { + Log3 $name, 3, "$name: illegal RegOpt in attr $name $aName $aVal"; + return "$name: illegal RegOpt in attr $name $aName $aVal"; + } + } elsif ($aName =~ /Expr/) { # validate all Expressions + my $val = 0; + my @matchlist = (); no warnings qw(uninitialized); eval $aVal; if ($@) { Log3 $name, 3, "$name: Attr with invalid Expression in attr $name $aName $aVal: $@"; return "Invalid Expression $aVal"; } - } elsif ($aName eq "enableXPath") { - if(!eval("use HTML::TreeBuilder::XPath;1")) { - Log3 $name, 3, "$name: Please install HTML::TreeBuilder::XPath to use the xpath-Option"; - return "Please install HTML::TreeBuilder::XPath to use the xpath-Option"; + if ($aName =~ /readingsExpr.*/) { + HTTPMOD_LogOldAttr($hash, $aName, "reading01Expr syntax"); + } elsif ($aName =~ /^(get[0-9]*)Expr/) { + HTTPMOD_LogOldAttr($hash, $aName, "${1}OExpr"); + } elsif ($aName =~ /^(reading[0-9]*)Expr/) { + HTTPMOD_LogOldAttr($hash, $aName, "${1}OExpr"); + } elsif ($aName =~ /^(set[0-9]*)Expr/) { + HTTPMOD_LogOldAttr($hash, $aName, "${1}IExpr"); } - } elsif ($aName eq "enableXPath-Strict") { - if(!eval("use XML::XPath;use XML::XPath::XMLParser;1")) { - Log3 $name, 3, "$name: Please install XML::XPath and XML::XPath::XMLParser to use the xpath-strict-Option"; - return "Please install XML::XPath and XML::XPath::XMLParser to use the xpath-strict-Option"; + + } elsif ($aName =~ /Map$/) { + if ($aName =~ /^(get[0-9]*)Map/) { + HTTPMOD_LogOldAttr($hash, $aName, "${1}OMap"); + } elsif ($aName =~ /^(reading[0-9]*)Map/) { + HTTPMOD_LogOldAttr($hash, $aName, "${1}OMap"); + } elsif ($aName =~ /^(set[0-9]*)Map/) { + HTTPMOD_LogOldAttr($hash, $aName, "${1}IMap"); + } + + } elsif ($aName =~ /replacement[0-9]*Mode/) { + if ($aVal !~ /^(reading|internal|text|expression|key)$/) { + Log3 $name, 3, "$name: illegal mode in attr $name $aName $aVal"; + return "$name: illegal mode in attr $name $aName $aVal"; + } + + } elsif ($aName =~ /([gs]et[0-9]*)?[Rr]eplacement([0-9]*)Value/) { + Log3 $name, 5, "$name: validating attr $name $aName $aVal"; + if (AttrVal($name, "replacement${2}Mode", "text") eq "expression") { + no warnings qw(uninitialized); + eval $aVal; + if ($@) { + Log3 $name, 3, "$name: Attr with invalid Expression (mode is expression) in attr $name $aName $aVal: $@"; + return "Attr with invalid Expression (mode is expression) in attr $name $aName $aVal: $@"; + } + } + + } elsif ($aName =~ /(get|reading)[0-9]*JSON$/ + || $aName =~ /[Ee]xtractAllJSON$/ + || $aName =~ /[Rr]eAuthJSON$/ + || $aName =~ /[Ii]dJSON$/) { + eval "use JSON"; + if($@) { + # Log3 $name, 3, "$name: Please install JSON Library to use JSON (apt-get install libjson-perl) - error was $@"; + return "Please install JSON Library to use JSON (apt-get install libjson-perl) - error was $@"; + } + $hash->{JSONEnabled} = 1; + } elsif ($aName eq "enableCookies") { + if ($aVal eq "0") { + delete $hash->{HTTPCookieHash}; + delete $hash->{HTTPCookies}; + } + } elsif ($aName eq "enableXPath" + || $aName =~ /(get|reading)[0-9]+XPath$/ + || $aName =~ /[Rr]eAuthXPath$/ + || $aName =~ /[Ii]dXPath$/) { + eval "use HTML::TreeBuilder::XPath"; + if($@) { + # Log3 $name, 3, "$name: Please install HTML::TreeBuilder::XPath to use the xpath-Option (apt-get install libxml-TreeBuilder-perl libhtml-treebuilder-xpath-perl) - error was $@"; + return "Please install HTML::TreeBuilder::XPath to use the xpath-Option (apt-get install libxml-TreeBuilder-perl libhtml-treebuilder-xpath-perl) - error was $@"; + } + $hash->{XPathEnabled} = 1; + + } elsif ($aName eq "enableXPath-Strict" + || $aName =~ /(get|reading)[0-9]+XPath-Strict$/ + || $aName =~ /[Rr]eAuthXPath-Strict$/ + || $aName =~ /[Ii]dXPath-Strict$/) { + eval "use XML::XPath;use XML::XPath::XMLParser"; + if($@) { + #Log3 $name, 3, "$name: Please install XML::XPath and XML::XPath::XMLParser to use the xpath-strict-Option (apt-get install libxml-parser-perl libxml-xpath-perl) - error was $@"; + return "Please install XML::XPath and XML::XPath::XMLParser to use the xpath-strict-Option (apt-get install libxml-parser-perl libxml-xpath-perl) - error was $@"; + } + $XML::XPath::SafeMode = 1; + $hash->{XPathStrictEnabled} = 1; + + } elsif ($aName =~ /^(reading|get)[0-9]*(-[0-9]+)?MaxAge$/) { + if ($aVal !~ '([0-9]+)') { + Log3 $name, 3, "$name: wrong format in attr $name $aName $aVal"; + return "Invalid Format $aVal in $aName"; + } + $hash->{MaxAgeEnabled} = 1; + + } elsif ($aName =~ /^(reading|get)[0-9]*(-[0-9]+)?MaxAgeReplacementMode$/) { + if ($aVal !~ /^(text|expression|delete)$/) { + Log3 $name, 3, "$name: illegal mode in attr $name $aName $aVal"; + return "$name: illegal mode in attr $name $aName $aVal, choose on of text, expression"; + } + + } elsif ($aName =~ /^(reading|get)([0-9]+)(-[0-9]+)?Name$/) { + $hash->{defptr}{readingBase}{$aVal} = $1; + $hash->{defptr}{readingNum}{$aVal} = $2 if ($2); + $hash->{defptr}{readingSubNum}{$aVal} = $3 if ($3); + } + + # handle wild card attributes -> Add to userattr to allow modification in fhemweb + #Log3 $name, 3, "$name: attribute $aName checking "; + if (" $modHash->{AttrList} " !~ m/ ${aName}[ :;]/) { + # nicht direkt in der Liste -> evt. wildcard attr in AttrList + foreach my $la (split " ", $modHash->{AttrList}) { + $la =~ /([^:;]+)(:?.*)/; + my $vgl = $1; # attribute name in list - probably a regex + my $opt = $2; # attribute hint in list + if ($aName =~ $vgl) { # yes - the name in the list now matches as regex + # $aName ist eine Auspr�gung eines wildcard attrs + #Log3 $name, 3, "$name: attribute $aName specified from $vgl, add to userattr" . + # ($opt ? " with extension $opt" : ""); + addToDevAttrList($name, "$aName" . $opt); # create userattr with hint to allow changing by click in fhemweb + if ($opt) { + # remove old entries without hint + my $ualist = $attr{$name}{userattr}; + $ualist = "" if(!$ualist); + my %uahash; + foreach my $a (split(" ", $ualist)) { + if ($a !~ /^${aName}$/) { # entry in userattr list is attribute without hint + $uahash{$a} = 1; + } else { + Log3 $name, 3, "$name: added hint $opt to attr $a in userattr list"; + } + } + $attr{$name}{userattr} = join(" ", sort keys %uahash); + } + } + } + } else { + # exakt in Liste enthalten -> sicherstellen, dass keine +* etc. drin sind. + if ($aName =~ /\|\*\+\[/) { + Log3 $name, 3, "$name: Atribute $aName is not valid. It still contains wildcard symbols"; + return "$name: Atribute $aName is not valid. It still contains wildcard symbols"; + } + } + + # Deletion of Attributes + } elsif ($cmd eq "del") { + #Log3 $name, 5, "$name: del attribute $aName"; + if ($aName =~ /(reading|get)[0-9]*JSON$/ + || $aName =~ /[Ee]xtractAllJSON$/ + || $aName =~ /[Rr]eAuthJSON$/ + || $aName =~ /[Ii]dJSON$/) { + if (!(grep !/$aName/, grep (/((reading|get)[0-9]*JSON$)|[Ee]xtractAllJSON$|[Rr]eAuthJSON$|[Ii]dJSON$/, + keys %{$attr{$name}}))) { + delete $hash->{JSONEnabled}; + #Log3 $name, 5, "$name: disable JSON"; + } + } elsif ($aName eq "enableXPath" + || $aName =~ /(get|reading)[0-9]+XPath$/ + || $aName =~ /[Rr]eAuthXPath$/ + || $aName =~ /[Ii]dXPath$/) { + if (!(grep !/$aName/, grep (/(get|reading)[0-9]+XPath$|enableXPath|[Rr]eAuthXPath$|[Ii]dXPath$/, + keys %{$attr{$name}}))) { + delete $hash->{XPathEnabled}; + #Log3 $name, 5, "$name: disable XPath"; + } + } elsif ($aName eq "enableXPath-Strict" + || $aName =~ /(get|reading)[0-9]+XPath-Strict$/ + || $aName =~ /[Rr]eAuthXPath-Strict$/ + || $aName =~ /[Ii]dXPath-Strict$/) { + + if (!(grep !/$aName/, grep (/(get|reading)[0-9]+XPath-Strict$|enableXPath-Strict|[Rr]eAuthXPath-Strict$|[Ii]dXPath-Strict$/, + keys %{$attr{$name}}))) { + delete $hash->{XPathStrictEnabled}; + #Log3 $name, 5, "$name: disable XPathStrict"; + } + } elsif ($aName eq "enableCookies") { + delete $hash->{HTTPCookieHash}; + delete $hash->{HTTPCookies}; + } elsif ($aName =~ /(reading|get)[0-9]*(-[0-9]+)?MaxAge$/) { + if (!(grep !/$aName/, grep (/(reading|get)[0-9]*(-[0-9]+)?MaxAge$/, keys %{$attr{$name}}))) { + delete $hash->{MaxAgeEnabled}; + #Log3 $name, 5, "$name: disable MaxAge"; + } + } elsif ($aName =~ /([gs]et[0-9]*)?[Rr]eplacement[0-9]*Regex/) { + if (!(grep !/$aName/, grep (/([gs]et[0-9]*)?[Rr]eplacement[0-9]*Regex/, keys %{$attr{$name}}))) { + delete $hash->{ReplacementEnabled}; + #Log3 $name, 5, "$name: disable Replacement"; } } - addToDevAttrList($name, $aName); } + if ($aName =~ /^[gs]et/ || $aName eq "enableControlSet") { + $hash->{".updateHintList"} = 1; + } + if ($aName =~ /^(get|reading)/) { + $hash->{".updateReadingList"} = 1; + } + return undef; } + + +# Upgrade attribute names from older versions +############################################## +sub HTTPMOD_UpgradeAttributes($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my %dHash; + my %numHash; + + #Log3 $name, 3, "$name: UpgradeAttributes called, userattr list is $attr{$name}{userattr}"; + foreach my $aName (keys %{$attr{$name}}) { + if ($aName =~ /(.+)IDRegex$/) { + my $new = $1 . "IdRegex"; + my $val = $attr{$name}{$aName}; + CommandAttr(undef, "$name $new $val"); # also adds new attr to userattr list through _Attr function + CommandDeleteAttr(undef, "$name $aName"); + $dHash{$aName} = 1; + Log3 $name, 3, "$name: upgraded attribute name $aName to new sytax $new"; + } elsif ($aName =~ /(.+)Regex$/) { + my $ctx = $1; + my $val = $attr{$name}{$aName}; + #Log3 $name, 3, "$name: upgradeAttributes check attr $aName, val $val"; + if ($val =~ /^xpath:(.*)/) { + $val = $1; + my $new = $ctx . "XPath"; + CommandAttr(undef, "$name $new $val"); + CommandAttr(undef, "$name $ctx" . "RecombineExpr join(\",\", \@matchlist)"); + CommandDeleteAttr(undef, "$name $aName"); + $dHash{$aName} = 1; + Log3 $name, 3, "$name: upgraded attribute name $aName to new sytax $new"; + } + if ($val =~ /^xpath-strict:(.*)/) { + $val = $1; + my $new = $ctx . "XPath-Strict"; + CommandAttr(undef, "$name $new $val"); + CommandAttr(undef, "$name $ctx" . "RecombineExpr join(\",\", \@matchlist)"); + CommandDeleteAttr(undef, "$name $aName"); + $dHash{$aName} = 1; + Log3 $name, 3, "$name: upgraded attribute name $aName to new sytax $new"; + } + } elsif ($aName eq "enableXPath" || $aName eq "enableXPath-Strict" ) { + CommandDeleteAttr(undef, "$name $aName"); + Log3 $name, 3, "$name: removed attribute name $aName"; + + } elsif ($aName =~ /(set[0-9]*)Expr$/) { + my $new = $1 . "IExpr"; + my $val = $attr{$name}{$aName}; + CommandAttr(undef, "$name $new $val"); + CommandDeleteAttr(undef, "$name $aName"); + $dHash{$aName} = 1; + Log3 $name, 3, "$name: upgraded attribute name $aName to new sytax $new"; + } elsif ($aName =~ /(get[0-9]*)Expr$/) { + my $new = $1 . "OExpr"; + my $val = $attr{$name}{$aName}; + CommandAttr(undef, "$name $new $val"); + CommandDeleteAttr(undef, "$name $aName"); + $dHash{$aName} = 1; + Log3 $name, 3, "$name: upgraded attribute name $aName to new sytax $new"; + } elsif ($aName =~ /(reading[0-9]*)Expr$/) { + my $new = $1 . "OExpr"; + my $val = $attr{$name}{$aName}; + CommandAttr(undef, "$name $new $val"); + CommandDeleteAttr(undef, "$name $aName"); + $dHash{$aName} = 1; + Log3 $name, 3, "$name: upgraded attribute name $aName to new sytax $new"; + + } elsif ($aName =~ /(set[0-9]*)Map$/) { + my $new = $1 . "IMap"; + my $val = $attr{$name}{$aName}; + CommandAttr(undef, "$name $new $val"); + CommandDeleteAttr(undef, "$name $aName"); + $dHash{$aName} = 1; + Log3 $name, 3, "$name: upgraded attribute name $aName to new sytax $new"; + } elsif ($aName =~ /(get[0-9]*)Map$/) { + my $new = $1 . "OMap"; + my $val = $attr{$name}{$aName}; + CommandAttr(undef, "$name $new $val"); + CommandDeleteAttr(undef, "$name $aName"); + $dHash{$aName} = 1; + Log3 $name, 3, "$name: upgraded attribute name $aName to new sytax $new"; + } elsif ($aName =~ /(reading[0-9]*)Map$/) { + my $new = $1 . "OMap"; + my $val = $attr{$name}{$aName}; + CommandAttr(undef, "$name $new $val"); + CommandDeleteAttr(undef, "$name $aName"); + $dHash{$aName} = 1; + Log3 $name, 3, "$name: upgraded attribute name $aName to new sytax $new"; + } elsif ($aName =~ /^readings(Name|Expr|Regex)(.*)$/) { + my $typ = $1; + my $sfx = $2; + my $num; + if (defined($numHash{$sfx})) { + $num = $numHash{$sfx}; + } else { + my $max = 0; + foreach my $a (keys %{$attr{$name}}) { + if ($a =~ /^reading([0-9]+)\D+$/) { + $max = $1 if ($1 > $max); + } + } + $num = sprintf("%02d", $max + 1); + $numHash{$sfx} = $num; + } + my $new = "reading${num}${typ}"; + my $val = $attr{$name}{$aName}; + CommandAttr(undef, "$name $new $val"); + CommandDeleteAttr(undef, "$name $aName"); + $dHash{$aName} = 1; + Log3 $name, 3, "$name: upgraded attribute name $aName to new sytax $new"; + } + } + + $dHash{"enableXpath"} = 1; + $dHash{"enableXpath-Strict"} = 1; + + my $ualist = $attr{$name}{userattr}; + $ualist = "" if(!$ualist); + my %uahash; + foreach my $a (split(" ", $ualist)) { + if (!$dHash{$a}) { + $uahash{$a} = 1; + #Log3 $name, 3, "$name: keeping $a in userattr list"; + } else { + Log3 $name, 3, "$name: dropping $a from userattr list"; + } + } + $attr{$name}{userattr} = join(" ", sort keys %uahash); + #Log3 $name, 3, "$name: UpgradeAttribute done, userattr list is $attr{$name}{userattr}"; +} + + +# get attribute based specification +# for format, map or similar +# with generic and absolute default (empty variable num part) +# if num is like 1-1 then check for 1 if 1-1 not found +############################################################# +sub HTTPMOD_GetFAttr($$$$;$) +{ + my ($name, $prefix, $num, $type, $val) = @_; + # first look for attribute with the full num in it + if (defined ($attr{$name}{$prefix . $num . $type})) { + $val = $attr{$name}{$prefix . $num . $type}; + # if not found then check if num contains a subnum (for regexes with multiple capture groups etc) and look for attribute without this subnum + } elsif (($num =~ /([0-9]+)-[0-9]+/) && defined ($attr{$name}{$prefix .$1 . $type})) { + $val = $attr{$name}{$prefix . $1 . $type}; + # if again not found then look for generic attribute without num + } elsif (defined ($attr{$name}{$prefix . $type})) { + $val = $attr{$name}{$prefix . $type}; + } + return $val; +} + + +################################################### +# checks and stores obfuscated keys like passwords +# based on / copied from FRITZBOX_storePassword +sub HTTPMOD_StoreKeyValue($$$) +{ + my ($hash, $kName, $value) = @_; + + my $index = $hash->{TYPE}."_".$hash->{NAME}."_".$kName; + my $key = getUniqueId().$index; + my $enc = ""; + + if(eval "use Digest::MD5;1") + { + $key = Digest::MD5::md5_hex(unpack "H*", $key); + $key .= Digest::MD5::md5_hex($key); + } + + for my $char (split //, $value) + { + my $encode=chop($key); + $enc.=sprintf("%.2x",ord($char)^ord($encode)); + $key=$encode.$key; + } + + my $err = setKeyValue($index, $enc); + return "error while saving the value - $err" if(defined($err)); + return undef; +} + + +##################################################### +# reads obfuscated value +sub HTTPMOD_ReadKeyValue($$) +{ + my ($hash, $kName) = @_; + my $name = $hash->{NAME}; + + my $index = $hash->{TYPE}."_".$hash->{NAME}."_".$kName; + my $key = getUniqueId().$index; + + my ($value, $err); + + Log3 $name, 5, "$name: ReadKeyValue tries to read value for $kName from file"; + ($err, $value) = getKeyValue($index); + + if ( defined($err) ) { + Log3 $name, 4, "$name: ReadKeyValue is unable to read value from file: $err"; + return undef; + } + + if ( defined($value) ) { + if ( eval "use Digest::MD5;1" ) { + $key = Digest::MD5::md5_hex(unpack "H*", $key); + $key .= Digest::MD5::md5_hex($key); + } + + my $dec = ''; + + for my $char (map { pack('C', hex($_)) } ($value =~ /(..)/g)) { + my $decode=chop($key); + $dec.=chr(ord($char)^ord($decode)); + $key=$decode.$key; + } + + return $dec; + } + else { + Log3 $name, 4, "$name: ReadKeyValue could not find key $kName in file"; + return undef; + } +} + + +# replace strings as defined in Attributes for URL, Header and Data +# type is request type and can be set01, get03, auth01, update +######################################################################### +sub HTTPMOD_Replace($$$) +{ + my ($hash, $type, $string) = @_; + my $name = $hash->{NAME}; + my $context = ""; + + if ($type =~ /(auth|set|get)(.*)/) { + $context = $1; # context is type without num + # for type update there is no num so no individual replacement - only one for the whiole update request + } + + #Log3 $name, 4, "$name: Replace called for request type $type"; + # Loop through all Replacement Regex attributes + foreach my $rr (sort grep (/replacement[0-9]*Regex/, keys %{$attr{$name}})) { + $rr =~ /replacement([0-9]*)Regex/; + my $rNum = $1; + #Log3 $name, 5, "$name: Replace: rr=$rr, rNum $rNum, look for ${type}Replacement${rNum}Value"; + my $regex = AttrVal($name, "replacement${rNum}Regex", ""); + my $mode = AttrVal($name, "replacement${rNum}Mode", "text"); + next if (!$regex); + + my $value = ""; + if ($context && defined ($attr{$name}{"${type}Replacement${rNum}Value"})) { + # get / set / auth mit individuellem Replacement f�r z.B. get01 + $value = $attr{$name}{"${type}Replacement${rNum}Value"}; + } elsif ($context && defined ($attr{$name}{"${context}Replacement${rNum}Value"})) { + # get / set / auth mit generischem Replacement f�r alle gets / sets + $value = $attr{$name}{"${context}Replacement${rNum}Value"}; + } elsif (defined ($attr{$name}{"replacement${rNum}Value"})) { + # ganz generisches Replacement + $value = $attr{$name}{"replacement${rNum}Value"}; + } + Log3 $name, 5, "$name: Replace called for type $type, regex $regex, mode $mode, " . + ($value ? "value $value" : "empty value") . " input: $string"; + + my $match = 0; + if ($mode eq 'text') { + $match = ($string =~ s/$regex/$value/g); + } elsif ($mode eq 'reading') { + my $device = $name; + my $reading = $value; + if ($value =~ /^([^\:]+):(.+)$/) { + $device = $1; + $reading = $2; + } + my $rvalue = ReadingsVal($device, $reading, ""); + if ($string =~ s/$regex/$rvalue/g) { + Log3 $name, 5, "$name: Replace: reading value is $rvalue"; + $match = 1; + } + } elsif ($mode eq 'internal') { + my $device = $name; + my $internal = $value; + if ($value =~ /^([^\:]+):(.+)$/) { + $device = $1; + $internal = $2; + } + my $rvalue = InternalVal($device, $internal, ""); + if ($string =~ s/$regex/$rvalue/g) { + Log3 $name, 5, "$name: Replace: internal value is $rvalue"; + $match = 1; + } + } elsif ($mode eq 'expression') { + $match = eval {$string =~ s/$regex/$value/gee}; + if ($@) { + Log3 $name, 3, "$name: Replace: invalid regex / expression: /$regex/$value/gee - $@"; + } + } elsif ($mode eq 'key') { + my $rvalue = HTTPMOD_ReadKeyValue($hash, $value); + if ($string =~ s/$regex/$rvalue/g) { + Log3 $name, 5, "$name: Replace: key $value value is $rvalue"; + $match = 1; + } + } + Log3 $name, 5, "$name: Replace: match and result is $string" if ($match); + } + return $string; +} + + +# +######################################################################### +sub HTTPMOD_ModifyWithExpr($$$$$) +{ + my ($name, $context, $num, $attr, $text) = @_; + my $exp = AttrVal($name, "${context}${num}${attr}", undef); + if ($exp) { + my $old = $text; + $text = eval($exp); + if ($@) { + Log3 $name, 3, "$name: error in $attr for $context $num: $@"; + } + Log3 $name, 5, "$name: $context $num used $attr to convert\n$old\nto\n$text\nusing expr $exp"; + } + return $text; +} + + + +# +######################################################################### +sub HTTPMOD_PrepareRequest($$;$) +{ + my ($hash, $context, $num) = @_; + my $name = $hash->{NAME}; + my ($url, $header, $data, $exp); + $num = 0 if (!$num); # num is not passed wehn called for update request + + if ($context eq "reading") { + # called from GetUpdate - not Get / Set / Auth + $url = $hash->{MainURL}; + $header = join ("\r\n", map ($attr{$name}{$_}, sort grep (/requestHeader/, keys %{$attr{$name}}))); + $data = join ("\r\n", map ($attr{$name}{$_}, sort grep (/requestData/, keys %{$attr{$name}}))); + } else { + # called for Get / Set / Auth + # hole alle Header bzw. generischen Header ohne Nummer + $header = join ("\r\n", map ($attr{$name}{$_}, sort grep (/${context}${num}Header/, keys %{$attr{$name}}))); + if (length $header == 0) { + $header = join ("\r\n", map ($attr{$name}{$_}, sort grep (/${context}Header/, keys %{$attr{$name}}))); + } + if (! HTTPMOD_GetFAttr($name, $context, $num, "NoData")) { + # hole Bestandteile der Post data + $data = join ("\r\n", map ($attr{$name}{$_}, sort grep (/${context}${num}Data/, keys %{$attr{$name}}))); + if (length $data == 0) { + $data = join ("\r\n", map ($attr{$name}{$_}, sort grep (/${context}Data/, keys %{$attr{$name}}))); + } + } + # hole URL + $url = HTTPMOD_GetFAttr($name, $context, $num, "URL"); + if (!$url) { + $url = $hash->{MainURL}; + } + } + + $header = HTTPMOD_ModifyWithExpr($name, $context, $num, "HdrExpr", $header); + $data = HTTPMOD_ModifyWithExpr($name, $context, $num, "DatExpr", $data); + $url = HTTPMOD_ModifyWithExpr($name, $context, $num, "URLExpr", $url); + + if (AttrVal($name, "enableCookies", 0) && $hash->{HTTPCookies}) { + Log3 $name, 5, "$name: PrepareRequest is adding Cookies: " . $hash->{HTTPCookies}; + $header .= "Cookie: " . $hash->{HTTPCookies}; + } + + return ($url, $header, $data); +} + + # create a new authenticated session ######################################################################### sub HTTPMOD_Auth($@) { my ( $hash, @a ) = @_; my $name = $hash->{NAME}; - + my ($url, $header, $data); + # get all steps my %steps; foreach my $attr (keys %{$attr{$name}}) { - if ($attr =~ "sid([0-9]+).+") { + if ($attr =~ /sid([0-9]+).+/) { $steps{$1} = 1; } } Log3 $name, 4, "$name: Auth called with Steps: " . join (" ", sort keys %steps); - - $hash->{sid} = ""; - foreach my $step (sort keys %steps) { - - my ($url, $header, $data, $type, $retrycount, $ignoreredirects); - # hole alle Header bzw. generischen Header ohne Nummer - $header = join ("\r\n", map ($attr{$name}{$_}, sort grep (/sid${step}Header/, keys %{$attr{$name}}))); - if (length $header == 0) { - $header = join ("\r\n", map ($attr{$name}{$_}, sort grep (/sidHeader/, keys %{$attr{$name}}))); - } - # hole Bestandteile der Post Data - $data = join ("\r\n", map ($attr{$name}{$_}, sort grep (/sid${step}Data/, keys %{$attr{$name}}))); - if (length $data == 0) { - $data = join ("\r\n", map ($attr{$name}{$_}, sort grep (/sidData/, keys %{$attr{$name}}))); - } - # hole URL - $url = AttrVal($name, "sid${step}URL", undef); - if (!$url) { - $url = AttrVal($name, "sidURL", undef); - } - $ignoreredirects = AttrVal($name, "sid${step}IgnoreRedirects", undef); - $retrycount = 0; - $type = "Auth$step"; + + $hash->{sid} = "" if AttrVal($name, "clearSIdBeforeAuth", 0); + foreach my $step (sort {$b cmp $a} keys %steps) { # reverse sort + ($url, $header, $data) = HTTPMOD_PrepareRequest($hash, "sid", $step); if ($url) { - HTTPMOD_AddToQueue($hash, $url, $header, $data, $type, $retrycount, $ignoreredirects); + # add to front of queue (prio) + HTTPMOD_AddToQueue($hash, $url, $header, $data, "auth$step", undef, 0, AttrVal($name, "sid${step}IgnoreRedirects", 0), 1); } else { - Log3 $name, 3, "$name: no URL for $type"; + Log3 $name, 3, "$name: no URL for Auth $step"; } } + HTTPMOD_HandleSendQueue("direct:".$name); # AddToQueue with prio did not call this. return undef; } -# put URL, Header, Data etc. in hash for HTTPUtils Get -# for set with index $setNum -######################################################################### -sub HTTPMOD_DoSet($$$) +# create hint list for set / get ? +######################################## +sub HTTPMOD_UpdateHintList($) { - my ($hash, $setNum, $rawVal) = @_; + my ($hash, $context) = @_; my $name = $hash->{NAME}; - my ($url, $header, $data, $type, $count); - - # hole alle Header bzw. generischen Header ohne Nummer - $header = join ("\r\n", map ($attr{$name}{$_}, sort grep (/set${setNum}Header/, keys %{$attr{$name}}))); - if (length $header == 0) { - $header = join ("\r\n", map ($attr{$name}{$_}, sort grep (/setHeader/, keys %{$attr{$name}}))); - } - # hole Bestandteile der Post data - $data = join ("\r\n", map ($attr{$name}{$_}, sort grep (/set${setNum}Data/, keys %{$attr{$name}}))); - if (length $data == 0) { - $data = join ("\r\n", map ($attr{$name}{$_}, sort grep (/setData/, keys %{$attr{$name}}))); - } - # hole URL - $url = AttrVal($name, "set${setNum}URL", undef); - if (!$url) { - $url = AttrVal($name, "setURL", undef); - } - if (!$url) { - $url = $hash->{MainURL}; - } - - # ersetze $val in header, data und URL - $header =~ s/\$val/$rawVal/g; - $data =~ s/\$val/$rawVal/g; - $url =~ s/\$val/$rawVal/g; - - $type = "Set$setNum"; - if ($url) { - HTTPMOD_AddToQueue($hash, $url, $header, $data, $type); + Log3 $name, 5, "$name: UpdateHintList called"; + $hash->{".getList"} = ""; + if (AttrVal($name, "enableControlSet", undef)) { # spezielle Sets freigeschaltet? + #$hash->{".setList"} = "interval reread:noArg stop:noArg start:noArg "; + $hash->{".setList"} = "interval reread:noArg stop:noArg start:noArg upgradeAttributes:noArg storeKeyValue "; } else { - Log3 $name, 3, "$name: no URL for $type"; + $hash->{".setList"} = ""; } - return undef; + foreach my $aName (grep /[gs]et[0-9]+Name/, keys %{$attr{$name}}) { + if ($aName =~ /([gs]et)([0-9]+)Name/) { + my $context = $1; + my $num = $2; + my $opt; + my $oName = $attr{$name}{$aName}; # value of the [gs]etXName attribute is name of the set/get option + + if ($context eq "set") { + my $map = ""; + $map = AttrVal($name, "${context}${num}Map", "") if ($context ne "get"); # old Map for set is now IMap (Input) + $map = AttrVal($name, "${context}${num}IMap", $map); # new syntax ovverides old one + if ($map) { + my $hint = $map; # create hint from map + $hint =~ s/([^,\$]+):([^,\$]+)(,?) */$2$3/g; # allow spaces in names + $hint =~ s/\s/ /g; # convert spaces for fhemweb + $opt = $oName . ":$hint"; # opt is Name:Hint (from Map) + } elsif (AttrVal($name, "${context}${num}NoArg", undef)) { # NoArg explicitely specified for a set? + $opt = $oName . ":noArg"; + } else { + $opt = $oName; # nur den Namen f�r opt verwenden. + } + } elsif ($context eq "get") { + if (AttrVal($name, "${context}${num}TextArg", undef)) { # TextArg explicitely specified for a get? + $opt = $oName; # nur den Namen f�r opt verwenden. + } else { + $opt = $oName . ":noArg"; # sonst noArg bei get + } + } + if (AttrVal($name, "${context}${num}Hint", undef)) { # gibt es einen expliziten Hint? + $opt = $oName . ":" . AttrVal($name, "${context}${num}Hint", undef); + } + $hash->{".${context}List"} .= $opt . " "; # save new hint list + } + } + delete $hash->{".updateHintList"}; + Log3 $name, 5, "$name: UpdateHintList: setlist = " . $hash->{".setList"}; + Log3 $name, 5, "$name: UpdateHintList: getlist = " . $hash->{".getList"}; +} + + +# +# SET command - handle predifined control sets +################################################ +sub HTTPMOD_ControlSet($$$) +{ + my ($hash, $setName, $setVal) = @_; + my $name = $hash->{NAME}; + + if ($setName eq 'interval') { + if (!$setVal) { + Log3 $name, 3, "$name: no interval (sec) specified in set, continuing with $hash->{Interval} (sec)"; + return "No Interval specified"; + } else { + if (int $setVal > 5) { + $hash->{Interval} = $setVal; + my $nextTrigger = gettimeofday() + $hash->{Interval}; + RemoveInternalTimer("update:$name"); + $hash->{TRIGGERTIME} = $nextTrigger; + $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger); + InternalTimer($nextTrigger, "HTTPMOD_GetUpdate", "update:$name", 0); + Log3 $name, 3, "$name: timer interval changed to $hash->{Interval} seconds"; + return "0"; + } elsif (int $setVal <= 5) { + Log3 $name, 3, "$name: interval $setVal (sec) to small (must be >5), continuing with $hash->{Interval} (sec)"; + return "interval too small"; + } + } + } elsif ($setName eq 'reread') { + HTTPMOD_GetUpdate("reread:$name"); + return "0"; + } elsif ($setName eq 'stop') { + RemoveInternalTimer("update:$name"); + $hash->{TRIGGERTIME} = 0; + $hash->{TRIGGERTIME_FMT} = ""; + Log3 $name, 3, "$name: internal interval timer stopped"; + return "0"; + } elsif ($setName eq 'start') { + my $nextTrigger = gettimeofday() + $hash->{Interval}; + $hash->{TRIGGERTIME} = $nextTrigger; + $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger); + RemoveInternalTimer("update:$name"); + InternalTimer($nextTrigger, "HTTPMOD_GetUpdate", "update:$name", 0); + Log3 $name, 5, "$name: internal interval timer set to call GetUpdate in " . int($hash->{Interval}). " seconds"; + return "0"; + } elsif ($setName eq 'upgradeAttributes') { + HTTPMOD_UpgradeAttributes($hash); + return "0"; + } elsif ($setName eq 'storeKeyValue') { + my $key; + if ($setVal =~ /([^ ]+) +(.*)/) { + $key = $1; + my $err = HTTPMOD_StoreKeyValue($hash, $key, $2); + return $err if ($err); + } else { + return "Please give a key and a value to storeKeyValue"; + } + return "0"; + } + return undef; # no control set identified - continue with other sets } @@ -378,14 +1105,14 @@ sub HTTPMOD_DoSet($$$) ######################################################################### sub HTTPMOD_Set($@) { - my ( $hash, @a ) = @_; - return "\"set HTTPMOD\" needs at least an argument" if ( @a < 2 ); + my ($hash, @a) = @_; + return "\"set HTTPMOD\" needs at least an argument" if (@a < 2); - # @a is an array with DeviceName, setName and setVal - my ($name, $setName, $setVal) = @a; - my (%rmap, $setNum, $setOpt, $setList, $rawVal); - $setList = ""; - + # @a is an array with the command line: DeviceName, setName. Rest is setVal (splitted in fhem.pl by space and tab) + my ($name, $setName, @setValArr) = @a; + my $setVal = (@setValArr ? join(' ', @setValArr) : ""); + my (%rmap, $setNum, $setOpt, $rawVal); + if (AttrVal($name, "disable", undef)) { Log3 $name, 5, "$name: set called with $setName but device is disabled" if ($setName ne "?"); @@ -396,75 +1123,27 @@ sub HTTPMOD_Set($@) if ($setName ne "?"); if (AttrVal($name, "enableControlSet", undef)) { # spezielle Sets freigeschaltet? - $setList = "interval reread:noArg stop:noArg start:noArg "; - if ($setName eq 'interval') { - if (int $setVal > 5) { - $hash->{Interval} = $setVal; - my $nextTrigger = gettimeofday() + $hash->{Interval}; - RemoveInternalTimer("update:$name"); - $hash->{TRIGGERTIME} = $nextTrigger; - $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger); - InternalTimer($nextTrigger, "HTTPMOD_GetUpdate", "update:$name", 0); - Log3 $name, 3, "$name: timer interval changed to $hash->{Interval} seconds"; - return undef; - } elsif (int $setVal <= 5) { - Log3 $name, 3, "$name: interval $setVal (sec) to small (must be >5), continuing with $hash->{Interval} (sec)"; - } else { - Log3 $name, 3, "$name: no interval (sec) specified in set, continuing with $hash->{Interval} (sec)"; - } - } elsif ($setName eq 'reread') { - HTTPMOD_GetUpdate("reread:$name"); - return undef; - } elsif ($setName eq 'stop') { - RemoveInternalTimer("update:$name"); - $hash->{TRIGGERTIME} = 0; - $hash->{TRIGGERTIME_FMT} = ""; - Log3 $name, 3, "$name: internal interval timer stopped"; - return undef; - } elsif ($setName eq 'start') { - my $nextTrigger = gettimeofday() + $hash->{Interval}; - $hash->{TRIGGERTIME} = $nextTrigger; - $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger); - RemoveInternalTimer("update:$name"); - InternalTimer($nextTrigger, "HTTPMOD_GetUpdate", "update:$name", 0); - Log3 $name, 5, "$name: internal interval timer set to call GetUpdate in " . int($hash->{Interval}). " seconds"; - return undef; - } + my $error = HTTPMOD_ControlSet($hash, $setName, $setVal); + return undef if (defined($error) && $error eq "0"); # control set found and done. + return $error if ($error); # error + # continue if function returned undef } - # verarbeite Attribute "set[0-9]*Name set[0-9]*URL set[0-9]*Data.* set[0-9]*Header.* - # set[0-9]*Min set[0-9]*Max set[0-9]*Map set[0-9]*Expr set[0-9]*Hint - # Vorbereitung: - # suche den �bergebenen setName in den Attributen, setze setNum und erzeuge rmap falls gefunden + # suche den �bergebenen setName in den Attributen und setze setNum + foreach my $aName (keys %{$attr{$name}}) { - if ($aName =~ "set([0-9]+)Name") { # ist das Attribut ein "setXName" ? - my $setI = $1; # merke die Nummer im Namen - my $iName = $attr{$name}{$aName}; # Name der Set-Option diser Schleifen-Iteration - - if ($setName eq $iName) { # ist es der im konkreten Set verwendete setName? - $setNum = $setI; # gefunden -> merke Nummer X im Attribut - } - - # erzeuge setOpt f�r die R�ckgabe bei set X ? - if (AttrVal($name, "set${setI}Map", undef)) { # nochmal: gibt es eine Map (f�r Hint) - my $hint = AttrVal($name, "set${setI}Map", undef); # create hint from map - $hint =~ s/([^ ,\$]+):([^ ,\$]+,?) ?/$2/g; - $setOpt = $iName . ":$hint"; # setOpt ist Name:Hint (aus Map) - } else { - $setOpt = $iName; # nur den Namen f�r setopt verwenden. - } - if (AttrVal($name, "set${setI}Hint", undef)) { # gibt es einen expliziten Hint? - $setOpt = $iName . ":" . - AttrVal($name, "set${setI}Hint", undef); - } - $setList .= $setOpt . " "; # speichere Liste mit allen Sets inkl. der Hints nach ":" f�r R�ckgabe bei Set ? + if ($aName =~ /set([0-9]+)Name/) { # ist das Attribut ein "setXName" ? + if ($setName eq $attr{$name}{$aName}) { # ist es der im konkreten Set verwendete setName? + $setNum = $1; # gefunden -> merke Nummer X im Attribut + } } } # g�ltiger set Aufruf? ($setNum oben schon gesetzt?) if(!defined ($setNum)) { - return "Unknown argument $setName, choose one of $setList"; + HTTPMOD_UpdateHintList($hash) if ($hash->{".updateHintList"}); + return "Unknown argument $setName, choose one of " . $hash->{".setList"}; } Log3 $name, 5, "$name: set found option $setName in attribute set${setNum}Name"; @@ -476,11 +1155,18 @@ sub HTTPMOD_Set($@) # Eingabevalidierung von Sets mit Definition per Attributen # 1. Schritt, falls definiert, per Umkehrung der Map umwandeln (z.B. Text in numerische Codes) - if (AttrVal($name, "set${setNum}Map", undef)) { # gibt es eine Map? - my $rm = AttrVal($name, "set${setNum}Map", undef); - #$rm =~ s/([^ ,\$]+):([^ ,\$]+),? ?/$2 $1 /g; # reverse map string erzeugen - $rm =~ s/([^, ][^,\$]*):([^, ][^,\$]*),? ?/$2:$1, /g; # reverse map string erzeugen - %rmap = split (/, +|:/, $rm); # reverse hash aus dem reverse string + + + my $map = AttrVal($name, "set${setNum}Map", ""); # old Map for set is now IMap (Input) + $map = AttrVal($name, "set${setNum}IMap", $map); # new syntax ovverides old one + if ($map) { + my $rm = $map; + $rm =~ s/([^, ][^,\$]*):([^,][^,\$]*),? */$2:$1, /g; # reverse map string erzeugen + $setVal = decode ('UTF-8', $setVal); # convert nbsp from fhemweb + $setVal =~ s/\s| / /g; # back to normal spaces + + %rmap = split (/, *|:/, $rm); # reverse hash aus dem reverse string + if (defined($rmap{$setVal})) { # Eintrag f�r den �bergebenen Wert in der Map? $rawVal = $rmap{$setVal}; # entsprechender Raw-Wert f�r das Ger�t Log3 $name, 5, "$name: set found $setVal in rmap and converted to $rawVal"; @@ -498,15 +1184,17 @@ sub HTTPMOD_Set($@) } $rawVal = $setVal; } - + + # kein TextArg? if (!AttrVal($name, "set${setNum}TextArg", undef)) { - # 2. Schritt: falls definiert Min- und Max-Werte pr�fen - falls kein TextArg + # pr�fe Min if (AttrVal($name, "set${setNum}Min", undef)) { my $min = AttrVal($name, "set${setNum}Min", undef); Log3 $name, 5, "$name: is checking value $rawVal against min $min"; return "set value $rawVal is smaller than Min ($min)" if ($rawVal < $min); } + # Pr�fe Max if (AttrVal($name, "set${setNum}Max", undef)) { my $max = AttrVal($name, "set${setNum}Max", undef); Log3 $name, 5, "$name: set is checking value $rawVal against max $max"; @@ -515,82 +1203,30 @@ sub HTTPMOD_Set($@) } } - # 3. Schritt: Konvertiere mit setexpr falls definiert - if (AttrVal($name, "set${setNum}Expr", undef)) { + # Konvertiere input mit IExpr falls definiert + my $exp = AttrVal($name, "set${setNum}Expr", ""); # old syntax for input in set + $exp = AttrVal($name, "set${setNum}IExpr", ""); # new syntax overrides old one + if ($exp) { my $val = $rawVal; - my $exp = AttrVal($name, "set${setNum}Expr", undef); $rawVal = eval($exp); - Log3 $name, 5, "$name: set converted value $val to $rawVal using expr $exp"; - } - + if ($@) { + Log3 $name, 3, "$name: Set error in setExpr $exp: $@"; + } else { + Log3 $name, 5, "$name: set converted value $val to $rawVal using expr $exp"; + } + } Log3 $name, 4, "$name: set will now set $setName -> $rawVal"; - my $result = HTTPMOD_DoSet($hash, $setNum, $rawVal); - return "$setName -> $rawVal"; } else { + # NoArg + $rawVal = 0; Log3 $name, 4, "$name: set will now set $setName"; - HTTPMOD_DoSet($hash, $setNum, 0); - return $setName; } - -} - - - -# put URL, Header, Data etc. in hash for HTTPUtils Get -# for get with index $getNum -######################################################################### -sub HTTPMOD_DoGet($$) -{ - my ($hash, $getNum) = @_; - my $name = $hash->{NAME}; - my ($url, $header, $data, $type, $count); - my $seq = $hash->{GetSeq}; - - # hole alle Header bzw. generischen Header ohne Nummer - $header = join ("\r\n", map ($attr{$name}{$_}, sort grep (/get${getNum}Header/, keys %{$attr{$name}}))); - if (length $header == 0) { - $header = join ("\r\n", map ($attr{$name}{$_}, sort grep (/getHeader/, keys %{$attr{$name}}))); - } - if (AttrVal($name, "get${getNum}HdrExpr", undef)) { - my $exp = AttrVal($name, "get${getNum}HdrExpr", undef); - my $old = $header; - $header = eval($exp); - Log3 $name, 5, "$name: get converted the header $old\n to $header\n using expr $exp"; - } - - # hole Bestandteile der Post data - $data = join ("\r\n", map ($attr{$name}{$_}, sort grep (/get${getNum}Data/, keys %{$attr{$name}}))); - if (length $data == 0) { - $data = join ("\r\n", map ($attr{$name}{$_}, sort grep (/getData/, keys %{$attr{$name}}))); - } - if (AttrVal($name, "get${getNum}DatExpr", undef)) { - my $exp = AttrVal($name, "get${getNum}DatExpr", undef); - my $old = $data; - $data = eval($exp); - Log3 $name, 5, "$name: get converted the post data $old\n to $data\n using expr $exp"; - } - - # hole URL - $url = AttrVal($name, "get${getNum}URL", undef); - if (!$url) { - $url = AttrVal($name, "getURL", undef); - } - if (AttrVal($name, "get${getNum}URLExpr", undef)) { - my $exp = AttrVal($name, "get${getNum}URLExpr", undef); - my $old = $url; - $url = eval($exp); - Log3 $name, 5, "$name: get converted the url $old to $url using expr $exp"; - } - if (!$url) { - $url = $hash->{MainURL}; - } - - $type = "Get$getNum"; + my ($url, $header, $data) = HTTPMOD_PrepareRequest($hash, "set", $setNum); if ($url) { - HTTPMOD_AddToQueue($hash, $url, $header, $data, $type); + HTTPMOD_AddToQueue($hash, $url, $header, $data, "set$setNum", $rawVal); } else { - Log3 $name, 3, "$name: no URL for $type"; + Log3 $name, 3, "$name: no URL for set $setNum"; } return undef; @@ -602,65 +1238,64 @@ sub HTTPMOD_DoGet($$) ######################################################################### sub HTTPMOD_Get($@) { - my ( $hash, @a ) = @_; + my ($hash, @a) = @_; return "\"get HTTPMOD\" needs at least an argument" if ( @a < 2 ); - # @a is an array with DeviceName, getName - my ($name, $getName) = @a; - my ($getNum, $getList); - $hash->{GetSeq} = 0; - $getList = ""; + # @a is an array with DeviceName, getName, options + my ($name, $getName, @getValArr) = @a; + my $getVal = (@getValArr ? join(' ', @getValArr) : ""); # optional value after get name - might be used in HTTP request + my $getNum; if (AttrVal($name, "disable", undef)) { Log3 $name, 5, "$name: get called with $getName but device is disabled" if ($getName ne "?"); return undef; } - - Log3 $name, 5, "$name: get called with $getName " - if ($getName ne "?"); + Log3 $name, 5, "$name: get called with $getName " if ($getName ne "?"); - # verarbeite Attribute "get[0-9]*Name get[0-9]*URL get[0-9]*Data.* get[0-9]*Header.* - # Vorbereitung: # suche den �bergebenen getName in den Attributen, setze getNum falls gefunden foreach my $aName (keys %{$attr{$name}}) { - if ($aName =~ "get([0-9]+)Name") { # ist das Attribut ein "getXName" ? - my $getI = $1; # merke die Nummer im Namen - my $iName = $attr{$name}{$aName}; # Name der get-Option diser Schleifen-Iteration - - if ($getName eq $iName) { # ist es der im konkreten get verwendete getName? - $getNum = $getI; # gefunden -> merke Nummer X im Attribut + if ($aName =~ /get([0-9]+)Name/) { # ist das Attribut ein "getXName" ? + if ($getName eq $attr{$name}{$aName}) { # ist es der im konkreten get verwendete getName? + $getNum = $1; # gefunden -> merke Nummer X im Attribut } - $getList .= $iName . " "; # speichere Liste mit allen gets f�r R�ckgabe bei get ? } } - + # g�ltiger get Aufruf? ($getNum oben schon gesetzt?) if(!defined ($getNum)) { - return "Unknown argument $getName, choose one of $getList"; + HTTPMOD_UpdateHintList($hash) if ($hash->{".updateHintList"}); + return "Unknown argument $getName, choose one of " . $hash->{".getList"}; } Log3 $name, 5, "$name: get found option $getName in attribute get${getNum}Name"; - Log3 $name, 4, "$name: get will now request $getName"; + Log3 $name, 4, "$name: get will now request $getName" . + ($getVal ? ", value = $getVal" : ", no optional value"); + + my ($url, $header, $data) = HTTPMOD_PrepareRequest($hash, "get", $getNum); + if ($url) { + HTTPMOD_AddToQueue($hash, $url, $header, $data, "get$getNum", $getVal); + } else { + Log3 $name, 3, "$name: no URL for Get $getNum"; + } - my $result = HTTPMOD_DoGet($hash, $getNum); return "$getName requested, watch readings"; } - # # request new data from device +# calltype can be update and reread ################################### sub HTTPMOD_GetUpdate($) { - my ($calltype,$name) = split(':', $_[0]); + my ($calltype, $name) = split(':', $_[0]); my $hash = $defs{$name}; - my ($url, $header, $data, $type, $count); + my ($url, $header, $data, $count); my $now = gettimeofday(); Log3 $name, 4, "$name: GetUpdate called ($calltype)"; - + if ($calltype eq "update" && $hash->{Interval}) { RemoveInternalTimer ("update:$name"); my $nt = gettimeofday() + $hash->{Interval}; @@ -675,17 +1310,13 @@ sub HTTPMOD_GetUpdate($) return undef; } - if ( $hash->{MainURL} ne "none" ) { - $url = $hash->{MainURL}; - $header = join ("\r\n", map ($attr{$name}{$_}, sort grep (/requestHeader/, keys %{$attr{$name}}))); - $data = join ("\r\n", map ($attr{$name}{$_}, sort grep (/requestData/, keys %{$attr{$name}}))); - $type = "Update"; - + if ($hash->{MainURL}) { # queue main get request + ($url, $header, $data) = HTTPMOD_PrepareRequest($hash, "reading"); # context "reading" is used for other attrs relevant for GetUpdate if ($url) { - HTTPMOD_AddToQueue($hash, $url, $header, $data, $type); + HTTPMOD_AddToQueue($hash, $url, $header, $data, "update"); # use request type "update" } else { - Log3 $name, 3, "$name: no URL for $type"; + Log3 $name, 3, "$name: GetUpdate: no Main URL specified"; } } @@ -705,147 +1336,638 @@ sub HTTPMOD_GetUpdate($) Log3 $name, 5, "$name: GetUpdate will request $getName"; $hash->{lastpoll}{$getName} = $now; - # hole alle Header bzw. generischen Header ohne Nummer - $header = join ("\r\n", map ($attr{$name}{$_}, sort grep (/get${getNum}Header/, keys %{$attr{$name}}))); - if (length $header == 0) { - $header = join ("\r\n", map ($attr{$name}{$_}, sort grep (/getHeader/, keys %{$attr{$name}}))); - } - # hole Bestandteile der Post data - $data = join ("\r\n", map ($attr{$name}{$_}, sort grep (/get${getNum}Data/, keys %{$attr{$name}}))); - if (length $data == 0) { - $data = join ("\r\n", map ($attr{$name}{$_}, sort grep (/getData/, keys %{$attr{$name}}))); - } - # hole URL - $url = AttrVal($name, "get${getNum}URL", undef); - if (!$url) { - $url = AttrVal($name, "getURL", undef); - } - if (!$url) { - $url = $hash->{MainURL} if ( $hash->{MainURL} ne "none" ); - } - - $type = "Get$getNum"; + ($url, $header, $data) = HTTPMOD_PrepareRequest($hash, "get", $getNum); if ($url) { - HTTPMOD_AddToQueue($hash, $url, $header, $data, $type); + HTTPMOD_AddToQueue($hash, $url, $header, $data, "get$getNum"); } else { - Log3 $name, 3, "$name: no URL to get $type"; - } + Log3 $name, 3, "$name: no URL for Get $getNum"; + } } else { Log3 $name, 5, "$name: GetUpdate will skip $getName, delay not over"; } + } else { + Log3 $name, 3, "$name: GetUpdate found $poll without a matching Name attribute - ignoring it"; } } } -# extract one reading for a buffer -# and apply Expr, Map and Format -################################### -sub HTTPMOD_ExtractReading($$$$$$$$$) +# Try to call a parse function if defined +######################################### +sub HTTPMOD_TryCall($$$$) { - my ($hash, $buffer, $reading, $regex, $expr, $map, $format, $decode, $encode) = @_; + my ($hash, $buffer, $fName, $type) = @_; my $name = $hash->{NAME}; - my $val = ""; - my $match; + if (AttrVal($name, $fName, undef)) { + Log3 $name, 5, "$name: Read is calling $fName for HTTP Response to $type"; + my $func = AttrVal($name, 'parseFunction1', undef); + no strict "refs"; + eval { &{$func}($hash,$buffer) }; + if( $@ ) { + Log3 $name, 3, "$name: error calling $func: $@"; + } + use strict "refs"; + } +} - if (AttrVal($name, "enableXPath", undef) && $regex =~ /^xpath:(.*)/) { - Log3 $name, 5, "$name: ExtractReading $reading with xpath $1 ..."; - my $xpath = $1; - my $tree = HTML::TreeBuilder::XPath->new; - my $html = $buffer; - $html =~ s/.*?(\r\n){2}//s; # remove HTTP-header - - # if the xpath isn't syntactically correct, fhem would crash - # the use of eval prevents this from happening - $val = eval(' - $tree->parse($html); - $val = join ",", $tree->findvalues($xpath); - $tree->delete(); - $val; - '); - $match = $val; - } elsif (AttrVal($name, "enableXPath-Strict", undef) && $regex =~ /^xpath-strict:(.*)/) { - Log3 $name, 5, "$name: ExtractReading $reading with strict xpath $1 ..."; - my $xpath = $1; - my $xml= $buffer; - $xml =~ s/.*?(\r\n){2}//s; # remove HTTP-header - - # if the xml isn't wellformed, fhem would crash - # the use of eval prevents this from happening - $val = eval(' - my $xp = XML::XPath->new(xml => $xml); - my $nodeset = $xp->find($xpath); - my @vals; - foreach my $node ($nodeset->get_nodelist) { - push @vals, XML::XPath::XMLParser::as_string($node); - } - $val = join ",", @vals; - $xp->cleanup(); - $val; - '); - $match = $val; + +# recoursive main part for +# HTTPMOD_FlattenJSON($$) +################################### +sub HTTPMOD_JsonFlatter($$;$) +{ + my ($hash,$ref,$prefix) = @_; + my $name = $hash->{NAME}; + + $prefix = "" if( !$prefix ); + + #Log3 $name, 5, "$name: JSON Flatter with prefix $prefix, ref $ref, pointer to " . ref($ref); + if (ref($ref) eq "ARRAY" ) { + while( my ($key,$value) = each @{$ref}) { + #Log3 $name, 5, "$name: JSON Flatter recursive call in array while, key = $key, value = $value"; + HTTPMOD_JsonFlatter($hash, $value, $prefix.sprintf("%02i",$key+1)."_"); + } + } elsif (ref($ref) eq "HASH" ) { + while( my ($key,$value) = each %{$ref}) { + #Log3 $name, 5, "$name: JSON Flatter in hash while, key = $key, value = $value, ref(value) = " . ref($value); + if(ref($value) eq "HASH" or ref($value) eq "ARRAY") { + #Log3 $name, 5, "$name: JSON Flatter recursive call in hash while, key = $key, value = $value"; + HTTPMOD_JsonFlatter($hash, $value, $prefix.$key."_"); + } else { + if (defined ($value)) { + Log3 $name, 5, "$name: JSON Flatter sets $prefix$key to $value"; + $hash->{ParserData}{JSON}{$prefix.$key} = $value; + } + } + } + } +} + +# entry to create a flat hash +# out of a pares JSON hash hierarchy +#################################### +sub HTTPMOD_FlattenJSON($$) +{ + my ($hash, $buffer) = @_; + my $name = $hash->{NAME}; + + my $decoded = eval 'decode_json($buffer)'; + if ($@) { + Log3 $name, 3, "$name: error while parsing JSON data: $@"; } else { - Log3 $name, 5, "$name: ExtractReading $reading with regex /$regex/..."; - $match = ($buffer =~ /$regex/); - $val = $1 if ($match); + HTTPMOD_JsonFlatter($hash, $decoded); + Log3 $name, 5, "$name: extracted JSON values to internal"; + } +} + + +# format a reading value +################################### +sub HTTPMOD_FormatReading($$$$) +{ + my ($name, $context, $num, $val) = @_; + my ($format, $decode, $encode); + my $expr = ""; + my $map = ""; + + if ($context eq "reading") { + $expr = AttrVal($name, 'readingsExpr' . $num, "") if ($context ne "set"); # very old syntax, not for set! + } + + $decode = HTTPMOD_GetFAttr($name, $context, $num, "Decode"); + $encode = HTTPMOD_GetFAttr($name, $context, $num, "Encode"); + $map = HTTPMOD_GetFAttr($name, $context, $num, "Map") if ($context ne "set"); # not for set! + $map = HTTPMOD_GetFAttr($name, $context, $num, "OMap", $map); # new syntax + $format = HTTPMOD_GetFAttr($name, $context, $num, "Format"); + $expr = HTTPMOD_GetFAttr($name, $context, $num, "Expr", $expr) if ($context ne "set"); # not for set! + $expr = HTTPMOD_GetFAttr($name, $context, $num, "OExpr", $expr); # new syntax + + $val = decode($decode, $val) if ($decode); + $val = encode($encode, $val) if ($encode); + + if ($expr) { + my $old = $val; + $val = eval $expr; + if ($@) { + Log3 $name, 3, "$name: FormatReading error, context $context, expression $expr: $@"; + } + + Log3 $name, 5, "$name: FormatReading changed value with Expr $expr from $old to $val"; } - if ($match) { - - $val = decode($decode, $val) if ($decode); - $val = encode($encode, $val) if ($encode); - - if ($expr) { - $val = eval $expr; - Log3 $name, 5, "$name: ExtractReading changed $reading with Expr $expr from $1 to $val"; + if ($map) { # gibt es eine Map? + my %map = split (/, +|:/, $map); # hash aus dem map string + if (defined($map{$val})) { # Eintrag f�r den gelesenen Wert in der Map? + my $nVal = $map{$val}; # entsprechender sprechender Wert f�r den rohen Wert aus dem Ger�t + Log3 $name, 5, "$name: FormatReading found $val in map and converted to $nVal"; + $val = $nVal; + } else { + Log3 $name, 3, "$name: FormatReading could not match $val to defined map"; } - - if ($map) { # gibt es eine Map? - my %map = split (/, +|:/, $map); # hash aus dem map string - if (defined($map{$val})) { # Eintrag f�r den gelesenen Wert in der Map? - my $nVal = $map{$val}; # entsprechender sprechender Wert f�r den rohen Wert aus dem Ger�t - Log3 $name, 5, "$name: ExtractReading found $val in map and converted to $nVal"; - $val = $nVal; - } else { - Log3 $name, 3, "$name: ExtractReading cound not match $val to defined map"; - } - } - - if ($format) { - Log3 $name, 5, "$name: ExtractReading for $reading does sprintf with format " . $format . - " value is $val"; - $val = sprintf($format, $val); - Log3 $name, 5, "$name: ExtractReading for $reading sprintf result is $val"; - } - - Log3 $name, 5, "$name: ExtractReading sets $reading to $val"; - readingsBulkUpdate( $hash, $reading, $val ); - return 1; - } else { - Log3 $name, 5, "$name: ExtractReading $reading did not match (val is >$val<)"; - return 0; } -} - - -# get attribute based specification -# for format, map or similar -# with generic default (empty variable part) -############################################# -sub HTTPMOD_GetFAttr($$$$) -{ - my ($name, $prefix, $num, $type) = @_; - my $val = ""; - if (defined ($attr{$name}{$prefix . $num . $type})) { - $val = $attr{$name}{$prefix . $num . $type}; - } elsif - (defined ($attr{$name}{$prefix . $type})) { - $val = $attr{$name}{$prefix . $type}; + + if ($format) { + Log3 $name, 5, "$name: FormatReading does sprintf with format " . $format . + " value is $val"; + $val = sprintf($format, $val); + Log3 $name, 5, "$name: FormatReading sprintf result is $val"; } return $val; } +# extract reading for a buffer +################################### +sub HTTPMOD_ExtractReading($$$$) +{ + my ($hash, $buffer, $context, $num) = @_; + my $name = $hash->{NAME}; + my ($val, $reading, $regex) = ("", "", ""); + my ($json, $xpath, $xpathst, $recomb, $regopt, $sublen); + my @subrlist = (); + my @matchlist = (); + my $try = 1; # was there any applicable parsing definition? + + $json = HTTPMOD_GetFAttr($name, $context, $num, "JSON"); + $xpath = HTTPMOD_GetFAttr($name, $context, $num, "XPath"); + $xpathst = HTTPMOD_GetFAttr($name, $context, $num, "XPath-Strict"); + $regopt = HTTPMOD_GetFAttr($name, $context, $num, "RegOpt"); + $recomb = HTTPMOD_GetFAttr($name, $context, $num, "RecombineExpr"); + $sublen = HTTPMOD_GetFAttr($name, $context, $num, "AutoNumLen"); + + # support for old syntax + if ($context eq "reading") { + $reading = AttrVal($name, 'readingsName'.$num, ($json ? $json : "unnamed-$num")); + $regex = AttrVal($name, 'readingsRegex'.$num, ""); + } + # new syntax overrides reading and regex + $reading = HTTPMOD_GetFAttr($name, $context, $num, "Name", $reading); + $regex = HTTPMOD_GetFAttr($name, $context, $num, "Regex", $regex); + + + if ($regex) { + # old syntax for xpath and xpath-strict as prefix in regex - one result joined + if (AttrVal($name, "enableXPath", undef) && $regex =~ /^xpath:(.*)/) { + $xpath = $1; + Log3 $name, 5, "$name: ExtractReading $reading with old XPath syntax in regex /$regex/, xpath = $xpath"; + eval {@matchlist = $hash->{ParserData}{XPathTree}->findnodes_as_strings($xpath)}; + Log3 $name, 3, "$name: error in findvalues for XPathTree: $@" if ($@); + @matchlist = (join ",", @matchlist); # old syntax returns only one value + } elsif (AttrVal($name, "enableXPath-Strict", undef) && $regex =~ /^xpath-strict:(.*)/) { + $xpathst = $1; + Log3 $name, 5, "$name: ExtractReading $reading with old XPath-strict syntax in regex /$regex/..."; + my $nodeset; + eval {$nodeset = $hash->{ParserData}{XPathStrictNodeset}->find($xpathst)}; + if ($@) { + Log3 $name, 3, "$name: error in find for XPathStrictNodeset: $@"; + } else { + foreach my $node ($nodeset->get_nodelist) { + push @matchlist, XML::XPath::XMLParser::as_string($node); + } + } + @matchlist = (join ",", @matchlist); # old syntax returns only one value + } else { + # normal regex + if ($regopt) { + Log3 $name, 5, "$name: ExtractReading $reading with regex /$regex/$regopt ..."; + eval '@matchlist = ($buffer =~ /' . "$regex/$regopt" . ')'; + Log3 $name, 3, "$name: error in regex matching with regex option: $@" if ($@); + } else { + Log3 $name, 5, "$name: ExtractReading $reading with regex /$regex/..."; + @matchlist = ($buffer =~ /$regex/); + } + Log3 $name, 5, "$name: " . @matchlist . " capture group(s), matchlist = " . join ",", @matchlist if (@matchlist); + } + } elsif ($json) { + if (defined($hash->{ParserData}{JSON}) && + defined($hash->{ParserData}{JSON}{$json})) { + @matchlist = ($hash->{ParserData}{JSON}{$json}); + } + } elsif ($xpath) { + Log3 $name, 5, "$name: ExtractReading $reading with XPath $xpath"; + eval {@matchlist = $hash->{ParserData}{XPathTree}->findnodes_as_strings($xpath)}; + Log3 $name, 3, "$name: error in findvalues for XPathTree: $@" if ($@); + } elsif ($xpathst) { + Log3 $name, 5, "$name: ExtractReading $reading with XPath-Strict $xpathst"; + my $nodeset; + eval {$nodeset = $hash->{ParserData}{XPathStrictNodeset}->find($xpathst)}; + if ($@) { + Log3 $name, 3, "$name: error in find for XPathStrictNodeset: $@"; + } else { + foreach my $node ($nodeset->get_nodelist) { + push @matchlist, XML::XPath::XMLParser::as_string($node); + } + } + } else { + $try = 0; # neither regex, xpath nor json attribute found ... + Log3 $name, 5, "$name: ExtractReading for context $context, num $num - no individual parse definition"; + } + + my $match = @matchlist; + if ($match) { + my ($eNum, $subReading); + my $group = 1; + my $subNum = ""; + + if ($recomb) { + Log3 $name, 5, "$name: ExtractReading is recombining $match matches with expression $recomb"; + my $val = (eval $recomb); + if ($@) { + Log3 $name, 3, "$name: ExtractReading error in RecombineExpr: $@"; + } + Log3 $name, 5, "$name: ExtractReading recombined matchlist to $val"; + @matchlist = ($val); + $match = 1; + } + foreach $val (@matchlist) { + if ($match == 1) { + # only one match + $eNum = $num; + $subReading = $reading; + @subrlist = ($reading); + } else { + # multiple matches -> check for special name of readings + $eNum = $num ."-".$group; + # don't use GetFAttr here because we don't want to get the value of the generic attribute "Name" + # but this name with -group number added as default + if (defined ($attr{$name}{$context . $eNum . "Name"})) { + $subReading = $attr{$name}{$context . $eNum . "Name"}; + } else { + if ($sublen) { + $subReading = "${reading}-" . sprintf ("%0${sublen}d", $group); + } else { + $subReading = "${reading}-$group"; + } + $subNum = "-$group"; + } + push @subrlist, $subReading; + } + $val = HTTPMOD_FormatReading($name, $context, $eNum, $val); + Log3 $name, 5, "$name: ExtractReading for match $group sets $subReading to $val"; + readingsBulkUpdate( $hash, $subReading, $val ); + $hash->{defptr}{readingBase}{$subReading} = $context; + $hash->{defptr}{readingNum}{$subReading} = $num; + $hash->{defptr}{readingSubNum}{$subReading} = $subNum; + delete $hash->{defptr}{readingOutdated}{$subReading}; + $group++; + } + } else { + Log3 $name, 5, "$name: ExtractReading $reading did not match" if ($try); + } + return ($try, $match, $reading, @subrlist); +} + + + +# pull log lines to a file +################################### +sub HTTPMOD_PullToFile($$$$) +{ + my ($hash, $buffer, $num, $file) = @_; + my $name = $hash->{NAME}; + + my $reading = HTTPMOD_GetFAttr($name, "get", $num, "Name"); + my $regex = HTTPMOD_GetFAttr($name, "get", $num, "Regex"); + my $iterate = HTTPMOD_GetFAttr($name, "get", $num, "PullIterate"); + my $recombine = HTTPMOD_GetFAttr($name, "get", $num, "RecombineExpr"); + $recombine = '$1' if not ($recombine); + my $matches = 0; + $hash->{GetSeq} = 0 if (!$hash->{GetSeq}); + + Log3 $name, 5, "$name: Read is pulling to file, sequence is $hash->{GetSeq}"; + while ($buffer =~ /$regex/g) { + $matches++; + no warnings qw(uninitialized); + my $val = eval($recombine); + if ($@) { + Log3 $name, 3, "$name: PullToFile error in RecombineExpr $recombine: $@"; + } else { + Log3 $name, 3, "$name: Read pulled line $val"; + } + } + Log3 $name, 3, "$name: Read pulled $matches lines"; + if ($matches) { + if ($iterate && $hash->{GetSeq} < $iterate) { + $hash->{GetSeq}++; + Log3 $name, 5, "$name: Read is iterating pull until $iterate, next is $hash->{GetSeq}"; + my ($url, $header, $data) = HTTPMOD_PrepareRequest($hash, "get", $num); + HTTPMOD_AddToQueue($hash, $url, $header, $data, "get$num"); + } else { + Log3 $name, 5, "$name: Read is done with pull after $hash->{GetSeq}."; + } + } else { + Log3 $name, 5, "$name: Read is done with pull, no more lines matched"; + } + return (1, 1, $reading); +} + + +# check max age of all readings +################################### +sub HTTPMOD_DoMaxAge($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my ($base, $num, $sub, $max, $rep, $mode, $time, $now); + my $readings = $hash->{READINGS}; + return if (!$readings); + $now = gettimeofday(); + readingsBeginUpdate($hash); + foreach my $reading (sort keys %{$readings}) { + my $key = $reading; # in most cases the reading name can be looked up in the readingBase hash + Log3 $name, 5, "$name: MaxAge: check reading $reading"; + if ($hash->{defptr}{readingOutdated}{$reading}) { + Log3 $name, 5, "$name: MaxAge: reading $reading was outdated before - skipping"; + next; + } + + # get base name of definig attribute like "reading" or "get" + $base = $hash->{defptr}{readingBase}{$reading}; + if (!$base && $reading =~ /(.*)(-[0-9]+)$/) { + # reading name endet auf -Zahl und ist nicht selbst per attr Name definiert + # -> suche nach attr Name mit Wert ohne -Zahl + $key = $1; + $base = $hash->{defptr}{readingBase}{$key}; + Log3 $name, 5, "$name: MaxAge: no defptr for this name - reading name seems automatically created with $2 from $key and not updated recently"; + } + if (!$base) { + Log3 $name, 5, "$name: MaxAge: reading $reading doesn't come from a -Name attr -> skipping"; + next; + } + + $num = $hash->{defptr}{readingNum}{$key}; + if ($hash->{defptr}{readingSubNum}{$key}) { + $sub = $hash->{defptr}{readingSubNum}{$key}; + } else { + $sub = ""; + } + + Log3 $name, 5, "$name: MaxAge: reading definition comes from $base, $num" . ($sub ? ", $sub" : ""); + $max = HTTPMOD_GetFAttr($name, $base, $num . $sub, "MaxAge"); + if ($max) { + $rep = HTTPMOD_GetFAttr($name, $base, $num . $sub, "MaxAgeReplacement", ""); + $mode = HTTPMOD_GetFAttr($name, $base, $num . $sub, "MaxAgeReplacementMode", "text"); + $time = ReadingsTimestamp($name, $reading, 0); + Log3 $name, 5, "$name: MaxAge: max = $max, mode = $mode, rep = $rep"; + if ($now - time_str2num($time) > $max) { + if ($mode eq "expression") { + Log3 $name, 5, "$name: MaxAge: reading $reading too old - using Perl expression as MaxAge replacement: $rep"; + my $val = ReadingsVal($name, $reading, ""); + $rep = eval($rep); + if($@) { + Log3 $name, 3, "$name: MaxAge: error in replacement expression $1: $@"; + $rep = "error in replacement expression"; + } else { + Log3 $name, 5, "$name: MaxAge: result is $rep"; + } + readingsBulkUpdate($hash, $reading, $rep); + } elsif ($mode eq "text") { + Log3 $name, 5, "$name: MaxAge: reading $reading too old - using $rep instead"; + readingsBulkUpdate($hash, $reading, $rep); + } elsif ($mode eq "delete") { + Log3 $name, 5, "$name: MaxAge: reading $reading too old - delete it"; + delete($defs{$name}{READINGS}{$reading}); + delete $hash->{defptr}{readingOutdated}{$reading}; + } + $hash->{defptr}{readingOutdated}{$reading} = 1; + } + } else { + Log3 $name, 5, "$name: MaxAge: No MaxAge attr for $base, $num, $sub"; + } + } + readingsEndUpdate($hash, 1); +} + + +# +# extract cookies from HTTP Response Header +# called from _Read +########################################### +sub HTTPMOD_GetCookies($$) +{ + my ($hash, $header) = @_; + my $name = $hash->{NAME}; + Log3 $name, 5, "$name: looking for Cookies in $header"; + foreach my $cookie ($header =~ m/set-cookie: ?(.*)/gi) { + Log3 $name, 5, "$name: Set-Cookie: $cookie"; + $cookie =~ /([^,; ]+)=([^,; ]+)[;, ]*(.*)/; + Log3 $name, 5, "$name: Cookie: $1 Wert $2 Rest $3"; + $hash->{HTTPCookieHash}{$1}{Value} = $2; + $hash->{HTTPCookieHash}{$1}{Options} = ($3 ? $3 : ""); + } + $hash->{HTTPCookies} = join ("; ", map ($_ . "=".$hash->{HTTPCookieHash}{$_}{Value}, + sort keys %{$hash->{HTTPCookieHash}})); +} + + +# initialize Parsers +# called from _Read +################################### +sub HTTPMOD_InitParsers($$) +{ + my ($hash, $body) = @_; + my $name = $hash->{NAME}; + + # initialize parsers + if ($hash->{JSONEnabled}) { + HTTPMOD_FlattenJSON($hash, $body); + } + if ($hash->{XPathEnabled} && $body) { + $hash->{ParserData}{XPathTree} = HTML::TreeBuilder::XPath->new; + eval {$hash->{ParserData}{XPathTree}->parse($body)}; + Log3 $name, ($@ ? 3 : 5), "$name: InitParsers: XPath parsing " . ($@ ? "error: $@" : "done."); + } + if ($hash->{XPathStrictEnabled} && $body) { + eval {$hash->{ParserData}{XPathStrictNodeset} = XML::XPath->new(xml => $body)}; + Log3 $name, ($@ ? 3 : 5), "$name: InitParsers: XPath-Strict parsing " . ($@ ? "error: $@" : "done."); + } +} + + +# cleanup Parsers +# called from _Read +################################### +sub HTTPMOD_CleanupParsers($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + if ($hash->{XPathEnabled}) { + eval {$hash->{ParserData}{XPathTree}->delete()}; + Log3 $name, 3, "$name: error deleting XPathTree: $@" if ($@); + } + if ($hash->{XPathStrictEnabled}) { + eval {$hash->{ParserData}{XPathStrictNodeset}->cleanup()}; + Log3 $name, 3, "$name: error deleting XPathStrict nodeset: $@" if ($@); + } + delete $hash->{ParserData}; +} + + +# Extract SID +# called from _Read +################################### +sub HTTPMOD_ExtractSid($$$$) +{ + my ($hash, $buffer, $context, $num) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 5, "$name: ExtractSid called, context $context, num $num"; + my $regex = AttrVal($name, "idRegex", ""); + my $json = AttrVal($name, "idJSON", ""); + my $xpath = AttrVal($name, "idXPath", ""); + my $xpathst = AttrVal($name, "idXPath-Strict", ""); + + $regex = HTTPMOD_GetFAttr($name, $context, $num, "IDRegex", $regex); + $regex = HTTPMOD_GetFAttr($name, $context, $num, "IdRegex", $regex); + $json = HTTPMOD_GetFAttr($name, $context, $num, "IdJSON", $json); + $xpath = HTTPMOD_GetFAttr($name, $context, $num, "IdXPath", $xpath); + $xpathst = HTTPMOD_GetFAttr($name, $context, $num, "IdXPath-Strict", $xpathst); + + my @matchlist; + if ($json) { + Log3 $name, 5, "$name: Checking SID with JSON $json"; + if (defined($hash->{ParserData}{JSON}) && + defined($hash->{ParserData}{JSON}{$json})) { + @matchlist = ($hash->{ParserData}{JSON}{$json}); + } + } elsif ($xpath) { + Log3 $name, 5, "$name: Checking SID with XPath $xpath"; + eval {@matchlist = $hash->{ParserData}{XPathTree}->findnodes_as_strings($xpath)}; + Log3 $name, 3, "$name: error in findvalues for XPathTree: $@" if ($@); + } elsif ($xpathst) { + Log3 $name, 5, "$name: Checking SID with XPath-Strict $xpathst"; + my $nodeset; + eval {$nodeset = $hash->{ParserData}{XPathStrictNodeset}->find($xpathst)}; + if ($@) { + Log3 $name, 3, "$name: error in find for XPathStrictNodeset: $@"; + } else { + foreach my $node ($nodeset->get_nodelist) { + push @matchlist, XML::XPath::XMLParser::as_string($node); + } + } + } + + if (@matchlist) { + $buffer = join (' ', @matchlist); + if ($regex) { + Log3 $name, 5, "$name: ExtractSis is replacing buffer to check with match: $buffer"; + } else { + $hash->{sid} = $buffer; + Log3 $name, 5, "$name: ExtractSid set sid to $hash->{sid}"; + return 1; + } + } + + if ($regex) { + if ($buffer =~ $regex) { + $hash->{sid} = $1; + Log3 $name, 5, "$name: ExtractSid set sid to $hash->{sid}"; + return 1; + } else { + Log3 $name, 5, "$name: ExtractSid could not match buffer to IdRegex $regex"; + } + } + return 0; +} + + +# Check if Auth is necessary +# called from _Read +################################### +sub HTTPMOD_CheckAuth($$$$$) +{ + my ($hash, $buffer, $request, $context, $num) = @_; + my $name = $hash->{NAME}; + + my $regex = AttrVal($name, "reAuthRegex", ""); + my $json = AttrVal($name, "reAuthJSON", ""); + my $xpath = AttrVal($name, "reAuthXPath", ""); + my $xpathst = AttrVal($name, "reAuthXPath-Strict", ""); + + if ($context =~ /([gs])et/) { + $regex = HTTPMOD_GetFAttr($name, $context, $num, "ReAuthRegex", $regex); + $json = HTTPMOD_GetFAttr($name, $context, $num, "ReAuthJSON", $json); + $xpath = HTTPMOD_GetFAttr($name, $context, $num, "ReAuthXPath", $xpath); + $xpathst = HTTPMOD_GetFAttr($name, $context, $num, "ReAuthXPath-Strict", $xpathst); + } + + my @matchlist; + if ($json) { + Log3 $name, 5, "$name: Checking Auth with JSON $json"; + if (defined($hash->{ParserData}{JSON}) && + defined($hash->{ParserData}{JSON}{$json})) { + @matchlist = ($hash->{ParserData}{JSON}{$json}); + } + } elsif ($xpath) { + Log3 $name, 5, "$name: Checking Auth with XPath $xpath"; + eval {@matchlist = $hash->{ParserData}{XPathTree}->findnodes_as_strings($xpath)}; + Log3 $name, 3, "$name: error in findvalues for XPathTree: $@" if ($@); + } elsif ($xpathst) { + Log3 $name, 5, "$name: Checking Auth with XPath-Strict $xpathst"; + my $nodeset; + eval {$nodeset = $hash->{ParserData}{XPathStrictNodeset}->find($xpathst)}; + if ($@) { + Log3 $name, 3, "$name: error in find for XPathStrictNodeset: $@"; + } else { + foreach my $node ($nodeset->get_nodelist) { + push @matchlist, XML::XPath::XMLParser::as_string($node); + } + } + } + + if (@matchlist) { + if ($regex) { + $buffer = join (' ', @matchlist); + Log3 $name, 5, "$name: CheckAuth is replacing buffer to check with match: $buffer"; + } else { + Log3 $name, 5, "$name: CheckAuth matched: $buffer"; + return 1; + } + } + + if ($regex) { + Log3 $name, 5, "$name: CheckAuth is checking buffer with ReAuthRegex $regex"; + if ($buffer =~ $regex) { + Log3 $name, 4, "$name: CheckAuth decided new authentication required (ReAuthRegex matched: $regex)"; + if ($request->{retryCount} < AttrVal($name, "authRetries", 1)) { + HTTPMOD_Auth $hash; + #$request->{retryCount}++; # better add one in the call to AddToQueue + HTTPMOD_AddToQueue ($hash, $request->{url}, $request->{header}, + $request->{data}, $request->{type}, $request->{value}, $request->{retryCount}+1); + Log3 $name, 4, "$name: CheckAuth requeued request $request->{type} after auth, retryCount $request->{retryCount} ..."; + return 1; + } else { + Log3 $name, 4, "$name: CheckAuth has no more retries left - did authentication fail?"; + } + } + } + return 0; +} + + +# update List of Readings to parse +# during GetUpdate cycle +################################### +sub HTTPMOD_UpdateReadingList($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + my %khash; + foreach my $a (sort (grep (/readings?[0-9]*/, keys %{$attr{$name}}))) { + if (($a =~ /readingsName(.*)/) && defined ($attr{$name}{'readingsName' . $1})) { + $khash{$1} = 1; # old syntax + } elsif ($a =~ /reading([0-9]+).*/) { + $khash{$1} = 1; # new syntax + } + } + my @list = sort keys %khash; + $hash->{".readingParseList"} = \@list; + Log3 $name, 5, "$name: UpdateReadingList created list of reading.* nums to parse during getUpdate as @list"; + delete $hash->{".updateReadingList"}; +} + # # read / parse new data from device @@ -853,174 +1975,153 @@ sub HTTPMOD_GetFAttr($$$$) ################################### sub HTTPMOD_Read($$$) { - my ($hash, $err, $buffer) = @_; + my ($hash, $err, $body) = @_; my $name = $hash->{NAME}; my $request = $hash->{REQUEST}; + my $header = ($hash->{httpheader} ? $hash->{httpheader} : ""); my $type = $request->{type}; + my ($num, $context, $authQueued); + my @subrlist = (); + + + # set attribute prefix and num for parsing and formatting depending on request type + if ($type =~ /(set|get)(.*)/) { + $num = $2; + $context = $1; + } elsif ($type =~ /(auth)(.*)/) { + $num = $2; + $context = "sid"; + } else { + # request type was update for GetUpdate cycle + $num = ""; + $context = "reading"; # relevant attributes start with "reading..." + } $hash->{BUSY} = 0; - RemoveInternalTimer ($hash); # Remove remaining timeouts of HttpUtils (should be done in HttpUtils) + my $ll = ($err ? 3 : 5); # Log Level - 3 if error + Log3 $name, $ll, "$name: Read callback: request type was $type" . + " retry $request->{retryCount}" . + ($header ? ",\r\nHeader: $header" : ", no headers") . + ($body ? ",\r\nBody: $body" : ", body empty") . + ($err ? ", \r\nError: $err" : "no error"); - $hash->{HTTPHEADER} = "" if (!$hash->{HTTPHEADER}); - $hash->{httpheader} = "" if (!$hash->{httpheader}); - my $header = $hash->{HTTPHEADER} . $hash->{httpheader}; + my $buffer = ($header ? $header . "\r\n\r\n" . $body : $body); # so header can be used to match e.g. sid if ($err) { - Log3 $name, 3, "$name: Read callback: request type was $type" . - ($header ? ",\r\nheader: $header" : ", no headers") . - ($buffer ? ",\r\nbuffer: $buffer" : ", buffer empty") . - ($err ? ", \r\nError $err" : ""); - return; + $buffer = $buffer . "\r\n\r\n" . $err; # so err can be used in reAuthRegex matching + readingsSingleUpdate ($hash, "LAST_ERROR", $err, 1) + if (AttrVal($name, "showError", undef)) } - Log3 $name, 5, "$name: Read Callback: Request type was $type" . - ($header ? ",\r\nheader: $header" : ", no headers") . - ($buffer ? ",\r\nbuffer: $buffer" : ", buffer empty"); + HTTPMOD_UpdateReadingList($hash) if ($hash->{".updateReadingList"}); - - $buffer = $header . "\r\n\r\n" . $buffer if ($header); - - $type =~ "(Auth|Set|Get)(.*)"; - my $num = $2; - - if ($type =~ "Auth") { - # Doing Authentication step -> extract sid - my $idRegex = HTTPMOD_GetFAttr($name, "sid", $num, "IDRegex"); - if ($idRegex) { - if ($buffer =~ $idRegex) { - $hash->{sid} = $1; - Log3 $name, 5, "$name: Read set sid to $hash->{sid}"; - } else { - Log3 $name, 5, "$name: Read could not match buffer to IDRegex $idRegex"; - } - } - return undef; + HTTPMOD_GetCookies($hash, $header) if (AttrVal($name, "enableCookies", 0)); + + HTTPMOD_InitParsers($hash, $body); + + if ($context eq "sid") { + HTTPMOD_ExtractSid($hash, $buffer, $context, $num); } else { - # not in Auth, so check if Auth is necessary - my $ReAuthRegex; - if ($type =~ "Set") { - $ReAuthRegex = AttrVal($name, "set${num}ReAuthRegex", AttrVal($name, "setReAuthRegex", undef)); - } else { - $ReAuthRegex = AttrVal($name, "reAuthRegex", undef); - } - if ($ReAuthRegex) { - Log3 $name, 5, "$name: Read is checking response with ReAuthRegex $ReAuthRegex"; - if ($buffer =~ $ReAuthRegex) { - Log3 $name, 4, "$name: Read decided new authentication required"; - if ($request->{retryCount} < 1) { - HTTPMOD_Auth $hash; - $request->{retryCount}++; - Log3 $name, 4, "$name: Read is requeuing request $type after Auth, retryCount $request->{retryCount} ..."; - HTTPMOD_AddToQueue ($hash, $request->{url}, $request->{header}, - $request->{data}, $request->{type}, $request->{retryCount}); - return undef; - } else { - Log3 $name, 4, "$name: Read has no more retries left - did authentication fail?"; - } - } - } + $authQueued = HTTPMOD_CheckAuth($hash, $body, $request, $context, $num); } - - return undef if ($type =~ "Set"); - - my $checkAll = 0; - my $unmatched = ""; - my $matched = ""; - my ($reading, $regex, $expr, $map, $format, $encode, $decode, $pull); + + if ($err || $authQueued || + ($context =~ "set|sid" && !HTTPMOD_GetFAttr($name, $context, $num, "ParseResponse"))) { + # don't continue parsing response but still check maxAge for all readings + HTTPMOD_DoMaxAge($hash) if ($hash->{MaxAgeEnabled}); + #Log3 $name, 4, "$name: Read: no further parsing"; + HTTPMOD_CleanupParsers($hash); + return undef; + } + + my ($checkAll, $tried, $match, $reading); + my @unmatched = (); my @matched = (); readingsBeginUpdate($hash); - if ($type =~ "Get") { - $checkAll = AttrVal($name, "get" . $num . "CheckAllReadings", 0); - $reading = $attr{$name}{"get" . $num . "Name"}; - $regex = HTTPMOD_GetFAttr($name, "get", $num, "Regex"); - #Log3 $name, 5, "$name: Read is extracting Reading with $regex from HTTP Response to $type"; - if (!$regex) { - $checkAll = 1; + if ($context =~ "get|set") { + my $file = HTTPMOD_GetFAttr($name, $context, $num, "PullToFile"); + if ($file) { + ($tried, $match, $reading) = HTTPMOD_PullToFile($hash, $buffer, $num, $file); + @subrlist = ($reading); } else { - $expr = HTTPMOD_GetFAttr($name, "get", $num, "Expr"); - $map = HTTPMOD_GetFAttr($name, "get", $num, "Map"); - $format = HTTPMOD_GetFAttr($name, "get", $num, "Format"); - $decode = HTTPMOD_GetFAttr($name, "get", $num, "Decode"); - $encode = HTTPMOD_GetFAttr($name, "get", $num, "Encode"); - $pull = HTTPMOD_GetFAttr($name, "get", $num, "PullToFile"); - - if ($pull) { - Log3 $name, 5, "$name: Read is pulling to file, sequence is $hash->{GetSeq}"; - my $iterate = HTTPMOD_GetFAttr($name, "get", $num, "PullIterate"); - my $matches = 0; - while ($buffer =~ /$regex/g) { - my $recombine = HTTPMOD_GetFAttr($name, "get", $num, "RecombineExpr"); - no warnings qw(uninitialized); - $recombine = '$1' if not ($recombine); - my $val = eval($recombine); - Log3 $name, 3, "$name: Read pulled line $val"; - $matched = $reading; - $matches++; - } - Log3 $name, 3, "$name: Read pulled $matches lines"; - if ($matches) { - if ($iterate && $hash->{GetSeq} < $iterate) { - $hash->{GetSeq}++; - Log3 $name, 5, "$name: Read is iterating pull until $iterate, next is $hash->{GetSeq}"; - HTTPMOD_DoGet($hash, $num); - } else { - Log3 $name, 5, "$name: Read is done with pull after $hash->{GetSeq}."; - } - } else { - Log3 $name, 5, "$name: Read is done with pull, no more lines matched"; - } - } elsif (HTTPMOD_ExtractReading($hash, $buffer, $reading, $regex, $expr, $map, $format, $decode, $encode)) { - $matched = ($matched ? "$matched $reading" : "$reading"); + ($tried, $match, $reading, @subrlist) = HTTPMOD_ExtractReading($hash, $buffer, $context, $num); + } + if ($tried) { + if($match) { + push @matched, @subrlist; } else { - $unmatched = ($unmatched ? "$unmatched $reading" : "$reading"); + push @unmatched, $reading; } } + + $checkAll = HTTPMOD_GetFAttr($name, $context, $num, 'CheckAllReadings', !$tried); + # if ExtractReading2 could not find any parsing instruction (e.g. regex) then check all Readings + } else { + $checkAll = 1; } - if (($type eq "Update") || ($checkAll)) { - Log3 $name, 5, "$name: Read starts extracting all Readings from HTTP Response to $type"; - foreach my $a (sort (grep (/readings?[0-9]*Name/, keys %{$attr{$name}}))) { - if (($a =~ /readingsName(.*)/) && defined ($attr{$name}{'readingsName' . $1}) - && defined ($attr{$name}{'readingsRegex' . $1})) { - # old syntax - $reading = AttrVal($name, 'readingsName' . $1, ""); - $regex = AttrVal($name, 'readingsRegex' . $1, ""); - $expr = AttrVal($name, 'readingsExpr' . $1, ""); - } elsif(($a =~ /reading([0-9]+)Name/) && defined ($attr{$name}{"reading${1}Name"}) - && defined ($attr{$name}{"reading${1}Regex"})) { - # new syntax - $reading = AttrVal($name, "reading${1}Name", ""); - $regex = AttrVal($name, "reading${1}Regex", ""); - $expr = HTTPMOD_GetFAttr($name, "reading", $1, "Expr"); - $map = HTTPMOD_GetFAttr($name, "reading", $1, "Map"); - $format = HTTPMOD_GetFAttr($name, "reading", $1, "Format"); - $decode = HTTPMOD_GetFAttr($name, "reading", $1, "Decode"); - $encode = HTTPMOD_GetFAttr($name, "reading", $1, "Encode"); - } else { - Log3 $name, 3, "$name: Read found inconsistant attributes for $a"; - next; + if (AttrVal($name, "extractAllJSON", "") || HTTPMOD_GetFAttr($name, $context, $num, "ExtractAllJSON")) { + # create a reading for each JSON object and use formatting options if a correspondig reading name / formatting is defined + if (ref $hash->{ParserData}{JSON} eq "HASH") { + foreach my $object (keys %{$hash->{ParserData}{JSON}}) { + my $value = $hash->{ParserData}{JSON}{$object}; + my $rname = $object; + my $rnum = 0; + #Log3 $name, 5, "$name: looking at JSON object $object, value $value"; + # is there a defined reading with that JSON path? -> take name and formatting + foreach my $rx (sort grep (/^reading[0-9]+JSON$/, keys %{$attr{$name}})) { + if ($object eq AttrVal($name, $rx, "")) { + # Name und ggf. Formattierung angegeben, nutze sie. + $rx =~ /^reading([0-9]+)JSON$/; + $rnum = $1; + $rname = AttrVal($name, "reading${rnum}Name", ""); + $value = HTTPMOD_FormatReading($name, "reading", $rnum, $value); + } + } + Log3 $name, 5, "$name: Read set JSON $object as reading $rname to value " . $value; + readingsBulkUpdate($hash, $object, $value); + push @matched, $rname; + # unmatched is not filled for "ExtractAllJSON" + delete $hash->{defptr}{readingOutdated}{$object}; } - if (HTTPMOD_ExtractReading($hash, $buffer, $reading, $regex, $expr, $map, $format, $decode, $encode)) { - $matched = ($matched ne "" ? "$matched $reading" : "$reading"); - } else { - $unmatched = ($unmatched ne "" ? "$unmatched $reading" : "$reading"); - } - } - } - if ($type =~ "(Update|Get)") { - if (!$matched) { - readingsBulkUpdate( $hash, "MATCHED_READINGS", "") - if (AttrVal($name, "showMatched", undef)); - Log3 $name, 3, "$name: Read response to $type didn't match any Reading(s)"; } else { - readingsBulkUpdate( $hash, "MATCHED_READINGS", $matched) - if (AttrVal($name, "showMatched", undef)); - Log3 $name, 4, "$name: Read response to $type matched Reading(s) $matched"; - Log3 $name, 4, "$name: Read response to $type did not match $unmatched" if ($unmatched); + Log3 $name, 3, "$name: no parsed JSON structure available"; + } + } elsif ($checkAll && defined($hash->{".readingParseList"})) { + # check all defined readings and try to extract them + + Log3 $name, 5, "$name: Read starts parsing response to $type with defined readings: " . + join (",", @{$hash->{".readingParseList"}}); + foreach $num (@{$hash->{".readingParseList"}}) { + # try to parse readings defined in reading.* attributes + (undef, $match, $reading, @subrlist) = HTTPMOD_ExtractReading($hash, $buffer, 'reading', $num); + if($match) { + push @matched, @subrlist; + } else { + push @unmatched, $reading; + } } } - readingsEndUpdate( $hash, 1 ); + readingsBulkUpdate($hash, "MATCHED_READINGS", join ' ', @matched) + if (AttrVal($name, "showMatched", undef)); + + if (!@matched) { + Log3 $name, 3, "$name: Read response to $type didn't match any Reading"; + } else { + Log3 $name, 4, "$name: Read response to $type matched Reading(s) " . join ' ', @matched; + Log3 $name, 4, "$name: Read response to $type did not match " . join ' ', @unmatched if (@unmatched); + } + + HTTPMOD_TryCall($hash, $buffer, 'parseFunction1', $type); + readingsEndUpdate($hash, 1); + HTTPMOD_TryCall($hash, $buffer, 'parseFunction2', $type); HTTPMOD_HandleSendQueue("direct:".$name); + + HTTPMOD_CleanupParsers($hash); + + # check maxAge for all readings + HTTPMOD_DoMaxAge($hash) if ($hash->{MaxAgeEnabled}); + return undef; } @@ -1050,10 +2151,10 @@ HTTPMOD_HandleSendQueue($) Log3 $name, 3, "$name: HandleSendQueue - init not done, delay sending from queue"; return; } - if ($hash->{BUSY}) { # still waiting for reply to last request - InternalTimer($now+$queueDelay, "HTTPMOD_HandleSendQueue", "queue:$name", 0); - Log3 $name, 5, "$name: HandleSendQueue - still waiting for reply to last request, delay sending from queue"; - return; + if ($hash->{BUSY}) { # still waiting for reply to last request + InternalTimer($now+$queueDelay, "HTTPMOD_HandleSendQueue", "queue:$name", 0); + Log3 $name, 5, "$name: HandleSendQueue - still waiting for reply to last request, delay sending from queue"; + return; } $hash->{REQUEST} = $queue->[0]; @@ -1067,33 +2168,66 @@ HTTPMOD_HandleSendQueue($) return; } - $hash->{BUSY} = 1; # HTTPMOD queue is busy until response is received - $hash->{LASTSEND} = $now; # remember when last sent - $hash->{redirects} = 0; - $hash->{callback} = \&HTTPMOD_Read; - $hash->{url} = $hash->{REQUEST}{url}; - $hash->{header} = $hash->{REQUEST}{header}; - $hash->{data} = $hash->{REQUEST}{data}; - $hash->{timeout} = AttrVal($name, "timeout", 2); + # set parameters for HttpUtils from request into hash + $hash->{BUSY} = 1; # HTTPMOD queue is busy until response is received + $hash->{LASTSEND} = $now; # remember when last sent + $hash->{redirects} = 0; + $hash->{callback} = \&HTTPMOD_Read; + $hash->{url} = $hash->{REQUEST}{url}; + $hash->{header} = $hash->{REQUEST}{header}; + $hash->{data} = $hash->{REQUEST}{data}; + $hash->{value} = $hash->{REQUEST}{value}; + $hash->{timeout} = AttrVal($name, "timeout", 2); $hash->{ignoreredirects} = $hash->{REQUEST}{ignoreredirects}; + $hash->{httpversion} = AttrVal($name, "httpVersion", "1.0"); + + my $sslArgList = AttrVal($name, "sslArgs", undef); + if ($sslArgList) { + Log3 $name, 5, "$name: sslArgs is set to $sslArgList"; + my %sslArgs = split (',', $sslArgList); + Log3 $name, 5, "$name: sslArgs hash keys: " . join(",", keys %sslArgs); + Log3 $name, 5, "$name: sslArgs hash values: " . join(",", values %sslArgs); + $hash->{sslargs} = \%sslArgs; + } if (AttrVal($name, "noShutdown", undef)) { $hash->{noshutdown} = 1; } else { delete $hash->{noshutdown}; }; - + + # do user defined replacements first + if ($hash->{ReplacementEnabled}) { + $hash->{header} = HTTPMOD_Replace($hash, $hash->{REQUEST}{type}, $hash->{header}); + $hash->{data} = HTTPMOD_Replace($hash, $hash->{REQUEST}{type}, $hash->{data}); + $hash->{url} = HTTPMOD_Replace($hash, $hash->{REQUEST}{type}, $hash->{url}); + } + + # then replace $val in header, data and URL with value from request (setVal) if it is still there + $hash->{header} =~ s/\$val/$hash->{value}/g; + $hash->{data} =~ s/\$val/$hash->{value}/g; + $hash->{url} =~ s/\$val/$hash->{value}/g; + + # sid replacement is also done here - just before sending so changes in session while request was queued will be reflected if ($hash->{sid}) { $hash->{header} =~ s/\$sid/$hash->{sid}/g; $hash->{data} =~ s/\$sid/$hash->{sid}/g; $hash->{url} =~ s/\$sid/$hash->{sid}/g; } + Log3 $name, 4, "$name: HandleSendQueue sends request type $hash->{REQUEST}{type} to " . - "URL $hash->{url}, data $hash->{data}, header $hash->{header}, timeout $hash->{timeout}"; + "URL $hash->{url}, " . + ($hash->{data} ? "data $hash->{data}, " : "No Data, ") . + ($hash->{header} ? "header $hash->{header}, " : "No Header, ") . + "timeout $hash->{timeout}"; + + shift(@{$queue}); # remove first element from queue HttpUtils_NonblockingGet($hash); + } else { + shift(@{$queue}); # remove invalid first element from queue } - shift(@{$queue}); # remove first element from queue + if(@{$queue} > 0) { # more items in queue -> schedule next handle InternalTimer($now+$queueDelay, "HTTPMOD_HandleSendQueue", "queue:$name", 0); } @@ -1104,10 +2238,11 @@ HTTPMOD_HandleSendQueue($) ##################################### sub -HTTPMOD_AddToQueue($$$$$;$$$){ - my ($hash, $url, $header, $data, $type, $count, $ignoreredirects, $prio) = @_; +HTTPMOD_AddToQueue($$$$$;$$$$){ + my ($hash, $url, $header, $data, $type, $value, $count, $ignoreredirects, $prio) = @_; my $name = $hash->{NAME}; + $value = 0 if (!$value); $count = 0 if (!$count); $ignoreredirects = 0 if (!$ignoreredirects); @@ -1116,18 +2251,23 @@ HTTPMOD_AddToQueue($$$$$;$$$){ $request{header} = $header; $request{data} = $data; $request{type} = $type; + $request{value} = $value; $request{retryCount} = $count; $request{ignoreredirects} = $ignoreredirects; my $qlen = ($hash->{QUEUE} ? scalar(@{$hash->{QUEUE}}) : 0); - Log3 $name, 5, "$name: AddToQueue called, initial send queue length : $qlen"; - Log3 $name, 5, "$name: AddToQueue adds type $request{type} to " . - "URL $request{url}, data $request{data}, header $request{header}"; + Log3 $name, 5, "$name: AddToQueue called, initial send queue length : $qlen" . ($prio ? " prio" : ""); + Log3 $name, 5, "$name: AddToQueue " . ($prio ? "prepends " : "adds ") . + "type $request{type} to " . + "URL $request{url}, " . + ($request{data} ? "data $request{data}, " : "no data, ") . + ($request{header} ? "header $request{header}, " : "no headers, ") . + "retry $count"; if(!$qlen) { $hash->{QUEUE} = [ \%request ]; } else { if ($qlen > AttrVal($name, "queueMax", 20)) { - Log3 $name, 3, "$name: AddToQueue - send queue too long, dropping request"; + Log3 $name, 3, "$name: AddToQueue - send queue too long ($qlen), dropping request ($type), BUSY = $hash->{BUSY}"; } else { if ($prio) { unshift (@{$hash->{QUEUE}}, \%request); # an den Anfang @@ -1136,12 +2276,11 @@ HTTPMOD_AddToQueue($$$$$;$$$){ } } } - HTTPMOD_HandleSendQueue("direct:".$name); + HTTPMOD_HandleSendQueue("direct:".$name) if (!$prio); # if prio is set, wait until all steps are added to the front - Auth will call HandleSendQueue then. } 1; - =pod =begin html @@ -1149,10 +2288,10 @@ HTTPMOD_AddToQueue($$$$$;$$$){ <h3>HTTPMOD</h3> <ul> - This module provides a generic way to retrieve information from devices with an HTTP Interface and store them in Readings. - It queries a given URL with Headers and data defined by attributes. - From the HTTP Response it extracts Readings named in attributes using Regexes also defined by attributes. <br> - In an advanced configuration the module can also send information to devices. To do this a generic set option can be configured using attributes. + This module provides a generic way to retrieve information from devices with an HTTP Interface and store them in Readings or send information to such devices. + It queries a given URL with Headers and data defined by attributes. <br> + From the HTTP Response it extracts Readings named in attributes using Regexes, JSON or XPath also defined by attributes. <br> + To send information to a device, set commands can be configured using attributes. <br><br> <b>Prerequisites</b> <ul> @@ -1171,7 +2310,9 @@ HTTPMOD_AddToQueue($$$$$;$$$){ <br> <code>define <name> HTTPMOD <URL> <Interval></code> <br><br> - The module connects to the given URL every Interval seconds, sends optional headers and data and then parses the response<br> + The module connects to the given URL every Interval seconds, sends optional headers + and data and then parses the response.<br> + URL can be "none" and Interval can be 0 if you prefer to only query data manually with a get command and not automatically in a defined interval.<br> <br> Example:<br> <br> @@ -1180,13 +2321,18 @@ HTTPMOD_AddToQueue($$$$$;$$$){ <br> <a name="HTTPMODconfiguration"></a> - <b>Configuration of HTTP Devices</b><br><br> + <b>Simple configuration of HTTP Devices</b><br><br> <ul> - Specify optional headers as <code>attr requestHeader1</code> to <code>attr requestHeaderX</code>, <br> - optional POST data as <code>attr requestData</code> and then <br> - pairs of <code>attr readingXName</code> and <code>attr readingXRegex</code> to define which readings you want to extract from the HTTP - response and how to extract them. (The old syntax <code>attr readingsNameX</code> and <code>attr readingsRegexX</code> is still supported - but the new one with <code>attr readingXName</code> and <code>attr readingXRegex</code> should be preferred. + In a simple configuration you don't need to define get or set commands. One common HTTP request is automatically sent in the + interval specified in the define command and to the URL specified in the define command.<br> + Optional HTTP headers can be specified as <code>attr requestHeader1</code> to <code>attr requestHeaderX</code>, <br> + optional POST data as <code>attr requestData</code> and then + pairs of <code>attr readingXName</code> and <code>attr readingXRegex</code>, + <code>attr readingXXPath</code>, <code>attr readingXXPath-Strict</code> or <code>attr readingXJSON</code> + to define how values are parsed from the HTTP response and in which readings they are stored. <br> + (The old syntax <code>attr readingsNameX</code> and <code>attr readingsRegexX</code> is still supported + but it can go away in a future version of HTTPMOD so the new one with <code>attr readingXName</code> + and <code>attr readingXRegex</code> should be preferred. <br><br> Example for a PoolManager 5:<br><br> <ul><code> @@ -1203,23 +2349,107 @@ HTTPMOD_AddToQueue($$$$$;$$$){ attr PM requestData {"get" :["34.4001.value" ,"34.4008.value" ,"34.4033.value", "14.16601.value", "14.16602.value"]}<br> attr PM requestHeader1 Content-Type: application/json<br> attr PM requestHeader2 Accept: */*<br> + <br> attr PM stateFormat {sprintf("%.1f Grad, PH %.1f, %.1f mg/l Chlor", ReadingsVal($name,"TEMP",0), ReadingsVal($name,"PH",0), ReadingsVal($name,"CL",0))}<br> </code></ul> <br> - If you need to do some calculation on a raw value before it is used as a reading, you can define the attribute <code>readingXExpr</code> - which can use the raw value from the variable $val - <br><br> - Example:<br><br> + The regular expressions used will take the value that matches a capture group. This is the part of the regular expression inside (). + In the above example "([\d\.]+)" refers to numerical digits or points between double quotation marks. Only the string consiting of digits and points + will match inside (). This piece is assigned to the reading. + + You can also use regular expressions that have several capture groups which might be helpful when parsing tables. In this case an attribute like + <code><ul> + reading02Regex something[ \t]+([\d\.]+)[ \t]+([\d\.]+) + </code></ul> + could match two numbers. When you specify only one reading02Name like + <code><ul> + reading02Name Temp + </code></ul> + the name Temp will be used with the extension -1 and -2 thus giving a reading Temp-1 for the first number and Temp-2 for the second. + You can also specify individual names for several readings that get parsed from one regular expression with several capture groups by + defining attributes + <code><ul> + reading02-1Name<br> + reading02-2Name + ... + </code></ul> + The same notation can be used for formatting attributes like readingXOMap, readingXFormat and so on.<br> + <br> + The usual way to define readings is however to have an individual regular expression with just one capture group per reading as shown in the above example. + <br> + </ul> + <br> + + <a name="HTTPMODformat"></a> + <b>formating and manipulating values / readings</b><br><br> + <ul> + Values that are parsed from an HTTP response can be further treated or formatted with the following attributes:<br> <ul><code> - attr PM reading03Expr $val * 10<br> + (reading|get)[0-9]*(-[0-9]+)?OExpr<br> + (reading|get)[0-9]*(-[0-9]+)?OMap<br> + (reading|get)[0-9]*(-[0-9]+)?Format<br> + (reading|get)[0-9]*(-[0-9]+)?Decode<br> + (reading|get)[0-9]*(-[0-9]+)?Encode </code></ul> - - <br><br> - <b>Advanced configuration to define a <code>set</code> or <code>get</code> and send data to a device</b> - <br><br> + They can all be specified for an individual reading, for all readings in one match (e.g. if a regular expression has several capture groups) + or for all readings in a get command (defined by getXX) or for all readings in the main reading list (defined by readingXX): + <ul><code> + reading01Format %.1f + </code></ul> + will format the reading with the name specified by the attribute reading01Name to be numerical with one digit after the decimal point. <br> + If the attribute reading01Regex is used and contains several capture groups then the format will be applied to all readings thet are parsed + by this regex unless these readings have their own format specified by reading01-1Format, reading01-2Format and so on. + <br> + <ul><code> + reading01-2Format %.1f + </code></ul> + Can be used in cases where a regular expression specified as reading1regex contains several capture groups or an xpath specified + as reading01XPath creates several readings. + In this case reading01-2Format specifies the format to be applied to the second match. + <br> + <ul><code> + readingFormat %.1f + </code></ul> + applies to all readings defined by a reading-Attribute that have no more specific format. + <br> + <br> + If you need to do some calculation on a raw value before it is used as a reading, you can define the attribute <code>readingOExpr</code>.<br> + It defines a Perl expression that is used in an eval to compute the readings value. The raw value will be in the variable $val.<br> + Example:<br> + <ul><code> + attr PM reading03OExpr $val * 10<br> + </code></ul> + Just like in the above example of the readingFormat attributes, readingOExpr and the other following attributes + can be applied on several levels. + <br> + <br> + To map a raw numerical value to a name, you can use the readingOMap attribute. + It defines a mapping from raw values read from the device to visible values like "0:mittig, 1:oberhalb, 2:unterhalb". <br> + Example:<br> + <ul><code> + attr PM reading02-3OMap 0:kalt, 1:warm, 2:sehr warm + </code></ul> + <br> + If the value read from a http response is 1, the above map will transalte it to the string warm and the reading value will be set to warm.<br> - When a set option is defined by attributes, the module will use the value given to the set command and translate it into an HTTP-Request that sends the value to the device. <br><br> + To convert character sets, the module can first decode a string read from the device and then encode it again. For example: + <ul><code> + attr PM getDecode UTF-8 + </code></ul> + This applies to all readings defined for Get-Commands. + + </ul> + <br> + + <a name="HTTPMODsetconfiguration"></a> + <b>Configuration to define a <code>set</code> command and send data to a device</b><br><br> + <ul> + When a set option is defined by attributes, the module will use the value given to the set command + and translate it into an HTTP-Request that sends the value to the device. <br> + HTTPMOD has a built in replacement that replaces $val in URLs, headers or Post data with the value passed in the set command.<br> + This value is internally stored in the internal "value" ($hash->{value}) so it can also be used in a user defined replacement. + <br> Extension to the above example for a PoolManager 5:<br><br> <ul><code> attr PM set01Name HeizungSoll <br> @@ -1233,11 +2463,34 @@ HTTPMOD_AddToQueue($$$$$;$$$){ <br> This example defines a set option with the name HeizungSoll. <br> By issuing <code>set PM HeizungSoll 10</code> in FHEM, the value 10 will be sent in the defined HTTP - Post to URL <code>http://MyPoolManager/cgi-bin/webgui.fcgi</code> in the Post Data as <br> + Post to URL <code>http://MyPoolManager/cgi-bin/webgui.fcgi</code> and with Post Data as <br> <code>{"set" :{"34.3118.value" :"10" }}</code><br> The optional attributes set01Min and set01Max define input validations that will be checked in the set function.<br> - the optional attribute set01Hint will define a selection list for the Fhemweb GUI.<br><br> + the optional attribute set01Hint will define the way the Fhemweb GUI shows the input. This might be a slider or a selection list for example.<br> + <br> + The HTTP response to such a request will be ignored unless you specify the attribute <code>setParseResponse</code> + for all set commands or <code>set01ParseResponse</code> for the set command with number 01.<br> + If the HTTP response to a set command is parsed then this is done like the parsing of responses to get commands and you can use the attributes ending e.g. on + Format, Encode, Decode, OMap and OExpr to manipulate / format the values read. + <br> + If a parameter to a set command is not numeric but should be passed on to the device as text, then you can specify the attribute setTextArg. For example: + <ul><code> + attr PM set01TextArg + </code></ul> + If a set command should not require a parameter at all, then you can specify the attribute NoArg. For example: + <ul><code> + attr PM set03Name On + attr PM set03NoArg + </code></ul> + <br> + + </ul> + <br> + <a name="HTTPMODgetconfiguration"></a> + <b>Configuration to define a <code>get</code> command</b><br><br> + <ul> + When a get option is defined by attributes, the module allows querying additional values from the device that require individual HTTP-Requests or special parameters to be sent<br><br> Extension to the above example:<br><br> @@ -1261,63 +2514,327 @@ HTTPMOD_AddToQueue($$$$$;$$$){ <br> The first attribute includes this reading in the automatic update cycle and the second defines an - alternative lower update frequency. When the interval defined initially is over and the normal readings + alternative lower update frequency. When the interval defined initially in the define is over and the normal readings are read from the device, the update function will check for additional get parameters that should be included in the update cycle. If a PollDelay is specified for a get parameter, the update function also checks if the time passed since it has last read this value is more than the given PollDelay. If not, this reading is skipped and it will be rechecked in the next cycle when - interval is over again. So the effective PollDelay will always be a multiple of the interval specified in the initial define. - - <br><br> - <b>Advanced configuration to create a valid session id that might be necessary in set options</b> - <br><br> - when sending data to an HTTP-Device in a set, HTTPMOD will replace any <code>$sid</code> in the URL, Headers and Post data with the internal <code>$hash->{sid}</code>. To authenticate towards the device and give this internal a value, you can use an optional multi step login procedure defined by the following attributes: <br> - <ul> - <li>sid[0-9]*URL</li> - <li>sid[0-9]*IDRegex</li> - <li>sid[0-9]*Data.*</li> - <li>sid[0-9]*Header.*</li> - </ul><br> - Each step can have a URL, Headers, Post Data pieces and a Regex to extract a resulting Session ID into <code>$hash->{sid}</code>.<br> - HTTPMOD will create a sorted list of steps (the numbers between sid and URL / Data / Header) and the loop through these steps and send the corresponding requests to the device. For each step a $sid in a Header or Post Data will be replaced with the current content of <code>$hash->{sid}</code>. <br> - Using this feature, HTTPMOD can perform a forms based authentication and send user name, password or other necessary data to the device and save the session id for further requests. <br><br> - - To determine when this login procedure is necessary, HTTPMOD will first try to do a set without - doing the login procedure. If the Attribute reAuthRegex is defined, it will then compare the HTTP Response to the set request with the regular expression from reAuthRegex. If it matches, then a - login is performed. The reAuthRegex is meant to match the error page a device returns if authentication or reauthentication is required e.g. because a session timeout has expired. <br><br> - - If for one step not all of the URL, Data or Header Attributes are set, then HTTPMOD tries to use a - <code>sidURL</code>, <code>sidData.*</code> or <code>sidHeader.*</code> Attribue (without the step number after sid). This way parts that are the same for all steps don't need to be defined redundantly. <br><br> - - Example for a multi step login procedure: - <br><br> - - <ul><code> - attr PM sidURL http://192.168.70.90/cgi-bin/webgui.fcgi?sid=$sid<br> - attr PM sidHeader1 Content-Type: application/json<br> - attr PM sid1IDRegex wui.init\('([^']+)'<br> - attr PM sid2Data {"set" :{"9.17401.user" :"fhem" ,"9.17401.pass" :"password" }}<br> - attr PM sid3Data {"set" :{"35.5062.value" :"128" }}<br> - attr PM sid4Data {"set" :{"42.8026.code" :"pincode" }}<br> - </ul></code> + interval is over again. So the effective PollDelay will always be a multiple of the interval specified in the initial define.<br> + <br> + Please note that each defined get command that is included in the regular update cycle will create its own HTTP request. So if you want to extract several values from the same request, it is much more efficient to do this by defining readingXXName and readingXXRegex, XPath or JSON attributes and to specify an interval and a URL in the define of the HTTPMOD device. </ul> <br> + <a name="HTTPMODsessionconfiguration"></a> + <b>Handling sessions and logging in</b><br><br> + <ul> + In simple cases logging in works with basic authentication. In the case HTTPMOD accepts a username and password as part of the URL + in the form http://User:Password@192.168.1.18/something<br> + However basic auth is seldom used. If you need to fill in a username and password in a HTML form and the session is then managed by a session id, + here is how to configure this: + + when sending data to an HTTP-Device in a set, HTTPMOD will replace any <code>$sid</code> in the URL, Headers and Post data + with the internal <code>$hash->{sid}</code>. + To authenticate towards the device and give this internal a value, you can use an optional multi step login procedure + defined by the following attributes: <br> + <ul> + <li>sid[0-9]*URL</li> + <li>sid[0-9]*Data.*</li> + <li>sid[0-9]*Header.*</li> + <li>idRegex</li> + <li>idJSON</li> + <li>idXPath</li> + <li>idXPath-Strict</li> + <li>(get|set|sid)[0-9]*IdRegex</li> + <li>(get|set|sid)[0-9]*IdJSON</li> + <li>(get|set|sid)[0-9]*IdXPath</li> + <li>(get|set|sid)[0-9]*IdXPath-Strict</li> + </ul><br> + Each step can have a URL, Headers and Post Data. To extract the actual session Id, you can use regular expressions, JSON or XPath just like for the parsing of readings but with the attributes (get|set|sid)[0-9]*IdRegex, (get|set|sid)[0-9]*IdJSON, (get|set|sid)[0-9]*IdXPath or (get|set|sid)[0-9]*IdXPath-Strict.<br> + An extracted session Id will be stored in the internal <code>$hash->{sid}</code>.<br> + HTTPMOD will create a sorted list of steps (the numbers between sid and URL / Data / Header) + and the loop through these steps and send the corresponding requests to the device. + For each step a $sid in a Header or Post Data will be replaced with the current content of <code>$hash->{sid}</code>. <br> + Using this feature, HTTPMOD can perform a forms based authentication and send user name, password or other necessary data to the device and save the session id for further requests.<br> + If for one step not all of the URL, Data or Header Attributes are set, then HTTPMOD tries to use a + <code>sidURL</code>, <code>sidData.*</code> or <code>sidHeader.*</code> Attribue (without the step number after sid). + This way parts that are the same for all steps don't need to be defined redundantly. <br> + <br> + To determine when this login procedure is necessary, HTTPMOD will first try to send a request without + doing the login procedure. If the result contains an error that authentication is necessary, then a login is performed. + To detect such an error in the HTTP response, you can again use a regular expression, JSON or XPath, this time with the attributes + <ul> + <li>reAuthRegex</li> + <li>reAuthJSON</li> + <li>reAuthXPath</li> + <li>reAuthXPath-Strict</li> + <li>[gs]et[0-9]*ReAuthRegex</li> + <li>[gs]et[0-9]*ReAuthJSON</li> + <li>[gs]et[0-9]*ReAuthXPath</li> + <li>[gs]et[0-9]*ReAuthXPath-Strict</li> + </ul> + <br> + reAuthJSON or reAuthXPath typically only extract one piece of data from a response. + If the existance of the specified piece of data is sufficent to start a login procedure, then nothing more needs to be defined to detect this situation. + If however the indicator is a status code that contains different values depending on a successful request and a failed request if a new authentication is needed, + then you can combine things like reAuthJSON with reAuthRegex. In this case the regex is only matched to the data extracted by JSON (or XPath). + This way you can easily extract the status code using JSON parsing and then specify the code that means "authentication needed" as a regular expression. <br> + <br> + Example for a multi step login procedure: + <br><br> + <ul><code> + attr PM reAuthRegex /html/dummy_login.htm + attr PM sidURL http://192.168.70.90/cgi-bin/webgui.fcgi?sid=$sid<br> + attr PM sidHeader1 Content-Type: application/json<br> + attr PM sid1IdRegex wui.init\('([^']+)'<br> + attr PM sid2Data {"set" :{"9.17401.user" :"fhem" ,"9.17401.pass" :"password" }}<br> + attr PM sid3Data {"set" :{"35.5062.value" :"128" }}<br> + attr PM sid4Data {"set" :{"42.8026.code" :"pincode" }}<br> + </ul></code> + + In this case HTTPMOD detects that a login is necessary by looking for the pattern /html/dummy_login.htm in the HTTP response. + If it matches, it starts a login sequence. In the above example all steps request the same URL. In step 1 only the defined header + is sent in an HTTP get request. The response will contain a session id that is extraced with the regex wui.init\('([^']+)'. + In the next step this session id is sent in a post request to the same URL where tha post data contains a username and password. + The a third and a fourth request follow that set a value and a code. The result will be a valid and authorized session id that can be used in other requests where $sid is part of a URL, header or post data and will be replaced with the session id extracted above.<br> + <br> + In the special case where a session id is set as a HTTP-Cookie (with the header Set-cookie: in the HTTP response) HTTPMOD offers an even simpler way. With the attribute enableCookies a very basic cookie handling mechanism is activated that stores all cookies that the server sends to the HTTPMOD device and puts them back as cookie headers in the following requests. <br> + For such cases no sidIdRegex and no $sid in a user defined header is necessary. + + </ul> + <br> + + <a name="HTTPMODjsonconfiguration"></a> + <b>Parsing JSON</b><br><br> + <ul> + If a webservice delivers data in JSON format, HTTPMOD can directly parse JSON which might be easier in this case than definig regular expressions.<br> + The next example shows the data that can be requested from a Poolmanager with the following partial configuration: + + <ul><code> + define test2 HTTPMOD none 0<br> + attr test2 get01Name Chlor<br> + attr test2 getURL http://192.168.70.90/cgi-bin/webgui.fcgi<br> + attr test2 getHeader1 Content-Type: application/json<br> + attr test2 getHeader2 Accept: */*<br> + attr test2 getData {"get" :["34.4008.value"]}<br> + </ul></code> + + The data in the HTTP response looks like this: + + <ul><pre> + { + "data": { + "34.4008.value": "0.25" + }, + "status": { + "code": 0 + }, + "event": { + "type": 1, + "data": "48.30000.0" + } + } + </ul></pre> + + the classic way to extract the value 0.25 into a reading with the name Chlor with a regex would have been + <ul><code> + attr test2 get01Regex 34.4008.value":[ \t]+"([\d\.]+)" + </ul></code> + + with JSON you can write + <ul><code> + attr test2 get01JSON data_34.4008.value + </ul></code> + + or if you don't care about the naming of your readings, you can simply extract all JSON data with + <ul><code> + attr test2 extractAllJSON + </ul></code> + which would apply to all data read from this device and create the following readings out of the HTTP response shown above:<br> + + <ul><code> + data_34.4008.value 0.25 <br> + event_data 48.30000.0 <br> + event_type 1 <br> + status_code 0 <br> + </ul></code> + + or you can specify + <ul><code> + attr test2 get01ExtractAllJSON + </ul></code> + which would only apply to all data read as response to the get command defined as get01. + + </ul> + <br> + + <a name="HTTPMODxpathconfiguration"></a> + <b>Parsing http / XML using xpath</b><br><br> + <ul> + + Another alternative to regex parsing is the use of XPath to extract values from HTTP responses.<br> + The following example shows how XML data can be parsed with XPath-Strict or HTML Data can be parsed with XPath. <br> + Both work similar and the example uses XML Data parsed with the XPath-Strict option: + + If The XML data in the HTTP response looks like this: + + <ul><code> + <root xmlns:foo="http://www.foo.org/" xmlns:bar="http://www.bar.org"><br> + <actors><br> + <actor id="1">Peter X</actor><br> + <actor id="2">Charles Y</actor><br> + <actor id="3">John Doe</actor><br> + </actors><br> + </root> + </ul></code> + + with XPath you can write + <ul><code> + attr htest reading01Name Actor<br> + attr htest reading01XPath-Strict //actor[2]/text() + </ul></code> + This will create a reading with the Name "Actor" and the value "Charles Y".<br> + <br> + Since XPath specifications can define several values / matches, HTTPMOD can also interpret these and store them in + multiple readings: + <ul><code> + attr htest reading01Name Actor<br> + attr htest reading01XPath-Strict //actor/text() + </ul></code> + will create the readings + <ul><code> + Actor-1 Peter X<br> + Actor-2 Charles Y<br> + Actor-3 John Doe + </ul></code> + + </ul> + <br> + + <a name="HTTPMODreplacements"></a> + <b>Further replacements of URL, header or post data</b><br><br> + <ul> + sometimes it is helpful to dynamically change parts of a URL, HTTP header or post data depending on existing readings, internals or perl expressions at runtime. <br> + HTTPMOD has two built in replacements: one for values passed to a set or get command and the other one for the session id.<br> + Before a request is sent, the placeholder $val is replaced with the value that is passed in a set command or an optional value that can be passed in a get command (see getXTextArg). This value is internally stored in the internal "value" so it can also be used in a user defined replacement as explaind in this section.<br> + The other built in replacement is for the session id. If a session id is extracted via a regex, JSON or XPath the it is stored in the internal "sid" and the placeholder $sid in a URL, header or post data is replaced by the content of thus internal. + + User defined replacement can exted this functionality and this might be needed to pass further variables to a server, a current date or other things. <br> + To support this HTTPMOD offers user defined replacements that are as well applied to a request before it is sent to the server. + A replacement can be defined with the attributes + <ul><code> + "replacement[0-9]*Regex "<br> + "replacement[0-9]*Mode "<br> + "replacement[0-9]*Value "<br> + "[gs]et[0-9]*Replacement[0-9]*Value " + </ul></code> + <br> + A replacement always replaces a match of a regular expression. + The way the replacement value is defined can be specified with the replacement mode. If the mode is <code>reading</code>, + then the value is interpreted as the name of a reading of the same device or as device:reading to refer to another device. + If the mode is <code>internal</code>, then the value is interpreted as the name of an internal of the same device or as device:internal to refer to another device.<br> + The mode <code>text</code> will use the value as a static text and the mode <code>expression</code> will evaluate the value as a perl expression to compute the replacement. Inside such a replacement expression it is possible to refer to capture groups of the replacement regex.<br> + The mode <code>key</code> will use a value from a key / value pair that is stored in an obfuscated form in the file system with the set storeKeyValue command. This might be useful for storing passwords.<br> + <br> + Example: + <ul><code> + attr mydevice getData {"get" :["%%value%%.value"]} <br> + attr mydevice replacement01Mode text <br> + attr mydevice replacement01Regex %%value%% <br> + <br> + attr mydevice get01Name Chlor <br> + attr mydevice get01Replacement01Value 34.4008 <br> + <br> + attr mydevice get02Name Something<br> + attr mydevice get02Replacement01Value 31.4024 <br> + <br> + attr mydevice get05Name profile <br> + attr mydevice get05URL http://www.mydevice.local/getprofile?password=%%password%% <br> + attr mydevice replacement02Mode key <br> + attr mydevice replacement02Regex %%password%% <br> + attr mydevice get05Replacement02Value password <br> + </ul></code> + defines that %%value%% will be replaced by a static text.<br> + All Get commands will be HTTP post requests of a similar form. Only the %%value%% will be different from get to get.<br> + The first get will set the reading named Chlor and for the request it will take the generic getData and replace %%value%% with 34.4008.<br> + A second get will look the same except a different name and replacement value. <br> + With the command <code>set storeKeyValue password geheim</code> you can store the password geheim in an obfuscated form in the file system. To use this password and send it in a request you can use the above replacement with mode key. The value password will then refer to the ofuscated string stored with the key password.<br> + <br> + HTTPMOD has two built in replacements: One for session Ids and another one for the input value in a set command. + The placeholder $sid is always replaced with the internal <code>$hash->{sid}</code> which contains the session id after it is extracted from a previous HTTP response. If you don't like to use the placeholder $sid the you can define your own replacement for example like: + <ul><code> + attr mydevice replacement01Mode internal<br> + attr mydevice replacement01Regex %session% <br> + attr mydevice replacement01Value sid<br> + </ul></code> + Now the internal <code>$hash->{sid}</code> will be used as a replacement for the placeholder %session%.<br> + <br> + In the same way a value that is passed to a set-command can be put into a request with a user defined replacement. In this case the internal <code>$hash->{value}</code> will contain the value passed to the set command. It might even be a string containing several values that could be put into several different positions in a request by using user defined replacements. + <br> + The mode expression allows you to define your own replacement syntax: + <ul><code> + attr mydevice replacement01Mode expression <br> + attr mydevice replacement01Regex {{([^}]+)}}<br> + attr mydevice replacement01Value ReadingsVal("mydevice", $1, "")<br> + attr mydevice getData {"get" :["{{temp}}.value"]} + </ul></code> + In this example any {{name}} in a URL, header or post data will be passed on to the perl function ReadingsVal + which uses the string between {{}} as second parameter. This way one defined replacement can be used for many different + readings. + + </ul> + <br> + + <a name="HTTPMODaging"></a> + <b>replacing reading values when they have not been updated / the device did not respond</b><br><br> + <ul> + If a device does not respond then the values stored in readings will keep the same and only their timestamp shows that they are outdated. + If you want to modify reading values that have not been updated for a number of seconds, you can use the attributes + <ul><code> + (reading|get)[0-9]*(-[0-9]+)?MaxAge<br> + (reading|get)[0-9]*(-[0-9]+)?MaxAgeReplacementMode<br> + (reading|get)[0-9]*(-[0-9]+)?MaxAgeReplacement<br> + </ul></code> + Every time the module tries to read from a device, it will also check if readings have not been updated + for longer than the MaxAge attributes allow. If readings are outdated, the MaxAgeReplacementMode defines how the affected + reading values should be replaced. MaxAgeReplacementMode can be <code>text</code>, <code>expression</code> or <code>delete</code>. <br> + MaxAge specifies the number of seconds that a reading should remain untouched before it is replaced. <br> + MaxAgeReplacement contains either a static text that is used as replacement value or a Perl expression that is evaluated to + give the replacement value. This can be used for example to replace a temperature that has not bee updated for more than 5 minutes + with the string "outdated - was 12": + <ul><code> + attr PM readingMaxAge 300<br> + attr PM readingMaxAgeReplacement "outdated - was " . $val <br> + attr PM readingMaxAgeReplacementMode expression + </ul></code> + The variable $val contains the value of the reading before it became outdated.<br> + If the mode is delete then the reading will be deleted if it has not been updated for the defined time.<br> + If you want to replace or delete a reading immediatley if a device doid not respond, simply set the maximum time to a number smaller than the update interval. Since the max age is checked after a HTTP request was either successful or it failed, the reading will always contain the read value or the replacement after a failed update. + </ul> + <br> + <a name="HTTPMODset"></a> <b>Set-Commands</b><br> <ul> - as defined by the attributes set.*Name + As defined by the attributes set.*Name<br> If you set the attribute enableControlSet to 1, the following additional built in set commands are available:<br> <ul> <li><b>interval</b></li> set new interval time in seconds and restart the timer<br> <li><b>reread</b></li> - request the defined URL and try to parse it just like the automatic update would do it every Interval seconds without modifying the running timer. <br> + request the defined URL and try to parse it just like the automatic update would do it every Interval seconds + without modifying the running timer. <br> <li><b>stop</b></li> stop interval timer.<br> <li><b>start</b></li> restart interval timer to call GetUpdate after interval seconds<br> + <li><b>upgradeAttributes</b></li> + convert the attributes for this device from the old syntax to the new one.<br> + atributes with the description "this attribute should not be used anymore" or similar will be translated to the new syntax, e.g. readingsName1 to reading01Name. + <li><b>storeKeyValue</b></li> + stores a key value pair in an obfuscated form in the file system. Such values can then be used in replacements where + the mode is "key" e.g. to avoid storing passwords in the configuration in clear text<br> </ul> <br> </ul> @@ -1327,6 +2844,9 @@ HTTPMOD_AddToQueue($$$$$;$$$){ <ul> as defined by the attributes get.*Name </ul> + + + <br> <a name="HTTPMODattr"></a> <b>Attributes</b><br><br> @@ -1334,110 +2854,99 @@ HTTPMOD_AddToQueue($$$$$;$$$){ <li><a href="#do_not_notify">do_not_notify</a></li> <li><a href="#readingFnAttributes">readingFnAttributes</a></li> <br> + + <li><b>reading[0-9]+Name</b></li> + the name of a reading to extract with the corresponding readingRegex, readingJSON, readingXPath or readingXPath-Strict<br> + Please note that the old syntax <b>readingsName.*</b> does not work with all features of HTTPMOD and should be avoided. It might go away in a future version of HTTPMOD. + <li><b>(get|set)[0-9]+Name</b></li> + Name of a get or set command to be defined. If the HTTP response that is received after the command is parsed with an individual parse option then this name is also used as a reading name. Please note that no individual parsing needs to be defined for a get or set. If no regex, XPath or JSON is specified for the command, then HTTPMOD will try to parse the response using all the defined readingRegex, reading XPath or readingJSON attributes. + + <li><b>(get|set|reading)[0-9]+Regex</b></li> + If this attribute is specified, the Regex defined here is used to extract the value from the HTTP Response + and assign it to a Reading with the name defined in the (get|set|reading)[0-9]+Name attribute.<br> + If this attribute is not specified for an individual Reading or get or set but without the numbers in the middle, e.g. as getRegex or readingRegex, then it applies to all the other readings / get / set commands where no specific Regex is defined.<br> + The value to extract should be in a capture group / sub expression e.g. ([\d\.]+) in the above example. + Multiple capture groups will create multiple readings (see explanation above)<br> + Using this attribute for a set command (setXXRegex) only makes sense if you want to parse the HTTP response to the HTTP request that the set command sent by defining the attribute setXXParseResponse.<br> + Please note that the old syntax <b>readingsRegex.*</b> does not work with all features of HTTPMOD and should be avoided. It might go away in a future version of HTTPMOD. + If for get or set commands neither a generic Regex attribute without numbers nor a specific (get|set)[0-9]+Regex attribute is specified and also no XPath or JSON parsing specification is given for the get or set command, then HTTPMOD tries to use the parsing definitions for general readings defined in reading[0-9]+Name, reading[0-9]+Regex or XPath or JSON attributes and assigns the Readings that match here. + <li><b>(get|set|reading)[0-9]+RegOpt</b></li> + Lets the user specify regular expression modifiers. For example if the same regular expression should be matched as often as possible in the HTTP response, + then you can specify RegOpt g which will case the matching to be done as /regex/g<br> + The results will be trated the same way as multiple capture groups so the reading name will be extended with -number. + For other possible regular expression modifiers see http://perldoc.perl.org/perlre.html#Modifiers + <li><b>(get|set|reading)[0-9]+XPath</b></li> + defines an xpath to one or more values when parsing HTML data (see examples above)<br> + Using this attribute for a set command only makes sense if you want to parse the HTTP response to the HTTP request that the set command sent by defining the attribute setXXParseResponse.<br> + <li><b>get|set|reading[0-9]+XPath-Strict</b></li> + defines an xpath to one or more values when parsing XML data (see examples above)<br> + Using this attribute for a set command only makes sense if you want to parse the HTTP response to the HTTP request that the set command sent by defining the attribute setXXParseResponse.<br> + <li><b>(get|set|reading)[0-9]+AutoNumLen</b></li> + In cases where a regular expression or an XPath results in multiple results and these results are stored in a common reading name with extension -number, then + you can modify the format of this number to have a fixed length with leading zeros. AutoNumLen 3 for example will lead to reading names ending with -001 -002 and so on. + <li><b>get|set|reading[0-9]+JSON</b></li> + defines a path to the JSON object wanted by concatenating the object names. See the above example.<br> + If you don't know the paths, then start by using extractAllJSON and the use the names of the readings as values for the JSON attribute.<br> + Please don't forget to also specify a name for a reading, get or set. + Using this attribute for a set command only makes sense if you want to parse the HTTP response to the HTTP request that the set command sent by defining the attribute setXXParseResponse.<br> + <li><b>(get|set|reading)[0-9]*RecombineExpr</b></li> + defines an expression that is used in an eval to compute one reading value out of the list of matches. <br> + This is supposed to be used for regexes or xpath specifications that produce multiple results if only one result that combines them is wanted. The list of matches will be in the variable @matchlist.<br> + Using this attribute for a set command only makes sense if you want to parse the HTTP response to the HTTP request that the set command sent by defining the attribute setXXParseResponse.<br> + <li><b>get[0-9]*CheckAllReadings</b></li> + this attribute modifies the behavior of HTTPMOD when the HTTP Response of a get command is parsed. <br> + If this attribute is set to 1, then additionally to the matching of get specific regexe (get[0-9]*Regex), XPath or JSON + also all the reading names and parse definitions defined in Reading[0-9]+Name and Reading[0-9]+Regex, XPath or JSON attributes are checked and if they match, the coresponding Readings are assigned as well.<br> + This is automatically done if a get or set command is defined without its own parse attributes. + <br> + + <li><b>(get|reading)[0-9]*OExpr</b></li> + defines an optional expression that is used in an eval to compute / format a readings value after parsing an HTTP response<br> + The raw value from the parsing will be in the variable $val.<br> + If specified as readingOExpr then the attribute value is a default for all other readings that don't specify an explicit reading[0-9]*Expr.<br> + Please note that the old syntax <b>readingsExpr.*</b> does not work with all features of HTTPMOD and should be avoided. It might go away in a future version of HTTPMOD. + <li><b>(get|reading)[0-9]*Expr</b></li> + This is the old syntax for (get|reading)[0-9]*OExpr. It should be replaced by (get|reading)[0-9]*OExpr. The set command upgradeAttributes which becomes visible when the attribute enableControlSet is set to 1, can do this renaming automatically. + <li><b>(get|reading)[0-9]*OMap</b></li> + Map that defines a mapping from raw value parsed to visible values like "0:mittig, 1:oberhalb, 2:unterhalb". <br> + If specified as readingOMap then the attribute value is a default for all other readings that don't specify + an explicit reading[0-9]*Map.<br> + The individual options in a map are separated by a komma and an optional space. Spaces are allowed to appear in a visible value however kommas are not possible. + <li><b>(get|reading)[0-9]*Map</b></li> + This is the old syntax for (get|reading)[0-9]*OMap. It should be replaced by (get|reading)[0-9]*OMap. The set command upgradeAttributes which becomes visible when the attribute enableControlSet is set to 1, can do this renaming automatically. + <li><b>(get|set|reading)[0-9]*Format</b></li> + Defines a format string that will be used in sprintf to format a reading value.<br> + If specified without the numbers in the middle e.g. as readingFormat then the attribute value is a default for all other readings that don't specify an explicit reading[0-9]*Format. + Using this attribute for a set command only makes sense if you want to parse the HTTP response to the HTTP request that the set command sent by defining the attribute setXXParseResponse.<br> + <li><b>(get|set|reading)[0-9]*Decode</b></li> + defines an encoding to be used in a call to the perl function decode to convert the raw data string read from the device to a reading. + This can be used if the device delivers strings in an encoding like cp850 instead of utf8.<br> + If your reading values contain Umlauts and they are shown as strange looking icons then you probably need to use this feature. + Using this attribute for a set command only makes sense if you want to parse the HTTP response to the HTTP request that the set command sent by defining the attribute setXXParseResponse.<br> + <li><b>(get|set|reading)[0-9]*Encode</b></li> + defines an encoding to be used in a call to the perl function encode to convert the raw data string read from the device to a reading. + This can be used if the device delivers strings in an encoding like cp850 and after decoding it you want to reencode it to e.g. utf8. + If your reading values contain Umlauts and they are shown as strange looking icons then you probably need to use this feature. + Using this attribute for a set command only makes sense if you want to parse the HTTP response to the HTTP request that the set command sent by defining the attribute setXXParseResponse.<br> + <br> + + <li><b>(get|set)[0-9]*URL</b></li> + URL to be requested for the get or set command. + If this option is missing, the URL specified during define will be used. + <li><b>(get|set)[0-9]*Data</b></li> + optional data to be sent to the device as POST data when the get oer set command is executed. + if this attribute is specified, an HTTP POST method will be sent instead of an HTTP GET + <li><b>(get|set)[0-9]*NoData</b></li> + can be used to override a more generic attribute that specifies POST data for all get commands. + With NoData no data is sent and therefor the request will be an HTTP GET. + <li><b>(get|set)[0-9]*Header.*</b></li> + optional HTTP Headers to be sent to the device when the get or set command is executed <li><b>requestHeader.*</b></li> Define an optional additional HTTP Header to set in the HTTP request <br> <li><b>requestData</b></li> optional POST Data to be sent in the request. If not defined, it will be a GET request as defined in HttpUtils used by this module<br> - <li><b>reading[0-9]+Name</b> or <b>readingsName.*</b></li> - the name of a reading to extract with the corresponding readingRegex<br> - <li><b>reading[0-9]+Regex</b> ro <b>readingsRegex.*</b></li> - defines the regex to be used for extracting the reading. The value to extract should be in a sub expression e.g. ([\d\.]+) in the above example <br> - <li><b>reading[0-9]*Expr</b> or <b>readingsExpr.*</b></li> - defines an expression that is used in an eval to compute the readings value. <br> - The raw value will be in the variable $val.<br> - If specified as readingExpr then the attribute value is a default for all other readings that don't specify - an explicit reading[0-9]*Expr. - <li><b>reading[0-9]*Map</b></li> - Map that defines a mapping from raw to visible values like "0:mittig, 1:oberhalb, 2:unterhalb". <br> - If specified as readingMap then the attribute value is a default for all other readings that don't specify - an explicit reading[0-9]*Map. - <li><b>reading[0-9]*Format</b></li> - Defines a format string that will be used in sprintf to format a reading value.<br> - If specified as readingFormat then the attribute value is a default for all other readings that don't specify - an explicit reading[0-9]*Format. + <br> - <li><b>reading[0-9]*Decode</b></li> - defines an encoding to be used in a call to the perl function decode to convert the raw data string read from the device to a reading. This can be used if the device delivers strings in an encoding like cp850 instead of utf8. - <li><b>reading[0-9]*Encode</b></li> - defines an encoding to be used in a call to the perl function encode to convert the raw data string read from the device to a reading. This can be used if the device delivers strings in an encoding like cp850 and after decoding it you want to reencode it to e.g. utf8. - - <li><b>noShutdown</b></li> - pass the noshutdown flag to HTTPUtils for webservers that need it (some embedded webservers only deliver empty pages otherwise) - <li><b>disable</b></li> - stop doing automatic HTTP requests while this attribute is set to 1 - <li><b>timeout</b></li> - time in seconds to wait for an answer. Default value is 2 - <li><b>enableControlSet</b></li> - enables the built in set commands interval, stop, start, reread. - <li><b>enableXPath</b></li> - enables the use of "xpath:" instead of a regular expression to parse the HTTP response - <li><b>enableXPath-Strict</b></li> - enables the use of "xpath-strict:" instead of a regular expression to parse the HTTP response - </ul> - <br> - <b> advanced attributes </b> - <br> - <ul> - <li><b>reAuthRegex</b></li> - regular Expression to match an error page indicating that a session has expired and a new authentication for read access needs to be done. This attribute only makes sense if you need a forms based authentication for reading data and if you specify a multi step login procedure based on the sid.. attributes. - <br><br> - <li><b>sid[0-9]*URL</b></li> - different URLs or one common URL to be used for each step of an optional login procedure. - <li><b>sid[0-9]*IDRegex</b></li> - different Regexes per login procedure step or one common Regex for all steps to extract the session ID from the HTTP response - <li><b>sid[0-9]*Data.*</b></li> - data part for each step to be sent as POST data to the corresponding URL - <li><b>sid[0-9]*Header.*</b></li> - HTTP Headers to be sent to the URL for the corresponding step - <li><b>sid[0-9]*IgnoreRedirects</b></li> - tell HttpUtils to not follow redirects for this authentication request - <br> - <br> - <li><b>set[0-9]+Name</b></li> - Name of a set option - <li><b>set[0-9]*URL</b></li> - URL to be requested for the set option - <li><b>set[0-9]*Data</b></li> - optional Data to be sent to the device as POST data when the set is executed. if this atribute is not specified, an HTTP GET method - will be used instead of an HTTP POST - <li><b>set[0-9]*Header</b></li> - optional HTTP Headers to be sent to the device when the set is executed - <li><b>set[0-9]+Min</b></li> - Minimum value for input validation. - <li><b>set[0-9]+Max</b></li> - Maximum value for input validation. - <li><b>set[0-9]+Expr</b></li> - Perl Expression to compute the raw value to be sent to the device from the input value passed to the set. - <li><b>set[0-9]+Map</b></li> - Map that defines a mapping from raw to visible values like "0:mittig, 1:oberhalb, 2:unterhalb". This attribute atomatically creates a hint for FhemWEB so the user can choose one of the visible values. - <li><b>set[0-9]+Hint</b></li> - Explicit hint for fhemWEB that will be returned when set ? is seen. - <li><b>set[0-9]*ReAuthRegex</b></li> - Regex that will detect when a session has expired an a new login needs to be performed. - <li><b>set[0-9]*NoArg</b></li> - Defines that this set option doesn't require arguments. It allows sets like "on" or "off" without further values. - <li><b>set[0-9]*TextArg</b></li> - Defines that this set option doesn't require any validation / conversion. - The raw value is passed on as text to the device. - <br> - <br> - <li><b>get[0-9]+Name</b></li> - Name of a get option and Reading to be retrieved / extracted - <li><b>get[0-9]*URL</b></li> - URL to be requested for the get option. If this option is missing, the URL specified during define will be used. - <li><b>get[0-9]*Data</b></li> - optional data to be sent to the device as POST data when the get is executed. if this attribute is not specified, an HTTP GET method - will be used instead of an HTTP POST - <li><b>get[0-9]*Header</b></li> - optional HTTP Headers to be sent to the device when the get is executed - - <li><b>get[0-9]*URLExpr</b></li> - optional Perl expression that allows modification of the URL at runtime. The origial value is available as $old. - <li><b>get[0-9]*DatExpr</b></li> - optional Perl expression that allows modification of the Post data at runtime. The origial value is available as $old. - <li><b>get[0-9]*HdrExpr</b></li> - optional Perl expression that allows modification of the Headers at runtime. The origial value is available as $old. - <li><b>get[0-9]+Poll</b></li> if set to 1 the get is executed automatically during the normal update cycle (after the interval provided in the define command has elapsed) <li><b>get[0-9]+PollDelay</b></li> @@ -1445,49 +2954,140 @@ HTTPMOD_AddToQueue($$$$$;$$$){ minimum delay can be specified with this attribute. This has only an effect if the above Poll attribute has also been set. Every time the update function is called, it checks if since this get has been read the last time, the defined delay has elapsed. If not, then it is skipped this time.<br> PollDelay can be specified as seconds or as x[0-9]+ which means a multiple of the interval in the define command. - <li><b>get[0-9]*Regex</b></li> - If this attribute is specified, the Regex defined here is used to extract the value from the HTTP Response - and assign it to a Reading with the name defined in the get[0-9]+Name attribute.<br> - if this attribute is not specified for an individual Reading but as getRegex, then it applies to all get options - where no specific Regex is defined.<br> - If neither a generic getRegex attribute nor a specific get[0-9]+Regex attribute is specified, then HTTPMOD - tries all Regex / Reading pairs defined in Reading[0-9]+Name and Reading[0-9]+Regex attributes and assigns the - Readings that match. - <li><b>get[0-9]*Expr</b></li> - this attribute behaves just like Reading[0-9]*Expr but is applied to a get value. - <li><b>get[0-9]*Map</b></li> - this attribute behaves just like Reading[0-9]*Map but is applied to a get value. - <li><b>get[0-9]*Format</b></li> - this attribute behaves just like Reading[0-9]*Format but is applied to a get value. - - <li><b>get[0-9]*Decode</b></li> - defines an encoding to be used in a call to the perl function decode to convert the raw data string read from the device to a reading. This can be used if the device delivers strings in an encoding like cp850 instead of utf8. - <li><b>get[0-9]*Encode</b></li> - defines an encoding to be used in a call to the perl function encode to convert the raw data string read from the device to a reading. This can be used if the device delivers strings in an encoding like cp850 and after decoding it you want to reencode it to e.g. utf8. + <br> + + <li><b>(get|set)[0-9]*TextArg</b></li> + For a get command this defines that the command accepts a text value after the option name. + By default a get command doesn't accept optional values after the command name. + If TextArg is specified and a value is passed after the get name then this value can then be used in a request URL, header or data + as replacement for $val or in a user defined replacement that uses the internal "value" ($hash->{value}).<br> + If used for a set command then it defines that the value to be set doesn't require any validation / conversion. + The raw value is passed on as text to the device. By default a set command expects a numerical value or a text value that is converted to a numeric value using a map. - <li><b>get[0-9]*CheckAllReadings</b></li> - this attribute modifies the behavior of HTTPMOD when the HTTP Response of a get command is parsed. <br> - If this attribute is set to 1, then additionally to any matching of get specific regexes (get[0-9]*Regex), - also all the Regex / Reading pairs defined in Reading[0-9]+Name and Reading[0-9]+Regex attributes are checked and if they match, the coresponding Readings are assigned as well. + <li><b>set[0-9]+Min</b></li> + Minimum value for input validation. + <li><b>set[0-9]+Max</b></li> + Maximum value for input validation. + <li><b>set[0-9]+IExpr</b></li> + Perl Expression to compute the raw value to be sent to the device from the input value passed to the set. + <li><b>set[0-9]+Expr</b></li> + This is the old syntax for (get|reading)[0-9]*IExpr. It should be replaced by (get|reading)[0-9]*IExpr. The set command upgradeAttributes which becomes visible when the attribute enableControlSet is set to 1, can do this renaming automatically. + + <li><b>set[0-9]+IMap</b></li> + Map that defines a mapping from raw to visible values like "0:mittig, 1:oberhalb, 2:unterhalb". This attribute atomatically creates a hint for FhemWEB so the user can choose one of the visible values and HTTPMOD sends the raw value to the device. + <li><b>set[0-9]+Map</b></li> + This is the old syntax for (get|reading)[0-9]*IMap. It should be replaced by (get|reading)[0-9]*IMap. The set command upgradeAttributes which becomes visible when the attribute enableControlSet is set to 1, can do this renaming automatically. + <li><b>set[0-9]+Hint</b></li> + Explicit hint for fhemWEB that will be returned when set ? is seen. + <li><b>set[0-9]*NoArg</b></li> + Defines that this set option doesn't require arguments. It allows sets like "on" or "off" without further values. + <li><b>set[0-9]*ParseResponse</b></li> + defines that the HTTP response to the set will be parsed as if it was the response to a get command. <br> - <li><b>get[0-9]*URLExpr</b></li> - Defines a Perl expression to specify the HTTP Headers for this request. This overwrites any other header specification and should be used carefully only if needed e.g. to pass additional variable data to a web service. The original Header is availabe as $old. - <li><b>get[0-9]*DatExpr</b></li> - Defines a Perl expression to specify the HTTP Post data for this request. This overwrites any other post data specification and should be used carefully only if needed e.g. to pass additional variable data to a web service. - The original Data is availabe as $old. - <li><b>get[0-9]*HdrExpr</b></li> - Defines a Perl expression to specify the URL for this request. This overwrites any other URL specification and should be used carefully only if needed e.g. to pass additional variable data to a web service. - The original URL is availabe as $old. + + <li><b>(get|set)[0-9]*URLExpr</b></li> + Defines a Perl expression to specify the HTTP Headers for this request. This overwrites any other header specification and should be used carefully only if needed. The original Header is availabe as $old. Typically this feature is not needed and it might go away in future versions of HTTPMOD. Please use the "replacement" attributes if you want to pass additional variable data to a web service. + <li><b>(get|set)[0-9]*DatExpr</b></li> + Defines a Perl expression to specify the HTTP Post data for this request. This overwrites any other post data specification and should be used carefully only if needed. The original Data is availabe as $old. Typically this feature is not needed and it might go away in future versions of HTTPMOD. Please use the "replacement" attributes if you want to pass additional variable data to a web service. + <li><b>(get|set)[0-9]*HdrExpr</b></li> + Defines a Perl expression to specify the URL for this request. This overwrites any other URL specification and should be used carefully only if needed. The original URL is availabe as $old. Typically this feature is not needed and it might go away in future versions of HTTPMOD. Please use the "replacement" attributes if you want to pass additional variable data to a web service. <br> + + + <li><b>set[0-9]*ReAuthRegex</b></li> + Regex that will detect when a session has expired during a set operation and a new login needs to be performed. + It works like the global reAuthRegex but is used for set operations. + + <li><b>reAuthRegex</b></li> + regular Expression to match an error page indicating that a session has expired and a new authentication for read access needs to be done. + This attribute only makes sense if you need a forms based authentication for reading data and if you specify a multi step login procedure based on the sid.. attributes.<br> + This attribute is used for all requests. For set operations you can however specify individual reAuthRegexes with the set[0-9]*ReAuthRegex attributes. + <br><br> + <li><b>sid[0-9]*URL</b></li> + different URLs or one common URL to be used for each step of an optional login procedure. + <li><b>sid[0-9]*IdRegex</b></li> + different Regexes per login procedure step or one common Regex for all steps to extract the session ID from the HTTP response + <li><b>sid[0-9]*Data.*</b></li> + data part for each step to be sent as POST data to the corresponding URL + <li><b>sid[0-9]*Header.*</b></li> + HTTP Headers to be sent to the URL for the corresponding step + <li><b>sid[0-9]*IgnoreRedirects</b></li> + tell HttpUtils to not follow redirects for this authentication request + <li><b>clearSIdBeforeAuth</b></li> + will set the session id to "" before doing the authentication steps + <li><b>authRetries</b></li> + number of retries for authentication procedure - defaults to 1 <br> + + <li><b>replacement[0-9]*Regex</b></li> + Defines a replacement to be applied to an HTTP request header, data or URL before it is sent. This allows any part of the request to be modified based on a reading, an internal or an expression. + The regex defines which part of a header, data or URL should be replaced. The replacement is defined with the following attributes: + <li><b>replacement[0-9]*Mode</b></li> + Defines how the replacement should be done and what replacementValue means. Valid options are text, reading, internal and expression. + <li><b>replacement[0-9]*Value</b></li> + Defines the replacement. If the corresponding replacementMode is <code>text</code>, then value is a static text that is used as the replacement.<br> + If replacementMode is <code>reading</code> then Value can be the name of a reading of this device or it can be a reading of a different device referred to by devicename:reading.<br> + If replacementMode is <code>internal</code> the Value can be the name of an internal of this device or it can be an internal of a different device referred to by devicename:internal.<br> + If replacementMode is <code>expression</code> the the Value is treated as a Perl expression that computes the replacement value. The expression can use $1, $2 and so on to refer to capture groups of the corresponding regex that is matched against the original URL, header or post data.<br> + If replacementMode is <code>key</code> then the module will use a value from a key / value pair that is stored in an obfuscated form in the file system with the set storeKeyValue command. This might be useful for storing passwords. + + <li><b>[gs]et[0-9]*Replacement[0-9]*Value</b></li> + This attribute can be used to override the replacement value for a specific get or set. + <br> + + <li><b>get|reading[0-9]*MaxAge</b></li> + Defines how long a reading is valid before it is automatically overwritten with a replacement when the read function is called the next time. + <li><b>get|reading[0-9]*MaxAgeReplacement</b></li> + specifies the replacement for MaxAge - either as a static text or as a perl expression. + <li><b>get|reading[0-9]*MaxAgeReplacementMode</b></li> + specifies how the replacement is interpreted: can be text, expression and delete. + <br> + + + <li><b>httpVersion</b></li> + defines the HTTP-Version to be sent to the server. This defaults to 1.0. + <li><b>sslVersion</b></li> + defines the SSL Version for the negotiation with the server. The attribute is evaluated by HttpUtils. If it is not specified, HttpUtils assumes SSLv23:!SSLv3:!SSLv2 + <li><b>sslArgs</b></li> + defines a list that is converted to a key / value hash and gets passed to HttpUtils. To avoid certificate validation for broken servers you can for example specify + <code>attr myDevice sslArgs SSL_verify_mode,SSL_VERIFY_NONE</code> + <li><b>noShutdown</b></li> + pass the noshutdown flag to HTTPUtils for webservers that need it (some embedded webservers only deliver empty pages otherwise) + + <li><b>disable</b></li> + stop doing automatic HTTP requests while this attribute is set to 1 + <li><b>enableControlSet</b></li> + enables the built in set commands interval, stop, start, reread, upgradeAttributes, storeKeyValue. + <li><b>enableCookies</b></li> + enables the built cookie handling if set to 1. With cookie handling each HTTPMOD device will remember cookies that the server sets and send them back to the server in the following requests. + This simplifies session magamenet in cases where the server uses a session ID in a cookie. In such cases enabling Cookies should be sufficient and no sidRegex and no manual definition of a Cookie Header should be necessary. <li><b>showMatched</b></li> - if set to 1 then HTTPMOD will create a reading that contains the names of all readings that could be matched in the last request. + if set to 1 then HTTPMOD will create a reading with the name MATCHED_READINGS + that contains the names of all readings that could be matched in the last request. + <li><b>showError</b></li> + if set to 1 then HTTPMOD will create a reading and event with the Name LAST_ERROR + that contains the error message of the last error returned from HttpUtils. + <li><b>timeout</b></li> + time in seconds to wait for an answer. Default value is 2 <li><b>queueDelay</b></li> HTTP Requests will be sent from a queue in order to avoid blocking when several Requests have to be sent in sequence. This attribute defines the delay between calls to the function that handles the send queue. It defaults to one second. <li><b>queueMax</b></li> Defines the maximum size of the send queue. If it is reached then further HTTP Requests will be dropped and not be added to the queue <li><b>minSendDelay</b></li> Defines the minimum time between two HTTP Requests. + <br> + + + <li><b>enableXPath</b></li> + This attribute should no longer be used. Please specify an HTTP XPath in the dedicated attributes shown above. + <li><b>enableXPath-Strict</b></li> + This attribute should no longer be used. Please specify an XML XPath in the dedicated attributes shown above. + <li><b>parseFunction1</b> and <b>parseFunction2</b></li> + These functions allow an experienced Perl / Fhem developer to plug in his own parsing functions.<br> + Please look into the module source to see how it works and don't use them if you are not sure what you are doing. + + <li><b>Remarks regarding the automatically created userattr entries</b></li> + Fhemweb allows attributes to be edited by clicking on them. However this does not work for attributes that match to a wildcard attribute. To circumvent this restriction HTTPMOD automatically adds an entry for each instance of a defined wildcard attribute to the device userattr list. E.g. if you define a reading[0-9]Name attribute as reading01Name, HTTPMOD will add reading01Name to the device userattr list. These entries only have the purpose of making editing in Fhemweb easier. </ul> <br> <b>Author's notes</b><br><br>