####################################################################### # $Id$ package main; use strict; use warnings; sub holiday_refresh($;$$); ##################################### sub holiday_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "holiday_Define"; $hash->{GetFn} = "holiday_Get"; $hash->{SetFn} = "holiday_Set"; $hash->{UndefFn} = "holiday_Undef"; $hash->{AttrList} = $readingFnAttributes; $hash->{FW_detailFn} = "holiday_FW_detailFn"; } ##################################### sub holiday_Define($$) { my ($hash, $def) = @_; return holiday_refresh($hash->{NAME}, undef, 1) if($init_done); InternalTimer(gettimeofday()+1, "holiday_refresh", $hash->{NAME}, 0); return undef; } sub holiday_Undef($$) { my ($hash, $name) = @_; RemoveInternalTimer($name); return undef; } sub holiday_refresh($;$$) { my ($name, $fordate, $showAvailable) = (@_); my $hash = $defs{$name}; my $fromTimer=0; my $foryeardate; return if(!$hash); # Just deleted my $nt = gettimeofday(); my @lt = localtime($nt); my @fd; if(!$fordate) { $fromTimer = 1; $fordate = sprintf("%02d-%02d", $lt[4]+1, $lt[3]); $foryeardate = sprintf("%4d-%02d-%02d",$lt[5]+1900, $lt[4]+1, $lt[3]); @fd = @lt; } else { $fordate =~ m/^((\d{4})-)?([01]\d)-([0-3]\d)$/; # fmt is already checked my ($y,$m,$d) = ($2, $3,$4); $fordate = "$m-$d"; $lt[5] = $y-1900 if($y); $foryeardate = $y ? "$y-$m-$d" : sprintf("%4d-%02d-%02d",$lt[5]+1900,$m,$d); @fd = localtime(mktime(1,1,1,$d,$m-1,$lt[5],0,0,-1)); } Log3 $name, 5, "holiday_refresh $name called for $fordate/$foryeardate ($fromTimer)"; my $dir = $attr{global}{modpath} . "/FHEM"; my ($err, @holidayfile) = FileRead("$dir/$name.holiday"); if($err) { $dir = $attr{global}{modpath}."/FHEM/holiday"; ($err, @holidayfile) = FileRead("$dir/$name.holiday"); $hash->{READONLY} = 1; } else { $hash->{READONLY} = 0; } if($err) { if($showAvailable) { my @ret; if(configDBUsed()) { @ret = cfgDB_FW_fileList($dir,".*.holiday",@ret); map { s/\.configDB$//;$_ } @ret; } else { if(opendir(DH, $dir)) { @ret = grep { m/\.holiday$/ } readdir(DH); closedir(DH); } } $err .= "\nAvailable holiday files: ". join(" ", map { s/.holiday//;$_ } @ret); } else { Log 1, "$name: $err"; } return $err; } $hash->{HOLIDAYFILE} = "$dir/$name.holiday"; my @foundList; my %foundHash; foreach my $l (@holidayfile) { next if($l =~ m/^\s*#/); next if($l =~ m/^\s*$/); my $found; if($l =~ m/^1/) { # Exact date: 1 MM-DD Holiday (MM-DD-YYYY, Forum #93277) my @args = split(" ", $l, 3); if(@args == 3 && ($args[1] eq $fordate || $args[1] eq $foryeardate)) { $found = $args[2]; } } elsif($l =~ m/^2/) { # Easter date: 2 +1 Ostermontag ###mh new code for easter sunday calc w.o. requirement for # DateTime::Event::Easter # replace $a1 with $1 !!! # split line from file into args '2 ' my @a = split(" ", $l, 3); # get month & day for E-sunday my ($Om,$Od) = western_easter(($lt[5]+1900)); my $timex = mktime(0,0,12,$Od,$Om-1, $lt[5],0,0,-1); # gen timevalue if(!defined $timex) { my $dt = sprintf("%04d-%02d-%02d", $lt[5]+1900, $Om, $Od); Log 1, "holiday/$name: mktime failed for $dt"; return "Cannot process $dt"; } $timex = $timex + $a[1]*86400; # add offset days my ($msecond, $mminute, $mhour, $mday, $mmonth, $myear, $mrest) = localtime($timex); $myear = $myear+1900; $mmonth = $mmonth+1; #Log 1,"$name:Eastern:".sprintf("%04d-%02d-%02d", $lt[5]+1900, $Om, $Od). # " Target:".sprintf("%04d-%02d-%02d", $myear, $mmonth, $mday); next if($mday != $fd[3] || $mmonth != $fd[4]+1); $found = $a[2]; Log 4, "$name: Match day: $a[2]\n"; } elsif($l =~ m/^3/) { # Relative date: 3 -1 Mon 03 Holiday my @a = split(" ", $l, 5); my %wd = ("Sun"=>0, "Mon"=>1, "Tue"=>2, "Wed"=>3, "Thu"=>4, "Fri"=>5, "Sat"=>6); my @md = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); $md[1]=29 if(schaltjahr($fd[5]+1900) && $fd[4] == 1); my $wd = $wd{$a[2]}; if(!defined($wd)) { Log 1, "Wrong timespec: $l"; next; } next if($wd != $fd[6]); # Weekday next if($a[3] != ($fd[4]+1)); # Month if($a[1] > 0) { # N'th day from the start my $d = $fd[3] - ($a[1]-1)*7; next if($d < 1 || $d > 7); } elsif($a[1] < 0) { # N'th day from the end my $d = $fd[3] - ($a[1]+1)*7; my $md = $md[$fd[4]]; next if($d > $md || $d < $md-6); } $found = $a[4]; } elsif($l =~ m/^4/) { # Interval: 4 MM-DD MM-DD Holiday my @args = split(" ", $l, 4); if(@args == 4 && (($args[1] le $fordate && $args[2] ge $fordate) || ($args[1] le $foryeardate && $args[2] ge $foryeardate))) { $found = $args[3]; } } elsif($l =~ m/^5/) { # nth weekday since MM-DD / before MM-DD my @a = split(" ", $l, 6); # arguments: 5 my %wd = ("Sun"=>0, "Mon"=>1, "Tue"=>2, "Wed"=>3, "Thu"=>4, "Fri"=>5, "Sat"=>6); my $wd = $wd{$a[2]}; if(!defined($wd)) { Log 1, "Wrong weekday spec: $l"; next; } next if $wd != $fd[6]; # check wether weekday matches today my $yday=$fd[7]; # create time object of target date - mktime counts months and their # days from 0 instead of 1, so subtract 1 from each my $tgt=mktime(0,0,1,$a[4],$a[3]-1,$fd[5],0,0,-1); my $tgtmin=$tgt; my $tgtmax=$tgt; my $weeksecs=7*24*60*60; # 7 days, 24 hours, 60 minutes, 60seconds each my $cd=mktime(0,0,1,$fd[3],$fd[4],$fd[5],0,0,-1); if ( $a[1] =~ /^-([0-9])*$/ ) { $tgtmin -= $1*$weeksecs; # Minimum: target date minus $1 weeks $tgtmax = $tgtmin+$weeksecs; # Maximum: one week after minimum # needs to be lower than max and greater than or equal to min if( ($cd ge $tgtmin) && ( $cd lt $tgtmax) ) { $found=$a[5]; } } elsif ( $a[1] =~ /^\+?([0-9])*$/ ) { $tgtmin += ($1-1)*$weeksecs; # Minimum: target date plus $1-1 weeks $tgtmax = $tgtmin+$weeksecs; # Maximum: one week after minimum # needs to be lower than or equal to max and greater min if( ($cd gt $tgtmin) && ( $cd le $tgtmax) ) { $found=$a[5]; } } else { Log 1, "Wrong distance spec: $l"; next; } } elsif($l =~ m/^6/) { # own calculation my @args = split(" ", $l, 4); my $res = "?"; no strict "refs"; eval { $res = &{$args[1]}($args[2]); }; use strict "refs"; if($@) { Log 1, "holiday: Error in own function: $@"; next; } if($res eq $fordate) { $found = $args[3]; } } if($found && !$foundHash{$found}) { push @foundList, $found; $foundHash{$found} = 1; } } push @foundList, "none" if(!int(@foundList)); my $found = join(", ", @foundList); if($fromTimer) { RemoveInternalTimer($name); $nt -= ($lt[2]*3600+$lt[1]*60+$lt[0]); # Midnight $nt += 86400 + 2; # Tomorrow $hash->{TRIGGERTIME} = $nt; InternalTimer($nt, "holiday_refresh", $name, 0); readingsBeginUpdate($hash); readingsBulkUpdate($hash, 'state', $found); readingsBulkUpdate($hash, 'today', $found); readingsBulkUpdate($hash, 'yesterday', CommandGet(undef,"$name yesterday")); readingsBulkUpdate($hash, 'tomorrow', CommandGet(undef,"$name tomorrow")); readingsEndUpdate($hash,1); return undef; } else { return $found; } } sub holiday_Set($@) { my ($hash, @a) = @_; my %sets = ( createPrivateCopy => $hash->{READONLY}, deletePrivateCopy => !$hash->{READONLY}, reload => 1 ); return "unknown argument $a[1], choose one of ". join(" ", map { "$_:noArg" } grep { $sets{$_} } keys %sets) if(!$sets{$a[1]}); if($a[1] eq "createPrivateCopy") { return "Already a private version" if(!$hash->{READONLY}); my $fname = $attr{global}{modpath}."/FHEM/holiday/$hash->{NAME}.holiday"; my ($err, @holidayfile) = FileRead($fname); return $err if($err); $fname = $attr{global}{modpath}."/FHEM/$hash->{NAME}.holiday"; $err = FileWrite($fname, @holidayfile); return $err if($err); holiday_refresh($hash->{NAME}); } elsif($a[1] eq "deletePrivateCopy") { return "Not a private version" if($hash->{READONLY}); my $err = FileDelete($attr{global}{modpath}."/FHEM/$hash->{NAME}.holiday"); return $err if($err); holiday_refresh($hash->{NAME}); } elsif($a[1] eq "reload") { holiday_refresh($hash->{NAME}); } return undef; } sub holiday_Get($@) { my ($hash, @a) = @_; shift(@a) if($a[1] && ($a[1] eq "MM-DD" || $a[1] eq "YYYY-MM-DD")); return "argument is missing" if(int(@a) < 2); my $arg; if($a[1] =~ m/^(\d{4}-)?[01]\d-[0-3]\d/) { $arg = $a[1]; } elsif($a[1] =~ m/^(yesterday|today|tomorrow)$/) { my $t = time(); if($a[1] =~ m/^(yesterday|tomorrow)$/) { # clock change issues, #123808 my $inc = ($a[1] eq "tomorrow" ? 3600 : -3600); my @now = localtime($t); for(;;) { $t += $inc; my @then = localtime($t); last if($then[3] != $now[3]); } } my @a = localtime($t); $arg = sprintf("%04d-%02d-%02d", $a[5]+1900, $a[4]+1, $a[3]); } elsif($a[1] eq "days") { my $t = time() + ($a[2] ? int($a[2]) : 0)*86400; my @a = localtime($t); $arg = sprintf("%04d-%02d-%02d", $a[5]+1900, $a[4]+1, $a[3]); } else { return "unknown argument $a[1], choose one of ". "yesterday:noArg today:noArg tomorrow:noArg ". "days:2,3,4,5,6,7 MM-DD YYYY-MM-DD"; } return holiday_refresh($hash->{NAME}, $arg); } sub schaltjahr($) { my($jahr) = @_; return 0 if $jahr % 4; # 2009 return 1 unless $jahr % 400; # 2000 return 0 unless $jahr % 100; # 2100 return 1; # 2012 } ### mh sub western_easter copied from cpan Date::Time::Easter ### mh changes marked with # mh ### mh ### mh calling parameter is 4 digit year ### mh sub western_easter($) { my $year = shift; my $golden_number = $year % 19; #quasicentury is so named because its a century, only its # the number of full centuries rather than the current century my $quasicentury = int($year / 100); my $epact = ($quasicentury - int($quasicentury/4) - int(($quasicentury * 8 + 13)/25) + ($golden_number*19) + 15) % 30; my $interval = $epact - int($epact/28)* (1 - int(29/($epact+1)) * int((21 - $golden_number)/11) ); my $weekday = ($year + int($year/4) + $interval + 2 - $quasicentury + int($quasicentury/4)) % 7; my $offset = $interval - $weekday; my $month = 3 + int(($offset+40)/44); my $day = $offset + 28 - 31* int($month/4); return $month, $day; } sub holiday_FW_detailFn($$$$) { my ($FW_wname, $d, $room, $pageHash) = @_; # pageHash is set for summaryFn. return "" if($defs{$d}{READONLY}); my $cfgDB = (configDBUsed() ? "configDB" : ""); return FW_pH("cmd=style edit $d.holiday $cfgDB", "
Edit $d.holiday
", 0, "dval", 1); } 1; =pod =item summary define holidays in a local file =item summary_DE Urlaubs-/Feiertagskalender aus einer lokalen Datei =begin html

holiday

    Define
      define <name> holiday

      Define a set of holidays. The module will try to open the file <name>.holiday in the modpath/FHEM directory first, then in the modpath/FHEM/holiday directory, the latter containing a set of predefined files. This list of available holiday files will be shown if an error occurs at the time of the definition, e.g. if you type "define help holiday"
      If entries in the holiday file match the current day, then the STATE of this holiday instance displayed in the list command will be set to the corresponding values, else the state is set to the text none. Most probably you'll want to query this value in some perl script: see Value() in the perl section or the global attribute holiday2we.
      Note: since March 2019 the IsWe() function (and $we) are accessing the state, tomorrow and yesterday readings, and not the STATE internal.
      The file will be reread once every night, to compute the value for the current day, and by each get command (see below).

      Holiday file definition:
      The file may contain comments (beginning with #) or empty lines. Significant lines begin with a number (type) and contain some space separated words, depending on the type. The different types are:
      • 1
        Exact date. Arguments: <MM-DD> <holiday-name>
        Exampe: 1 12-24 Christmas
        MM-DD can also be written as YYYY-MM-DD.
      • 2
        Easter-dependent date. Arguments: <day-offset> <holiday-name>. The offset is counted from Easter-Sunday.
        Exampe: 2 1 Easter-Monday
        Sidenote: You can check the easter date with: fhem> { join("-", western_easter(2011)) }
      • 3
        Month dependent date. Arguments: <nth> <weekday> <month <holiday-name>.
        Examples:
          3 1 Mon 05 First Monday In May
          3 2 Mon 05 Second Monday In May
          3 -1 Mon 05 Last Monday In May
          3 0 Mon 05 Each Monday In May
      • 4
        Interval. Arguments: <MM-DD> <MM-DD> <holiday-name> .
        Note: An interval cannot contain the year-end. Example:
          4 06-01 06-30 Summer holiday
          4 12-20 01-10 Winter holiday # DOES NOT WORK. Use the following 2 lines instead:
          4 12-20 12-31 Winter holiday
          4 01-01 01-10 Winter holiday
        MM-DD can also be written as YYYY-MM-DD.
      • 5
        Date relative, weekday fixed holiday. Arguments: <nth> <weekday> <month> <day> < holiday-name>
        Note that while +0 or -0 as offsets are not forbidden, their behaviour is undefined in the sense that it might change without notice.
        Examples:
          5 -1 Wed 11 23 Buss und Bettag (first Wednesday before Nov, 23rd)
          5 1 Mon 01 31 First Monday after Jan, 31st (1st Monday in February)
      • 6
        Use a perl function to calculate a date. Function must return the result as an exact date in format "mm-dd", e.g. "12-02"

        Example:
          6 calcAdvent 21 1.Advent
          6 calcAdvent 14 2.Advent
          6 calcAdvent 7 3.Advent
          6 calcAdvent 0 4.Advent

        Explanation:
          calcAdvent = name of function, e.g. included in 99_myUtils.pm
          21 = parameter given to function
          1.Advent = text to be shown in readings

        this will call "calcAdvent(21)" to calculate a date.
        Errors will be logged in Loglevel 1.

    Set
    • createPrivateCopy
        if the holiday file is opened from the FHEM/holiday directory (which is refreshed by FHEM-update), then it is readonly, and should not be modified. With createPrivateCopy the file will be copied to the FHEM directory, where it can be modified.
    • deletePrivateCopy
        delete the private copy, see createPrivateCopy above
    • reload
        set the state, tomorrow and yesterday readings. Useful after manually editing the file.

    Get
      get <name> <YYYY-MM-DD>
      get <name> <MM-DD>
      get <name> yesterday
      get <name> today
      get <name> tomorrow
      get <name> days <offset>


      Return the holiday name of the specified date or the text none.


    Attributes
      N/A

=end html =begin html_DE

holiday

    Define
      define <name> holiday

      Definiert einen Satz mit Urlaubsinformationen. Das Modul versucht die Datei <name>.holiday erst in modpath/FHEM zu öffnen, und dann in modpath/FHEM/holiday, Letzteres enthält eine Liste von per FHEM-update verteilten Dateien für diverse (Bundes-)Länder. Diese Liste wird bei einer Fehlermeldung angezeigt. Wenn Einträge im der Datei auf den aktuellen Tag passen wird der STATE der Holiday-Instanz die im list Befehl angezeigt wird auf die entsprechenden Werte gesetzt. Andernfalls ist der STATE auf den Text "none" gesetzt. Meistens wird dieser Wert mit einem Perl Script abgefragt: siehe Value() im perl Abschnitt oder im globalen Attribut holiday2we.
      Achtung: Seit März 2019 verwendet die IsWe() Funktion (und $we) die state, tomorrow und yesterday Readings, und nicht mehr das STATE Internal.
      Die Datei wird jede Nacht neu eingelesen um den Wert des aktuellen Tages zu erzeugen. Auch jeder "get" Befehl liest die Datei neu ein.


      Holiday file Definition:
      Die Datei darf Kommentare, beginnend mit #, und Leerzeilen enthalten. Die entscheidenden Zeilen beginnen mit einer Zahl (Typ) und enthalten durch Leerzeichen getrennte Wörter, je nach Typ. Die verschiedenen Typen sind:
      • 1
        Genaues Datum. Argument: <MM-TT> <Feiertag-Name>
        Beispiel: 1 12-24 Weihnachten
        MM-TT kann auch als JJJJ-MM-TT geschrieben werden.
      • 2
        Oster-abhängiges Datum. Argument: <Tag-Offset> <Feiertag-Name>. Der Offset wird vom Oster-Sonntag an gezählt.
        Beispiel: 2 1 Oster-Montag
        Hinweis: Das Osterdatum kann vorher geprüft werden: fhem> { join("-", western_easter(2011)) }
      • 3
        Monats-abhängiges Datum. Argument: <X> <Wochentag> <Monat> <Feiertag-Name>.
        Beispiel:
          3 1 Mon 05 Erster Montag In Mai
          3 2 Mon 05 Zweiter Montag In Mai
          3 -1 Mon 05 Letzter Montag In Mai
          3 0 Mon 05 Jeder Montag In Mai
      • 4
        Intervall. Argument: <MM-TT> <MM-TT> <Feiertag-Name> .
        Achtung: Ein Intervall darf kein Jahresende enthalten. Beispiel:
          4 06-01 06-30 Sommerferien
          4 12-20 01-10 Winterferien # FUNKTIONIER NICHT, stattdessen folgendes verwenden:
          4 12-20 12-31 Winterferien
          4 01-01 01-10 Winterferien
        MM-TT kann auch als JJJJ-MM-TT geschrieben werden.
      • 5
        Datum relativ, Wochentags ein fester Urlaubstag/Feiertag. Argument: <X> <Wochentag> <Monat> <Tag> <Feiertag-Name>
        Hinweis: Da +0 oder -0 als Offset nicht verboten sind, ist das Verhalten hier nicht definiert, kann sich also ohne Info ändern;
        Beispiel:
          5 -1 Wed 11 23 Buss und Bettag (erster Mittwoch vor dem 23. Nov)
          5 1 Mon 01 31 Erster Montag in Februar
      • 6
        Datum mit einer perl Funktion berechnen. Das Ergebnis muss ein exaktes Datum im Format "mm-dd" sein, z.B."12-02"

        Beispiel:
          6 calcAdvent 21 1.Advent
          6 calcAdvent 14 2.Advent
          6 calcAdvent 7 3.Advent
          6 calcAdvent 0 4.Advent

        Erklärung:
          calcAdvent = Name der Funktion, z.B. enthalten in 99_myUtils.pm
          21 = Parameter zum Funktionsaufruf
          1.Advent = Text für die Anzeige in readings

        erzeugt einen Funktionsaufruf "calcAdvent(21)" zur Berechnung eines Datums.
        Fehler werden im Loglevel 1 protokolliert.

    Set
    • createPrivateCopy
        Falls die Datei in der FHEM/holiday Verzeichnis geöffnet wurde, dann ist sie nicht beschreibbar, da dieses Verzeichnis mit FHEM update aktualisiert wird. Mit createPrivateCopy kann eine private Kopie im FHEM Verzeichnis erstellt werden.
    • deletePrivateCopy
        Entfernt die private Kopie, siehe auch createPrivateCopy
    • reload
        setzt die state, tomorrow und yesterday Readings. Wird nach einem manuellen Bearbeiten der .holiday Datei benötigt.

    Get
      get <name> <YYYY-MM-DD>
      get <name> <MM-DD>
      get <name> yesterday
      get <name> today
      get <name> tomorrow
      get <name> days <offset>


      Gibt den Name des Feiertages zum angebenenen Datum zurück oder den Text none.


    Attributes
=end html_DE =cut