# $Id$ #################################################################################################### # # 98_HourCounter.pm # The HourCounter accumulates single events to a counter object. # In the case of binary weighted events pulse- and pause-time are determined # # This module is written by john. # # This file is part of fhem. # # Fhem 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. # # Fhem 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. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # # # 16.11.13 - 0.99.b # Loglevel adjusted # 02.12.13 - 0.99.c # $readingFnAttributes added # 03.12.13 - 0.99.d # missed attribute event-on-change-reading # 02.02.14 - 1.00 # command queues # 04.02.14 - 1.01 # queue removed # 17.03.14 - 1.02 # adjusting log-levels, forceYearChange,HourCounter_RoundYear # 07.06.14 - 1.03 # $ID changed # setter for pulseTimeIncrement, pauseTimeIncrement # 25.10.14 - 1.0.0.4 # official part of fhem # adjusting log-output # update documentation # 14.11.14 - 1.0.0.5 # minor fixes for logging in HourCounter_Set: thanks kubuntufan # reformating # 17.11.14 - 1.0.0.6 # cyclic calculation of pulse/pause-duration # correctly restores counter values after restart # 10.12.14 - 1.0.1.0 # new readings pulseTimeEdge, pauseTimeEdge hold the last pusle*-Increment value # all operative readings beside the tick*-readings are updated every cycle now # new reading tickChanged is fired, if the value is changed # new reading tickUpdated is fired each time the operative readings are updated # some bug fixes concerning duration and calc calculations # note, that also 99_UtilsHourCounter needs changes # 21.12.14 - 1.0.1.1 # bug: if OFF is not defined, nothing was counted # html : check with tidy # 24.12.14 - 1.0.1.2 # bug: if cvent occurs without value change, wrong calculcation of pulseTimeIncrement, pauseTimeIncrement #################################################################################################### package main; use strict; use warnings; use vars qw(%defs); use vars qw($readingFnAttributes); use vars qw(%attr); use vars qw(%modules); my $HourCounter_Version = "1.0.1.2 - 24.12.2014"; my @HourCounter_cmdQeue = (); my $DEBUG = 1; ########################## sub HourCounter_Log($$$) { my ( $hash, $loglevel, $text ) = @_; my $xline = ( caller(0) )[2]; my $xsubroutine = ( caller(1) )[3]; my $sub = ( split( ':', $xsubroutine ) )[2]; $sub =~ s/HourCounter_//; my $instName = ( ref($hash) eq "HASH" ) ? $hash->{NAME} : "HourCounter"; Log3 $hash, $loglevel, "HourCounter $instName $sub.$xline " . $text; } ########################## sub HourCounter_AddLog($$$) { my ( $logdevice, $readingName, $value ) = @_; my $cmd = ''; if ( $readingName =~ m,state,i ) { $cmd = "trigger $logdevice $value << addLog"; } else { $cmd = "trigger $logdevice $readingName: $value << addLog"; } HourCounter_Log '', 3, $cmd; fhem($cmd); } ########################## # execute the content of the given parameter sub HourCounter_Exec($) { my $doit = shift; my $ret = ''; eval $doit; $ret = $@ if ($@); return $ret; } ########################## # add command to queue sub HourCounter_cmdQueueAdd($$) { my ( $hash, $cmd ) = @_; push( @{ $hash->{helper}{cmdQueue} }, $cmd ); } ########################## # execute command queue sub HourCounter_ExecQueue($) { my ($hash) = @_; my $result; my $cnt = $#{ $hash->{helper}{cmdQueue} }; my $loops = 0; my $cntAll = 0; HourCounter_Log $hash, 4, "cnt: $cnt"; while ( $cnt >= 0 ) { for my $i ( 0 .. $cnt ) { my $cmd = ${ $hash->{helper}{cmdQueue} }[$i]; ${ $hash->{helper}{cmdQueue} }[$i] = ''; $result = HourCounter_Exec($cmd); if ($result) { HourCounter_Log $hash, 2, "$result"; } else { HourCounter_Log $hash, 4, "exec ok:$cmd"; } $cntAll++; } # bearbeitete eintraege loeschen for ( my $i = $cnt ; $i > -1 ; $i-- ) { splice( @{ $hash->{helper}{cmdQueue} }, $i, 1 ); } $cnt = $#HourCounter_cmdQeue; $loops++; if ( $loops >= 5 || $cntAll > 100 ) { HourCounter_Log $hash, 2, "!!! too deep recursion"; last; } } } ########################## # round off the date passed to the hour sub HourCounter_RoundHour($) { my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime(shift); return mktime( 0, 0, $hour, $mday, $mon, $year ); } ########################## # round off the date passed to the day sub HourCounter_RoundDay($) { my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime(shift); return mktime( 0, 0, 0, $mday, $mon, $year ); } ########################## # round off the date passed to the week sub HourCounter_RoundWeek($) { my ($time) = @_; my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime($time); # wday 0 Sonntag 1 Montag ... $time -= $wday * 86400; return HourCounter_RoundDay($time); } ########################## # returns the seconds since the start of the day sub HourCounter_SecondsOfDay() { my $timeToday = gettimeofday(); return int( $timeToday - HourCounter_RoundDay($timeToday) ); } ########################## # round off the date passed to the month sub HourCounter_RoundMonth($) { my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime(shift); return mktime( 0, 0, 0, 1, $mon, $year ); } ########################## # round off the date passed to the year sub HourCounter_RoundYear($) { my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime(shift); return mktime( 0, 0, 0, 1, 1, $year ); } ########################## sub HourCounter_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "HourCounter_Define"; $hash->{UndefFn} = "HourCounter_Undef"; $hash->{SetFn} = "HourCounter_Set"; $hash->{GetFn} = "HourCounter_Get"; $hash->{NotifyFn} = "HourCounter_Notify"; $hash->{AttrFn} = "HourCounter_Attr"; $hash->{AttrList} = "disable:0,1 interval:1,2,3,4,5,10,15,20,30,60 " . $readingFnAttributes; HourCounter_Log "", 3, "Init Done with Version $HourCounter_Version"; } ########################## sub HourCounter_Define($$$) { my ( $hash, $def ) = @_; my @a = split( "[ \t][ \t]*", $def ); my $name = $a[0]; HourCounter_Log $hash, ($DEBUG) ? 0 : 4, "parameters: @a"; if ( @a < 3 ) { return "wrong syntax: define HourCounter []"; } my $onRegexp = $a[2]; my $offRegexp = ( @a == 4 ) ? $a[3] : undef; # Checking for misleading regexps eval { "Hallo" =~ m/^$onRegexp/ }; return "Bad regexp_for_ON : $@" if ($@); if ($offRegexp) { eval { "Hallo" =~ m/^$offRegexp/ }; return "Bad regexp_for_ON : $@" if ($@); } # # some inits $hash->{VERSION} = $HourCounter_Version; $hash->{helper}{ON_Regexp} = $onRegexp; $hash->{helper}{OFF_Regexp} = $offRegexp; $hash->{helper}{isFirstRun} = 1; $hash->{helper}{value} = -1; $hash->{helper}{forceHourChange} = ''; $hash->{helper}{forceDayChange} = ''; $hash->{helper}{forceWeekChange} = ''; $hash->{helper}{forceMonthChange} = ''; $hash->{helper}{forceYearChange} = ''; $hash->{helper}{forceClear} = ''; $hash->{helper}{calledByEvent} = ''; $hash->{helper}{changedTimestamp} = ''; @{ $hash->{helper}{cmdQueue} } = (); $modules{HourCounter}{defptr}{$name} = $hash; RemoveInternalTimer($name); # wait until alle readings have been restored InternalTimer( int( gettimeofday() + 15 ), "HourCounter_Run", $name, 0 ); return undef; } ########################## sub HourCounter_Undef($$) { my ( $hash, $arg ) = @_; HourCounter_Log $hash, 3, "Done"; return undef; } ########################### sub HourCounter_Get($@) { my ( $hash, @a ) = @_; my $name = $hash->{NAME}; my $ret = "Unknown argument $a[1], choose one of version:noArg"; my $cmd = lc( $a[1] ); if ( $cmd eq 'version' ) { $ret = "Version : $HourCounter_Version\n"; } return $ret; } ########################### sub HourCounter_Set($@) { my ( $hash, @a ) = @_; my $name = $hash->{NAME}; my $reINT = '^([\\+,\\-]?\\d+$)'; # int # determine userReadings beginning with app my @readingNames = keys( %{ $hash->{READINGS} } ); my @userReadings = (); foreach (@readingNames) { if ( $_ =~ m/app.*/ ) { push( @userReadings, $_ ); } } my $strUserReadings = join( " ", @userReadings ) . " "; # standard commands with parameter my @cmdPara = ( "countsOverall", "countsPerDay", "pauseTimeIncrement", "pauseTimePerDay", "pauseTimeOverall", "pulseTimeIncrement", "pulseTimePerDay", "pulseTimeOverall" ); # standard commands with no parameter my @cmdNoPara = ( "clear", "forceHourChange", "forceDayChange", "forceWeekChange", "forceMonthChange", "forceYearChange", "calc" ); my @allCommands = ( @cmdPara, @cmdNoPara, @userReadings ); my $strAllCommands = join( " ", ( @cmdPara, @userReadings ) ) . " " . join( ":noArg ", @cmdNoPara ) . ":noArg "; #HourCounter_Log $hash, 2, "strAllCommands : $strAllCommands"; # stop:noArg my $usage = "Unknown argument $a[1], choose one of " . $strAllCommands; # we need at least 2 parameters return "Need a parameter for set" if ( @a < 2 ); my $cmd = $a[1]; if ( $cmd eq "?" ) { return $usage; } my $value = $a[2]; # is command defined ? if ( ( grep { /$cmd/ } @allCommands ) <= 0 ) { HourCounter_Log $hash, 2, "cmd:$cmd no match for : @allCommands"; return return "unknown command : $cmd"; } # need we a parameter ? my $hits = scalar grep { /$cmd/ } @cmdNoPara; my $needPara = ( $hits > 0 ) ? '' : 1; HourCounter_Log $hash, 4, "hits: $hits needPara:$needPara"; # if parameter needed, it must be an integer return "Value must be an integer" if ( $needPara && !( $value =~ m/$reINT/ ) ); my $info = "command : " . $cmd; $info .= " " . $value if ($needPara); HourCounter_Log $hash, 4, $info; my $doRun = ''; if ($needPara) { readingsSingleUpdate( $hash, $cmd, $value, 1 ); } elsif ( $cmd eq "forceHourChange" ) { $hash->{helper}{forceHourChange} = 1; $doRun = 1; } elsif ( $cmd eq "forceDayChange" ) { $hash->{helper}{forceDayChange} = 1; $doRun = 1; } elsif ( $cmd eq "forceWeekChange" ) { $hash->{helper}{forceWeekChange} = 1; $doRun = 1; } elsif ( $cmd eq "forceMonthChange" ) { $hash->{helper}{forceMonthChange} = 1; $doRun = 1; } elsif ( $cmd eq "forceYearChange" ) { $hash->{helper}{forceYearChange} = 1; $doRun = 1; } elsif ( $cmd eq "clear" ) { $hash->{helper}{forceClear} = 1; $doRun = 1; } elsif ( $cmd eq "calc" ) { $doRun = 1; } else { return "unknown command (2): $cmd"; } # perform run if ( $doRun && !$hash->{helper}{isFirstRun} ) { $hash->{helper}{value} = -1; $hash->{helper}{calledByEvent} = 1; HourCounter_Run( $hash->{NAME} ); } return; } ########################## sub HourCounter_Notify($$) { my ( $hash, $dev ) = @_; my $name = $hash->{NAME}; my $devName = $dev->{NAME}; # return if disabled if ( AttrVal( $name, 'disable', '0' ) eq '1' ) { return ""; } my $onRegexp = $hash->{helper}{ON_Regexp}; my $offRegexp = $hash->{helper}{OFF_Regexp}; my $max = int( @{ $dev->{CHANGED} } ); for ( my $i = 0 ; $i < $max ; $i++ ) { my $s = $dev->{CHANGED}[$i]; # read changed reading $s = "" if ( !defined($s) ); my $isOnReading = ( "$devName:$s" =~ m/^$onRegexp$/ ); my $isOffReading = ($offRegexp) ? ( "$devName:$s" =~ m/^$offRegexp$/ ) : ''; # HourCounter_Log $hash, 5, "devName:$devName; CHANGED:$s; isOnReading:$isOnReading; isOffReading:$isOffReading;"; next if ( !( $isOnReading || ( $isOffReading && $offRegexp ) ) ); $hash->{helper}{value} = 1 if ($isOnReading); $hash->{helper}{value} = 0 if ($isOffReading); $hash->{helper}{calledByEvent} = 1; if ( !$hash->{helper}{isFirstRun} ) { HourCounter_Run( $hash->{NAME} ); } } } ########################## sub HourCounter_Attr($$$$) { my ( $command, $name, $attribute, $value ) = @_; my $msg = undef; my $hash = $defs{$name}; if ( $attribute eq "interval" ) { #HourCounter_Log $hash, 0, "cmd:$command name:$name attribute:$attribute"; if ( !$hash->{helper}{isFirstRun} ) { HourCounter_Run($name); } } return $msg; } ########################## # converts the seconds in the date format sub HourCounter_Seconds2HMS($) { my ($seconds) = @_; my ( $Sekunde, $Minute, $Stunde, $Monatstag, $Monat, $Jahr, $Wochentag, $Jahrestag, $Sommerzeit ) = localtime($seconds); my $days = int( $seconds / 86400 ); return sprintf( "%d Tage %02d:%02d:%02d", $days, $Stunde - 1, $Minute, $Sekunde ); } ########################## # rounds the timestamp do the beginning of the week sub HourCounter_weekBase($) { my ($time) = @_; my $dayDiff = 60 * 60 * 24; my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime($time); # wday 0 Sonntag 1 Montag ... my $a = $time - $wday * $dayDiff; my $b = int( $a / $dayDiff ); # auf tage gehen my $c = $b * $dayDiff; return $c; } ########################## # this either called by timer for cyclic update # or it is called by an event (on/off) sub HourCounter_Run($) { # print "xxx TAG A\n" ; my ($name) = @_; my $hash = $defs{$name}; # must be of type hourcounter return if ( !defined( $hash->{TYPE} ) || $hash->{TYPE} ne 'HourCounter' ); # timestamps for event-log-file-entries, older than current time delete( $hash->{CHANGETIME} ); # flag for called by event my $calledByEvent = $hash->{helper}{calledByEvent}; # reset flag $hash->{helper}{calledByEvent} = ''; # if call was made by timer, than force value to -1 my $valuePara = ($calledByEvent) ? $hash->{helper}{value} : -1; # initialize changedTimestamp, if it does not exist $hash->{helper}{changedTimestamp} = ReadingsTimestamp( $name, "value", TimeNow() ) if ( !$hash->{helper}{changedTimestamp} ); # serial date for changed timestamp my $sdValue = time_str2num( $hash->{helper}{changedTimestamp} ); my $sdCurTime = gettimeofday(); my $isOffDefined = ( $hash->{helper}{OFF_Regexp} ) ? 1 : ''; # calc time diff my $timeIncrement = int( $sdCurTime - $sdValue ); # wrong time offset in case of summer/winter time $timeIncrement = 0 if ( $timeIncrement < 0 ); # get the old value my $valueOld = ReadingsVal( $name, 'value', 0 ); # variable for reading update my $value = undef; my $countsPerDay = ReadingsVal( $name, "countsPerDay", 0 ); my $countsOverall = ReadingsVal( $name, "countsOverall", 0 ); my $pulseTimeIncrement = ReadingsVal( $name, "pulseTimeIncrement", 0 ); my $pulseTimePerDay = ReadingsVal( $name, "pulseTimePerDay", 0 ); my $pulseTimeOverall = ReadingsVal( $name, "pulseTimeOverall", 0 ); my $pulseTimeEdge = ReadingsVal( $name, "pulseTimeEdge", 0 ); my $pauseTimeIncrement = ReadingsVal( $name, "pauseTimeIncrement", 0 ); my $pauseTimePerDay = ReadingsVal( $name, "pauseTimePerDay", 0 ); my $pauseTimeOverall = ReadingsVal( $name, "pauseTimeOverall", 0 ); my $pauseTimeEdge = ReadingsVal( $name, "pauseTimeEdge", 0 ); my $tickUpdated = ReadingsVal( $name, "tickUpdated", 0 ) + 1; $tickUpdated = 1 if ( $tickUpdated >= 1000 ); my $tickChanged = ReadingsVal( $name, "tickChanged", 0 ); my $tickHour = ReadingsVal( $name, "tickHour", 0 ); my $tickDay = ReadingsVal( $name, "tickDay", 0 ); my $tickWeek = ReadingsVal( $name, "tickWeek", 0 ); my $tickMonth = ReadingsVal( $name, "tickMonth", 0 ); my $tickYear = ReadingsVal( $name, "tickYear", 0 ); my $state = ''; my $sdTickHour = time_str2num( ReadingsTimestamp( $name, "tickHour", TimeNow() ) ); # serial date for current hour my $sdRoundHour = HourCounter_RoundHour($sdCurTime); my $sdRoundHourLast = HourCounter_RoundHour($sdTickHour); $sdRoundHourLast = $sdRoundHour if ( !$sdRoundHourLast ); my $isHourChanged = ( $sdRoundHour != $sdRoundHourLast ) || $hash->{helper}{forceHourChange}; # serial date for current day my $sdRoundDayCurTime = HourCounter_RoundDay($sdCurTime); my $sdRoundDayValue = HourCounter_RoundDay($sdRoundHourLast); my $isDayChanged = ( $sdRoundDayCurTime != $sdRoundDayValue ) || $hash->{helper}{forceDayChange}; # serial date for current week my $sdRoundWeekCurTime = HourCounter_RoundWeek($sdCurTime); my $sdRoundWeekValue = HourCounter_RoundWeek($sdRoundHourLast); my $isWeekChanged = ( $sdRoundWeekCurTime != $sdRoundWeekValue ) || $hash->{helper}{forceWeekChange}; # serial date for current month my $sdRoundMonthCurTime = HourCounter_RoundMonth($sdCurTime); my $sdRoundMonthValue = HourCounter_RoundMonth($sdRoundHourLast); my $isMonthChanged = ( $sdRoundMonthCurTime != $sdRoundMonthValue ) || $hash->{helper}{forceMonthChange}; # serial date for current year my $sdRoundYearCurTime = HourCounter_RoundYear($sdCurTime); my $sdRoundYearValue = HourCounter_RoundYear($sdRoundHourLast); my $isYearChanged = ( $sdRoundYearCurTime != $sdRoundYearValue ) || $hash->{helper}{forceYearChange}; # loop forever while (1) { # stop if disabled last if ( AttrVal( $name, 'disable', '0' ) eq '1' ); # variables for controlling HourCounter_Log $hash, 5, "value:$valuePara changedTimestamp:" . $hash->{helper}{changedTimestamp}; # ------------ basic init, when first run if ( $hash->{helper}{isFirstRun} ) { $hash->{helper}{isFirstRun} = undef; $hash->{helper}{sdRoundHourLast} = $sdRoundHourLast; # first init after startup readingsBeginUpdate($hash); readingsBulkUpdate( $hash, 'tickHour', 0 ); readingsBulkUpdate( $hash, 'tickDay', 0 ); readingsBulkUpdate( $hash, 'tickWeek', 0 ); readingsBulkUpdate( $hash, 'tickMonth', 0 ); readingsBulkUpdate( $hash, 'tickYear', 0 ); readingsEndUpdate( $hash, 0 ); # set initial values $value = $valueOld; # value als reading anlegen falls nicht vorhanden $timeIncrement = 0; HourCounter_Log $hash, 0, "first run done countsOverall:" . $countsOverall; #4 } # -------- force clear request if ( $hash->{helper}{forceClear} ) { HourCounter_Log $hash, 0, "force clear request"; readingsSingleUpdate( $hash, 'clearDate', TimeNow(), 1 ); # reset all counters $countsOverall = 0; $countsPerDay = 0; $pauseTimeIncrement = 0; $pauseTimeEdge = 0; $pauseTimeOverall = 0; $pauseTimePerDay = 0; $pulseTimeIncrement = 0; $pulseTimeEdge = 0; $pulseTimeOverall = 0; $pulseTimePerDay = 0; $hash->{helper}{forceClear} = ''; $timeIncrement = 0; } # -------------- handling of transitions my $hasValueChanged = 0; if ( ( $isOffDefined && $valuePara >= 0 && $valuePara != $valueOld ) || ( !$isOffDefined && $calledByEvent ) ) { $hasValueChanged = 1; } if ($hasValueChanged) { $value = $valuePara; $valueOld = $valuePara; # -------------- positive edge if ( $valuePara == 1 ) { # handling of counters $countsPerDay += 1; $countsOverall += 1; # handling of pause time if ($isOffDefined) { # calc the rest of puse-time until edge $pauseTimeIncrement += $timeIncrement; $pauseTimePerDay += $timeIncrement; $pauseTimeOverall += $timeIncrement; $pulseTimeIncrement = 0; $pauseTimeEdge = $pauseTimeIncrement; } HourCounter_Log $hash, 4, "rising edge; pauseTimeIncr:$pauseTimeIncrement countPerDay:$countsPerDay"; } # ------------ negative edge elsif ( $valuePara == 0 ) { # handlich of pulse time if ($isOffDefined) { $pulseTimeIncrement += $timeIncrement; $pulseTimePerDay += $timeIncrement; $pulseTimeOverall += $timeIncrement; $pauseTimeIncrement = 0; $pulseTimeEdge = $pulseTimeIncrement; } HourCounter_Log $hash, 4, "falling edge pulseTimeIncrement:$pulseTimeIncrement"; } } # ------------ no value change # it is possible to receive an event without change of value (e.g. Max Shutter does it hourly) elsif ($isOffDefined) { if ( $valueOld == 0 ) { $pauseTimeIncrement += $timeIncrement; $pauseTimePerDay += $timeIncrement; $pauseTimeOverall += $timeIncrement; } elsif ( $valueOld == 1 ) { $pulseTimeIncrement += $timeIncrement; $pulseTimePerDay += $timeIncrement; $pulseTimeOverall += $timeIncrement; } } $hash->{helper}{changedTimestamp} = TimeNow(); $value = $valueOld; $state = $countsPerDay; # ---------update readings, if vars defined readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "countsPerDay", $countsPerDay ); readingsBulkUpdate( $hash, "countsOverall", $countsOverall ); if ($isOffDefined) { readingsBulkUpdate( $hash, "pulseTimeIncrement", $pulseTimeIncrement ); readingsBulkUpdate( $hash, "pulseTimeEdge", $pulseTimeEdge ); readingsBulkUpdate( $hash, "pulseTimePerDay", $pulseTimePerDay ); readingsBulkUpdate( $hash, "pulseTimeOverall", $pulseTimeOverall ); readingsBulkUpdate( $hash, "pauseTimeIncrement", $pauseTimeIncrement ); readingsBulkUpdate( $hash, "pauseTimeEdge", $pauseTimeEdge ); readingsBulkUpdate( $hash, "pauseTimePerDay", $pauseTimePerDay ); readingsBulkUpdate( $hash, "pauseTimeOverall", $pauseTimeOverall ); } readingsBulkUpdate( $hash, "value", $value ); readingsBulkUpdate( $hash, 'state', $state ); readingsBulkUpdate( $hash, 'tickUpdated', $tickUpdated ); readingsEndUpdate( $hash, 1 ); # --------------- fire time interval ticks for hour,day,month if ($hasValueChanged) { $tickChanged++; $tickChanged = 1 if ( $tickChanged >= 1000 ); readingsSingleUpdate( $hash, 'tickChanged', $tickChanged, 1 ); HourCounter_Log $hash, 4, 'tickChanged fired '; } if ($isHourChanged) { $tickHour++; $tickHour = 1 if ( $tickHour >= 1000 ); $hash->{helper}{forceHourChange} = ''; $hash->{helper}{sdRoundHourLast} = $sdRoundHour; readingsSingleUpdate( $hash, 'tickHour', $tickHour, 1 ); HourCounter_Log $hash, 4, "tickHour fired"; } if ($isDayChanged) { $tickDay++; $tickDay = 1 if ( $tickDay >= 1000 ); $hash->{helper}{forceDayChange} = ''; readingsSingleUpdate( $hash, 'tickDay', $tickDay, 1 ); HourCounter_Log $hash, 4, "tickDay fired"; } if ($isWeekChanged) { $tickWeek++; $tickWeek = 1 if ( $tickWeek >= 1000 ); $hash->{helper}{forceWeekChange} = ''; readingsSingleUpdate( $hash, 'tickWeek', $tickWeek, 1 ); HourCounter_Log $hash, 4, "tickWeek fired"; } if ($isMonthChanged) { $tickMonth++; $tickMonth = 1 if ( $tickMonth >= 1000 ); $hash->{helper}{forceMonthChange} = ''; readingsSingleUpdate( $hash, 'tickMonth', $tickMonth, 1 ); HourCounter_Log $hash, 4, "tickMonth fired"; } if ($isYearChanged) { $tickYear++; $tickYear = 1 if ( $tickYear >= 1000 ); $hash->{helper}{forceYearChange} = ''; readingsSingleUpdate( $hash, 'tickYear', $tickYear, 1 ); HourCounter_Log $hash, 4, "tickYear fired"; } # execute command queue HourCounter_ExecQueue($hash); # day change, so reset day readings if ($isDayChanged) { ### reset all day counters readingsBeginUpdate($hash); readingsBulkUpdate( $hash, "countsPerDay", 0 ); readingsBulkUpdate( $hash, "pulseTimePerDay", 0 ); readingsBulkUpdate( $hash, "pauseTimePerDay", 0 ); readingsEndUpdate( $hash, 1 ); HourCounter_Log $hash, 4, "reset day counters"; } last; } # ------------ calculate seconds until next hour starts my $interval = AttrVal( $name, 'interval', '60' ); my $actTime = int( gettimeofday() ); my ( $sec, $min, $hour ) = localtime($actTime); # round to next interval my $seconds = $interval * 60; my $nextHourTime = int( ( $actTime + $seconds ) / $seconds ) * $seconds; # calc diff in seconds my $nextCall = $nextHourTime - $actTime; HourCounter_Log $hash, 5, "nextCall:$nextCall changedTimestamp:" . $hash->{helper}{changedTimestamp}; RemoveInternalTimer($name); InternalTimer( gettimeofday() + $nextCall, "HourCounter_Run", $hash->{NAME}, 0 ); return undef; } 1; =pod =begin html

HourCounter

Define

define <name> HourCounter <pattern_for_ON> [<pattern_for_OFF>]

Hourcounter can detect both the activiy-time and the inactivity-time of a property.
The "pattern_for_ON" identifies the events, that signal the activity of the desired property.
The "pattern_for_OFF" identifies the events, that signal the inactivity of the desired property.

If "pattern_for_OFF" is not defined, any matching event of "patter_for_ON" will be counted.
Otherwise only the rising edges of "pattern_for_ON" will be counted.
This means a "pattern_for_OFF"-event must be detected before a "pattern_for_ON"-event is accepted.

"pattern_for_ON" and "pattern_for_OFF" must be formed using the following structure:

device:[regexp]

The forming-rules are the same as for the notify-command.

Example:

define BurnerCounter HourCounter SHUTTER_TEST:on SHUTTER_TEST:off

Set-Commands

set <name> calc

starts the calculation of pulse/pause-time.


set <name> clear

clears the readings countsPerDay, countsOverall,pauseTimeIncrement, pauseTimePerDay, pauseTimeOverall, pulseTimeIncrement, pulseTimePerDay, pulseTimeOverall by setting to 0.
The reading clearDate is set to the current Date/Time.


set <name> countsOverall <value>

Sets the reading countsOverall to the given value.This is the total-counter.


set <name> countsPerDay <value>

Sets the reading countsPerDay to the given value. This reading will automatically be set to 0, after change of day.


set <name> pauseTimeIncrement <value>

Sets the reading pauseTimeIncrement to the given value.
This reading in seconds is automatically set after a rising edge.


set <name> pauseTimeEdge <value>

Sets the reading pauseTimeEdge to the given value.
This reading in seconds is automatically set after a rising edge.


set <name> pauseTimeOverall <value>

Sets the reading pauseTimeOverall to the given value.
This reading in seconds is automatically adjusted after a change of pauseTimeIncrement.


set <name> pauseTimePerDay <value>

Sets the reading pauseTimePerDay to the given value.
This reading in seconds is automatically adjusted after a change of pauseTimeIncrement and set to 0 after change of day.


set <name> pulseTimeIncrement <value>

Sets the reading pulseTimeIncrement to the given value.
This reading in seconds is automatically set after a falling edge of the property.


set <name> pulseTimeEdge <value>

Sets the reading pulseTimeEdge to the given value.
This reading in seconds is automatically set after a rising edge.


set <name> pulseTimeOverall <value>

Sets the reading pulseTimeOverall to the given value.
This reading in seconds is automatically adjusted after a change of pulseTimeIncrement.


set <name> pulseTimePerDay <value>

Sets the reading pulseTimePerDay to the given value.
This reading in seconds is automatically adjusted after a change of pulseTimeIncrement and set to 0 after change of day.


set <name> forceHourChange

This modifies the reading tickHour, which is automatically modified after change of hour.


set <name> forceDayChange

This modifies the reading tickDay, which is automatically modified after change of day.


set <name> forceWeekChange

This modifies the reading tickWeek, which is automatically modified after change of week.


set <name> forceMonthChange

This modifies the reading tickMonth, which is automatically modified after change of month.


set <name> forceYearChange

This modifies the reading tickYear, which is automatically modified after change of year.


set <name> app.* <value>

Any reading with the leading term "app", can be modified.
This can be useful for user-readings.


Get-Commands

get <name> version

Get the current version of the module.


Attributes
  • interval
    the update interval for pulse/pause-time in minutes [default 60]

  • readingFnAttributes

Additional information
  • Discussion in FHEM forum

  • WIKI information in FHEM Wiki

  • The file 99_UtilsHourCounter.pm is a reference implementation for user defined extensions.
    It shows how to create sum values for hours,days, weeks, months and years.
    This file is located in the sub-folder contrib. For further information take a look to FHEM Wiki.

=end html =cut