98_HourCounter.pm 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023
  1. # $Id: 98_HourCounter.pm 11307 2016-04-25 08:02:06Z rudolfkoenig $
  2. ####################################################################################################
  3. #
  4. # 98_HourCounter.pm
  5. # The HourCounter accumulates single events to a counter object.
  6. # In the case of binary weighted events pulse- and pause-time are determined
  7. #
  8. # This module is written by john.
  9. #
  10. # This file is part of fhem.
  11. #
  12. # Fhem is free software: you can redistribute it and/or modify
  13. # it under the terms of the GNU General Public License as published by
  14. # the Free Software Foundation, either version 2 of the License, or
  15. # (at your option) any later version.
  16. #
  17. # Fhem is distributed in the hope that it will be useful,
  18. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. # GNU General Public License for more details.
  21. #
  22. # You should have received a copy of the GNU General Public License
  23. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  24. #
  25. #
  26. # 16.11.13 - 0.99.b
  27. # Loglevel adjusted
  28. # 02.12.13 - 0.99.c
  29. # $readingFnAttributes added
  30. # 03.12.13 - 0.99.d
  31. # missed attribute event-on-change-reading
  32. # 02.02.14 - 1.00
  33. # command queues
  34. # 04.02.14 - 1.01
  35. # queue removed
  36. # 17.03.14 - 1.02
  37. # adjusting log-levels, forceYearChange,HourCounter_RoundYear
  38. # 07.06.14 - 1.03
  39. # $ID changed
  40. # setter for pulseTimeIncrement, pauseTimeIncrement
  41. # 25.10.14 - 1.0.0.4
  42. # official part of fhem
  43. # adjusting log-output
  44. # update documentation
  45. # 14.11.14 - 1.0.0.5
  46. # minor fixes for logging in HourCounter_Set: thanks kubuntufan
  47. # reformating
  48. # 17.11.14 - 1.0.0.6
  49. # cyclic calculation of pulse/pause-duration
  50. # correctly restores counter values after restart
  51. # 10.12.14 - 1.0.1.0
  52. # new readings pulseTimeEdge, pauseTimeEdge hold the last pusle*-Increment value
  53. # all operative readings beside the tick*-readings are updated every cycle now
  54. # new reading tickChanged is fired, if the value is changed
  55. # new reading tickUpdated is fired each time the operative readings are updated
  56. # some bug fixes concerning duration and calc calculations
  57. # note, that also 99_UtilsHourCounter needs changes
  58. # 21.12.14 - 1.0.1.1
  59. # bug: if OFF is not defined, nothing was counted
  60. # html : check with tidy
  61. # 24.12.14 - 1.0.1.2
  62. # bug: if cvent occurs without value change, wrong calculcation of pulseTimeIncrement, pauseTimeIncrement
  63. ####################################################################################################
  64. package main;
  65. use strict;
  66. use warnings;
  67. use vars qw(%defs);
  68. use vars qw($readingFnAttributes);
  69. use vars qw(%attr);
  70. use vars qw(%modules);
  71. my $HourCounter_Version = "1.0.1.2 - 24.12.2014";
  72. my @HourCounter_cmdQeue = ();
  73. my $DEBUG = 1;
  74. ##########################
  75. sub HourCounter_Log($$$)
  76. {
  77. my ( $hash, $loglevel, $text ) = @_;
  78. my $xline = ( caller(0) )[2];
  79. my $xsubroutine = ( caller(1) )[3];
  80. my $sub = ( split( ':', $xsubroutine ) )[2];
  81. $sub =~ s/HourCounter_//;
  82. my $instName = ( ref($hash) eq "HASH" ) ? $hash->{NAME} : "HourCounter";
  83. Log3 $hash, $loglevel, "HourCounter $instName $sub.$xline " . $text;
  84. }
  85. ##########################
  86. sub HourCounter_AddLog($$$)
  87. {
  88. my ( $logdevice, $readingName, $value ) = @_;
  89. my $cmd = '';
  90. if ( $readingName =~ m,state,i )
  91. {
  92. $cmd = "trigger $logdevice $value << addLog";
  93. } else
  94. {
  95. $cmd = "trigger $logdevice $readingName: $value << addLog";
  96. }
  97. HourCounter_Log '', 3, $cmd;
  98. fhem($cmd);
  99. }
  100. ##########################
  101. # execute the content of the given parameter
  102. sub HourCounter_Exec($)
  103. {
  104. my $doit = shift;
  105. my $ret = '';
  106. eval $doit;
  107. $ret = $@ if ($@);
  108. return $ret;
  109. }
  110. ##########################
  111. # add command to queue
  112. sub HourCounter_cmdQueueAdd($$)
  113. {
  114. my ( $hash, $cmd ) = @_;
  115. push( @{ $hash->{helper}{cmdQueue} }, $cmd );
  116. }
  117. ##########################
  118. # execute command queue
  119. sub HourCounter_ExecQueue($)
  120. {
  121. my ($hash) = @_;
  122. my $result;
  123. my $cnt = $#{ $hash->{helper}{cmdQueue} };
  124. my $loops = 0;
  125. my $cntAll = 0;
  126. HourCounter_Log $hash, 4, "cnt: $cnt";
  127. while ( $cnt >= 0 )
  128. {
  129. for my $i ( 0 .. $cnt )
  130. {
  131. my $cmd = ${ $hash->{helper}{cmdQueue} }[$i];
  132. ${ $hash->{helper}{cmdQueue} }[$i] = '';
  133. $result = HourCounter_Exec($cmd);
  134. if ($result)
  135. {
  136. HourCounter_Log $hash, 2, "$result";
  137. } else
  138. {
  139. HourCounter_Log $hash, 4, "exec ok:$cmd";
  140. }
  141. $cntAll++;
  142. }
  143. # bearbeitete eintraege loeschen
  144. for ( my $i = $cnt ; $i > -1 ; $i-- )
  145. {
  146. splice( @{ $hash->{helper}{cmdQueue} }, $i, 1 );
  147. }
  148. $cnt = $#HourCounter_cmdQeue;
  149. $loops++;
  150. if ( $loops >= 5 || $cntAll > 100 )
  151. {
  152. HourCounter_Log $hash, 2, "!!! too deep recursion";
  153. last;
  154. }
  155. }
  156. }
  157. ##########################
  158. # round off the date passed to the hour
  159. sub HourCounter_RoundHour($)
  160. {
  161. my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime(shift);
  162. return mktime( 0, 0, $hour, $mday, $mon, $year );
  163. }
  164. ##########################
  165. # round off the date passed to the day
  166. sub HourCounter_RoundDay($)
  167. {
  168. my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime(shift);
  169. return mktime( 0, 0, 0, $mday, $mon, $year );
  170. }
  171. ##########################
  172. # round off the date passed to the week
  173. sub HourCounter_RoundWeek($)
  174. {
  175. my ($time) = @_;
  176. my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime($time);
  177. # wday 0 Sonntag 1 Montag ...
  178. $time -= $wday * 86400;
  179. return HourCounter_RoundDay($time);
  180. }
  181. ##########################
  182. # returns the seconds since the start of the day
  183. sub HourCounter_SecondsOfDay()
  184. {
  185. my $timeToday = gettimeofday();
  186. return int( $timeToday - HourCounter_RoundDay($timeToday) );
  187. }
  188. ##########################
  189. # round off the date passed to the month
  190. sub HourCounter_RoundMonth($)
  191. {
  192. my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime(shift);
  193. return mktime( 0, 0, 0, 1, $mon, $year );
  194. }
  195. ##########################
  196. # round off the date passed to the year
  197. sub HourCounter_RoundYear($)
  198. {
  199. my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime(shift);
  200. return mktime( 0, 0, 0, 1, 1, $year );
  201. }
  202. ##########################
  203. sub HourCounter_Initialize($)
  204. {
  205. my ($hash) = @_;
  206. $hash->{DefFn} = "HourCounter_Define";
  207. $hash->{UndefFn} = "HourCounter_Undef";
  208. $hash->{SetFn} = "HourCounter_Set";
  209. $hash->{GetFn} = "HourCounter_Get";
  210. $hash->{NotifyFn} = "HourCounter_Notify";
  211. $hash->{AttrFn} = "HourCounter_Attr";
  212. $hash->{AttrList} = "disable:0,1 interval:1,2,3,4,5,10,15,20,30,60 " . $readingFnAttributes;
  213. HourCounter_Log "", 3, "Init Done with Version $HourCounter_Version";
  214. }
  215. ##########################
  216. sub HourCounter_Define($$$)
  217. {
  218. my ( $hash, $def ) = @_;
  219. my @a = split( "[ \t][ \t]*", $def );
  220. my $name = $a[0];
  221. HourCounter_Log $hash, ($DEBUG) ? 0 : 4, "parameters: @a";
  222. if ( @a < 3 )
  223. {
  224. return "wrong syntax: define <name> HourCounter <regexp_for_ON> [<regexp_for_OFF>]";
  225. }
  226. my $onRegexp = $a[2];
  227. my $offRegexp = ( @a == 4 ) ? $a[3] : undef;
  228. # Checking for misleading regexps
  229. eval { "Hallo" =~ m/^$onRegexp/ };
  230. return "Bad regexp_for_ON : $@" if ($@);
  231. if ($offRegexp)
  232. {
  233. eval { "Hallo" =~ m/^$offRegexp/ };
  234. return "Bad regexp_for_ON : $@" if ($@);
  235. }
  236. #
  237. # some inits
  238. $hash->{VERSION} = $HourCounter_Version;
  239. $hash->{helper}{ON_Regexp} = $onRegexp;
  240. $hash->{helper}{OFF_Regexp} = $offRegexp;
  241. $hash->{helper}{isFirstRun} = 1;
  242. $hash->{helper}{value} = -1;
  243. $hash->{helper}{forceHourChange} = '';
  244. $hash->{helper}{forceDayChange} = '';
  245. $hash->{helper}{forceWeekChange} = '';
  246. $hash->{helper}{forceMonthChange} = '';
  247. $hash->{helper}{forceYearChange} = '';
  248. $hash->{helper}{forceClear} = '';
  249. $hash->{helper}{calledByEvent} = '';
  250. $hash->{helper}{changedTimestamp} = '';
  251. @{ $hash->{helper}{cmdQueue} } = ();
  252. $modules{HourCounter}{defptr}{$name} = $hash;
  253. RemoveInternalTimer($name);
  254. # wait until alle readings have been restored
  255. InternalTimer( int( gettimeofday() + 15 ), "HourCounter_Run", $name, 0 );
  256. return undef;
  257. }
  258. ##########################
  259. sub HourCounter_Undef($$)
  260. {
  261. my ( $hash, $arg ) = @_;
  262. HourCounter_Log $hash, 3, "Done";
  263. return undef;
  264. }
  265. ###########################
  266. sub HourCounter_Get($@)
  267. {
  268. my ( $hash, @a ) = @_;
  269. my $name = $hash->{NAME};
  270. my $ret = "Unknown argument $a[1], choose one of version:noArg";
  271. my $cmd = lc( $a[1] );
  272. if ( $cmd eq 'version' )
  273. {
  274. $ret = "Version : $HourCounter_Version\n";
  275. }
  276. return $ret;
  277. }
  278. ###########################
  279. sub HourCounter_Set($@)
  280. {
  281. my ( $hash, @a ) = @_;
  282. my $name = $hash->{NAME};
  283. my $reINT = '^([\\+,\\-]?\\d+$)'; # int
  284. # determine userReadings beginning with app
  285. my @readingNames = keys( %{ $hash->{READINGS} } );
  286. my @userReadings = ();
  287. foreach (@readingNames)
  288. {
  289. if ( $_ =~ m/app.*/ )
  290. {
  291. push( @userReadings, $_ );
  292. }
  293. }
  294. my $strUserReadings = join( " ", @userReadings ) . " ";
  295. # standard commands with parameter
  296. my @cmdPara = (
  297. "countsOverall", "countsPerDay", "pauseTimeIncrement", "pauseTimePerDay",
  298. "pauseTimeOverall", "pulseTimeIncrement", "pulseTimePerDay", "pulseTimeOverall"
  299. );
  300. # standard commands with no parameter
  301. my @cmdNoPara =
  302. ( "clear", "forceHourChange", "forceDayChange", "forceWeekChange", "forceMonthChange", "forceYearChange", "calc" );
  303. my @allCommands = ( @cmdPara, @cmdNoPara, @userReadings );
  304. my $strAllCommands =
  305. join( " ", ( @cmdPara, @userReadings ) ) . " " . join( ":noArg ", @cmdNoPara ) . ":noArg ";
  306. #HourCounter_Log $hash, 2, "strAllCommands : $strAllCommands";
  307. # stop:noArg
  308. my $usage = "Unknown argument $a[1], choose one of " . $strAllCommands;
  309. # we need at least 2 parameters
  310. return "Need a parameter for set" if ( @a < 2 );
  311. my $cmd = $a[1];
  312. if ( $cmd eq "?" )
  313. {
  314. return $usage;
  315. }
  316. my $value = $a[2];
  317. # is command defined ?
  318. if ( ( grep { /$cmd/ } @allCommands ) <= 0 )
  319. {
  320. HourCounter_Log $hash, 2, "cmd:$cmd no match for : @allCommands";
  321. return return "unknown command : $cmd";
  322. }
  323. # need we a parameter ?
  324. my $hits = scalar grep { /$cmd/ } @cmdNoPara;
  325. my $needPara = ( $hits > 0 ) ? '' : 1;
  326. HourCounter_Log $hash, 4, "hits: $hits needPara:$needPara";
  327. # if parameter needed, it must be an integer
  328. return "Value must be an integer" if ( $needPara && !( $value =~ m/$reINT/ ) );
  329. my $info = "command : " . $cmd;
  330. $info .= " " . $value if ($needPara);
  331. HourCounter_Log $hash, 4, $info;
  332. my $doRun = '';
  333. if ($needPara)
  334. {
  335. readingsSingleUpdate( $hash, $cmd, $value, 1 );
  336. } elsif ( $cmd eq "forceHourChange" )
  337. {
  338. $hash->{helper}{forceHourChange} = 1;
  339. $doRun = 1;
  340. } elsif ( $cmd eq "forceDayChange" )
  341. {
  342. $hash->{helper}{forceDayChange} = 1;
  343. $doRun = 1;
  344. } elsif ( $cmd eq "forceWeekChange" )
  345. {
  346. $hash->{helper}{forceWeekChange} = 1;
  347. $doRun = 1;
  348. } elsif ( $cmd eq "forceMonthChange" )
  349. {
  350. $hash->{helper}{forceMonthChange} = 1;
  351. $doRun = 1;
  352. } elsif ( $cmd eq "forceYearChange" )
  353. {
  354. $hash->{helper}{forceYearChange} = 1;
  355. $doRun = 1;
  356. } elsif ( $cmd eq "clear" )
  357. {
  358. $hash->{helper}{forceClear} = 1;
  359. $doRun = 1;
  360. } elsif ( $cmd eq "calc" )
  361. {
  362. $doRun = 1;
  363. } else
  364. {
  365. return "unknown command (2): $cmd";
  366. }
  367. # perform run
  368. if ( $doRun && !$hash->{helper}{isFirstRun} )
  369. {
  370. $hash->{helper}{value} = -1;
  371. $hash->{helper}{calledByEvent} = 1;
  372. HourCounter_Run( $hash->{NAME} );
  373. }
  374. return;
  375. }
  376. ##########################
  377. sub HourCounter_Notify($$)
  378. {
  379. my ( $hash, $dev ) = @_;
  380. my $name = $hash->{NAME};
  381. my $devName = $dev->{NAME};
  382. # return if disabled
  383. if ( AttrVal( $name, 'disable', '0' ) eq '1' )
  384. {
  385. return "";
  386. }
  387. my $onRegexp = $hash->{helper}{ON_Regexp};
  388. my $offRegexp = $hash->{helper}{OFF_Regexp};
  389. my $max = int( @{ $dev->{CHANGED} } );
  390. for ( my $i = 0 ; $i < $max ; $i++ )
  391. {
  392. my $s = $dev->{CHANGED}[$i]; # read changed reading
  393. $s = "" if ( !defined($s) );
  394. my $isOnReading = ( "$devName:$s" =~ m/^$onRegexp$/ );
  395. my $isOffReading = ($offRegexp) ? ( "$devName:$s" =~ m/^$offRegexp$/ ) : '';
  396. # HourCounter_Log $hash, 5, "devName:$devName; CHANGED:$s; isOnReading:$isOnReading; isOffReading:$isOffReading;";
  397. next if ( !( $isOnReading || ( $isOffReading && $offRegexp ) ) );
  398. $hash->{helper}{value} = 1 if ($isOnReading);
  399. $hash->{helper}{value} = 0 if ($isOffReading);
  400. $hash->{helper}{calledByEvent} = 1;
  401. if ( !$hash->{helper}{isFirstRun} )
  402. {
  403. HourCounter_Run( $hash->{NAME} );
  404. }
  405. }
  406. }
  407. ##########################
  408. sub HourCounter_Attr($$$$)
  409. {
  410. my ( $command, $name, $attribute, $value ) = @_;
  411. my $msg = undef;
  412. my $hash = $defs{$name};
  413. if ( $attribute eq "interval" )
  414. {
  415. #HourCounter_Log $hash, 0, "cmd:$command name:$name attribute:$attribute";
  416. if ( !$hash->{helper}{isFirstRun} )
  417. {
  418. HourCounter_Run($name);
  419. }
  420. }
  421. return $msg;
  422. }
  423. ##########################
  424. # converts the seconds in the date format
  425. sub HourCounter_Seconds2HMS($)
  426. {
  427. my ($seconds) = @_;
  428. my ( $Sekunde, $Minute, $Stunde, $Monatstag, $Monat, $Jahr, $Wochentag, $Jahrestag, $Sommerzeit ) =
  429. localtime($seconds);
  430. my $days = int( $seconds / 86400 );
  431. return sprintf( "%d Tage %02d:%02d:%02d", $days, $Stunde - 1, $Minute, $Sekunde );
  432. }
  433. ##########################
  434. # rounds the timestamp do the beginning of the week
  435. sub HourCounter_weekBase($)
  436. {
  437. my ($time) = @_;
  438. my $dayDiff = 60 * 60 * 24;
  439. my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime($time);
  440. # wday 0 Sonntag 1 Montag ...
  441. my $a = $time - $wday * $dayDiff;
  442. my $b = int( $a / $dayDiff ); # auf tage gehen
  443. my $c = $b * $dayDiff;
  444. return $c;
  445. }
  446. ##########################
  447. # this either called by timer for cyclic update
  448. # or it is called by an event (on/off)
  449. sub HourCounter_Run($)
  450. {
  451. # print "xxx TAG A\n" ;
  452. my ($name) = @_;
  453. my $hash = $defs{$name};
  454. # must be of type hourcounter
  455. return if ( !defined( $hash->{TYPE} ) || $hash->{TYPE} ne 'HourCounter' );
  456. # timestamps for event-log-file-entries, older than current time
  457. delete( $hash->{CHANGETIME} );
  458. # flag for called by event
  459. my $calledByEvent = $hash->{helper}{calledByEvent};
  460. # reset flag
  461. $hash->{helper}{calledByEvent} = '';
  462. # if call was made by timer, than force value to -1
  463. my $valuePara = ($calledByEvent) ? $hash->{helper}{value} : -1;
  464. # initialize changedTimestamp, if it does not exist
  465. $hash->{helper}{changedTimestamp} = ReadingsTimestamp( $name, "value", TimeNow() )
  466. if ( !$hash->{helper}{changedTimestamp} );
  467. # serial date for changed timestamp
  468. my $sdValue = time_str2num( $hash->{helper}{changedTimestamp} );
  469. my $sdCurTime = gettimeofday();
  470. my $isOffDefined = ( $hash->{helper}{OFF_Regexp} ) ? 1 : '';
  471. # calc time diff
  472. my $timeIncrement = int( $sdCurTime - $sdValue );
  473. # wrong time offset in case of summer/winter time
  474. $timeIncrement = 0 if ( $timeIncrement < 0 );
  475. # get the old value
  476. my $valueOld = ReadingsVal( $name, 'value', 0 );
  477. # variable for reading update
  478. my $value = undef;
  479. my $countsPerDay = ReadingsVal( $name, "countsPerDay", 0 );
  480. my $countsOverall = ReadingsVal( $name, "countsOverall", 0 );
  481. my $pulseTimeIncrement = ReadingsVal( $name, "pulseTimeIncrement", 0 );
  482. my $pulseTimePerDay = ReadingsVal( $name, "pulseTimePerDay", 0 );
  483. my $pulseTimeOverall = ReadingsVal( $name, "pulseTimeOverall", 0 );
  484. my $pulseTimeEdge = ReadingsVal( $name, "pulseTimeEdge", 0 );
  485. my $pauseTimeIncrement = ReadingsVal( $name, "pauseTimeIncrement", 0 );
  486. my $pauseTimePerDay = ReadingsVal( $name, "pauseTimePerDay", 0 );
  487. my $pauseTimeOverall = ReadingsVal( $name, "pauseTimeOverall", 0 );
  488. my $pauseTimeEdge = ReadingsVal( $name, "pauseTimeEdge", 0 );
  489. my $tickUpdated = ReadingsVal( $name, "tickUpdated", 0 ) + 1;
  490. $tickUpdated = 1 if ( $tickUpdated >= 1000 );
  491. my $tickChanged = ReadingsVal( $name, "tickChanged", 0 );
  492. my $tickHour = ReadingsVal( $name, "tickHour", 0 );
  493. my $tickDay = ReadingsVal( $name, "tickDay", 0 );
  494. my $tickWeek = ReadingsVal( $name, "tickWeek", 0 );
  495. my $tickMonth = ReadingsVal( $name, "tickMonth", 0 );
  496. my $tickYear = ReadingsVal( $name, "tickYear", 0 );
  497. my $state = '';
  498. my $sdTickHour = time_str2num( ReadingsTimestamp( $name, "tickHour", TimeNow() ) );
  499. # serial date for current hour
  500. my $sdRoundHour = HourCounter_RoundHour($sdCurTime);
  501. my $sdRoundHourLast = HourCounter_RoundHour($sdTickHour);
  502. $sdRoundHourLast = $sdRoundHour if ( !$sdRoundHourLast );
  503. my $isHourChanged = ( $sdRoundHour != $sdRoundHourLast ) || $hash->{helper}{forceHourChange};
  504. # serial date for current day
  505. my $sdRoundDayCurTime = HourCounter_RoundDay($sdCurTime);
  506. my $sdRoundDayValue = HourCounter_RoundDay($sdRoundHourLast);
  507. my $isDayChanged = ( $sdRoundDayCurTime != $sdRoundDayValue ) || $hash->{helper}{forceDayChange};
  508. # serial date for current week
  509. my $sdRoundWeekCurTime = HourCounter_RoundWeek($sdCurTime);
  510. my $sdRoundWeekValue = HourCounter_RoundWeek($sdRoundHourLast);
  511. my $isWeekChanged =
  512. ( $sdRoundWeekCurTime != $sdRoundWeekValue ) || $hash->{helper}{forceWeekChange};
  513. # serial date for current month
  514. my $sdRoundMonthCurTime = HourCounter_RoundMonth($sdCurTime);
  515. my $sdRoundMonthValue = HourCounter_RoundMonth($sdRoundHourLast);
  516. my $isMonthChanged =
  517. ( $sdRoundMonthCurTime != $sdRoundMonthValue ) || $hash->{helper}{forceMonthChange};
  518. # serial date for current year
  519. my $sdRoundYearCurTime = HourCounter_RoundYear($sdCurTime);
  520. my $sdRoundYearValue = HourCounter_RoundYear($sdRoundHourLast);
  521. my $isYearChanged =
  522. ( $sdRoundYearCurTime != $sdRoundYearValue ) || $hash->{helper}{forceYearChange};
  523. # loop forever
  524. while (1)
  525. {
  526. # stop if disabled
  527. last if ( AttrVal( $name, 'disable', '0' ) eq '1' );
  528. # variables for controlling
  529. HourCounter_Log $hash, 5, "value:$valuePara changedTimestamp:" . $hash->{helper}{changedTimestamp};
  530. # ------------ basic init, when first run
  531. if ( $hash->{helper}{isFirstRun} )
  532. {
  533. $hash->{helper}{isFirstRun} = undef;
  534. $hash->{helper}{sdRoundHourLast} = $sdRoundHourLast;
  535. # first init after startup
  536. readingsBeginUpdate($hash);
  537. readingsBulkUpdate( $hash, 'tickHour', 0 );
  538. readingsBulkUpdate( $hash, 'tickDay', 0 );
  539. readingsBulkUpdate( $hash, 'tickWeek', 0 );
  540. readingsBulkUpdate( $hash, 'tickMonth', 0 );
  541. readingsBulkUpdate( $hash, 'tickYear', 0 );
  542. readingsEndUpdate( $hash, 0 );
  543. # set initial values
  544. $value = $valueOld; # value als reading anlegen falls nicht vorhanden
  545. $timeIncrement = 0;
  546. HourCounter_Log $hash, 0, "first run done countsOverall:" . $countsOverall; #4
  547. }
  548. # -------- force clear request
  549. if ( $hash->{helper}{forceClear} )
  550. {
  551. HourCounter_Log $hash, 0, "force clear request";
  552. readingsSingleUpdate( $hash, 'clearDate', TimeNow(), 1 );
  553. # reset all counters
  554. $countsOverall = 0;
  555. $countsPerDay = 0;
  556. $pauseTimeIncrement = 0;
  557. $pauseTimeEdge = 0;
  558. $pauseTimeOverall = 0;
  559. $pauseTimePerDay = 0;
  560. $pulseTimeIncrement = 0;
  561. $pulseTimeEdge = 0;
  562. $pulseTimeOverall = 0;
  563. $pulseTimePerDay = 0;
  564. $hash->{helper}{forceClear} = '';
  565. $timeIncrement = 0;
  566. }
  567. # -------------- handling of transitions
  568. my $hasValueChanged = 0;
  569. if ( ( $isOffDefined && $valuePara >= 0 && $valuePara != $valueOld )
  570. || ( !$isOffDefined && $calledByEvent ) )
  571. {
  572. $hasValueChanged = 1;
  573. }
  574. if ($hasValueChanged)
  575. {
  576. $value = $valuePara;
  577. $valueOld = $valuePara;
  578. # -------------- positive edge
  579. if ( $valuePara == 1 )
  580. {
  581. # handling of counters
  582. $countsPerDay += 1;
  583. $countsOverall += 1;
  584. # handling of pause time
  585. if ($isOffDefined)
  586. {
  587. # calc the rest of puse-time until edge
  588. $pauseTimeIncrement += $timeIncrement;
  589. $pauseTimePerDay += $timeIncrement;
  590. $pauseTimeOverall += $timeIncrement;
  591. $pulseTimeIncrement = 0;
  592. $pauseTimeEdge = $pauseTimeIncrement;
  593. }
  594. HourCounter_Log $hash, 4, "rising edge; pauseTimeIncr:$pauseTimeIncrement countPerDay:$countsPerDay";
  595. }
  596. # ------------ negative edge
  597. elsif ( $valuePara == 0 )
  598. {
  599. # handlich of pulse time
  600. if ($isOffDefined)
  601. {
  602. $pulseTimeIncrement += $timeIncrement;
  603. $pulseTimePerDay += $timeIncrement;
  604. $pulseTimeOverall += $timeIncrement;
  605. $pauseTimeIncrement = 0;
  606. $pulseTimeEdge = $pulseTimeIncrement;
  607. }
  608. HourCounter_Log $hash, 4, "falling edge pulseTimeIncrement:$pulseTimeIncrement";
  609. }
  610. }
  611. # ------------ no value change
  612. # it is possible to receive an event without change of value (e.g. Max Shutter does it hourly)
  613. elsif ($isOffDefined)
  614. {
  615. if ( $valueOld == 0 )
  616. {
  617. $pauseTimeIncrement += $timeIncrement;
  618. $pauseTimePerDay += $timeIncrement;
  619. $pauseTimeOverall += $timeIncrement;
  620. } elsif ( $valueOld == 1 )
  621. {
  622. $pulseTimeIncrement += $timeIncrement;
  623. $pulseTimePerDay += $timeIncrement;
  624. $pulseTimeOverall += $timeIncrement;
  625. }
  626. }
  627. $hash->{helper}{changedTimestamp} = TimeNow();
  628. $value = $valueOld;
  629. $state = $countsPerDay;
  630. # ---------update readings, if vars defined
  631. readingsBeginUpdate($hash);
  632. readingsBulkUpdate( $hash, "countsPerDay", $countsPerDay );
  633. readingsBulkUpdate( $hash, "countsOverall", $countsOverall );
  634. if ($isOffDefined)
  635. {
  636. readingsBulkUpdate( $hash, "pulseTimeIncrement", $pulseTimeIncrement );
  637. readingsBulkUpdate( $hash, "pulseTimeEdge", $pulseTimeEdge );
  638. readingsBulkUpdate( $hash, "pulseTimePerDay", $pulseTimePerDay );
  639. readingsBulkUpdate( $hash, "pulseTimeOverall", $pulseTimeOverall );
  640. readingsBulkUpdate( $hash, "pauseTimeIncrement", $pauseTimeIncrement );
  641. readingsBulkUpdate( $hash, "pauseTimeEdge", $pauseTimeEdge );
  642. readingsBulkUpdate( $hash, "pauseTimePerDay", $pauseTimePerDay );
  643. readingsBulkUpdate( $hash, "pauseTimeOverall", $pauseTimeOverall );
  644. }
  645. readingsBulkUpdate( $hash, "value", $value );
  646. readingsBulkUpdate( $hash, 'state', $state );
  647. readingsBulkUpdate( $hash, 'tickUpdated', $tickUpdated );
  648. readingsEndUpdate( $hash, 1 );
  649. # --------------- fire time interval ticks for hour,day,month
  650. if ($hasValueChanged)
  651. {
  652. $tickChanged++;
  653. $tickChanged = 1 if ( $tickChanged >= 1000 );
  654. readingsSingleUpdate( $hash, 'tickChanged', $tickChanged, 1 );
  655. HourCounter_Log $hash, 4, 'tickChanged fired ';
  656. }
  657. if ($isHourChanged)
  658. {
  659. $tickHour++;
  660. $tickHour = 1 if ( $tickHour >= 1000 );
  661. $hash->{helper}{forceHourChange} = '';
  662. $hash->{helper}{sdRoundHourLast} = $sdRoundHour;
  663. readingsSingleUpdate( $hash, 'tickHour', $tickHour, 1 );
  664. HourCounter_Log $hash, 4, "tickHour fired";
  665. }
  666. if ($isDayChanged)
  667. {
  668. $tickDay++;
  669. $tickDay = 1 if ( $tickDay >= 1000 );
  670. $hash->{helper}{forceDayChange} = '';
  671. readingsSingleUpdate( $hash, 'tickDay', $tickDay, 1 );
  672. HourCounter_Log $hash, 4, "tickDay fired";
  673. }
  674. if ($isWeekChanged)
  675. {
  676. $tickWeek++;
  677. $tickWeek = 1 if ( $tickWeek >= 1000 );
  678. $hash->{helper}{forceWeekChange} = '';
  679. readingsSingleUpdate( $hash, 'tickWeek', $tickWeek, 1 );
  680. HourCounter_Log $hash, 4, "tickWeek fired";
  681. }
  682. if ($isMonthChanged)
  683. {
  684. $tickMonth++;
  685. $tickMonth = 1 if ( $tickMonth >= 1000 );
  686. $hash->{helper}{forceMonthChange} = '';
  687. readingsSingleUpdate( $hash, 'tickMonth', $tickMonth, 1 );
  688. HourCounter_Log $hash, 4, "tickMonth fired";
  689. }
  690. if ($isYearChanged)
  691. {
  692. $tickYear++;
  693. $tickYear = 1 if ( $tickYear >= 1000 );
  694. $hash->{helper}{forceYearChange} = '';
  695. readingsSingleUpdate( $hash, 'tickYear', $tickYear, 1 );
  696. HourCounter_Log $hash, 4, "tickYear fired";
  697. }
  698. # execute command queue
  699. HourCounter_ExecQueue($hash);
  700. # day change, so reset day readings
  701. if ($isDayChanged)
  702. {
  703. ### reset all day counters
  704. readingsBeginUpdate($hash);
  705. readingsBulkUpdate( $hash, "countsPerDay", 0 );
  706. readingsBulkUpdate( $hash, "pulseTimePerDay", 0 );
  707. readingsBulkUpdate( $hash, "pauseTimePerDay", 0 );
  708. readingsEndUpdate( $hash, 1 );
  709. HourCounter_Log $hash, 4, "reset day counters";
  710. }
  711. last;
  712. }
  713. # ------------ calculate seconds until next hour starts
  714. my $interval = AttrVal( $name, 'interval', '60' );
  715. my $actTime = int( gettimeofday() );
  716. my ( $sec, $min, $hour ) = localtime($actTime);
  717. # round to next interval
  718. my $seconds = $interval * 60;
  719. my $nextHourTime = int( ( $actTime + $seconds ) / $seconds ) * $seconds;
  720. # calc diff in seconds
  721. my $nextCall = $nextHourTime - $actTime;
  722. HourCounter_Log $hash, 5, "nextCall:$nextCall changedTimestamp:" . $hash->{helper}{changedTimestamp};
  723. RemoveInternalTimer($name);
  724. InternalTimer( gettimeofday() + $nextCall, "HourCounter_Run", $hash->{NAME}, 0 );
  725. return undef;
  726. }
  727. 1;
  728. =pod
  729. =begin html
  730. <div>
  731. <a name="HourCounter" id="HourCounter"></a>
  732. <h3>HourCounter</h3>
  733. <div>
  734. <a name="HourCounterdefine" id="HourCounterdefine"></a> <b>Define</b>
  735. <div>
  736. <br />
  737. <code>define &lt;name&gt; HourCounter &lt;pattern_for_ON&gt; [&lt;pattern_for_OFF&gt;]</code><br />
  738. <br />
  739. Hourcounter can detect both the activiy-time and the inactivity-time of a property.<br />
  740. The "pattern_for_ON" identifies the events, that signal the activity of the desired property.<br />
  741. The "pattern_for_OFF" identifies the events, that signal the inactivity of the desired property.<br />
  742. <br />
  743. If "pattern_for_OFF" is not defined, any matching event of "patter_for_ON" will be counted.<br />
  744. Otherwise only the rising edges of "pattern_for_ON" will be counted.<br />
  745. This means a "pattern_for_OFF"-event must be detected before a "pattern_for_ON"-event is accepted.<br />
  746. <br />
  747. "pattern_for_ON" and "pattern_for_OFF" must be formed using the following structure:<br />
  748. <br />
  749. <code>device:[regexp]</code><br />
  750. <br />
  751. The forming-rules are the same as for the notify-command.<br />
  752. <br />
  753. <b>Example:</b><br />
  754. <br />
  755. <div>
  756. <code>define BurnerCounter HourCounter SHUTTER_TEST:on SHUTTER_TEST:off</code>
  757. </div>
  758. </div><br />
  759. <a name="HourCounterset" id="HourCounterset"></a> <b>Set-Commands</b>
  760. <div>
  761. <br />
  762. <code>set &lt;name&gt; calc</code><br />
  763. <br />
  764. <div>
  765. starts the calculation of pulse/pause-time.<br />
  766. </div><br />
  767. <br />
  768. <code>set &lt;name&gt; clear</code><br />
  769. <br />
  770. <div>
  771. clears the readings countsPerDay, countsOverall,pauseTimeIncrement, pauseTimePerDay, pauseTimeOverall,
  772. pulseTimeIncrement, pulseTimePerDay, pulseTimeOverall by setting to 0.<br />
  773. The reading clearDate is set to the current Date/Time.
  774. </div><br />
  775. <br />
  776. <code>set &lt;name&gt; countsOverall &lt;value&gt;</code><br />
  777. <br />
  778. <div>
  779. Sets the reading countsOverall to the given value.This is the total-counter.
  780. </div><br />
  781. <br />
  782. <code>set &lt;name&gt; countsPerDay &lt;value&gt;</code><br />
  783. <br />
  784. <div>
  785. Sets the reading countsPerDay to the given value. This reading will automatically be set to 0, after change
  786. of day.
  787. </div><br />
  788. <br />
  789. <code>set &lt;name&gt; pauseTimeIncrement &lt;value&gt;</code><br />
  790. <br />
  791. <div>
  792. Sets the reading pauseTimeIncrement to the given value.<br />
  793. This reading in seconds is automatically set after a rising edge.
  794. </div><br />
  795. <br />
  796. <code>set &lt;name&gt; pauseTimeEdge &lt;value&gt;</code><br />
  797. <br />
  798. <div>
  799. Sets the reading pauseTimeEdge to the given value.<br />
  800. This reading in seconds is automatically set after a rising edge.
  801. </div><br />
  802. <br />
  803. <code>set &lt;name&gt; pauseTimeOverall &lt;value&gt;</code><br />
  804. <br />
  805. <div>
  806. Sets the reading pauseTimeOverall to the given value.<br />
  807. This reading in seconds is automatically adjusted after a change of pauseTimeIncrement.
  808. </div><br />
  809. <br />
  810. <code>set &lt;name&gt; pauseTimePerDay &lt;value&gt;</code><br />
  811. <br />
  812. <div>
  813. Sets the reading pauseTimePerDay to the given value.<br />
  814. This reading in seconds is automatically adjusted after a change of pauseTimeIncrement and set to 0 after
  815. change of day.
  816. </div><br />
  817. <br />
  818. <code>set &lt;name&gt; pulseTimeIncrement &lt;value&gt;</code><br />
  819. <br />
  820. <div>
  821. Sets the reading pulseTimeIncrement to the given value.<br />
  822. This reading in seconds is automatically set after a falling edge of the property.
  823. </div><br />
  824. <br />
  825. <code>set &lt;name&gt; pulseTimeEdge &lt;value&gt;</code><br />
  826. <br />
  827. <div>
  828. Sets the reading pulseTimeEdge to the given value.<br />
  829. This reading in seconds is automatically set after a rising edge.
  830. </div><br />
  831. <br />
  832. <code>set &lt;name&gt; pulseTimeOverall &lt;value&gt;</code><br />
  833. <br />
  834. <div>
  835. Sets the reading pulseTimeOverall to the given value.<br />
  836. This reading in seconds is automatically adjusted after a change of pulseTimeIncrement.
  837. </div><br />
  838. <br />
  839. <code>set &lt;name&gt; pulseTimePerDay &lt;value&gt;</code><br />
  840. <br />
  841. <div>
  842. Sets the reading pulseTimePerDay to the given value.<br />
  843. This reading in seconds is automatically adjusted after a change of pulseTimeIncrement and set to 0 after
  844. change of day.
  845. </div><br />
  846. <br />
  847. <code>set &lt;name&gt; forceHourChange</code><br />
  848. <br />
  849. <div>
  850. This modifies the reading tickHour, which is automatically modified after change of hour.
  851. </div><br />
  852. <br />
  853. <code>set &lt;name&gt; forceDayChange</code><br />
  854. <br />
  855. <div>
  856. This modifies the reading tickDay, which is automatically modified after change of day.
  857. </div><br />
  858. <br />
  859. <code>set &lt;name&gt; forceWeekChange</code><br />
  860. <br />
  861. <div>
  862. This modifies the reading tickWeek, which is automatically modified after change of week.
  863. </div><br />
  864. <br />
  865. <code>set &lt;name&gt; forceMonthChange</code><br />
  866. <br />
  867. <div>
  868. This modifies the reading tickMonth, which is automatically modified after change of month.
  869. </div><br />
  870. <br />
  871. <code>set &lt;name&gt; forceYearChange</code><br />
  872. <br />
  873. <div>
  874. This modifies the reading tickYear, which is automatically modified after change of year.
  875. </div><br />
  876. <br />
  877. <code>set &lt;name&gt; app.* &lt;value&gt;</code><br />
  878. <br />
  879. <div>
  880. Any reading with the leading term "app", can be modified.<br />
  881. This can be useful for user-readings.
  882. </div><br />
  883. </div><br />
  884. <a name="HourCounterget" id="HourCounterget"></a> <b>Get-Commands</b><br />
  885. <div>
  886. <br />
  887. <code>get &lt;name&gt; version</code><br />
  888. <br />
  889. <div>
  890. Get the current version of the module.
  891. </div><br />
  892. </div><br />
  893. <a name="HourCounterattr" id="HourCounterattr"></a> <b>Attributes</b>
  894. <br />
  895. <ul>
  896. <li><p><b>interval</b><br />
  897. the update interval for pulse/pause-time in minutes [default 60]</p></li>
  898. <li><p><a href="#readingFnAttributes">readingFnAttributes</a></p></li>
  899. </ul>
  900. <b>Additional information</b>
  901. <br />
  902. <ul>
  903. <li><p><a href="http://forum.fhem.de/index.php/topic,12216.0.html">Discussion in FHEM forum</a></p></li>
  904. <li><p><a href="http://www.fhemwiki.de/wiki/HourCounter">WIKI information in FHEM Wiki</a></p></li>
  905. <li><p>The file 99_UtilsHourCounter.pm is a reference implementation for user defined extensions.<br />
  906. It shows how to create sum values for hours,days, weeks, months and years.<br />
  907. This file is located in the sub-folder contrib. For further information take a look to FHEM Wiki.</p></li>
  908. </ul>
  909. </div>
  910. </div>
  911. =end html
  912. =cut