From 4f900cfa5da96738459ee59461764cb08ef267f8 Mon Sep 17 00:00:00 2001 From: borisneubert <> Date: Sat, 6 Feb 2016 08:48:07 +0000 Subject: [PATCH] 57_Calendar: complete rewrite, see http://forum.fhem.de/index.php/topic,48315.0.html git-svn-id: https://svn.fhem.de/fhem/trunk@10732 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 2 + fhem/FHEM/57_Calendar.pm | 3450 +++++++++++++++++++++++++++----------- fhem/FHEM/66_ECMD.pm | 20 + 3 files changed, 2529 insertions(+), 943 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index b24236b1f..0a49feace 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,7 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - change: 57_Calendar: complete rewrite + see http://forum.fhem.de/index.php/topic,48315.0.html - feature: new events for home automation buttons and unassigned buttons - feature: 10_KOPP_FC: added blinds and switches - updated: 74_AMAD: New Mijor Release 1.2.0 diff --git a/fhem/FHEM/57_Calendar.pm b/fhem/FHEM/57_Calendar.pm index 2160b1af7..f1c3611e9 100644 --- a/fhem/FHEM/57_Calendar.pm +++ b/fhem/FHEM/57_Calendar.pm @@ -37,29 +37,607 @@ no if $] >= 5.017011, warnings => 'experimental::smartmatch'; # *** Potential isses: # # There might be issues when turning to daylight saving time and back that -# need further investigation. For reference please see +# need further investigation. For counterpart please see # http://forum.fhem.de/index.php?topic=18707 # http://forum.fhem.de/index.php?topic=15827 # -# modeStarted triggered on calendar update -# http://forum.fhem.de/index.php?topic=28516 -# -# take care of unitialized value warnings -# http://forum.fhem.de/index.php?topic=28409 -# # *** Potential future extensions: # -# add support for EXDATE -# http://forum.fhem.de/index.php?topic=24485 -# -# nonblocking retrieval -# http://forum.fhem.de/index.php?topic=29622 -# # sequence of events fired sorted by time # http://forum.fhem.de/index.php?topic=29112 # # document ownCloud ical use # http://forum.fhem.de/index.php?topic=28667 +# +# high load when parsing +# http://forum.fhem.de/index.php/topic,40783.0.html + + +=for comment + +RFC +--- + +https://tools.ietf.org/html/rfc5545 + + +Data structures +--------------- + +We call a set of calendar events (short: events) a series, even for sets +consisting only of a single event. A series may consist of only one single +event, a series of regularly reccuring events and reccuring events with +exceptions. A series is identified by a UID. + + +*** VEVENT record, class ICal::Entry + +In the iCalendar, a series is represented by one or more VEVENT records. + +The unique key for a VEVENT record is UID, RECURRENCE-ID (3.8.4.4, p. 112) and +SEQUENCE (3.8.7.4, p. 138). + +The internal primary key for a VEVENT is ID. + +FHEM keeps a set of VEVENT records (record set). When the calendar is updated, +a new record set is retrieved from the iCalendar and updates the old record set +to form the resultant record set. + +A record in the resultant record set can be in exactly one of these states: +- deleted: + a record from the old record set for which no record with the same + (UID, RECURRENCE-ID) was in the new record set. +- new: + a record from the new record set for which no record with same + (UID, RECURRENCE-ID) was in the old record set. +- changed-old: + a record from the old record set for which a record with the same + (UID, RECURRENCE-ID) but different SEQUENCE was in the new record + set. +- changed-new: + a record from the new record set for which a record with the same + (UID, RECURRENCE-ID) but different SEQUENCE was in the old record + set. +- known: + a record with this (UID, RECURRENCE-ID, SEQUENCE) was both in the + old and in the new record set and both records have the same + LAST-MODIFIED. The record from the old record set was + kept and the record from the new record set was discarded. +- modified-new: + a record with this (UID, RECURRENCE-ID, SEQUENCE) was both in the + old and in the new record set and both records differ in + LAST-MODIFIED. This is the record from the new record set. +- modified-old: + a record with this (UID, RECURRENCE-ID, SEQUENCE) was both in the + old and in the new record set and both records differ in + LAST-MODIFIED. This is the record from the old record set. + +Records in states modified-old and changed-old refer to the corresponding records +in states modified-new and change-new, and vice versa. + +Records in state deleted, modified-old or changed-old are removed upon +the next update. They are said to be "obsolete". + +A record is said to be "recurring" if it has a RRULE property. + +A record is said to be an "exception" if it has a RECURRENCE-ID property. + +Each records has a set of events attached. + + + +*** calendar event, class Calendar::Event + +Events are attached to single records (VEVENTs). + +The uid of the event is the UID of the record with all non-alphanumerical +characters removed. + +At a given point in time t, an event is in exactly one of these modes: +- upcoming: + the start time of the event is in the future +- alarm: + alarm time <= t < start time for any of the alarms for the event +- start: + start time <= t <= end time of the event +- end: + end time < t + +An event is said to be "changed", when its mode has changed during the most +recent run of calendar event processing. + +An event is said to be "hidden", when +- it was in mode end and end time of the event < t - horizonPast, or +- it was in mode upcoming and start time of the event > t + horizonFuture +at the most recent run of calendar event processing. horizonPast defaults to 0, +horizonFuture defaults to 366 days. + + + +Processing of iCalender +----------------------- + +*** Initial situation: +We have an old record set of VEVENTs. It is empty on a restart of FHEM or upon +issueing the "set ... reload" command. + +*** Step 1: Retrieval of new record set (Calendar_GetUpdate) +1) The iCalendar is downloaded from its location into FHEM memory. +2) It is parsed into a new record set of VEVENTs. + +*** Step 2: Update of internal record set (Calendar_UpdateCalendar) +1) All records in the old record set that are in state deleted or obsolete are +removed. +2) All states of all records in the old record set are set to blank. +3) The old and new record sets are merged to create a resultant record set +according to the following procedure: + +If the new record set contains a record with the same (UID, RECURRENCE-ID, +SEQUENCE) as a record in the old record set: + - if the two records differ in LAST-MODIFIED, then both records + are kept. The state of the record from the old record set is set to + modified-old, the state of the record from the new record set is set to + modified-new. + - else the record from the old record set is kept, state set to known, and the + record from the new record set is discarded. + +If the new record set contains a record with the same (UID, RECURRENCE-ID) but + different SEQUENCE as a record in the old record set, then both records are + kept. The state of the record from the new record set is set to changed-new, + and the state of record from the old record set is set to changed-old. + +If the new record set contains a record that differs from any record in the old +record set by both UID and RECURRENCE-ID, the record from the new record set +id added to the resultant record set and its state is set to new. + +4) The state of all records in the old record set that have not been touched +in 3) are set to deleted. + +Notes: +- This procedure favors records from the new record set over records from the + old record set, even if the SEQUENCE is lower or LAST-MODIFIED is earlier. +- DTSTAMP is the time stamp of the creation of the iCalendar entry. For Google + Calendar it is the time stamp of the latest retrieval of the calendar. + + +*** Step 3: Update of calendar events (Calendar_UpdateCalendar) +We walk over all records and treat the corresponding events according to +the state of the record: + +- deleted, changed-old, modified-old: + all events are removed +- new, changed-new, modified-new: + all events are removed and events are created anew +- known: + all events are left alone + +No events older than 400 days or more than 400 days in the future will be +created. + +Creation of events in a series works as follows: + +If we have several events in a series, the main series has the RRULE tag and +the exceptions have RECURRENCE-IDs. The RECURRENCE-ID match the start +dates in the series created from the RRULE that need to be exempted. We +therefore collect all events from records with same UID and RECURRENCE-ID set +as they form the list of records with the exceptions for the UID. + +Starting with the start date of the series, one event is created after the +other. Creation stops when the series ends or when an event more than 400 days +in the future has been created. If the event is in the list of exceptions +(either defined by other events with same UID and a RECURRENCE-ID or by the +EXDATE property), it is not added. + +What attributes are recognized and which of these are honored or ignored? + +The following frequencies (FREQ) are recognized and honored: +SECONDLY +MINUTELY +HOURLY +DAILY +WEEKLY + BYDAY: recognizes and honors one or several weekdays but not with prefix (e.g. -1SU, 2MO) +MONTHLY + BYMONTHDAY: recognized but ignored + BYMONTH: recognized but ignored +YEARLY + +For all of the above: + INTERVAL: recognized and honored + UNTIL: recognized and honored + COUNT: recognized and honored + WKST: recognized but ignored + EXDATE: recognized and honored + + +*** Step 4: The device readings related to updates are set + +- calname +- lastUpdate +- nextUpdate +- nextWakeup +- state + + +Note: the state... readings from the previous version of this module (2015 and +earlier) are not available any more. + + + +Processing of calendar events +----------------------------- +Calendar_CheckTimes + +In case of a series of calendar events, several calendar events may exist for +the same uid which may be in different modes. Therefore only the most +interesting mode is chosen over any other mode of any calendar event with +the same uid. The most interesting mode is the first applicable from the +following list: +- start +- alarm +- upcoming +- end + +Apart from these actual modes, virtual modes apply: +- changed: the actual mode has changed during this call of Calendar_CheckTimes +- alarmed: modes are alarm and changed +- started: modes are start and changed +- ended: modes are end and changed +- alarm or start: mode is alarm or start + +If the mode has changed to , the following FHEM events are created: +changed uid + uid + +Note: there is no colon in these FHEM events. + + +Program flow +------------ + +Calendar_Initialize sets the Calendar_Notify to watch for notifications. +Calendar_Notify acts on the INITIALIZED and REREADCFG events by starting the + timer to call Calendar_Wakeup between 10 and 29 seconds after the + notification. +Calendar_Wakeup starts a processing run. + It sets the current time t as baseline for process. + If the time for the next update has been reached, + Calendar_GetUpdate is called + else + Calendar_CheckTimes + Calendar_RearmTimer + are called. +Calendar_GetUpdate retrieves the iCal file. If the source is url, this is + done asynchronously. Upon successfull retrieval of the iCal file, we + continue with Calendar_ProcessUpdate. +Calendar_ProcessUpdate calls + Calendar_UpdateCalendar + Calendar_CheckTimes + Calendar_RearmTimer + in sequence. +Calendar_UpdateCalendar updates the VEVENT records in the + $hash->{".fhem"}{vevents} hash and creates the associated calendar events. +Calendar_CheckTimes checks for a mode change of the calendar events and + creates the readings and FHEM events. +Calendar_RearmTimer sets the timer to call Calendar_Wakeup to time of the + next mode change or update, whatever comes earlier. + + +What's new? +----------- +This module version replaces the 2015 version that has been widely. Noteworthy +changes +- No more state... readings; "all" reading has been removed as well. +- The mode... readings (modeAlarm, modeAlarmOrStart, etc.) are deprecated + and will be removed in a future version. Use the mode= filter instead. +- Handles recurring calendar events with out-of-order events and exceptions + (EXDATE). +- Keeps ALL calendar events within plus/minus 400 days from the date of the + in FHEM: this means that you can have more than one calendar event with the + same UID. +- You can restrict visible calendar events with attributes hideLaterThan, + hideOlderThan. +- Nonblocking retrieval of calendar from URL. +- New get commands: + get vevents + get vcalendar + get + get mode= + get uid= +- The get commands + get ... + may not work as before since several calendar events may exist for a + single UID, particularly the get command + get all + show all calendar events from a series (past, current, and future); you + probably want to replace "all" by "next": + get next + to get only the first (not past but current or future) calendar event from + each series. +- Migration hints: + + Replace + get all + by + get next + + Replace + get + by + get uid= 1 + + Replace + get modeAlarmOrStart + by + get mode=alarm|start + +- The FHEM events created for mode changes of single calendar events have been + amended: + changed: UID + : UID (this is new) + is the current mode of the calendar event after the change. It is + highly advisable to trigger actions based on these FHEM events instead of + notifications for changes of the mode... readings. + +=cut + +##################################### +# +# Event +# +##################################### + +package Calendar::Event; + +sub new { + my $class= shift; + my $self= {}; # I am a hash + bless $self, $class; + $self->{_previousMode}= "undefined"; + $self->{_mode}= "undefined"; + return($self); +} + +sub uid { + my ($self)= @_; + return $self->{uid}; +} + +sub start { + my ($self)= @_; + return $self->{start}; +} + +sub end { + my ($self)= @_; + return $self->{end}; +} + +sub setNote($$) { + my ($self,$note)= @_; + $self->{_note}= $note; + return $note; +} + +sub getNote($) { + my ($self)= @_; + return $self->{_note}; +} + +sub hasNote($) { + my ($self)= @_; + return defined($self->{_note}) ? 1 : 0; +} + + +sub setMode { + my ($self,$mode)= @_; + $self->{_previousMode}= $self->{_mode}; + $self->{_mode}= $mode; + #main::Debug "After setMode $mode: Modes(" . $self->uid() . ") " . $self->{_previousMode} . " -> " . $self->{_mode}; + return $mode; +} + +sub setModeUnchanged { + my ($self)= @_; + $self->{_previousMode}= $self->{_mode}; +} + +sub getMode { + my ($self)= @_; + return $self->{_mode}; +} + +sub lastModified { + my ($self)= @_; + return $self->{lastModified}; +} + +sub modeChanged { + my ($self)= @_; + return (($self->{_mode} ne $self->{_previousMode}) and + ($self->{_previousMode} ne "undefined")) ? 1 : 0; +} + + +sub summary { + my ($self)= @_; + return $self->{summary}; +} + +sub location { + my ($self)= @_; + return $self->{location}; +} + +sub ts($$) { + my ($self,$tm)= @_; + return "" unless($tm); + my ($second,$minute,$hour,$day,$month,$year,$wday,$yday,$isdst)= localtime($tm); + return sprintf("%02d.%02d.%4d %02d:%02d:%02d", $day,$month+1,$year+1900,$hour,$minute,$second); +} + +sub ts0($$) { + my ($self,$tm)= @_; + return "" unless($tm); + my ($second,$minute,$hour,$day,$month,$year,$wday,$yday,$isdst)= localtime($tm); + return sprintf("%02d.%02d.%2d %02d:%02d", $day,$month+1,$year-100,$hour,$minute); +} + +sub asText { + my ($self)= @_; + return sprintf("%s %s", + $self->ts0($self->{start}), + $self->{summary} + ); +} + +sub asFull { + my ($self)= @_; + return sprintf("%s %9s %s %s-%s %s %s", + $self->uid(), + $self->getMode(), + $self->{alarm} ? $self->ts($self->{alarm}) : " ", + $self->ts($self->{start}), + $self->ts($self->{end}), + $self->{summary}, + $self->{location} + ); +} + +sub asDebug { + my ($self)= @_; + return sprintf("%s %s %9s %s %s-%s %s %s %s", + $self->uid(), + $self->modeChanged() ? "*" : " ", + $self->getMode(), + $self->{alarm} ? $self->ts($self->{alarm}) : " ", + $self->ts($self->{start}), + $self->ts($self->{end}), + $self->{summary}, + $self->{location}, + $self->hasNote() ? $self->getNote() : "" + ); +} + + +sub alarmTime { + my ($self)= @_; + return $self->ts($self->{alarm}); +} + +sub startTime { + my ($self)= @_; + return $self->ts($self->{start}); +} + +sub endTime { + my ($self)= @_; + return $self->ts($self->{end}); +} + + +# returns 1 if time is before alarm time and before start time, else 0 +sub isUpcoming { + my ($self,$t) = @_; + if($self->{alarm}) { + return $t< $self->{alarm} ? 1 : 0; + } else { + return $t< $self->{start} ? 1 : 0; + } +} + +# returns 1 if time is between alarm time and start time, else 0 +sub isAlarmed { + my ($self,$t) = @_; + return $self->{alarm} ? + (($self->{alarm}<= $t && $t< $self->{start}) ? 1 : 0) : 0; +} + +# return 1 if time is between start time and end time, else 0 +sub isStarted { + my ($self,$t) = @_; + return 0 unless(defined($self->{start})); + return 0 if($t < $self->{start}); + if(defined($self->{end})) { + return 0 if($t>= $self->{end}); + } + return 1; +} + +sub isSeries { + my ($self)= @_; + #main::Debug " freq= " . $self->{freq}; + return exists($self->{freq}) ? 1 : 0; +} + +sub isAfterSeriesEnded { + my ($self,$t) = @_; + #main::Debug " isSeries? " . $self->isSeries(); + return 0 unless($self->isSeries()); + #main::Debug " until= " . $self->{until}; + return 0 unless(exists($self->{until})); + #main::Debug " has until!"; + return $self->{until}< $t ? 1 : 0; +} + +sub isEnded { + my ($self,$t) = @_; + #main::Debug "isEnded for " . $self->asFull(); + #main::Debug " isAfterSeriesEnded? " . $self->isAfterSeriesEnded($t); + #return 1 if($self->isAfterSeriesEnded($t)); + #main::Debug " has end? " . (defined($self->{end}) ? 1 : 0); + return 0 unless(defined($self->{end})); + return $self->{end}<= $t ? 1 : 0; +} + +sub nextTime { + my ($self,$t) = @_; + my @times= ( ); + push @times, $self->{start} if(defined($self->{start})); + push @times, $self->{end} if(defined($self->{end})); + unshift @times, $self->{alarm} if($self->{alarm}); + @times= sort grep { $_ > $t } @times; + +# main::Debug "Calendar: " . $self->asFull(); +# main::Debug "Calendar: Start " . main::FmtDateTime($self->{start}); +# main::Debug "Calendar: End " . main::FmtDateTime($self->{end}); +# main::Debug "Calendar: Alarm " . main::FmtDateTime($self->{alarm}) if($self->{alarm}); +# main::Debug "Calendar: times[0] " . main::FmtDateTime($times[0]); +# main::Debug "Calendar: times[1] " . main::FmtDateTime($times[1]); +# main::Debug "Calendar: times[2] " . main::FmtDateTime($times[2]); + + if(@times) { + return $times[0]; + } else { + return undef; + } +} + +##################################### +# +# Events +# +##################################### + +package Calendar::Events; + +sub new { + my $class= shift; + my $self= []; # I am an array + bless $self, $class; + return($self); +} + +sub addEvent($$) { + my ($self,$event)= @_; + return push @{$self}, $event; +} + +sub clear($) { + my ($self)= @_; + return @{$self}= (); +} ##################################### # @@ -70,18 +648,209 @@ no if $] >= 5.017011, warnings => 'experimental::smartmatch'; package ICal::Entry; -sub new { +sub new($$) { my $class= shift; my ($type)= @_; + #main::Debug "new ICal::Entry $type"; my $self= {}; bless $self, $class; $self->{type}= $type; - $self->{entries}= []; - #main::Debug "NEW: $type"; + #$self->clearState(); set here: + $self->{state}= ""; + #$self->clearCounterpart(); unnecessary + #$self->clearReferences(); set here: + $self->{references}= []; + #$self->clearTags(); unnecessary + $self->{entries}= []; # array of subordinated ICal::Entry + $self->{events}= Calendar::Events->new(); + $self->{skippedEvents}= Calendar::Events->new(); return($self); } -sub addproperty { +# +# keys, properties, values +# + +# is key a repeated property? +sub isMultiple($$) { + my ($self,$key)= @_; + return $self->{properties}{$key}{multiple}; +} + +# has a property named key? +sub hasKey($$) { + my ($self,$key)= @_; + return exists($self->{properties}{$key}) ? 1 : 0; +} + +# value for single property key +sub value($$) { + my ($self,$key)= @_; + return undef if($self->isMultiple($key)); + return $self->{properties}{$key}{VALUE}; +} + +# value for property key or default, if non-existant +sub valueOrDefault($$$) { + my ($self,$key,$default)= @_; + return $self->hasKey($key) ? $self->value($key) : $default; +} + +# value for multiple property key (array counterpart) +sub values($$) { + my ($self,$key)= @_; + return undef unless($self->isMultiple($key)); + return $self->{properties}{$key}{VALUES}; +} + +# true, if the property exists at both entries and have the same value +# or neither entry has this property +sub sameValue($$$) { + my ($self,$other,$key)= @_; + my $value1= $self->hasKey($key) ? $self->value($key) : ""; + my $value2= $other->hasKey($key) ? $other->value($key) : ""; + return $value1 eq $value2; +} + +sub parts($$) { + my ($self,$key)= @_; + return split(";", $self->{properties}{$key}{PARTS}); +} + +# +# state +# +sub setState { + my ($self,$state)= @_; + $self->{state}= $state; + return $state; +} + +sub clearState { + my ($self)= @_; + $self->{state}= ""; +} + +sub state($) { + my($self)= @_; + return $self->{state}; +} + +sub inState($$) { + my($self, $state)= @_; + return ($self->{state} eq $state ? 1 : 0); +} + +sub isObsolete($) { + my($self)= @_; + # VEVENT records in these states are obsolete + my @statesObsolete= qw/deleted changed-old modified-old/; + return $self->state() ~~ @statesObsolete ? 1 : 0; +} + +sub hasChanged($) { + my($self)= @_; + # VEVENT records in these states have changed + my @statesChanged= qw/new changed-new modified-new/; + return $self->state() ~~ @statesChanged ? 1 : 0; +} + +# +# type +# +sub type($) { + my($self)= @_; + return $self->{type}; +} + +# +# counterpart, for changed or modified records +# +sub counterpart($) { + my($self)= @_; + return $self->{counterpart}; +} + +sub setCounterpart($$) { + my ($self, $id)= @_; + $self->{counterpart}= $id; + return $id; +} + +sub hasCounterpart($) { + my($self)= @_; + return (defined($self->{counterpart}) ? 1 : 0); +} + +sub clearCounterpart($) { + my($self)= @_; + delete $self->{counterpart} if(defined($self->{counterpart})); +} + +# +# series +# +sub isRecurring($) { + my($self)= @_; + return $self->hasKey("RRULE"); +} + +sub isException($) { + my($self)= @_; + return $self->hasKey("RECURRENCE-ID"); +} + + +sub hasReferences($) { + my($self)= @_; + return scalar(@{$self->references()}); +} + +sub references($) { + my($self)= @_; + return $self->{references}; +} + +sub clearReferences($) { + my($self)= @_; + $self->{references}= []; +} + +# +# tags +# + +# sub tags($) { +# my($self)= @_; +# return $self->{tags}; +# } +# +# sub clearTags($) { +# my($self)= @_; +# $self->{tags}= []; +# } +# +# sub tagAs($$) { +# my ($self, $tag)= @_; +# push @{$self->{tags}}, $tag unless($self->isTaggedAs($tag)); +# } +# +# sub isTaggedAs($$) { +# my ($self, $tag)= @_; +# return grep { $_ eq $tag } @{$self->{tags}} ? 1 : 0; +# } +# +# sub numTags($) { +# my ($self)= @_; +# return scalar @{$self->{tags}}; +# } + + +# +# parsing +# + +sub addproperty($$) { my ($self,$line)= @_; # contentline = name *(";" param ) ":" value CRLF [Page 13] # example: @@ -101,34 +870,37 @@ sub addproperty { return; } return unless($key); - #main::Debug "-> key=\'$key\' parts=\'$parts\' parameter=\'$parameter\'"; + + # ignore some properties + my @ignores= qw(TRANSP STATUS ATTENDEE); + return if($key ~~ @ignores); + return if($key =~ /^X-/); + if($key eq "EXDATE") { - push @{$self->{properties}{exdates}}, $parameter; - } - $self->{properties}{$key}= { - PARTS => "$parts", - VALUE => "$parameter" - }; - #main::Debug "ADDPROPERTY: ". $self ." key= $key, parts= $parts, value= $parameter"; - #main::Debug "WE ARE " . $self->{properties}{$key}{VALUE}; + # handle multiple properties + my @values; + @values= @{$self->values($key)} if($self->hasKey($key)); + push @values, $parameter; + $self->{properties}{$key}= { + multiple => 1, + VALUES => \@values, + } + } else { + # handle single properties + $self->{properties}{$key}= { + multiple => 0, + PARTS => "$parts", + VALUE => "$parameter", + } + }; } -sub value { - my ($self,$key)= @_; - return $self->{properties}{$key}{VALUE}; -} - -sub parts { - my ($self,$key)= @_; - return split(";", $self->{properties}{$key}{PARTS}); -} - -sub parse { +sub parse($$) { my ($self,@ical)= @_; - $self->parseSub(0, @ical); + return $self->parseSub(0, @ical); } -sub parseSub { +sub parseSub($$$) { my ($self,$ln,@ical)= @_; #main::Debug "ENTER @ $ln"; while($ln<$#ical) { @@ -150,6 +922,7 @@ sub parseSub { last if($line =~ m/^END:.*$/); if($line =~ m/^BEGIN:(.*)$/) { my $entry= ICal::Entry->new($1); + $entry->{ln}= $ln; push @{$self->{entries}}, $entry; $ln= $entry->parseSub($ln,@ical); } else { @@ -160,140 +933,69 @@ sub parseSub { return $ln; } -sub asString() { - my ($self,$level)= @_; - $level= "" unless(defined($level)); - my $s= $level . $self->{type} . "\n"; - $level .= " "; - for my $key (keys %{$self->{properties}}) { - $s.= $level . "$key: ". $self->value($key) . "\n"; - } - my @entries= @{$self->{entries}}; - for(my $i= 0; $i<=$#entries; $i++) { - $s.= $entries[$i]->asString($level); - } - return $s; -} - -##################################### # -# Event +# events # -##################################### - -package Calendar::Event; - -sub new { - my $class= shift; - my $self= {}; - bless $self, $class; - $self->{_state}= ""; - $self->{_mode}= "undefined"; - $self->setState("new"); - $self->setMode("undefined"); - $self->{alarmTriggered}= 0; - $self->{startTriggered}= 0; - $self->{endTriggered}= 0; - return($self); -} - -sub uid { +sub events($) { my ($self)= @_; - return $self->{uid}; + return $self->{events}; } -sub start { +sub clearEvents($) { my ($self)= @_; - return $self->{start}; + $self->{events}->clear(); } - -sub setState { - my ($self,$state)= @_; - #main::Debug "Before setState $state: States(" . $self->uid() . ") " . $self->{_previousState} . " -> " . $self->{_state}; - $self->{_previousState}= $self->{_state}; - $self->{_state}= $state; - #main::Debug "After setState $state: States(" . $self->uid() . ") " . $self->{_previousState} . " -> " . $self->{_state}; - return $state; -} - -sub setMode { - my ($self,$mode)= @_; - $self->{_previousMode}= $self->{_mode}; - $self->{_mode}= $mode; - #main::Debug "After setMode $mode: Modes(" . $self->uid() . ") " . $self->{_previousMode} . " -> " . $self->{_mode}; - return $mode; -} - -sub touch { - my ($self,$t)= @_; - $self->{_lastSeen}= $t; - return $t; -} - -sub lastSeen { +sub numEvents($) { my ($self)= @_; - return $self->{_lastSeen}; + return scalar(@{$self->{events}}); } -sub state { +sub addEvent($$) { + my ($self, $event)= @_; + $self->{events}->addEvent($event); +} + +sub skippedEvents($) { my ($self)= @_; - return $self->{_state}; + return $self->{skippedEvents}; } -sub mode { +sub clearSkippedEvents($) { my ($self)= @_; - return $self->{_mode}; + $self->{skippedEvents}->clear(); } -sub lastModified { +sub numSkippedEvents($) { my ($self)= @_; - return $self->{lastModified}; + return scalar(@{$self->{skippedEvents}}); } -sub isState { - my ($self,$state)= @_; - return $self->{_state} eq $state ? 1 : 0; +sub addSkippedEvent($$) { + my ($self, $event)= @_; + $self->{skippedEvents}->addEvent($event); } -sub isNew { + + +sub createEvent($) { my ($self)= @_; - return $self->isState("new"); + + my $event= Calendar::Event->new(); + + $event->{uid}= $self->value("UID"); + $event->{uid}=~ s/\W//g; # remove all non-alphanumeric characters, this makes life easier for perl specials + + return $event; } -sub isKnown { - my ($self)= @_; - return $self->isState("known"); -} - -sub isUpdated { - my ($self)= @_; - return $self->isState("updated"); -} - -sub isDeleted { - my ($self)= @_; - return $self->isState("deleted"); -} - - -sub stateChanged { - my ($self)= @_; - #main::Debug "States(" . $self->uid() . ") " . $self->{_previousState} . " -> " . $self->{_state}; - return $self->{_state} ne $self->{_previousState} ? 1 : 0; -} - -sub modeChanged { - my ($self)= @_; - return $self->{_mode} ne $self->{_previousMode} ? 1 : 0; -} # converts a date/time string to the number of non-leap seconds since the epoch # 20120520T185202Z: date/time string in ISO8601 format, time zone GMT # 20121129T222200: date/time string in ISO8601 format, time zone local # 20120520: a date string has no time zone associated -sub tm { - my ($t)= @_; +sub tm($$) { + my ($self, $t)= @_; return undef if(!$t); #main::Debug "convert >$t<"; my ($year,$month,$day)= (substr($t,0,4), substr($t,4,2),substr($t,6,2)); @@ -325,8 +1027,8 @@ sub tm { # dur-day = 1*DIGIT "D" # # example: -P0DT0H30M0S -sub d { - my ($d)= @_; +sub d($$) { + my ($self, $d)= @_; #main::Debug "Duration $d"; @@ -356,171 +1058,41 @@ sub d { return $t; } -sub dt { - my ($t0,$value,$parts)= @_; +sub dt($$$$) { + my ($self,$t0,$value,$parts)= @_; #main::Debug "t0= $t0 parts= $parts value= $value"; if(defined($parts) && $parts =~ m/VALUE=DATE/) { - return tm($value); + return $self->tm($value); } else { - return $t0+d($value); + return $t0+$self->d($value); } } -sub ts { - my ($tm)= @_; - return "" unless($tm); - my ($second,$minute,$hour,$day,$month,$year,$wday,$yday,$isdst)= localtime($tm); - return sprintf("%02d.%02d.%4d %02d:%02d:%02d", $day,$month+1,$year+1900,$hour,$minute,$second); + +sub makeEventDetails($$) { + my ($self, $event)= @_; + + $event->{summary}= $self->valueOrDefault("SUMMARY", ""); + $event->{location}= $self->valueOrDefault("LOCATION", ""); + + return $event; } -sub ts0 { - my ($tm)= @_; - return "" unless($tm); - my ($second,$minute,$hour,$day,$month,$year,$wday,$yday,$isdst)= localtime($tm); - return sprintf("%02d.%02d.%2d %02d:%02d", $day,$month+1,$year-100,$hour,$minute); -} - -sub fromVEvent { - my ($self,$vevent)= @_; - - $self->{uid}= $vevent->value("UID"); - $self->{uid}=~ s/\W//g; # remove all non-alphanumeric characters, this makes life easier for perl specials - $self->{start}= tm($vevent->value("DTSTART")); - if(defined($vevent->value("DTEND"))) { - $self->{end}= tm($vevent->value("DTEND")); - } elsif(defined($vevent->value("DURATION"))) { - $self->{end}= $self->{start} + d($vevent->value("DURATION")); - } - # we take the creation time if the last modification time is unavailable - if(defined($vevent->value("LAST-MODIFIED"))) { - $self->{lastModified}= tm($vevent->value("LAST-MODIFIED")); - #main::Debug "LAST-MOD: $self->{lastModified} "; +sub makeEventAlarms($$) { + my ($self, $event)= @_; + + # alarms + my @valarms= grep { $_->{type} eq "VALARM" } @{$self->{entries}}; + my @alarmtimes= sort map { $self->dt($event->{start}, $_->value("TRIGGER"), $_->parts("TRIGGER")) } @valarms; + if(@alarmtimes) { + $event->{alarm}= $alarmtimes[0]; } else { - $self->{lastModified}= tm($vevent->value("DTSTAMP")); - #main::Debug "DTSTAMP: $self->{lastModified} "; - } - $self->{summary}= $vevent->value("SUMMARY"); - $self->{location}= $vevent->value("LOCATION"); - - #Dates to exclude in reoccuring rule - my @exdate; - if(exists($vevent->{properties}{exdates})) { - foreach my $entry (@{$vevent->{properties}{exdates}}) { - my @ed = split(",", $entry); - @ed = map { tm($_) } @ed; - push @exdate, @ed; - } - } - - #@exdate= split(",", $vevent->value("EXDATE")) if($vevent->value("EXDATE")); - #@exdate = map { tm($_) } @exdate; - $self->{exdate} = \@exdate; - - #$self->{summary}=~ s/;/,/g; - - # - # recurring events - # - # this part is under construction - # we have to think a lot about how to deal with the migration of states for recurring events - my $rrule= $vevent->value("RRULE"); - if($rrule) { - my @rrparts= split(";", $rrule); - my %r= map { split("=", $_); } @rrparts; - - my @keywords= qw(FREQ INTERVAL UNTIL COUNT BYMONTHDAY BYDAY BYMONTH WKST); - foreach my $k (keys %r) { - if(not($k ~~ @keywords)) { - main::Log3 undef, 2, "Calendar: RRULE $rrule is not supported"; - } - } - - $self->{freq} = $r{"FREQ"}; - #According to RFC, interval defaults to 1 - $self->{interval} = exists($r{"INTERVAL"}) ? $r{"INTERVAL"} : 1; - $self->{until} = tm($r{"UNTIL"}) if(exists($r{"UNTIL"})); - $self->{count} = $r{"COUNT"} if(exists($r{"COUNT"})); - $self->{bymonthday} = $r{"BYMONTHDAY"} if(exists($r{"BYMONTHDAY"})); # stored but ignored - $self->{byday} = $r{"BYDAY"} if(exists($r{"BYDAY"})); # stored but ignored - $self->{bymonth} = $r{"BYMONTH"} if(exists($r{"BYMONTH"})); # stored but ignored - $self->{wkst} = $r{"WKST"} if(exists($r{"WKST"})); # stored but ignored - - # advanceToNextOccurance until we are in the future - my $t = time(); - while($self->{end} < $t and $self->advanceToNextOccurance()) { ; } + $event->{alarm}= undef; } - - # alarms - my @valarms= grep { $_->{type} eq "VALARM" } @{$vevent->{entries}}; - my @alarmtimes= sort map { dt($self->{start}, $_->value("TRIGGER"), $_->parts("TRIGGER")) } @valarms; - if(@alarmtimes) { - $self->{alarm}= $alarmtimes[0]; - } else { - $self->{alarm}= undef; - } + return $event; } -# sub asString { -# my ($self)= @_; -# return sprintf("%s %s(%s);%s;%s;%s;%s", -# $self->state(), -# $self->{uid}, -# ts($self->{lastModified}), -# $self->{alarm} ? ts($self->{alarm}) : "", -# ts($self->{start}), -# ts($self->{end}), -# $self->{summary} -# ); -# } - -sub summary { - my ($self)= @_; - return $self->{summary}; -} - -sub location { - my ($self)= @_; - return $self->{location}; -} - - -sub asText { - my ($self)= @_; - return sprintf("%s %s", - ts0($self->{start}), - $self->{summary} - ); -} - -sub asFull { - my ($self)= @_; - return sprintf("%s %7s %8s %s %s-%s %s %s", - $self->uid(), - $self->state(), - $self->mode(), - $self->{alarm} ? ts($self->{alarm}) : " ", - ts($self->{start}), - ts($self->{end}), - $self->{summary}, - $self->{location} - ); -} - -sub alarmTime { - my ($self)= @_; - return ts($self->{alarm}); -} - -sub startTime { - my ($self)= @_; - return ts($self->{start}); -} - -sub endTime { - my ($self)= @_; - return ts($self->{end}); -} sub DSTOffset($$) { my ($t1,$t2)= @_; @@ -537,6 +1109,7 @@ sub DSTOffset($$) { # into summertime. Thus, e.g., adding a multiple of 24*60*60 seconds # to 5 o'clock always gives 5 o'clock and not 4 o'clock or 6 o'clock # upon a change of summertime to wintertime or vice versa. + sub plusNSeconds($$$) { my ($t1, $seconds, $n)= @_; $n= 1 unless defined($n); @@ -555,245 +1128,255 @@ sub plusNMonths($$) { return main::fhemTimeLocal($second,$minute,$hour,$day,$month,$year); } +use constant eventsLimitMinus => -34560000; # -400d +use constant eventsLimitPlus => 34560000; # +400d +sub addEventLimited($$$) { + my ($self, $t, $event)= @_; + + return -1 if($event->start()< $t+eventsLimitMinus); + return 1 if($event->start()> $t+eventsLimitPlus); + #main::Debug " addEvent: " . $event->asFull(); + $self->addEvent($event); + return 0; - -sub advanceToNextOccurance { - my ($self) = @_; - # See RFC 2445 page 39 and following - - return if(!exists($self->{freq})); #This event is not reoccuring - $self->{count}-- if(exists($self->{count})); # since we look for the next occurance we have to decrement count first - return if(exists($self->{count}) and $self->{count} <= 0); #We are already at the last occurance - - my @weekdays = qw(SU MO TU WE TH FR SA); - #There are no leap seconds in epoch time - #Valid values for freq: SECONDLY, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY - my $nextstart = $self->{start}; - do - { - if($self->{freq} eq "SECONDLY") { - $nextstart = plusNSeconds($nextstart, 1, $self->{interval}); - } elsif($self->{freq} eq "MINUTELY") { - $nextstart = plusNSeconds($nextstart, 60, $self->{interval}); - } elsif($self->{freq} eq "HOURLY") { - $nextstart = plusNSeconds($nextstart, 60*60, $self->{interval}); - } elsif($self->{freq} eq "DAILY") { - $nextstart = plusNSeconds($nextstart, 24*60*60, $self->{interval}); - } elsif($self->{freq} eq "WEEKLY") { - # special handling for WEEKLY and BYDAY - if(exists($self->{byday})) { - # this fails for intervals > 1 - # BYDAY with prefix (e.g. -1SU or 2MO) is not recognized - # BYDAY with list (e.g. SU,TU,TH) is not recognized - my ($msec, $mmin, $mhour, $mday, $mmon, $myear, $mwday, $yday, $isdat); - my $preventloop = 0; - do { - $nextstart = plusNSeconds($nextstart, 24*60*60, $self->{interval}); - ($msec, $mmin, $mhour, $mday, $mmon, $myear, $mwday, $yday, $isdat) = gmtime($nextstart); - $preventloop ++; - } while(index($self->{byday}, $weekdays[$mwday]) == -1 and $preventloop < 10); - } - else { - # default WEEKLY handling - $nextstart = plusNSeconds($nextstart, 7*24*60*60, $self->{interval}); - } - } elsif($self->{freq} eq "MONTHLY") { - # here we ignore BYMONTHDAY as we consider the day of month of $self->{start} - # to be equal to BYMONTHDAY. - $nextstart= plusNMonths($nextstart, $self->{interval}); - } elsif($self->{freq} eq "YEARLY") { - $nextstart= plusNMonths($nextstart, 12*$self->{interval}); - } else { - main::Log3 undef, 1, "Calendar: event frequency '" . $self->{freq} . "' not implemented"; - return; - } - - # Loop if nextstart is in the "dates to exclude" - } while(exists($self->{exdate}) and ($nextstart ~~ $self->{exdate})); - - #the UNTIL clause is inclusive, so $newt == $self->{until} is okey - return if(exists($self->{until}) and $nextstart > $self->{until}); - - my $duration = $self->{end} - $self->{start}; - $self->{start} = $nextstart; - $self->{end} = $self->{start} + $duration; - main::Log3 undef, 5, "Next time of $self->{summary} is: start " . ts($self->{"start"}) . ", end " . ts($self->{"end"}); - return 1; -} - - -# returns 1 if time is before alarm time and before start time, else 0 -sub isUpcoming { - my ($self,$t) = @_; - return 0 if($self->isDeleted()); - if($self->{alarm}) { - return $t< $self->{alarm} ? 1 : 0; - } else { - return $t< $self->{start} ? 1 : 0; - } -} - -# returns 1 if time is between alarm time and start time, else 0 -sub isAlarmed { - my ($self,$t) = @_; - return 0 if($self->isDeleted()); - return $self->{alarm} ? - (($self->{alarm}<= $t && $t< $self->{start}) ? 1 : 0) : 0; -} - -# return 1 if time is between start time and end time, else 0 -sub isStarted { - my ($self,$t) = @_; - return 0 if($self->isDeleted()); - return 0 unless(defined($self->{start})); - return 0 if($t < $self->{start}); - if(defined($self->{end})) { - return 0 if($t>= $self->{end}); - } - return 1; -} - -sub isEnded { - my ($self,$t) = @_; - return 0 if($self->isDeleted()); - return 0 unless(defined($self->{end})); - return $self->{end}<= $t ? 1 : 0; -} - -sub nextTime { - my ($self,$t) = @_; - my @times= ( ); - push @times, $self->{start} if(defined($self->{start})); - push @times, $self->{end} if(defined($self->{end})); - unshift @times, $self->{alarm} if($self->{alarm}); - @times= sort grep { $_ > $t } @times; - -# main::Debug "Calendar: " . $self->asFull(); -# main::Debug "Calendar: Start " . main::FmtDateTime($self->{start}); -# main::Debug "Calendar: End " . main::FmtDateTime($self->{end}); -# main::Debug "Calendar: Alarm " . main::FmtDateTime($self->{alarm}) if($self->{alarm}); -# main::Debug "Calendar: times[0] " . main::FmtDateTime($times[0]); -# main::Debug "Calendar: times[1] " . main::FmtDateTime($times[1]); -# main::Debug "Calendar: times[2] " . main::FmtDateTime($times[2]); +} + +sub createSingleEvent($$$) { + + my ($self, $nextstart, $onCreateEvent)= @_; + + my $event= $self->createEvent(); + my $start= $self->tm($self->value("DTSTART")); + $nextstart= $start unless(defined($nextstart)); + $event->{start}= $nextstart; + if($self->hasKey("DTEND")) { + my $end= $self->tm($self->value("DTEND")); + $event->{end}= $nextstart+($end-$start); + } elsif($self->hasKey("DURATION")) { + my $duration= $self->d($self->value("DURATION")); + $event->{end}= $nextstart + $duration; + } + $self->makeEventDetails($event); + $self->makeEventAlarms($event); + + #main::Debug "createSingleEvent Start " . main::FmtDateTime($event->{start}); + + # plug-in + if(defined($onCreateEvent)) { + my $e= $event; + #main::Debug "Executing $onCreateEvent for " . $e->asDebug(); + eval $onCreateEvent; + if($@) { + main::Log3 undef, 2, "Erronenous onCreateEvent $onCreateEvent: $@"; + } else { + $event= $e; + } + } + + return $event; +} + +sub createEvents($$$%) { + my ($self, $t, $onCreateEvent, %vevents)= @_; + + $self->clearEvents(); + $self->clearSkippedEvents(); + + if($self->isRecurring()) { + # + # recurring event creates a series + # + my $rrule= $self->value("RRULE"); + my @rrparts= split(";", $rrule); + my %r= map { split("=", $_); } @rrparts; + + my @keywords= qw(FREQ INTERVAL UNTIL COUNT BYMONTHDAY BYDAY BYMONTH WKST); + foreach my $k (keys %r) { + if(not($k ~~ @keywords)) { + main::Log3 undef, 2, "Calendar: keyword $k in RRULE $rrule is not supported"; + } + } + + # Valid values for freq: SECONDLY, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY + my $freq = $r{"FREQ"}; + # According to RFC, interval defaults to 1 + my $interval = exists($r{"INTERVAL"}) ? $r{"INTERVAL"} : 1; + my $until = exists($r{"UNTIL"}) ? $self->tm($r{"UNTIL"}) : 99999999999999999; + my $count = exists($r{"COUNT"}) ? $r{"COUNT"} : 999999; + my $bymonthday = $r{"BYMONTHDAY"} if(exists($r{"BYMONTHDAY"})); # stored but ignored + my $byday = $r{"BYDAY"} if(exists($r{"BYDAY"})); # stored but ignored + my $bymonth = $r{"BYMONTH"} if(exists($r{"BYMONTH"})); # stored but ignored + my $wkst = $r{"WKST"} if(exists($r{"WKST"})); # stored but ignored + + my @weekdays = qw(SU MO TU WE TH FR SA); + + + #main::Debug "createEvents: " . $self->asString(); + + # first event in the series + my $event= $self->createSingleEvent(undef, $onCreateEvent); + my $n= 0; + + + while(1) { + my $skip= 0; + + # check if superseded by out-of-series event + if($self->hasReferences()) { + foreach my $id (@{$self->references()}) { + my $vevent= $vevents{$id}; + my $recurrenceid= $vevent->value("RECURRENCE-ID"); + my $originalstart= $vevent->tm($recurrenceid); + if($originalstart == $event->start()) { + $event->setNote("RECURRENCE-ID: $recurrenceid"); + $self->addSkippedEvent($event); + $skip++; + last; + } + } + } + + # check if excluded by EXDATE + if($self->hasKey('EXDATE')) { + foreach my $exdate (@{$self->values("EXDATE")}) { + if($self->tm($exdate) == $event->start()) { + $event->setNote("EXDATE: $exdate"); + $self->addSkippedEvent($event); + $skip++; + last; + } + } + + } + + return if($event->{start} > $until); # return if we are after end of series + if(!$skip) { + # add event + # and return if we exceed storage limit + return if($self->addEventLimited($t, $event) > 0); + } + $n++; + return if($n>= $count); # return if we exceeded occurances + + # advance to next occurence + my $nextstart = $event->{start}; + if($freq eq "SECONDLY") { + $nextstart = plusNSeconds($nextstart, 1, $interval); + } elsif($freq eq "MINUTELY") { + $nextstart = plusNSeconds($nextstart, 60, $interval); + } elsif($freq eq "HOURLY") { + $nextstart = plusNSeconds($nextstart, 60*60, $interval); + } elsif($freq eq "DAILY") { + $nextstart = plusNSeconds($nextstart, 24*60*60, $interval); + } elsif($freq eq "WEEKLY") { + # special handling for WEEKLY and BYDAY + if(exists($self->{byday})) { + # BYDAY with prefix (e.g. -1SU or 2MO) is not recognized + #main::Debug "weekdays: " . $self->{byday}; + my @bydays= split(',', $self->{byday}); + # we skip interval-1 weeks + $nextstart = plusNSeconds($nextstart, 7*24*60*60, $interval-1); + #main::Debug "Fast forward to: start " . ts($nextstart); + my ($msec, $mmin, $mhour, $mday, $mmon, $myear, $mwday, $yday, $isdat); + my $preventloop = 0; + do { + $nextstart = plusNSeconds($nextstart, 24*60*60, 1); # forward day by day + ($msec, $mmin, $mhour, $mday, $mmon, $myear, $mwday, $yday, $isdat) = gmtime($nextstart); + #main::Debug "Skip to: start " . ts($nextstart) . " = " . $weekdays[$mwday]; + $preventloop ++; + } until(($weekdays[$mwday] ~~ @bydays) or ($preventloop > 7)); + } + else { + # default WEEKLY handling + $nextstart = plusNSeconds($nextstart, 7*24*60*60, $interval); + } + } elsif($freq eq "MONTHLY") { + # here we ignore BYMONTHDAY as we consider the day of month of $self->{start} + # to be equal to BYMONTHDAY. + $nextstart= plusNMonths($nextstart, $interval); + } elsif($freq eq "YEARLY") { + $nextstart= plusNMonths($nextstart, 12*$interval); + } else { + main::Log3 undef, 2, "Calendar: event frequency '$freq' not implemented"; + return; + } + # the next event + $event= $self->createSingleEvent($nextstart, $onCreateEvent); + + } + + - if(@times) { - return $times[0]; } else { - return undef; + # + # single event + # + my $event= $self->createSingleEvent(undef, $onCreateEvent); + $self->addEventLimited($t, $event); } + } -##################################### + # -# Events +# friendly string # -##################################### - -package Calendar::Events; - -sub new { - my $class= shift; - my $self= {}; - bless $self, $class; - $self->{events}= {}; - return($self); -} - -sub uids { - my ($self)= @_; - return keys %{$self->{events}}; -} - -sub events { - my ($self)= @_; - return values %{$self->{events}}; -} - -sub event { - my ($self,$uid)= @_; - return $self->{events}{$uid}; -} - -sub setEvent { - my ($self,$event)= @_; - $self->{events}{$event->uid()}= $event; -} -sub deleteEvent { - my ($self,$uid)= @_; - delete $self->{events}{$uid}; -} - -# sub ts { -# my ($tm)= @_; -# my ($second,$minute,$hour,$day,$month,$year,$wday,$yday,$isdst)= localtime($tm); -# return sprintf("%02d.%02d.%4d %02d:%02d:%02d", $day,$month+1,$year+1900,$hour,$minute,$second); -# } - -sub updateFromCalendar { - my ($self,$calendar,$removeall)= @_; - my $t= time(); - my $uid; - my $event; - - # we first remove all elements which were previously marked for deletion - foreach $event ($self->events()) { - if($event->isDeleted() || $removeall) { - $self->deleteEvent($event->uid()); +sub asString($$) { + my ($self,$level)= @_; + $level= "" unless(defined($level)); + my $s= $level . $self->{type}; + $s.= " @" . $self->{ln} if(defined($self->{ln})); + $s.= " ["; + $s.= "obsolete, " if($self->isObsolete()); + $s.= $self->state(); + $s.= ", refers to " . $self->counterpart() if($self->hasCounterpart()); + $s.= ", in a series with " . join(",", sort @{$self->references()}) if($self->hasReferences()); + $s.= "]"; + #$s.= " (tags: " . join(",", @{$self->tags()}) . ")" if($self->numTags()); + $s.= "\n"; + $level .= " "; + for my $key (sort keys %{$self->{properties}}) { + $s.= $level . "$key: "; + if($self->{properties}{$key}{multiple}) { + $s.= "(" . join(" ", @{$self->values($key)}) . ")"; + } else { + $s.= $self->value($key); } + $s.= "\n"; } - - # we iterate over the VEVENTs in the calendar - my @vevents= grep { $_->{type} eq "VEVENT" } @{$calendar->{entries}}; - foreach my $vevent (@vevents) { - # convert event to entry - my $event= Calendar::Event->new(); - $event->fromVEvent($vevent); - - $uid= $event->uid(); - #main::Debug "Processing event $uid."; - #foreach my $ee ($self->events()) { - # main::Debug $ee->asFull(); - #} - if(defined($self->event($uid))) { - # the event already exists - #main::Debug "Event $uid already exists."; - $event->setState($self->event($uid)->state()); # copy the state from the existing event - $event->setMode($self->event($uid)->mode()); # copy the mode from the existing event - #main::Debug "Our lastModified: " . ts($self->event($uid)->lastModified()); - #main::Debug "New lastModified: " . ts($event->lastModified()); - if( - defined($self->event($uid)->lastModified()) && - defined($event->lastModified()) && - ($self->event($uid)->lastModified() != $event->lastModified()) - ) { - $event->setState("updated"); - #main::Debug "We set it to updated."; - } else { - $event->setState("known") - } - }; - # new events that have ended are omitted - if($event->state() ne "new" || !$event->isEnded($t)) { - $event->touch($t); - $self->setEvent($event); + if($self->{type} eq "VEVENT") { + if($self->isRecurring()) { + $s.= $level . ">>> is a series\n"; } - } - - # untouched elements get marked as deleted - foreach $event ($self->events()) { - if($event->lastSeen() != $t) { - $event->setState("deleted"); + if($self->isException()) { + $s.= $level . ">>> is an exception\n"; } + $s.= $level . ">>> Events:\n"; + foreach my $event (@{$self->{events}}) { + $s.= "$level " . $event->asDebug() . "\n"; + } + $s.= $level . ">>> Skipped events:\n"; + foreach my $event (@{$self->{skippedEvents}}) { + $s.= "$level " . $event->asDebug() . "\n"; + } + } + my @entries= @{$self->{entries}}; + for(my $i= 0; $i<=$#entries; $i++) { + $s.= $entries[$i]->asString($level); } + + return $s; } -##################################### +########################################################################## +# +# main +# +########################################################################## package main; - - ##################################### sub Calendar_Initialize($) { @@ -802,296 +1385,13 @@ sub Calendar_Initialize($) { $hash->{UndefFn} = "Calendar_Undef"; $hash->{GetFn} = "Calendar_Get"; $hash->{SetFn} = "Calendar_Set"; - $hash->{AttrList}= $readingFnAttributes; + $hash->{AttrFn} = "Calendar_Attr"; + $hash->{NOTIFYDEV} = "global"; + $hash->{NotifyFn}= "Calendar_Notify"; + $hash->{AttrList}= "hideOlderThan hideLaterThan onCreateEvent $readingFnAttributes"; } -################################### -sub Calendar_Wakeup($$) { - - my ($hash,$removeall) = @_; - - my $t= time(); - Log3 $hash, 4, "Calendar " . $hash->{NAME} . ": Wakeup"; - - Calendar_GetUpdate($hash,$removeall) if($t>= $hash->{fhem}{nxtUpdtTs}); - - $hash->{fhem}{lastChkTs}= $t; - $hash->{fhem}{lastCheck}= FmtDateTime($t); - Calendar_CheckTimes($hash); - - # find next event - my $nt= $hash->{fhem}{nxtUpdtTs}; - foreach my $event ($hash->{fhem}{events}->events()) { - next if $event->isDeleted(); - my $et= $event->nextTime($t); - # we only consider times in the future to avoid multiple - # invocations for calendar events with the event time - $nt= $et if(defined($et) && ($et< $nt) && ($et > $t)); - } - $hash->{fhem}{nextChkTs}= $nt; - $hash->{fhem}{nextCheck}= FmtDateTime($nt); - - InternalTimer($nt, "Calendar_Wakeup", $hash, 0) ; - -} - -################################### -sub Calendar_CheckTimes($) { - - my ($hash) = @_; - - my $eventsObj= $hash->{fhem}{events}; - my $t= time(); - Log3 $hash, 4, "Calendar " . $hash->{NAME} . ": Checking times..."; - - # we now run over all events and update the readings - my @allevents= $eventsObj->events(); - my @endedevents= grep { $_->isEnded($t) } @allevents; - foreach (@endedevents) { $_->advanceToNextOccurance(); } - - my @upcomingevents= grep { $_->isUpcoming($t) } @allevents; - my @alarmedevents= grep { $_->isAlarmed($t) } @allevents; - my @startedevents= grep { $_->isStarted($t) } @allevents; - - my $event; - #main::Debug "Updating modes..."; - foreach $event (@upcomingevents) { $event->setMode("upcoming"); } - foreach $event (@alarmedevents) { $event->setMode("alarm"); } - foreach $event (@startedevents) { $event->setMode("start"); } - foreach $event (@endedevents) { $event->setMode("end"); } - - my @changedevents= grep { $_->modeChanged() } @allevents; - - - my @upcoming= sort map { $_->uid() } @upcomingevents; - my @alarm= sort map { $_->uid() } @alarmedevents; - my @alarmed= sort map { $_->uid() } grep { $_->modeChanged() } @alarmedevents; - my @start= sort map { $_->uid() } @startedevents; - my @started= sort map { $_->uid() } grep { $_->modeChanged() } @startedevents; - my @end= sort map { $_->uid() } @endedevents; - my @ended= sort map { $_->uid() } grep { $_->modeChanged() } @endedevents; - my @changed= sort map { $_->uid() } @changedevents; - - readingsBeginUpdate($hash); # clears all events in CHANGED, thus must be called first - # we create one fhem event for one changed calendar event - map { addEvent($hash, "changed: " . $_->uid() . " " . $_->mode() ); } @changedevents; - readingsBulkUpdate($hash, "lastCheck", $hash->{fhem}{lastCheck}); - readingsBulkUpdate($hash, "modeUpcoming", join(";", @upcoming)); - readingsBulkUpdate($hash, "modeAlarm", join(";", @alarm)); - readingsBulkUpdate($hash, "modeAlarmed", join(";", @alarmed)); - readingsBulkUpdate($hash, "modeAlarmOrStart", join(";", @alarm,@start)); - readingsBulkUpdate($hash, "modeChanged", join(";", @changed)); - readingsBulkUpdate($hash, "modeStart", join(";", @start)); - readingsBulkUpdate($hash, "modeStarted", join(";", @started)); - readingsBulkUpdate($hash, "modeEnd", join(";", @end)); - readingsBulkUpdate($hash, "modeEnded", join(";", @ended)); - readingsEndUpdate($hash, 1); # DoTrigger, because sub is called by a timer instead of dispatch - -} - - -################################### -sub Calendar_GetUpdate($$) { - - my ($hash,$removeall) = @_; - - my $t= time(); - $hash->{fhem}{lstUpdtTs}= $t; - $hash->{fhem}{lastUpdate}= FmtDateTime($t); - - $t+= $hash->{fhem}{interval}; - $hash->{fhem}{nxtUpdtTs}= $t; - $hash->{fhem}{nextUpdate}= FmtDateTime($t); - - $hash->{STATE}= "No data"; - - Log3 $hash, 4, "Calendar " . $hash->{NAME} . ": Updating..."; - my $type = $hash->{fhem}{type}; - my $url= $hash->{fhem}{url}; - - my $errmsg= ""; - my $ics; - - if($type eq "url"){ - #$ics= GetFileFromURLQuiet($url,10,undef,0,5) if($type eq "url"); - ($errmsg, $ics)= HttpUtils_BlockingGet( { url => $url, hideurl => 1, timeout => 10, } ); - } elsif($type eq "file") { - if(open(ICSFILE, $url)) { - while() { - $ics .= $_; - } - close(ICSFILE); - } else { - Log3 $hash, 1, "Calendar " . $hash->{NAME} . ": Could not open file $url"; - return 0; - } - } else { - # this case never happens by virtue of _Define, so just - die "Software Error"; - } - - - if(!defined($ics) or ("$ics" eq "") or ($errmsg ne "")) { - Log3 $hash, 1, "Calendar " . $hash->{NAME} . ": Could not retrieve file at URL. $errmsg"; - return 0; - } - - # we parse the calendar into a recursive ICal::Entry structure - my $ical= ICal::Entry->new("root"); - $ical->parse(split("\n",$ics)); - #main::Debug "*** Result:\n"; - #main::Debug $ical->asString(); - - my @entries= @{$ical->{entries}}; - if($#entries<0) { - eval { require Compress::Zlib; }; - if($@) { - Log3 $hash, 1, "Calendar " . $hash->{NAME} . ": Maybe gzip data, but cannot load Compress::Zlib"; - } - else { - $ics = Compress::Zlib::memGunzip($ics); - $ical->parse(split("\n",$ics)); - @entries= @{$ical->{entries}}; - } - }; - if($#entries<0) { - Log3 $hash, 1, "Calendar " . $hash->{NAME} . ": Not an ical file at URL"; - $hash->{STATE}= "Not an ical file at URL"; - return 0; - }; - - my $root= @{$ical->{entries}}[0]; - my $calname= "?"; - if($root->{type} ne "VCALENDAR") { - Log3 $hash, 1, "Calendar " . $hash->{NAME} . ": Root element is not a VCALENDAR"; - $hash->{STATE}= "Root element is not a VCALENDAR"; - return 0; - } else { - $calname= $root->value("X-WR-CALNAME"); - } - - - $hash->{STATE}= "Active"; - - # we now create the events from it - #main::Debug "Creating events..."; - my $eventsObj= $hash->{fhem}{events}; - $eventsObj->updateFromCalendar($root,$removeall); - $hash->{fhem}{events}= $eventsObj; - - # we now update the readings - my @allevents= $eventsObj->events(); - - my @all= sort map { $_->uid() } @allevents; - my @new= sort map { $_->uid() } grep { $_->isNew() } @allevents; - my @updated= sort map { $_->uid() } grep { $_->isUpdated() } @allevents; - my @deleted = sort map { $_->uid() } grep { $_->isDeleted() } @allevents; - my @changed= sort (@new, @updated, @deleted); - - #$hash->{STATE}= $val; - readingsBeginUpdate($hash); - readingsBulkUpdate($hash, "calname", $calname); - readingsBulkUpdate($hash, "lastUpdate", $hash->{fhem}{lastUpdate}); - readingsBulkUpdate($hash, "all", join(";", @all)); - readingsBulkUpdate($hash, "stateNew", join(";", @new)); - readingsBulkUpdate($hash, "stateUpdated", join(";", @updated)); - readingsBulkUpdate($hash, "stateDeleted", join(";", @deleted)); - readingsBulkUpdate($hash, "stateChanged", join(";", @changed)); - readingsEndUpdate($hash, 1); # DoTrigger, because sub is called by a timer instead of dispatch - - return 1; -} - -################################### -sub Calendar_Set($@) { - my ($hash, @a) = @_; - - my $cmd= $a[1]; - $cmd= "?" unless($cmd); - - # usage check - if((@a == 2) && ($a[1] eq "update")) { - $hash->{fhem}{nxtUpdtTs}= 0; # force update - Calendar_Wakeup($hash,0); - return undef; - } elsif((@a == 2) && ($a[1] eq "reload")) { - $hash->{fhem}{nxtUpdtTs}= 0; # force update - Calendar_Wakeup($hash,1); # remove all events before update - return undef; - } else { - return "Unknown argument $cmd, choose one of update:noArg reload:noArg"; - } -} - -################################### -sub Calendar_Get($@) { - - my ($hash, @a) = @_; - - my $eventsObj= $hash->{fhem}{events}; - my @events; - - my $cmd= $a[1]; - $cmd= "?" unless($cmd); - - my @cmds2= qw/text full summary location alarm start end/; - if($cmd ~~ @cmds2) { - - return "argument is missing" if($#a < 2); - my $reading= $a[2]; - - # $reading is alarmed, all, changed, deleted, new, started, updated - # if $reading does not match any of these it is assumed to be a uid - if(defined($hash->{READINGS}{$reading})) { - @events= grep { my $uid= $_->uid(); $hash->{READINGS}{$reading}{VAL} =~ m/$uid/ } $eventsObj->events(); - } else { - @events= grep { $_->uid() eq $reading } $eventsObj->events(); - } - - my @texts; - - - if(@events) { - foreach my $event (sort { $a->start() <=> $b->start() } @events) { - push @texts, $event->asText() if $cmd eq "text"; - push @texts, $event->asFull() if $cmd eq "full"; - push @texts, $event->summary() if $cmd eq "summary"; - push @texts, $event->location() if $cmd eq "location"; - push @texts, $event->alarmTime() if $cmd eq "alarm"; - push @texts, $event->startTime() if $cmd eq "start"; - push @texts, $event->endTime() if $cmd eq "end"; - } - } - if(defined($a[3])) { - my $keep= $a[3]; - return "Argument $keep is not a number." unless($keep =~ /\d+/); - $keep= $#texts+1 if($keep> $#texts); - splice @texts, $keep if($keep>= 0); - } - return "" if($#texts<0); - return join("\n", @texts); - - } elsif($cmd eq "find") { - - return "argument is missing" if($#a != 2); - my $regexp= $a[2]; - my @uids; - eval { - foreach my $event ($eventsObj->events()) { - push @uids, $event->uid() if($event->summary() =~ m/$regexp/); - } - }; - Log3($hash, 2, "Calendar " . $hash->{NAME} . - ": The regular expression $regexp caused a problem: $@") if($@); - return join(";", @uids); - - } else { - return "Unknown argument $cmd, choose one of find text full summary location alarm start end"; - } - -} - ##################################### sub Calendar_Define($$) { @@ -1105,7 +1405,7 @@ sub Calendar_Define($$) { " define Calendar ical file [interval]" if(($#a < 4 && $#a > 5) || ($a[2] ne 'ical') || (($a[3] ne 'url') && ($a[3] ne 'file'))); - $hash->{STATE} = "Initialized"; + readingsSingleUpdate($hash, "state", "initialized", 1); my $name = $a[0]; my $type = $a[3]; @@ -1114,14 +1414,22 @@ sub Calendar_Define($$) { $interval= $a[5] if($#a==5); - $hash->{fhem}{type}= $type; - $hash->{fhem}{url}= $url; - $hash->{fhem}{interval}= $interval; - $hash->{fhem}{events}= Calendar::Events->new(); + $hash->{".fhem"}{type}= $type; + $hash->{".fhem"}{url}= $url; + $hash->{".fhem"}{interval}= $interval; + $hash->{".fhem"}{lastid}= 0; + $hash->{".fhem"}{vevents}= {}; + $hash->{".fhem"}{nxtUpdtTs}= 0; + #$attr{$name}{"hideOlderThan"}= 0; + #main::Debug "Interval: ${interval}s"; - $hash->{fhem}{nxtUpdtTs}= 0; - Calendar_Wakeup($hash,0); + # if initialization is not yet done, we do not wake up at this point already to + # avoid the following race condition: + # events are loaded from fhem.save and data are updated asynchronousy from + # non-blocking Http get + Calendar_Wakeup($hash, 0) if($init_done); + return undef; } @@ -1131,12 +1439,969 @@ sub Calendar_Undef($$) { my ($hash, $arg) = @_; - RemoveInternalTimer($hash); + Calendar_DisarmTimer($hash); return undef; } + +##################################### +sub Calendar_Attr(@) { + + my ($cmd, $name, @a) = @_; + + return undef unless($cmd eq "set"); + + my $hash= $defs{$name}; + + return "attr $name needs at least one argument." if(!@a); + + my $arg= $a[1]; + if($a[0] eq "onCreateEvent") { + if($arg !~ m/^{.*}$/s) { + return "$arg must be a perl command in curly brackets but you supplied $arg."; + } + } + + return undef; + +} + +################################### +sub Calendar_Notify($$) +{ + my ($hash,$dev) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + + return if($dev->{NAME} ne "global"); + return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); + + return if($attr{$name} && $attr{$name}{disable}); + + # update calendar after initialization or change of configuration + # wait 10 to 29 seconds to avoid congestion due to concurrent activities + Calendar_DisarmTimer($hash); + my $delay= 10+int(rand(20)); + + # delay removed until further notice + $delay= 2; + + Log3 $hash, 5, "Calendar $name: FHEM initialization or rereadcfg triggered update, delay $delay seconds."; + InternalTimer(time()+$delay, "Calendar_Wakeup", $hash, 0) ; + + return undef; +} + +################################### +sub Calendar_Set($@) { + my ($hash, @a) = @_; + + my $cmd= $a[1]; + $cmd= "?" unless($cmd); + + + my $t= time(); + # usage check + if((@a == 2) && ($a[1] eq "update")) { + Calendar_DisarmTimer($hash); + Calendar_GetUpdate($hash, $t, 0); + return undef; + } elsif((@a == 2) && ($a[1] eq "reload")) { + Calendar_DisarmTimer($hash); + Calendar_GetUpdate($hash, $t, 1); # remove all events before update + return undef; + } else { + return "Unknown argument $cmd, choose one of update:noArg reload:noArg"; + } +} + +################################### +sub Calendar_Get($@) { + + my ($hash, @a) = @_; + + my $t= time(); + + my $eventsObj= $hash->{".fhem"}{events}; + my @events; + + my $cmd= $a[1]; + $cmd= "?" unless($cmd); + + + if($cmd eq "update") { + # this is the same as set update for convenience + Calendar_DisarmTimer($hash); + Calendar_GetUpdate($hash, $t, 0); + return undef; + } + + if($cmd eq "reload") { + # this is the same as set reload for convenience + Calendar_DisarmTimer($hash); + Calendar_GetUpdate($hash, $t, 1); # remove all events before update + return undef; + } + + if($cmd eq "events") { + + my @texts; + my @events= Calendar_GetEvents($hash, $t, undef, undef); + foreach my $event (@events) { + push @texts, $event->asFull(); + } + return "" if($#texts<0); + return join("\n", @texts); + + } + + my @cmds2= qw/text full summary location alarm start end debug/; + if($cmd ~~ @cmds2) { + + return "argument is missing" if($#a < 2); + my $filter= $a[2]; + + + # $reading is alarm, all, changed, start, end, upcoming + my $filterref; + my $param= undef; + my $keeppos= 3; + if($filter eq "changed") { + $filterref= \&filter_changed; + } elsif($filter eq "alarm") { + $filterref= \&filter_alarm; + } elsif($filter eq "start") { + $filterref= \&filter_start; + } elsif($filter eq "end") { + $filterref= \&filter_end; + } elsif($filter eq "upcoming") { + $filterref= \&filter_upcoming; + } elsif($filter =~ /^uid=(.+)$/) { + $filterref= \&filter_uids; + $param= $1; + } elsif($filter =~ /^mode=(.+)$/) { + $filterref= \&filter_modes; + $param= $1; + } elsif(($filter =~ /^mode\w+$/) and (defined($hash->{READINGS}{$filter}))) { + #main::Debug "apply filter_reading"; + $filterref= \&filter_reading; + my @uids= split(";", $hash->{READINGS}{$filter}{VAL}); + $param= \@uids; + } elsif($filter eq "all") { + $filterref= undef; + } elsif($filter eq "next") { + $filterref= \&filter_notend; + $param= { }; # reference to anonymous (unnamed) empty hash, thus $ in $param + } else { # everything else is interpreted as uid + $filterref= \&filter_uid; + $param= $a[2]; + } + + @events= Calendar_GetEvents($hash, $t, $filterref, $param); + + # special treatment for next + if($filter eq "next") { + my %uids; # remember the UIDs + + # the @events are ordered by start time ascending + # they do contain all events that have not ended + @events= grep { + my $seen= defined($uids{$_->uid()}); + $uids{$_->uid()}= 1; + not $seen; + } @events; + + } + + my @texts; + + if(@events) { + foreach my $event (sort { $a->start() <=> $b->start() } @events) { + push @texts, $event->asText() if $cmd eq "text"; + push @texts, $event->asFull() if $cmd eq "full"; + push @texts, $event->asDebug() if $cmd eq "debug"; + push @texts, $event->summary() if $cmd eq "summary"; + push @texts, $event->location() if $cmd eq "location"; + push @texts, $event->description() if $cmd eq "description"; + push @texts, $event->alarmTime() if $cmd eq "alarm"; + push @texts, $event->startTime() if $cmd eq "start"; + push @texts, $event->endTime() if $cmd eq "end"; + } + } + if(defined($a[$keeppos])) { + my $keep= $a[$keeppos]; + return "Argument $keep is not a number." unless($keep =~ /\d+/); + $keep= $#texts+1 if($keep> $#texts); + splice @texts, $keep if($keep>= 0); + } + return "" if($#texts<0); + return join("\n", @texts); + + } elsif($cmd eq "vevents") { + + my %vevents= %{$hash->{".fhem"}{vevents}}; + my $s= ""; + foreach my $key (sort {$a<=>$b} keys %vevents) { + $s .= "$key: "; + $s .= $vevents{$key}->asString(); + $s .= "\n"; + } + return $s; + + } elsif($cmd eq "vcalendar") { + + return undef unless(defined($hash->{".fhem"}{iCalendar})); + return $hash->{".fhem"}{iCalendar} + + } elsif($cmd eq "find") { + + return "argument is missing" if($#a != 2); + my $regexp= $a[2]; + + my %vevents= %{$hash->{".fhem"}{vevents}}; + my %uids; + foreach my $id (keys %vevents) { + my $v= $vevents{$id}; + my @events= @{$v->{events}}; + if(@events) { + eval { + if($events[0]->summary() =~ m/$regexp/) { + $uids{$events[0]->uid()}= 1; # + } + } + } + Log3($hash, 2, "Calendar " . $hash->{NAME} . + ": The regular expression $regexp caused a problem: $@") if($@); + } + return join(";", keys %uids); + + } else { + return "Unknown argument $cmd, choose one of update:noArg reload:noArg find text full summary location description alarm start end vcalendar:noArg vevents:noArg"; + } + +} + +################################### +sub Calendar_Wakeup($$) { + + my ($hash, $removeall) = @_; + + Log3 $hash, 4, "Calendar " . $hash->{NAME} . ": Wakeup"; + + my $t= time(); # baseline + # we could arrive here 1 second before nextWakeTs for unknown reasons + use constant delta => 5; # avoid waking up again in a few seconds + if($t>= $hash->{".fhem"}{nxtUpdtTs} - delta) { + # GetUpdate does CheckTimes and RearmTimer asynchronously + Calendar_GetUpdate($hash, $t, $removeall); + } else { + Calendar_CheckTimes($hash, $t); + Calendar_RearmTimer($hash, $t); + } +} + +################################### +sub Calendar_RearmTimer($$) { + + my ($hash, $t) = @_; + + #main::Debug "RearmTimer now " . FmtDateTime($t); + my $nt= $hash->{".fhem"}{nxtUpdtTs}; + #main::Debug "RearmTimer next update " . FmtDateTime($nt); + # find next event + my %vevents= %{$hash->{".fhem"}{vevents}}; + foreach my $uid (keys %vevents) { + my $v= $vevents{$uid}; + foreach my $e (@{$v->{events}}) { + my $et= $e->nextTime($t); + # we only consider times in the future to avoid multiple + # invocations for calendar events with the event time + $nt= $et if(defined($et) && ($et< $nt) && ($et > $t)); + } + } + + $hash->{".fhem"}{nextWakeTs}= $nt; + $hash->{".fhem"}{nextWake}= FmtDateTime($nt); + #main::Debug "RearmTimer for " . $hash->{".fhem"}{nextWake}; + readingsSingleUpdate($hash, "nextWakeup", $hash->{".fhem"}{nextWake}, 1); + + if($nt< $t) { $nt= $t+1 }; # sanity check / do not wake-up at or before the same second + InternalTimer($nt, "Calendar_Wakeup", $hash, 0) ; + +} + +sub Calendar_DisarmTimer($) { + + my ($hash)= @_; + RemoveInternalTimer($hash); +} +# + +################################### +sub Calendar_GetSecondsFromTimeSpec($) { + + my ($tspec) = @_; + + # days + if($tspec =~ m/^([0-9]+)d$/) { + return ("", $1*86400); + } + + # seconds + if($tspec =~ m/^[0-9]+s?$/) { + return ("", $tspec); + } + + # D:HH:MM:SS + if($tspec =~ m/^([0-9]+):([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/) { + return ("", $4+60*($3+60*($2+24*$1))); + } + + # HH:MM:SS + if($tspec =~ m/^([0-9]+):([0-5][0-9]):([0-5][0-9])$/) { # HH:MM:SS + return ("", $3+60*($2+(60*$1))); + } + + # HH:MM + if($tspec =~ m/^([0-9]+):([0-5][0-9])$/) { + return ("", 60*($2+60*$1)); + } + + return ("Wrong time specification $tspec", undef); + +} + +################################### +# Filters + +sub filter_modes($$) { + my ($event,$regex)= @_; + my $hit; + eval { $hit= ($event->getMode() =~ $regex); }; + return 0 if($@); + return $hit ? 1 : 0; +} + +sub filter_uids($$) { + my ($event,$regex)= @_; + my $hit; + eval { $hit= ($event->uid() =~ $regex); }; + return 0 if($@); + return $hit ? 1 : 0; +} + +sub filter_changed($) { + my ($event)= @_; + return $event->modeChanged(); +} + +sub filter_alarm($) { + my ($event)= @_; + return $event->getMode() eq "alarm" ? 1 : 0; +} + +sub filter_start($) { + my ($event)= @_; + return $event->getMode() eq "start" ? 1 : 0; +} + +sub filter_end($) { + my ($event)= @_; + return $event->getMode() eq "end" ? 1 : 0; +} + +sub filter_notend($) { + my ($event)= @_; + return $event->getMode() eq "end" ? 0 : 1; +} + +sub filter_upcoming($) { + my ($event)= @_; + return $event->getMode() eq "upcoming" ? 1 : 0; +} + +sub filter_uid($$) { + my ($event, $param)= @_; + return $event->uid() eq "$param" ? 1 : 0; +} + +sub filter_reading($$) { + my ($event, $param)= @_; + my @uids= @{$param}; + #foreach my $u (@uids) { main::Debug "UID $u"; } + my $uid= $event->uid(); + #main::Debug "SUCHE $uid"; + #main::Debug "GREP: " . grep(/^$uid$/, @uids); + return grep(/^$uid$/, @uids); +} + + + + +################################### +sub Calendar_GetEvents($$$$) { + + my ($hash, $t, $filterref, $param)= @_; + my $name= $hash->{NAME}; + my @result= (); + + # time window + my ($error, $t1, $t2)= (undef, undef, undef); + my $hideOlderThan= AttrVal($name, "hideOlderThan", undef); + my $hideLaterThan= AttrVal($name, "hideLaterThan", undef); + + # start of time window + if(defined($hideOlderThan)) { + ($error, $t1)= Calendar_GetSecondsFromTimeSpec($hideOlderThan); + if($error) { + Log3 $hash, 2, "$name: attribute hideOlderThan: $error"; + } else { + $t1= $t- $t1; + } + } + + # end of time window + if(defined($hideLaterThan)) { + ($error, $t2)= Calendar_GetSecondsFromTimeSpec($hideLaterThan); + if($error) { + Log3 $hash, 2, "$name: attribute hideLaterThan: $error"; + } else { + $t2= $t+ $t2; + } + } + + # get and filter events + my %vevents= %{$hash->{".fhem"}{vevents}}; + foreach my $id (keys %vevents) { + my $v= $vevents{$id}; + my @events= @{$v->{events}}; + foreach my $event (@events) { + if(defined($filterref)) { + next unless(&$filterref($event, $param)); + } + if(defined($t1)) { next if($event->end() < $t1); } + if(defined($t2)) { next if($event->start() > $t2); } + push @result, $event; + } + } + return sort { $a->start() <=> $b->start() } @result; + +} + +################################### +sub Calendar_GetUpdate($$$) { + + my ($hash, $t, $removeall) = @_; + my $name= $hash->{NAME}; + + $hash->{".fhem"}{lstUpdtTs}= $t; + $hash->{".fhem"}{lastUpdate}= FmtDateTime($t); + + my $nut= $t+ $hash->{".fhem"}{interval}; + $hash->{".fhem"}{nxtUpdtTs}= $nut; + $hash->{".fhem"}{nextUpdate}= FmtDateTime($nut); + + #main::Debug "Getting update now: " . $hash->{".fhem"}{lastUpdate}; + #main::Debug "Next Update is at : " . $hash->{".fhem"}{nextUpdate}; + + Log3 $hash, 4, "Calendar $name: Updating..."; + my $type = $hash->{".fhem"}{type}; + my $url= $hash->{".fhem"}{url}; + + my $errmsg= ""; + my $ics; + + if($type eq "url") { + + HttpUtils_NonblockingGet({ + url => $url, + hideurl => 1, + noshutdown => 1, + hash => $hash, + timeout => 30, + type => 'caldata', + removeall => $removeall, + t => $t, + callback => \&Calendar_ProcessUpdate, + }); + Log3 $hash, 4, "Calendar $name: Getting data from URL "; # $url + + } elsif($type eq "file") { + + Log3 $hash, 4, "Calendar $name: Getting data from file $url"; + if(open(ICSFILE, $url)) { + while() { + $ics .= $_; + } + close(ICSFILE); + + my $paramhash; + $paramhash->{hash} = $hash; + $paramhash->{removeall} = $removeall; + $paramhash->{t} = $t; + $paramhash->{type} = 'caldata'; + Calendar_ProcessUpdate($paramhash, '', $ics); + return undef; + + } else { + Log3 $hash, 1, "Calendar $name: Could not open file $url"; + readingsSingleUpdate($hash, "state", "error (could not open file)", 1); + return 0; + } + } else { + # this case never happens by virtue of _Define, so just + die "Software Error"; + } + +} + + +################################### +sub Calendar_ProcessUpdate($$$) { + + my ($param, $errmsg, $ics) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $removeall = $param->{removeall}; + my $t= $param->{t}; + + delete($hash->{".fhem"}{iCalendar}); + + if($errmsg) { + Log3 $name, 1, "Calendar $name: retrieval failed with error message $errmsg"; + readingsSingleUpdate($hash, "state", "error ($errmsg)", 1); + } else { + readingsSingleUpdate($hash, "state", "retrieved", 1); + } + + if($errmsg or !defined($ics) or ("$ics" eq "") ) { + Log3 $hash, 1, "Calendar $name: retrieved no or empty data"; + readingsSingleUpdate($hash, "state", "error (no or empty data)", 1); + } else { + Calendar_UpdateCalendar($hash, $t, $ics, $removeall); + } + + #main::Debug "Calendar $name: iCalendar=\n$ics"; + + Calendar_CheckTimes($hash, $t); + Calendar_RearmTimer($hash, $t); + +} + +################################### +sub Calendar_UpdateCalendar($$$$) { + + my ($hash, $t, $ics, $removeall) = @_; + + + # ********************* + # *** Step 1 Parsing + # ********************* + + # + # 1 + # + + $hash->{".fhem"}{iCalendar}= $ics; # the plain text iCalendar + + # + # 2 Parsing + # + + my $name= $hash->{NAME}; + Log3 $hash, 4, "Calendar $name: parsing data"; + #main::Debug "Calendar $name: parsing data"; + + + # we remove disturbing CRs from the file + $ics =~ s/[\r\n]+/\n/g; + + # we parse the calendar into a recursive ICal::Entry structure + my $ical= ICal::Entry->new("root"); + $ical->parse(split("\n",$ics)); + + #main::Debug "*** Result:"; + #main::Debug $ical->asString(); + + my @entries= @{$ical->{entries}}; + if($#entries<0) { + eval { require Compress::Zlib; }; + if($@) { + readingsSingleUpdate($hash, "state", + "error (data not in ICal format or no Compress::Zlib)", 1); + Log3 $hash, 1, "Calendar $name: maybe gzip data, but cannot load Compress::Zlib"; + } + else { + Log3 $hash, 4, "Calendar $name: unzipping data"; + $ics = Compress::Zlib::memGunzip($ics); + $ical->parse(split("\n",$ics)); + @entries= @{$ical->{entries}}; + } + }; + if($#entries<0) { + Log3 $hash, 1, "Calendar $name: data not in ICal format"; + readingsSingleUpdate($hash, "state", "error (data not in ICal format)", 1); + return 0; + }; + + my $root= @{$ical->{entries}}[0]; + my $calname= "?"; + if($root->{type} ne "VCALENDAR") { + Log3 $hash, 1, "Calendar $name: root element is not VCALENDAR"; + readingsSingleUpdate($hash, "state", "error (root element is not VCALENDAR)", 1); + return 0; + } else { + $calname= $root->value("X-WR-CALNAME"); + } + + + # ********************* + # *** Step 2 Merging + # ********************* + + Log3 $hash, 4, "Calendar $name: merging data"; + #main::Debug "Calendar $name: merging data"; + + # this the hash of VEVENTs that have been created on the previous update + my %vevents; + %vevents= %{$hash->{".fhem"}{vevents}} if(!$removeall); + + # the keys to the hash are numbers taken from a sequence + my $lastid= $hash->{".fhem"}{lastid}; + + # + # 1, 2, 4 + # + + # we first discard all VEVENTs that have been tagged as deleted in the previous run + # and untag the rest + foreach my $key (keys %vevents) { + #main::Debug "Preparing id $key..."; + if($vevents{$key}->isObsolete() ) { + delete($vevents{$key}); + } else { + $vevents{$key}->setState("deleted"); # will be changed if record is touched in the next step + $vevents{$key}->clearCounterpart(); + $vevents{$key}->clearReferences(); + } + } + + # + # 3 + # + + # we now run through the list of freshly retrieved VEVENTs and merge them into + # the hash + my ($n, $nknown, $nmodified, $nnew, $nchanged)= (0,0,0,0,0,0); + + # this code is O(n^2) and stalls FHEM for large numbers of VEVENTs + # to speed up the code we first build a reverse hash (UID,RECURRENCE-ID) -> id + sub kf($) { my ($v)= @_; return $v->value("UID").$v->valueOrDefault("RECURRENCE-ID","") } + + my %lookup; + foreach my $id (keys %vevents) { + my $k= kf($vevents{$id}); + Log3 $hash, 2, "Calendar $name: Duplicate VEVENT" if(defined($lookup{$k})); + $lookup{$k}= $id; + #main::Debug "Adding event $id with key $k to lookup hash."; + } + + foreach my $v (grep { $_->{type} eq "VEVENT" } @{$root->{entries}}) { + #main::Debug "Merging " . $v->asString(); + my $found= 0; + my $added= 0; # flag to prevent multiple additions + $n++; + # some braindead calendars provide no UID - add one: + $v->addproperty(sprintf("UID:synthetic-%06d", $v->{ln})) + unless($v->hasKey("UID") or !defined($v->{ln})); + # look for related records in the old record set + my $k= kf($v); + #main::Debug "Looking for event with key $k"; + my $id= $lookup{$k}; + if(defined($id)) { + my $v0= $vevents{$id}; + #main::Debug "Found $id"; + + # + # same UID and RECURRENCE-ID + # + $found++; + if($v0->sameValue($v, "SEQUENCE")) { + # + # and same SEQUENCE + # + if($v0->sameValue($v, "LAST-MODIFIED")) { + # + # is not modified + # + # we only keep the record from the old record set + $v0->setState("known"); + $nknown++; + } else { + # + # is modified + # + # we keep both records + next if($added); + $added++; + $vevents{++$lastid}= $v; + $v->setState("modified-new"); + $v->setCounterpart($id); + $v0->setState("modified-old"); + $v0->setCounterpart($lastid); + $nmodified++; + } + } else { + # + # and different SEQUENCE + # + # we keep both records + next if($added); + $added++; + $vevents{++$lastid}= $v; + $v->setState("changed-new"); + $v->setCounterpart($id); + $v0->setState("changed-old"); + $v0->setCounterpart($lastid); + $nchanged++; + } + } + + if(!$found) { + $v->setState("new"); + $vevents{++$lastid}= $v; + $added++; + $nnew++; + } + } + + # + # Cross-referencing series + # + # this code is O(n^2) and stalls FHEM for large numbers of VEVENTs + # to speed up the code we build a hash of a hash UID => {id => VEVENT} + %lookup= (); + foreach my $id (keys %vevents) { + my $v= $vevents{$id}; + $lookup{$v->value("UID")}{$id}= $v unless($v->isObsolete); + } + for my $idref (values %lookup) { + my %vs= %{$idref}; + foreach my $v (values %vs) { + foreach my $id (keys %vs) { + push @{$v->references()}, $id unless($vs{$id} eq $v); + } + } + } + +# foreach my $id (keys %vevents) { +# my $v= $vevents{$id}; +# next if($v->isObsolete()); +# foreach my $id0 (keys %vevents) { +# next if($id==$id0); +# my $v0= $vevents{$id0}; +# next if($v0->isObsolete()); +# push @{$v0->references()}, $id if($v->sameValue($v0, "UID")); +# } +# } + + + Log3 $hash, 4, "Calendar $name: $n records processed, $nnew new, ". + "$nknown known, $nmodified modified, $nchanged changed."; + + # save the VEVENTs hash and lastid + $hash->{".fhem"}{vevents}= \%vevents; + $hash->{".fhem"}{lastid}= $lastid; + + # ********************* + # *** Step 3 Events + # ********************* + + + # + # Recreating the events + # + Log3 $hash, 4, "Calendar $name: creating calendar events"; + #main::Debug "Calendar $name: creating calendar events"; + + foreach my $id (keys %vevents) { + my $v= $vevents{$id}; + if($v->isObsolete()) { + $v->clearEvents(); + next; + } + + my $onCreateEvent= AttrVal($name, "onCreateEvent", undef); + if($v->hasChanged() or !$v->numEvents()) { + #main::Debug "createEvents"; + $v->createEvents($t, $onCreateEvent, %vevents); + } + + } + + #main::Debug "*** Result:"; + #main::Debug $ical->asString(); + + + # ********************* + # *** Step 4 Readings + # ********************* + + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, "calname", $calname); + readingsBulkUpdate($hash, "lastUpdate", $hash->{".fhem"}{lastUpdate}); + readingsBulkUpdate($hash, "nextUpdate", $hash->{".fhem"}{nextUpdate}); + readingsEndUpdate($hash, 1); # DoTrigger, because sub is called by a timer instead of dispatch + + + + + return 1; +} + + +################################### +sub Calendar_CheckTimes($$) { + + my ($hash, $t) = @_; + + Log3 $hash, 4, "Calendar " . $hash->{NAME} . ": Checking times..."; + + # + # determine the uids of all events and their most interesting mode + # + my %priority= ( + "none" => 0, + "end" => 1, + "upcoming" => 2, + "alarm" => 3, + "start" => 4, + ); + my %mim; # most interesting mode per id + my %changed; # changed per id + my %vevents= %{$hash->{".fhem"}{vevents}}; + foreach my $uid (keys %vevents) { + my $v= $vevents{$uid}; + foreach my $e (@{$v->{events}}) { + my $uid= $e->uid(); + my $mode= defined($mim{$uid}) ? $mim{$uid} : "none"; + if($e->isEnded($t)) { + $e->setMode("end"); + } elsif($e->isUpcoming($t)) { + $e->setMode("upcoming"); + } elsif($e->isStarted($t)) { + $e->setMode("start"); + } elsif($e->isAlarmed($t)) { + $e->setMode("alarm"); + } + if($priority{$e->getMode()} > $priority{$mode}) { + $mim{$uid}= $e->getMode(); + } + $changed{$uid}= 0 unless(defined($changed{$uid})); + # create the FHEM event + if($e->modeChanged()) { + $changed{$uid}= 1; + addEvent($hash, "changed: $uid " . $e->getMode()); + addEvent($hash, $e->getMode() . ": $uid "); + } + } + } + + # + # determine the uids of events in certain modes + # + my @changed; + my @upcoming; + my @start; + my @started; + my @alarm; + my @alarmed; + my @end; + my @ended; + foreach my $uid (keys %mim) { + push @changed, $uid if($changed{$uid}); + push @upcoming, $uid if($mim{$uid} eq "upcoming"); + if($mim{$uid} eq "alarm") { + push @alarm, $uid; + push @alarmed, $uid if($changed{$uid}); + } + if($mim{$uid} eq "start") { + push @start, $uid; + push @started, $uid if($changed{$uid}); + } + if($mim{$uid} eq "end") { + push @end, $uid; + push @ended, $uid if($changed{$uid}); + } + } + + + #sub uniq { my %uids; return grep {!$uids{$_->uid()}++} @_; } + + + #@allevents= sort { $a->start() <=> $b->start() } uniq(@allevents); + + + #foreach my $event (@allevents) { + # main::Debug $event->asFull(); + #} + + + sub es(@) { + my (@events)= @_; + return join(";", @events); + } + + sub rbu($$$) { + my ($hash, $reading, $value)= @_; + if(!defined($hash->{READINGS}{$reading}) or + ($hash->{READINGS}{$reading}{VAL} ne $value)) { + readingsBulkUpdate($hash, $reading, $value); + } + } + + # clears all events in CHANGED, thus must be called first + readingsBeginUpdate($hash); + # we update the readings + rbu($hash, "modeUpcoming", es(@upcoming)); + rbu($hash, "modeAlarm", es(@alarm)); + rbu($hash, "modeAlarmed", es(@alarmed)); + rbu($hash, "modeAlarmOrStart", es(@alarm,@start)); + rbu($hash, "modeChanged", es(@changed)); + rbu($hash, "modeStart", es(@start)); + rbu($hash, "modeStarted", es(@started)); + rbu($hash, "modeEnd", es(@end)); + rbu($hash, "modeEnded", es(@ended)); + readingsBulkUpdate($hash, "state", "triggered"); + # DoTrigger, because sub is called by a timer instead of dispatch + readingsEndUpdate($hash, 1); + +} + + ##################################### +# filter:next count:3 +sub CalendarAsHtml($;$) { + + my ($d,$o) = @_; + $d = "" if(!$d); + return "$d is not a Calendar instance
" + if(!$defs{$d} || $defs{$d}{TYPE} ne "Calendar"); + + my $l= Calendar_Get($defs{$d}, split("[ \t]+", "- text $o")); + my @lines= split("\n", $l); + + my $ret = ''; + + foreach my $line (@lines) { + my @fields= split(" ", $line, 3); + $ret.= sprintf("", @fields); + } + $ret .= '
%s%s%s
'; + + return $ret; +} ##################################### @@ -1152,7 +2417,7 @@ sub Calendar_Undef($$) {
- Define + Define

    define <name> Calendar ical url <URL> [<interval>]
    define <name> Calendar ical file <FILENAME> [<interval>]
    @@ -1167,8 +2432,6 @@ sub Calendar_Undef($$) { (use cpan -i IO::Socket::SSL).

    Note for users of Google Calendar: You can literally use the private ICal URL from your Google Calendar. - If your Google Calendar URL starts with https:// and the perl module IO::Socket::SSL is not installed on your system, you can replace it by http:// if and only if there is no redirection to the https:// URL. @@ -1187,15 +2450,12 @@ sub Calendar_Undef($$) {
    - Set + Set

      - set <name> update

      - - Forces the retrieval of the calendar from the URL. The next automatic retrieval is scheduled to occur - interval seconds later.

      - - set <name> reload

      - + set <name> update
      + Forces the retrieval of the calendar from the URL. The next automatic retrieval is scheduled to occur interval seconds later.

      + + set <name> reload
      Same as update but all calendar events are removed first.

    @@ -1203,81 +2463,163 @@ sub Calendar_Undef($$) { - Get + Get

      - get <name> full|text|summary|location|alarm|start|end <reading>|<uid> [max]

      + get <name> update
      + Same as set <name> update

      + + get <name> reload
      + Same as set <name> update

      + + get <name> <format> <filter> [<max>]
      + Returns, line by line, information on the calendar events in the calendar <name>. The content depends on the + <format> specifier:

      + + + + + + + + + + + +
      <format>content
      texta user-friendly textual representation, best suited for display
      summarythe content of the summary field (subject, title)
      locationthe content of the location field
      alarmalarm time in human-readable format
      startstart time in human-readable format
      endend time in human-readable format
      fullthe full state
      debuglike full with additional information for debugging purposes

      - Returns, line by line, the full state or a textual representation or the summary (subject, title) or the - location or the alarm time or the start time or the end time - of the calendar event(s) listed in the - reading <reading> or identified by the UID <uid>. The optional parameter max limits + The <filter> specifier determines the selected subset of calendar events:

      + + + + + + + + + + +
      <filter>selection
      mode=<regex>all calendar events with mode matching the regular expression <regex>
      <mode>all calendar events in the mode <mode>
      uid=<regex>all calendar events identified by UIDs that match the regular expression <regex>.
      <uid>all calendar events identified by the UID <uid>
      <reading>all calendar events listed in the reading <reading> (modeAlarm, modeAlarmed, modeStart, etc.) - this is deprecated and will be removed in a future version, use mode=<regex> instead.
      allall calendar events (past, current and future)
      nextonly calendar events that have not yet ended and among these only the first in a series, best suited for display

      + + The mode=<regex> and uid=<regex> filters should be preferred over the + <mode> and <uid> filters.

      + + The optional parameter <max> limits the number of returned lines.

      - - get <name> find <regexp>

      - + + See attributes hideOlderThan and + hideLaterThan for how to return events within a certain time window. + Please remember that the global ±400 days limits apply.

      + + Examples:
      + get MyCalendar text next
      + get MyCalendar summary uid:435kjhk435googlecom 1
      + get MyCalendar summary 435kjhk435googlecom 1
      + get MyCalendar full all
      + get MyCalendar text mode=alarm|start
      + get MyCalendar text uid=.*6286.*
      +
      + + get <name> find <regexp>
      Returns, line by line, the UIDs of all calendar events whose summary matches the regular expression <regexp>.

      - + + get <name> vcalendar
      + Returns the calendar in ICal format as retrieved from the source.

      + + get <name> vevents
      + Returns a list of all VEVENT entries in the calendar with additional information for + debugging. Only properties that have been kept during processing of the source + are shown. The list of calendar events created from each VEVENT entry is shown as well + as the list of calendar events that have been omitted. +

    Attributes +

      +
    • hideOlderThan <timespec>
      + hideLaterThan <timespec>

      + + These attributes limit the list of events shown by + get <name> full|debug|text|summary|location|alarm|start|end ....

      + + The time is specified relative to the current time t. If hideOlderThan is set, + calendar events that ended before t-hideOlderThan are not shown. If hideLaterThan is + set, calendar events that will start after t+hideLaterThan are not shown.

      + + <timespec> must have one of the following formats:
      + + + + + + + + +
      formatdescriptionexample
      SSSseconds3600
      SSSsseconds3600s
      HH:MMhours:minutes02:30
      HH:MM:SShours:minutes:seconds00:01:30
      D:HH:MM:SSdays:hours:minutes:seconds122:10:00:00
      DDDddays100d

    • +

      + +

    • onCreateEvent <perl-code>
      + + This attribute allows to run the Perl code <perl-code> for every + calendar event that is created. See section Plug-ins below. +
    • +

    • readingFnAttributes

    Description
      - - A calendar is a set of calendar events. A calendar event has a summary (usually the title shown in a visual - representation of the source calendar), a start time, an end time, and zero, one or more alarm times. The calendar events are - fetched from the source calendar at the given URL.

      +
      + A calendar is a set of calendar events. The calendar events are + fetched from the source calendar at the given URL on a regular basis.

      - In case of multiple alarm times for a calendar event, only the + A calendar event has a summary (usually the title shown in a visual + representation of the source calendar), a start time, an end time, and zero, one or more alarm times. In case of multiple alarm times for a calendar event, only the earliest alarm time is kept.

      - Recurring calendar events are currently supported to an extent: + Recurring calendar events (series) are currently supported to an extent: FREQ INTERVAL UNTIL COUNT are interpreted, BYMONTHDAY BYMONTH WKST are recognized but not interpreted. BYDAY is only correctly interpreted for weekly events. The module will get it most likely wrong if you have recurring calendar events with unrecognized or uninterpreted keywords. + Out-of-order events and events excluded from a series (EXDATE) are handled.

      + + Calendar events are created when FHEM is started or when the respective entry in the source + calendar has changed and the calendar is updated or when the calendar is reloaded with + get <name> reload. + Only calendar events within ±400 days around the event creation time are created. Consider + reloading the calendar from time to time to avoid running out of upcoming events. You can use something like define reloadCalendar at +*240:00:00 set MyCalendar reload for that purpose.

      + + Some dumb calendars do not use LAST-MODIFIED. This may result in modifications in the source calendar + go unnoticed. Reload the calendar if you experience this issue.

      - A calendar event is identified by its UID. The UID is taken from the source calendar. All non-alphanumerical characters - are stripped off the UID to make your life easier.

      - - A calendar event can be in one of the following states: - - - - - -
      newThe calendar event was first seen at the most recent update. Either this was your first retrieval of - the calendar or you newly added the calendar event to the source calendar.
      knownThe calendar event was already there before the most recent update.
      updatedThe calendar event was already there before the most recent update but it has changed since it - was last retrieved.
      deletedThe calendar event was there before the most recent update but is no longer. You removed it from the source calendar. The calendar event will be removed from all lists at the next update.

      - Calendar events that lie completely in the past (current time on wall clock is later than the calendar event's end time) - are not retrieved and are thus not accessible through the calendar. -

      - + A calendar event is identified by its UID. The UID is taken from the source calendar. + All events in a series including out-of-order events habe the same UID. + All non-alphanumerical characters + are stripped off the original UID to make your life easier.

      + A calendar event can be in one of the following modes: - +
      upcomingNeither the alarm time nor the start time of the calendar event is reached.
      alarmThe alarm time has passed but the start time of the calendar event is not yet reached.
      startThe start time has passed but the end time of the calendar event is not yet reached.
      endThe end time of the calendar event has passed.

      + A calendar event transitions from one mode to another immediately when the time for the change has come. This is done by waiting for the earliest future time among all alarm, start or end times of all calendar events.

      A calendar device has several readings. Except for calname, each reading is a semicolon-separated list of UIDs of calendar events that satisfy certain conditions: - +
      - @@ -1287,34 +2629,83 @@ sub Calendar_Undef($$) { - - - -
      calnamename of the calendar
      allall events
      modeAlarmevents in alarm mode
      modeAlarmOrStartevents in alarm or start mode
      modeAlarmedevents that have just transitioned from upcoming to alarm mode
      modeStartevents in start mode
      modeStartedevents that have just transitioned to start mode
      modeUpcomingevents in upcoming mode
      stateChangedevents that have just changed their state somehow
      stateDeletedevents in state deleted
      stateNewevents in state new
      stateUpdatedevents in state updated

    - When a calendar event has changed, an event is created in the form - changed: UID mode with mode being the current mode the calendar event is in after the change. + For recurring events, usually several calendar events exists with the same UID. In such a case, + the UID is only shown in the mode reading for the most interesting mode. The most + interesting mode is the first applicable of start, alarm, upcoming, end.

    + In particular, you will never see the UID of a series in modeEnd or modeEnded as long as the series + has not yet ended - the UID will be in one of the other mode... readings. This means that you better + do not trigger FHEM events for series based on mode... readings. See below for a recommendation.

    + + Events +


      + When the calendar was reloaded or updated or when an alarm, start or end time was reached, one + FHEM event is created:

      + + triggered

      + + When you receive this event, you can rely on the calendar's readings being in a consistent and + most recent state.

      + + + When a calendar event has changed, two FHEM events are created:

      + + changed: UID <mode>
      + <mode>: UID

      + + <mode> is the current mode of the calendar event after the change. Note: there is a + colon followed by a single space in the FHEM event specification.

      + + The recommended way of reacting on mode changes of calendar events is to get notified + on the aforementioned FHEM events and do not check for the FHEM events triggered + by a change of a mode reading.

      - - Usage scenarios +

    + + + Plug-ins
      +
      + This is experimental. Use with caution.

      + + A plug-in is a piece of Perl code that modifies a calendar event on the fly. The Perl code operates on the + hash reference $e. The most important elements are as follows: + + + + + + + + +
      codedescription
      $e->{start}the start time of the calendar event, in seconds since the epoch
      $e->{end}the end time of the calendar event, in seconds since the epoch
      $e->{alarm}the alarm time of the calendar event, in seconds since the epoch
      $e->{summary}the summary (caption, title) of the calendar event
      $e->{location}the location of the calendar event

      + + To add or change the alarm time of a calendar event for all events with the string "Tonne" in the + summary, the following plug-in can be used:

      + attr MyCalendar onCreateEvent { $e->{alarm}= $e->{start}-86400 if($e->{summary} =~ /Tonne/);; }
      +
      The double semicolon masks the semicolon. Perl specials cannot be used.
      +

    +

    + + Usage scenarios +


      Show all calendar events with details

        get MyCalendar full all
        - 2767324dsfretfvds7dsfn3e4­dsa234r234sdfds6bh874­googlecom known alarm 31.05.2012 17:00:00 07.06.2012 16:30:00-07.06.2012 18:00:00 Erna for coffee
        - 992hydf4y44awer5466lhfdsr­gl7tin6b6mckf8glmhui4­googlecom known upcoming 08.06.2012 00:00:00-09.06.2012 00:00:00 Vacation + 2767324dsfretfvds7dsfn3e4­dsa234r234sdfds6bh874­googlecom alarm 31.05.2012 17:00:00 07.06.2012 16:30:00-07.06.2012 18:00:00 Erna for coffee
        + 992hydf4y44awer5466lhfdsr­gl7tin6b6mckf8glmhui4­googlecom upcoming 08.06.2012 00:00:00-09.06.2012 00:00:00 Vacation


      Show calendar events in your photo frame

        Put a line in the layout description to show calendar events in alarm or start mode:

        - text 20 60 { fhem("get MyCalendar text modeAlarmOrStart") }

        + text 20 60 { fhem("get MyCalendar text next 2") }

        This may look like:

        07.06.12 16:30 Erna for coffee
        @@ -1329,13 +2720,13 @@ sub Calendar_Undef($$) { get MyCalendar find .*Erna.*
        2767324dsfretfvds7dsfn3e4­dsa234r234sdfds6bh874­googlecom


        - Then define a notify:

        + Then define a notify (the dot after the second colon matches the space):

        - define ErnaComes notify MyCalendar:modeStarted.*2767324dsfretfvds7dsfn3e4­dsa234r234sdfds6bh874­googlecom.* set MyLight on + define ErnaComes notify MyCalendar:start:.2767324dsfretfvds7dsfn3e4­dsa234r234sdfds6bh874­googlecom set MyLight on

        You can also do some logging:

        - define LogErna notify MyCalendar:modeAlarmed.*2767324dsfretfvds7dsfn3e4­dsa234r234sdfds6bh874­googlecom.* { Log3 $NAME, 1, "ALARM name=$NAME event=$EVENT part1=$EVTPART0 part2=$EVTPART1" } + define LogErna notify MyCalendar:alarm:.2767324dsfretfvds7dsfn3e4­dsa234r234sdfds6bh874­googlecom { Log3 $NAME, 1, "ALARM name=$NAME event=$EVENT part1=$EVTPART0 part2=$EVTPART1" }

      @@ -1343,8 +2734,8 @@ sub Calendar_Undef($$) {
        Think about a calendar with calendar events whose summaries (subjects, titles) are the names of devices in your fhem installation. You want the respective devices to switch on when the calendar event starts and to switch off when the calendar event ends.

        - - define SwitchActorOn notify MyCalendar:modeStarted.* { + + define SwitchActorOn notify MyCalendar:start:.* { my $reading="$EVTPART0";; my $uid= "$EVTPART1";; my $actor= fhem("get MyCalendar summary $uid");; @@ -1352,7 +2743,7 @@ sub Calendar_Undef($$) { fhem("set $actor on") } }

        - define SwitchActorOff notify MyCalendar:modeEnded.* { + define SwitchActorOff notify MyCalendar:end:.* { my $reading="$EVTPART0";; my $uid= "$EVTPART1";; my $actor= fhem("get MyCalendar summary $uid");; @@ -1363,13 +2754,26 @@ sub Calendar_Undef($$) {


        You can also do some logging:

        - define LogActors notify MyCalendar:mode(Started|Ended).* { my $reading= "$EVTPART0";; my $uid= "$EVTPART1";; my $actor= fhem("get MyCalendar summary $uid");; Log 3 $NAME, 1, "Actor: $actor, Reading $reading" } + define LogActors notify MyCalendar:start|end:.* { my $reading= "$EVTPART0";; my $uid= "$EVTPART1";; my $actor= fhem("get MyCalendar summary $uid");; Log 3 $NAME, 1, "Actor: $actor, Reading $reading" }

    + + Embedded HTML +

      + The module provides an additional function CalendarAsHtml(<name>,<options>). It + returns the HTML code for a list of calendar events. <name> is the name of the + Calendar device and <options> is what you would write after get <name> text .... +

      + Example: define MyCalendarWeblink weblink htmlCode { CalendarAsHtml("MyCalendar","next 3") } +

      + This is a rudimentary function which might be extended in a future version. +

      +

    +
@@ -1378,10 +2782,10 @@ sub Calendar_Undef($$) { =begin html_DE -

Kalender

+

Calender


    - + Define
      @@ -1390,18 +2794,19 @@ sub Calendar_Undef($$) {
      Definiert ein Kalender-Device.

      - Ein Kalender-Device ermittelt periodisch Ereignisse aus einem Quell-Kalender. Dieser kann eine URL oder eine Datei sein. Die Datei muss im iCal-Format vorliegen.

      + Ein Kalender-Device ermittelt (Serien-) Termine aus einem Quell-Kalender. Dieser kann eine URL oder eine Datei sein. + Die Datei muss im iCal-Format vorliegen.

      Beginnt die URL mit https://, muss das Perl-Modul IO::Socket::SSL installiert sein (use cpan -i IO::Socket::SSL).

      Hinweis für Nutzer des Google-Kalenders: Du kann direkt die private iCal-URL des Google Kalender nutzen. - - Sollte Deine Google-Kalender-URL mit https:// beginnen und das Perl-Modul IO::Socket::SSL ist nicht auf Deinem Systeme installiert, kannst Du in der URL https:// durch http:// ersetzen, falls keine automatische Umleitung auf die https:// URL erfolgt. + + Sollte Deine Google-Kalender-URL mit https:// beginnen und das Perl-Modul IO::Socket::SSL ist nicht auf Deinem Systeme installiert, + kannst Du in der URL https:// durch http:// ersetzen, falls keine automatische Umleitung auf die https:// URL erfolgt. Solltest Du unsicher sein, ob dies der Fall ist, überprüfe es bitte zuerst mit Deinem Browser.

      - Der optionale Paramter interval bestimmt die Zeit in Sekunden zwischen den Updates. Default-Wert ist 3600 (1 Stunde).

      + Der optionale Parameter interval bestimmt die Zeit in Sekunden zwischen den Updates. Default-Wert ist 3600 (1 Stunde).

      Beispiele:
      @@ -1413,62 +2818,152 @@ sub Calendar_Undef($$) {
         
      - Set + Set

        - set <name> update

        + set <name> update
        - Erzwingt das Einlesen des Kalenders von der definierten URL. Das nächste automatische Einlesen erfolgt + Erzwingt das Einlesen des Kalenders von der definierten URL. Das nächste automatische Einlesen erfolgt in interval Sekunden später.

        + + set <name> reload
        + Dasselbe wie update, jedoch werden zuerst alle Termine entfernt.

        +

      - Get + Get

        - get <name> full|text|summary|location|alarm|start|end <reading>|<uid> [max]

        + get <name> update
        + Entspricht set <name> update

        - Gibt - Zeile für Zeile - den vollen Status, eine textbasierte Darstellung, eine Zusammenfassung (Betreff, Titel), den Ort, die Alarmzeit, die Startzeit oder die Endzeit des/der Kalender-Ereignisse aus, die im Reading <reading> gelistet werden oder die durch die UID <uid> identifiziert werden. Der optionale Parameter max limitiert die Anzahl der zurückgegebenen Lines.

        + get <name> reload
        + Entspricht set <name> reload

        - get <name> find <regexp>

        + get <name> <format> <filter> [<max>]
        + Die Termine für den Kalender <name> werden Zeile für Zeile ausgegeben.

        - Gibt - Zeile für Zeile - die UIDs aller Kalender-Ereignisse zurück, deren Zusammenfassung der Regular Expression - <regexp> entspricht.

        + Folgende Selektoren/Filter stehen zur Verfügung:

        + Der Selektor <format> legt den zurückgegeben Inhalt fest:

        + + + + + + + + + + + +
        <format>Inhalt
        textBenutzer-/Monitorfreundliche Textausgabe.
        summaryÜbersicht (Betreff, Titel)
        locationOrt
        alarmAlarmzeit
        startStartzeit
        endEndezeit
        fullVollständiger Status
        debugwie <full> mit zusätzlichen Informationen zur Fehlersuche

        + + Der Filter <filter> grenzt die Termine ein:

        + + + + + + + + + + +
        <filter>Inhalt
        mode=<regex>alle Termine, deren Modus durch den regulären Ausdruck <regex> beschrieben werden.
        <mode>alle Termine mit Modus <mode>.
        uid=<regex>Alle Termine, deren UIDs durch den regulären Ausdruck <regex> beschrieben werden.
        <uid>Alle Termine mit der UID <uid>
        <reading>Alle Termine die im Reading <reading> aufgelistet werden (modeAlarm, modeAlarmed, modeStart, etc.) + - dieser Filter ist abgekündigt und steht in einer zukünftigen Version nicht mehr zur Verfügung, bitte mode=<regex> benutzen.
        allAlle Termine (vergangene, aktuelle und zukünftige)
        nextAlle Termine, die noch nicht beendet sind. Bei Serienterminen der erste Termin. Benutzer-/Monitorfreundliche Textausgabe

        + + Die Filter mode=<regex> und uid=<regex> sollten den Filtern + <mode> und <uid> vorgezogen werden.

        + + Der optionale Parameter <max> schränkt die Anzahl der zurückgegebenen Zeilen ein.

        + + Bitte beachte die Attribute hideOlderThan und + hideLaterThan für die Seletion von Terminen in einem bestimmten Zeitfenster. + Bitte berücksichtige, dass das globale ±400 Tageslimit gilt .

        + + Beispiele:
        + get MyCalendar text next
        + get MyCalendar summary uid:435kjhk435googlecom 1
        + get MyCalendar summary 435kjhk435googlecom 1
        + get MyCalendar full all
        + get MyCalendar text mode=alarm|start
        + get MyCalendar text uid=.*6286.*
        +
        + + get <name> find <regexp>
        + Gibt Zeile für Zeile die UIDs aller Termine deren Zusammenfassungen durch den regulären Ausdruck <regex> beschrieben werden. + <regexp>.

        + + get <name> vcalendar
        + Gibt den Kalender ICal-Format, so wie er von der Quelle gelesen wurde, zurück.

        + + get <name> vevents
        + Gibt eine Liste aller VEVENT-Einträge des Kalenders <name>, angereichert um Ausgaben für die Fehlersuche, zurück. + Es werden nur Eigenschaften angezeigt, die während der Programmausführung beibehalten wurden. Es wird sowohl die Liste + der Termine, die von jedem VEVENT-Eintrag erzeugt wurden, als auch die Liste der ausgelassenen Termine angezeigt. +

      Attributes +

        +
      • hideOlderThan <timespec>
        + hideLaterThan <timespec>

        + + Dieses Attribut grenzt die Liste der durch get <name> full|debug|text|summary|location|alarm|start|end ... gezeigten Termine ein. + + Die Zeit wird relativ zur aktuellen Zeit t angegeben.
        + Wenn <hideOlderThan> gesetzt ist, werden Termine, die vor <t-hideOlderThan> enden, ingnoriert.
        + Wenn <hideLaterThan> gesetzt ist, werden Termine, die nach <t+hideLaterThan> anfangen, ignoriert.

        + + <timespec> muss einem der folgenden Formate entsprechen:
        + + + + + + + + +
        FormatBeschreibungBeispiel
        SSSSekunden3600
        SSSsSekunden3600s
        HH:MMStunden:Minuten02:30
        HH:MM:SSStunden:Minuten:Sekunden00:01:30
        D:HH:MM:SSTage:Stunden:Minuten:Sekunden122:10:00:00
        DDDdTage100d

      • +

        + +

      • onCreateEvent <perl-code>
        + + Dieses Attribut führt ein Perlprogramm <perl-code> für jeden erzeugten Termin aus. + Weitere Informationen unter Plug-ins im Text. +
      • +

      • readingFnAttributes

      Beschreibung -
        +

          - Ein Kalender ist eine Menge von Kalender-Ereignissen. Ein Kalender-Ereignis hat eine Zusammenfassung (normalerweise der Titel, welcher im Quell-Kalender angezeigt wird), eine Startzeit, eine Endzeit und keine, eine oder mehrere Alarmzeiten. Die Kalender-Ereignisse werden - aus dem Quellkalender ermittelt, welcher über die URL angegeben wird. Sollten mehrere Alarmzeiten für ein Kalender-Ereignis existieren, wird nur der früheste Alarmzeitpunkt behalten. Wiederkehrende Kalendereinträge werden in einem gewissen Umfang unterstützt: + Ein Kalender ist eine Menge von Terminen. Ein Termin hat eine Zusammenfassung (normalerweise der Titel, welcher im Quell-Kalender angezeigt wird), eine Startzeit, eine Endzeit und keine, eine oder mehrere Alarmzeiten. Die Termine werden + aus dem Quellkalender ermittelt, welcher über die URL angegeben wird. Sollten mehrere Alarmzeiten für einen Termin existieren, wird nur der früheste Alarmzeitpunkt beibehalten. Wiederkehrende Kalendereinträge werden in einem gewissen Umfang unterstützt: FREQ INTERVAL UNTIL COUNT werden ausgewertet, BYMONTHDAY BYMONTH WKST - werden erkannt aber nicht ausgewertet. BYDAY wird nur für wöchentliche Kalender-Ereignisse - korrekt behandelt. Das Modul wird es sehr wahrscheinlich falsch machen, wenn Du wiederkehrende Kalender-Ereignisse mit unerkannten oder nicht ausgewerteten Schlüsselworten hast.

          + werden erkannt aber nicht ausgewertet. BYDAY wird nur für wöchentliche Termine + korrekt behandelt. Das Modul wird es sehr wahrscheinlich falsch machen, wenn Du wiederkehrende Termine mit unerkannten oder nicht ausgewerteten Schlüsselwörtern hast.

          - Ein Kalender-Ereignis wird durch seine UID identifiziert. Die UID wird vom Quellkalender bezogen. Um das Leben leichter zu machen, werden alle nicht-alphanumerischen Zeichen automatisch aus der UID entfernt.

          + Termine werden erzeugt, wenn FHEM gestartet wird oder der betreffende Eintrag im Quell-Kalender verändert + wurde oder der Kalender mit get <name> reload neu geladen wird. Es werden nur Termine + innerhalb ±400 Tage um die Erzeugungs des Termins herum erzeugt. Ziehe in Betracht, den Kalender von Zeit zu Zeit + neu zu laden, um zu vermeiden, dass die künftigen Termine ausgehen. Du kann so etwas wie define reloadCalendar at +*240:00:00 set MyCalendar reload dafür verwenden.

          + + Manche dummen Kalender benutzen LAST-MODIFIED nicht. Das kann dazu führen, dass Veränderungen im + Quell-Kalender unbemerkt bleiben. Lade den Kalender neu, wenn Du dieses Problem hast.

          - Ein Kalender-Ereignis kann sich in einem der folgenden Zustände befinden: - - - - - -
          newDas Kalender-Ereignis wurde das erste Mal beim letzten Update gefunden. Entweder war dies das erste Mal des Kalenderzugriffs oder Du hast einen neuen Kalendereintrag zum Quellkalender hinzugefügt.
          knownDas Kalender-Ereignis existierte bereits vor dem letzten Update.
          updatedDas Kalender-Ereignis existierte bereits vor dem letzten Update, wurde aber geändert.
          deletedDas Kalender-Ereignis existierte bereits vor dem letzten Update, wurde aber seitdem gelöscht. Das Kalender-Ereignis wird beim nächsten Update von allen Listen entfernt.

          - Kalender-Ereignisse, welche vollständig in der Vergangenheit liegen (aktuelle Zeit liegt nach dem Ende-Termin des Kalendereintrags) werden nicht bezogen und sind daher nicht im Kalender verfügbar. -

          + Ein Termin wird durch seine UID identifiziert. Die UID wird vom Quellkalender bezogen. Um das Leben leichter zu machen, werden alle nicht-alphanumerischen Zeichen automatisch aus der UID entfernt.

          - Ein Kalender-Ereignis kann sich in einem der folgenden Modi befinden: - + Ein Termin kann sich in einem der folgenden Modi befinden: +
          @@ -1478,9 +2973,8 @@ sub Calendar_Undef($$) {

          Ein Kalender-Device hat verschiedene Readings. Mit Ausnahme von calname stellt jedes Reading eine Semikolon-getrennte Liste von UIDs von Kalender-Ereignisse dar, welche bestimmte Zustände haben: -

          upcomingWeder die Alarmzeit noch die Startzeit des Kalendereintrags ist erreicht.
          alarmDie Alarmzeit ist überschritten, aber die Startzeit des Kalender-Ereignisses ist noch nicht erreicht.
          startDie Startzeit ist überschritten, aber die Ende-Zeit des Kalender-Ereignisses ist noch nicht erreicht.
          +
          - @@ -1490,22 +2984,69 @@ sub Calendar_Undef($$) { - - - -
          calnameName des Kalenders
          allAlle Ereignisse
          modeAlarmEreignisse im Alarm-Modus
          modeAlarmOrStartEreignisse im Alarm- oder Startmodus
          modeAlarmedEreignisse, welche gerade in den Alarmmodus gewechselt haben
          modeStartEreignisse im Startmodus
          modeStartedEreignisse, welche gerade in den Startmodus gewechselt haben
          modeUpcomingEreignisse im zukünftigen Modus
          stateChangedEreignisse, welche gerade in irgendeiner Form ihren Status gewechselt haben
          stateDeletedEreignisseim Status "deleted"
          stateNewEreignisse im Status "new"
          stateUpdatedEreignisse im Status "updated"
          +

          + + Für Serientermine werden mehrere Termine mit der selben UID erzeugt. In diesem Fall + wird die UID nur im interessantesten gelesenen Modus-Reading angezeigt. + Der interessanteste Modus ist der erste zutreffende Modus aus der Liste der Modi start, alarm, upcoming, end.

          + + Die UID eines Serientermins wird nicht angezeigt, solange sich der Termin im Modus: modeEnd oder modeEnded befindet + und die Serie nicht beendet ist. Die UID befindet sich in einem der anderen mode... Readings. + Hieraus ergibts sich, das FHEM-Events nicht auf einem mode... Reading basieren sollten. + Weiter unten im Text gibt es hierzu eine Empfehlung.

        -

        - Wenn ein Kalender-Ereignis geändert wurde, wird ein Event in der Form - changed: UID mode getriggert, wobei mode den gegenwärtigen Modus des Kalender-Ereignisse nach der Änderung darstellt. + Events +


          + Wenn der Kalendar neu geladen oder aktualisiert oder eine Alarm-, Start- oder Endezeit + erreicht wurde, wird ein FHEM-Event erzeugt:

          + triggered

          + + Man kann sich darauf verlassen, dass alle Readings des Kalenders in einem konsistenten und aktuellen + Zustand befinden, wenn dieses Event empfangen wird.

          + + Wenn ein Termin geändert wurde, werden zwei FHEM-Events erzeugt:

          + + changed: UID <mode>
          + <mode>: UID

          + + <mode> ist der aktuelle Modus des Termins nach der änderung. Bitte beachten: Im FHEM-Event befindet sich ein Doppelpunkt gefolgt von einem Leerzeichen.

          + + FHEM-Events sollten nur auf den vorgenannten Events basieren und nicht auf FHEM-Events, die durch ändern eines mode... Readings ausgelöst werden.

          +

        - Anwendungsbeispiele + + Plug-ins
          - Zeige alle Kalendereinträge inkl. Details

          +
          + Experimentell, bitte mit Vorsicht nutzen.

          + + Ein Plug-In ist ein kleines Perl-Programm, dass Termine nebenher verändern kann. + Das Perl-Programm arbeitet mit der Hash-Referenz $e.
          + Die wichtigsten Elemente sind: + + + + + + + + +
          codedescription
          $e->{start}Startzeit des Termins, in Sekunden seit 1.1.1970
          $e->{end}Endezeit des Termins, in Sekunden seit 1.1.1970
          $e->{alarm}Alarmzeit des Termins, in Sekunden seit 1.1.1970
          $e->{summary}die Zusammenfassung (Betreff, Titel) des Termins
          $e->{location}Der Ort des Termins

          + + Um für alle Termine mit dem Text "Tonne" in der Zusammenfassung die Alarmzeit zu ergänzen / zu ändern, + kann folgendes Plug-In benutzt werden:

          + attr MyCalendar onCreateEvent { $e->{alarm}= $e->{start}-86400 if($e->{summary} =~ /Tonne/);; }
          +
          Das doppelte Semikolon maskiert das Semikolon. Perl specials können nicht genutzt werden.
          +

        +

        + + Anwendungsbeispiele +

          + Alle Termine inkl. Details anzeigen

            get MyCalendar full all
            @@ -1514,10 +3055,10 @@ sub Calendar_Undef($$) {


          - Zeige Kalendereinträge in Deinem Bilderrahmen

          + Zeige Termine in Deinem Bilderrahmen

            - Füge eine Zeile in die layout description ein, um Kalendereinträge im Alarm- oder Startmodus anzuzeigen:

            - text 20 60 { fhem("get MyCalendar text modeAlarmOrStart") }

            + Füge eine Zeile in die layout description ein, um Termine im Alarm- oder Startmodus anzuzeigen:

            + text 20 60 { fhem("get MyCalendar text next 2") }

            Dies kann dann z.B. so aussehen:

            07.06.12 16:30 Erna zum Kaffee
            @@ -1527,56 +3068,79 @@ sub Calendar_Undef($$) { Schalte das Licht ein, wenn Erna kommt

              - Finde zuerst die UID des Kalendereintrags:

              + Finde zuerst die UID des Termins:

              get MyCalendar find .*Erna.*
              2767324dsfretfvds7dsfn3e4­dsa234r234sdfds6bh874­googlecom


              - Definiere dann ein notify:

              + Definiere dann ein notify: (Der Punkt nach dem zweiten Doppelpunkt steht für ein Leerzeichen)

              - define ErnaComes notify MyCalendar:modeStarted.*2767324dsfretfvds7dsfn3e4­dsa234r234sdfds6bh874­googlecom.* set MyLight on + define ErnaComes notify MyCalendar:start:.2767324dsfretfvds7dsfn3e4­dsa234r234sdfds6bh874­googlecom.* set MyLight on

              Du kannst auch ein Logging aufsetzen:

              - define LogErna notify MyCalendar:modeAlarmed.*2767324dsfretfvds7dsfn3e4­dsa234r234sdfds6bh874­googlecom.* { Log3 $NAME, 1, "ALARM name=$NAME event=$EVENT part1=$EVTPART0 part2=$EVTPART1" } + define LogErna notify MyCalendar:alarm:.2767324dsfretfvds7dsfn3e4­dsa234r234sdfds6bh874­googlecom.* { Log3 $NAME, 1, "ALARM name=$NAME event=$EVENT part1=$EVTPART0 part2=$EVTPART1" }

            Schalte die Aktoren an und aus

              Stell Dir einen Kalender vor, dessen Zusammenfassungen (Betreff, Titel) die Namen von Devices in Deiner fhem-Installation sind. - Du willst nun die entsprechenden Devices an- und ausschalten wenn das Kalender-Ereignis beginnt bzw. endet.

              - - define SwitchActorOn notify MyCalendar:modeStarted.* { - my $reading="$EVTPART0";; - my $uid= "$EVTPART1";; - my $actor= fhem("get MyCalendar summary $uid");; + Du willst nun die entsprechenden Devices an- und ausschalten, wenn das Kalender-Ereignis beginnt bzw. endet.

              + + define SwitchActorOn notify MyCalendar:start:.* {}
              +
              + Dann auf DEF klicken und im DEF-Editor folgendes zwischen die beiden geschweiften Klammern {} eingeben: + + my $reading="$EVTPART0"; + my $uid= "$EVTPART1"; + my $actor= fhem("get MyCalendar summary $uid"); if(defined $actor) { fhem("set $actor on") } - }

              - define SwitchActorOff notify MyCalendar:modeEnded.* { - my $reading="$EVTPART0";; - my $uid= "$EVTPART1";; - my $actor= fhem("get MyCalendar summary $uid");; +

              + define SwitchActorOff notify MyCalendar:end:.* {}
              +
              + Dann auf DEF klicken und im DEF-Editor folgendes zwischen die beiden geschweiften Klammern {} eingeben: + + my $reading="$EVTPART0"; + my $uid= "$EVTPART1"; + my $actor= fhem("get MyCalendar summary $uid"); if(defined $actor) { fhem("set $actor off") } - }

              Auch hier kann ein Logging aufgesetzt werden:

              - define LogActors notify MyCalendar:mode(Started|Ended).* { my $reading= "$EVTPART0";; my $uid= "$EVTPART1";; my $actor= fhem("get MyCalendar summary $uid");; Log 3 $NAME, 1, "Actor: $actor, Reading $reading" } + define LogActors notify MyCalendar:mode(start|end).* {}
              +
              + Dann auf DEF klicken und im DEF-Editor folgendes zwischen die beiden geschweiften Klammern {} eingeben: + + my $reading= "$EVTPART0"; + my $uid= "$EVTPART1"; + my $actor= fhem("get MyCalendar summary $uid"); + Log 3 $NAME, 1, "Actor: $actor, Reading $reading";

            -
          - + Eingebettetes HTML +

            + Das Modul stellt eine zusätzliche Funktion CalendarAsHtml(<name>,<options>) bereit. + Diese gibt den HTML-Kode für eine Liste von Terminen zurück. <name> ist der Name des + Kalendar-Device und <options> ist das, was Du hinter get <name> text ... + schreiben würdest. +

            + Beispiel: define MyCalendarWeblink weblink htmlCode { CalendarAsHtml("MyCalendar","next 3") } +

            + Dies ist eine rudimentäre Funktion, die vielleicht in künftigen Versionen erweitert wird. +

            +

          + +
        - =end html_DE =cut diff --git a/fhem/FHEM/66_ECMD.pm b/fhem/FHEM/66_ECMD.pm index 72eb7c4bb..f960e6745 100644 --- a/fhem/FHEM/66_ECMD.pm +++ b/fhem/FHEM/66_ECMD.pm @@ -138,6 +138,26 @@ ECMD_DoInit($) sub dq($) { +=for comment + '\a' => "\\a", + '\e' => "\\e", + '\f' => "\\f", + '\n' => "\\n", + '\r' => "\\r", + '\t' => "\\t", + ); + + $s =~ s/\\/\\\\/g; + foreach my $regex (keys %escSequences) { + $s =~ s/$regex/$escSequences{$regex}/g; + } + $s =~ s/([\000-\037])/sprintf("\\%03o", ord($1))/eg; + + + +=cut + + my ($s)= @_; $s= "" unless(defined($s)); return "\"" . escapeLogLine($s) . "\"";