95_holiday.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. #######################################################################
  2. # $Id: 95_holiday.pm 12908 2016-12-30 09:29:05Z rudolfkoenig $
  3. package main;
  4. use strict;
  5. use warnings;
  6. use POSIX;
  7. sub holiday_refresh($;$);
  8. #####################################
  9. sub
  10. holiday_Initialize($)
  11. {
  12. my ($hash) = @_;
  13. $hash->{DefFn} = "holiday_Define";
  14. $hash->{GetFn} = "holiday_Get";
  15. $hash->{UndefFn} = "holiday_Undef";
  16. $hash->{AttrList} = $readingFnAttributes;
  17. }
  18. #####################################
  19. sub
  20. holiday_Define($$)
  21. {
  22. my ($hash, $def) = @_;
  23. return holiday_refresh($hash->{NAME}) if($init_done);
  24. InternalTimer(gettimeofday()+1, "holiday_refresh", $hash->{NAME}, 0);
  25. return undef;
  26. }
  27. sub
  28. holiday_Undef($$)
  29. {
  30. my ($hash, $name) = @_;
  31. RemoveInternalTimer($name);
  32. return undef;
  33. }
  34. sub
  35. holiday_refresh($;$)
  36. {
  37. my ($name, $fordate) = (@_);
  38. my $hash = $defs{$name};
  39. my $internal;
  40. return if(!$hash); # Just deleted
  41. my $nt = gettimeofday();
  42. my @lt = localtime($nt);
  43. my @fd;
  44. if(!$fordate) {
  45. $internal = 1;
  46. $fordate = sprintf("%02d-%02d", $lt[4]+1, $lt[3]);
  47. @fd = @lt;
  48. } else {
  49. my ($m,$d) = split("-", $fordate);
  50. @fd = localtime(mktime(1,1,1,$d,$m-1,$lt[5],0,0,-1));
  51. }
  52. my $fname = $attr{global}{modpath} . "/FHEM/" . $hash->{NAME} . ".holiday";
  53. my ($err, @holidayfile) = FileRead($fname);
  54. return $err if($err);
  55. my @foundList;
  56. foreach my $l (@holidayfile) {
  57. next if($l =~ m/^\s*#/);
  58. next if($l =~ m/^\s*$/);
  59. my $found;
  60. if($l =~ m/^1/) { # Exact date: 1 MM-DD Holiday
  61. my @args = split(" +", $l, 3);
  62. if($args[1] eq $fordate) {
  63. $found = $args[2];
  64. }
  65. } elsif($l =~ m/^2/) { # Easter date: 2 +1 Ostermontag
  66. ###mh new code for easter sunday calc w.o. requirement for
  67. # DateTime::Event::Easter
  68. # replace $a1 with $1 !!!
  69. # split line from file into args '2 <offset from E-sunday> <tagname>'
  70. my @a = split(" ", $l, 3);
  71. # get month & day for E-sunday
  72. my ($Om,$Od) = western_easter(($lt[5]+1900));
  73. my $timex = mktime(0,0,12,$Od,$Om-1, $lt[5],0,0,-1); # gen timevalue
  74. $timex = $timex + $a[1]*86400; # add offset days
  75. my ($msecond, $mminute, $mhour,
  76. $mday, $mmonth, $myear, $mrest) = localtime($timex);
  77. $myear = $myear+1900;
  78. $mmonth = $mmonth+1;
  79. #Log 1, "$name: Ostern:".sprintf("%04d-%02d-%02d", $lt[5]+1900, $Om, $Od).
  80. # " Target:".sprintf("%04d-%02d-%02d", $myear, $mmonth, $mday);
  81. next if($mday != $fd[3] || $mmonth != $fd[4]+1);
  82. $found = $a[2];
  83. Log 4, "$name: Match day: $a[2]\n";
  84. } elsif($l =~ m/^3/) { # Relative date: 3 -1 Mon 03 Holiday
  85. my @a = split(" +", $l, 5);
  86. my %wd = ("Sun"=>0, "Mon"=>1, "Tue"=>2, "Wed"=>3,
  87. "Thu"=>4, "Fri"=>5, "Sat"=>6);
  88. my @md = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
  89. $md[1]=29 if(schaltjahr($fd[5]+1900) && $fd[4] == 1);
  90. my $wd = $wd{$a[2]};
  91. if(!defined($wd)) {
  92. Log 1, "Wrong timespec: $l";
  93. next;
  94. }
  95. next if($wd != $fd[6]); # Weekday
  96. next if($a[3] != ($fd[4]+1)); # Month
  97. if($a[1] > 0) { # N'th day from the start
  98. my $d = $fd[3] - ($a[1]-1)*7;
  99. next if($d < 1 || $d > 7);
  100. } elsif($a[1] < 0) { # N'th day from the end
  101. my $d = $fd[3] - ($a[1]+1)*7;
  102. my $md = $md[$fd[4]];
  103. next if($d > $md || $d < $md-6);
  104. }
  105. $found = $a[4];
  106. } elsif($l =~ m/^4/) { # Interval: 4 MM-DD MM-DD Holiday
  107. my @args = split(" +", $l, 4);
  108. if($args[1] le $fordate && $args[2] ge $fordate) {
  109. $found = $args[3];
  110. }
  111. } elsif($l =~ m/^5/) { # nth weekday since MM-DD / before MM-DD
  112. my @a = split(" +", $l, 6);
  113. # arguments: 5 <distance> <weekday> <month> <day> <name>
  114. my %wd = ("Sun"=>0, "Mon"=>1, "Tue"=>2, "Wed"=>3,
  115. "Thu"=>4, "Fri"=>5, "Sat"=>6);
  116. my $wd = $wd{$a[2]};
  117. if(!defined($wd)) {
  118. Log 1, "Wrong weekday spec: $l";
  119. next;
  120. }
  121. next if $wd != $fd[6]; # check wether weekday matches today
  122. my $yday=$fd[7];
  123. # create time object of target date - mktime counts months and their
  124. # days from 0 instead of 1, so subtract 1 from each
  125. my $tgt=mktime(0,0,1,$a[4]-1,$a[3]-1,$fd[5],0,0,-1);
  126. my $tgtmin=$tgt;
  127. my $tgtmax=$tgt;
  128. my $weeksecs=7*24*60*60; # 7 days, 24 hours, 60 minutes, 60seconds each
  129. my $cd=mktime(0,0,1,$fd[3],$fd[4],$fd[5],0,0,-1);
  130. if ( $a[1] =~ /^-([0-9])*$/ ) {
  131. $tgtmin -= $1*$weeksecs; # Minimum: target date minus $1 weeks
  132. $tgtmax = $tgtmin+$weeksecs; # Maximum: one week after minimum
  133. # needs to be lower than max and greater than or equal to min
  134. if( ($cd ge $tgtmin) && ( $cd lt $tgtmax) ) {
  135. $found=$a[5];
  136. }
  137. } elsif ( $a[1] =~ /^\+?([0-9])*$/ ) {
  138. $tgtmin += ($1-1)*$weeksecs; # Minimum: target date plus $1-1 weeks
  139. $tgtmax = $tgtmin+$weeksecs; # Maximum: one week after minimum
  140. # needs to be lower than or equal to max and greater min
  141. if( ($cd gt $tgtmin) && ( $cd le $tgtmax) ) {
  142. $found=$a[5];
  143. }
  144. } else {
  145. Log 1, "Wrong distance spec: $l";
  146. next;
  147. }
  148. }
  149. push @foundList, $found if($found);
  150. }
  151. push @foundList, "none" if(!int(@foundList));
  152. my $found = join(", ", @foundList);
  153. RemoveInternalTimer($name);
  154. $nt -= ($lt[2]*3600+$lt[1]*60+$lt[0]); # Midnight
  155. $nt += 86400 + 2; # Tomorrow
  156. $hash->{TRIGGERTIME} = $nt;
  157. InternalTimer($nt, "holiday_refresh", $name, 0);
  158. if($internal) {
  159. readingsBeginUpdate($hash);
  160. readingsBulkUpdate($hash, 'state', $found);
  161. readingsBulkUpdate($hash, 'yesterday', CommandGet(undef,"$name yesterday"));
  162. readingsBulkUpdate($hash, 'tomorrow', CommandGet(undef,"$name tomorrow"));
  163. readingsEndUpdate($hash,1);
  164. return undef;
  165. } else {
  166. return $found;
  167. }
  168. }
  169. sub
  170. holiday_Get($@)
  171. {
  172. my ($hash, @a) = @_;
  173. shift(@a) if($a[1] && $a[1] eq "MM-DD");
  174. return "argument is missing" if(int(@a) < 2);
  175. my $arg;
  176. if($a[1] =~ m/^[01]\d-[0-3]\d/) {
  177. $arg = $a[1];
  178. } elsif($a[1] =~ m/^(yesterday|today|tomorrow)$/) {
  179. my $t = time();
  180. $t += 86400 if($a[1] eq "tomorrow");
  181. $t -= 86400 if($a[1] eq "yesterday");
  182. my @a = localtime($t);
  183. $arg = sprintf("%02d-%02d", $a[4]+1, $a[3]);
  184. } elsif($a[1] eq "days") {
  185. my $t = time() + ($a[2] ? int($a[2]) : 0)*86400;
  186. my @a = localtime($t);
  187. $arg = sprintf("%02d-%02d", $a[4]+1, $a[3]);
  188. } else {
  189. return "unknown argument $a[1], choose one of ".
  190. "yesterday:noArg today:noArg tomorrow:noArg days:2,3,4,5,6,7 MM-DD";
  191. }
  192. return holiday_refresh($hash->{NAME}, $arg);
  193. }
  194. sub
  195. schaltjahr($)
  196. {
  197. my($jahr) = @_;
  198. return 0 if $jahr % 4; # 2009
  199. return 1 unless $jahr % 400; # 2000
  200. return 0 unless $jahr % 100; # 2100
  201. return 1; # 2012
  202. }
  203. ### mh sub western_easter copied from cpan Date::Time::Easter
  204. ### mh changes marked with # mh
  205. ### mh
  206. ### mh calling parameter is 4 digit year
  207. ### mh
  208. sub
  209. western_easter($)
  210. {
  211. my $year = shift;
  212. my $golden_number = $year % 19;
  213. #quasicentury is so named because its a century, only its
  214. # the number of full centuries rather than the current century
  215. my $quasicentury = int($year / 100);
  216. my $epact = ($quasicentury - int($quasicentury/4) -
  217. int(($quasicentury * 8 + 13)/25) + ($golden_number*19) + 15) % 30;
  218. my $interval = $epact - int($epact/28)*
  219. (1 - int(29/($epact+1)) * int((21 - $golden_number)/11) );
  220. my $weekday = ($year + int($year/4) + $interval +
  221. 2 - $quasicentury + int($quasicentury/4)) % 7;
  222. my $offset = $interval - $weekday;
  223. my $month = 3 + int(($offset+40)/44);
  224. my $day = $offset + 28 - 31* int($month/4);
  225. return $month, $day;
  226. }
  227. 1;
  228. =pod
  229. =item helper
  230. =item summary define holidays in a local file
  231. =item summary_DE Urlaubs-/Feiertagskalender aus einer lokalen Datei
  232. =begin html
  233. <a name="holiday"></a>
  234. <h3>holiday</h3>
  235. <ul>
  236. <a name="holidaydefine"></a>
  237. <b>Define</b>
  238. <ul>
  239. <code>define &lt;name&gt; holiday</code>
  240. <br><br>
  241. Define a set of holidays. The module will try to open the file
  242. &lt;name&gt;.holiday in the <a href="#modpath">modpath</a>/FHEM directory.
  243. If entries in the holiday file match the current day, then the STATE of
  244. this holiday instance displayed in the <a href="#list">list</a> command
  245. will be set to the corresponding values, else the state is set to the text
  246. none. Most probably you'll want to query this value in some perl script:
  247. see Value() in the <a href="#perl">perl</a> section or the global attribute
  248. <a href="#holiday2we"> holiday2we</a>.<br> The file will be reread once
  249. every night, to compute the value for the current day, and by each get
  250. command (see below).<br>
  251. <br>
  252. Holiday file definition:<br>
  253. The file may contain comments (beginning with #) or empty lines.
  254. Significant lines begin with a number (type) and contain some space
  255. separated words, depending on the type. The different types are:<br>
  256. <ul>
  257. <li>1<br>
  258. Exact date. Arguments: &lt;MM-DD&gt; &lt;holiday-name&gt;<br>
  259. Exampe: 1 12-24 Christmas
  260. </li>
  261. <li>2<br>
  262. Easter-dependent date. Arguments: &lt;day-offset&gt;
  263. &lt;holiday-name&gt;.
  264. The offset is counted from Easter-Sunday.
  265. <br>
  266. Exampe: 2 1 Easter-Monday<br>
  267. Sidenote: You can check the easter date with:
  268. fhem> { join("-", western_easter(2011)) }
  269. </li>
  270. <li>3<br>
  271. Month dependent date. Arguments: &lt;nth&gt; &lt;weekday&gt;
  272. &lt;month &lt;holiday-name&gt;.<br>
  273. Examples:<br>
  274. <ul>
  275. 3 1 Mon 05 First Monday In May<br>
  276. 3 2 Mon 05 Second Monday In May<br>
  277. 3 -1 Mon 05 Last Monday In May<br>
  278. 3 0 Mon 05 Each Monday In May<br>
  279. </ul>
  280. </li>
  281. <li>4<br>
  282. Interval. Arguments: &lt;MM-DD&gt; &lt;MM-DD&gt; &lt;holiday-name&gt;
  283. .<br>
  284. Note: An interval cannot contain the year-end.
  285. Example:<br>
  286. <ul>
  287. 4 06-01 06-30 Summer holiday<br>
  288. 4 12-20 01-10 Winter holiday # DOES NOT WORK.
  289. Use the following 2 lines instead:<br>
  290. 4 12-20 12-31 Winter holiday<br>
  291. 4 01-01 01-10 Winter holiday<br>
  292. </ul>
  293. </li>
  294. <li>5<br>
  295. Date relative, weekday fixed holiday. Arguments: &lt;nth&gt;
  296. &lt;weekday&gt; &lt;month&gt; &lt;day&gt; &lt; holiday-name&gt;<br>
  297. Note that while +0 or -0 as offsets are not forbidden, their behaviour
  298. is undefined in the sense that it might change without notice.<br>
  299. Examples:<br>
  300. <ul>
  301. 5 -1 Wed 11 23 Buss und Bettag (first Wednesday before Nov, 23rd)<br>
  302. 5 1 Mon 01 31 First Monday after Jan, 31st (1st Monday in February)<br>
  303. </ul>
  304. </li>
  305. </ul>
  306. See also he.holiday in the contrib directory for official holidays in the
  307. german country of Hessen, and by.holiday for the Bavarian definition.
  308. </ul>
  309. <br>
  310. <a name="holidayset"></a>
  311. <b>Set</b> <ul>N/A</ul><br>
  312. <a name="holidayget"></a>
  313. <b>Get</b>
  314. <ul>
  315. <code>get &lt;name&gt; &lt;MM-DD&gt;</code><br>
  316. <code>get &lt;name&gt; yesterday</code><br>
  317. <code>get &lt;name&gt; today</code><br>
  318. <code>get &lt;name&gt; tomorrow</code><br>
  319. <code>get &lt;name&gt; days <offset></code><br>
  320. <br><br>
  321. Return the holiday name of the specified date or the text none.
  322. <br><br>
  323. </ul>
  324. <br>
  325. <a name="holidayattr"></a>
  326. <b>Attributes</b><ul>N/A</ul><br>
  327. </ul>
  328. =end html
  329. =begin html_DE
  330. <a name="holiday"></a>
  331. <h3>holiday</h3>
  332. <ul>
  333. <a name="holidaydefine"></a>
  334. <b>Define</b>
  335. <ul>
  336. <code>define &lt;name&gt; holiday</code>
  337. <br><br>
  338. Definiert einen Satz mit Urlaubsinformationen. Das Modul versucht die Datei
  339. &lt;name&gt;.holiday im Pfad <a href="#modpath">modpath</a>/FHEM zu
  340. &ouml;ffnen.
  341. Wenn Eintr&auml;ge im der Datei auf den aktuellen Tag passen wird der STATE
  342. der Holiday-Instanz die im <a href="#list">list</a> Befehl angezeigt wird
  343. auf die entsprechenden Werte gesetzt. Andernfalls ist der STATE auf den
  344. Text "none" gesetzt.
  345. Meistens wird dieser Wert mit einem Perl Script abgefragt: siehe Value() im
  346. <a href="#perl">perl</a> Abschnitt oder im globalen Attribut <a
  347. href="#holiday2we"> holiday2we</a>.<br> Die Datei wird jede Nacht neu
  348. eingelesen um den Wert des aktuellen Tages zu erzeugen. Auch jeder "get"
  349. Befehl liest die Datei neu ein.
  350. <br><br>
  351. Holiday file Definition:<br>
  352. Die Datei darf Kommentare, beginnend mit #, und Leerzeilen enthalten. Die
  353. entscheidenden Zeilen beginnen mit einer Zahl (Typ) und enthalten durch
  354. Leerzeichen getrennte W&ouml;rter, je nach Typ. Die verschiedenen Typen
  355. sind:<br>
  356. <ul>
  357. <li>1<br>
  358. Genaues Datum. Argument: &lt;MM-TT&gt; &lt;Feiertag-Name&gt;<br>
  359. Beispiel: 1 12-24 Weihnachten
  360. </li>
  361. <li>2<br>
  362. Oster-abh&auml;ngiges Datum. Argument: &lt;Tag-Offset&gt;
  363. &lt;Feiertag-Name&gt;.
  364. Der Offset wird vom Oster-Sonntag an gez&auml;hlt.
  365. <br>
  366. Beispiel: 2 1 Oster-Montag<br>
  367. Hinweis: Das Osterdatum kann vorher gepr&uuml;ft werden:
  368. fhem> { join("-", western_easter(2011)) }
  369. </li>
  370. <li>3<br>
  371. Monats-abh&auml;ngiges Datum. Argument: &lt;X&gt; &lt;Wochentag&gt;
  372. &lt;Monat&gt; &lt;Feiertag-Name&gt;.<br>
  373. Beispiel:<br>
  374. <ul>
  375. 3 1 Mon 05 Erster Montag In Mai<br>
  376. 3 2 Mon 05 Zweiter Montag In Mai<br>
  377. 3 -1 Mon 05 Letzter Montag In Mai<br>
  378. 3 0 Mon 05 Jeder Montag In Mai<br>
  379. </ul>
  380. </li>
  381. <li>4<br>
  382. Intervall. Argument: &lt;MM-TT&gt; &lt;MM-TT&gt; &lt;Feiertag-Name&gt;
  383. .<br>
  384. Achtung: Ein Intervall darf kein Jahresende enthalten.
  385. Beispiel:<br>
  386. <ul>
  387. 4 06-01 06-30 Sommerferien<br>
  388. 4 12-20 01-10 Winterferien # FUNKTIONIER NICHT,
  389. stattdessen folgendes verwenden:<br>
  390. 4 12-20 12-31 Winterferien<br>
  391. 4 01-01 01-10 Winterferien<br>
  392. </ul>
  393. </li>
  394. <li>5<br>
  395. Datum relativ, Wochentags ein fester Urlaubstag/Feiertag. Argument:
  396. &lt;X&gt; &lt;Wochentag&gt; &lt;Monat&gt; &lt;Tag&gt;
  397. &lt;Feiertag-Name&gt;<br> Hinweis: Da +0 oder -0 als Offset nicht
  398. verboten sind, ist das Verhalten hier nicht definiert, kann sich also
  399. ohne Info &auml;ndern;<br>
  400. Beispiel:<br>
  401. <ul>
  402. 5 -1 Wed 11 23 Buss und Bettag (erster Mittwoch vor dem 23. Nov)<br>
  403. 5 1 Mon 01 31 Erster Montag in Februar<br>
  404. </ul>
  405. </li>
  406. </ul>
  407. Siehe auch he.holiday im contrib Verzeichnis f&uuml;r offizielle Feiertage
  408. in den deutschen Bundesl&auml;ndern Hessen und by.holiday f&uuml;r Bayern.
  409. </ul>
  410. <br>
  411. <a name="holidayset"></a>
  412. <b>Set</b> <ul>N/A</ul><br>
  413. <a name="holidayget"></a>
  414. <b>Get</b>
  415. <ul>
  416. <code>get &lt;name&gt; &lt;MM-DD&gt;</code><br>
  417. <code>get &lt;name&gt; yesterday</code><br>
  418. <code>get &lt;name&gt; today</code><br>
  419. <code>get &lt;name&gt; tomorrow</code><br>
  420. <code>get &lt;name&gt; days <offset></code><br>
  421. <br><br>
  422. Gibt den Name des Feiertages zum angebenenen Datum zur&uuml;ck oder den
  423. Text none.
  424. <br><br>
  425. </ul>
  426. <br>
  427. <a name="holidayattr"></a>
  428. <b>Attributes</b><ul>
  429. <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
  430. </ul><br>
  431. </ul>
  432. =end html_DE
  433. =cut