From 97836f48fa6e6941d64cdac1ad0e9f3322d20996 Mon Sep 17 00:00:00 2001 From: nasseeder1 Date: Sun, 19 Jan 2020 14:21:41 +0000 Subject: [PATCH] 57_SSCal: contrib 1.1.12 git-svn-id: https://svn.fhem.de/fhem/trunk@21013 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/contrib/DS_Starter/57_SSCal.pm | 2808 +++++++++++++++++++++++++++ 1 file changed, 2808 insertions(+) create mode 100644 fhem/contrib/DS_Starter/57_SSCal.pm diff --git a/fhem/contrib/DS_Starter/57_SSCal.pm b/fhem/contrib/DS_Starter/57_SSCal.pm new file mode 100644 index 000000000..41f538ebb --- /dev/null +++ b/fhem/contrib/DS_Starter/57_SSCal.pm @@ -0,0 +1,2808 @@ +######################################################################################################################## +# $Id: $ +######################################################################################################################### +# 57_SSCal.pm +# +# (c) 2019 - 2020 by Heiko Maaz +# e-mail: Heiko dot Maaz at t-online dot de +# +# This Module integrate the Synology Calendar into FHEM +# +# This script is part of fhem. +# +# Fhem is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# Fhem is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with fhem. If not, see . +# +######################################################################################################################### +# +# Definition: define SSCal [ServerPort] [Protocol] +# +# Example: define SynCal SSCal 192.168.2.20 [5000] [HTTP(S)] +# + +package main; + +use strict; +use warnings; +eval "use JSON;1;" or my $SSCalMM = "JSON"; # Debian: apt-get install libjson-perl +use Data::Dumper; # Perl Core module +use MIME::Base64; +use Time::HiRes; +use HttpUtils; +use Encode; +use Blocking; +no if $] >= 5.017011, warnings => 'experimental::smartmatch'; +eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; + +# no if $] >= 5.017011, warnings => 'experimental'; + +# Versions History intern +my %SSCal_vNotesIntern = ( + "1.1.12" => "19.01.2020 add attribute interval, automatic event fetch ", + "1.1.11" => "18.01.2020 status information added: upcoming, alarmed, started, ended ", + "1.1.10" => "17.01.2020 attribute asyncMode for parsing events in BlockingCall, some fixes ", + "1.1.9" => "14.01.2020 preparation of asynchronous calendar event extraction, some fixes ", + "1.1.8" => "13.01.2020 can proces WEEKLY general recurring events, use \$data{SSCal}{\$name}{eventlist} as Hash of Events ", + "1.1.7" => "12.01.2020 can proces WEEKLY recurring events BYDAY ", + "1.1.6" => "11.01.2020 can proces DAILY recurring events ", + "1.1.5" => "10.01.2020 can proces MONTHLY recurring events BYDAY ", + "1.1.4" => "07.01.2020 can proces MONTHLY recurring events BYMONTHDAY ", + "1.1.3" => "06.01.2020 can proces YEARLY recurring events ", + "1.1.2" => "04.01.2020 logout if new credentials are set ", + "1.1.1" => "03.01.2020 add array of 'evt_notify_setting' ", + "1.1.0" => "01.01.2020 logout command ", + "1.0.0" => "18.12.2019 initial " +); + +# Versions History extern +my %SSCal_vNotesExtern = ( + "1.0.0" => "18.12.2019 initial " +); + +my %SSCal_errlist = ( + 100 => "Unknown error", + 101 => "No parameter of API, method or version", + 102 => "The requested API does not exist - may be the Synology Calendar package is stopped", + 103 => "The requested method does not exist", + 104 => "The requested version does not support the functionality", + 105 => "The logged in session does not have permission", + 106 => "Session timeout", + 107 => "Session interrupted by duplicate login", + 114 => "Missing required parameters", + 117 => "Unknown internal error", + 119 => "session id not valid", + 120 => "Invalid parameter", + 160 => "Insufficient application privilege", + 400 => "Invalid parameter of file operation", + 401 => "Unknown error of file operation", + 402 => "System is too busy", + 403 => "The user does not have permission to execute this operation", + 404 => "The group does not have permission to execute this operation", + 405 => "The user/group does not have permission to execute this operation", + 406 => "Cannot obtain user/group information from the account server", + 407 => "Operation not permitted", + 408 => "No such file or directory", + 409 => "File system not supported", + 410 => "Failed to connect internet-based file system (ex: CIFS)", + 411 => "Read-only file system", + 412 => "Filename too long in the non-encrypted file system", + 413 => "Filename too long in the encrypted file system", + 414 => "File already exists", + 415 => "Disk quota exceeded", + 416 => "No space left on device", + 417 => "Input/output error", + 418 => "Illegal name or path", + 419 => "Illegal file name", + 420 => "Illegal file name on FAT file system", + 421 => "Device or resource busy", + 599 => "No such task of the file operation", + 800 => "malformed or unsupported URL", + 805 => "empty API data received - may be the Synology cal Server package is stopped", + 806 => "couldn't get Synology cal API information", + 810 => "The credentials couldn't be retrieved", + 900 => "malformed JSON string received from Synology Calendar Server", + 910 => "Wrong timestamp definition. Check attributes \"cutOlderDays\", \"cutLaterDays\". ", +); + +# Standardvariablen und Forward-Deklaration +use vars qw(%SSCal_vHintsExt_en); +use vars qw(%SSCal_vHintsExt_de); +our %SSCal_api; + +################################################################ +sub SSCal_Initialize($) { + my ($hash) = @_; + $hash->{DefFn} = "SSCal_Define"; + $hash->{UndefFn} = "SSCal_Undef"; + $hash->{DeleteFn} = "SSCal_Delete"; + $hash->{SetFn} = "SSCal_Set"; + $hash->{GetFn} = "SSCal_Get"; + $hash->{AttrFn} = "SSCal_Attr"; + $hash->{DelayedShutdownFn} = "SSCal_DelayedShutdown"; + $hash->{FW_deviceOverview} = 1; + + $hash->{AttrList} = "asyncMode:1,0 ". + "cutOlderDays ". + "cutLaterDays ". + "disable:1,0 ". + "interval ". + "loginRetries:1,2,3,4,5,6,7,8,9,10 ". + "showRepeatEvent:true,false ". + "showPassInLog:1,0 ". + "timeout ". + "usedCalendars:--wait#for#Calendar#list-- ". + $readingFnAttributes; + + eval { FHEM::Meta::InitMod( __FILE__, $hash ) }; # für Meta.pm (https://forum.fhem.de/index.php/topic,97589.0.html) + +return; +} + +################################################################ +# define SyncalBot SSCal 192.168.2.10 [5000] [HTTP(S)] +# ($hash) [1] [2] [3] [4] +# +################################################################ +sub SSCal_Define($@) { + my ($hash, $def) = @_; + my $name = $hash->{NAME}; + + return "Error: Perl module ".$SSCalMM." is missing. Install it on Debian with: sudo apt-get install libjson-perl" if($SSCalMM); + + my @a = split("[ \t][ \t]*", $def); + + if(int(@a) < 2) { + return "You need to specify more parameters.\n". "Format: define SSCal [Port] [HTTP(S)]"; + } + + my $addr = $a[2]; + my $port = $a[3] ? $a[3] : 5000; + my $prot = $a[4] ? lc($a[4]) : "http"; + + $hash->{ADDR} = $addr; + $hash->{PORT} = $port; + $hash->{MODEL} = "Calendar"; + $hash->{PROT} = $prot; + $hash->{RESEND} = "next planned SendQueue start: immediately by next entry"; + $hash->{HELPER}{MODMETAABSENT} = 1 if($modMetaAbsent); # Modul Meta.pm nicht vorhanden + $hash->{HELPER}{CALFETCHED} = 0; # vorhandene Kalender sind noch nicht abgerufen + $hash->{HELPER}{APIPARSET} = 0; # es sind keine API Informationen gesetzt -> neu abrufen + + CommandAttr(undef,"$name room SSCal"); + CommandAttr(undef,"$name event-on-update-reading .*Summary.*,state"); + + %SSCal_api = ( + "APIINFO" => { "NAME" => "SYNO.API.Info" }, # Info-Seite für alle API's, einzige statische Seite ! + "APIAUTH" => { "NAME" => "SYNO.API.Auth" }, # API used to perform session login and logout + "CALCAL" => { "NAME" => "SYNO.Cal.Cal" }, # API to manipulate calendar + "CALEVENT" => { "NAME" => "SYNO.Cal.Event" }, # Provide methods to manipulate events in the specific calendar + "CALSHARE" => { "NAME" => "SYNO.Cal.Sharing" }, # Get/set sharing setting of calendar + "CALTODO" => { "NAME" => "SYNO.Cal.Todo" }, # Provide methods to manipulate events in the specific calendar + ); + + # Versionsinformationen setzen + SSCal_setVersionInfo($hash); + + # Credentials lesen + SSCal_getcredentials($hash,1,"credentials"); + + # Index der Sendequeue initialisieren + $data{SSCal}{$name}{sendqueue}{index} = 0; + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "Errorcode", "none"); + readingsBulkUpdateIfChanged ($hash, "Error", "none"); + readingsBulkUpdateIfChanged ($hash, "QueueLenth", 0); # Länge Sendqueue initialisieren + readingsBulkUpdate ($hash, "nextUpdate", "Manual"); # Abrufmode initial auf "Manual" setzen + readingsBulkUpdate ($hash, "state", "Initialized"); # Init state + readingsEndUpdate ($hash,1); + + # initiale Routinen nach Start ausführen , verzögerter zufälliger Start + SSCal_initonboot($name); + +return undef; +} + +################################################################ +# Die Undef-Funktion wird aufgerufen wenn ein Gerät mit delete +# gelöscht wird oder bei der Abarbeitung des Befehls rereadcfg, +# der ebenfalls alle Geräte löscht und danach das +# Konfigurationsfile neu einliest. +# Funktion: typische Aufräumarbeiten wie das +# saubere Schließen von Verbindungen oder das Entfernen von +# internen Timern, sofern diese im Modul zum Pollen verwendet +# wurden. +################################################################ +sub SSCal_Undef($$) { + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + BlockingKill($hash->{HELPER}{RUNNING_PID}) if($hash->{HELPER}{RUNNING_PID}); + delete $data{SSCal}{$name}; + RemoveInternalTimer($name); + +return undef; +} + +####################################################################################################### +# Mit der X_DelayedShutdown Funktion kann eine Definition das Stoppen von FHEM verzögern um asynchron +# hinter sich aufzuräumen. +# Je nach Rückgabewert $delay_needed wird der Stopp von FHEM verzögert (0|1). +# Sobald alle nötigen Maßnahmen erledigt sind, muss der Abschluss mit CancelDelayedShutdown($name) an +# FHEM zurückgemeldet werden. +####################################################################################################### +sub SSCal_DelayedShutdown($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + if($hash->{HELPER}{SID}) { + SSCal_logout($hash); # Session alter User beenden falls vorhanden + return 1; + } + +return 0; +} + +################################################################# +# Wenn ein Gerät in FHEM gelöscht wird, wird zuerst die Funktion +# X_Undef aufgerufen um offene Verbindungen zu schließen, +# anschließend wird die Funktion X_Delete aufgerufen. +# Funktion: Aufräumen von dauerhaften Daten, welche durch das +# Modul evtl. für dieses Gerät spezifisch erstellt worden sind. +# Es geht hier also eher darum, alle Spuren sowohl im laufenden +# FHEM-Prozess, als auch dauerhafte Daten bspw. im physikalischen +# Gerät zu löschen die mit dieser Gerätedefinition zu tun haben. +################################################################# +sub SSCal_Delete($$) { + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + my $index = $hash->{TYPE}."_".$hash->{NAME}."_credentials"; + + # gespeicherte Credentials löschen + setKeyValue($index, undef); + +return undef; +} + +################################################################ +sub SSCal_Attr($$$$) { + my ($cmd,$name,$aName,$aVal) = @_; + my $hash = $defs{$name}; + my ($do,$val,$cache); + + # $cmd can be "del" or "set" + # $name is device name + # aName and aVal are Attribute name and value + + if ($aName eq "disable") { + if($cmd eq "set") { + $do = $aVal?1:0; + } + $do = 0 if($cmd eq "del"); + + $val = ($do == 1 ? "disabled" : "initialized"); + + if ($do == 1) { + RemoveInternalTimer($name); + } else { + InternalTimer(gettimeofday()+2, "SSCal_initonboot", $name, 0) if($init_done); + } + + readingsBeginUpdate($hash); + readingsBulkUpdate ($hash, "state", $val); + readingsEndUpdate ($hash,1); + } + + if ($cmd eq "set") { + if ($aName =~ m/timeout|cutLaterDays|cutOlderDays|interval/) { + unless ($aVal =~ /^\d+$/) { return "The Value for $aName is not valid. Use only figures 1-9 !";} + } + if($aName =~ m/interval/) { + RemoveInternalTimer($name,"SSCal_periodicCall"); + if($aVal > 0) { + InternalTimer(gettimeofday()+1.0, "SSCal_periodicCall", $name, 0); + } + } + } + +return undef; +} + +################################################################ +sub SSCal_Set($@) { + my ($hash, @a) = @_; + return "\"set X\" needs at least an argument" if ( @a < 2 ); + my $name = $a[0]; + my $opt = $a[1]; + my $prop = $a[2]; + my $prop1 = $a[3]; + my $prop2 = $a[4]; + my $prop3 = $a[5]; + my ($success,$setlist); + + return if(IsDisabled($name)); + + my $idxlist = join(",", SSCal_sortVersion("asc",keys %{$data{SSCal}{$name}{sendqueue}{entries}})); + + if(!$hash->{CREDENTIALS}) { + # initiale setlist für neue Devices + $setlist = "Unknown argument $opt, choose one of ". + "credentials " + ; + } else { + $setlist = "Unknown argument $opt, choose one of ". + "calEventList ". + "credentials ". + "eraseReadings:noArg ". + "listSendqueue:noArg ". + "logout:noArg ". + ($idxlist?"purgeSendqueue:-all-,-permError-,$idxlist ":"purgeSendqueue:-all-,-permError- "). + "restartSendqueue:noArg " + ; + } + + if ($opt eq "credentials") { + return "The command \"$opt\" needs an argument." if (!$prop); + SSCal_logout($hash) if($hash->{HELPER}{SID}); # Session alter User beenden falls vorhanden + ($success) = SSCal_setcredentials($hash,$prop,$prop1); + + if($success) { + SSCal_addQueue($name,"listcal","CALCAL","list","&is_todo=true&is_evt=true"); + SSCal_getapisites($name); + return "credentials saved successfully"; + } else { + return "Error while saving credentials - see logfile for details"; + } + + } elsif ($opt eq "listSendqueue") { + my $sub = sub ($) { + my ($idx) = @_; + my $ret; + foreach my $key (reverse sort keys %{$data{SSCal}{$name}{sendqueue}{entries}{$idx}}) { + $ret .= ", " if($ret); + $ret .= $key."=>".$data{SSCal}{$name}{sendqueue}{entries}{$idx}{$key}; + } + return $ret; + }; + + if (!keys %{$data{SSCal}{$name}{sendqueue}{entries}}) { + return "SendQueue is empty."; + } + my $sq; + foreach my $idx (sort{$a<=>$b} keys %{$data{SSCal}{$name}{sendqueue}{entries}}) { + $sq .= $idx." => ".$sub->($idx)."\n"; + } + return $sq; + + } elsif ($opt eq "purgeSendqueue") { + if($prop eq "-all-") { + delete $hash->{OPIDX}; + delete $data{SSCal}{$name}{sendqueue}{entries}; + $data{SSCal}{$name}{sendqueue}{index} = 0; + return "All entries of SendQueue are deleted"; + } elsif($prop eq "-permError-") { + foreach my $idx (keys %{$data{SSCal}{$name}{sendqueue}{entries}}) { + delete $data{SSCal}{$name}{sendqueue}{entries}{$idx} + if($data{SSCal}{$name}{sendqueue}{entries}{$idx}{forbidSend}); + } + return "All entries with state \"permanent send error\" are deleted"; + } else { + delete $data{SSCal}{$name}{sendqueue}{entries}{$prop}; + return "SendQueue entry with index \"$prop\" deleted"; + } + + } elsif ($opt eq "calEventList") { # Termine einer Cal_id (Liste) in Zeitgrenzen abrufen + return "Obtain the Calendar list first with \"get $name getCalendars\" command." if(!$hash->{HELPER}{CALFETCHED}); + my ($err,$tstart,$tend) = SSCal_timeEdge ($name); + + if($err) { + Log3($name, 2, "$name - ERROR in timestamp: $err"); + + my $errorcode = "910"; + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "Error", $err); + readingsBulkUpdateIfChanged ($hash, "Errorcode", $errorcode); + readingsBulkUpdate ($hash, "state", "Error"); + readingsEndUpdate ($hash,1); + + return "ERROR in timestamp: $err"; + } + + my $cals = AttrVal($name,"usedCalendars", ""); + + shift @a; shift @a; + my $c = join(" ", @a); + $cals = $c?$c:$cals; + return "Please set attribute \"usedCalendars\" or specify the Calendar(s) you want read in \"$opt\" command." if(!$cals); + + # Kalender aufsplitten und zu jedem die ID ermitteln + my @ca = split(",", $cals); + my $oids; + foreach (@ca) { + my $oid = $hash->{HELPER}{CALENDARS}{"$_"}{id}; + next if(!$oid); + $oids .= "," if($oids); + $oids .= '"'.$oid.'"'; + Log3($name, 2, "$name - WARNING - The Calendar \"$_\" seems to be unknown because its ID couldn't be found.") if(!$oid); + } + + return "All Calendars seems to be unknown because their ID's couldn't be found." if(!$oids); + + Log3($name, 5, "$name - Calendar selection for add queue: $cals"); + my $lr = AttrVal($name,"showRepeatEvent", "true"); + SSCal_addQueue($name,"eventlist","CALEVENT","list","&cal_id_list=[$oids]&start=$tstart&end=$tend&list_repeat=$lr"); + SSCal_getapisites($name); + + } elsif ($opt eq "restartSendqueue") { + my $ret = SSCal_getapisites($name); + if($ret) { + return $ret; + } else { + return "The SendQueue has been restarted."; + } + + } elsif ($opt eq 'eraseReadings') { + SSCal_delReadings($name,0); # Readings löschen + + } elsif ($opt eq 'logout') { + SSCal_logout($hash); + + } else { + return "$setlist"; + } + +return; +} + +################################################################ +sub SSCal_Get($@) { + my ($hash, @a) = @_; + return "\"get X\" needs at least an argument" if ( @a < 2 ); + my $name = shift @a; + my $opt = shift @a; + my $arg = shift @a; + my $arg1 = shift @a; + my $arg2 = shift @a; + my $ret = ""; + my $getlist; + + if(!$hash->{CREDENTIALS}) { + return; + + } else { + $getlist = "Unknown argument $opt, choose one of ". + "getCalendars:noArg ". + "storedCredentials:noArg ". + "versionNotes " + ; + } + + return if(IsDisabled($name)); + + if ($opt eq "storedCredentials") { + if (!$hash->{CREDENTIALS}) {return "Credentials of $name are not set - make sure you've set it with \"set $name credentials \"";} + # Credentials abrufen + my ($success, $username, $passwd) = SSCal_getcredentials($hash,0,"credentials"); + unless ($success) {return "Credentials couldn't be retrieved successfully - see logfile"}; + + return "Stored Credentials:\n". + "===================\n". + "Username: $username, Password: $passwd \n" + ; + + } elsif ($opt eq "getCalendars") { # Liste aller Kalender abrufen + # übergebenen CL-Hash (FHEMWEB) in Helper eintragen + SSChatBot_getclhash($hash,1); + + SSCal_addQueue($name,"listcal","CALCAL","list","&is_todo=true&is_evt=true"); + SSCal_getapisites($name); + + } elsif ($opt =~ /versionNotes/) { + my $header = "Module release information
"; + my $header1 = "Helpful hints
"; + my %hs; + + # Ausgabetabelle erstellen + my ($ret,$val0,$val1); + my $i = 0; + + $ret = ""; + + # Hints + if(!$arg || $arg =~ /hints/ || $arg =~ /[\d]+/) { + $ret .= sprintf("
$header1
"); + $ret .= ""; + $ret .= ""; + $ret .= ""; + if($arg && $arg =~ /[\d]+/) { + my @hints = split(",",$arg); + foreach (@hints) { + if(AttrVal("global","language","EN") eq "DE") { + $hs{$_} = $SSCal_vHintsExt_de{$_}; + } else { + $hs{$_} = $SSCal_vHintsExt_en{$_}; + } + } + } else { + if(AttrVal("global","language","EN") eq "DE") { + %hs = %SSCal_vHintsExt_de; + } else { + %hs = %SSCal_vHintsExt_en; + } + } + $i = 0; + foreach my $key (SSCal_sortVersion("desc",keys %hs)) { + $val0 = $hs{$key}; + $ret .= sprintf("" ); + $ret .= ""; + $i++; + if ($i & 1) { + # $i ist ungerade + $ret .= ""; + } else { + $ret .= ""; + } + } + $ret .= ""; + $ret .= ""; + $ret .= "
$key $val0
"; + $ret .= "
"; + } + + # Notes + if(!$arg || $arg =~ /rel/) { + $ret .= sprintf("
$header
"); + $ret .= ""; + $ret .= ""; + $ret .= ""; + $i = 0; + foreach my $key (SSCal_sortVersion("desc",keys %SSCal_vNotesExtern)) { + ($val0,$val1) = split(/\s/,$SSCal_vNotesExtern{$key},2); + $ret .= sprintf("" ); + $ret .= ""; + $i++; + if ($i & 1) { + # $i ist ungerade + $ret .= ""; + } else { + $ret .= ""; + } + } + $ret .= ""; + $ret .= ""; + $ret .= "
$key $val0 $val1
"; + $ret .= "
"; + } + + $ret .= ""; + + return $ret; + + } else { + return "$getlist"; + } + +return $ret; # not generate trigger out of command +} + +###################################################################################### +# initiale Startroutinen nach Restart FHEM +###################################################################################### +sub SSCal_initonboot ($) { + my ($name) = @_; + my $hash = $defs{$name}; + my ($ret); + + RemoveInternalTimer($name, "SSCal_initonboot"); + + if ($init_done) { + CommandGet(undef, "$name getCalendars"); # Kalender Liste initial abrufen + } else { + InternalTimer(gettimeofday()+3, "SSCal_initonboot", $name, 0); + } + +return; +} + +############################################################################################# +# regelmäßiger Intervallabruf +############################################################################################# +sub SSCal_periodicCall($) { + my ($name) = @_; + my $hash = $defs{$name}; + my $interval = AttrVal($name, "interval", 0); + my $new; + + if(!$interval) { + $hash->{MODE} = "Manual"; + } else { + $new = gettimeofday()+$interval; + readingsBeginUpdate ($hash); + readingsBulkUpdate ($hash, "nextUpdate", "Automatic - next polltime: ".FmtTime($new)); # Abrufmode initial auf "Manual" setzen + readingsEndUpdate ($hash,1); + } + + RemoveInternalTimer($name,"SSCal_periodicCall"); + return if(!$interval); + + if($hash->{CREDENTIALS} && !IsDisabled($name)) { + CommandSet(undef, "$name calEventList"); # Events aller gewählter Kalender abrufen (in Queue stellen) + } + + InternalTimer($new, "SSCal_periodicCall", $name, 0); + +return; +} + +###################################################################################### +# Eintrag zur SendQueue hinzufügen +###################################################################################### +sub SSCal_addQueue ($$$$$) { + my ($name,$opmode,$api,$method,$params) = @_; + my $hash = $defs{$name}; + + $data{SSCal}{$name}{sendqueue}{index}++; + my $index = $data{SSCal}{$name}{sendqueue}{index}; + + Log3($name, 5, "$name - Add sendItem to queue - Idx: $index, Opmode: $opmode, API: $api, Method: $method, Params: $params"); + + my $pars = {'opmode' => $opmode, + 'api' => $api, + 'method' => $method, + 'params' => $params, + 'retryCount' => 0 + }; + + $data{SSCal}{$name}{sendqueue}{entries}{$index} = $pars; + + SSCal_updQLength ($hash); # updaten Länge der Sendequeue + +return; +} + + +############################################################################################# +# Erfolg einer Rückkehrroutine checken und ggf. Send-Retry ausführen +# bzw. den SendQueue-Eintrag bei Erfolg löschen +# $name = Name des calbot-Devices +# $retry = 0 -> Opmode erfolgreich (DS löschen), +# 1 -> Opmode nicht erfolgreich (Abarbeitung nach ckeck errorcode +# eventuell verzögert wiederholen) +############################################################################################# +sub SSCal_checkretry ($$) { + my ($name,$retry) = @_; + my $hash = $defs{$name}; + my $idx = $hash->{OPIDX}; + my $forbidSend = ""; + + if(!keys %{$data{SSCal}{$name}{sendqueue}{entries}}) { + Log3($name, 4, "$name - SendQueue is empty. Nothing to do ..."); + SSCal_updQLength ($hash); + return; + } + + if(!$retry) { # Befehl erfolgreich, Senden nur neu starten wenn weitere Einträge in SendQueue + delete $hash->{OPIDX}; + delete $data{SSCal}{$name}{sendqueue}{entries}{$idx}; + Log3($name, 4, "$name - Opmode \"$hash->{OPMODE}\" finished successfully, Sendqueue index \"$idx\" deleted."); + SSCal_updQLength ($hash); + return SSCal_getapisites($name); # nächsten Eintrag abarbeiten (wenn SendQueue nicht leer) + + } else { # Befehl nicht erfolgreich, (verzögertes) Senden einplanen + $data{SSCal}{$name}{sendqueue}{entries}{$idx}{retryCount}++; + my $rc = $data{SSCal}{$name}{sendqueue}{entries}{$idx}{retryCount}; + + my $errorcode = ReadingsVal($name, "Errorcode", 0); + if($errorcode =~ /119/) { + delete $hash->{HELPER}{SID}; + } + if($errorcode =~ /100|101|117|120|407|409|800|900/) { # bei diesen Errorcodes den Queueeintrag nicht wiederholen, da dauerhafter Fehler ! + $forbidSend = SSCal_experror($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + $data{SSCal}{$name}{sendqueue}{entries}{$idx}{forbidSend} = $forbidSend; + + Log3($name, 2, "$name - ERROR - \"$hash->{OPMODE}\" SendQueue index \"$idx\" not executed. It seems to be a permanent error. Exclude it from new send attempt !"); + + delete $hash->{OPIDX}; + delete $hash->{OPMODE}; + + SSCal_updQLength ($hash); # updaten Länge der Sendequeue + + return SSCal_getapisites($name); # nächsten Eintrag abarbeiten (wenn SendQueue nicht leer); + } + + if(!$forbidSend) { + my $rs = 0; + if($rc <= 1) { + $rs = 5; + } elsif ($rc < 3) { + $rs = 20; + } elsif ($rc < 5) { + $rs = 60; + } elsif ($rc < 7) { + $rs = 1800; + } elsif ($rc < 30) { + $rs = 3600; + } else { + $rs = 86400; + } + + Log3($name, 2, "$name - ERROR - \"$hash->{OPMODE}\" SendQueue index \"$idx\" not executed. Restart SendQueue in $rs seconds (retryCount $rc)."); + + my $rst = gettimeofday()+$rs; # resend Timer + SSCal_updQLength ($hash,$rst); # updaten Länge der Sendequeue mit resend Timer + + RemoveInternalTimer($name, "SSCal_getapisites"); + InternalTimer($rst, "SSCal_getapisites", "$name", 0); + } + } + +return +} + +############################################################################################################################# +####### Begin Kameraoperationen mit NonblockingGet (nicht blockierender HTTP-Call) ####### +############################################################################################################################# +sub SSCal_getapisites($) { + my ($name) = @_; + my $hash = $defs{$name}; + my $addr = $hash->{ADDR}; + my $port = $hash->{PORT}; + my $prot = $hash->{PROT}; + my ($url,$param,$idxset,$ret); + + $hash->{HELPER}{LOGINRETRIES} = 0; + + my ($err,$tstart,$tend) = SSCal_timeEdge($name); + $tstart = FmtDateTime($tstart); + $tend = FmtDateTime($tend); + + # API-Pfade und MaxVersions ermitteln + Log3($name, 4, "$name - ####################################################"); + Log3($name, 4, "$name - ### start Synology Calendar operation "); + Log3($name, 4, "$name - ####################################################"); + + if(!keys %{$data{SSCal}{$name}{sendqueue}{entries}}) { + $ret = "Sendqueue is empty. Nothing to do ..."; + Log3($name, 4, "$name - $ret"); + return $ret; + } + + # den nächsten Eintrag aus "SendQueue" selektieren und ausführen wenn nicht forbidSend gesetzt ist + foreach my $idx (sort{$a<=>$b} keys %{$data{SSCal}{$name}{sendqueue}{entries}}) { + if (!$data{SSCal}{$name}{sendqueue}{entries}{$idx}{forbidSend}) { + $hash->{OPIDX} = $idx; + $hash->{OPMODE} = $data{SSCal}{$name}{sendqueue}{entries}{$idx}{opmode}; + $idxset = 1; + last; + } + } + + if(!$idxset) { + $ret = "Only entries with \"forbidSend\" are in Sendqueue. Escaping ..."; + Log3($name, 4, "$name - $ret"); + return $ret; + } + + readingsBeginUpdate ($hash); + readingsBulkUpdate ($hash, "state", "running"); + readingsEndUpdate ($hash,1); + + Log3($name, 4, "$name - Time selection start: ".$tstart); + Log3($name, 4, "$name - Time selection end: ".$tend); + + if ($hash->{HELPER}{APIPARSET}) { # API-Hashwerte sind bereits gesetzt -> Abruf überspringen + Log3($name, 4, "$name - API hash values already set - ignore get apisites"); + return SSCal_checkSID($name); + } + + my $timeout = AttrVal($name,"timeout",20); + Log3($name, 5, "$name - HTTP-Call will be done with timeout: $timeout s"); + + # URL zur Abfrage der Eigenschaften der API's + $url = "$prot://$addr:$port/webapi/query.cgi?api=$SSCal_api{APIINFO}{NAME}&method=Query&version=1&query=$SSCal_api{APIAUTH}{NAME},$SSCal_api{CALCAL}{NAME},$SSCal_api{CALEVENT}{NAME},$SSCal_api{CALSHARE}{NAME},$SSCal_api{CALTODO}{NAME}"; + + Log3($name, 4, "$name - Call-Out: $url"); + + $param = { + url => $url, + timeout => $timeout, + hash => $hash, + method => "GET", + header => "Accept: application/json", + callback => \&SSCal_getapisites_parse + }; + HttpUtils_NonblockingGet ($param); + +return; +} + +#################################################################################### +# Auswertung Abruf apisites +#################################################################################### +sub SSCal_getapisites_parse ($) { + my ($param, $err, $myjson) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $addr = $hash->{ADDR}; + my $port = $hash->{PORT}; + + my ($error,$errorcode,$success); + + if ($err ne "") { + # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist + Log3($name, 2, "$name - ERROR message: $err"); + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "Error", $err); + readingsBulkUpdateIfChanged ($hash, "Errorcode", "none"); + readingsBulkUpdate ($hash, "state", "Error"); + readingsEndUpdate ($hash,1); + + SSCal_checkretry($name,1); + return; + + } elsif ($myjson ne "") { + # Evaluiere ob Daten im JSON-Format empfangen wurden + ($hash,$success,$myjson) = SSCal_evaljson($hash,$myjson); + unless ($success) { + Log3($name, 4, "$name - Data returned: ".$myjson); + SSCal_checkretry($name,1); + return; + } + + my $data = decode_json($myjson); + + # Logausgabe decodierte JSON Daten + Log3($name, 5, "$name - JSON returned: ". Dumper $data); + + $success = $data->{'success'}; + + if ($success) { + my $logstr; + + # Pfad und Maxversion von "SYNO.API.Auth" ermitteln + my $apiauthpath = $data->{data}->{$SSCal_api{APIAUTH}{NAME}}->{path}; + $apiauthpath =~ tr/_//d if (defined($apiauthpath)); + my $apiauthmaxver = $data->{data}->{$SSCal_api{APIAUTH}{NAME}}->{maxVersion}; + + $logstr = defined($apiauthpath) ? "Path of $SSCal_api{APIAUTH}{NAME} selected: $apiauthpath" : "Path of $SSCal_api{APIAUTH}{NAME} undefined - Synology cal Server may be stopped"; + Log3($name, 4, "$name - $logstr"); + $logstr = defined($apiauthmaxver) ? "MaxVersion of $SSCal_api{APIAUTH}{NAME} selected: $apiauthmaxver" : "MaxVersion of $SSCal_api{APIAUTH}{NAME} undefined - Synology cal Server may be stopped"; + Log3($name, 4, "$name - $logstr"); + + # Pfad und Maxversion von "SYNO.Cal.Cal" ermitteln + my $apicalpath = $data->{data}->{$SSCal_api{CALCAL}{NAME}}->{path}; + $apicalpath =~ tr/_//d if (defined($apicalpath)); + my $apicalmaxver = $data->{data}->{$SSCal_api{CALCAL}{NAME}}->{maxVersion}; + + $logstr = defined($apicalpath) ? "Path of $SSCal_api{CALCAL}{NAME} selected: $apicalpath" : "Path of $SSCal_api{CALCAL}{NAME} undefined - Synology cal Server may be stopped"; + Log3($name, 4, "$name - $logstr"); + $logstr = defined($apicalmaxver) ? "MaxVersion of $SSCal_api{CALCAL}{NAME} selected: $apicalmaxver" : "MaxVersion of $SSCal_api{CALCAL}{NAME} undefined - Synology cal Server may be stopped"; + Log3($name, 4, "$name - $logstr"); + + # Pfad und Maxversion von "SYNO.Cal.Event" ermitteln + my $apievtpath = $data->{data}->{$SSCal_api{CALEVENT}{NAME}}->{path}; + $apievtpath =~ tr/_//d if (defined($apievtpath)); + my $apievtmaxver = $data->{data}->{$SSCal_api{CALEVENT}{NAME}}->{maxVersion}; + + $logstr = defined($apievtpath) ? "Path of $SSCal_api{CALEVENT}{NAME} selected: $apievtpath" : "Path of $SSCal_api{CALEVENT}{NAME} undefined - Synology cal Server may be stopped"; + Log3($name, 4, "$name - $logstr"); + $logstr = defined($apievtmaxver) ? "MaxVersion of $SSCal_api{CALEVENT}{NAME} selected: $apievtmaxver" : "MaxVersion of $SSCal_api{CALEVENT}{NAME} undefined - Synology cal Server may be stopped"; + Log3($name, 4, "$name - $logstr"); + + # Pfad und Maxversion von "SYNO.Cal.Sharing" ermitteln + my $apisharepath = $data->{data}->{$SSCal_api{CALSHARE}{NAME}}->{path}; + $apisharepath =~ tr/_//d if (defined($apisharepath)); + my $apisharemaxver = $data->{data}->{$SSCal_api{CALSHARE}{NAME}}->{maxVersion}; + + $logstr = defined($apisharepath) ? "Path of $SSCal_api{CALSHARE}{NAME} selected: $apisharepath" : "Path of $SSCal_api{CALSHARE}{NAME} undefined - Synology cal Server may be stopped"; + Log3($name, 4, "$name - $logstr"); + $logstr = defined($apisharemaxver) ? "MaxVersion of $SSCal_api{CALSHARE}{NAME} selected: $apisharemaxver" : "MaxVersion of $SSCal_api{CALSHARE}{NAME} undefined - Synology cal Server may be stopped"; + Log3($name, 4, "$name - $logstr"); + + # Pfad und Maxversion von "SYNO.Cal.Todo" ermitteln + my $apitodopath = $data->{data}->{$SSCal_api{CALTODO}{NAME}}->{path}; + $apitodopath =~ tr/_//d if (defined($apitodopath)); + my $apitodomaxver = $data->{data}->{$SSCal_api{CALTODO}{NAME}}->{maxVersion}; + + $logstr = defined($apitodopath) ? "Path of $SSCal_api{CALTODO}{NAME} selected: $apitodopath" : "Path of $SSCal_api{CALTODO}{NAME} undefined - Synology cal Server may be stopped"; + Log3($name, 4, "$name - $logstr"); + $logstr = defined($apitodomaxver) ? "MaxVersion of $SSCal_api{CALTODO}{NAME} selected: $apitodomaxver" : "MaxVersion of $SSCal_api{CALTODO}{NAME} undefined - Synology cal Server may be stopped"; + Log3($name, 4, "$name - $logstr"); + + + # ermittelte Werte in $hash einfügen + $SSCal_api{APIAUTH}{PATH} = $apiauthpath; + $SSCal_api{APIAUTH}{MAX} = $apiauthmaxver; + $SSCal_api{CALCAL}{PATH} = $apicalpath; + $SSCal_api{CALCAL}{MAX} = $apicalmaxver; + $SSCal_api{CALEVENT}{PATH} = $apievtpath; + $SSCal_api{CALEVENT}{MAX} = $apievtmaxver; + $SSCal_api{CALSHARE}{PATH} = $apisharepath; + $SSCal_api{CALSHARE}{MAX} = $apisharemaxver; + $SSCal_api{CALTODO}{PATH} = $apitodopath; + $SSCal_api{CALTODO}{MAX} = $apitodomaxver; + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash,"Errorcode","none"); + readingsBulkUpdateIfChanged ($hash,"Error", "none"); + readingsEndUpdate ($hash,1); + + # API values sind gesetzt in Hash + $hash->{HELPER}{APIPARSET} = 1; + + } else { + $errorcode = "806"; + $error = SSCal_experror($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "Errorcode", $errorcode); + readingsBulkUpdateIfChanged ($hash, "Error", $error); + readingsBulkUpdate ($hash,"state", "Error"); + readingsEndUpdate ($hash, 1); + + Log3($name, 2, "$name - ERROR - the API-Query couldn't be executed successfully"); + + SSCal_checkretry($name,1); + return; + } + } + +return SSCal_checkSID($name); +} + +############################################################################################# +# Ausführung Operation +############################################################################################# +sub SSCal_calop ($) { + my ($name) = @_; + my $hash = $defs{$name}; + my $prot = $hash->{PROT}; + my $addr = $hash->{ADDR}; + my $port = $hash->{PORT}; + my $sid = $hash->{HELPER}{SID}; + my ($url,$timeout,$param,$error,$errorcode); + + my $idx = $hash->{OPIDX}; + my $opmode = $hash->{OPMODE}; + my $method = $data{SSCal}{$name}{sendqueue}{entries}{$idx}{method}; + my $api = $data{SSCal}{$name}{sendqueue}{entries}{$idx}{api}; + my $params = $data{SSCal}{$name}{sendqueue}{entries}{$idx}{params}; + + Log3($name, 4, "$name - start SendQueue entry index \"$idx\" ($hash->{OPMODE}) for operation."); + + $timeout = AttrVal($name, "timeout", 20); + + Log3($name, 5, "$name - HTTP-Call will be done with timeout: $timeout s"); + + $url = "$prot://$addr:$port/webapi/".$SSCal_api{$api}{PATH}."?api=".$SSCal_api{$api}{NAME}."&version=".$SSCal_api{$api}{MAX}."&method=$method".$params."&_sid=$sid"; + + my $part = $url; + if(AttrVal($name, "showPassInLog", "0") == 1) { + Log3($name, 4, "$name - Call-Out: $url"); + } else { + $part =~ s/$sid//; + Log3($name, 4, "$name - Call-Out: $part"); + } + + $param = { + url => $url, + timeout => $timeout, + hash => $hash, + method => "GET", + header => "Accept: application/json", + callback => \&SSCal_calop_parse + }; + + HttpUtils_NonblockingGet ($param); +} + +############################################################################################# +# Callback from SSCal_calop +############################################################################################# +sub SSCal_calop_parse ($) { + my ($param, $err, $myjson) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $prot = $hash->{PROT}; + my $addr = $hash->{ADDR}; + my $port = $hash->{PORT}; + my $opmode = $hash->{OPMODE}; + my $am = AttrVal($name, "asyncMode", 0); + my ($ts,$data,$success,$error,$errorcode,$cherror,$r); + + if ($err ne "") { + # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist + Log3($name, 2, "$name - ERROR message: $err"); + + $errorcode = "none"; + $errorcode = "800" if($err =~ /: malformed or unsupported URL$/s); + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "Error", $err); + readingsBulkUpdateIfChanged ($hash, "Errorcode", $errorcode); + readingsBulkUpdate ($hash, "state", "Error"); + readingsEndUpdate ($hash,1); + + SSCal_checkretry($name,1); + return; + + } elsif ($myjson ne "") { + # wenn die Abfrage erfolgreich war ($data enthält die Ergebnisdaten des HTTP Aufrufes) + # Evaluiere ob Daten im JSON-Format empfangen wurden + ($hash,$success,$myjson) = SSCal_evaljson($hash,$myjson); + unless ($success) { + Log3($name, 4, "$name - Data returned: ".$myjson); + SSCal_checkretry($name,1); + return; + } + + $data = decode_json($myjson); + + # Logausgabe decodierte JSON Daten + Log3($name, 5, "$name - JSON returned: ". Dumper $data); + + $success = $data->{'success'}; + + if ($success) { + + if ($opmode eq "listcal") { # alle Kalender abrufen + my %calendars = (); + my ($cals,$dnm,$typ,$oid,$des,$prv,$psi); + my $i = 0; + + my $out = ""; + $out .= "Synology Calendar List

"; + $out .= ""; + $out .= ""; + $out .= ""; + + while ($data->{data}[$i]) { + $dnm = $data->{data}[$i]{cal_displayname}; + next if (!$dnm); + $typ = "Event" if($data->{data}[$i]{is_evt}); + $typ = "ToDo" if($data->{data}[$i]{is_todo}); + $oid = $data->{data}[$i]{original_cal_id}; + $des = encode("UTF-8", $data->{data}[$i]{cal_description}); + $prv = $data->{data}[$i]{cal_privilege}; + $psi = $data->{data}[$i]{cal_public_sharing_id}; + $psi = $psi?$psi:""; + + $calendars{$dnm}{id} = $oid; + $calendars{$dnm}{description} = $des; + $calendars{$dnm}{privilege} = $prv; + $calendars{$dnm}{publicshareid} = $psi; + $calendars{$dnm}{type} = $typ; + + $cals .= "," if($cals); + $cals .= $dnm; + $out .= ""; + + $i++; + } + + $out .= "
Calendar ID Type Description Privilege Public share ID
$dnm $oid $typ $des $prv $psi
"; + $out .= ""; + + $hash->{HELPER}{CALENDARS} = \%calendars if(%calendars); + $hash->{HELPER}{CALFETCHED} = 1; + + my @newa; + my $list = $modules{$hash->{TYPE}}{AttrList}; + my @deva = split(" ", $list); + foreach (@deva) { + push @newa, $_ if($_ !~ /usedCalendars:/); + } + + $cals =~ s/ /#/g if($cals); + + push @newa, ($cals?"usedCalendars:multiple-strict,$cals ":"usedCalendars:--no#Calendar#selectable--"); + + $hash->{".AttrList"} = join(" ", @newa); # Device spezifische AttrList, überschreibt Modul AttrList ! + + # Ausgabe Popup der User-Daten (nach readingsEndUpdate positionieren sonst + # "Connection lost, trying reconnect every 5 seconds" wenn > 102400 Zeichen) + asyncOutput($hash->{HELPER}{CL}{1},"$out"); + delete($hash->{HELPER}{CL}); + + SSCal_checkretry($name,0); + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "Errorcode", "none"); + readingsBulkUpdateIfChanged ($hash, "Error", "none"); + readingsBulkUpdate ($hash, "state", "done"); + readingsEndUpdate ($hash,1); + + } elsif ($opmode eq "eventlist") { # Events der ausgewählten Kalender aufbereiten + delete $data{SSCal}{$name}{eventlist}; # zentrales Eventlist-Hash löschen + $hash->{eventlist} = $data; # Data-Hashreferenz im Hash speichern + + if ($am) { # Extrahieren der Events asynchron (nicht-blockierend) + Log3($name, 4, "$name - Event parse mode: asynchronous"); + my $timeout = AttrVal($name, "timeout", 20)+180; + + $hash->{HELPER}{RUNNING_PID} = BlockingCall("SSCal_extractEventlist", $name, "SSCal_createReadings", $timeout, "SSCal_blockingTimeout", $hash); + $hash->{HELPER}{RUNNING_PID}{loglevel} = 5 if($hash->{HELPER}{RUNNING_PID}); # Forum #77057 + + } else { # Extrahieren der Events synchron (blockierend) + Log3($name, 4, "$name - Event parse mode: synchronous"); + SSCal_extractEventlist ($name); + } + } + + } else { + # die API-Operation war fehlerhaft + # Errorcode aus JSON ermitteln + $errorcode = $data->{error}->{code}; + $cherror = $data->{error}->{errors}; # vom cal gelieferter Fehler + $error = SSCal_experror($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + if ($error =~ /not found/) { + $error .= " New error: ".($cherror?$cherror:""); + } + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash,"Errorcode", $errorcode); + readingsBulkUpdateIfChanged ($hash,"Error", $error); + readingsBulkUpdate ($hash,"state", "Error"); + readingsEndUpdate ($hash, 1); + + Log3($name, 2, "$name - ERROR - Operation $opmode was not successful. Errorcode: $errorcode - $error"); + + SSCal_checkretry($name,1); + } + + undef $data; + undef $myjson; + } + +return; +} + +############################################################################################# +# Extrahiert empfangene Events der Kalenders aus $data +############################################################################################# +sub SSCal_extractEventlist ($) { + my ($name) = @_; + my $hash = $defs{$name}; + my $data = delete $hash->{eventlist}; + my $am = AttrVal($name, "asyncMode", 0); + + my ($val,$tz,$td,$d,$t,$uts); + my ($bdate,$btime,$bts,$edate,$etime,$ets,$ci,$numday,$bi,$ei,$startEndDiff); + my ($bmday,$bmonth,$emday,$emonth,$byear,$eyear,$nbdate,$nbtime,$nbts,$nedate,$netime,$nets,$ydiff); + my @row_array; + + my (undef,$tstart,$tend) = SSCal_timeEdge($name); # Sollstart- und Sollendezeit der Kalenderereignisse ermitteln + my $datetimestart = FmtDateTime($tstart); + my $datetimeend = FmtDateTime($tend); + + my $n = 0; + foreach my $key (keys %{$data->{data}}) { + my $i = 0; + + while ($data->{data}{$key}[$i]) { + my $ignore = 0; + my $done = 0; + ($nbdate,$nedate) = ("",""); + + ($bi,$tz,$bdate,$btime,$bts) = SSCal_explodeDateTime ($hash,$data->{data}{$key}[$i]{dtstart}); # Beginn des Events + ($ei,undef,$edate,$etime,$ets) = SSCal_explodeDateTime ($hash,$data->{data}{$key}[$i]{dtend}); # Ende des Events + $startEndDiff = $ets - $bts; # Differenz Event Ende / Start in Sekunden + + $bdate =~ /(\d{4})-(\d{2})-(\d{2})/; + $bmday = $3; + $bmonth = $2; + $byear = $1; + $nbtime = $btime; + + $edate =~ /(\d{4})-(\d{2})-(\d{2})/; + $emday = $3; + $emonth = $2; + $eyear = $1; + $netime = $etime; + + if(!$data->{data}{$key}[$i]{is_repeat_evt}) { # einmaliger Event + Log3($name, 5, "$name - Single event Begin: $bdate, End: $edate"); + + if($ets < $tstart || $bts > $tend) { + Log3($name, 4, "$name - Ignore single event -> $data->{data}{$key}[$i]{summary} start: $bdate $btime, end: $edate $etime"); + $ignore = 1; + $done = 0; + } else { + @row_array = SSCal_writeValuesToArray ($name,$n,$data->{data}{$key}[$i],$tz,$bdate,$btime,$bts,$edate,$etime,$ets,\@row_array); + $ignore = 0; + $done = 1; + } + + } elsif ($data->{data}{$key}[$i]{is_repeat_evt}) { # Event ist wiederholend + Log3($name, 5, "$name - Recurring event Begin: $bdate, End: $edate"); + + my ($freq,$count,$interval,$until,$uets,$bymonthday,$byday); + my $rr = $data->{data}{$key}[$i]{evt_repeat_setting}{repeat_rule}; + + # Format: FREQ=YEARLY;COUNT=1;INTERVAL=2;BYMONTHDAY=15;BYMONTH=10;UNTIL=2020-12-31T00:00:00 + my @para = split(";", $rr); + + foreach my $par (@para) { + my ($p1,$p2) = split("=", $par); + if ($p1 eq "FREQ") { + $freq = $p2; + } elsif ($p1 eq "COUNT") { # Event endet automatisch nach x Wiederholungen + $count = $p2; + } elsif ($p1 eq "INTERVAL") { # Wiederholungsintervall + $interval = $p2; + } elsif ($p1 eq "UNTIL") { # festes Intervallende angegeben + $until = $p2; + $until =~ s/[-:]//g; + (undef,undef,undef,undef,$uets) = SSCal_explodeDateTime ($hash,$until); + if ($uets < $tstart) { + Log3($name, 4, "$name - Ignore recurring event -> $data->{data}{$key}[$i]{summary} , interval end \"$nedate $netime\" is less than selection start \"$datetimestart\""); + $ignore = 1; + } + } elsif ($p1 eq "BYMONTHDAY") { # Wiederholungseigenschaft -> Tag des Monats z.B. 13 (Tag 13) + $bymonthday = $p2; + } elsif ($p1 eq "BYDAY") { # Wiederholungseigenschaft -> Wochentag z.B. 2WE,-1SU,4FR (kann auch Liste bei WEEKLY sein) + $byday = $p2; + } + } + + $count = $count?$count:9999999; # $count "unendlich" wenn kein COUNT angegeben + $interval = $interval?$interval:1; + $bymonthday = $bymonthday?$bymonthday:""; + $byday = $byday?$byday:""; + $until = $until?$until:""; + + Log3($name, 4, "$name - Recurring params - FREQ: $freq, COUNT: $count, INTERVAL: $interval, BYMONTHDAY: $bymonthday, BYDAY: $byday, UNTIL: $until"); + + if ($freq eq "YEARLY") { # jährliche Wiederholung + for ($ci=-1; $ci<($count*$interval); $ci+=$interval) { + $byear += ($ci>=0?1:0); + $eyear += ($ci>=0?1:0); + + $nbtime =~ s/://g; + $netime =~ s/://g; + + ($bi,undef,$nbdate,$nbtime,$nbts) = SSCal_explodeDateTime ($hash, $byear.$bmonth.$bmday."T".$nbtime); # Beginn des Wiederholungsevents + ($ei,undef,$nedate,$netime,$nets) = SSCal_explodeDateTime ($hash, $eyear.$emonth.$emday."T".$netime); # Ende des Wiederholungsevents + + Log3($name, 5, "$name - YEARLY event - Begin: $nbdate $nbtime, End: $nedate $netime"); + + if (defined $uets && ($uets < $nbts)) { # Event Ende (UNTIL) kleiner aktueller Select Start + Log3($name, 4, "$name - Ignore YEARLY event due to UNTIL -> $data->{data}{$key}[$i]{summary} , start: $nbdate $nbtime, end: $nedate $netime, until: $until"); + $ignore = 1; + $done = 0; + } elsif ($nets < $tstart || $nbts > $tend) { # Event Ende kleiner Select Start oder Beginn Event größer als Select Ende + Log3($name, 4, "$name - Ignore YEARLY event -> $data->{data}{$key}[$i]{summary} , start: $nbdate $nbtime, end: $nedate $netime"); + $ignore = 1; + $done = 0; + } else { + $bdate = $nbdate?$nbdate:$bdate; + $btime = $nbtime?$nbtime:$btime; + $bts = $nbts?$nbts:$bts; + + $edate = $nedate?$nedate:$edate; + $etime = $netime?$netime:$etime; + $ets = $nets?$nets:$ets; + + @row_array = SSCal_writeValuesToArray ($name,$n,$data->{data}{$key}[$i],$tz,$bdate,$btime,$bts,$edate,$etime,$ets,\@row_array); + + $ignore = 0; + $done = 1; + $n++; + next; + } + last if((defined $uets && ($uets < $nbts)) || $nbts > $tend); + } + } + + if ($freq eq "MONTHLY") { # monatliche Wiederholung + if ($bymonthday) { # Wiederholungseigenschaft am Tag X des Monats + for ($ci=-1; $ci<($count*$interval); $ci+=$interval) { + $bmonth += $interval; + $byear += int( $bmonth/13); + $bmonth %= 12 if($bmonth>12); + $bmonth = sprintf("%02d", $bmonth); + + $emonth += $interval; + $eyear += int( $emonth/13); + $emonth %= 12 if($emonth>12); + $emonth = sprintf("%02d", $emonth); + + $nbtime =~ s/://g; + $netime =~ s/://g; + + ($bi,undef,$nbdate,$nbtime,$nbts) = SSCal_explodeDateTime ($hash, $byear.$bmonth.$bmday."T".$nbtime); # Beginn des Wiederholungsevents + ($ei,undef,$nedate,$netime,$nets) = SSCal_explodeDateTime ($hash, $eyear.$emonth.$emday."T".$netime); # Ende des Wiederholungsevents + + Log3($name, 5, "$name - MONTHLY event - Begin: $nbdate $nbtime, End: $nedate $netime"); + + if (defined $uets && ($uets < $nbts)) { # Event Ende (UNTIL) kleiner aktueller Select Start + Log3($name, 4, "$name - Ignore MONTHLY event due to UNTIL -> $data->{data}{$key}[$i]{summary} , start: $nbdate $nbtime, end: $nedate $netime, until: $until"); + $ignore = 1; + $done = 0; + } elsif ($nets < $tstart || $nbts > $tend) { # Event Ende kleiner Select Start oder Beginn Event größer als Select Ende + Log3($name, 4, "$name - Ignore MONTHLY event -> $data->{data}{$key}[$i]{summary} , start: $nbdate $nbtime, end: $nedate $netime"); + $ignore = 1; + $done = 0; + } else { + $bdate = $nbdate?$nbdate:$bdate; + $btime = $nbtime?$nbtime:$btime; + $bts = $nbts?$nbts:$bts; + + $edate = $nedate?$nedate:$edate; + $etime = $netime?$netime:$etime; + $ets = $nets?$nets:$ets; + + @row_array = SSCal_writeValuesToArray ($name,$n,$data->{data}{$key}[$i],$tz,$bdate,$btime,$bts,$edate,$etime,$ets,\@row_array); + + $ignore = 0; + $done = 1; + $n++; + next; + } + last if((defined $uets && ($uets < $nbts)) || $nbts > $tend); + } + } + if ($byday) { # Wiederholungseigenschaft -> Wochentag z.B. 2WE,-1SU,4FR (kann auch Liste bei WEEKLY sein) + my ($nbhh,$nbmm,$nbss,$nehh,$nemm,$ness,$rDayOfWeekNew,$rDaysToAddOrSub,$rNewTime,$rbYday); + my @ByDays = split(",", $byday); # Array der Wiederholungstage + + foreach (@ByDays) { + my $rByDay = $_; # das erste Wiederholungselement + my $rByDayLength = length($rByDay); # die Länge des Strings + + my $rDayStr; # Tag auf den das Datum gesetzt werden soll + my $rDayInterval; # z.B. 2 = 2nd Tag des Monats oder -1 = letzter Tag des Monats + if ($rByDayLength > 2) { + $rDayStr = substr($rByDay, -2); + $rDayInterval = int(substr($rByDay, 0, $rByDayLength - 2)); + } else { + $rDayStr = $rByDay; + $rDayInterval = 1; + } + + my @weekdays = qw(SU MO TU WE TH FR SA); + my ($rDayOfWeek) = grep {$weekdays[$_] eq $rDayStr} 0..$#weekdays; # liefert Nr des Wochentages: SU = 0 ... SA = 6 + + for ($ci=-1; $ci<($count); $ci++) { + if ($rDayInterval > 0) { # Angabe "jeder x Wochentag" ist positiv (-2 wäre z.B. vom Ende des Monats zu zähelen) + $bmonth += $interval; + $byear += int( $bmonth/13); + $bmonth %= 12 if($bmonth>12); + $bmonth = sprintf("%02d", $bmonth); + + ($nbhh,$nbmm,$nbss) = split(":", $nbtime); + my $firstOfNextMonth = fhemTimeLocal($nbss, $nbmm, $nbhh, 1, $bmonth-1, $byear-1900); + ($nbss, $nbmm, $nbhh, $bmday, $bmonth, $byear, $rDayOfWeekNew, undef, undef) = localtime($firstOfNextMonth); # den 1. des Monats sowie die dazu gehörige Nr. des Wochentages + + if ($rDayOfWeekNew <= $rDayOfWeek) { # Nr Wochentag des 1. des Monats <= als Wiederholungstag + $rDaysToAddOrSub = $rDayOfWeek - $rDayOfWeekNew; + } else { + $rDaysToAddOrSub = 7 - $rDayOfWeekNew + $rDayOfWeek; + } + $rDaysToAddOrSub += (7 * ($rDayInterval - 1)); # addiere Tagesintervall, z.B. 4th Freitag ... + + $rNewTime = SSCal_plusNSeconds($firstOfNextMonth, 86400*$rDaysToAddOrSub, 1); + ($nbss,$nbmm,$nbhh,$bmday,$bmonth,$byear,$ness,$nemm,$nehh,$emday,$emonth,$eyear) = SSCal_DTfromStartandDiff ($rNewTime,$startEndDiff); + + } else { + Log3($name, 2, "$name - WARNING - negative values for BYDAY are currently not implemented and will be ignored"); + $ignore = 1; + $done = 0; + $n++; + next; + } + + $nbtime = $nbhh.$nbmm.$nbss; + $netime = $nehh.$nemm.$ness; + + ($bi,undef,$nbdate,$nbtime,$nbts) = SSCal_explodeDateTime ($hash, $byear.$bmonth.$bmday."T".$nbtime); # Beginn des Wiederholungsevents + ($ei,undef,$nedate,$netime,$nets) = SSCal_explodeDateTime ($hash, $eyear.$emonth.$emday."T".$netime); # Ende des Wiederholungsevents + + Log3($name, 5, "$name - MONTHLY event - Begin: $nbdate $nbtime, End: $nedate $netime"); + + if (defined $uets && ($uets < $nbts)) { # Event Ende (UNTIL) kleiner aktueller Select Start + Log3($name, 4, "$name - Ignore MONTHLY event due to UNTIL -> $data->{data}{$key}[$i]{summary} , start: $nbdate $nbtime, end: $nedate $netime, until: $until"); + $ignore = 1; + $done = 0; + } elsif ($nets < $tstart || $nbts > $tend) { # Event Ende kleiner Select Start oder Beginn Event größer als Select Ende + Log3($name, 4, "$name - Ignore MONTHLY event -> $data->{data}{$key}[$i]{summary} , start: $nbdate $nbtime, end: $nedate $netime"); + $ignore = 1; + $done = 0; + } else { + $bdate = $nbdate?$nbdate:$bdate; + $btime = $nbtime?$nbtime:$btime; + $bts = $nbts?$nbts:$bts; + + $edate = $nedate?$nedate:$edate; + $etime = $netime?$netime:$etime; + $ets = $nets?$nets:$ets; + + @row_array = SSCal_writeValuesToArray ($name,$n,$data->{data}{$key}[$i],$tz,$bdate,$btime,$bts,$edate,$etime,$ets,\@row_array); + + $ignore = 0; + $done = 1; + $n++; + next; + } + last if((defined $uets && ($uets < $nbts)) || $nbts > $tend); + } + } + } + } + + if ($freq eq "WEEKLY") { # wöchentliche Wiederholung + if ($byday) { # Wiederholungseigenschaft -> Wochentag z.B. 2WE,-1SU,4FR (kann auch Liste bei WEEKLY sein) + my ($nbhh,$nbmm,$nbss,$nehh,$nemm,$ness,$rDayOfWeekNew,$rDaysToAddOrSub); + my @ByDays = split(",", $byday); # Array der Wiederholungstage + my $btsstart = $bts; + + foreach (@ByDays) { + my $rNewTime = $btsstart; + my $rByDay = $_; # das erste Wiederholungselement + my $rByDayLength = length($rByDay); # die Länge des Strings + + my $rDayStr; # Tag auf den das Datum gesetzt werden soll + my $rDayInterval; # z.B. 2 = 2nd Tag des Monats oder -1 = letzter Tag des Monats + if ($rByDayLength > 2) { + $rDayStr = substr($rByDay, -2); + $rDayInterval = int(substr($rByDay, 0, $rByDayLength - 2)); + } else { + $rDayStr = $rByDay; + $rDayInterval = 1; + } + + my @weekdays = qw(SU MO TU WE TH FR SA); + my ($rDayOfWeek) = grep {$weekdays[$_] eq $rDayStr} 0..$#weekdays; # liefert Nr des Wochentages: SU = 0 ... SA = 6 + + for ($ci=-1; $ci<($count*$interval); $ci++) { + + $rNewTime += $interval*604800 if($ci>=0); # Wochenintervall addieren + ($nbss, $nbmm, $nbhh, $bmday, $bmonth, $byear, $rDayOfWeekNew, undef, undef) = localtime($rNewTime); + + ($nbhh,$nbmm,$nbss) = split(":", $nbtime); + + if ($rDayOfWeekNew <= $rDayOfWeek) { # Nr aktueller Wochentag <= Sollwochentag + $rDaysToAddOrSub = $rDayOfWeek - $rDayOfWeekNew; + } else { + $rDaysToAddOrSub = 7 - $rDayOfWeekNew + $rDayOfWeek; + $rNewTime -= 604800; # eine Woche zurückgehen wenn Korrektur aufaddiert wurde + } + + $rDaysToAddOrSub += (7 * ($rDayInterval - 1)); # addiere Tagesintervall, z.B. 4th Freitag ... + + $rNewTime = SSCal_plusNSeconds($rNewTime, 86400*$rDaysToAddOrSub, 1); + ($nbss,$nbmm,$nbhh,$bmday,$bmonth,$byear,$ness,$nemm,$nehh,$emday,$emonth,$eyear) = SSCal_DTfromStartandDiff ($rNewTime,$startEndDiff); + + $nbtime = $nbhh.$nbmm.$nbss; + $netime = $nehh.$nemm.$ness; + + ($bi,undef,$nbdate,$nbtime,$nbts) = SSCal_explodeDateTime ($hash, $byear.$bmonth.$bmday."T".$nbtime); # Beginn des Wiederholungsevents + ($ei,undef,$nedate,$netime,$nets) = SSCal_explodeDateTime ($hash, $eyear.$emonth.$emday."T".$netime); # Ende des Wiederholungsevents + + Log3($name, 5, "$name - WEEKLY event - Begin: $nbdate $nbtime, End: $nedate $netime"); + + if (defined $uets && ($uets < $nbts)) { # Event Ende (UNTIL) kleiner aktueller Select Start + Log3($name, 4, "$name - Ignore WEEKLY event due to UNTIL -> $data->{data}{$key}[$i]{summary} , start: $nbdate $nbtime, end: $nedate $netime, until: $until"); + $ignore = 1; + $done = 0; + } elsif ($nets < $tstart || $nbts > $tend) { # Event Ende kleiner Select Start oder Beginn Event größer als Select Ende + Log3($name, 4, "$name - Ignore WEEKLY event -> $data->{data}{$key}[$i]{summary} , start: $nbdate $nbtime, end: $nedate $netime"); + $ignore = 1; + $done = 0; + } else { + $bdate = $nbdate?$nbdate:$bdate; + $btime = $nbtime?$nbtime:$btime; + $bts = $nbts?$nbts:$bts; + + $edate = $nedate?$nedate:$edate; + $etime = $netime?$netime:$etime; + $ets = $nets?$nets:$ets; + + @row_array = SSCal_writeValuesToArray ($name,$n,$data->{data}{$key}[$i],$tz,$bdate,$btime,$bts,$edate,$etime,$ets,\@row_array); + + $ignore = 0; + $done = 1; + $n++; + next; + } + last if((defined $uets && ($uets < $nbts)) || $nbts > $tend); + } + } + + } else { + my ($nbhh,$nbmm,$nbss,$nehh,$nemm,$ness,$rDayOfWeekNew,$rDaysToAddOrSub); + my $rNewTime = $bts; + + for ($ci=-1; $ci<($count*$interval); $ci++) { + $rNewTime += $interval*604800 if($ci>=0); # Wochenintervall addieren + + ($nbss,$nbmm,$nbhh,$bmday,$bmonth,$byear,$ness,$nemm,$nehh,$emday,$emonth,$eyear) = SSCal_DTfromStartandDiff ($rNewTime,$startEndDiff); + $nbtime = $nbhh.$nbmm.$nbss; + $netime = $nehh.$nemm.$ness; + + ($bi,undef,$nbdate,$nbtime,$nbts) = SSCal_explodeDateTime ($hash, $byear.$bmonth.$bmday."T".$nbtime); # Beginn des Wiederholungsevents + ($ei,undef,$nedate,$netime,$nets) = SSCal_explodeDateTime ($hash, $eyear.$emonth.$emday."T".$netime); # Ende des Wiederholungsevents + + Log3($name, 5, "$name - WEEKLY event - Begin: $nbdate $nbtime, End: $nedate $netime"); + + if (defined $uets && ($uets < $nbts)) { # Event Ende (UNTIL) kleiner aktueller Select Start + Log3($name, 4, "$name - Ignore WEEKLY event due to UNTIL -> $data->{data}{$key}[$i]{summary} , start: $nbdate $nbtime, end: $nedate $netime, until: $until"); + $ignore = 1; + $done = 0; + } elsif ($nets < $tstart || $nbts > $tend) { # Event Ende kleiner Select Start oder Beginn Event größer als Select Ende + Log3($name, 4, "$name - Ignore WEEKLY event -> $data->{data}{$key}[$i]{summary} , start: $nbdate $nbtime, end: $nedate $netime"); + $ignore = 1; + $done = 0; + } else { + $bdate = $nbdate?$nbdate:$bdate; + $btime = $nbtime?$nbtime:$btime; + $bts = $nbts?$nbts:$bts; + + $edate = $nedate?$nedate:$edate; + $etime = $netime?$netime:$etime; + $ets = $nets?$nets:$ets; + + @row_array = SSCal_writeValuesToArray ($name,$n,$data->{data}{$key}[$i],$tz,$bdate,$btime,$bts,$edate,$etime,$ets,\@row_array); + + $ignore = 0; + $done = 1; + $n++; + next; + } + last if((defined $uets && ($uets < $nbts)) || $nbts > $tend); + } + } + } + + if ($freq eq "DAILY") { # tägliche Wiederholung + my ($nbhh,$nbmm,$nbss,$nehh,$nemm,$ness); + for ($ci=-1; $ci<($count*$interval); $ci+=$interval) { + + $bts += 86400 if($ci>=0); + + ($nbss,$nbmm,$nbhh,$bmday,$bmonth,$byear,$ness,$nemm,$nehh,$emday,$emonth,$eyear) = SSCal_DTfromStartandDiff ($bts,$startEndDiff); + + $nbtime = $nbhh.$nbmm.$nbss; + $netime = $nehh.$nemm.$ness; + + ($bi,undef,$nbdate,$nbtime,$nbts) = SSCal_explodeDateTime ($hash, $byear.$bmonth.$bmday."T".$nbtime); # Beginn des Wiederholungsevents + ($ei,undef,$nedate,$netime,$nets) = SSCal_explodeDateTime ($hash, $eyear.$emonth.$emday."T".$netime); # Ende des Wiederholungsevents + + Log3($name, 5, "$name - DAILY event - Begin: $nbdate $nbtime, End: $nedate $netime"); + + if (defined $uets && ($uets < $nbts)) { # Event Ende (UNTIL) kleiner aktueller Select Start + Log3($name, 4, "$name - Ignore DAILY event due to UNTIL -> $data->{data}{$key}[$i]{summary} , start: $nbdate $nbtime, end: $nedate $netime, until: $until"); + $ignore = 1; + $done = 0; + } elsif ($nets < $tstart || $nbts > $tend) { # Event Ende kleiner Select Start oder Beginn Event größer als Select Ende + Log3($name, 4, "$name - Ignore DAILY event -> $data->{data}{$key}[$i]{summary} , start: $nbdate $nbtime, end: $nedate $netime"); + $ignore = 1; + $done = 0; + } else { + $bdate = $nbdate?$nbdate:$bdate; + $btime = $nbtime?$nbtime:$btime; + $bts = $nbts?$nbts:$bts; + + $edate = $nedate?$nedate:$edate; + $etime = $netime?$netime:$etime; + $ets = $nets?$nets:$ets; + + @row_array = SSCal_writeValuesToArray ($name,$n,$data->{data}{$key}[$i],$tz,$bdate,$btime,$bts,$edate,$etime,$ets,\@row_array); + + $ignore = 0; + $done = 1; + $n++; + next; + } + last if((defined $uets && ($uets < $nbts)) || $nbts > $tend); + } + } + } + + if ($ignore == 1) { + $i++; + next; + } + + if(!$done) { # für Testzwecke mit $ignore = 0 und $done = 0 + $bdate = $nbdate?$nbdate:$bdate; + $btime = $nbtime?$nbtime:$btime; + $bts = $nbts?$nbts:$bts; + + $edate = $nedate?$nedate:$edate; + $etime = $netime?$netime:$etime; + $ets = $nets?$nets:$ets; + + @row_array = SSCal_writeValuesToArray ($name,$n,$data->{data}{$key}[$i],$tz,$bdate,$btime,$bts,$edate,$etime,$ets,\@row_array); + } + $i++; + $n++; + } + $n++; + } + + # encoding result + my $rowlist = join('_ESC_', @row_array); + $rowlist = encode_base64($rowlist,""); + + if($am) { # asynchroner Mode mit BlockingCall + return "$name|$rowlist"; + } else { # synchoner Modes + return SSCal_createReadings ("$name|$rowlist"); + } +} + +############################################################################################# +# füllt zentrales Datenhash +# $data{SSCal}{$name}{eventlist} = Referenz zum zentralen Valuehash +# erstellt Readings aus zentralen Eventarray +############################################################################################# +sub SSCal_createReadings ($) { + my ($string) = @_; + my @a = split("\\|",$string); + my $name = $a[0]; + my $hash = $defs{$name}; + my $rowlist = decode_base64($a[1]); + + my @row_array = split("_ESC_", $rowlist); + + # zentrales Datenhash füllen (erzeugt dadurch sortierbare Keys) + foreach my $row (@row_array) { + chomp $row; + my @r = split(" ", $row, 3); + $data{SSCal}{$name}{eventlist}{$r[0]}{$r[1]} = $r[2]; + } + + # Readings der Eventliste erstellen + if($data{SSCal}{$name}{eventlist}) { + my $l = length(keys %{$data{SSCal}{$name}{eventlist}}); # Anzahl Stellen des max. Index ermitteln + readingsBeginUpdate($hash); + $data{SSCal}{$name}{lstUpdtTs} = $hash->{".updateTime"}; # letzte Updatezeit speichern (Unix Format) + + my $k = 0; + foreach my $idx (sort keys %{$data{SSCal}{$name}{eventlist}}) { + my $idxstr = sprintf("%0$l.0f", $k); # Prestring erstellen + foreach my $r (keys %{$data{SSCal}{$name}{eventlist}{$idx}}) { + if($r =~ /.*Timestamp$/) { # Readings mit Unix Timestamps versteckt erstellen + readingsBulkUpdate($hash, ".".$idxstr."_".$r, $data{SSCal}{$name}{eventlist}{$idx}{$r}); + } else { + readingsBulkUpdate($hash, $idxstr."_".$r, $data{SSCal}{$name}{eventlist}{$idx}{$r}); + } + } + $k += 1; + } + + readingsEndUpdate($hash, 1); + + } else { + SSCal_delReadings($name,0); # alle Kalender-Readings löschen + } + + SSCal_checkretry($name,0); + + $data{SSCal}{$name}{lastUpdate} = FmtDateTime($data{SSCal}{$name}{lstUpdtTs}) if($data{SSCal}{$name}{lstUpdtTs}); + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "Errorcode", "none"); + readingsBulkUpdateIfChanged ($hash, "Error", "none"); + readingsBulkUpdate ($hash, "lastUpdate", $data{SSCal}{$name}{lastUpdate}); + readingsBulkUpdate ($hash, "state", "done"); + readingsEndUpdate ($hash,1); + + SSCal_delReadings($name,1) if($data{SSCal}{$name}{lstUpdtTs}); # Readings löschen wenn Timestamp nicht "lastUpdate" + +return; +} + +#################################################################################################### +# Abbruchroutine BlockingCall +#################################################################################################### +sub SSCal_blockingTimeout(@) { + my ($hash,$cause) = @_; + my $name = $hash->{NAME}; + + $cause = $cause?$cause:"Timeout: process terminated"; + Log3 ($name, 1, "$name -> BlockingCall $hash->{HELPER}{RUNNING_PID}{fn} pid:$hash->{HELPER}{RUNNING_PID}{pid} $cause"); + + SSCal_checkretry($name,0); + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "Error", $cause); + readingsBulkUpdateIfChanged ($hash, "Errorcode", "none"); + readingsBulkUpdate ($hash, "state", "Error"); + readingsEndUpdate ($hash,1); + + delete($hash->{HELPER}{RUNNING_PID}); + +return; +} + +############################################################################################# +# liefert aus Unix Timestamp Beginn $bts und einer Differenz (Sekunden) das Beginn und +# Endedatum in der Form: +# Beginn: SS,MM,HH,Tag(01-31),Monat(01-12),Jahr(YYYY) +# Ende: SS,MM,HH,Tag(01-31),Monat(01-12),Jahr(YYYY) +############################################################################################# +sub SSCal_DTfromStartandDiff ($$) { + my ($bts,$diff) = @_; + + my ($nbss, $nbmm, $nbhh, $bmday, $bmonth, $byear, $bWday, $bYday, $bisdst); + my ($ness, $nemm, $nehh, $emday, $emonth, $eyear, $eWday, $eYday, $eisdst); + ($nbss, $nbmm, $nbhh, $bmday, $bmonth, $byear, $bWday, $bYday, $bisdst) = localtime($bts); + $nbss = sprintf("%02d", $nbss); + $nbmm = sprintf("%02d", $nbmm); + $nbhh = sprintf("%02d", $nbhh); + $bmday = sprintf("%02d", $bmday); + $bmonth = sprintf("%02d", $bmonth+1); + $byear += 1900; + + ($ness, $nemm, $nehh, $emday, $emonth, $eyear, $eWday, $eYday, $eisdst) = localtime($bts+$diff); + $ness = sprintf("%02d", $ness); + $nemm = sprintf("%02d", $nemm); + $nehh = sprintf("%02d", $nehh); + $emday = sprintf("%02d", $emday); + $emonth = sprintf("%02d", $emonth+1); + $eyear += 1900; + +return ($nbss,$nbmm,$nbhh,$bmday,$bmonth,$byear,$ness,$nemm,$nehh,$emday,$emonth,$eyear); +} + +############################################################################################# +# schreibe Key/Value Pairs in zentrales Valuearray zur Readingerstellung +# $n = Zusatz f. lfd. Nr. zur Unterscheidung exakt +# zeitgleicher Events +# $vh = Referenz zum Kalenderdatenhash +# +# Ergebisarray Aufbau: +# 0 1 2 +# (Index aus BeginTimestamp+lfNr) , (Blockindex_Reading) , (Wert) +############################################################################################# +sub SSCal_writeValuesToArray ($$$$$$$$$$$) { + my ($name,$n,$vh,$tz,$bdate,$btime,$bts,$edate,$etime,$ets,$aref) = @_; + my @row_array = @{$aref}; + my $hash = $defs{$name}; + my $ts = time(); # Istzeit Timestamp + my $status = ""; + my ($val,$uts,$td); + + my ($upcoming,$alarmed,$started,$ended) = (0,0,0,0); + + $upcoming = SSCal_isUpcoming ($ts,0,$bts); # initiales upcoming + $started = SSCal_isStarted ($ts,$bts,$ets); + $ended = SSCal_isEnded ($ts,$ets); + + push(@row_array, $bts+$n." 02_Begin " .$bdate." ".$btime."\n"); + push(@row_array, $bts+$n." 03_End " .$edate." ".$etime."\n"); + push(@row_array, $bts+$n." 04_bTimestamp " .$bts."\n"); + push(@row_array, $bts+$n." 05_eTimestamp " .$ets."\n"); + push(@row_array, $bts+$n." 09_Timezone " .$tz."\n"); + + foreach my $p (keys %{$vh}) { + $vh->{$p} = "" if(!$vh->{$p}); + + # Log3($name, 4, "$name - Parameter: $p, Value: ".$vh->{$p}) if(ref $p ne "HASH"); + + $val = encode("UTF-8", $vh->{$p}); + + push(@row_array, $bts+$n." 01_Summary " .$val."\n") if($p eq "summary"); + push(@row_array, $bts+$n." 06_Description " .$val."\n") if($p eq "description"); + push(@row_array, $bts+$n." 07_Location " .$val."\n") if($p eq "location"); + push(@row_array, $bts+$n." 08_GPS " .$val."\n") if($p eq "gps"); + push(@row_array, $bts+$n." 11_isAllday " .$val."\n") if($p eq "is_all_day"); + push(@row_array, $bts+$n." 12_isRepeatEvt " .$val."\n") if($p eq "is_repeat_evt"); + push(@row_array, $bts+$n." 20_calName " .SSCal_getCalFromId($hash,$val)."\n") if($p eq "original_cal_id"); + + if($p eq "evt_repeat_setting") { + foreach my $r (keys %{$vh->{evt_repeat_setting}}) { + $vh->{$p}{$r} = "" if(!$vh->{$p}{$r}); + + # Log3($name, 4, "$name - id: $key, Entry: $i, Parameter: $p, Value: ".$data->{data}->{$key}->[$i]->{$p}->{$r}); + $val = encode("UTF-8", $vh->{$p}{$r}); + + push(@row_array, $bts+$n." 13_repeatRule ".$val."\n") if($r eq "repeat_rule"); + } + } + if($p eq "evt_notify_setting") { + my $l = length (scalar @{$vh->{evt_notify_setting}}); # Anzahl Stellen (Länge) des aktuellen Arrays + my $ens = 0; + + while ($vh->{evt_notify_setting}[$ens]) { + + foreach my $r (keys %{$vh->{evt_notify_setting}[$ens]}) { + $vh->{$p}[$ens]{$r} = "" if(!$vh->{$p}[$ens]{$r}); + $val = encode("UTF-8", $vh->{$p}[$ens]{$r}); + + if($r eq "time_value") { # Erinnerungstermine (Array) relativ zur Beginnzeit ermitteln + ($uts,$td) = SSCal_evtNotTime ($name,$val,$bts); + push(@row_array, $bts+$n." 14_".sprintf("%0$l.0f", $ens)."_notifyTimestamp ".$uts."\n"); + push(@row_array, $bts+$n." 14_".sprintf("%0$l.0f", $ens)."_notifyDateTime " .$td."\n"); + $alarmed = SSCal_isAlarmed ($ts,$uts,$bts) if(!$alarmed); + } + } + $ens++; + } + } + } + + $status = "upcoming" if($upcoming); + $status = "alarmed" if($alarmed); + $status = "started" if($started); + $status = "ended" if($ended); + + push(@row_array, $bts+$n." 10_Status " .$status."\n"); + push(@row_array, $bts+$n." 99_---------------------- " ."--------------------------------------------------------------------"."\n"); + +return @row_array; +} + +############################################################################################# +# Ist Event bevorstehend ? +# Rückkehrwert 1 wenn aktueller Timestamp $ts vor Alarmzeit $ats und vor Startzeit $bts, +# sonst 0 +############################################################################################# +sub SSCal_isUpcoming ($$$) { + my ($ts,$ats,$bts) = @_; + + if($ats) { + return $ts < $ats ? 1 : 0; + } else { + return $ts < $bts ? 1 : 0; + } +} + +############################################################################################# +# Ist Event Alarmzeit erreicht ? +# Rückkehrwert 1 wenn aktueller Timestamp $ts zwischen Alarmzeit $ats und Startzeit $bts, +# sonst 0 +############################################################################################# +sub SSCal_isAlarmed ($$$) { + my ($ts,$ats,$bts) = @_; + + return $ats ? (($ats <= $ts && $ts < $bts) ? 1 : 0) : 0; +} + +############################################################################################# +# Ist Event gestartet ? +# Rückkehrwert 1 wenn aktueller Timestamp $ts zwischen Startzeit $bts und Endezeit $ets, +# sonst 0 +############################################################################################# +sub SSCal_isStarted ($$$) { + my ($ts,$bts,$ets) = @_; + + return 0 unless($bts); + return 0 if($ts < $bts); + + if(defined($ets)) { + return 0 if($ts >= $ets); + } + +return 1; +} + +############################################################################################# +# Ist Event beendet ? +# Rückkehrwert 1 wenn aktueller Timestamp $ts größer Endezeit $ets, +# sonst 0 +############################################################################################# +sub SSCal_isEnded ($$) { + my ($ts,$ets) = @_; + + return 0 unless($ets && $ts); + return $ets <= $ts ? 1 : 0; +} + +############################################################################################# +# check SID +############################################################################################# +sub SSCal_checkSID ($) { + my ($name) = @_; + my $hash = $defs{$name}; + + # SID holen bzw. login + my $subref = "SSCal_calop"; + if(!$hash->{HELPER}{SID}) { + Log3($name, 3, "$name - no session ID found - get new one"); + SSCal_login($hash,$subref); + return; + } + +return SSCal_calop($name); +} + +#################################################################################### +# Login for SID +#################################################################################### +sub SSCal_login ($$) { + my ($hash,$fret) = @_; + my $name = $hash->{NAME}; + my $serveraddr = $hash->{ADDR}; + my $serverport = $hash->{PORT}; + my $proto = $hash->{PROT}; + my $apiauth = $SSCal_api{APIAUTH}{NAME}; + my $apiauthpath = $SSCal_api{APIAUTH}{PATH}; + my $apiauthmaxver = $SSCal_api{APIAUTH}{MAX}; + + my $lrt = AttrVal($name,"loginRetries",3); + my ($url,$param); + + delete $hash->{HELPER}{SID}; + + # Login und SID ermitteln + Log3($name, 4, "$name - --- Start Synology Calendar login ---"); + + # Credentials abrufen + my ($success, $username, $password) = SSCal_getcredentials($hash,0,"credentials"); + + unless ($success) { + Log3($name, 2, "$name - Credentials couldn't be obtained successfully - make sure you've set it with \"set $name credentials \""); + return; + } + + if($hash->{HELPER}{LOGINRETRIES} >= $lrt) { + # login wird abgebrochen + Log3($name, 2, "$name - ERROR - Login or privilege of user $username unsuccessful"); + return; + } + + my $timeout = AttrVal($name,"timeout",60); + $timeout = 60 if($timeout < 60); + Log3($name, 4, "$name - HTTP-Call login will be done with http timeout value: $timeout s"); + + my $urlwopw; # nur zur Anzeige bei verbose >= 4 und "showPassInLog" == 0 + + $url = "$proto://$serveraddr:$serverport/webapi/$apiauthpath?api=$apiauth&version=$apiauthmaxver&method=login&account=$username&passwd=$password&format=sid"; + $urlwopw = "$proto://$serveraddr:$serverport/webapi/$apiauthpath?api=$apiauth&version=$apiauthmaxver&method=login&account=$username&passwd=*****&format=sid"; + + AttrVal($name, "showPassInLog", "0") == 1 ? Log3($name, 4, "$name - Call-Out now: $url") : Log3($name, 4, "$name - Call-Out now: $urlwopw"); + $hash->{HELPER}{LOGINRETRIES}++; + + $param = { + url => $url, + timeout => $timeout, + hash => $hash, + user => $username, + funcret => $fret, + method => "GET", + header => "Accept: application/json", + callback => \&SSCal_login_return + }; + HttpUtils_NonblockingGet ($param); +} + +sub SSCal_login_return ($) { + my ($param, $err, $myjson) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $username = $param->{user}; + my $fret = $param->{funcret}; + my $subref = \&$fret; + my $success; + + # Verarbeitung der asynchronen Rückkehrdaten aus sub "login_nonbl" + if ($err ne "") { + # ein Fehler bei der HTTP Abfrage ist aufgetreten + Log3($name, 2, "$name - error while requesting ".$param->{url}." - $err"); + + readingsSingleUpdate($hash, "Error", $err, 1); + + return SSCal_login($hash,$fret); + + } elsif ($myjson ne "") { + # Evaluiere ob Daten im JSON-Format empfangen wurden + ($hash, $success) = SSCal_evaljson($hash,$myjson); + unless ($success) { + Log3($name, 4, "$name - no JSON-Data returned while login: ".$myjson); + return; + } + + my $data = decode_json($myjson); + + # Logausgabe decodierte JSON Daten + Log3($name, 5, "$name - JSON decoded: ". Dumper $data); + + $success = $data->{'success'}; + + if ($success) { + # login war erfolgreich + my $sid = $data->{data}{sid}; + + # Session ID in hash eintragen + $hash->{HELPER}{SID} = $sid; + + readingsBeginUpdate ($hash); + readingsBulkUpdate ($hash,"Errorcode","none"); + readingsBulkUpdate ($hash,"Error","none"); + readingsEndUpdate ($hash, 1); + + Log3($name, 4, "$name - Login of User $username successful - SID: $sid"); + + return &$subref($name); + + } else { + # Errorcode aus JSON ermitteln + my $errorcode = $data->{error}{code}; + + # Fehlertext zum Errorcode ermitteln + my $error = SSCal_experror($hash,$errorcode); + + readingsBeginUpdate ($hash); + readingsBulkUpdate ($hash,"Errorcode", $errorcode); + readingsBulkUpdate ($hash,"Error", $error); + readingsBulkUpdate ($hash,"state", "error"); + readingsEndUpdate ($hash, 1); + + Log3($name, 3, "$name - Login of User $username unsuccessful. Code: $errorcode - $error - try again"); + + return SSCal_login($hash,$fret); + } + } + +return SSCal_login($hash,$fret); +} + +################################################################################### +# Funktion logout +################################################################################### +sub SSCal_logout ($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + my $serveraddr = $hash->{ADDR}; + my $serverport = $hash->{PORT}; + my $proto = $hash->{PROT}; + my $apiauth = $SSCal_api{APIAUTH}{NAME}; + my $apiauthpath = $SSCal_api{APIAUTH}{PATH}; + my $apiauthmaxver = $SSCal_api{APIAUTH}{MAX}; + my $sid = $hash->{HELPER}{SID}; + my ($url,$param); + + Log3($name, 4, "$name - --- Start Synology Calendar logout ---"); + + my $timeout = AttrVal($name,"timeout",60); + $timeout = 60 if($timeout < 60); + Log3($name, 4, "$name - HTTP-Call logout will be done with http timeout value: $timeout s"); + + $url = "$proto://$serveraddr:$serverport/webapi/$apiauthpath?api=$apiauth&version=$apiauthmaxver&method=logout&_sid=$sid"; + + $param = { + url => $url, + timeout => $timeout, + hash => $hash, + method => "GET", + header => "Accept: application/json", + callback => \&SSCal_logout_return + }; + + HttpUtils_NonblockingGet ($param); + +} + +sub SSCal_logout_return ($) { + my ($param, $err, $myjson) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $sid = $hash->{HELPER}{SID}; + my $OpMode = $hash->{OPMODE}; + my ($success, $username, $password) = SSCal_getcredentials($hash,0,"credentials"); + my ($data,$error,$errorcode); + + if ($err ne "") { + # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist + Log3($name, 2, "$name - ERROR message: $err"); + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "Error", $err); + readingsBulkUpdateIfChanged ($hash, "Errorcode", "none"); + readingsBulkUpdate ($hash, "state", "Error"); + readingsEndUpdate ($hash,1); + + } elsif ($myjson ne "") { + # Evaluiere ob Daten im JSON-Format empfangen wurden + ($hash,$success,$myjson) = SSCal_evaljson($hash,$myjson); + unless ($success) { + Log3($name, 4, "$name - Data returned: ".$myjson); + return; + } + + $data = decode_json($myjson); + + # Logausgabe decodierte JSON Daten + Log3($name, 5, "$name - JSON returned: ". Dumper $data); + + $success = $data->{'success'}; + + if ($success) { + # die Logout-URL konnte erfolgreich aufgerufen werden + Log3($name, 2, "$name - Session of User \"$username\" terminated - session ID \"$sid\" deleted"); + + } else { + # Errorcode aus JSON ermitteln + $errorcode = $data->{error}->{code}; + + # Fehlertext zum Errorcode ermitteln + $error = SSCal_experror($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + + Log3($name, 2, "$name - ERROR - Logout of User $username was not successful, however SID: \"$sid\" has been deleted. Errorcode: $errorcode - $error"); + } + } + + # Session-ID aus Helper-hash löschen + delete $hash->{HELPER}{SID}; + + CancelDelayedShutdown($name); + +return; +} + +############################################################################### +# Test ob JSON-String empfangen wurde +############################################################################### +sub SSCal_evaljson($$) { + my ($hash,$myjson) = @_; + my $OpMode = $hash->{OPMODE}; + my $name = $hash->{NAME}; + my $success = 1; + my ($error,$errorcode); + + eval {decode_json($myjson)} or do { + $success = 0; + + $errorcode = "900"; + + # Fehlertext zum Errorcode ermitteln + $error = SSCal_experror($hash,$errorcode); + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "Errorcode", $errorcode); + readingsBulkUpdateIfChanged ($hash, "Error", $error); + readingsBulkUpdate ($hash, "state", "Error"); + readingsEndUpdate ($hash, 1); + }; + +return($hash,$success,$myjson); +} + +############################################################################### +# JSON Boolean Test und Mapping +############################################################################### +sub SSCal_jboolmap($){ + my ($bool) = @_; + + if(JSON::is_bool($bool)) { + $bool = $bool?"true":"false"; + } + +return $bool; +} + + +############################################################################## +# Auflösung Errorcodes SVS API +# Übernahmewerte sind $hash, $errorcode +############################################################################## +sub SSCal_experror ($$) { + my ($hash,$errorcode) = @_; + my $device = $hash->{NAME}; + my $error; + + unless (exists($SSCal_errlist{"$errorcode"})) { + $error = "Value of errorcode \"$errorcode\" not found."; + return ($error); + } + + # Fehlertext aus Hash-Tabelle %errorlist ermitteln + $error = $SSCal_errlist{"$errorcode"}; + +return ($error); +} + +################################################################ +# sortiert eine Liste von Versionsnummern x.x.x +# Schwartzian Transform and the GRT transform +# Übergabe: "asc | desc", +################################################################ +sub SSCal_sortVersion (@){ + my ($sseq,@versions) = @_; + + my @sorted = map {$_->[0]} + sort {$a->[1] cmp $b->[1]} + map {[$_, pack "C*", split /\./]} @versions; + + @sorted = map {join ".", unpack "C*", $_} + sort + map {pack "C*", split /\./} @versions; + + if($sseq eq "desc") { + @sorted = reverse @sorted; + } + +return @sorted; +} + +###################################################################################### +# credentials speichern +###################################################################################### +sub SSCal_setcredentials ($@) { + my ($hash, @credentials) = @_; + my $name = $hash->{NAME}; + my ($success, $credstr, $username, $passwd, $index, $retcode); + my (@key,$len,$i); + + my $ao = "credentials"; + $credstr = encode_base64(join(':', @credentials)); + + # Beginn Scramble-Routine + @key = qw(1 3 4 5 6 3 2 1 9); + $len = scalar @key; + $i = 0; + $credstr = join "", + map { $i = ($i + 1) % $len; + chr((ord($_) + $key[$i]) % 256) } split //, $credstr; + # End Scramble-Routine + + $index = $hash->{TYPE}."_".$hash->{NAME}."_".$ao; + $retcode = setKeyValue($index, $credstr); + + if ($retcode) { + Log3($name, 2, "$name - Error while saving Credentials - $retcode"); + $success = 0; + } else { + ($success, $username, $passwd) = SSCal_getcredentials($hash,1,$ao); # Credentials nach Speicherung lesen und in RAM laden ($boot=1) + } + +return ($success); +} + +###################################################################################### +# credentials lesen +###################################################################################### +sub SSCal_getcredentials ($$$) { + my ($hash,$boot, $ao) = @_; + my $name = $hash->{NAME}; + my ($success, $username, $passwd, $index, $retcode, $credstr); + my (@key,$len,$i); + + if ($boot) { + # mit $boot=1 credentials von Platte lesen und als scrambled-String in RAM legen + $index = $hash->{TYPE}."_".$hash->{NAME}."_".$ao; + ($retcode, $credstr) = getKeyValue($index); + + if ($retcode) { + Log3($name, 2, "$name - Unable to read credentials from file: $retcode"); + $success = 0; + } + + if ($credstr) { + # beim Boot scrambled credentials in den RAM laden + $hash->{HELPER}{CREDENTIALS} = $credstr; + + # "CREDENTIALS" wird als Statusbit ausgewertet. Wenn nicht gesetzt -> Warnmeldung und keine weitere Verarbeitung + $hash->{CREDENTIALS} = "Set"; + $success = 1; + } + + } else { + # boot = 0 -> credentials aus RAM lesen, decoden und zurückgeben + $credstr = $hash->{HELPER}{CREDENTIALS}; + + if($credstr) { + # Beginn Descramble-Routine + @key = qw(1 3 4 5 6 3 2 1 9); + $len = scalar @key; + $i = 0; + $credstr = join "", + map { $i = ($i + 1) % $len; + chr((ord($_) - $key[$i] + 256) % 256) } + split //, $credstr; + # Ende Descramble-Routine + + ($username, $passwd) = split(":",decode_base64($credstr)); + + my $logcre = AttrVal($name, "showPassInLog", "0") == 1 ? $passwd : "********"; + + Log3($name, 4, "$name - credentials read from RAM: $username $logcre"); + + } else { + Log3($name, 2, "$name - credentials not set in RAM !"); + } + + $success = (defined($passwd)) ? 1 : 0; + } + +return ($success, $username, $passwd); +} + +############################################################################################# +# Leerzeichen am Anfang / Ende eines strings entfernen +############################################################################################# +sub SSCal_trim ($) { + my $str = shift; + $str =~ s/^\s+|\s+$//g; + +return ($str); +} + +############################################################################################# +# Länge Senedequeue updaten +############################################################################################# +sub SSCal_updQLength ($;$) { + my ($hash,$rst) = @_; + my $name = $hash->{NAME}; + + my $ql = keys %{$data{SSCal}{$name}{sendqueue}{entries}}; + + readingsBeginUpdate($hash); + readingsBulkUpdate ($hash, "QueueLenth", $ql); # Länge Sendqueue updaten + readingsEndUpdate ($hash,1); + + my $head = "next planned SendQueue start:"; + if($rst) { # resend Timer gesetzt + $hash->{RESEND} = $head." ".FmtDateTime($rst); + } else { + $hash->{RESEND} = $head." immediately by next entry"; + } + +return; +} + +############################################################################################# +# Text für den Versand an Synology cal formatieren +# und nicht erlaubte Zeichen entfernen +############################################################################################# +sub SSCal_formText ($) { + my $txt = shift; + my (%replacements,$pat); + + %replacements = ( + '"' => "´", # doppelte Hochkomma sind im Text nicht erlaubt + " H" => " h", # Bug im cal wenn vor großem H ein Zeichen + Leerzeichen vorangeht + "#" => "%23", # Hashtags sind im Text nicht erlaubt und wird encodiert + "&" => "%26", # & ist im Text nicht erlaubt und wird encodiert + "%" => "%25", # % ist nicht erlaubt und wird encodiert + "+" => "%2B", + ); + + $txt =~ s/\n/ESC_newline_ESC/g; + my @acr = split (/\s+/, $txt); + + $txt = ""; + foreach (@acr) { # Einzeiligkeit für Versand herstellen + $txt .= " " if($txt); + $_ =~ s/ESC_newline_ESC/\\n/g; + $txt .= $_; + } + + $pat = join '|', map quotemeta, keys(%replacements); + + $txt =~ s/($pat)/$replacements{$1}/g; + +return ($txt); +} + +############################################################################################# +# Start- und Endezeit ermitteln +############################################################################################# +sub SSCal_timeEdge ($) { + my ($name) = @_; + my $hash = $defs{$name}; + my ($error,$t1,$t2) = ("","",""); + my ($mday,$mon,$year); + + my $t = time(); + my $corr = 86400; # Korrekturbetrag + + my $cutOlderDays = AttrVal($name, "cutOlderDays", 5)."d"; + my $cutLaterDays = AttrVal($name, "cutLaterDays", 5)."d"; + + # start of time window + ($error,$t1) = SSCal_GetSecondsFromTimeSpec($cutOlderDays); + if($error) { + Log3 $hash, 2, "$name: attribute cutOlderDays: $error"; + return ($error,"",""); + } else { + $t1 = $t-$t1; + (undef,undef,undef,$mday,$mon,$year,undef,undef,undef) = localtime($t1); # Istzeit Ableitung + $t1 = fhemTimeLocal(00, 00, 00, $mday, $mon, $year); + } + + # end of time window + ($error,$t2) = SSCal_GetSecondsFromTimeSpec($cutLaterDays); + if($error) { + Log3 $hash, 2, "$name: attribute cutLaterDays: $error"; + return ($error,"",""); + } else { + $t2 = $t+$t2+$corr; + (undef,undef,undef,$mday,$mon,$year,undef,undef,undef) = localtime($t2); # Istzeit Ableitung + $t2 = fhemTimeLocal(00, 00, 00, $mday, $mon, $year); + } + +return ("",$t1,$t2); +} + +############################################################################################# +# Erinnerungstermin relativ zur Beginnzeit $bts ermitteln +# Alarmformat: 'time_value' => '-P2D' +# 'time_value' => '-PT1H' +# 'time_value' => '-PT5M' +# 'time_value' => 'PT0S' +# +# Rückgabe: $uts: Unix-Timestamp +# $ts: Timstamp als YYYY-MM-DD HH:MM:SS +# +############################################################################################# +sub SSCal_evtNotTime ($$$) { + my ($name,$tv,$bts) = @_; + my $hash = $defs{$name}; + my ($uts,$ts) = ("",""); + my ($corr); + + return ("","") if(!$tv || !$bts); + + if($tv =~ /^-P(\d)+D$/) { + $corr = $1*86400; + } elsif ($tv =~ /^-PT(\d+)H$/) { + $corr = $1*3600; + } elsif ($tv =~ /^-PT(\d+)M$/) { + $corr = $1*60; + } elsif ($tv =~ /^PT(\d+)S$/) { + $corr = $1; + } + + if(defined $corr) { + $uts = $bts-$corr; + $ts = FmtDateTime($uts); + } + +return ($uts,$ts); +} + +############################################################################################# +# Unix timestamp aus Zeitdifferenz berechnen +############################################################################################# +sub SSCal_GetSecondsFromTimeSpec($) { + my ($tspec) = @_; + + # days + if($tspec =~ m/^([0-9]+)d$/) { + return ("", $1*86400); + } + + # seconds + if($tspec =~ m/^([0-9]+)s?$/) { + return ("", $1); + } + + # 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])$/) { + 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); +} + +############################################################################################# +# Clienthash übernehmen oder zusammenstellen +# Identifikation ob über FHEMWEB ausgelöst oder nicht -> erstellen $hash->CL +############################################################################################# +sub SSCal_getclhash($;$$) { + my ($hash,$nobgd)= @_; + my $name = $hash->{NAME}; + my $ret; + + if($nobgd) { + # nur übergebenen CL-Hash speichern, + # keine Hintergrundverarbeitung bzw. synthetische Erstellung CL-Hash + $hash->{HELPER}{CL}{1} = $hash->{CL}; + return undef; + } + + if (!defined($hash->{CL})) { + # Clienthash wurde nicht übergeben und wird erstellt (FHEMWEB Instanzen mit canAsyncOutput=1 analysiert) + my $outdev; + my @webdvs = devspec2array("TYPE=FHEMWEB:FILTER=canAsyncOutput=1:FILTER=STATE=Connected"); + my $i = 1; + foreach (@webdvs) { + $outdev = $_; + next if(!$defs{$outdev}); + $hash->{HELPER}{CL}{$i}->{NAME} = $defs{$outdev}{NAME}; + $hash->{HELPER}{CL}{$i}->{NR} = $defs{$outdev}{NR}; + $hash->{HELPER}{CL}{$i}->{COMP} = 1; + $i++; + } + } else { + # übergebenen CL-Hash in Helper eintragen + $hash->{HELPER}{CL}{1} = $hash->{CL}; + } + + # Clienthash auflösen zur Fehlersuche (aufrufende FHEMWEB Instanz + if (defined($hash->{HELPER}{CL}{1})) { + for (my $k=1; (defined($hash->{HELPER}{CL}{$k})); $k++ ) { + Log3($name, 4, "$name - Clienthash number: $k"); + while (my ($key,$val) = each(%{$hash->{HELPER}{CL}{$k}})) { + $val = $val?$val:" "; + Log3($name, 4, "$name - Clienthash: $key -> $val"); + } + } + } else { + Log3($name, 2, "$name - Clienthash was neither delivered nor created !"); + $ret = "Clienthash was neither delivered nor created. Can't use asynchronous output for function."; + } + +return ($ret); +} + +################################################################ +# Kalendername aus Kalender-Id liefern +################################################################ +sub SSCal_getCalFromId ($$) { + my ($hash,$cid) = @_; + my $cal = ""; + $cid = SSCal_trim($cid); + + foreach my $calname (keys %{$hash->{HELPER}{CALENDARS}}) { + my $oid = $hash->{HELPER}{CALENDARS}{"$calname"}{id}; + next if(!$oid); + $oid = SSCal_trim($oid); + if($oid eq $cid) { + $cal = $calname; + last; + } + } + +return $cal; +} + +################################################################ +# addiert Anzahl ($n) Sekunden ($s) zu $t1 +################################################################ +sub SSCal_plusNSeconds ($$$) { + my ($t1, $s, $n) = @_; + + $n = 1 unless defined($n); + my $t2 = $t1+$n*$s; + +return $t2; +} + +################################################################ +# alle Readings außer excludierte löschen +# $respts -> Respect Timestamp +# wenn gesetzt, wird Reading nicht gelöscht +# wenn Updatezeit identisch zu "lastUpdate" +################################################################ +sub SSCal_delReadings ($$) { + my ($name,$respts) = @_; + my ($lu,$rts,$excl); + + $excl = "Error|Errorcode|QueueLenth|state|nextUpdate"; + $excl .= "|lastUpdate" if($respts); + + my @allrds = keys%{$defs{$name}{READINGS}}; + foreach my $key(@allrds) { + if($respts) { + $lu = $data{SSCal}{$name}{lastUpdate}; + $rts = ReadingsTimestamp($name, $key, $lu); + next if($rts eq $lu); + } + delete($defs{$name}{READINGS}{$key}) if($key !~ m/^($excl)$/); + } + +return; +} + +############################################################################################# +# Datum/Zeit extrahieren +# Eingangsformat: TZID=Europe/Berlin:20191216T133000 oder +# 20191216T133000 +# Rückgabe: invalid, Zeitzone, Date(YYYY-MM-DD), Time (HH:MM:SS), UnixTimestamp +# (invalid =1 wenn Datum ungültig, ist nach RFC 5545 diese Wiederholung +# zu ignorieren und auch nicht zu zählen !) +############################################################################################# +sub SSCal_explodeDateTime ($$) { + my ($hash,$dt) = @_; + my $name = $hash->{NAME}; + my ($tz,$t) = ("",""); + my $invalid = 0; + my ($d,$tstamp,$sec,$min,$hour,$mday,$month,$year); + + if($dt =~ /^TZID=.*$/) { + ($tz,$dt) = split(":", $dt); + $tz = (split("=", $tz))[1]; + } + + ($d,$t) = split("T", $dt); + + $year = substr($d,0,4); + $month = substr($d,4,2); + $mday = substr($d,6,2); + $d = $year."-".$month."-".$mday; + + if($t) { + $hour = substr($t,0,2); + $min = substr($t,2,2); + $sec = substr($t,4,2); + $t = $hour.":".$min.":".$sec; + } else { + $hour = "00"; + $min = "00"; + $sec = "00"; + $t = "00:00:00"; + } + + unless ( ($d." ".$t) =~ /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/) { + Log3($name, 2, "$name - ERROR - invalid DateTime format for explodeDateTime: $d $t"); + } + + eval { timelocal($sec, $min, $hour, $mday, $month-1, $year-1900); }; + if ($@) { + Log3($name, 3, "$name - WARNING - invalid format of recurring event: $@. It will be ignored due to RFC 5545 standard."); + $invalid = 1; + } + + eval { $tstamp = fhemTimeLocal($sec, $min, $hour, $mday, $month-1, $year-1900); }; + +return ($invalid,$tz,$d,$t,$tstamp); +} + +############################################################################################# +# Versionierungen des Moduls setzen +# Die Verwendung von Meta.pm und Packages wird berücksichtigt +############################################################################################# +sub SSCal_setVersionInfo($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $v = (SSCal_sortVersion("desc",keys %SSCal_vNotesIntern))[0]; + my $type = $hash->{TYPE}; + $hash->{HELPER}{PACKAGE} = __PACKAGE__; + $hash->{HELPER}{VERSION} = $v; + + if($modules{$type}{META}{x_prereqs_src} && !$hash->{HELPER}{MODMETAABSENT}) { + # META-Daten sind vorhanden + $modules{$type}{META}{version} = "v".$v; # Version aus META.json überschreiben, Anzeige mit {Dumper $modules{SSCal}{META}} + if($modules{$type}{META}{x_version}) { # {x_version} ( nur gesetzt wenn $Id: 50_SSCal.pm 20534 2019-11-18 17:50:17Z DS_Starter $ im Kopf komplett! vorhanden ) + $modules{$type}{META}{x_version} =~ s/1.1.1/$v/g if($modules{$type}{META}{x_version} =~ /^1.1.1$/); + } else { + $modules{$type}{META}{x_version} = $v; + } + return $@ unless (FHEM::Meta::SetInternals($hash)); # FVERSION wird gesetzt ( nur gesetzt wenn $Id: 50_SSCal.pm 20534 2019-11-18 17:50:17Z DS_Starter $ im Kopf komplett! vorhanden ) + if(__PACKAGE__ eq "FHEM::$type" || __PACKAGE__ eq $type) { + # es wird mit Packages gearbeitet -> Perl übliche Modulversion setzen + # mit {->VERSION()} im FHEMWEB kann Modulversion abgefragt werden + use version 0.77; our $VERSION = FHEM::Meta::Get( $hash, 'version' ); + } + } else { + # herkömmliche Modulstruktur + $hash->{VERSION} = $v; + } + +return; +} + +############################################################################################# +# Hint Hash EN +############################################################################################# +%SSCal_vHintsExt_en = ( +); + +############################################################################################# +# Hint Hash DE +############################################################################################# +%SSCal_vHintsExt_de = ( + +); + +1; + +=pod +=item summary module to integrate Synology Calendar +=item summary_DE Modul zur Integration von Synology Calendar +=begin html + + +

SSCal

+
    + +The guide for this module is currently only available in the german Wiki. + +
+ + +=end html +=begin html_DE + + +

SSCal

+
    + +Die Beschreibung des Moduls ist momentan nur im Wiki vorhanden. + +
+ +=end html_DE + +=for :application/json;q=META.json 57_SSCal.pm +{ + "abstract": "Integration of Synology Calendar.", + "x_lang": { + "de": { + "abstract": "Integration des Synology Calendars." + } + }, + "keywords": [ + "Synology", + "Calendar", + "Appointments" + ], + "version": "v1.1.1", + "release_status": "testing", + "author": [ + "Heiko Maaz " + ], + "x_fhem_maintainer": [ + "DS_Starter" + ], + "x_fhem_maintainer_github": [ + "nasseeder1" + ], + "prereqs": { + "runtime": { + "requires": { + "FHEM": 5.00918799, + "perl": 5.014, + "JSON": 0, + "Data::Dumper": 0, + "MIME::Base64": 0, + "Time::HiRes": 0, + "HttpUtils": 0, + "Blocking": 0, + "Encode": 0 + }, + "recommends": { + "FHEM::Meta": 0 + }, + "suggests": { + } + } + }, + "resources": { + "x_wiki": { + "web": "https://wiki.fhem.de/wiki/SSCal - Integration des Synology Calendar Servers", + "title": "SSCal - Integration des Synology Calendar Servers" + }, + "repository": { + "x_dev": { + "type": "svn", + "url": "https://svn.fhem.de/trac/browser/trunk/fhem/contrib/DS_Starter", + "web": "https://svn.fhem.de/trac/browser/trunk/fhem/contrib/DS_Starter/57_SSCal.pm", + "x_branch": "dev", + "x_filepath": "fhem/contrib/", + "x_raw": "https://svn.fhem.de/fhem/trunk/fhem/contrib/DS_Starter/57_SSCal.pm" + } + } + } +} +=end :application/json;q=META.json + +=cut