################################################################ # $Id$ ################################################################ # # 98_readingsWatcher # # (c) 2015,2016 Copyright: HCS,Wzut # All rights reserved # # FHEM Forum : https://forum.fhem.de/index.php/topic,49408.0.html # # This code is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # The GNU General Public License can be found at # http://www.gnu.org/copyleft/gpl.html. # 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.3 => 20.04.20 remove all $_ , add attribut delimiter # 2.1.2 => 16.04.20 remove undef value for ReadingsAge # 2.1.1 => 14.04.20 remove List::Utils # 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); 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 GP_Export( qw(Initialize) ); } my $hasmeta = 0; # ältere Installationen haben noch kein Meta.pm if (-e $attr{global}{modpath}.'/FHEM/Meta.pm') { $hasmeta = 1; require FHEM::Meta; } my @stateDevs; my @toDevs; my @deadDevs; my @skipDevs; my @allDevs; 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 delimiter:-,--,_,__ ' .$readingFnAttributes; return FHEM::Meta::InitMod( __FILE__, $hash ) if ($hasmeta); return; } ##################################################################################### sub Define { my $hash = shift; my $def = shift; my ($name, $type, $noglobal) = split(m{ \s+ }xms, $def, 3); if (exists($modules{readingsWatcher}{defptr}) && ($modules{readingsWatcher}{defptr}->{NAME} ne $name)) { my $error = 'one readingsWatcher device is already defined !'; Log3($name, 1, $error); return $error; } if (defined($noglobal) && ($noglobal eq 'noglobal')) { $hash->{DEF} = 'noglobal'; } else { addToAttrList('readingsWatcher'); $hash->{DEF} = 'global'; # global -> userattr } $hash->{SVN} = (qw($Id$))[2]; CommandAttr(undef, "$name interval 60") unless (exists($attr{$name}{interval})); CommandAttr(undef, "$name readingActivity none") unless (exists($attr{$name}{readingActivity})); $modules{readingsWatcher}{defptr} = $hash; RemoveInternalTimer($hash); InternalTimer(gettimeofday()+5, 'FHEM::readingsWatcher::OnTimer', $hash, 0); Log3($name, 5, "$name, hasmeta $hasmeta"); 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') { # werden die meisten haben delFromAttrList('readingsWatcher'); # global -> userattr # wer hat alles ein Attribut readingsWatcher gesetzt ? foreach my $dev (devspec2array('readingsWatcher!=')) { delFromDevAttrList($dev, 'readingsWatcher'); # aufräumen } } 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') { my $delimiter = AttrVal($name,'delimiter','_'); foreach my $reading (keys %{$defs{$name}{READINGS}}) { # alle eigenen Readings if (index($reading, $delimiter) != -1) { # device_reading readingsDelete($hash, $reading); Log3($name,4,"$name, delete reading $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; @stateDevs = (); foreach my $device (devspec2array('readingsWatcher!=')) { my $rSA = ($device ne $name) ? AttrVal($device, 'readingsWatcher', '') : ''; next if ($rSA eq ''); if (IsDisabled($device)) { push @stateDevs, "$device,-,-,disabled,-"; } elsif (IsIgnored($device)) { push @stateDevs, "$device,-,-,ignored,-"; } else { # valid device push @stateDevs , IsValidDevice($device, $rSA); } } return formatStateList(); } ##################################################################################### sub IsValidDevice { my $device = shift; my @ar; foreach my $rs (split(';', shift)) { # Anzahl Regelsätze pro Device, meist nur einer $rs =~ s/\+/,/xg; # OR Readings wie normale Readingsliste behandeln $rs =~ s/ //g; my ($timeout,undef,@readings) = split(',', $rs); # der ggf. vorhandene Ersatzstring wird hier nicht benötigt return "$device,-,-,wrong parameters,-" if (!@readings); 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 ($age eq 'undef') { $state = 'unknown'; } else { $state = (int($age) > int($timeout)) ? 'timeout' : 'ok'; } } push @ar, "$device,$reading,$timeout,$state,$age"; } } return "$device,?,?,?,?" if (!@ar); return @ar; } ##################################################################################### sub formatStateList { # Device | Reading | Timeout | State | Age # -------+------------+---------+---------+-------- # CUL | credit10ms | 300 | ok | 56 # lamp | state | 900 | timeout | 3799924 # -------+------------+---------+---------+-------- return 'Sorry, no devices with valid attribute readingsWatcher found !' if (!@stateDevs); my ($dw,$rw,$tw,$sw,$aw) = (6,7,7,5,3); # Startbreiten, bzw. Mindestbreite durch Überschrift foreach my $dev (@stateDevs) { my ($d,$r,$t,$s,$g) = split(',', $dev); # die tatsächlichen Breiten aus den vorhandenen Werten ermitteln $dw = (length($d) > $dw) ? length($d) : $dw; $rw = (length($r) > $rw) ? length($r) : $rw; $tw = (length($t) > $tw) ? length($t) : $tw; $sw = (length($s) > $sw) ? length($s) : $sw; $aw = (length($g) > $aw) ? 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{\|}xg ) { # alle | Positionen durch + ersetzen substr $separator, (pos($head)-1), 1, '+'; } $head .= "\n".$separator."\n"; my $s; foreach my $dev (@stateDevs) { my ($d,$r,$t,$e,$g) = split(',', $dev); $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') { RemoveInternalTimer($hash); readingsSingleUpdate($hash, 'state', 'disabled', 1) if (int($attrVal) == 1); InternalTimer(gettimeofday() + 2, 'FHEM::readingsWatcher::OnTimer', $hash, 0) if (int($attrVal) == 0); return; } if (($attrName eq 'readingActivity') && (lc($attrVal) eq 'state')) { my $error = 'forbidden value state !'; Log3($name, 1, "$name, readingActivity $error"); return $error; } } if (($cmd eq 'del') && ($attrName eq 'disable')) { RemoveInternalTimer($hash); InternalTimer(gettimeofday() + 2, 'FHEM::readingsWatcher::OnTimer', $hash, 0); } 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 ); @toDevs = (); @deadDevs = (); @skipDevs = (); @allDevs = (); ($hash->{helper}{readingActivity},$hash->{helper}{dead},$hash->{helper}{alive}) = split(':', AttrVal($name, 'readingActivity', 'none:dead:alive')); $hash->{helper}{dead} //= 'dead'; # if (!defined($dead)); $hash->{helper}{alive} //= 'alive'; # if (!defined($alive)); $hash->{helper}{readingActivity} = '' if ($hash->{helper}{readingActivity} eq 'none'); $hash->{helper}{delimiter} = AttrVal($name,'delimiter','_'); # -, --, _, __ foreach my $reading (keys %{$defs{$name}{READINGS}}) { # alle eigenen Readings $hash->{helper}{readingsList} .= $reading .',' if (index($reading, $hash->{helper}{delimiter}) != -1); # nur die Readings mit _ im Namen (Device_Reading) } readingsBeginUpdate($hash); foreach my $device (devspec2array('readingsWatcher!=')) { $hash->{helper}{device} = $device; checkDevice($hash, $device); } # foreach device readingsBulkUpdate($hash, 'readings' , $hash->{helper}{readings_count}); readingsBulkUpdate($hash, 'devices' , int(@allDevs)); readingsBulkUpdate($hash, 'alive' , $hash->{helper}{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) if ($hash->{helper}{readingsList}); (@allDevs) ? readingsBulkUpdate($hash, '.associatedWith', join(',', @allDevs)) : readingsDelete($hash, '.associatedWith'); (@toDevs) ? readingsBulkUpdate($hash, 'timeoutDevs', join(',', @toDevs)) : readingsBulkUpdate($hash, 'timeoutDevs', 'none'); (@deadDevs) ? readingsBulkUpdate($hash, 'deadDevs', join(',', @deadDevs)) : readingsBulkUpdate($hash, 'deadDevs', 'none'); (@skipDevs) ? readingsBulkUpdate($hash, 'skippedDevs', join(',', @skipDevs)) : readingsBulkUpdate($hash, 'skippedDevs', 'none'); readingsEndUpdate($hash, 1); delete $hash->{helper}; return; } ##################################################################################### sub clearReadings { my $name = shift; my $hash = $defs{$name}; foreach my $reading (split(',', $hash->{helper}{readingsList})) # Liste der aktiven Readings { next if (!$reading); if (AttrNum($name, 'deleteUnusedReadings', 1)) { readingsDelete($hash, $reading); Log3($name, 3, "$name, delete unused reading $reading"); } else { readingsBulkUpdate($hash, $reading, 'unused'); Log3($name, 4, "$name, unused reading $reading"); } } return; } ##################################################################################### sub checkReadings { my $hash = shift; my $name = $hash->{NAME}; my $device = $hash->{helper}{device}; my $timeout = $hash->{helper}{timeout}; my $errorValue = $hash->{helper}{errorValue}; my @ar = split('\|' ,$hash->{helper}{readings_ar}); foreach my $reading (@ar) { # alle zu überwachenden Readings in einem Regelsatz $reading =~ s/ //g; my $state = 0; if ($reading eq 'STATE') { # Sonderfall STATE $reading = 'state'; $state = 1; } my $age = ReadingsAge($device, $reading, ''); my $d_r = $device.$hash->{helper}{delimiter}.$reading; if ($age ne '') { $hash->{helper}{readings_count} ++; if ($age > $timeout) { $hash->{helper}{timeOutState} = 'timeout'; $hash->{helper}{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 { $hash->{helper}{d_a} ++; # Device Lebende $hash->{helper}{timeOutState} = 'ok'; } readingsBulkUpdate($hash, $d_r, $hash->{helper}{timeOutState}) if ($hash->{helper}{timeOutState}); $hash->{helper}{readingsList} =~ s/$d_r,//xms if ($hash->{helper}{readingsList}); # das Reading aus der Liste streichen, leer solange noch kein Device das Attr hat ! } else { setReadingsVal($defs{$device},$reading,'unknown',TimeNow()) if ($errorValue); # leise setzen ohne Event $defs{$device}->{STATE} = 'unknown' if ($errorValue && $state); Log3($name, 3, "$name, reading Timestamp for $reading not found on device $device"); readingsBulkUpdate($hash, $d_r, 'no Timestamp'); } } # Readings in einem Regelsatz return; } sub checkDevice { my $hash = shift; my $name = $hash->{NAME}; my $device = shift; my $or_and = 0; # Readings als OR auswerten $hash->{helper}{timeOutState} = ''; $hash->{helper}{d_a} = 0; $hash->{helper}{d_d} = 0; my $rSA = ($device eq $name) ? '' : AttrVal($device, 'readingsWatcher', ''); return if (!$rSA || IsDisabled($device) || IsIgnored($device)); push @allDevs, $device; $or_and = 1 if (index($rSA,'+') != -1); # Readings als AND auswerten $rSA =~ s/\+/,/xg ; # 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 my $sets (split(';', $rSA)) { #Anzahl Regelsätze im Device my ($timeout, $errorValue, @readings_ar) = split(',', $sets); $hash->{helper}{readings_ar} = join('|', @readings_ar); $hash->{helper}{timeout} = int($timeout); $hash->{helper}{errorValue} = $errorValue; if (@readings_ar) { if ($timeout > 1) { $ok_device = 1; } else { Log3($name, 2, "$name, invalid timeout value $timeout for readings $device ".join(',',@readings_ar)); delete $hash->{helper}{readings_ar}; # das werten wir danach im foreach erst gar nicht mehr aus } checkReadings($hash); } } push @toDevs , $device if ($hash->{helper}{d_d}); if ($ok_device && $hash->{helper}{timeOutState}) { my $error; my $d_a = $hash->{helper}{d_a}; my $d_d = $hash->{helper}{d_d}; 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 $hash->{helper}{readingActivity} $hash->{helper}{dead}") if ($hash->{helper}{readingActivity}); push @deadDevs, $device; # dead devices } else { # wenn es nicht tot ist müsste es eigentlich noch leben .... $error = CommandSetReading(undef, "$device $hash->{helper}{readingActivity} $hash->{helper}{alive}") if ($hash->{helper}{readingActivity}); $hash->{helper}{alive_count} ++; # alive devices } Log3($name, 2, "$name, $error") if ($error); } else { Log3($name, 2, "$name, insufficient parameters for device $device - skipped !"); CommandSetReading(undef, "$device $hash->{helper}{readingActivity} unknown") if ($hash->{helper}{readingActivity}); push @skipDevs, $device; } return; } 1; __END__ =pod =over =encoding utf8 =item helper =item summary cyclical watching of readings updates =item summary_DE zyklische Überwachung von Readings auf Aktualisierung =begin html

readingsWatcher

=end html =begin html_DE

readingsWatcher

=end html_DE =for :application/json;q=META.json 98_readingsWatcher.pm { "abstract": "Module for cyclical watching of readings updates", "x_lang": { "de": { "abstract": "Modul zur zyklische Überwachung von Readings auf Aktualisierung" } }, "keywords": [ "readings", "watch", "supervision", "überwachung" ], "version": "2.1.3", "release_status": "stable", "author": [ "Wzut" ], "x_fhem_maintainer": [ "Wzut" ], "x_fhem_maintainer_github": [ ], "prereqs": { "runtime": { "requires": { "FHEM": 5.00918799, "GPUtils": 0, "Time::HiRes": 0 }, "recommends": { "FHEM::Meta": 0 }, "suggests": { } } } } =end :application/json;q=META.json =back =cut