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("$key | $val0 | " );
+ $ret .= "
";
+ $i++;
+ if ($i & 1) {
+ # $i ist ungerade
+ $ret .= "";
+ } else {
+ $ret .= "
";
+ }
+ }
+ $ret .= "
";
+ $ret .= "";
+ $ret .= "
";
+ $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("$key | $val0 | $val1 | " );
+ $ret .= "
";
+ $i++;
+ if ($i & 1) {
+ # $i ist ungerade
+ $ret .= "";
+ } else {
+ $ret .= "
";
+ }
+ }
+ $ret .= "
";
+ $ret .= "";
+ $ret .= "
";
+ $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 .= " Calendar | ID | Type | Description | Privilege | Public share ID | |
";
+ $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 .= " $dnm | $oid | $typ | $des | $prv | $psi | |
";
+
+ $i++;
+ }
+
+ $out .= "
";
+ $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