From ec7f5baf7f515193e53f99a825627f7ffd43b849 Mon Sep 17 00:00:00 2001 From: Wzut <> Date: Wed, 8 Apr 2020 17:57:23 +0000 Subject: [PATCH] 98_readingsWatcher: refactoring git-svn-id: https://svn.fhem.de/fhem/trunk@21629 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/98_readingsWatcher.pm | 1035 ++++++++++++++++--------------- 1 file changed, 524 insertions(+), 511 deletions(-) diff --git a/fhem/FHEM/98_readingsWatcher.pm b/fhem/FHEM/98_readingsWatcher.pm index b2147e8b6..273cc14a8 100644 --- a/fhem/FHEM/98_readingsWatcher.pm +++ b/fhem/FHEM/98_readingsWatcher.pm @@ -1,6 +1,8 @@ ################################################################ +# $Id$ +################################################################ # -# $Id$ +# 98_readingsWatcher # # (c) 2015,2016 Copyright: HCS,Wzut # All rights reserved @@ -13,550 +15,562 @@ # (at your option) any later version. # The GNU General Public License can be found at # http://www.gnu.org/copyleft/gpl.html. -# A copy is found in the textfile GPL.txt and important notices to the license -# from the author is found in LICENSE.txt distributed with these scripts. # This script is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. +# +# +# 2.1.0 => 06.04.20 +# 2.0.0 => 05.04.20 perlcritic -4 / PBP +# 1.7.1 => 25.01.20 fix ErrorValue 0 +# 1.7.0 => 12.01.20 add OR / AND watching +# 1.6.0 => 27.08.19 package, Meta +# 1.5.0 => 18.02.19 +# 1.3.0 => 26.01.18 use ReadingsAge +# 1.2.0 => 15.02.16 add Set, Get +# 1.1.0 => 14.02.16 +# 1.0.0 => (c) HCS, first version +# ################################################################ package FHEM::readingsWatcher; ## no critic 'package' +# das no critic könnte weg wenn die Module nicht mehr zwingend mit NN_ beginnnen müssen + use strict; use warnings; use utf8; use GPUtils qw(GP_Import GP_Export); # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt use Time::HiRes qw(gettimeofday); +use List::Util qw(uniq); -BEGIN +BEGIN { - # Import from main:: - GP_Import( - qw( - attr - AttrVal - AttrNum - CommandAttr - addToAttrList - delFromAttrList - delFromDevAttrList - defs - devspec2array - init_done - InternalTimer - RemoveInternalTimer - IsDisabled - IsIgnored - Log3 - modules - readingsSingleUpdate - readingsBulkUpdate - readingsBeginUpdate - readingsDelete - readingsEndUpdate - readingFnAttributes - ReadingsNum - ReadingsAge - ReadingsTimestamp - ReadingsVal - setReadingsVal - CommandSetReading - CommandDeleteReading - gettimeofday - TimeNow - ) - ); - - # Export to main context - GP_Export( - qw( - Initialize - ) + # Import from main:: + GP_Import( + qw( + attr + AttrVal + AttrNum + CommandAttr + addToAttrList + delFromAttrList + delFromDevAttrList + defs + devspec2array + init_done + InternalTimer + RemoveInternalTimer + IsDisabled + IsIgnored + Log3 + modules + readingsSingleUpdate + readingsBulkUpdate + readingsBeginUpdate + readingsDelete + readingsEndUpdate + readingFnAttributes + ReadingsNum + ReadingsAge + ReadingsTimestamp + ReadingsVal + setReadingsVal + CommandSetReading + CommandDeleteReading + gettimeofday + TimeNow) ); + + # Export to main + GP_Export( qw(Initialize) ); } -# Versions History intern -my %vNotesIntern = -( - "2.0.0" => "April 2020 , perlcritic -4 / PBP", - "1.7.1" => "25.01.20 fix ErrorValue 0", - "1.7.0" => "12.01.20 add OR / AND watching", - "1.6.0" => "27.08.19 package, Meta", - "1.5.0" => "18.02.19", - "1.3.0" => "26.01.18 use ReadingsAge", - "1.2.0" => "15.02.16 add Set, Get", - "1.1.0" => "14.02.16", - "1.0.0" => "(c) HCS, first version" -); - - my $hasmeta = 0; - -if (-e $attr{global}{modpath}.'/FHEM/Meta.pm') - { - $hasmeta = 1; - require FHEM::Meta; - } +# ältere Installationen haben noch kein Meta.pm +if (-e $attr{global}{modpath}.'/FHEM/Meta.pm') { + $hasmeta = 1; + require FHEM::Meta; +} -sub Initialize -{ - my $hash = shift; - $hash->{GetFn} = "FHEM::readingsWatcher::Get"; - $hash->{SetFn} = "FHEM::readingsWatcher::Set"; - $hash->{DefFn} = "FHEM::readingsWatcher::Define"; - $hash->{UndefFn} = "FHEM::readingsWatcher::Undefine"; - $hash->{AttrFn} = "FHEM::readingsWatcher::Attr"; - $hash->{AttrList} = "disable:0,1 interval deleteUnusedReadings:1,0 readingActivity ".$readingFnAttributes; +sub Initialize { - return FHEM::Meta::InitMod( __FILE__, $hash ) if ($hasmeta); + my $hash = shift; + $hash->{GetFn} = "FHEM::readingsWatcher::Get"; + $hash->{SetFn} = "FHEM::readingsWatcher::Set"; + $hash->{DefFn} = "FHEM::readingsWatcher::Define"; + $hash->{UndefFn} = "FHEM::readingsWatcher::Undefine"; + $hash->{AttrFn} = "FHEM::readingsWatcher::Attr"; + $hash->{AttrList} = "disable:0,1 interval deleteUnusedReadings:1,0 readingActivity ".$readingFnAttributes; - return; + return FHEM::Meta::InitMod( __FILE__, $hash ) if ($hasmeta); + + return; } ##################################################################################### -sub Define -{ - my $hash = shift; - my $def = shift; - my ($name, $type, $noglobal) = split("[ \t\n]+", $def, 3); +sub Define { - if(exists($modules{readingsWatcher}{defptr})) - { - my $error = 'one readingsWatcher device is already defined !'; - Log3 $name, 1, $error; - return $error; - } + my $hash = shift; + my $def = shift; + my ($name, $type, $noglobal) = split(m{ \s+ }xms, $def, 3); - $modules{readingsWatcher}{defptr} = $hash; - - if (defined($noglobal) && ($noglobal eq 'noglobal')) - { $hash->{DEF} = 'noglobal'; } - else { addToAttrList('readingsWatcher'); $hash->{DEF} = 'global';} # global -> userattr - - CommandAttr(undef,"$name interval 60") unless (exists($attr{$name}{interval})); - CommandAttr(undef,"$name readingActivity none") unless (exists($attr{$name}{readingActivity})); - - RemoveInternalTimer($hash); - InternalTimer(gettimeofday()+5, 'FHEM::readingsWatcher::OnTimer', $hash, 0); - - if ($hasmeta) { return $@ unless ( FHEM::Meta::SetInternals($hash) ) } - return; -} - -##################################################################################### - -sub Undefine -{ - my $hash = shift; - RemoveInternalTimer($hash); - delete($modules{readingsWatcher}{defptr}); - if ($hash->{DEF} eq 'global') - { - delFromAttrList('readingsWatcher'); # global -> userattr - my @devs = devspec2array("readingsWatcher!="); # wer hat alles ein Attribut readingsWatcher ? - foreach (@devs) - { delFromDevAttrList($_, 'readingsWatcher'); } # aufräumen - } - return; -} - -##################################################################################### - -sub Set -{ - my ($hash, $name, $cmd) = @_; - - if ($cmd eq 'inactive') - { - readingsSingleUpdate($hash, 'state', 'inactive', 1); - RemoveInternalTimer($hash); - $hash->{INTERVAL} = 0; - return; - } - elsif ($cmd eq 'active') - { - readingsSingleUpdate($hash, 'state', 'active', 1); - $hash->{INTERVAL} = AttrVal($name,'interval',60); - return; - } - - return if(IsDisabled($name)); - - if (($cmd eq 'checkNow') || ($cmd eq 'active')) - { - OnTimer($hash); - return; - } - - if ($cmd eq 'clearReadings') - { - foreach (keys %{$defs{$name}{READINGS}}) # alle eignen Readings - { - if ($_ =~ /_/) # device.reading - { - readingsDelete($hash, $_); - Log3 $name,4,"$name, delete reading $_"; - } - } - return; - } - - return "unknown argument $cmd, choose one of checkNow:noArg inactive:noArg active:noArg clearReadings:noArg"; -} - -##################################################################################### - -sub Get -{ - my ($hash, $name , $cmd)= @_; - return if(!$cmd); - - my (@parts, $rSA, $age , @devs, $state); - my ($dw,$rw,$tw,$sw,$aw) = (6,7,7,5,3); - - if ($cmd eq 'devices') - { - foreach my $deviceName (devspec2array("readingsWatcher!=")) - { - $rSA = ($deviceName eq $name) ? '' : AttrVal($deviceName, 'readingsWatcher', ''); - $dw = length($deviceName) if (length($deviceName) > $dw); - - if ($rSA) - { - if (IsDisabled($deviceName)) - { - $sw = 8 if ($sw<8); - push @devs, "$deviceName,-,-,disabled,-"; + if (exists($modules{readingsWatcher}{defptr})) { + my $error = 'one readingsWatcher device is already defined !'; + Log3 $hash, 1, $error; + return $error; } - elsif (IsIgnored($deviceName)) - { - $sw = 7 if ($sw<7); - push @devs, "$deviceName,-,-,ignored,-"; + + $modules{readingsWatcher}{defptr} = $hash; + + if (defined($noglobal) && ($noglobal eq 'noglobal')) { + $hash->{DEF} = 'noglobal'; + } + else { + addToAttrList('readingsWatcher'); + $hash->{DEF} = 'global'; # global -> userattr } - else - { - my @r = split(';',$rSA); - foreach (@r) - { - $_ =~ s/\+/,/g; - @parts = split(',', $_); - if (@parts > 2) - { - my $timeout = int($parts[0]); - $tw = length($timeout) if(length($timeout) > $tw); + CommandAttr(undef, "$name interval 60") unless (exists($attr{$name}{interval})); + CommandAttr(undef, "$name readingActivity none") unless (exists($attr{$name}{readingActivity})); - shift @parts; # Timeoutwert - shift @parts; # Ersatzwert + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+5, 'FHEM::readingsWatcher::OnTimer', $hash, 0); - foreach (@parts) # alle zu überwachenden Readings - { - $_ =~ s/^\s+|\s+$//g; - $_ = 'state' if ($_ eq 'STATE'); - $rw = length($_) if(length($_) > $rw); + if ($hasmeta) { + return $@ unless ( FHEM::Meta::SetInternals($hash) ) + } - if (($_ eq 'state') && (ReadingsVal($deviceName,'state','') eq 'inactive')) - { - $state = 'inactive'; - $age = '-'; - } - else - { - $age = ReadingsAge($deviceName, $_, undef); + return; +} + +##################################################################################### + +sub Undefine { + + my $hash = shift; + RemoveInternalTimer($hash); + delete($modules{readingsWatcher}{defptr}); + + if ($hash->{DEF} eq 'global') { # werden die meisten haben + + delFromAttrList('readingsWatcher'); # global -> userattr + # wer hat alles ein Attribut readingsWatcher gesetzt ? + foreach (devspec2array("readingsWatcher!=")) { + delFromDevAttrList($_, 'readingsWatcher'); # aufräumen + } + } - if (!defined($age)) - { - $state = 'unknown'; - $age = 'undef'; - } - else - { - $state = ($age>$timeout) ? 'timeout' : 'ok'; - } - } - $aw = length($age) if(length($age) > $aw); - $sw = length($state) if(length($state) > $sw); - push @devs, "$deviceName, $_, $timeout, $state, $age"; - } - } # @parts >2 - - else - { - $sw = 16 if ($sw<16); - push @devs, "$deviceName,-,-,wrong parameters,-"; - } - } # not disabled - } # rSA - } - } # foreach - - if (int(@devs)) - { - $dw += 2; - $rw += 2; - $sw += 2; - $aw += 2; - - my $s = 'Device'.(' ' x ($dw-6)).'| Reading'.(' ' x ($rw-7)).'|'.(' ' x ($tw-6)).'Timeout |'.(' ' x ($sw-5)).'State |'.(' ' x ($aw-3)).'Age'; - my $line = ('-'x(length($s))); - while ( $s =~ m/\|/g ) { substr $line,(pos($s)-1),1,'+'; } - $s .= "\n".$line."\n"; - - foreach(@devs) - { - my @ar = split(',',$_); - $s .= $ar[0] . (' ' x ($dw - length$ar[0])); # linksbündig - $s .= '| '.$ar[1] . (' ' x ($rw - length$ar[1])); # linksbündig - $s .= '| '.(' ' x ($tw - length$ar[2])).$ar[2]; # rechtsbündig - $s .= ' |'.(' ' x ($sw - length$ar[3])).$ar[3].' |'; # rechtsbündig - $s .= (' ' x ($aw - length$ar[4])).$ar[4]; # rechtsbündig - $s .= "\n"; - } - return $s.$line; - } - return 'Sorry, no devices with valid attribute readingsWatcher found !'; - } # get devices - - return "unknown command $cmd, choose one of devices:noArg"; -} - -##################################################################################### - -sub OnTimer -{ - my $hash = shift; - my $name = $hash->{NAME}; - my $interval = AttrNum($name, 'interval', 0); - $hash->{INTERVAL} = $interval; - RemoveInternalTimer($hash); - - return if (!$interval); - - InternalTimer(gettimeofday()+$interval, 'FHEM::readingsWatcher::OnTimer', $hash, 0); - - readingsSingleUpdate($hash, 'state', 'disabled', 0) if (IsDisabled($name)); - return if(IsDisabled($name) || !$init_done ); - - my ($timeOutState, $errorValue, $timeout, $error, $readingsList); - my ($rSA, $age, @devices, $rts, @parts, @devs); - my ($alives, $deads, $state, $readings, $skipped, $timeouts) = (0, 0, '', 0, 0, 0); - my @timeOutdevs = (); my @deadDevs = (); my @skipDevs = (); - - foreach (keys %{$defs{$name}{READINGS}}) # alle eignen Readings - { $readingsList .= $_ .',' if ($_ =~ /_/); }# nur die mit _ im Namen - - @devs = devspec2array("readingsWatcher!="); - - my ($areading,$dead,$alive) = split(":",AttrVal($name,'readingActivity','none:dead:alive')); - $dead = 'dead' if (!defined($dead)); - $alive= 'alive' if (!defined($alive)); - $areading = '' if ($areading eq 'none'); - - readingsBeginUpdate($hash); - - foreach my $deviceName (@devs) - { - my $or_and = 0; # Readings beim auswerten OR - my ($d_a,$d_d,$ok_device) = (0,0,0); - $timeOutState = ''; - - $rSA = ($deviceName eq $name) ? '' : AttrVal($deviceName, 'readingsWatcher', ''); - - if($rSA && !IsDisabled($deviceName) && !IsIgnored($deviceName)) - { - push @devices, $deviceName if !grep {/$deviceName/} @devices; # keine doppelten Namen - - $or_and = 1 if (index($rSA,'+') != -1); # Readings beim auswerten AND - $rSA =~ s/\+/,/g ; # eventuell vorhandene + auch in Komma wandeln - - # rSA: timeout, errorValue, reading1, reading2, reading3, ... - # 120,---,temperature,humidity,battery - # or 900,,current,eState / no errorValue = do not change reading - - my @r = split(';', $rSA); - foreach (@r) - { - @parts = split(',', $_); - - if (@parts > 2) - { - $ok_device = 1; - $timeout = int($parts[0]); - $errorValue = $parts[1]; # = leer, Readings des Device nicht anfassen ! - - # die ersten beiden brauchen wir nicht mehr - shift @parts; shift @parts; - - foreach (@parts) # alle zu überwachenden Readings - { - $_ =~ s/^\s+|\s+$//g; # $_ = Reading Name - - $state = 0; - if ($_ eq 'STATE') - { - $_ = 'state'; $state = 1; # Sonderfall STATE - } - - $age = ReadingsAge($deviceName, $_, undef); - - if (defined($age)) - { - $readings++; - - if (($age > $timeout) && ($timeout>0)) - { - push @timeOutdevs, $deviceName if !grep {/$deviceName/} @timeOutdevs; - $timeOutState = "timeout"; - $d_d++; # Device Tote - $timeouts++; - $rts = ReadingsTimestamp($deviceName, $_,0); - setReadingsVal($defs{$deviceName},$_,$errorValue,$rts) if (($errorValue ne'')&& $rts); # leise setzen ohne Event - $defs{$deviceName}->{STATE} = $errorValue if (($errorValue ne'') && $state); - } - else - { - $d_a++; # Device Lebende - $timeOutState = "ok"; - } - - my $r = $deviceName.'_'.$_; - - readingsBulkUpdate($hash, $r, $timeOutState) if ($timeout>0); - $readingsList =~ s/$r,// if ($readingsList) ; # das Reading aus der Liste streichen, leer solange noch kein Device das Attr hat ! - - if ($timeout < 1) - { - $error = "Invalid timeout value $timeout for reading $deviceName $_"; - Log3 $name,2,$name.', '.$error; - } - }#age - else - { - setReadingsVal($defs{$deviceName},$_,'unknown',TimeNow()) if ($errorValue); # leise setzen ohne Event - $defs{$deviceName}->{STATE} = 'unknown' if ($errorValue && $state); - Log3 $name,3,$name.', reading Timestamp for '.$_.' not found on device '.$deviceName; - readingsBulkUpdate($hash, $deviceName.'_'.$_, 'no Timestamp'); - } - }# foreach @parts , Reading - }# parts > 2 - } # Anzahl Readings Sätze im Device, meist nur einer - - if ($ok_device && $timeOutState) - { - if ((!$or_and && $d_d) || ($or_and && !$d_a)) # tot bei OR und mindestens einem Toten || AND aber kein noch Lebender - { - $error = CommandSetReading(undef, "$deviceName $areading $dead") if ($areading); - $deads++; # dead devices - push @deadDevs, $deviceName; - } - else # wenn es nicht tot ist muss es eigentlich noch leben .... - { - $error = CommandSetReading(undef, "$deviceName $areading $alive") if ($areading); - $alives++; # alive devices - } - Log3 $name,2,$name.', '.$error if ($error); - } - else - { - Log3 $name,2,$name.', insufficient parameters for device '.$deviceName.' - skipped !'; - $skipped++; - CommandSetReading(undef, "$deviceName $areading unknown") if ($areading); - push @skipDevs, $deviceName; - } - } # defined($rSA) && !IsDisabled($deviceName) && !IsIgnored($deviceName) - } # foreach $deviceName - - readingsBulkUpdate($hash,'readings' , $readings); - readingsBulkUpdate($hash,'devices' , int(@devices)); - readingsBulkUpdate($hash,'alive' , $alives); - readingsBulkUpdate($hash,'dead' , $deads); - readingsBulkUpdate($hash,'skipped' , $skipped) if ($skipped); - readingsBulkUpdate($hash,'timeouts' , $timeouts); - readingsBulkUpdate($hash,'state' , ($timeouts) ? 'timeout' : 'ok'); - - readingsDelete($hash, 'skipped') if (!$skipped && AttrNum($name,'deleteUnusedReadings','1')); - - # nicht aktualisierte Readings markieren oder löschen - if ($readingsList) - { - my @ar = split(",",$readingsList); - foreach (@ar) - { - if ($_) - { - if (AttrNum($name,'deleteUnusedReadings',1)) - { - readingsDelete($hash, $_); - Log3 $name,3,$name.', delete unused reading '.$_; - } - else - { - readingsBulkUpdate($hash, $_ , 'unused'); - Log3 $name,4,$name.', unused reading '.$_; - } - } - } - } - - if (@devices) - { readingsBulkUpdate($hash,'.associatedWith' , join(',',@devices)); } - else - { readingsDelete($hash, '.associatedWith'); } - - if (int(@timeOutdevs)) - { readingsBulkUpdate($hash,'timeoutdevs',join(',',@timeOutdevs));} - else - { readingsBulkUpdate($hash,'timeoutdevs','none');} - - if (int(@deadDevs)) - { readingsBulkUpdate($hash,'deadDevs',join(',',@deadDevs));} - else - { readingsBulkUpdate($hash,'deadDevs','none');} - - if (int(@skipDevs)) - { readingsBulkUpdate($hash,'skippedDevs',join(',',@skipDevs));} - else - { readingsBulkUpdate($hash,'skippedDevs','none');} - - readingsEndUpdate($hash, 1); - - return; -} - -sub Attr -{ - my ($cmd, $name, $attrName, $attrVal) = @_; - my $hash = $defs{$name}; - - return 'not allowed !' if ($name eq $attrName); - - if ($cmd eq 'set') - { - if ($attrName eq 'disable') - { - readingsSingleUpdate($hash,'state','disabled',1) if (int($attrVal) == 1); - OnTimer($hash) if (int($attrVal) == 0); return; - } - if (($attrName eq 'readingActivity') && ($attrVal eq 'state')) - { - my $error = 'forbidden value state !'; - Log3 $name,1,"$name, readingActivity $error"; - return $error; - } - } - elsif ($cmd eq 'del') - { - if ($attrName eq 'disable') - { - OnTimer($hash); - } - } - return; } + ##################################################################################### + +sub Set { + + my $hash = shift; + my $name = shift; + my $cmd = shift // return "set $name needs at least one argument !"; + + if ($cmd eq 'inactive') { + readingsSingleUpdate($hash, 'state', 'inactive', 1); + RemoveInternalTimer($hash); + $hash->{INTERVAL} = 0; + return; + } + + if ($cmd eq 'active') { + readingsSingleUpdate($hash, 'state', 'active', 1); + $hash->{INTERVAL} = AttrVal($name,'interval',60); + return; + } + + return if (IsDisabled($name)); + + if (($cmd eq 'checkNow') || ($cmd eq 'active')) { + OnTimer($hash); + return; + } + + if ($cmd eq 'clearReadings') { + + foreach (keys %{$defs{$name}{READINGS}}) { # alle eigenen Readings + if ($_ =~ /_/) { # device_reading + readingsDelete($hash, $_); + Log3 $hash,4,"$name, delete reading ".$_; + } + } + + return; + } + + return "unknown argument $cmd, choose one of checkNow:noArg inactive:noArg active:noArg clearReadings:noArg"; +} + +##################################################################################### + +sub Get { + + my $hash = shift; + my $name = shift; + my $cmd = shift // return "get $name needs at least one argument !"; + + return getStateList($name) if ($cmd eq 'devices'); + + return "unknown command $cmd, choose one of devices:noArg"; +} + +##################################################################################### + +sub getStateList { + + my $name = shift; + + my @devs; + + foreach my $device (devspec2array("readingsWatcher!=")) { + my $rSA = ($device ne $name) ? AttrVal($device, 'readingsWatcher', '') : ''; + + next if (!$rSA); + + if (IsDisabled($device)) { + push @devs, "$device,-,-,disabled,-"; + } + elsif (IsIgnored($device)) { + push @devs, "$device,-,-,ignored,-"; + } + else { # valid device + push @devs , IsValidDevice($device,$rSA); + } + } + + return formatStateList(@devs); +} + +##################################################################################### + +sub IsValidDevice { + + my $device = shift; + my @devs; + + foreach (split(';', shift)) { # Anzahl Regelsätze pro Device, meist nur einer + + $_ =~ s/\+/,/g; # OR Readings wie normale Readingsliste behandeln + $_ =~ s/ //g; + + my ($timeout,undef,@readings) = split(',', $_); # der ggf. vorhandene Ersatzstring wird hier nicht benötigt + + if (!@readings) { + push @devs, "$device,-,-,wrong parameters,-"; + } + else { + foreach my $reading (@readings) { # alle zu überwachenden Readings + + my ($age,$state); + + $reading =~ s/ //g; + + if (($reading eq 'state') && (ReadingsVal($device, 'state', '') eq 'inactive')) { + + $state = 'inactive'; + $age = '-'; + } + else { + $age = ReadingsAge($device, $reading, undef); + + if (!defined($age)) { + $state = 'unknown'; + $age = 'undef'; + } + else { + $state = (int($age) > int($timeout)) ? 'timeout' : 'ok'; + } + } + push @devs, "$device,$reading,$timeout,$state,$age"; + } + } + } + + return @devs; +} + +##################################################################################### + +sub formatStateList { + + # Device | Reading | Timeout | State | Age + # -------+------------+---------+---------+-------- + # CUL | credit10ms | 300 | ok | 56 + # lamp | state | 900 | timeout | 3799924 + # -------+------------+---------+---------+-------- + + my (@devs) = @_; + return 'Sorry, no devices with valid attribute readingsWatcher found !' if (!@devs); + + my ($dw,$rw,$tw,$sw,$aw) = (6,7,7,5,3); # Startbreiten, bzw. Mindestbreite durch Überschrift + + foreach (@devs) { + my ($d,$r,$t,$s,$g) = split(',', $_); + # die tatsächlichen Breiten aus den vorhandenen Werten ermitteln + $dw = length($d) if (length($d) > $dw); + $rw = length($r) if (length($r) > $rw); + $tw = length($t) if (length($t) > $tw); + $sw = length($s) if (length($s) > $sw); + $aw = length($g) if (length($g) > $aw); + } + + my $head = 'Device ' .(' ' x ($dw-6)) + .'| Reading '.(' ' x ($rw-7)).'| ' + .(' ' x ($tw-7)).'Timeout | ' + .(' ' x ($sw-5)).'State | ' + .(' ' x ($aw-3)).'Age'; + + my $separator = ('-' x length($head)); + + while ( $head =~ m/\|/g ) { # alle | Positionen durch + ersetzen + substr $separator, (pos($head)-1), 1, '+'; + } + + $head .= "\n".$separator."\n"; + + my $s; + foreach (@devs) { + my ($d,$r,$t,$e,$g) = split(',', $_); + + $s .= $d . (' ' x ($dw - length($d))).' '; # left-align Device + $s .= '| '. $r . (' ' x ($rw - length($r))).' '; # left-align Reading + $s .= '| ' . (' ' x ($tw - length($t))).$t.' '; # Timeout right-align + $s .= '| ' . (' ' x ($sw - length($e))).$e.' '; # State right-align + $s .= '| ' . (' ' x ($aw - length($g))).$g; # Age right-align + $s .= "\n"; + } + + return $head.$s.$separator; +} + +##################################################################################### + +sub Attr { + + my ($cmd, $name, $attrName, $attrVal) = @_; + + return 'attribute not allowed for self !' if ($attrName eq 'readingsWatcher'); + + my $hash = $defs{$name}; + + if ($cmd eq 'set') + { + if ($attrName eq 'disable') { + readingsSingleUpdate($hash, 'state', 'disabled', 1) if (int($attrVal) == 1); + OnTimer($hash) if (int($attrVal) == 0); + return; + } + + if (($attrName eq 'readingActivity') && ($attrVal eq 'state')) { + my $error = 'forbidden value state !'; + Log3 $hash,1,"$name, readingActivity $error"; + return $error; + } + } + + if (($cmd eq 'del') && ($attrName eq 'disable')) { + OnTimer($hash); + } + + return; +} + +##################################################################################### + +sub OnTimer { + + my $hash = shift; + my $name = $hash->{NAME}; + my $interval = AttrNum($name, 'interval', 0); + + $hash->{INTERVAL} = $interval; + RemoveInternalTimer($hash); + + return if (!$interval); + + InternalTimer(gettimeofday() + $interval, 'FHEM::readingsWatcher::OnTimer', $hash, 0); + + readingsSingleUpdate($hash, 'state', 'disabled', 0) if (IsDisabled($name)); + return if ( IsDisabled($name) || !$init_done ); + + my ($readingsList , @devices); + my ($alive_count , $readings_count) = (0, 0); + my @toDevs = (); + my @deadDevs = (); + my @skipDevs = (); + my ($readingActifity,$dead,$alive) = split(':', AttrVal($name, 'readingActivity', 'none:dead:alive')); + + $dead //= 'dead'; # if (!defined($dead)); + $alive //= 'alive'; # if (!defined($alive)); + $readingActifity = '' if ($readingActifity eq 'none'); + + foreach (keys %{$defs{$name}{READINGS}}) { # alle eigenen Readings + $readingsList .= $_ .',' if ( $_ =~ /_/ ); # nur die Readings mit _ im Namen (Device_Reading) + } + + readingsBeginUpdate($hash); + + foreach my $device (devspec2array('readingsWatcher!=')) { + + my $or_and = 0; # Readings als OR auswerten + my ($d_a, $d_d) = (0,0); # Device_alives , Device_deads + my $timeOutState = ''; + + my $rSA = ($device eq $name) ? '' : AttrVal($device, 'readingsWatcher', ''); + + next if (!$rSA || IsDisabled($device) || IsIgnored($device)); + + push @devices, $device; # if !grep {/$device/} @devices; keine doppelten Namen + + $or_and = 1 if (index($rSA,'+') != -1); # Readings als AND auswerten + $rSA =~ s/\+/,/g ; # eventuell vorhandene + auch in Komma wandeln + + # rSA: timeout, errorValue, reading1, reading2, reading3, ... + # 120,---,temperature,humidity,battery + # or 900,,current,eState / no errorValue = do not change reading + + my $ok_device = 0; + + foreach (split(';', $rSA)) { + + my ($timeout, $errorValue, @readings_ar) = split(',', $_); + + if (@readings_ar) { + $ok_device = 1; + $timeout = int($timeout); + } + + foreach my $reading (@readings_ar) { # alle zu überwachenden Readings + + $reading =~ s/ //g; + my $state = 0; + + if ($reading eq 'STATE') { # Sonderfall STATE + + $reading = 'state'; + $state = 1; + } + + my $age = ReadingsAge($device, $reading, undef); + + if (defined($age)) { + + $readings_count++; + + if (($age > $timeout) && ($timeout > 0)) { + + push @toDevs, $device; # if (!grep {/$device/} @toDevs); + $timeOutState = 'timeout'; + $d_d++; # Device Tote + my $rts = ReadingsTimestamp($device, $reading, 0); + setReadingsVal($defs{$device}, $reading, $errorValue, $rts) if ($rts && ($errorValue ne '')); # leise setzen ohne Event + $defs{$device}->{STATE} = $errorValue if ($state && ($errorValue ne '')); + } + else { + $d_a++; # Device Lebende + $timeOutState = 'ok'; + } + + my $d_r = $device.'_'.$reading; + + readingsBulkUpdate($hash, $d_r, $timeOutState) if ($timeout > 0); + $readingsList =~ s/$d_r,// if ($readingsList); # das Reading aus der Liste streichen, leer solange noch kein Device das Attr hat ! + + Log3 $hash,2,"name, invalid timeout value $timeout for reading $device $reading" if ($timeout < 1); + } + else { + setReadingsVal($defs{$device},$reading,'unknown',TimeNow()) if ($errorValue); # leise setzen ohne Event + $defs{$device}->{STATE} = 'unknown' if ($errorValue && $state); + Log3 $hash,3,"$name, reading Timestamp for $reading not found on device $device"; + readingsBulkUpdate($hash, $device.'_'.$reading, 'no Timestamp'); + } + } + } # Anzahl Readings Sätze im Device + + if ($ok_device && $timeOutState) { + my $error; + + if ((!$or_and && $d_d) || ($or_and && !$d_a)) { # tot bei OR und mindestens einem Toten || AND aber kein noch Lebender + $error = CommandSetReading(undef, "$device $readingActifity $dead") if ($readingActifity); + push @deadDevs, $device; # dead devices + } + else { # wenn es nicht tot ist müsste es eigentlich noch leben .... + $error = CommandSetReading(undef, "$device $readingActifity $alive") if ($readingActifity); + $alive_count ++; # alive devices + } + Log3 $hash,2,"$name, $error" if ($error); + } + else { + Log3 $hash,2,"$name, insufficient parameters for device $device - skipped !"; + CommandSetReading(undef, "$device $readingActifity unknown") if ($readingActifity); + push @skipDevs, $device; + } + } # foreach device + + # eventuell doppelte Einträge aus allen vier Listen entfernen + @devices = List::Util::uniq @devices if (@devices); + @toDevs = List::Util::uniq @toDevs if (@toDevs); + @skipDevs = List::Util::uniq @skipDevs if (@skipDevs); + @deadDevs = List::Util::uniq @deadDevs if (@deadDevs); + + readingsBulkUpdate($hash, 'readings' , $readings_count); + readingsBulkUpdate($hash, 'devices' , int(@devices)); + readingsBulkUpdate($hash, 'alive' , $alive_count); + readingsBulkUpdate($hash, 'dead' , int(@deadDevs)); + readingsBulkUpdate($hash, 'skipped' , int(@skipDevs)); + readingsBulkUpdate($hash, 'timeouts' , int(@toDevs)); + readingsBulkUpdate($hash, 'state' , (@toDevs) ? 'timeout' : 'ok'); + + # jetzt nicht aktualisierte Readings markieren oder gleich ganz löschen + # Vorwahl via Attribut deleteUnusedReadings + clearReadings($name,$readingsList) if ($readingsList); + + (@devices) ? readingsBulkUpdate($hash, '.associatedWith' , join(',', @devices)) : readingsDelete($hash, '.associatedWith'); + (@toDevs) ? readingsBulkUpdate($hash, 'timeoutDevs', join(',', @toDevs)) : readingsBulkUpdate($hash, 'toDevs', 'none'); + (@deadDevs) ? readingsBulkUpdate($hash, 'deadDevs', join(',', @deadDevs)) : readingsBulkUpdate($hash, 'deadDevs', 'none'); + (@skipDevs) ? readingsBulkUpdate($hash, 'skippedDevs', join(',', @skipDevs)) : readingsBulkUpdate($hash, 'skippedDevs','none'); + + readingsEndUpdate($hash, 1); + + return; +} + +##################################################################################### + +sub clearReadings { + + my $name = shift; + my $hash = $defs{$name}; + + foreach my $reading (split(',', shift)) # Liste der aktiven Readings + { + next if (!$reading); + + if (AttrNum($name, 'deleteUnusedReadings', 1)) + { + readingsDelete($hash, $reading); + Log3 $hash, 3, "$name, delete unused reading $reading"; + } + else + { + readingsBulkUpdate($hash, $reading, 'unused'); + Log3 $hash, 4, "$name, unused reading $reading"; + } + } + + return; +} + +##################################################################################### + 1; =pod @@ -724,7 +738,7 @@ sub Attr "supervision", "überwachung" ], - "version": "2.0.0", + "version": "2.1.0", "release_status": "stable", "author": [ "Wzut" @@ -733,15 +747,14 @@ sub Attr "Wzut" ], "x_fhem_maintainer_github": [ - "Wzut" ], "prereqs": { "runtime": { "requires": { "FHEM": 5.00918799, - "perl": 5.014, "GPUtils": 0, - "Time::HiRes": 0 + "Time::HiRes": 0, + "List::Util": 0 }, "recommends": { "FHEM::Meta": 0