# $Id: 98_HourCounter.pm 11307 2016-04-25 08:02:06Z rudolfkoenig $
####################################################################################################
#
#   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 <http://www.gnu.org/licenses/>.
#
#
#  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 <name> HourCounter <regexp_for_ON> [<regexp_for_OFF>]";
  }
  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

  <div>
    <a name="HourCounter" id="HourCounter"></a>

    <h3>HourCounter</h3>

    <div>
      <a name="HourCounterdefine" id="HourCounterdefine"></a> <b>Define</b>

      <div>
        <br />
        <code>define &lt;name&gt; HourCounter &lt;pattern_for_ON&gt; [&lt;pattern_for_OFF&gt;]</code><br />
        <br />
        Hourcounter can detect both the activiy-time and the inactivity-time of a property.<br />
        The "pattern_for_ON" identifies the events, that signal the activity of the desired property.<br />
        The "pattern_for_OFF" identifies the events, that signal the inactivity of the desired property.<br />
        <br />
        If "pattern_for_OFF" is not defined, any matching event of "patter_for_ON" will be counted.<br />
        Otherwise only the rising edges of "pattern_for_ON" will be counted.<br />
        This means a "pattern_for_OFF"-event must be detected before a "pattern_for_ON"-event is accepted.<br />
        <br />
        "pattern_for_ON" and "pattern_for_OFF" must be formed using the following structure:<br />
        <br />
        <code>device:[regexp]</code><br />
        <br />
        The forming-rules are the same as for the notify-command.<br />
        <br />
        <b>Example:</b><br />
        <br />

        <div>
          <code>define BurnerCounter HourCounter SHUTTER_TEST:on SHUTTER_TEST:off</code>
        </div>
      </div><br />
      <a name="HourCounterset" id="HourCounterset"></a> <b>Set-Commands</b>

      <div>
        <br />
        <code>set &lt;name&gt; calc</code><br />
        <br />

        <div>
          starts the calculation of pulse/pause-time.<br />
        </div><br />
        <br />
        <code>set &lt;name&gt; clear</code><br />
        <br />

        <div>
          clears the readings countsPerDay, countsOverall,pauseTimeIncrement, pauseTimePerDay, pauseTimeOverall,
          pulseTimeIncrement, pulseTimePerDay, pulseTimeOverall by setting to 0.<br />
          The reading clearDate is set to the current Date/Time.
        </div><br />
        <br />
        <code>set &lt;name&gt; countsOverall &lt;value&gt;</code><br />
        <br />

        <div>
          Sets the reading countsOverall to the given value.This is the total-counter.
        </div><br />
        <br />
        <code>set &lt;name&gt; countsPerDay &lt;value&gt;</code><br />
        <br />

        <div>
          Sets the reading countsPerDay to the given value. This reading will automatically be set to 0, after change
          of day.
        </div><br />
        <br />
        <code>set &lt;name&gt; pauseTimeIncrement &lt;value&gt;</code><br />
        <br />

        <div>
          Sets the reading pauseTimeIncrement to the given value.<br />
          This reading in seconds is automatically set after a rising edge.
        </div><br />
        <br />
        <code>set &lt;name&gt; pauseTimeEdge &lt;value&gt;</code><br />
        <br />

        <div>
          Sets the reading pauseTimeEdge to the given value.<br />
          This reading in seconds is automatically set after a rising edge.
        </div><br />
        <br />
        <code>set &lt;name&gt; pauseTimeOverall &lt;value&gt;</code><br />
        <br />

        <div>
          Sets the reading pauseTimeOverall to the given value.<br />
          This reading in seconds is automatically adjusted after a change of pauseTimeIncrement.
        </div><br />
        <br />
        <code>set &lt;name&gt; pauseTimePerDay &lt;value&gt;</code><br />
        <br />

        <div>
          Sets the reading pauseTimePerDay to the given value.<br />
          This reading in seconds is automatically adjusted after a change of pauseTimeIncrement and set to 0 after
          change of day.
        </div><br />
        <br />
        <code>set &lt;name&gt; pulseTimeIncrement &lt;value&gt;</code><br />
        <br />

        <div>
          Sets the reading pulseTimeIncrement to the given value.<br />
          This reading in seconds is automatically set after a falling edge of the property.
        </div><br />
        <br />
        <code>set &lt;name&gt; pulseTimeEdge &lt;value&gt;</code><br />
        <br />

        <div>
          Sets the reading pulseTimeEdge to the given value.<br />
          This reading in seconds is automatically set after a rising edge.
        </div><br />
        <br />
        <code>set &lt;name&gt; pulseTimeOverall &lt;value&gt;</code><br />
        <br />

        <div>
          Sets the reading pulseTimeOverall to the given value.<br />
          This reading in seconds is automatically adjusted after a change of pulseTimeIncrement.
        </div><br />
        <br />
        <code>set &lt;name&gt; pulseTimePerDay &lt;value&gt;</code><br />
        <br />

        <div>
          Sets the reading pulseTimePerDay to the given value.<br />
          This reading in seconds is automatically adjusted after a change of pulseTimeIncrement and set to 0 after
          change of day.
        </div><br />
        <br />
        <code>set &lt;name&gt; forceHourChange</code><br />
        <br />

        <div>
          This modifies the reading tickHour, which is automatically modified after change of hour.
        </div><br />
        <br />
        <code>set &lt;name&gt; forceDayChange</code><br />
        <br />

        <div>
          This modifies the reading tickDay, which is automatically modified after change of day.
        </div><br />
        <br />
        <code>set &lt;name&gt; forceWeekChange</code><br />
        <br />

        <div>
          This modifies the reading tickWeek, which is automatically modified after change of week.
        </div><br />
        <br />
        <code>set &lt;name&gt; forceMonthChange</code><br />
        <br />

        <div>
          This modifies the reading tickMonth, which is automatically modified after change of month.
        </div><br />
        <br />
        <code>set &lt;name&gt; forceYearChange</code><br />
        <br />

        <div>
          This modifies the reading tickYear, which is automatically modified after change of year.
        </div><br />
        <br />
        <code>set &lt;name&gt; app.* &lt;value&gt;</code><br />
        <br />

        <div>
          Any reading with the leading term "app", can be modified.<br />
          This can be useful for user-readings.
        </div><br />
      </div><br />
      <a name="HourCounterget" id="HourCounterget"></a> <b>Get-Commands</b><br />

      <div>
        <br />
        <code>get &lt;name&gt; version</code><br />
        <br />

        <div>
          Get the current version of the module.
        </div><br />
      </div><br />
      <a name="HourCounterattr" id="HourCounterattr"></a> <b>Attributes</b>
      <br />

      <ul>
        <li><p><b>interval</b><br />
        the update interval for pulse/pause-time in minutes [default 60]</p></li>

        <li><p><a href="#readingFnAttributes">readingFnAttributes</a></p></li>
      </ul>
      <b>Additional information</b>
      <br />

      <ul>
        <li><p><a href="http://forum.fhem.de/index.php/topic,12216.0.html">Discussion in FHEM forum</a></p></li>
        <li><p><a href="http://www.fhemwiki.de/wiki/HourCounter">WIKI information in FHEM Wiki</a></p></li>
        <li><p>The file 99_UtilsHourCounter.pm is a reference implementation for user defined extensions.<br />
        It shows how to create sum values for hours,days, weeks, months and years.<br />
        This file is located in the sub-folder contrib. For further information take a look to FHEM Wiki.</p></li>
      </ul>
    </div>
  </div>

=end html
=cut
