98_HTTPMOD: new version with many internal changes, own namespace and functions in a separate utils module

git-svn-id: https://svn.fhem.de/fhem/trunk/fhem@22997 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
StefanStrobel 2020-10-20 17:31:50 +00:00
parent 3fc241c56c
commit dcb50b8974
2 changed files with 1682 additions and 2095 deletions

File diff suppressed because it is too large Load Diff

View File

@ -27,20 +27,28 @@ use warnings;
use GPUtils qw(:all); use GPUtils qw(:all);
use Time::HiRes qw(gettimeofday); use Time::HiRes qw(gettimeofday);
use Encode qw(decode encode); use Encode qw(decode encode);
use Scalar::Util qw(looks_like_number);
use DevIo; use DevIo;
use Exporter ('import'); use Exporter ('import');
our @EXPORT_OK = qw(UpdateTimer FhemCaller our @EXPORT_OK = qw(UpdateTimer FhemCaller
StopQueueTimer
StartQueueTimer
ValidRegex ValidExpr ValidRegex ValidExpr
EvalExpr EvalExpr
FormatVal
MapConvert MapToHint MapConvert MapToHint
CheckRange
ReverseWordOrder
SwapByteOrder
ReadKeyValue StoreKeyValue ReadKeyValue StoreKeyValue
ManageUserAttr ManageUserAttr
MemReading MemReading
FlattenJSON FlattenJSON
BodyDecode BodyDecode
IsOpen IsOpen
FmtTime FmtTimeMs
ReadableArray
); );
our %EXPORT_TAGS = (all => [@EXPORT_OK]); our %EXPORT_TAGS = (all => [@EXPORT_OK]);
@ -104,43 +112,108 @@ sub UpdateTimer {
if ($cmd eq 'stop' || !$intvl) { # stop timer if ($cmd eq 'stop' || !$intvl) { # stop timer
RemoveInternalTimer("update:$name"); RemoveInternalTimer("update:$name");
if ($hash->{TRIGGERTIME}) { if ($hash->{'.TRIGGERTIME'}) {
Log3 $name, 4, "$name: UpdateTimer called from " . FhemCaller() . " with cmd $cmd and interval $intvl stops timer"; Log3 $name, 4, "$name: UpdateTimer called from " . FhemCaller() . " with cmd $cmd and interval $intvl stops timer";
delete $hash->{TRIGGERTIME}; delete $hash->{'.TRIGGERTIME'};
delete $hash->{TRIGGERTIME_FMT}; #delete $hash->{TRIGGERTIME_FMT};
delete $hash->{lastUpdate}; delete $hash->{'.LastUpdate'};
} }
return; return;
} }
if ($cmd eq 'next') { if ($cmd eq 'next') {
$hash->{lastUpdate} = $now; # start timer from now, ignore potential last update time $hash->{'.LastUpdate'} = $now; # start timer from now, ignore potential last update time
} }
my $nextUpdate; my $nextUpdate;
if ($hash->{TimeAlign}) { # TimeAlign: do as if interval started at time w/o drift ... if ($hash->{'.TimeAlign'}) { # TimeAlign: do as if interval started at time w/o drift ...
my $count = int(($now - $hash->{TimeAlign}) / $intvl); # $intvl <> 0,has been checked above my $count = int(($now - $hash->{'.TimeAlign'}) / $intvl); # $intvl <> 0,has been checked above
$nextUpdate = $count * $intvl + $hash->{TimeAlign}; # next aligned time >= now, lastUpdate doesn't matter with alignment $nextUpdate = $count * $intvl + $hash->{'.TimeAlign'}; # next aligned time >= now, lastUpdate doesn't matter with alignment
$nextUpdate += $intvl if ($nextUpdate <= $now); # works for initial alignment as welas for next round $nextUpdate += $intvl if ($nextUpdate <= $now); # works for initial alignment as welas for next round
} }
else { # no align time -> just add the interval to now else { # no align time -> just add the interval to now
if ($hash->{lastUpdate}) { if ($hash->{'.LastUpdate'}) {
$nextUpdate = $hash->{lastUpdate} + $intvl; $nextUpdate = $hash->{'.LastUpdate'} + $intvl;
} }
else { else {
$nextUpdate = $now; # first call -> don't wait for interval to pass $nextUpdate = $now; # first call -> don't wait for interval to pass
} }
} }
$hash->{TRIGGERTIME} = $nextUpdate; $hash->{'.TRIGGERTIME'} = $nextUpdate;
$hash->{TRIGGERTIME_FMT} = FmtDateTime($nextUpdate); #$hash->{TRIGGERTIME_FMT} = FmtDateTime($nextUpdate);
my $delay = sprintf ("%.1f", $nextUpdate - $now); my $delay = sprintf ("%.1f", $nextUpdate - $now);
Log3 $name, 4, "$name: UpdateTimer called from " . FhemCaller() . " with cmd $cmd" . Log3 $name, 4, "$name: UpdateTimer called from " . FhemCaller() . " with cmd $cmd" .
" sets timer to call update function in $delay sec at $hash->{TRIGGERTIME_FMT}, interval $intvl"; " sets timer to call update function in $delay sec at " . FmtDateTime($nextUpdate) . ", interval $intvl";
RemoveInternalTimer("update:$name"); RemoveInternalTimer("update:$name");
InternalTimer($nextUpdate, $updFn, "update:$name", 0); # now set the timer InternalTimer($nextUpdate, $updFn, "update:$name", 0); # now set the timer
return; return;
} }
######################################################
# set internal timer for next queue processing
# to now + passed delay (if delay is passed)
# if no delay is passed, use attribute queueDelay if no shorter timer is already set
#
# startQueueTimer is called from Modbus:
# - in queueRequest when something got added to the queue
# - end of get/set to set it to immediate processing
# - at the end of HandleResponse
# - in processRequestQueue to set a new delay
# - in checkDelay called from processRequestQueue
# before it returns 1 (to ask the caller to return because delay is not over yet)
# but startQueueTimer does only set the timer if the queue contains something
#
sub StartQueueTimer {
my $ioHash = shift;
my $pFunc = shift; # e.g. \&Modbus::ProcessRequestQueue
my $oRef = shift; # optional hash ref for passing options
my $name = $ioHash->{NAME};
my $pDelay = $oRef->{'delay'} // AttrVal($name, 'queueDelay', 1); # delay until queue processing call
my $silent = $oRef->{'silent'} // 0;
my $msg = $oRef->{'log'} // '';
my $qlen = ($ioHash->{QUEUE} ? scalar(@{$ioHash->{QUEUE}}) : 0);
if ($qlen) {
my $now = gettimeofday();
my $delay = (defined($pDelay) ? $pDelay : AttrVal($name, 'queueDelay', 1));
return if ($ioHash->{nextQueueRun} && $ioHash->{nextQueueRun} < $now+$delay);
RemoveInternalTimer ("queue:$name");
InternalTimer($now+$delay, $pFunc, "queue:$name", 0);
$ioHash->{nextQueueRun} = $now+$delay;
Log3 $name, 5, "$name: StartQueueTimer called from " . FhemCaller() .
' sets internal timer to process queue in ' .
sprintf ('%.3f', $delay) . ' seconds' . ($msg ? ", $msg" : '') if (!$silent);
}
else {
Log3 $name, 5, "$name: StartQueueTimer called from " . FhemCaller() .
' removes internal timer because queue is empty' if ($ioHash->{nextQueueRun} && !$silent);
delete $ioHash->{nextQueueRun};
RemoveInternalTimer ("queue:$name");
}
return;
}
########################################################################################
# remove internal timer for next queue processing
# called at the end of open and close (initialized state, queue should be empty)
# end when queue becomes empty while processing the queue (not really ... todo:)
# when processRequestQueue gets called from fhem.pl via internal timer,
# this timer is removed internally -> only nextQueueRun is deleted in processRequestQueue
sub StopQueueTimer {
my $ioHash = shift;
my $oRef = shift; # optional hash ref for passing options
my $silent = $oRef->{'silent'} // 0;
my $name = $ioHash->{NAME};
if ($ioHash->{nextQueueRun}) {
RemoveInternalTimer ("queue:$name");
delete $ioHash->{nextQueueRun};
Log3 $name, 5, "$name: StopQueueTimer called from " . FhemCaller() .
' removes internal timer for queue processing' if (!$silent);
}
return;
}
######################################################################### #########################################################################
# check if a regex is valid # check if a regex is valid
@ -161,39 +234,6 @@ sub ValidRegex {
} }
##################################################################################
# evaluate perl expression and make variables available for the expression
# call like $new = EvalExpr($hash, $exp, $rawVal, {'%setValArr' => \@setValArr});
#
# Problem: can not pass $val and @val at the same time because evalSpecials
# expects %val for both.
# also $hash can not be passed but will become %hash. Same reason.
#
sub EvalExprWithFhemFunctions {
my $hash = shift; # the current device hash
my $exp = shift; # the expression to be used
my $text = shift; # the original value to be avaliable as val / old / rawVal
my $val_ref = shift; # the values to be passed via EvalSpecials into eval
my $name = $hash->{NAME};
my %vHash;
$val_ref = \%vHash if (!$val_ref); # if no value hash is passed create one
$val_ref->{'%hash'} = $hash;
$val_ref->{'%name'} = $name;
$val_ref->{'%val'} = $text if (!exists $val_ref->{'%val'});
$val_ref->{'%old'} = $text if (!exists $val_ref->{'%old'});
$val_ref->{'%rawVal'} = $text if (!exists $val_ref->{'%rawVal'});
$val_ref->{'%inCheckEval'} = 1 if (!exists $val_ref->{'%inCheckEval'});
if ($exp) {
$exp = EvalSpecials($exp, %{$val_ref});
$text = AnalyzePerlCommand(undef, $exp);
#Log3 $name, 5, "$name: eval $exp resulted in $text";
}
return $text;
}
################################################################### ###################################################################
# new combined function for evaluating perl expressions # new combined function for evaluating perl expressions
# pass values via hash reference similar to fhem EvalSpecials # pass values via hash reference similar to fhem EvalSpecials
@ -213,56 +253,45 @@ sub EvalExprWithFhemFunctions {
# #
sub EvalExpr { sub EvalExpr {
my $hash = shift; # the current device hash my $hash = shift; # the current device hash
my $exp = shift; # the expression to be used my $oRef = shift; # optional hash ref for passing options and variables for use in expressions
my $vRef = shift; # optional setValArr as reference for use in expressions
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $val = 0; my $val = $oRef->{'val'} // ''; # need input value already now as potential return value
my @val = ($val); my $checkOnly = $oRef->{'checkOnly'} // 0; # only syntax check
my %vHash; my $NlIfNoExp = $oRef->{'nullIfNoExp'} // 0; # return 0 if expression is missing
my $exp = $oRef->{'expr'} // ''; # the expression to be used
my $r = ref $vRef; my $action = $oRef->{'action'} // 'perl expression eval'; # context for logging
if (ref $vRef eq '') { my @val = ($val); # predefined variables, can be overwritten in %vHash
$val = $vRef; my $old = $val;
$vRef = shift;
#Log3 $name, 5, "$name: old syntax used, ref is $r, val is $val";
} else {
$val = $vRef->{'val'} // ''; # need input value already now as potential return value
#Log3 $name, 5, "$name: new syntax used, ref is $r, val is $val";
}
$vRef = \%vHash if (ref ($vRef) ne 'HASH'); # create hash if not passed (rare case)
my $action = $vRef->{'action'} // 'perl expression eval'; # context for logging
my $checkOnly = $vRef->{'checkOnly'} // 0; # only syntax check
return $val if (!$exp);
my $old = $val; # predefined variables, can be overwritten in %vHash
my $rawVal = $val; my $rawVal = $val;
my $text = $val; my $text = $val;
return 0 if ($NlIfNoExp && !$exp);
return $val if (!$exp);
my $inCheckEval = ($checkOnly ? 0 : 1); my $inCheckEval = ($checkOnly ? 0 : 1);
my $assign = ''; my $assign = '';
foreach my $key (keys %{$vRef}) { foreach my $key (keys %{$oRef}) {
my $type = ref $vRef->{$key}; my $type = ref $oRef->{$key};
my $vName = substr($key,1); my $vName = substr($key,1);
my $vType = substr($key,0,1); my $vType = substr($key,0,1);
if ($type eq 'SCALAR') { if ($type eq 'SCALAR') {
$assign .= "my \$$vName = \${\$vRef->{'$key'}};"; # assign ref to scalar as scalar $assign .= "my \$$vName = \${\$oRef->{'$key'}};"; # assign ref to scalar as scalar
} }
elsif ($type eq 'ARRAY' && $vType eq '$') { elsif ($type eq 'ARRAY' && $vType eq '$') {
$assign .= "my \$$vName = \$vRef->{'$key'};"; # assign array ref as array ref $assign .= "my \$$vName = \$oRef->{'$key'};"; # assign array ref as array ref
} }
elsif ($type eq 'ARRAY') { elsif ($type eq 'ARRAY') {
$assign .= "my \@$vName = \@{\$vRef->{'$key'}};"; # assign array ref as array $assign .= "my \@$vName = \@{\$oRef->{'$key'}};"; # assign array ref as array
} }
elsif ($type eq 'HASH' && $vType eq '$') { elsif ($type eq 'HASH' && $vType eq '$') {
$assign .= "my \$$vName = \$vRef->{'$key'};"; # assign hash ref as hash ref $assign .= "my \$$vName = \$oRef->{'$key'};"; # assign hash ref as hash ref
} }
elsif ($type eq 'HASH') { elsif ($type eq 'HASH') {
$assign .= "my \%$vName = \%{\$vRef->{'$key'}};"; # assign hash ref as hash $assign .= "my \%$vName = \%{\$oRef->{'$key'}};"; # assign hash ref as hash
} }
elsif ($type eq '' && $vType eq '$') { elsif ($type eq '' && $vType eq '$') {
$assign .= "my \$$vName = \$vRef->{'$key'};"; # assign scalar as scalar $assign .= "my \$$vName = \$oRef->{'$key'};"; # assign scalar as scalar
} }
} }
$exp = $assign . ($checkOnly ? 'return undef;' : '') . $exp; $exp = $assign . ($checkOnly ? 'return undef;' : '') . $exp;
@ -280,7 +309,6 @@ sub EvalExpr {
} }
########################################################### ###########################################################
# return the name of the caling function for debug output # return the name of the caling function for debug output
sub FhemCaller { sub FhemCaller {
@ -295,19 +323,26 @@ sub FhemCaller {
######################################### #########################################
# Try to convert a value with a map # Try to convert a value with a map
# called from Set and FormatReading # called from Set and FormatReading
# todo: also pass map as named parameter
sub MapConvert { sub MapConvert {
my $hash = shift; my $hash = shift;
my $map = shift; my $oRef = shift; # hash ref for passing options and variables for use in expressions
my $val = shift;
my $reverse = shift; my $map = $oRef->{'map'} // ''; # map to use
my $reverse = $oRef->{'reverse'} // 0; # use reverse map
my $action = $oRef->{'action'} // 'apply map'; # context for logging
my $UndefIfNoMatch = $oRef->{'undefIfNoMatch'} // 0; # return undef if map is not matching,
my $inVal = $oRef->{'val'} // ''; # input value
my $name = $hash->{NAME}; my $name = $hash->{NAME};
return $inVal if (!$map); # don't change anyting if map is empty
$map =~ s/\s+/ /g; # substitute all \t \n etc. by one space only $map =~ s/\s+/ /g; # substitute all \t \n etc. by one space only
if ($reverse) { if ($reverse) {
$map =~ s/([^, ][^,\$]*):([^,][^,\$]*),? */$2:$1, /g; # reverse map $map =~ s/([^, ][^,\$]*):([^,][^,\$]*),? */$2:$1, /g; # reverse map
} }
# spaces in words allowed, separator is ',' or ':' # spaces in words allowed, separator is ',' or ':'
$val = decode ('UTF-8', $val); # convert nbsp from fhemweb my $val = decode ('UTF-8', $inVal); # convert nbsp from fhemweb
$val =~ s/\s|&nbsp;/ /g; # back to normal spaces in case it came from FhemWeb with coded Blank $val =~ s/\s|&nbsp;/ /g; # back to normal spaces in case it came from FhemWeb with coded Blank
my %mapHash = split (/, *|:/, $map); # reverse hash aus dem reverse string my %mapHash = split (/, *|:/, $map); # reverse hash aus dem reverse string
@ -321,7 +356,8 @@ sub MapConvert {
else { else {
Log3 $name, 3, "$name: MapConvert called from " . FhemCaller() . " did not find $val in" . Log3 $name, 3, "$name: MapConvert called from " . FhemCaller() . " did not find $val in" .
($reverse ? " reversed" : "") . " map $map"; ($reverse ? " reversed" : "") . " map $map";
return; return if ($UndefIfNoMatch);
return $inVal;
} }
} }
@ -336,6 +372,102 @@ sub MapToHint {
} }
#####################################################################
# check that a value is in a defined range
sub CheckRange {
my $hash = shift;
my $oRef = shift; # optional hash ref for passing options and variables for use in expressions
my $val = $oRef->{'val'} // ''; # input value
my $min = $oRef->{'min'} // ''; # min value
my $max = $oRef->{'max'} // ''; # max value
my $name = $hash->{NAME};
$val =~ s/\s+//g; # remove spaces just to be sure
# if either min or max are specified, val has to be numeric
if (!looks_like_number $val && (looks_like_number $min || looks_like_number $max)) {
Log3 $name, 5, "$name: checkRange for " . FhemCaller() . " rejects $val because it is not numeric";
return;
}
if (looks_like_number $min) {
Log3 $name, 5, "$name: checkRange for " . FhemCaller() . " checks $val against min $min";
return if ($val < $min);
}
if (looks_like_number $max) {
Log3 $name, 5, "$name: checkRange for " . FhemCaller() . " checks $val against max $max";
return if ($val > $max);
}
return 1;
}
#####################################################################
# check that a value is in a defined range
sub FormatVal {
my $hash = shift;
my $oRef = shift; # optional hash ref for passing options and variables for use in expressions
my $val = $oRef->{'val'} // ''; # input value
my $format = $oRef->{'format'} // ''; # format string
my $name = $hash->{NAME};
return $val if (!$format);
my $newVal = sprintf($format, $val);
Log3 $name, 5, "$name: FormatVal for " . FhemCaller() . " formats $val with $format, result is $newVal";
return $newVal;
}
#####################################
# called from send and parse
# reverse order of word registers
sub ReverseWordOrder {
my $hash = shift; # hash only needed for logging
my $buffer = shift;
my $len = shift;
my $name = $hash->{NAME}; # name of device for logging
use bytes;
$len = length($buffer) if (!defined $len); # optional parameter
return $buffer if ($len < 2 || length ($buffer) < 3); # nothing to be done if only one register
Log3 $name, 5, "$name: ReverseWordOrder is reversing order of up to $len registers";
my $work = substr($buffer, 0, $len * 2); # the first 2*len bytes of buffer
my $rest = substr($buffer, $len * 2); # everything after len
my $new = '';
while ($work) {
$new = substr($work, 0, 2) . $new; # prepend first two bytes of work to new
$work = substr($work, 2); # remove first word from work
}
my $newBuffer = $new . $rest;
Log3 $name, 5, "$name: ReverseWordOrder for " . FhemCaller() . " is transforming "
. unpack ('H*', $buffer) . " to " . unpack ('H*', $newBuffer);
return $newBuffer;
}
#####################################
# called from send and parse
# reverse byte order in word registers
sub SwapByteOrder {
my $hash = shift; # hash only needed for logging
my $buffer = shift;
my $len = shift;
my $name = $hash->{NAME}; # name of device for logging
use bytes;
$len = length($buffer) if (!defined $len); # optional parameter
Log3 $name, 5, "$name: SwapByteOrder is reversing byte order of up to $len registers";
my $rest = substr($buffer, $len * 2); # everything after len
my $nval = '';
for (my $i = 0; $i < $len; $i++) {
$nval = $nval . substr($buffer,$i*2 + 1,1) . substr($buffer,$i*2,1);
};
my $newBuffer = $nval . $rest;
Log3 $name, 5, "$name: SwapByteOrder for " . FhemCaller() . " is transforming "
. unpack ('H*', $buffer) . " to " . unpack ('H*', $newBuffer);
return $newBuffer;
}
######################################################################### #########################################################################
# set userAttr-Attribute for Regex-Attrs # set userAttr-Attribute for Regex-Attrs
# pass device hash and new attr based on a regex attr # pass device hash and new attr based on a regex attr
@ -547,22 +679,22 @@ sub BodyDecode {
if ($bodyDecode eq 'auto' or $bodyDecode eq 'Auto') { if ($bodyDecode eq 'auto' or $bodyDecode eq 'Auto') {
if ($header =~/Content-Type:.*charset=([\w\-\.]+)/i) { if ($header =~/Content-Type:.*charset=([\w\-\.]+)/i) {
$bodyDecode = $1; $bodyDecode = $1;
Log3 $name, 4, "$name: Read found charset header and set decoding to $bodyDecode (bodyDecode was set to auto)"; Log3 $name, 4, "$name: BodyDecode found charset header and set decoding to $bodyDecode (bodyDecode was set to auto)";
} }
else { else {
$bodyDecode = ""; $bodyDecode = "";
Log3 $name, 4, "$name: Read found no charset header (bodyDecode was set to auto)"; Log3 $name, 4, "$name: BodyDecode found no charset header (bodyDecode was set to auto)";
} }
} }
if ($bodyDecode) { if ($bodyDecode) {
if ($bodyDecode =~ m{\A [Nn]one \z}xms) { if ($bodyDecode =~ m{\A [Nn]one \z}xms) {
Log3 $name, 4, "$name: Read is not decoding the response body (set to none)"; Log3 $name, 4, "$name: BodyDecode is not decoding the response body (set to none)";
} }
else { else {
$body = decode($bodyDecode, $body); $body = decode($bodyDecode, $body);
Log3 $name, 4, "$name: Read is decoding the response body as $bodyDecode "; Log3 $name, 4, "$name: BodyDecode is decoding the response body as $bodyDecode ";
} }
#Log3 $name, 5, "$name: Read callback " . ($body ? "new body as utf-8 is: \n" . encode ('utf-8', $body) : "body empty"); #Log3 $name, 5, "$name: BodyDecode callback " . ($body ? "new body as utf-8 is: \n" . encode ('utf-8', $body) : "body empty");
} }
return $body; return $body;
} }
@ -583,8 +715,7 @@ sub IsOpen {
#################################################### ####################################################
# format time as string with msecs as fhem.pl does # format time as string with msecs as fhem.pl does
sub FmtTime($) sub FmtTimeMs {
{
my $time = shift // 0; my $time = shift // 0;
my $seconds; my $seconds;
my $mseconds; my $mseconds;
@ -607,6 +738,15 @@ sub FmtTime($)
} }
#########################################################
sub ReadableArray {
my $val = shift;
my $vString = '';
foreach my $v (@{$val}) {
$vString .= ($vString eq '' ? '' : ', ') . ($v =~ /^[[:print:]]+$/ ? $v : 'hex ' . unpack ('H*', $v));
}
return $vString
}
1; 1;