######################################################################################################################## # $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.9.0" => "11.02.2020 new reading Weekday with localization, more field selection for overview table ", "1.8.0" => "09.02.2020 evaluate icons for DaysLeft, Map and State in sub SSCal_evalTableSpecs , fix no table is shown after FHEM restart ", "1.7.0" => "09.02.2020 respect global language setting for some presentation, new attributes tableSpecs & tableColumnMap, days left in overview ". "formatting overview table, feature smallScreen for tableSpecs, rename attributes to tableFields, ". "tableInDetail, tableInRoom, correct enddate/time if is_all_day incl. bugfix API, function SSCal_boolean ". "to avoid fhem crash if an older JSON module is installed ", "1.6.1" => "03.02.2020 rename attributes to \"calOverviewInDetail\",\"calOverviewInRoom\", bugfix of gps extraction ", "1.6.0" => "03.02.2020 new attribute \"tableFields\" to show specified fields in calendar overview in detail/room view, ". "Model Diary/Tasks defined, periodic call of ToDo-Liists now possible ", "1.5.0" => "02.02.2020 new attribute \"calOverviewInDetail\",\"calOverviewInRoom\" to control calendar overview in room or detail view ", "1.4.0" => "02.02.2020 get calAsHtml command or use sub SSCal_calAsHtml(\$name) ", "1.3.1" => "01.02.2020 add SSCal_errauthlist hash for login/logout API error codes ", "1.3.0" => "01.02.2020 new command \"cleanCompleteTasks\" to delete completed tasks, \"deleteEventId\" to delete an event id, ". "new get command \"apiInfo\" - detect and show API info, avoid empty readings ", "1.2.0" => "29.01.2020 get tasks from calendar with set command 'calToDoList' ", "1.1.14" => "29.01.2020 ignore calendars of type ne 'Event' for set calEventList ", "1.1.13" => "20.01.2020 change save and read credentials routine ", "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 " ); # Aufbau Errorcode-Hashes my %SSCal_errauthlist = ( 400 => "No such account or the password is incorrect", 401 => "Account disabled", 402 => "Permission denied", 403 => "2-step verification code required", 404 => "Failed to authenticate 2-step verification code", ); 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"; # Darstellung FHEMWEB # $hash->{FW_summaryFn} = "SSCal_FWsummaryFn"; $hash->{FW_addDetailToSummary} = 1 ; # zusaetzlich zu der Device-Summary auch eine Neue mit dem Inhalt von DetailFn angezeigt $hash->{FW_detailFn} = "SSCal_FWdetailFn"; $hash->{FW_deviceOverview} = 1; $hash->{AttrList} = "asyncMode:1,0 ". "cutOlderDays ". "cutLaterDays ". "disable:1,0 ". "tableSpecs:textField-long ". "filterCompleteTask:1,2,3 ". "filterDueTask:1,2,3 ". "interval ". "loginRetries:1,2,3,4,5,6,7,8,9,10 ". "tableColumnMap:icon,data,text ". "showRepeatEvent:true,false ". "showPassInLog:1,0 ". "tableInDetail:0,1 ". "tableInRoom:0,1 ". "tableFields:multiple-strict,Begin,End,DaysLeft,DaysLeftLong,Weekday,Timezone,Summary,Description,Status,Completion,Location,Map,Calendar,EventId ". "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)] # [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)] [Tasks]"; } shift @a; shift @a; my $addr = $a[0] if($a[0] ne "Tasks"); my $port = ($a[1] && $a[1] ne "Tasks") ? $a[1] : 5000; my $prot = ($a[2] && $a[2] ne "Tasks") ? lc($a[2]) : "http"; my $model = "Diary"; $model = "Tasks" if( grep {$_ eq "Tasks"} @a ); $hash->{ADDR} = $addr; $hash->{PORT} = $port; $hash->{MODEL} = "Calendar"; $hash->{PROT} = $prot; $hash->{MODEL} = $model; $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 $model = $hash->{MODEL}; my ($do,$val,$cache); # $cmd can be "del" or "set" # $name is device name # aName and aVal are Attribute name and value if ($cmd eq "set") { my $attrVal = $aVal; if ($aName =~ /filterCompleteTask|filterDueTask/ && $model ne "Tasks") { return " The attribute \"$aName\" is only valid for devices of MODEL \"Tasks\"! Please set this attribute in a device of this model."; } if ($aName =~ /showRepeatEvent/ && $model ne "Diary") { return " The attribute \"$aName\" is only valid for devices of MODEL \"Diary\"! Please set this attribute in a device of this model."; } if ($attrVal =~ m/^\{.*\}$/s && $attrVal =~ m/=>/) { $attrVal =~ s/\@/\\\@/g; $attrVal =~ s/\$/\\\$/g; my $av = eval $attrVal; if($@) { Log3($name, 2, "$name - Error while evaluate: ".$@); return $@; } else { $attrVal = $av if(ref($av) eq "HASH"); } } $hash->{HELPER}{$aName} = $attrVal; } else { delete $hash->{HELPER}{$aName}; } 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 $model = $hash->{MODEL}; my ($success,$setlist); return if(IsDisabled($name)); my $idxlist = join(",", SSCal_sortVersion("asc",keys %{$data{SSCal}{$name}{sendqueue}{entries}})); # alle aktuell angezeigten Event Id's ermitteln my (@idarray,$evids); foreach my $key (keys %{$defs{$name}{READINGS}}) { next if $key !~ /^.*_EventId$/; push (@idarray, $defs{$name}{READINGS}{$key}{VAL}); } if(@idarray) { my %seen; my @unique = sort{$a<=>$b} grep { !$seen{$_}++ } @idarray; # distinct / unique the keys $evids = join(",", @unique); } if(!$hash->{CREDENTIALS}) { # initiale setlist für neue Devices $setlist = "Unknown argument $opt, choose one of ". "credentials " ; } elsif ($model eq "Diary") { $setlist = "Unknown argument $opt, choose one of ". "calEventList ". "credentials ". ($evids?"deleteEventId:$evids ":"deleteEventId:noArg "). "eraseReadings:noArg ". "listSendqueue:noArg ". "logout:noArg ". ($idxlist?"purgeSendqueue:-all-,-permError-,$idxlist ":"purgeSendqueue:-all-,-permError- "). "restartSendqueue:noArg " ; } else { # Model ist "Tasks" $setlist = "Unknown argument $opt, choose one of ". "calToDoList ". "cleanCompleteTasks:noArg ". "credentials ". ($evids?"deleteEventId:$evids ":"deleteEventId:noArg "). "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); if ($hash->{HELPER}{CALENDARS}{"$_"}{type} ne "Event") { Log3($name, 3, "$name - The Calendar \"$_\" is not of type \"Event\" and will be ignored."); next; } $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 "No Calendar of type \"Event\" was selected or its 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 "calToDoList") { # Aufgaben einer Cal_id (Liste) abrufen return "Obtain the Calendar list first with \"get $name getCalendars\" command." if(!$hash->{HELPER}{CALFETCHED}); 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); if ($hash->{HELPER}{CALENDARS}{"$_"}{type} ne "ToDo") { Log3($name, 3, "$name - The Calendar \"$_\" is not of type \"ToDo\" and will be ignored."); next; } $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 "No Calendar of type \"ToDo\" was selected or its ID(s) couldn't be found." if(!$oids); Log3($name, 5, "$name - Calendar selection for add queue: $cals"); my $limit = ""; # Limit of matched tasks my $offset = 0; # offset of mnatched tasks my $filterdue = AttrVal($name,"filterDueTask", 3); # show tasks with and without due time my $filtercomplete = AttrVal($name,"filterCompleteTask", 3); # show completed and not completed tasks SSCal_addQueue($name,"todolist","CALTODO","list","&cal_id_list=[$oids]&limit=$limit&offset=$offset&filter_due=$filterdue&filter_complete=$filtercomplete"); SSCal_getapisites($name); } elsif ($opt eq "cleanCompleteTasks") { # erledigte Aufgaben einer Cal_id (Liste) löschen return "Obtain the Calendar list first with \"get $name getCalendars\" command." if(!$hash->{HELPER}{CALFETCHED}); 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); if ($hash->{HELPER}{CALENDARS}{"$_"}{type} ne "ToDo") { Log3($name, 3, "$name - The Calendar \"$_\" is not of type \"ToDo\" and will be ignored."); next; } $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 "No Calendar of type \"ToDo\" was selected or its ID(s) couldn't be found." if(!$oids); Log3($name, 5, "$name - Calendar selection for add queue: $cals"); # SSCal_addQueue($name,"cleanCompleteTasks","CALTODO","clean_complete","&cal_id_list=[$oids]"); SSCal_getapisites($name); } elsif ($opt eq "deleteEventId") { return "You must specify an event id (Reading EventId) what is to be deleted." if(!$prop); my $eventid = $prop; # Blocknummer ermitteln my $bnr; my @allrds = keys%{$defs{$name}{READINGS}}; foreach my $key(@allrds) { next if $key !~ /^.*_EventId$/; $bnr = (split("_", $key))[0] if($defs{$name}{READINGS}{$key}{VAL} == $eventid); # Blocknummer ermittelt } return "The blocknumber of specified event id could not be identified. Make sure you have specified a valid event id." if(!$bnr); # die Summary zur Event Id ermitteln my $sum = ReadingsVal($name, $bnr."_01_Summary", ""); # Kalendername und dessen id und Typ ermitteln my $calname = ReadingsVal($name, $bnr."_90_calName", ""); my $calid = $hash->{HELPER}{CALENDARS}{"$calname"}{id}; my $caltype = $hash->{HELPER}{CALENDARS}{"$calname"}{type}; # Kalender-API in Abhängigkeit des Kalendertyps wählen my $api = ($caltype eq "Event")?"CALEVENT":"CALTODO"; Log3($name, 3, "$name - The event \"$sum\" with id \"$eventid\" will be deleted in calendar \"$calname\"."); # SSCal_addQueue($name,"deleteEventId",$api,"delete","&evt_id=$eventid"); 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 ". "apiInfo:noArg ". "calAsHtml:noArg ". "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 "apiInfo") { # Liste aller Kalender abrufen # übergebenen CL-Hash (FHEMWEB) in Helper eintragen SSCal_getclhash($hash,1); $hash->{HELPER}{APIPARSET} = 0; # Abruf API Infos erzwingen # SSCal_addQueue($name,"apiInfo","","",""); SSCal_getapisites($name); } elsif ($opt eq "getCalendars") { # Liste aller Kalender abrufen # übergebenen CL-Hash (FHEMWEB) in Helper eintragen SSCal_getclhash($hash,1); SSCal_addQueue($name,"listcal","CALCAL","list","&is_todo=true&is_evt=true"); SSCal_getapisites($name); } elsif ($opt eq "calAsHtml") { my $out = SSCal_calAsHtml($name); return $out; } elsif ($opt =~ /versionNotes/) { my $header = "Module release information
"; my $header1 = "Helpful hints
"; my %hs; # Ausgabetabelle erstellen my ($ret,$val0,$val1); my $i = 0; $ret = ""; # Hints if(!$arg || $arg =~ /hints/ || $arg =~ /[\d]+/) { $ret .= sprintf("
$header1
"); $ret .= ""; $ret .= ""; $ret .= ""; if($arg && $arg =~ /[\d]+/) { my @hints = split(",",$arg); foreach (@hints) { if(AttrVal("global","language","EN") eq "DE") { $hs{$_} = $SSCal_vHintsExt_de{$_}; } else { $hs{$_} = $SSCal_vHintsExt_en{$_}; } } } else { if(AttrVal("global","language","EN") eq "DE") { %hs = %SSCal_vHintsExt_de; } else { %hs = %SSCal_vHintsExt_en; } } $i = 0; foreach my $key (SSCal_sortVersion("desc",keys %hs)) { $val0 = $hs{$key}; $ret .= sprintf("" ); $ret .= ""; $i++; if ($i & 1) { # $i ist ungerade $ret .= ""; } else { $ret .= ""; } } $ret .= ""; $ret .= ""; $ret .= "
$key $val0
"; $ret .= "
"; } # Notes if(!$arg || $arg =~ /rel/) { $ret .= sprintf("
$header
"); $ret .= ""; $ret .= ""; $ret .= ""; $i = 0; foreach my $key (SSCal_sortVersion("desc",keys %SSCal_vNotesExtern)) { ($val0,$val1) = split(/\s/,$SSCal_vNotesExtern{$key},2); $ret .= sprintf("" ); $ret .= ""; $i++; if ($i & 1) { # $i ist ungerade $ret .= ""; } else { $ret .= ""; } } $ret .= ""; $ret .= ""; $ret .= "
$key $val0 $val1
"; $ret .= "
"; } $ret .= ""; return $ret; } else { return "$getlist"; } return $ret; # not generate trigger out of command } ###################################################################################### # Kalenderübersicht in Detailanzeige darstellen ###################################################################################### sub SSCal_FWdetailFn ($$$$) { my ($FW_wname, $d, $room, $pageHash) = @_; my $hash = $defs{$d}; my $ret = ""; $hash->{".calhtml"} = SSCal_calAsHtml($d,$FW_wname); if($hash->{".calhtml"} ne "" && !$room && AttrVal($d,"tableInDetail",1)) { # Anzeige Übersicht in Detailansicht $ret .= $hash->{".calhtml"}; return $ret; } if($hash->{".calhtml"} ne "" && $room && AttrVal($d,"tableInRoom",1)) { # Anzeige in Raumansicht zusätzlich zur Statuszeile $ret = $hash->{".calhtml"}; return $ret; } return undef; } ###################################################################################### # 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 $model = $hash->{MODEL}; 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)) { if($model eq "Diary") { CommandSet(undef, "$name calEventList") }; # Einträge aller gewählter Terminkalender abrufen (in Queue stellen) if($model eq "Tasks") { CommandSet(undef, "$name calToDoList") }; # Einträge aller gewählter Aufgabenlisten abrufen (in Queue stellen) } InternalTimer($new, "SSCal_periodicCall", $name, 0); return; } ###################################################################################### # Eintrag zur SendQueue hinzufügen # $name = Name Kalenderdevice # $opmode = operation mode # $api = API (siehe %SSCal_api) # $method = auszuführende API-Methode # $params = spezifische API-Parameter # ###################################################################################### 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|103|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},$SSCal_api{APIINFO}{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 $opmode = $hash->{OPMODE}; 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"); # Pfad und Maxversion von "SYNO.API.Info" ermitteln my $apiinfopath = $data->{data}->{$SSCal_api{APIINFO}{NAME}}->{path}; $apiinfopath =~ tr/_//d if (defined($apiinfopath)); my $apiinfomaxver = $data->{data}->{$SSCal_api{APIINFO}{NAME}}->{maxVersion}; $logstr = defined($apiinfopath) ? "Path of $SSCal_api{APIINFO}{NAME} selected: $apiinfopath" : "Path of $SSCal_api{APIINFO}{NAME} undefined - Synology cal Server may be stopped"; Log3($name, 4, "$name - $logstr"); $logstr = defined($apiinfomaxver) ? "MaxVersion of $SSCal_api{APIINFO}{NAME} selected: $apiinfomaxver" : "MaxVersion of $SSCal_api{APIINFO}{NAME} undefined - Synology cal Server may be stopped"; Log3($name, 4, "$name - $logstr"); # ermittelte Werte in $hash einfügen $SSCal_api{APIINFO}{PATH} = $apiinfopath; $SSCal_api{APIINFO}{MAX} = $apiinfomaxver; $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; # API values sind gesetzt in Hash $hash->{HELPER}{APIPARSET} = 1; if ($opmode eq "apiInfo") { # API Infos in Popup anzeigen my $out = ""; $out .= "Synology Calendar API Info

"; $out .= ""; $out .= ""; $out .= ""; foreach my $key (keys %SSCal_api) { my $apiname = $SSCal_api{$key}{NAME}; my $apipath = $SSCal_api{$key}{PATH}; my $apiver = $SSCal_api{$key}{MAX}; $out .= ""; } $out .= "
API Path Version
$apiname $apipath $apiver
"; $out .= ""; readingsBeginUpdate ($hash); readingsBulkUpdateIfChanged ($hash,"Errorcode","none"); readingsBulkUpdateIfChanged ($hash,"Error", "none"); readingsBulkUpdate ($hash, "state", "done"); readingsEndUpdate ($hash,1); # 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); return; } } 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"; if($opmode eq "deleteEventId" && $api eq "CALEVENT") { # Workaround !!! Methode delete funktioniert nicht mit SYNO.Cal.Event version > 1 $url = "$prot://$addr:$port/webapi/".$SSCal_api{$api}{PATH}."?api=".$SSCal_api{$api}{NAME}."&version=1&method=$method".$params."&_sid=$sid"; } my $part = $url; if(AttrVal($name, "showPassInLog", "0") == 1) { Log3($name, 4, "$name - Call-Out: $url"); } else { $part =~ s/$sid//; Log3($name, 4, "$name - Call-Out: $part"); } $param = { url => $url, timeout => $timeout, hash => $hash, method => "GET", header => "Accept: application/json", callback => \&SSCal_calop_parse }; HttpUtils_NonblockingGet ($param); } ############################################################################################# # Callback from SSCal_calop ############################################################################################# sub SSCal_calop_parse ($) { my ($param, $err, $myjson) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; my $prot = $hash->{PROT}; my $addr = $hash->{ADDR}; my $port = $hash->{PORT}; my $opmode = $hash->{OPMODE}; my $am = AttrVal($name, "asyncMode", 0); my ($ts,$data,$success,$error,$errorcode,$cherror,$r); if ($err ne "") { # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist Log3($name, 2, "$name - ERROR message: $err"); $errorcode = "none"; $errorcode = "800" if($err =~ /: malformed or unsupported URL$/s); readingsBeginUpdate ($hash); readingsBulkUpdateIfChanged ($hash, "Error", $err); readingsBulkUpdateIfChanged ($hash, "Errorcode", $errorcode); readingsBulkUpdate ($hash, "state", "Error"); readingsEndUpdate ($hash,1); SSCal_checkretry($name,1); return; } elsif ($myjson ne "") { # wenn die Abfrage erfolgreich war ($data enthält die Ergebnisdaten des HTTP Aufrufes) # Evaluiere ob Daten im JSON-Format empfangen wurden ($hash,$success,$myjson) = SSCal_evaljson($hash,$myjson); unless ($success) { Log3($name, 4, "$name - Data returned: ".$myjson); SSCal_checkretry($name,1); return; } $data = decode_json($myjson); # Logausgabe decodierte JSON Daten Log3($name, 5, "$name - JSON returned: ". Dumper $data); $success = $data->{'success'}; if ($success) { if ($opmode eq "listcal") { # alle Kalender abrufen my %calendars = (); my ($cals,$dnm,$typ,$oid,$des,$prv,$psi); my $i = 0; my $out = ""; $out .= "Synology Calendar List

"; $out .= ""; $out .= ""; $out .= ""; while ($data->{data}[$i]) { $dnm = $data->{data}[$i]{cal_displayname}; next if (!$dnm); $typ = "Event" if($data->{data}[$i]{is_evt}); $typ = "ToDo" if($data->{data}[$i]{is_todo}); $oid = $data->{data}[$i]{original_cal_id}; $des = encode("UTF-8", $data->{data}[$i]{cal_description}); $prv = $data->{data}[$i]{cal_privilege}; $psi = $data->{data}[$i]{cal_public_sharing_id}; $psi = $psi?$psi:""; $calendars{$dnm}{id} = $oid; $calendars{$dnm}{description} = $des; $calendars{$dnm}{privilege} = $prv; $calendars{$dnm}{publicshareid} = $psi; $calendars{$dnm}{type} = $typ; $cals .= "," if($cals); $cals .= $dnm; $out .= ""; $i++; } $out .= "
Calendar ID Type Description Privilege Public share ID
$dnm $oid $typ $des $prv $psi
"; $out .= ""; $hash->{HELPER}{CALENDARS} = \%calendars if(%calendars); $hash->{HELPER}{CALFETCHED} = 1; my @newa; my $list = $modules{$hash->{TYPE}}{AttrList}; my @deva = split(" ", $list); foreach (@deva) { push @newa, $_ if($_ !~ /usedCalendars:/); } $cals =~ s/ /#/g if($cals); push @newa, ($cals?"usedCalendars:multiple-strict,$cals ":"usedCalendars:--no#Calendar#selectable--"); $hash->{".AttrList"} = join(" ", @newa); # Device spezifische AttrList, überschreibt Modul AttrList ! # Ausgabe Popup der User-Daten (nach readingsEndUpdate positionieren sonst # "Connection lost, trying reconnect every 5 seconds" wenn > 102400 Zeichen) asyncOutput($hash->{HELPER}{CL}{1},"$out"); delete($hash->{HELPER}{CL}); SSCal_checkretry($name,0); readingsBeginUpdate ($hash); readingsBulkUpdateIfChanged ($hash, "Errorcode", "none"); readingsBulkUpdateIfChanged ($hash, "Error", "none"); readingsBulkUpdate ($hash, "state", "done"); readingsEndUpdate ($hash,1); } elsif ($opmode eq "eventlist") { # Events der ausgewählten Kalender aufbereiten delete $data{SSCal}{$name}{eventlist}; # zentrales Event/ToDo 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); } } elsif ($opmode eq "todolist") { # ToDo's der ausgewählten Tasks-Kalender aufbereiten delete $data{SSCal}{$name}{eventlist}; # zentrales Event/ToDo Hash löschen $hash->{eventlist} = $data; # Data-Hashreferenz im Hash speichern if ($am) { # Extrahieren der ToDos asynchron (nicht-blockierend) Log3($name, 4, "$name - Task parse mode: asynchronous"); my $timeout = AttrVal($name, "timeout", 20)+180; $hash->{HELPER}{RUNNING_PID} = BlockingCall("SSCal_extractToDolist", $name, "SSCal_createReadings", $timeout, "SSCal_blockingTimeout", $hash); $hash->{HELPER}{RUNNING_PID}{loglevel} = 5 if($hash->{HELPER}{RUNNING_PID}); # Forum #77057 } else { # Extrahieren der ToDos synchron (blockierend) Log3($name, 4, "$name - Task parse mode: synchronous"); SSCal_extractToDolist ($name); } } elsif ($opmode eq "cleanCompleteTasks") { # abgeschlossene ToDos wurden gelöscht readingsBeginUpdate ($hash); readingsBulkUpdateIfChanged ($hash, "Errorcode", "none"); readingsBulkUpdateIfChanged ($hash, "Error", "none"); readingsBulkUpdate ($hash, "state", "done"); readingsEndUpdate ($hash,1); Log3($name, 3, "$name - All completed tasks were deleted from selected ToDo lists"); SSCal_checkretry($name,0); } elsif ($opmode eq "deleteEventId") { # ein Kalendereintrag mit Event Id wurde gelöscht readingsBeginUpdate ($hash); readingsBulkUpdateIfChanged ($hash, "Errorcode", "none"); readingsBulkUpdateIfChanged ($hash, "Error", "none"); readingsBulkUpdate ($hash, "state", "done"); readingsEndUpdate ($hash,1); Log3($name, 3, "$name - The specified event id was deleted"); # Queuedefinition sichern vor checkretry my $idx = $hash->{OPIDX}; my $api = $data{SSCal}{$name}{sendqueue}{entries}{$idx}{api}; my $set = ($api eq "CALEVENT")?"calEventList":"calToDoList"; SSCal_checkretry($name,0); # Kalendereinträge neu einlesen nach dem löschen Event Id CommandSet(undef, "$name $set"); } } 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 Kalendertermine (Events) ############################################################################################# sub SSCal_extractEventlist ($) { my ($name) = @_; my $hash = $defs{$name}; my $data = delete $hash->{eventlist}; my $am = AttrVal($name, "asyncMode", 0); my ($tz,$bdate,$btime,$bts,$edate,$etime,$ets,$ci,$bi,$ei,$startEndDiff); my ($bmday,$bmonth,$emday,$emonth,$byear,$eyear,$nbdate,$nbtime,$nbts,$nedate,$netime,$nets); 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) = ("",""); my $isallday = $data->{data}{$key}[$i]{is_all_day}; ($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}, $isallday); # Ende des Events $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; # Bugfix API - wenn is_all_day und an erster Stelle im 'data' Ergebnis des API-Calls ist Endedate/time nicht korrekt ! if($isallday && ($bdate ne $edate) && $netime =~ /^00:59:59$/) { $eyear = $byear; $emonth = $bmonth; $emday = $bmday; $nbtime =~ s/://g; $netime = "235959"; ($bi,undef,$bdate,$btime,$bts) = SSCal_explodeDateTime ($hash, $byear.$bmonth.$bmday."T".$nbtime); ($ei,undef,$edate,$etime,$ets) = SSCal_explodeDateTime ($hash, $eyear.$emonth.$emday."T".$netime); } $startEndDiff = $ets - $bts; # Differenz Event Ende / Start in Sekunden 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"); } } ############################################################################################# # Extrahiert empfangene Tasks aus ToDo-Kalender (Aufgabenliste) ############################################################################################# sub SSCal_extractToDolist ($) { 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]{due}); # Fälligkeit des Tasks (falls gesetzt) ($ei,undef,$edate,$etime,$ets) = SSCal_explodeDateTime ($hash,$data->{data}{$key}[$i]{due}); # Ende = Fälligkeit des Tasks (falls gesetzt) if ($bdate && $edate) { # nicht jede Aufgabe hat Date / Time gesetzt $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 Task (momentan gibt es keine Wiederholungstasks) Log3($name, 5, "$name - Single task Begin: $bdate, End: $edate") if($bdate && $edate); if(($ets && $ets < $tstart) || ($bts && $bts > $tend)) { Log3($name, 4, "$name - Ignore single task -> $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; } } 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]) if($a[1]); if ($rowlist) { 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 $lang = AttrVal("global", "language", "EN"); my $ts = time(); # Istzeit Timestamp my $om = $hash->{OPMODE}; # aktuelle Operation Mode my $status = "initialized"; my ($val,$uts,$td,$dleft,$bWday); 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); if($bdate && $btime) { push(@row_array, $bts+$n." 05_Begin " .$bdate." ".$btime."\n"); my ($ny,$nm,$nd,undef) = split(/[ -]/, TimeNow()); # Datum Jetzt my ($by,$bm,$bd) = split("-", $bdate); # Beginn Datum my $ntimes = fhemTimeLocal(00, 00, 00, $nd, $nm-1, $ny-1900); my $btimes = fhemTimeLocal(00, 00, 00, $bd, $bm-1, $by-1900); if($btimes >= $ntimes) { $dleft = int(($btimes - $ntimes)/86400); } my @days; (undef, undef, undef, undef, undef, undef, $bWday, undef, undef) = localtime($btimes); if($lang eq "DE") { @days = qw(Sontag Montag Dienstag Mittwoch Donnerstag Freitag Samstag); } else { @days = qw(Sunday Monday Tuesday Wednesday Thursday Friday Saturday); } $bWday = $days[$bWday]; } push(@row_array, $bts+$n." 07_bTimestamp " .$bts."\n") if($bts); push(@row_array, $bts+$n." 10_End " .$edate." ".$etime."\n") if($edate && $etime); push(@row_array, $bts+$n." 13_eTimestamp " .$ets."\n") if($ets); push(@row_array, $bts+$n." 15_Timezone " .$tz."\n") if($tz); push(@row_array, $bts+$n." 20_daysLeft " .$dleft."\n") if(defined $dleft); push(@row_array, $bts+$n." 25_daysLeftLong " ."in ".$dleft." Tagen\n") if(defined $dleft); push(@row_array, $bts+$n." 30_Weekday " .$bWday."\n") if(defined $bWday); foreach my $p (keys %{$vh}) { $vh->{$p} = "" if(!defined $vh->{$p}); $vh->{$p} = SSCal_jboolmap($vh->{$p}); next if($vh->{$p} eq ""); $val = encode("UTF-8", $vh->{$p}); push(@row_array, $bts+$n." 01_Summary " .$val."\n") if($p eq "summary"); push(@row_array, $bts+$n." 03_Description " .$val."\n") if($p eq "description"); push(@row_array, $bts+$n." 35_Location " .$val."\n") if($p eq "location"); if($p eq "gps") { my ($address,$lng,$lat) = ("","",""); foreach my $r (keys %{$vh->{gps}}) { $vh->{$p}{$r} = "" if(!defined $vh->{$p}{$r}); next if($vh->{$p}{$r} eq ""); if ($r eq "address") { $address = encode("UTF-8", $vh->{$p}{$r}) if($vh->{$p}{$r}); } if ($r eq "gps") { $lng = encode("UTF-8", $vh->{$p}{$r}{lng}); $lat = encode("UTF-8", $vh->{$p}{$r}{lat}); } } push(@row_array, $bts+$n." 40_gpsAddress " .$address."\n"); $val = "lat=".$lat.",lng=".$lng; push(@row_array, $bts+$n." 45_gpsCoordinates " .$val."\n"); } push(@row_array, $bts+$n." 50_isAllday " .$val."\n") if($p eq "is_all_day"); push(@row_array, $bts+$n." 55_isRepeatEvt " .$val."\n") if($p eq "is_repeat_evt"); if($p eq "due") { my (undef,undef,$duedate,$duetime,$duets) = SSCal_explodeDateTime ($hash,$val); push(@row_array, $bts+$n." 60_dueDateTime " .$duedate." ".$duetime."\n"); push(@row_array, $bts+$n." 65_dueTimestamp " .$duets."\n"); } push(@row_array, $bts+$n." 85_percentComplete " .$val."\n") if($p eq "percent_complete" && $om eq "todolist"); push(@row_array, $bts+$n." 90_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(!defined $vh->{$p}{$r}); next if($vh->{$p}{$r} eq ""); $val = encode("UTF-8", $vh->{$p}{$r}); push(@row_array, $bts+$n." 70_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." 75_".sprintf("%0$l.0f", $ens)."_notifyTimestamp ".$uts."\n"); push(@row_array, $bts+$n." 80_".sprintf("%0$l.0f", $ens)."_notifyDateTime " .$td."\n"); $alarmed = SSCal_isAlarmed ($ts,$uts,$bts) if(!$alarmed); } } $ens++; } } push(@row_array, $bts+$n." 98_EventId " .$val."\n") if($p eq "evt_id"); } $status = "upcoming" if($upcoming); $status = "alarmed" if($alarmed); $status = "started" if($started); $status = "ended" if($ended); push(@row_array, $bts+$n." 17_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_experrorauth($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_experrorauth($hash,$errorcode); 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 Calendar AUTH API # Übernahmewerte sind $hash, $errorcode ############################################################################## sub SSCal_experrorauth ($$) { my ($hash,$errorcode) = @_; my $device = $hash->{NAME}; my $error; unless (exists($SSCal_errauthlist{"$errorcode"})) { $error = "Value of errorcode \"$errorcode\" not found."; return ($error); } $error = $SSCal_errauthlist{"$errorcode"}; return ($error); } ############################################################################## # Auflösung Errorcodes Calendar 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); } $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('!_ESC_!', @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("!_ESC_!",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; } ############################################################################################# # 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' # 'time_value' => 'PT6H' # 'time_value' => '-P1DT15H' # # 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*$1*86400; } elsif ($tv =~ /^-PT(\d+)H$/) { $corr = -1*$1*3600; } elsif ($tv =~ /^-PT(\d+)M$/) { $corr = -1*$1*60; } elsif ($tv =~ /^PT(\d+)S$/) { $corr = $1; } elsif ($tv =~ /^PT(\d+)M$/) { $corr = $1*60; } elsif ($tv =~ /^PT(\d+)H$/) { $corr = $1*3600; } elsif ($tv =~ /^-P(\d)+DT(\d+)H$/) { $corr = -1*($1*86400 + $2*3600); } 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,$isallday) = @_; my $name = $hash->{NAME}; my ($tz,$t) = ("",""); my ($d,$tstamp) = ("",0); my $invalid = 0; my $corrsec = 0; # Korrektursekunde my ($sec,$min,$hour,$mday,$month,$year); return ($invalid,$tz,$d,$t,$tstamp) if(!$dt); $corrsec = 1 if($isallday); # wenn Ganztagsevent, Endetermin um 1 Sekunde verkürzen damit Termin am selben Tag 23:59:59 endet (sonst Folgetag 00:00:00) 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"); } if ($corrsec) { # Termin um 1 Sekunde verkürzen damit Termin am selben Tag 23:59:59 endet (sonst Folgetag 00:00:00) eval { $tstamp = fhemTimeLocal($sec, $min, $hour, $mday, $month-1, $year-1900); }; $tstamp -= $corrsec; my $nt = FmtDateTime($tstamp); $nt =~ /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/; $year = $1; $month = $2; $mday = $3; $hour = $4; $min = $5; $sec = $6; $d = $year."-".$month."-".$mday; $t = $hour.":".$min.":".$sec; } 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; } ############################################################################### # JSON Boolean Test und Mapping ############################################################################### sub SSCal_jboolmap($){ my ($bool)= @_; if(JSON::is_bool($bool)) { my $b = SSCal_boolean($bool); $bool = 1 if($b == $JSON::true); $bool = 0 if($b == $JSON::false); } return $bool; } sub SSCal_boolean { # might be called as method or as function, so pop() to get the last arg # instead of shift() to get the first pop() ? $JSON::true : $JSON::false } ############################################################################################# # Kalenderliste als HTML-Tabelle zurückgeben ############################################################################################# sub SSCal_calAsHtml($;$) { my ($name,$FW_wname) = @_; my $hash = $defs{$name}; my $lang = AttrVal("global", "language", "EN"); my $mi = AttrVal($name, "tableColumnMap", "icon"); my ($begin,$begind,$begint,$end,$endd,$endt,$summary,$location,$status,$desc,$gps,$gpsa,$gpsc); my ($cal,$completion,$tz,$dleft,$dleftlong,$weekday,$edleft,$id,$isallday); # alle Readings in Array einlesen my @allrds = keys%{$defs{$name}{READINGS}}; # Sprachsteuerung my $de = 0; if($lang eq "DE") {$de = 1}; # Entscheidung ob Tabelle für Small Screen optimiert my $small = 0; if ($FW_wname && $hash->{HELPER}{tableSpecs}{smallScreenStyles}) { # Aufruf durch FHEMWEB und smallScreen-Eigenschaft gesetzt my %specs; my $FW_style = AttrVal($FW_wname, "stylesheetPrefix", "default"); my @scspecs = split(",", $hash->{HELPER}{tableSpecs}{smallScreenStyles}); # Eigenschaft smallScreen in Array lesen grep { !$specs{$_}++ } @scspecs; $small = 1 if($specs{$FW_style}); # Tabelle für small-Style anpassen } # Auswahl der darzustellenden Tabellenfelder my %seen; my @cof = split(",", AttrVal($name, "tableFields", "Begin,End,Summary,Status,Location")); grep { !$seen{$_}++ } @cof; # Tabelle my $out = ""; $out .= ""; $out .= ""; $out .= ""; $out .= ""; $out .= ""; $out .= ""; $out .= ""; $out .= ""; if ($small) { # nur ein Datumfeld umbrechbar $out .= "" if($seen{Begin}); $out .= "" if($seen{End}); } else { $out .= "" if($seen{Begin}); $out .= "" if($seen{Begin}); $out .= "" if($seen{End}); $out .= "" if($seen{End}); } $out .= "" if($seen{DaysLeft}); $out .= "" if($seen{DaysLeftLong}); $out .= "" if($seen{Weekday}); $out .= "" if($seen{Timezone}); $out .= "" if($seen{Summary}); $out .= "" if($seen{Description}); $out .= "" if($seen{Status}); $out .= "" if($seen{Completion}); $out .= "" if($seen{Location}); $out .= "" if($seen{Map}); $out .= "" if($seen{Calendar}); $out .= "" if($seen{EventId}); my $maxbnr; foreach my $key (keys %{$defs{$name}{READINGS}}) { next if $key !~ /^(\d+)_\d+_EventId$/; $maxbnr = $1 if(!$maxbnr || $1>$maxbnr); } return "" if(!defined $maxbnr); my $l = length($maxbnr); my $k; for ($k=0;$k<=$maxbnr;$k++) { my $bnr = sprintf("%0$l.0f", $k); # Prestring erstellen last if(!ReadingsVal($name, $bnr."_98_EventId", "")); # keine Ausgabe wenn es keine EventId mit Blocknummer 0 gibt -> kein Event/Aufgabe vorhanden ($begind,$begint,$endd,$endt,$gps) = ("","","","",""); $summary = ReadingsVal($name, $bnr."_01_Summary", ""); $desc = ReadingsVal($name, $bnr."_03_Description", ""); $begin = ReadingsVal($name, $bnr."_05_Begin", ""); $end = ReadingsVal($name, $bnr."_10_End", ""); $tz = ReadingsVal($name, $bnr."_15_Timezone", ""); $status = ReadingsVal($name, $bnr."_17_Status", ""); $dleft = ReadingsVal($name, $bnr."_20_daysLeft", ""); $dleftlong = ReadingsVal($name, $bnr."_25_daysLeftLong", ""); $weekday = ReadingsVal($name, $bnr."_30_Weekday", ""); $location = ReadingsVal($name, $bnr."_35_Location", ""); $gpsa = ReadingsVal($name, $bnr."_40_gpsAddress", ""); $gpsc = ReadingsVal($name, $bnr."_45_gpsCoordinates", ""); $completion = ReadingsVal($name, $bnr."_85_percentComplete", ""); $cal = ReadingsVal($name, $bnr."_90_calName", ""); $id = ReadingsVal($name, $bnr."_98_EventId", ""); $isallday = ReadingsVal($name, $bnr."_50_isAllday", ""); if($gpsc) { my $micon; if ($mi eq "icon") { # Karten-Icon auswählen my $di = "it_i-net"; $micon = SSCal_evalTableSpecs ($hash,$di,$hash->{HELPER}{tableSpecs}{columnMapIcon},$bnr,\@allrds,"image"); # in Image umwandeln (versuchen) wenn SSCal_evalTableSpecs String liefert # if($ui =~ /{HELPER}{tableSpecs}{columnMapText},$bnr,\@allrds,"string"); } else { $micon = ""; } my ($lat,$lng) = split(",", $gpsc); $lat = (split("=", $lat))[1]; $lng = (split("=", $lng))[1]; # Kartenanbieter auswählen my $up = SSCal_evalTableSpecs ($hash,"",$hash->{HELPER}{tableSpecs}{columnMapProvider},$bnr,\@allrds,"string"); if ($up eq "GoogleMaps") { # Kartenprovider: Google Maps $gps = " $micon "; } elsif ($up eq "OpenStreetMap") { $gps = " $micon "; # Kartenprovider: OpenstreetMap } else { $gps = " $micon "; # Kartenprovider default: Google Maps } } if($begin ne "") { # Datum sprachabhängig konvertieren bzw. heute/morgen setzen my ($ny,$nm,$nd,undef) = split(/[ -]/, TimeNow()); # Jetzt my ($by,$bm,$bd,$bt) = split(/[ -]/, $begin); my ($ey,$em,$ed,$et) = split(/[ -]/, $end); my $ntimes = fhemTimeLocal(00, 00, 00, $nd, $nm-1, $ny-1900); my $btimes = fhemTimeLocal(00, 00, 00, $bd, $bm-1, $by-1900); my $etimes = fhemTimeLocal(00, 00, 00, $ed, $em-1, $ey-1900); if($de) { $begind = "$bd.$bm.$by"; $endd = "$ed.$em.$ey"; } else { $begind = "$by-$bm-$bd"; $endd = "$ey-$em-$ed"; } my($a,$b,undef) = split(":", $bt); $begint = "$a:$b"; my($c,$d,undef) = split(":", $et); $endt = "$c:$d"; $edleft = ""; if($etimes >= $ntimes) { $edleft = int(($etimes - $ntimes)/86400); } $begind = (($de)?'heute ':'today ') if($dleft eq "0"); $endd = (($de)?'heute ':'today ') if($edleft eq "0"); $begind = (($de)?'morgen ':'tomorrow ') if($dleft eq "1"); $endd = (($de)?'morgen ':'tomorrow ') if($edleft eq "1"); if (($begind eq $endd) && !$isallday) { $endd = ""; # bei "Ende" nur Uhrzeit angeben wenn Termin am gleichen Tag beginnt/endet aber kein Ganztagstermin ist } elsif (($begind eq $endd) && $isallday) { $begint = ""; $endt = ""; } } # Icon für Spalte Resttage spezifizieren $dleft = SSCal_evalTableSpecs ($hash,$dleft,$hash->{HELPER}{tableSpecs}{columnDaysLeftIcon},$bnr,\@allrds,"image"); # Icon für Spalte Status spezifizieren $status = SSCal_evalTableSpecs ($hash,$status,$hash->{HELPER}{tableSpecs}{columnStateIcon},$bnr,\@allrds,"image"); $out .= "
"; if($small) { $out .= " ".$begind." ".$begint. "" if($seen{Begin}); $out .= " ".$endd ." ".$endt. "" if($seen{End}); } else { $out .= " $begind " if($seen{Begin}); $out .= " $begint " if($seen{Begin}); $out .= " $endd " if($seen{End}); $out .= " $endt " if($seen{End}); } $out .= " $dleft " if($seen{DaysLeft}); $out .= " $dleftlong " if($seen{DaysLeftLong}); $out .= " $weekday " if($seen{Weekday}); $out .= " $tz " if($seen{Timezone}); $out .= " $summary " if($seen{Summary}); $out .= " $desc " if($seen{Description}); $out .= " $status " if($seen{Status}); $out .= " $completion " if($seen{Completion}); $out .= " $location " if($seen{Location}); $out .= " $gps " if($seen{Map}); $out .= " $cal " if($seen{Calendar}); $out .= " $id " if($seen{EventId}); $out .= "
"; } $out .= "
".(($de)?'Start' :'Begin')." ".(($de)?'Ende' :'End')." ".(($de)?'Start' :'Begin')." ".(($de)?'----' :'----')." ".(($de)?'Ende' :'End')." ".(($de)?'----' :'----')." ".(($de)?'Resttage' :'Days left')." ".(($de)?'Terminziel' :'Goal')." ".(($de)?'Wochentag' :'Weekday')." ".(($de)?'Zeitzone' :'Timezone')." ".(($de)?'Zusammenfassung' :'Summary')." ".(($de)?'Beschreibung' :'Description')." ".(($de)?'Status' :'State')." ".(($de)?'Erfüllung (%)' :'Completion (%)')." ".(($de)?'Ort' :'Location')." ".(($de)?'Karte' :'Map')." ".(($de)?'Kalender' :'Calendar')." ".(($de)?'ID' :'ID')."
"; $out .= ""; return $out; } ###################################################################################### # Evaluiere Eigenschaften von Attribut "tableSpecs" # # $hash: Devicehash # $default: Standardwert - wird wieder zurückgegeben wenn kein Funktionsergebnis # $specs: Basisschlüssel (z.B. $hash->{HELPER}{tableSpecs}{columnDaysLeft}) # $allreads: Referenz zum ARRAY was alle vorhandenen Readings des Devices enthält # $bnr: Blocknummer Readings # $rdtype: erwarteter Datentyp als Rückgabe (image, string) # ###################################################################################### sub SSCal_evalTableSpecs (@){ my ($hash,$default,$specs,$bnr,$allrds,$rdtype) = @_; my $name = $hash->{NAME}; my $check; $rdtype = $rdtype ? $rdtype : "string"; # "string" als default Rückgabe Datentyp # anonymous sub für Abarbeitung Perl-Kommandos $check = sub ($) { my ($specs) = @_; my $ret = AnalyzePerlCommand(undef, $specs); return $ret; }; no warnings; if ($specs) { # Eigenschaft muß Wert haben my ($rn,$reading,$uval,$ui,$rval); if (ref($specs) eq "ARRAY") { # Wenn Schlüssel ein ARRAY enthält my $i = 0; while ($specs->[$i]) { my $n = keys %{$specs->[$i]}; # Anzahl Elemente (Entscheidungskriterien) in Hash foreach my $k (keys %{$specs->[$i]}) { if ($k eq "icon") { $ui = $specs->[$i]{$k}; $n--; next; } foreach my $r (@{$allrds}) { # alle vorhandenen Readings evaluieren if($r =~ m/$k$/) { (undef,$rn,$reading) = split("_", $r); # Readingnummer evaluieren $uval = $specs->[$i]{$k}; # Vergleichswert last; } } $rval = ReadingsVal($name, $bnr."_".$rn."_".$reading, "empty"); $rval = "\"".$rval."\""; if ( eval ($rval . $uval) ) { $ui = $specs->[$i]{icon}; $n--; } else { $ui = ""; } } if($n == 0 && $ui) { $default = $ui; # Defaultwert mit Select ersetzen wenn alle Bedingungen erfüllt } $i++; } } elsif (ref($specs) eq "HASH") { # Wenn Schlüssel ein HASH enthält my $n = keys %{$specs}; # Anzahl Elemente (Entscheidungskriterien) in Hash foreach my $k (keys %{$specs}) { if ($k eq "icon") { $ui = $specs->{$k}; $n--; next; } foreach my $r (@{$allrds}) { # alle vorhandenen Readings evaluieren if($r =~ m/$k$/) { (undef,$rn,$reading) = split("_", $r); # Readingnummer evaluieren $uval = $specs->{$k}; # Vergleichswert last; } } $rval = ReadingsVal($name, $bnr."_".$rn."_".$reading, "empty"); $rval = "\"".$rval."\""; if ( eval ($rval . $uval) ) { $ui = $specs->{icon}; $n--; } else { $ui = ""; } } if($n == 0 && $ui) { $default = $ui; # Defaultwert mit Select ersetzen wenn alle Bedingungen erfüllt } } else { # ref Wert der Eigenschaft ist nicht HASH oder ARRAY if($specs =~ m/^\{.*\}$/s) { # den Wert als Perl-Funktion ausführen wenn in {} $specs =~ s/\$NAME/$name/g; # Platzhalter $NAME, $BNR ersetzen $specs =~ s/\$BNR/$bnr/g; $default = $check->($specs); } else { # einfache key-value Zuweisung eval ($default = $specs); } } } if($default && $rdtype eq "image") { $default = FW_makeImage($default); # Icon aus "string" errechnen wenn "image" als Rückgabe erwartet wird und $default gesetzt } use warnings; return $default; } ############################################################################################# # 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": "stable", "author": [ "Heiko Maaz " ], "x_fhem_maintainer": [ "DS_Starter" ], "x_fhem_maintainer_github": [ "nasseeder1" ], "prereqs": { "runtime": { "requires": { "FHEM": 5.00918799, "perl": 5.014, "JSON": 4.020, "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