From 5af154030ebda91f2a7a604b1f66e8b79a553bd3 Mon Sep 17 00:00:00 2001 From: nasseeder1 Date: Sun, 23 Jan 2022 20:18:43 +0000 Subject: [PATCH] 49_SSCam: contrib 9.10.3 git-svn-id: https://svn.fhem.de/fhem/trunk@25551 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/contrib/DS_Starter/49_SSCam.pm | 162 ++- fhem/contrib/DS_Starter/API.pm | 102 +- fhem/contrib/DS_Starter/SMUtils.pm | 1927 +++++++++++++++++++++++++++ 3 files changed, 2073 insertions(+), 118 deletions(-) create mode 100644 fhem/contrib/DS_Starter/SMUtils.pm diff --git a/fhem/contrib/DS_Starter/49_SSCam.pm b/fhem/contrib/DS_Starter/49_SSCam.pm index a2631593b..2ca660f71 100644 --- a/fhem/contrib/DS_Starter/49_SSCam.pm +++ b/fhem/contrib/DS_Starter/49_SSCam.pm @@ -1,5 +1,5 @@ ######################################################################################################################## -# $Id: 49_SSCam.pm 24484 2021-05-21 21:07:42Z DS_Starter $ +# $Id: 49_SSCam.pm 25176 2021-11-03 18:53:51Z DS_Starter $ ######################################################################################################################### # 49_SSCam.pm # @@ -49,6 +49,7 @@ use FHEM::SynoModules::SMUtils qw( showModuleInfo jboolmap completeAPI + ApiVal showAPIinfo setCredentials getCredentials @@ -184,6 +185,9 @@ BEGIN { # Versions History intern my %vNotesIntern = ( + "9.10.3" => "23.11.2022 made SYNO.SurveillanceStation.AudioStream, SYNO.SurveillanceStation.VideoStream optional for SVS compatibility to 9.0.0 ", + "9.10.2" => "03.11.2021 set SVS compatibility to 8.2.10 ", + "9.10.1" => "18.07.2021 set SVS compatibility to 8.2.9 ", "9.10.0" => "03.07.2021 change getApiSites_Parse for better simu_SVSversion, new value 8.2.8-xxxx for attr simu_SVSversion ", "9.9.0" => "21.05.2021 new get command saveLastSnap ", "9.8.5" => "22.02.2021 remove sscam_tooltip.js, substitute /fhem by \$FW_ME ", @@ -629,39 +633,47 @@ my %sdswfn = ( # Fun "hls" => {fn => "__switchedHLS" }, ); +my %hvada = ( # Funktionshash Version Adaption + "a01" => {AUTH => "6" }, +); + my %hsimu = ( # Funktionshash Version Simulation - "71xxxx-simu" => {AUTH => "4", EXTREC => "2", CAM => "8", SNAPSHOT => "1", PTZ => "4", - PRESET => "1", SVSINFO => "5", CAMEVENT => "1", EVENT => "5", VIDEOSTM => "1", - EXTEVT => "1", STM => "1", LOG => "1", REC => "4" }, - "72xxxx-simu" => {AUTH => "6", EXTREC => "3", CAM => "8", SNAPSHOT => "1", PTZ => "5", - PRESET => "1", SVSINFO => "6", CAMEVENT => "1", EVENT => "5", VIDEOSTM => "1", - EXTEVT => "1", STM => "1", LOG => "1", REC => "4" }, - "800xxxx-simu" => {AUTH => "6", EXTREC => "3", CAM => "9", SNAPSHOT => "1", PTZ => "5", - PRESET => "1", SVSINFO => "6", CAMEVENT => "1", EVENT => "5", VIDEOSTM => "1", - EXTEVT => "1", STM => "1", LOG => "1", REC => "6" }, - "815xxxx-simu" => {AUTH => "6", EXTREC => "3", CAM => "9", SNAPSHOT => "1", PTZ => "5", - PRESET => "1", SVSINFO => "6", CAMEVENT => "1", EVENT => "5", VIDEOSTM => "1", - EXTEVT => "1", STM => "1", HMODE => "1", LOG => "3", AUDIOSTM => "2", - VIDEOSTMS => "1", REC => "6" }, - "820xxxx-simu" => {AUTH => "6", EXTREC => "3", CAM => "9", SNAPSHOT => "1", PTZ => "5", - PRESET => "1", SVSINFO => "6", CAMEVENT => "1", EVENT => "5", VIDEOSTM => "1", - EXTEVT => "1", STM => "1", HMODE => "1", LOG => "3", AUDIOSTM => "2", - VIDEOSTMS => "1", REC => "6" }, - "828xxxx-simu" => {AUTH => "6", EXTREC => "3", CAM => "9", SNAPSHOT => "1", PTZ => "6", - PRESET => "1", SVSINFO => "8", CAMEVENT => "1", EVENT => "5", VIDEOSTM => "1", - EXTEVT => "1", STM => "1", HMODE => "1", LOG => "3", AUDIOSTM => "2", - VIDEOSTMS => "1", REC => "6" }, + "71xxxx-simu" => {INFO => "1", AUTH => "4", EXTREC => "2", CAM => "8", SNAPSHOT => "1", + PTZ => "4", PRESET => "1", SVSINFO => "5", CAMEVENT => "1", EVENT => "5", + VIDEOSTM => "1", EXTEVT => "1", STM => "1", LOG => "1", REC => "4" }, + "72xxxx-simu" => {INFO => "1", AUTH => "6", EXTREC => "3", CAM => "8", SNAPSHOT => "1", + PTZ => "5", PRESET => "1", SVSINFO => "6", CAMEVENT => "1", EVENT => "5", + VIDEOSTM => "1", EXTEVT => "1", STM => "1", LOG => "1", REC => "4" }, + "800xxxx-simu" => {INFO => "1", AUTH => "6", EXTREC => "3", CAM => "9", SNAPSHOT => "1", + PTZ => "5", PRESET => "1", SVSINFO => "6", CAMEVENT => "1", EVENT => "5", + VIDEOSTM => "1", EXTEVT => "1", STM => "1", LOG => "1", REC => "6" }, + "815xxxx-simu" => {INFO => "1", AUTH => "6", EXTREC => "3", CAM => "9", SNAPSHOT => "1", + PTZ => "5", PRESET => "1", SVSINFO => "6", CAMEVENT => "1", EVENT => "5", + VIDEOSTM => "1", EXTEVT => "1", STM => "1", LOG => "3", REC => "6", + AUDIOSTM => "2", VIDEOSTMS => "1", HMODE => "1" }, + "820xxxx-simu" => {INFO => "1", AUTH => "6", EXTREC => "3", CAM => "9", SNAPSHOT => "1", + PTZ => "5", PRESET => "1", SVSINFO => "6", CAMEVENT => "1", EVENT => "5", + VIDEOSTM => "1", EXTEVT => "1", STM => "1", HMODE => "1", LOG => "3", + AUDIOSTM => "2", VIDEOSTMS => "1", REC => "6" }, + "828xxxx-simu" => {INFO => "1", AUTH => "6", EXTREC => "3", CAM => "9", SNAPSHOT => "1", + PTZ => "6", PRESET => "1", SVSINFO => "8", CAMEVENT => "1", EVENT => "5", + VIDEOSTM => "1", EXTEVT => "1", STM => "1", HMODE => "1", LOG => "3", + AUDIOSTM => "2", VIDEOSTMS => "1", REC => "6" }, ); # Standardvariablen und Forward-Deklaration my $defSlim = 3; # default Anzahl der abzurufenden Schnappschüsse mit snapGallery my $defColumns = 3; # default Anzahl der Spalten einer snapGallery my $defSnum = "1,2,3,4,5,6,7,8,9,10"; # mögliche Anzahl der abzurufenden Schnappschüsse mit snapGallery -my $compstat = "8.2.8"; # getestete SVS-Version +my $compstat = "9.0.00"; # getestete SVS-Version my $valZoom = ".++,+,stop,-,--."; # Inhalt des Setters "setZoom" my $shutdownInProcess = 0; # Statusbit shutdown my $todef = 20; # httptimeout default Wert + my @simus = qw(7.1-xxxx 7.2-xxxx 8.0.0-xxxx + 8.1.5-xxxx 8.2.0-xxxx 8.2.8-xxxx + ); # mögliche Simulationsversionen + #use vars qw($FW_ME); # webname (default is fhem), used by 97_GROUP/weblink #use vars qw($FW_subdir); # Sub-path in URL, used by FLOORPLAN/weblink #use vars qw($FW_room); # currently selected room @@ -813,6 +825,8 @@ sub Initialize { $hash->{FW_detailFn} = \&FWdetailFn; $hash->{FW_deviceOverview} = 1; + my $simver = join ",", @simus; + $hash->{AttrList} = "disable:1,0 ". "debugactivetoken:1,0 ". "debugCachetime:1,0 ". @@ -854,7 +868,7 @@ sub Initialize { "session:SurveillanceStation,DSM ". "showPassInLog:1,0 ". "showStmInfoFull:1,0 ". - "simu_SVSversion:7.1-xxxx,7.2-xxxx,8.0.0-xxxx,8.1.5-xxxx,8.2.0-xxxx,8.2.8-xxxx ". + "simu_SVSversion:$simver ". "videofolderMap ". "webCmd ". $readingFnAttributes; @@ -3632,7 +3646,7 @@ sub __runLiveview { if ($hash->{HELPER}{RUNVIEW} !~ m/snap|^live_.*hls$/x) { if ($hash->{HELPER}{RUNVIEW} =~ m/live/x) { - if($hash->{HELPER}{API}{AUDIOSTM}{VER}) { # Audio aktivieren + if(ApiVal ($hash, $hash->{HELPER}{API}{AUDIOSTM}, 'VER', '')) { # Audio aktivieren $hash->{HELPER}{ACALL}{AKEY} = "AUDIOSTM"; $hash->{HELPER}{ACALL}{APART} = qq{api=_ANAME_&version=_AVER_&method=Stream&cameraId=_CID_&_sid=_SID_}; } @@ -3640,7 +3654,7 @@ sub __runLiveview { delete $hash->{HELPER}{AUDIOLINK}; } - if($hash->{HELPER}{API}{VIDEOSTMS}{VER}) { # API "SYNO.SurveillanceStation.VideoStream" vorhanden ? (removed ab API v2.8) + if(ApiVal ($hash, $hash->{HELPER}{API}{VIDEOSTMS}, 'VER', '')) { # API "SYNO.SurveillanceStation.VideoStream" vorhanden ? (removed ab API v2.8) $hash->{HELPER}{CALL}{VKEY} = "VIDEOSTMS"; $hash->{HELPER}{CALL}{PART} = qq{api=_NAME_&version=_VER_&method=Stream&cameraId=_CID_&format=mjpeg&_sid=_SID_}; } @@ -4597,11 +4611,9 @@ sub __getCaminfoAll { __getSvsInfo ($hash); - # wenn gesetzt = manuelle Abfrage - # return if ($mode); # 24.03.2018 geänd. + my $pcia = AttrVal($name,"pollcaminfoall", 0); + my $pnl = AttrVal($name,"pollnologging", 0); - my $pcia = AttrVal($name,"pollcaminfoall",0); - my $pnl = AttrVal($name,"pollnologging",0); if ($pcia) { my $new = gettimeofday()+$pcia; InternalTimer($new, $caller, $hash, 0); @@ -5002,7 +5014,7 @@ sub __getStreamFormat { my $caller = (caller(0))[3]; RemoveInternalTimer($hash, $caller); - return if(IsDisabled($name)); + return if(IsDisabled($name) || !ApiVal ($hash, $hash->{HELPER}{API}{VIDEOSTMS}, 'VER', '')); if ($hash->{HELPER}{ACTIVE} eq "off") { $hash->{OPMODE} = "getstreamformat"; @@ -5269,8 +5281,8 @@ sub getApiSites_Parse { delActiveToken($hash); # ausgeführte Funktion ist abgebrochen, Freigabe Funktionstoken return; - - } elsif ($myjson ne "") { # Evaluiere ob Daten im JSON-Format empfangen wurden + } + elsif ($myjson ne "") { # Evaluiere ob Daten im JSON-Format empfangen wurden ($success) = evaljson($hash,$myjson); if(!$success) { @@ -5322,16 +5334,14 @@ sub getApiSites_Parse { ########################################################################################################### Log3($name, 4, "$name - ------- Begin of adaption section -------"); - my @sims; + my $adavs = "a01"; # adaptierte Version - # push @sims, "CAM:8"; - # push @sims, "PTZ:4"; - - for my $esim (@sims) { - my($k,$v) = split ":", $esim; - $hash->{HELPER}{API}{$k}{VER} = $v; - $hash->{HELPER}{API}{$k}{MOD} = "yes"; - Log3($name, 4, "$name - Version of $hash->{HELPER}{API}{$k}{NAME} adapted to: $hash->{HELPER}{API}{$k}{VER}"); + if($adavs) { + for my $av (sort keys %{$hvada{$adavs}}) { + $hash->{HELPER}{API}{$av}{VER} = $hvada{$adavs}{$av}; + $hash->{HELPER}{API}{$av}{MOD} = "yes"; + Log3($name, 4, "$name - Version of $hash->{HELPER}{API}{$av}{NAME} adapted to: $hash->{HELPER}{API}{$av}{VER}"); + } } Log3($name, 4, "$name - ------- End of adaption section -------"); @@ -5342,8 +5352,17 @@ sub getApiSites_Parse { if (AttrVal($name, "simu_SVSversion", undef)) { Log3($name, 4, "$name - SVS version $actvs will be simulated"); + + for my $ak (sort keys %{$hash->{HELPER}{API}} ) { + next if($ak =~ /^PARSET$/x); + if(!exists $hsimu{$actvs}{$ak}) { + Log3($name, 4, "$name - delete $hash->{HELPER}{API}{$ak}{NAME} due to version setting"); + delete $hash->{HELPER}{API}{$ak}; + } + } - for my $k (sort keys %{$hsimu{$actvs}}) { + for my $k (sort keys %{$hsimu{$actvs}} ) { + next if(!ApiVal ($hash, $hash->{HELPER}{API}{$k}, 'NAME', '')); $hash->{HELPER}{API}{$k}{VER} = $hsimu{$actvs}{$k}; $hash->{HELPER}{API}{$k}{MOD} = "yes"; Log3($name, 4, "$name - Version of $hash->{HELPER}{API}{$k}{NAME} adapted to: $hash->{HELPER}{API}{$k}{VER}"); @@ -5908,8 +5927,8 @@ sub camOp_Parse { readingsSingleUpdate($hash, "CamStreamFormat", $sformat, 1); setReadingErrorNone ($hash, 1); } - elsif ($OpMode eq "runpatrol") { # eine Tour wurde gestartet - my $st = (ReadingsVal("$name", "Record", "Stop") eq "Start") ? "on" : "off"; # falls Aufnahme noch läuft -> state = on setzen + elsif ($OpMode eq "runpatrol") { # eine Tour wurde gestartet + my $st = (ReadingsVal("$name", "Record", "Stop") eq "Start") ? "on" : "off"; # falls Aufnahme noch läuft -> state = on setzen DoTrigger($name,"patrol started"); Log3 ($name, 3, qq{$name - Patrol "$hash->{HELPER}{GOPATROLNAME}" of camera $camname has been started successfully} ); @@ -6431,7 +6450,7 @@ sub _parsegetsvsinfo { ## no critic "not used" } my $avsc = $major.$minor.(($small=~/\d/x) ? $small : 0); # Kompatibilitätscheck - my $avcomp = $hash->{COMPATIBILITY}; + my $avcomp = $compstat; $avcomp =~ s/\.//gx; my $compstate = ($avsc <= $avcomp) ? "true" : "false"; @@ -6499,9 +6518,9 @@ sub __parserunliveviewHLS { ## no critic "not used" my $serveraddr = $hash->{SERVERADDR}; my $serverport = $hash->{SERVERPORT}; my $camid = $hash->{CAMID}; - my $apivideostms = $hash->{HELPER}{API}{VIDEOSTMS}{NAME}; - my $apivideostmspath = $hash->{HELPER}{API}{VIDEOSTMS}{PATH}; - my $apivideostmsver = $hash->{HELPER}{API}{VIDEOSTMS}{VER}; + my $apivideostms = ApiVal ($hash, $hash->{HELPER}{API}{VIDEOSTMS}, 'NAME', ''); + my $apivideostmspath = ApiVal ($hash, $hash->{HELPER}{API}{VIDEOSTMS}, 'PATH', ''); + my $apivideostmsver = ApiVal ($hash, $hash->{HELPER}{API}{VIDEOSTMS}, 'VER', ''); my $sid = $hash->{HELPER}{SID}; $hash->{HELPER}{HLSSTREAM} = "active"; @@ -7489,7 +7508,7 @@ return; ############################################################################### # Eigenschaften des Device liefern ############################################################################### -sub IsModelCam { # Modelleigenschaft liefern Cam-> 1 , sonst 0 +sub IsModelCam { # Modelleigenschaft liefern Cam-> 1 , sonst 0 my $hash = shift; my $m = ($hash->{MODEL} ne "SVS") ? 1 : 0; @@ -7498,13 +7517,13 @@ return $m; } sub IsCapHLS { # HLS Lieferfähigkeit (existiert "SYNO.SurveillanceStation.VideoStream" & Reading) - my ($hash) = @_; + my $hash = shift; my $name = $hash->{NAME}; my $cap = 0; - my $api = $hash->{HELPER}{API}{VIDEOSTMS}{VER}; - my $csf = (ReadingsVal($name,"CamStreamFormat","MJPEG") eq "HLS")?1:0; + my $apiver = ApiVal ($hash, $hash->{HELPER}{API}{VIDEOSTMS}, 'VER', ''); + my $csf = (ReadingsVal($name,"CamStreamFormat","MJPEG") eq "HLS") ? 1 : 0; - $cap = 1 if($api && $csf); + $cap = 1 if($apiver && $csf); return $cap; } @@ -7723,15 +7742,15 @@ sub streamDev { ## no critic 'comp pws => $pws, serveraddr => $hash->{SERVERADDR}, serverport => $hash->{SERVERPORT}, - apivideostm => $hash->{HELPER}{API}{VIDEOSTM}{NAME}, - apivideostmpath => $hash->{HELPER}{API}{VIDEOSTM}{PATH}, - apivideostmver => $hash->{HELPER}{API}{VIDEOSTM}{VER}, - apiaudiostm => $hash->{HELPER}{API}{AUDIOSTM}{NAME}, - apiaudiostmpath => $hash->{HELPER}{API}{AUDIOSTM}{PATH}, - apiaudiostmver => $hash->{HELPER}{API}{AUDIOSTM}{VER}, - apivideostms => $hash->{HELPER}{API}{VIDEOSTMS}{NAME}, - apivideostmspath => $hash->{HELPER}{API}{VIDEOSTMS}{PATH}, - apivideostmsver => $hash->{HELPER}{API}{VIDEOSTMS}{VER}, + apivideostm => ApiVal ($hash, $hash->{HELPER}{API}{VIDEOSTM}, 'NAME', ''), + apivideostmpath => ApiVal ($hash, $hash->{HELPER}{API}{VIDEOSTM}, 'PATH', ''), + apivideostmver => ApiVal ($hash, $hash->{HELPER}{API}{VIDEOSTM}, 'VER', ''), + apiaudiostm => ApiVal ($hash, $hash->{HELPER}{API}{AUDIOSTM}, 'NAME', ''), + apiaudiostmpath => ApiVal ($hash, $hash->{HELPER}{API}{AUDIOSTM}, 'PATH', ''), + apiaudiostmver => ApiVal ($hash, $hash->{HELPER}{API}{AUDIOSTM}, 'VER', ''), + apivideostms => ApiVal ($hash, $hash->{HELPER}{API}{VIDEOSTMS}, 'NAME', ''), + apivideostmspath => ApiVal ($hash, $hash->{HELPER}{API}{VIDEOSTMS}, 'PATH', ''), + apivideostmsver => ApiVal ($hash, $hash->{HELPER}{API}{VIDEOSTMS}, 'VER', ''), camid => $hash->{CAMID}, sid => $hash->{HELPER}{SID}, proto => $hash->{PROTOCOL}, @@ -7841,10 +7860,10 @@ sub _streamDevMJPEG { ## no critic 'complexity not my $serverport = $params->{serverport}; my $apivideostms = $params->{apivideostms}; my $apivideostmspath = $params->{apivideostmspath}; - my $apivideostmsver = $params->{apivideostmsver}; + my $apivideostmsver = $params->{apivideostmsver}; my $apiaudiostm = $params->{apiaudiostm}; my $apiaudiostmpath = $params->{apiaudiostmpath}; - my $apiaudiostmver = $params->{apiaudiostmver}; + my $apiaudiostmver = $params->{apiaudiostmver}; my $cmdrecendless = $params->{cmdrecendless}; my $ttrecstart = $params->{ttrecstart}; @@ -7867,9 +7886,10 @@ sub _streamDevMJPEG { ## no critic 'complexity not else { if($apivideostmsver) { $link = "$proto://$serveraddr:$serverport/webapi/$apivideostmspath?api=$apivideostms&version=$apivideostmsver&method=Stream&cameraId=$camid&format=mjpeg&_sid=$sid"; - - } elsif ($hash->{HELPER}{STMKEYMJPEGHTTP}) { + } + elsif ($hash->{HELPER}{STMKEYMJPEGHTTP}) { $link = $hash->{HELPER}{STMKEYMJPEGHTTP}; + $link =~ s/"//gx; # vermeidet Javascript Fehler "SyntaxError: " unterminated string literal" } return $ret if(!$link); @@ -7879,10 +7899,10 @@ sub _streamDevMJPEG { ## no critic 'complexity not } if(!$ftui) { - $ret .= "')\">
"; + $ret .= qq{
}; } else { - $ret .= "
"; + $ret .= qq{
}; } $streamHash->{HELPER}{STREAM} = ""; # Stream für "get popupStream" speichern @@ -7896,6 +7916,7 @@ sub _streamDevMJPEG { ## no critic 'complexity not else { # Aufnahmebutton Stop $ret .= "$imgrecstop "; } + $ret .= "$imgdosnap "; } @@ -11653,7 +11674,8 @@ sub exitOnDis { if ($avail eq "disabled") { $errorcode = "402"; $exit = 1; - } elsif ($avail eq "disconnected") { + } + elsif ($avail eq "disconnected") { $errorcode = "502"; $exit = 1; } diff --git a/fhem/contrib/DS_Starter/API.pm b/fhem/contrib/DS_Starter/API.pm index d2d5062ac..649a95110 100644 --- a/fhem/contrib/DS_Starter/API.pm +++ b/fhem/contrib/DS_Starter/API.pm @@ -3,7 +3,7 @@ ######################################################################################################################### # API.pm # -# (c) 2022 by Heiko Maaz +# (c) 2020 - 2022 by Heiko Maaz # e-mail: Heiko dot Maaz at t-online dot de # # This Module provides Synology API information. @@ -32,7 +32,9 @@ use warnings; use utf8; use Carp qw(croak carp); -use version; our $VERSION = version->declare('1.2.1'); # Testversion SVS 9.0.0 +use version; our $VERSION = version->declare('1.3.0'); + +# use lib qw(/opt/fhem/FHEM /opt/fhem/lib); # für Syntaxcheck mit: perl -c /opt/fhem/lib/FHEM/SynoModules/API.pm use Exporter ('import'); our @EXPORT_OK = qw(apistatic); @@ -70,26 +72,30 @@ return; ######################################################################## # Liefert die statischen Informationen der Surveillance API +# mk : 1 - API Key muss vorhanden sein +# 0 - API Key kann vorhanden sein ######################################################################## sub _staticSurveillance { my %hapi = ( - INFO => { NAME => "SYNO.API.Info" }, # Info-Seite für alle API's, einzige statische Seite ! - AUTH => { NAME => "SYNO.API.Auth" }, # API used to perform session login and logout - SVSINFO => { NAME => "SYNO.SurveillanceStation.Info" }, - EVENT => { NAME => "SYNO.SurveillanceStation.Event" }, - EXTREC => { NAME => "SYNO.SurveillanceStation.ExternalRecording" }, - EXTEVT => { NAME => "SYNO.SurveillanceStation.ExternalEvent" }, - CAM => { NAME => "SYNO.SurveillanceStation.Camera" }, # stark geändert ab API v2.8 - SNAPSHOT => { NAME => "SYNO.SurveillanceStation.SnapShot" }, # This API provides functions on snapshot, including taking, editing and deleting snapshots. - PTZ => { NAME => "SYNO.SurveillanceStation.PTZ" }, - PRESET => { NAME => "SYNO.SurveillanceStation.PTZ.Preset" }, - CAMEVENT => { NAME => "SYNO.SurveillanceStation.Camera.Event" }, - VIDEOSTM => { NAME => "SYNO.SurveillanceStation.VideoStreaming" }, # verwendet in Response von "SYNO.SurveillanceStation.Camera: GetLiveViewPath" -> StreamKey-Methode - STM => { NAME => "SYNO.SurveillanceStation.Stream" }, # Beschreibung ist falsch und entspricht "SYNO.SurveillanceStation.Streaming" auch noch ab v2.8 - HMODE => { NAME => "SYNO.SurveillanceStation.HomeMode" }, - LOG => { NAME => "SYNO.SurveillanceStation.Log" }, - REC => { NAME => "SYNO.SurveillanceStation.Recording" }, # This API provides method to query recording information. - ); + INFO => { NAME => "SYNO.API.Info", mk => 1 }, # Info-Seite für alle API's, einzige statische Seite ! + AUTH => { NAME => "SYNO.API.Auth", mk => 1 }, # API used to perform session login and logout + SVSINFO => { NAME => "SYNO.SurveillanceStation.Info", mk => 1 }, + EVENT => { NAME => "SYNO.SurveillanceStation.Event", mk => 1 }, + EXTREC => { NAME => "SYNO.SurveillanceStation.ExternalRecording", mk => 1 }, + EXTEVT => { NAME => "SYNO.SurveillanceStation.ExternalEvent", mk => 1 }, + CAM => { NAME => "SYNO.SurveillanceStation.Camera", mk => 1 }, # stark geändert ab API v2.8 + SNAPSHOT => { NAME => "SYNO.SurveillanceStation.SnapShot", mk => 1 }, # This API provides functions on snapshot, including taking, editing and deleting snapshots. + PTZ => { NAME => "SYNO.SurveillanceStation.PTZ", mk => 1 }, + PRESET => { NAME => "SYNO.SurveillanceStation.PTZ.Preset", mk => 1 }, + CAMEVENT => { NAME => "SYNO.SurveillanceStation.Camera.Event", mk => 1 }, + VIDEOSTM => { NAME => "SYNO.SurveillanceStation.VideoStreaming", mk => 1 }, # verwendet in Response von "SYNO.SurveillanceStation.Camera: GetLiveViewPath" -> StreamKey-Methode + STM => { NAME => "SYNO.SurveillanceStation.Stream", mk => 1 }, # Beschreibung ist falsch und entspricht "SYNO.SurveillanceStation.Streaming" auch noch ab v2.8 + HMODE => { NAME => "SYNO.SurveillanceStation.HomeMode", mk => 0 }, + LOG => { NAME => "SYNO.SurveillanceStation.Log", mk => 1 }, + AUDIOSTM => { NAME => "SYNO.SurveillanceStation.AudioStream", mk => 0 }, # Audiostream mit SID, removed in API v2.8 (noch undokumentiert verfügbar vor SVS 9.0.0 / API V 3.11) + VIDEOSTMS => { NAME => "SYNO.SurveillanceStation.VideoStream", mk => 0 }, # Videostream mit SID, removed in API v2.8 (noch undokumentiert verfügbar vor SVS 9.0.0 / API V 3.11) + REC => { NAME => "SYNO.SurveillanceStation.Recording", mk => 1 }, # This API provides method to query recording information. +); return \%hapi; } @@ -99,8 +105,8 @@ return \%hapi; ######################################################################## sub _staticChat { my %hapi = ( - INFO => { NAME => "SYNO.API.Info" }, - EXTERNAL => { NAME => "SYNO.Chat.External" }, + INFO => { NAME => "SYNO.API.Info", mk => 1 }, + EXTERNAL => { NAME => "SYNO.Chat.External", mk => 1 }, ); return \%hapi; @@ -111,12 +117,12 @@ return \%hapi; ######################################################################## sub _staticCalendar { my %hapi = ( - INFO => { NAME => "SYNO.API.Info" }, - AUTH => { NAME => "SYNO.API.Auth" }, # API used to perform session login and logout - CAL => { NAME => "SYNO.Cal.Cal" }, # API to manipulate calendar - EVENT => { NAME => "SYNO.Cal.Event" }, # Provide methods to manipulate events in the specific calendar - SHARE => { NAME => "SYNO.Cal.Sharing" }, # Get/set sharing setting of calendar - TODO => { NAME => "SYNO.Cal.Todo" }, # Provide methods to manipulate events in the specific calendar + INFO => { NAME => "SYNO.API.Info", mk => 1 }, + AUTH => { NAME => "SYNO.API.Auth", mk => 1 }, # API used to perform session login and logout + CAL => { NAME => "SYNO.Cal.Cal", mk => 1 }, # API to manipulate calendar + EVENT => { NAME => "SYNO.Cal.Event", mk => 1 }, # Provide methods to manipulate events in the specific calendar + SHARE => { NAME => "SYNO.Cal.Sharing", mk => 1 }, # Get/set sharing setting of calendar + TODO => { NAME => "SYNO.Cal.Todo", mk => 1 }, # Provide methods to manipulate events in the specific calendar ); return \%hapi; @@ -127,27 +133,27 @@ return \%hapi; ######################################################################## sub _staticFile { my %hapi = ( - INFO => { NAME => "SYNO.API.Info" }, - AUTH => { NAME => "SYNO.API.Auth" }, # Perform login and logout - FSINFO => { NAME => "SYNO.FileStation.Info" }, # Provide File Station info - LIST => { NAME => "SYNO.FileStation.List" }, # List all shared folders, enumerate files in a shared folder, and get detailed file information - SEARCH => { NAME => "SYNO.FileStation.Search" }, # Search files on given criteria - LVFOLDER => { NAME => "SYNO.FileStation.VirtualFolder" }, # List all mount point folders of virtual file system, ex: CIFS or ISO - FAVORITE => { NAME => "SYNO.FileStation.Favorite" }, # Add a folder to user’s favorites or do operations on user’s favorites - THUMB => { NAME => "SYNO.FileStation.Thumb" }, # Get a thumbnail of a file - DIRSIZE => { NAME => "SYNO.FileStation.DirSize" }, # Get the total size of files/folders within folder(s) - MD5 => { NAME => "SYNO.FileStation.MD5" }, # Get MD5 of a file - CHECKPERM => { NAME => "SYNO.FileStation.CheckPermission" }, # Check if the file/folder has a permission of a file/folder or not - UPLOAD => { NAME => "SYNO.FileStation.Upload" }, # Upload a file - DOWNLOAD => { NAME => "SYNO.FileStation.Download" }, # Download files/folders - SHARING => { NAME => "SYNO.FileStation.Sharing" }, # Generate a sharing link to share files/folders with other people and perform operations on sharing links - CFOLDER => { NAME => "SYNO.FileStation.CreateFolder" }, # Create folder(s) - RENAME => { NAME => "SYNO.FileStation.Rename" }, # Rename a file/folder - COPYMOVE => { NAME => "SYNO.FileStation.CopyMove" }, # Copy/Move files/folders - DELETE => { NAME => "SYNO.FileStation.Delete" }, # Delete files/folders - EXTRACT => { NAME => "SYNO.FileStation.Extract" }, # Extract an archive and do operations on an archive - COMPRESS => { NAME => "SYNO.FileStation.Compress" }, # Compress files/folders - BGTASK => { NAME => "SYNO.FileStation.BackgroundTask" }, # Get information regarding tasks of file operations which are run as the background process including copy, move, delete, compress and extract tasks or perform operations on these background tasks + INFO => { NAME => "SYNO.API.Info", mk => 1 }, + AUTH => { NAME => "SYNO.API.Auth", mk => 1 }, # Perform login and logout + FSINFO => { NAME => "SYNO.FileStation.Info", mk => 1 }, # Provide File Station info + LIST => { NAME => "SYNO.FileStation.List", mk => 1 }, # List all shared folders, enumerate files in a shared folder, and get detailed file information + SEARCH => { NAME => "SYNO.FileStation.Search", mk => 1 }, # Search files on given criteria + LVFOLDER => { NAME => "SYNO.FileStation.VirtualFolder", mk => 1 }, # List all mount point folders of virtual file system, ex: CIFS or ISO + FAVORITE => { NAME => "SYNO.FileStation.Favorite", mk => 1 }, # Add a folder to user’s favorites or do operations on user’s favorites + THUMB => { NAME => "SYNO.FileStation.Thumb", mk => 1 }, # Get a thumbnail of a file + DIRSIZE => { NAME => "SYNO.FileStation.DirSize", mk => 1 }, # Get the total size of files/folders within folder(s) + MD5 => { NAME => "SYNO.FileStation.MD5", mk => 1 }, # Get MD5 of a file + CHECKPERM => { NAME => "SYNO.FileStation.CheckPermission", mk => 1 }, # Check if the file/folder has a permission of a file/folder or not + UPLOAD => { NAME => "SYNO.FileStation.Upload", mk => 1 }, # Upload a file + DOWNLOAD => { NAME => "SYNO.FileStation.Download", mk => 1 }, # Download files/folders + SHARING => { NAME => "SYNO.FileStation.Sharing", mk => 1 }, # Generate a sharing link to share files/folders with other people and perform operations on sharing links + CFOLDER => { NAME => "SYNO.FileStation.CreateFolder", mk => 1 }, # Create folder(s) + RENAME => { NAME => "SYNO.FileStation.Rename", mk => 1 }, # Rename a file/folder + COPYMOVE => { NAME => "SYNO.FileStation.CopyMove", mk => 1 }, # Copy/Move files/folders + DELETE => { NAME => "SYNO.FileStation.Delete", mk => 1 }, # Delete files/folders + EXTRACT => { NAME => "SYNO.FileStation.Extract", mk => 1 }, # Extract an archive and do operations on an archive + COMPRESS => { NAME => "SYNO.FileStation.Compress", mk => 1 }, # Compress files/folders + BGTASK => { NAME => "SYNO.FileStation.BackgroundTask", mk => 1 }, # Get information regarding tasks of file operations which are run as the background process including copy, move, delete, compress and extract tasks or perform operations on these background tasks ); return \%hapi; diff --git a/fhem/contrib/DS_Starter/SMUtils.pm b/fhem/contrib/DS_Starter/SMUtils.pm new file mode 100644 index 000000000..a4195ccaf --- /dev/null +++ b/fhem/contrib/DS_Starter/SMUtils.pm @@ -0,0 +1,1927 @@ +######################################################################################################################## +# $Id: SMUtils.pm 24885 2021-08-29 07:42:07Z DS_Starter $ +######################################################################################################################### +# SMUtils.pm +# +# (c) 2020-2022 by Heiko Maaz +# e-mail: Heiko dot Maaz at t-online dot de +# +# This Module provides routines for FHEM modules developed for Synology use cases. +# +# 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 . +# +######################################################################################################################### + +# Version History +# 1.23.1 correct version format +# 1.23.0 new sub evalDecodeJSON +# 1.22.0 new sub addCHANGED +# 1.21.0 new sub timestringToTimestamp / createReadingsFromArray +# 1.20.7 change to defined ... in sub _addSendqueueSimple +# 1.20.6 delete $hash->{OPMODE} in checkSendRetry + +package FHEM::SynoModules::SMUtils; + +use strict; +use warnings; +use utf8; +use MIME::Base64; +use Time::HiRes qw(gettimeofday); +eval "use JSON;1;" or my $nojsonmod = 1; ## no critic 'eval' +use Data::Dumper; +use Encode; + +# use lib qw(/opt/fhem/FHEM /opt/fhem/lib); # für Syntaxcheck mit: perl -c /opt/fhem/lib/FHEM/SynoModules/SMUtils.pm + +use FHEM::SynoModules::ErrCodes qw(:all); # Error Code Modul +use GPUtils qw( GP_Import GP_Export ); +use Carp qw(croak carp); + +use version 0.77; our $VERSION = version->declare('1.23.2'); + +use Exporter ('import'); +our @EXPORT_OK = qw( + getClHash + delClHash + delReadings + createReadingsFromArray + addCHANGED + trim + slurpFile + moduleVersion + sortVersion + showModuleInfo + convertHashToTable + jboolmap + smUrlEncode + plotPngToFile + completeAPI + ApiVal + showAPIinfo + setCredentials + getCredentials + showStoredCredentials + evaljson + evalDecodeJSON + login + logout + setActiveToken + delActiveToken + delCallParts + setReadingErrorNone + setReadingErrorState + addSendqueue + listSendqueue + startFunctionDelayed + checkSendRetry + purgeSendqueue + updQueueLength + timestringToTimestamp + ); + +our %EXPORT_TAGS = (all => [@EXPORT_OK]); + +# Run before module compilation +BEGIN { + # Import from main:: + GP_Import( + qw( + attr + AttrVal + asyncOutput + Log3 + data + defs + modules + CancelDelayedShutdown + devspec2array + FmtDateTime + fhemTimeLocal + setKeyValue + getKeyValue + InternalTimer + plotAsPng + RemoveInternalTimer + ReadingsVal + ReadingsTimestamp + readingsSingleUpdate + readingsBeginUpdate + readingsBulkUpdate + readingsBulkUpdateIfChanged + readingsEndUpdate + readingsDelete + HttpUtils_NonblockingGet + ) + ); +}; + +# Standardvariablen +my $splitdef = ":"; # Standard Character für split ... + +my $carpnohash = "got no hash value"; +my $carpnoname = "got no name value"; +my $carpnoctyp = "got no Credentials type code"; +my $carpnoapir = "got no API Hash reference"; +my $carpnotfn = "got no function name"; +my $carpnotfarg = "got no Timer function argument"; +my $carpnoaddr = "got no server address from hash"; +my $carpnoport = "got no server port from hash"; +my $carpnoprot = "got no protocol from hash"; + +my %hasqhandler = ( # Hash addSendqueue Handler + SSCal => { fn => \&_addSendqueueSimple, }, + SSFile => { fn => \&_addSendqueueSimple, }, + SSChatBot => { fn => \&_addSendqueueExtended, }, +); + +############################################################################### +# Clienthash übernehmen oder zusammenstellen +# Identifikation ob über FHEMWEB ausgelöst oder nicht -> erstellen $hash->CL +############################################################################### +sub getClHash { + my $hash = shift // carp $carpnohash && return; + my $nobgd = shift; + 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; + } + + 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; + + for my $outdev (@webdvs) { + 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}; + } + + if (defined($hash->{HELPER}{CL}{1})) { # Clienthash auflösen zur Fehlersuche (aufrufende FHEMWEB Instanz) + 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 // q{}; + 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; +} + +#################################################################################### +# Clienthash löschen +#################################################################################### +sub delClHash { + my $name = shift; + my $hash = $defs{$name}; + + delete($hash->{HELPER}{CL}); + +return; +} + +#################################################################################### +# alle Readings außer excludierte löschen +# $respts -> Respect Timestamp +# wenn gesetzt, wird Reading nicht gelöscht +# wenn Updatezeit identisch zu "lastUpdate" +#################################################################################### +sub delReadings { + my $name = shift // carp $carpnoname && return; + my $respts = shift; + + my $hash = $defs{$name}; + my $type = $hash->{TYPE}; + + my ($lu,$rts,$excl); + + $excl = "Error|Errorcode|QueueLength|state|nextUpdate"; # Blacklist + $excl .= "|lastUpdate" if($respts); + + my @allrds = keys%{$defs{$name}{READINGS}}; + for my $key(@allrds) { + if($respts) { + $lu = $data{$type}{$name}{lastUpdate}; + $rts = ReadingsTimestamp($name, $key, $lu); + next if($rts eq $lu); + } + readingsDelete($hash, $key) if($key !~ m/^$excl$/x); + } + +return; +} + +############################################################################### +# Leerzeichen am Anfang / Ende eines strings entfernen +############################################################################### +sub trim { + my $str = shift; + + return if(!$str); + + $str =~ s/^\s+|\s+$//gx; + +return $str; +} + +############################################################################### +# File in einem Gang einlesen (schlürfen) +############################################################################### +sub slurpFile { + my $name = shift // carp $carpnoname && return 417; + my $file = shift // carp "got no filename to slurp" && return 417; + + my $errorcode = 0; + my $content = q{}; + my $fh; + + open $fh, "<", encode("iso_8859_1", "$file") or do { Log3($name, 2, qq{$name - cannot open local File "$file": $!}); + close ($fh) if($fh); + $errorcode = 9002; + }; + if(!$errorcode) { + local $/ = undef; # enable slurp mode, locally + $content = <$fh>; + + close ($fh); + } + +return ($errorcode, $content); +} + +############################################################################### +# einen Zeitstring YYYY-MM-TT hh:mm:ss in einen Unix +# Timestamp umwandeln +############################################################################### +sub timestringToTimestamp { + my $hash = shift // carp $carpnohash && return; + my $tstring = shift // carp "got no time string to convert" && return; + my $name = $hash->{NAME}; + + my($y, $mo, $d, $h, $m, $s) = $tstring =~ /([0-9]{4})-([0-9]{2})-([0-9]{2})\s([0-9]{2}):([0-9]{2}):([0-9]{2})/xs; + return if(!$mo || !$y); + + my $timestamp = fhemTimeLocal($s, $m, $h, $d, $mo-1, $y-1900); + +return $timestamp; +} + +############################################################################### +# Readings aus Array erstellen +# $daref: Referenz zum Array der zu erstellenden Readings +# muß Paare : enthalten +# $doevt: 1-Events erstellen, 0-keine Events erstellen +############################################################################### +sub createReadingsFromArray { + my $hash = shift // carp $carpnohash && return; + my $daref = shift // carp "got no reading array reference" && return; + my $doevt = shift // 0; + + readingsBeginUpdate($hash); + + for my $elem (@$daref) { + my ($rn,$rval) = split ":", $elem, 2; + readingsBulkUpdate($hash, $rn, $rval); + } + + readingsEndUpdate($hash, $doevt); + +return; +} + +################################################################ +# Zusätzliche Events im CHANGED Hash eintragen +# $val - Wert für Trigger Event +# $ts - Timestamp für Trigger Event +################################################################ +sub addCHANGED { + my $hash = shift // carp $carpnohash && return; + my $val = shift // carp "got no value for event trigger" && return; + my $ts = shift // carp "got no timestamp for event trigger" && return; + + if($hash->{CHANGED}) { + push @{$hash->{CHANGED}}, $val; + } + else { + $hash->{CHANGED}[0] = $val; + } + + if($hash->{CHANGETIME}) { + push @{$hash->{CHANGETIME}}, $ts; + } + else { + $hash->{CHANGETIME}[0] = $ts; + } + +return; +} + +############################################################################################# +# liefert die Versionierung des Moduls zurück +# Verwendung mit Packages: use version 0.77; our $VERSION = moduleVersion ($params) +# Verwendung ohne Packages: moduleVersion ($params) +# +# Die Verwendung von Meta.pm und Packages wird berücksichtigt +# +# Variablen $useAPI, $useSMUtils, $useErrCodes enthalten die Versionen von SynoModules +# wenn verwendet und sind in diesem Fall zu übergeben. +# +# Beispiel für Übergabe Parameter: +# my $params = { +# hash => $hash, +# notes => \%vNotesIntern, +# useAPI => 1, +# useSMUtils => 1, +# useErrCodes => 1 +# }; +############################################################################################# +sub moduleVersion { + my $paref = shift; + my $hash = $paref->{hash} // carp $carpnohash && return; + my $notes = $paref->{notes} // carp "got no reference of a version hash" && return; + my $useAPI = $paref->{useAPI}; + my $useSMUtils = $paref->{useSMUtils}; + my $useErrCodes = $paref->{useErrCodes}; + + my $type = $hash->{TYPE}; + my $package = (caller)[0]; # das PACKAGE des aufrufenden Moduls + + $hash->{HELPER}{VERSION_API} = $useAPI ? FHEM::SynoModules::API->VERSION() : "unused"; + $hash->{HELPER}{VERSION_SMUtils} = $useSMUtils ? FHEM::SynoModules::SMUtils->VERSION() : "unused"; + $hash->{HELPER}{VERSION_ErrCodes} = $useErrCodes ? FHEM::SynoModules::ErrCodes->VERSION() : "unused"; + + my $v = (sortVersion("desc",keys %{$notes}))[0]; # die Modulversion aus Versionshash selektieren + $hash->{HELPER}{VERSION} = $v; + $hash->{HELPER}{PACKAGE} = $package; + + 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{}{META}} + + if($modules{$type}{META}{x_version}) { # {x_version} nur gesetzt wenn $Id: SMUtils.pm 24885 2021-08-29 07:42:07Z DS_Starter $ im Kopf komplett! vorhanden + $modules{$type}{META}{x_version} =~ s/1\.1\.1/$v/gx; + } + else { + $modules{$type}{META}{x_version} = $v; + } + + FHEM::Meta::SetInternals($hash); # FVERSION wird gesetzt ( nur gesetzt wenn $Id: SMUtils.pm 24885 2021-08-29 07:42:07Z DS_Starter $ im Kopf komplett! vorhanden ) + } + else { # herkömmliche Modulstruktur + $hash->{VERSION} = $v; # Internal VERSION setzen + } + + if($package =~ /FHEM::$type/x || $package eq $type) { # es wird mit Packages gearbeitet -> mit {->VERSION()} im FHEMWEB kann Modulversion abgefragt werden + return $v; + } + +return; +} + +################################################################ +# sortiert eine Liste von Versionsnummern x.x.x +# Schwartzian Transform and the GRT transform +# Übergabe: "asc | desc", +################################################################ +sub sortVersion { + my ($sseq,@versions) = @_; + + my @sorted = map {$_->[0]} + sort {$a->[1] cmp $b->[1]} + map {[$_, pack "C*", split /\./x]} @versions; + + @sorted = map {join ".", unpack "C*", $_} + sort + map {pack "C*", split /\./x} @versions; + + if($sseq eq "desc") { + @sorted = reverse @sorted; + } + +return @sorted; +} + +############################################################################################# +# Gibt die erste Key-Ebene eines Hash als Tabelle formatiert zurück +# $headl: Überschrift über Tabelle +# $thead: String der Elemente des Tabellenkopfes (Komma getrennt), z.B. +# "local Object,remote Object,Date,Time" +# $datah: Referenz zum Hashobjekt mit Daten zur Konvertierung in eine Tabelle +############################################################################################# +sub convertHashToTable { + my $paref = shift; + my $hash = $paref->{hash} // carp $carpnohash && return; + my $datah = $paref->{datah} // carp "got no hash ref of data for table convert" && return; + my $headl = $paref->{headl} // q{}; + my $thead = $paref->{thead} // q{}; + + my $name = $hash->{NAME}; + + my $sub = sub { + my $idx = shift; + my @ret; + for my $key (sort keys %{$datah->{$idx}}) { + push @ret, $datah->{$idx}{$key}; + } + return @ret; + }; + + my $out = ""; + $out .= "
$headl
"; + $out .= ""; + $out .= ""; + $out .= ""; + + if ($thead) { + my @hd = split ",", $thead; + for my $elem (@hd) { + $out .= ""; + } + } + + $out .= ""; + + my $i = 0; + for my $idx (sort keys %{$datah}) { + my @sq = $sub->($idx); + next if(!@sq); + + if ($i & 1) { # $i ist ungerade + $out .= ""; + } + else { + $out .= ""; + } + $i++; + + $out .= ""; + + for my $he (@sq) { + $out .= ""; + } + + $out .= ""; + } + + $out .= ""; + $out .= "
$elem
$idx $he
"; + $out .= "
"; + $out .= ""; + +return $out; +} + +############################################################################################# +# gibt die angeforderten Hinweise / Release Notes als +# HTML-Tabelle zurück +############################################################################################# +sub showModuleInfo { + my $paref = shift; + my $arg = $paref->{arg}; + my $vHintsExt_de = $paref->{hintextde}; # Referenz zum deutschen Hinweis-Hash + my $vHintsExt_en = $paref->{hintexten}; # Referenz zum englischen Hinweis-Hash + my $vNotesExtern = $paref->{notesext}; # Referenz zum Hash der Modul Release Notes + + my $header = "Module release information
"; + my $header1 = "Helpful hints
"; + my $ret = ""; + + my (%hs,$val0,$val1,$i); + + $ret = ""; + + # Hints + if(!$arg || $arg =~ /hints/x || $arg =~ /[\d]+/x) { + $ret .= sprintf("
$header1
"); + $ret .= ""; + $ret .= ""; + $ret .= ""; + + if($arg && $arg =~ /[\d]+/x) { + my @hints = split ",", $arg; + + for my $hint (@hints) { + if(AttrVal("global","language","EN") eq "DE") { + $hs{$hint} = $vHintsExt_de->{$hint}; + } + else { + $hs{$hint} = $vHintsExt_en->{$hint}; + } + } + } + else { + if(AttrVal("global","language","EN") eq "DE") { + %hs = %{$vHintsExt_de}; + } + else { + %hs = %{$vHintsExt_en}; + } + } + + $i = 0; + for my $key (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/x) { + $ret .= sprintf("
$header
"); + $ret .= ""; + $ret .= ""; + $ret .= ""; + + $i = 0; + for my $key (sortVersion("desc", keys %{$vNotesExtern})) { + ($val0,$val1) = split /\s/x, $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; +} + +############################################################################### +# JSON Boolean Test und Mapping +# $var = Variante der boolean Auswertung: +# "char": Rückgabe von true / false für wahr / falsch +# "bin" : Rückgabe von 1 / 0 für wahr / falsch +############################################################################### +sub jboolmap { + my $bool = shift // carp "got no value to check if bool" && return; + my $var = shift // "char"; + + my $true = ($var eq "char") ? "true" : 1; + my $false = ($var eq "char") ? "false" : 0; + + my $is_boolean = JSON::is_bool($bool); + + if($is_boolean) { + $bool = $bool ? $true : $false; + } + +return $bool; +} + +############################################################################################# +# Zeichen URL encoden +# $str : der zu formatierende String +############################################################################################# +sub smUrlEncode { + my $str = shift // carp "got no string for URL encoding" && return; + + my $hextourl = { map { sprintf("\\x{%02x}", $_) => sprintf( "%%%02X", $_ ) } ( 0 ... 255 ) }; # Standard Hex Codes zu UrlEncode, z.B. \x{c2}\x{b6} -> %C2%B6 -> ¶ + + my $replacements = { + "#" => "%23", + "&" => "%26", + "%" => "%25", + "+" => "%2B", + " " => "%20", + }; + + %$replacements = (%$replacements, %$hextourl); + my $pat = join '|', map { quotemeta; } keys(%$replacements); + + $str =~ s/($pat)/$replacements->{$1}/xg; + +return $str; +} + +#################################################################################### +# Ausgabe der SVG-Funktion "plotAsPng" in eine Datei schreiben +# Die Datei wird im Verzeichnis "/opt/fhem/www/images" erstellt +#################################################################################### +sub plotPngToFile { + my $name = shift; + my $svg = shift; + my $hash = $defs{$name}; + my $file = $name."_SendPlot.png"; + my $path = $attr{global}{modpath}."/www/images"; + my $err = ""; + + my @options = split ",", $svg; + my $svgdev = $options[0]; + my $zoom = $options[1]; + my $offset = $options[2]; + + if(!$defs{$svgdev}) { + $err = qq{SVG device "$svgdev" doesn't exist}; + Log3($name, 1, "$name - ERROR - $err !"); + + setReadingErrorState ($hash, $err); + return $err; + } + + open (my $FILE, ">", "$path/$file") or do { + $err = qq{>PlotToFile< can't open $path/$file for write access}; + Log3($name, 1, "$name - ERROR - $err !"); + setReadingErrorState ($hash, $err); + return $err; + }; + binmode $FILE; + print $FILE plotAsPng(@options); + close $FILE; + +return ($err, $file); +} + +############################################################################### +# vervollständige das übergebene API-Hash mit den Werten aus $data der +# JSON-Antwort +# $jdata: Referenz zum $data-Hash der JSON-Antwort +# $apiref: Referenz zum instanziierten API-Hash +############################################################################### +sub completeAPI { + my $jdata = shift // carp "got no data Hash reference" && return; + my $apiref = shift // carp $carpnoapir && return; + + for my $key (keys %{$apiref}) { + next if($key =~ /^PARSET$/x); + $apiref->{$key}{PATH} = $jdata->{data}{$apiref->{$key}{NAME}}{path}; + $apiref->{$key}{VER} = $jdata->{data}{$apiref->{$key}{NAME}}{maxVersion}; + $apiref->{$key}{MOD} = "no"; # MOD = Version nicht modifiziert + + my $incomplete = defined $apiref->{$key}{PATH} ? 0 : 1; + return if(failInc ($apiref->{$key}{mk}, $incomplete)); + } + + $apiref->{PARSET} = 1; # alle API Hash values erfolgreich gesetzt + +return 1; +} + +############################################################################### +# API Key must include Check +############################################################################### +sub failInc { + my $mk = shift; + my $incomplete = shift; + + if ($incomplete && $mk) { + return 1; + } + +return; +} + +############################################################################### +# liefert den Wert eines API-Keys +# +# $apihash : Hash der API-Keys +# $key : MOD - Modifizierung des API-Keys +# NAME - API-Name +# PATH - API-Pfad +# VER - API-Version +# mk - Muß-Key ? -> 0 = optional, 1 = muß +# $def : default-Wert +############################################################################### +sub ApiVal { + my $hash = shift; + my $apihash = shift; + my $key = shift; + my $def = shift; + + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + + if(defined ($apihash) && + defined ($apihash->{$key})) { + return $apihash->{$key}; + } + +return $def; +} + +############################################################################### +# zeigt den Inhalt des verwendeten API Hash als Popup +# $apiref: Referenz zum instanziierten API-Hash +############################################################################### +sub showAPIinfo { + my $hash = shift // carp $carpnohash && return; + my $apiref = shift // carp $carpnoapir && return; + + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + + my $out = ""; + $out .= "Synology $type API Info

"; + $out .= ""; + $out .= ""; + $out .= ""; + + for my $key (sort keys %{$apiref}) { + next if($key =~ /^PARSET$/x); + my $apiname = ApiVal ($hash, $apiref->{$key}, 'NAME', ''); + my $apipath = ApiVal ($hash, $apiref->{$key}, 'PATH', ''); + my $apiver = ApiVal ($hash, $apiref->{$key}, 'VER', ''); + my $apimod = ApiVal ($hash, $apiref->{$key}, 'MOD', ''); + my $mk = ApiVal ($hash, $apiref->{$key}, 'mk', ''); + + $out .= ""; + $out .= ""; + $out .= ""; + $out .= ""; + $out .= ""; + $out .= ""; + $out .= ""; + } + + $out .= "
API Path Version MustKey Modified
$apiname $apipath $apiver $mk $apimod
"; + $out .= ""; + + asyncOutput($hash->{HELPER}{CL}{1},"$out"); + delClHash ($name); + +return; +} + +###################################################################################### +# Credentials / Token speichern +# $ctc = Credentials type code: +# "credentials" -> Standard Credentials +# "SMTPcredentials" -> Credentials für Mailversand +# "botToken" -> einen Token speichern +# $sep = Separator zum Split des $credstr, default ":" +###################################################################################### +sub setCredentials { + my $hash = shift // carp $carpnohash && return; + my $ctc = shift // carp $carpnoctyp && return; + my $cred = shift // carp "got no user name or Token" && return; + my $pass = shift; + my $sep = shift // $splitdef; + + if(!$pass && $ctc ne "botToken") { # botToken hat kein Paßwort + carp "got no password"; + return; + } + + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + + my ($success,$credstr); + + if($ctc eq "botToken") { + $credstr = _enscramble( encode_base64 ($cred) ); + } + else { + $credstr = _enscramble( encode_base64 ($cred.$sep.$pass) ); + } + + my $index = $type."_".$name."_".$ctc; + my $retcode = setKeyValue($index, $credstr); + + if ($retcode) { + Log3($name, 2, "$name - Error while saving the Credentials or Token - $retcode"); + $success = 0; + } + else { + getCredentials($hash,1,$ctc,$sep); # Credentials nach Speicherung lesen und in RAM laden ($boot=1), $ao = credentials oder SMTPcredentials + $success = 1; + } + +return $success; +} + +############################################################################### +# verscrambelt einen String +############################################################################### +sub _enscramble { + my $sstr = shift // carp "got no string to scramble" && return; + + my @key = qw(1 3 4 5 6 3 2 1 9); + my $len = scalar @key; + my $i = 0; + my $dstr = join "", map { $i = ($i + 1) % $len; chr((ord($_) + $key[$i]) % 256) } split //, $sstr; ## no critic 'Map blocks'; + +return $dstr; +} + +###################################################################################### +# gespeicherte Credentials dekodiert anzeigen +# +# $coc = Wert der anzuzeigenden Credentials (Code of Credentials) +# Wert 1 : Credentials Synology (default) +# Wert 2 : SMTP Credentials +# Wert 4 : Token +# +# $splitstr = String zum Splitten innerhalb getCredentials, default ":" +###################################################################################### +sub showStoredCredentials { + my $hash = shift // carp $carpnohash && return; + my $coc = shift // 1; + my $splitstr = shift // $splitdef; + + my $out; + + my $tokval = 4; + my $smtpval = 2; + my $credval = 1; + + my $dotok = int( $coc /$tokval ); + my $dosmtp = int( ($coc-($dotok*$tokval)) /$smtpval ); + my $docred = int( ($coc-($dotok*$tokval)-($dosmtp*$smtpval)) /$credval ); + + if($docred) { + my ($success, $username, $passwd) = getCredentials($hash, 0, "credentials", $splitstr); # Credentials + + my $cd = $success ? + "Username: $username, Password: $passwd" : + "Credentials are not set or couldn't be read"; + + $out .= "Stored Credentials for access the Synology System:\n". + "==================================================\n". + "$cd \n"; + } + + if($dosmtp) { + my ($smtpsuccess, $smtpuname, $smtpword) = getCredentials($hash, 0 , "SMTPcredentials", $splitstr); # SMTP-Credentials + + my $csmtp = $smtpsuccess ? + "SMTP-Username: $smtpuname, SMTP-Password: $smtpword" : + "SMTP credentials are not set or couldn't be read"; + + $out .= "\n". + "Stored Credentials for access the SMTP Server:\n". + "==============================================\n". + "$csmtp \n"; + } + + if($dotok) { + my ($toksuccess, $token) = getCredentials($hash, 0 ,"botToken"); # Token + + my $ctok = $toksuccess ? + $token : + "Token is not set or couldn't be read"; + + $out .= "\n". + "Stored Token:\n". + "=============\n". + "$ctok \n"; + } + +return $out; +} + +###################################################################################### +# gespeicherte Credentials laden/abrufen +# $boot = 1 beim erstmaligen laden +# $ctc = Credentials type code: +# "credentials" -> Standard Credentials +# "SMTPcredentials" -> Credentials für Mailversand +# "botToken" -> gespeicherten Token abfragen +# $sep = Separator zum Split des $credstr, default ":" +###################################################################################### +sub getCredentials { + my $hash = shift // carp $carpnohash && return; + my $boot = shift; + my $ctc = shift // carp $carpnoctyp && return; + my $sep = shift // $splitdef; + + my $getFn = $boot ? \&_readCredOnBoot : \&_readCredFromCache; + +return &{$getFn} ($hash, $ctc, $sep); +} + +###################################################################################### +# Credentials initial beim Boot laden/abrufen +# +# $ctc = Credentials type code: +# "credentials" -> Standard Credentials +# "SMTPcredentials" -> Credentials für Mailversand +# "botToken" -> gespeicherten Token abfragen +# $sep = Separator zum Split des $credstr, default ":" +###################################################################################### +sub _readCredOnBoot { + my $hash = shift; + my $ctc = shift; + my $sep = shift; + + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + + my $sc = q{}; + + my $index = $type."_".$name."_".$ctc; + my ($err, $credstr) = getKeyValue($index); + + if($err) { + Log3($name, 2, "$name - ERROR - Unable to read $ctc from file: $err"); + return; + } + + if(!$credstr) { + return; + } + + if($ctc eq "botToken") { # beim Boot scrambled botToken in den RAM laden + $hash->{HELPER}{TOKEN} = $credstr; + $hash->{TOKEN} = "Set"; + return 1; + } + + my ($username, $passwd) = split "$sep", decode_base64( _descramble($credstr) ); + + if(!$username || !$passwd) { + ($err,$sc) = _getCredentialsFromHash ($hash, $ctc); # nur Error und Credetials Shortcut lesen ! + $err = $err ? $err : qq{possible problem in splitting with separator "$sep"}; + Log3($name, 2, "$name - ERROR - ".$sc." not successfully decoded: $err"); + return; + } + + if($ctc eq "credentials") { # beim Boot scrambled Credentials in den RAM laden + $hash->{HELPER}{CREDENTIALS} = $credstr; + $hash->{CREDENTIALS} = "Set"; # "Credentials" wird als Statusbit ausgewertet. Wenn nicht gesetzt -> Warnmeldung und keine weitere Verarbeitung + } + elsif ($ctc eq "SMTPcredentials") { # beim Boot scrambled Credentials in den RAM laden + $hash->{HELPER}{SMTPCREDENTIALS} = $credstr; + $hash->{SMTPCREDENTIALS} = "Set"; # "Credentials" wird als Statusbit ausgewertet. Wenn nicht gesetzt -> Warnmeldung und keine weitere Verarbeitung + } + else { + Log3($name, 2, "$name - ERROR - no shortcut found for Credential type code: $ctc"); + return; + } + +return 1; +} + +###################################################################################### +# Credentials aus Cache lesen und dekodieren +# +# $ctc = Credentials type code: +# "credentials" -> Standard Credentials +# "SMTPcredentials" -> Credentials für Mailversand +# "botToken" -> gespeicherten Token abfragen +# $sep = Separator zum Split des $credstr, default ":" +###################################################################################### +sub _readCredFromCache { + my $hash = shift; + my $ctc = shift; + my $sep = shift; + + my $name = $hash->{NAME}; + + my ($err,$sc,$credstr) = _getCredentialsFromHash ($hash, $ctc); + + if($err) { + Log3($name, 2, "$name - ERROR - ".$sc." not set in RAM ! $err"); + return; + } + + if(!$credstr) { + return; + } + + if($ctc eq "botToken") { + my $token = decode_base64( _descramble($credstr) ); + my $logtok = AttrVal($name, "showTokenInLog", "0") == 1 ? $token : "********"; + + Log3($name, 4, "$name - botToken read from RAM: $logtok"); + + return (1, $token); + } + + my ($username, $passwd) = split "$sep", decode_base64( _descramble($credstr) ); + + if(!$username || !$passwd) { + $err = qq{possible problem in splitting with separator "$sep"}; + Log3($name, 2, "$name - ERROR - ".$sc." not successfully decoded ! $err"); + + if($ctc eq "credentials") { + delete $hash->{CREDENTIALS}; + } + + return; + } + + my $logpw = AttrVal($name, "showPassInLog", 0) ? $passwd // "" : "********"; + + Log3($name, 4, "$name - ".$sc." read from RAM: $username $logpw"); + +return (1, $username, $passwd); +} + +############################################################################### +# entpackt einen mit _enscramble behandelten String +############################################################################### +sub _descramble { + my $sstr = shift // carp "got no string to descramble" && return; + + my @key = qw(1 3 4 5 6 3 2 1 9); + my $len = scalar @key; + my $i = 0; + my $dstr = join "", map { $i = ($i + 1) % $len; chr((ord($_) - $key[$i] + 256) % 256) } split //, $sstr; ## no critic 'Map blocks'; + +return $dstr; +} + +############################################################################### +# liefert Kürzel eines Credentials und den Credetialstring aus dem Hash +# $ctc = Credentials Type Code +# $sc = Kürzel / Shortcut +############################################################################### +sub _getCredentialsFromHash { + my $hash = shift // carp $carpnohash && return; + my $ctc = shift // carp "got no Credentials type code" && return; + + my $name = $hash->{NAME}; + + my $credstr = q{}; + my $sc = q{}; + my $found = 0; + my $err = "no shortcut found for Credential type code: $ctc"; + + if ($ctc eq "credentials") { + $err = q{}; + $found = 1; + $sc = q{Credentials}; + $credstr = $hash->{HELPER}{CREDENTIALS}; + } + elsif ($ctc eq "SMTPcredentials") { + $err = q{}; + $found = 1; + $sc = q{SMTP-Credentials}; + $credstr = $hash->{HELPER}{SMTPCREDENTIALS}; + } + elsif ($ctc eq "botToken") { + $err = q{}; + $found = 1; + $sc = q{Token}; + $credstr = $hash->{HELPER}{TOKEN}; + } + + if($found && !$credstr) { + Log3($name, 5, qq{$name - The stored value of $ctc is empty}); + } + +return ($err,$sc,$credstr); +} + +############################################################################### +# Test ob JSON-String vorliegt +############################################################################### +sub evaljson { + my $hash = shift // carp $carpnohash && return; + my $myjson = shift // carp "got no string for JSON test" && return; + my $OpMode = $hash->{OPMODE}; + my $name = $hash->{NAME}; + + my $success = 1; + + if($nojsonmod) { + $success = 0; + Log3($name, 1, "$name - ERROR: Perl module 'JSON' is missing. You need to install it."); + return ($success,$myjson); + } + + eval {decode_json($myjson)} or do { + if( ($hash->{HELPER}{RUNVIEW} && $hash->{HELPER}{RUNVIEW} =~ m/^live_.*hls$/x) || + $OpMode =~ m/^.*_hls$/x ) { # SSCam: HLS aktivate/deaktivate bringt kein JSON wenn bereits aktiviert/deaktiviert + Log3($name, 5, "$name - HLS-activation data return: $myjson"); + + if ($myjson =~ m/{"success":true}/x) { + $success = 1; + $myjson = '{"success":true}'; + } + } + else { + $success = 0; + + my $errorcode = "9000"; + my $error = expErrors($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + + setReadingErrorState ($hash, $error, $errorcode); + } + }; + +return ($success,$myjson); +} + +############################################################################### +# testet und decodiert einen übergebenen JSON-String +# Die dekodierten Daten werden zurück gegeben bzw. im +# SSCam-Kontext angepasst +############################################################################### +sub evalDecodeJSON { + my $hash = shift // carp $carpnohash && return; + my $myjson = shift // carp "got no string for JSON test" && return; + my $OpMode = $hash->{OPMODE}; + my $name = $hash->{NAME}; + + my $success = 1; + my $decoded = q{}; + + if($nojsonmod) { + $success = 0; + Log3($name, 1, "$name - ERROR: Perl module 'JSON' is missing. You need to install it."); + return ($success,$myjson); + } + + eval {$decoded = decode_json($myjson)} or do { + if( ($hash->{HELPER}{RUNVIEW} && $hash->{HELPER}{RUNVIEW} =~ m/^live_.*hls$/x) || + $OpMode =~ m/^.*_hls$/x ) { # SSCam: HLS aktivate/deaktivate bringt kein JSON wenn bereits aktiviert/deaktiviert + Log3($name, 5, "$name - HLS-activation data return: $myjson"); + + if ($myjson =~ m/{"success":true}/x) { + $success = 1; + $myjson = '{"success":true}'; + $decoded = decode_json($myjson); + } + } + else { + $success = 0; + $decoded = q{}; + + my $errorcode = "9000"; + my $error = expErrors($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + + setReadingErrorState ($hash, $error, $errorcode); + } + }; + +return ($success,$decoded); +} + +#################################################################################### +# Login wenn keine oder ungültige Session-ID vorhanden ist +# $apiref = Referenz zum API Hash +# $fret = Referenz zur Rückkehrfunktion nach erfolgreichen Login +# $fretarg = Argument für Rückkehrfunktion, default: $hash +# $sep = Separator für split Credentials in getCredentials, default ":" +#################################################################################### +sub login { + my $hash = shift // carp $carpnohash && return; + my $apiref = shift // carp $carpnoapir && return; + my $fret = shift // carp "got no return function reference" && return; + my $fretarg = shift // $hash; + my $sep = shift // $splitdef; + + my $serveraddr = $hash->{SERVERADDR} // carp $carpnoaddr && return; + my $serverport = $hash->{SERVERPORT} // carp $carpnoport && return; + my $proto = $hash->{PROTOCOL} // carp $carpnoprot && return; + my $name = $hash->{NAME}; + my $apiauth = $apiref->{AUTH}{NAME}; + my $apiauthpath = $apiref->{AUTH}{PATH}; + my $apiauthver = $apiref->{AUTH}{VER}; + my $type = $hash->{TYPE}; + + my ($url,$param,$urlwopw); + + delete $hash->{HELPER}{SID}; + + Log3($name, 4, "$name - --- Begin Function login ---"); + + my ($success, $username, $password) = getCredentials($hash,0,"credentials",$sep); # Credentials abrufen + + if (!$success) { + Log3($name, 2, qq{$name - Credentials couldn't be retrieved successfully - make sure you've set it with "set $name credentials "}); + delActiveToken($hash) if($type eq "SSCam"); + return; + } + + my $lrt = AttrVal($name,"loginRetries",3); + + if($hash->{HELPER}{LOGINRETRIES} >= $lrt) { # Max Versuche erreicht -> login wird abgebrochen, Freigabe Funktionstoken + delActiveToken($hash) if($type eq "SSCam"); + Log3($name, 2, "$name - ERROR - Login or privilege of user $username unsuccessful"); + return; + } + + my $timeout = AttrVal($name,"timeout",60); # Kompatibilität zu Modulen die das Attr "timeout" verwenden + my $httptimeout = AttrVal($name,"httptimeout",$timeout); + $httptimeout = 60 if($httptimeout < 60); + + Log3($name, 4, "$name - HTTP-Call login will be done with httptimeout-Value: $httptimeout s"); + + my $sid = AttrVal($name, "noQuotesForSID", 0) ? "sid" : qq{"sid"}; # sid in Quotes einschliessen oder nicht -> bei Problemen mit 402 - Permission denied + + if (AttrVal($name,"session","DSM") eq "DSM") { + $url = "$proto://$serveraddr:$serverport/webapi/$apiauthpath?api=$apiauth&version=$apiauthver&method=Login&account=$username&passwd=$password&format=$sid"; + $urlwopw = "$proto://$serveraddr:$serverport/webapi/$apiauthpath?api=$apiauth&version=$apiauthver&method=Login&account=$username&passwd=*****&format=$sid"; + } + else { + $url = "$proto://$serveraddr:$serverport/webapi/$apiauthpath?api=$apiauth&version=$apiauthver&method=Login&account=$username&passwd=$password&session=SurveillanceStation&format=$sid"; + $urlwopw = "$proto://$serveraddr:$serverport/webapi/$apiauthpath?api=$apiauth&version=$apiauthver&method=Login&account=$username&passwd=*****&session=SurveillanceStation&format=$sid"; + } + + my $printurl = AttrVal($name, "showPassInLog", 0) ? $url : $urlwopw; + + Log3($name, 4, "$name - Call-Out now: $printurl"); + $hash->{HELPER}{LOGINRETRIES}++; + + $param = { + url => $url, + timeout => $httptimeout, + hash => $hash, + user => $username, + fret => $fret, + fretarg => $fretarg, + sep => $sep, + apiref => $apiref, + method => "GET", + header => "Accept: application/json", + callback => \&_loginReturn + }; + + HttpUtils_NonblockingGet ($param); + +return; +} + +sub _loginReturn { + my $param = shift; + my $err = shift; + my $myjson = shift; + my $hash = $param->{hash}; + + my $name = $hash->{NAME}; + my $username = $param->{user}; + my $fret = $param->{fret}; + my $fretarg = $param->{fretarg}; + my $sep = $param->{sep}; + my $apiref = $param->{apiref}; + my $type = $hash->{TYPE}; + + my $success; + + 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 login($hash,$apiref,$fret,$fretarg,$sep); + + } elsif ($myjson ne "") { # wenn die Abfrage erfolgreich war ($data enthält die Ergebnisdaten des HTTP Aufrufes) + ($success) = evaljson($hash,$myjson); # Evaluiere ob Daten im JSON-Format empfangen wurden + + if (!$success) { + Log3($name, 4, "$name - no JSON-Data returned: ".$myjson); + delActiveToken($hash) if($type eq "SSCam"); + return; + } + + my $data = decode_json($myjson); + + Log3($name, 5, "$name - JSON decoded: ". Dumper $data); + + $success = $data->{'success'}; + + if ($success) { # login war erfolgreich + my $sid = $data->{'data'}->{'sid'}; + + $hash->{HELPER}{SID} = $sid; # Session ID in hash eintragen + + 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 &$fret($fretarg); + } + else { + my $errorcode = $data->{'error'}->{'code'}; # Errorcode aus JSON ermitteln + my $error = expErrorsAuth($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + + readingsBeginUpdate ($hash); + readingsBulkUpdate ($hash, "Errorcode", $errorcode ); + readingsBulkUpdate ($hash, "Error", $error ); + readingsBulkUpdate ($hash, "state", "login Error"); + readingsEndUpdate ($hash, 1); + + Log3($name, 3, "$name - Login of User $username unsuccessful. Code: $errorcode - $error - try again"); + + return login($hash,$apiref,$fret,$fretarg,$sep); + } + } + +return login($hash,$apiref,$fret,$fretarg,$sep); +} + +################################################################################### +# Funktion logout +# $apiref = Referenz zum API Hash +# $sep = Separator für split Credentials in getCredentials, default ":" +################################################################################### +sub logout { + my $hash = shift // carp $carpnohash && return; + my $apiref = shift // carp $carpnoapir && return; + my $sep = shift // $splitdef; + + my $name = $hash->{NAME}; + my $serveraddr = $hash->{SERVERADDR}; + my $serverport = $hash->{SERVERPORT}; + my $proto = $hash->{PROTOCOL}; + my $type = $hash->{TYPE}; + + my $apiauth = $apiref->{AUTH}{NAME}; + my $apiauthpath = $apiref->{AUTH}{PATH}; + my $apiauthver = $apiref->{AUTH}{VER}; + + my $sid = delete $hash->{HELPER}{SID} // q{}; + + my $url; + + Log3($name, 4, "$name - --- Start Synology logout ---"); + + my ($success, $username) = getCredentials($hash,0,"credentials",$sep); + + if(!$sid) { + if($username) { + Log3($name, 2, qq{$name - User "$username" has no valid session, logout is cancelled}); + } + + readingsBeginUpdate ($hash); + readingsBulkUpdate ($hash, "Errorcode", "none"); + readingsBulkUpdate ($hash, "Error", "none"); + readingsBulkUpdate ($hash, "state", "logout done"); + readingsEndUpdate ($hash, 1); + + delActiveToken ($hash) if($type eq "SSCam"); # ausgeführte Funktion ist erledigt (auch wenn logout nicht erfolgreich), Freigabe Funktionstoken + CancelDelayedShutdown ($name); + return; + } + + my $timeout = AttrVal($name,"timeout",60); + $timeout = 60 if($timeout < 60); + Log3($name, 5, "$name - Call logout will be done with timeout value: $timeout s"); + + if (AttrVal($name,"session","DSM") eq "DSM") { + $url = "$proto://$serveraddr:$serverport/webapi/$apiauthpath?api=$apiauth&version=$apiauthver&method=Logout&_sid=$sid"; + } + else { + $url = "$proto://$serveraddr:$serverport/webapi/$apiauthpath?api=$apiauth&version=$apiauthver&method=Logout&session=SurveillanceStation&_sid=$sid"; + } + + my $param = { + url => $url, + timeout => $timeout, + hash => $hash, + sid => $sid, + username => $username, + method => "GET", + header => "Accept: application/json", + callback => \&_logoutReturn + }; + + HttpUtils_NonblockingGet ($param); + +return; +} + +sub _logoutReturn { + my $param = shift; + my $err = shift; + my $myjson = shift; + my $hash = $param->{hash}; + my $sid = $param->{sid}; + my $username = $param->{username}; + + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + + if ($err ne "") { # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist + Log3($name, 2, "$name - error while requesting ".$param->{url}." - $err"); + readingsSingleUpdate($hash, "Error", $err, 1); + + } elsif ($myjson ne "") { # wenn die Abfrage erfolgreich war ($data enthält die Ergebnisdaten des HTTP Aufrufes) + Log3($name, 4, "$name - URL-Call: ".$param->{url}); + + my ($success) = evaljson($hash,$myjson); # Evaluiere ob Daten im JSON-Format empfangen wurden + + if (!$success) { + Log3($name, 4, "$name - Data returned: ".$myjson); + delActiveToken ($hash) if($type eq "SSCam"); + return; + } + + my $data = decode_json($myjson); + + Log3($name, 4, "$name - JSON returned: ". Dumper $data); + + $success = $data->{'success'}; + + if ($success) { # die Logout-URL konnte erfolgreich aufgerufen werden + readingsBeginUpdate ($hash); + readingsBulkUpdate ($hash, "Errorcode", "none"); + readingsBulkUpdate ($hash, "Error", "none"); + readingsBulkUpdate ($hash, "state", "logout done"); + readingsEndUpdate ($hash, 1); + + Log3($name, 2, qq{$name - Session of User "$username" terminated - session ID "$sid" deleted}); + } + else { + my $errorcode = $data->{'error'}->{'code'}; # Errorcode aus JSON ermitteln + my $error = expErrorsAuth($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + + Log3($name, 2, qq{$name - ERROR - Logout of User $username was not successful, however SID: "$sid" has been deleted. Errorcode: $errorcode - $error}); + } + } + + delActiveToken ($hash) if($type eq "SSCam"); # ausgeführte Funktion ist erledigt (auch wenn logout nicht erfolgreich), Freigabe Funktionstoken + CancelDelayedShutdown ($name); + +return; +} + +############################################################################################# +# Token setzen +############################################################################################# +sub setActiveToken { + my $hash = shift // carp $carpnohash && return; + my $name = $hash->{NAME}; + + $hash->{HELPER}{ACTIVE} = "on"; + + if (AttrVal($name,"debugactivetoken",0)) { + Log3($name, 1, "$name - Active-Token set by OPMODE: $hash->{OPMODE}"); + } + +return; +} + +############################################################################################# +# Token freigeben +############################################################################################# +sub delActiveToken { + my $hash = shift // carp $carpnohash && return; + my $name = $hash->{NAME}; + + $hash->{HELPER}{ACTIVE} = "off"; + + delCallParts ($hash); + + if (AttrVal($name,"debugactivetoken",0)) { + Log3($name, 1, "$name - Active-Token deleted by OPMODE: $hash->{OPMODE}"); + } + +return; +} + +############################################################################################# +# lösche Helper der erstellten CALL / ACALL Teile +# CALL / ACALL werden bei auslösen einer Aktion durch Set/Get erstellt +############################################################################################# +sub delCallParts { + my $hash = shift; + + delete $hash->{HELPER}{CALL}; + delete $hash->{HELPER}{ACALL}; + +return; +} + +############################################################################################# +# Readings Error & Errorcode auf +# Standard "none" setzen +# $evt: 1 -> Event, 0/nicht gesetzt -> kein Event +############################################################################################# +sub setReadingErrorNone { + my $hash = shift // carp $carpnohash && return; + my $evt = shift // 0; + + readingsBeginUpdate($hash); + readingsBulkUpdate ($hash, "Errorcode", "none"); + readingsBulkUpdate ($hash, "Error" , "none"); + readingsEndUpdate ($hash, $evt); + +return; +} + +#################################################################################### +# zentrale Funktion Error State in Readings setzen +# $error = Fehler als Text +# $errcode = Fehlercode +#################################################################################### +sub setReadingErrorState { + my $hash = shift // carp $carpnohash && return; + my $error = shift; + my $errcode = shift // "none"; + + readingsBeginUpdate($hash); + readingsBulkUpdate ($hash, "Error", $error); + readingsBulkUpdate ($hash, "Errorcode", $errcode); + readingsBulkUpdate ($hash, "state", "Error"); + readingsEndUpdate ($hash,1); + +return; +} + +###################################################################################### +# Eintrag an SendQueue des Modultyps anhängen +# die Unterroutinen werden in Abhängigkeit des auslösenden Moduls angesprungen +###################################################################################### +sub addSendqueue { + my $paref = shift; + my $name = $paref->{name} // carp $carpnoname && return; + + my $hash = $defs{$name}; + my $type = $hash->{TYPE}; + + if($hasqhandler{$type}) { + &{$hasqhandler{$type}{fn}} ($paref); + return; + } + + Log3($name, 1, qq{$name - ERROR - no module specific add Sendqueue handler for type "$type" found}); + +return; +} + +###################################################################################### +# Eintrag zur SendQueue hinzufügen (Standard Parametersatz ohne Prüfung) +# +# $name = Name (Kalender)device +# $opmode = operation mode +# $api = API-Referenz (z.B. $data{SSCal}{$name}{calapi}) +# $method = auszuführende API-Methode +# $params = spezifische API-Parameter für GET +# +# Weitere Parameter hinzufügen falls vorhanden. +###################################################################################### +sub _addSendqueueSimple { + my $paref = shift; + my $name = $paref->{name}; + my $opmode = $paref->{opmode}; + my $api = $paref->{api}; + my $method = $paref->{method}; + my $params = $paref->{params}; + my $dest = $paref->{dest}; + my $reqtype = $paref->{reqtype}; + my $header = $paref->{header}; + my $postdata = $paref->{postdata}; + my $lclFile = $paref->{lclFile}; + my $remFile = $paref->{remFile}; + my $remDir = $paref->{remDir}; + my $timeout = $paref->{timeout}; + + my $hash = $defs{$name}; + + my $entry = { + 'opmode' => $opmode, + 'api' => $api, + 'method' => $method, + 'retryCount' => 0 + }; + + # optionale Zusatzfelder + $entry->{params} = $params if(defined $params); + $entry->{dest} = $dest if(defined $dest); + $entry->{reqtype} = $reqtype if(defined $reqtype); + $entry->{header} = $header if(defined $header); + $entry->{postdata} = $postdata if(defined $postdata); + $entry->{lclFile} = $lclFile if(defined $lclFile); + $entry->{remFile} = $remFile if(defined $remFile); + $entry->{remDir} = $remDir if(defined $remDir); + $entry->{timeout} = $timeout if(defined $timeout); + + __addSendqueueEntry ($hash, $entry); # den Datensatz zur Sendqueue hinzufügen # updaten Länge der Sendequeue + +return; +} + +###################################################################################### +# Eintrag zur SendQueue hinzufügen (erweiterte Parameter mit Prüfung) +# +# $name = Name des Devices +# $opmode = operation Mode +# $method = auszuführende API-Methode +# $userid = ID des (Chat)users +# $text = zu übertragender Text +# $fileUrl = opt. zu übertragendes File +# $channel = opt. Channel +# +###################################################################################### +sub _addSendqueueExtended { + my $paref = shift; + my $name = $paref->{name}; + my $hash = $defs{$name}; + my $opmode = $paref->{opmode} // do {my $err = qq{internal ERROR -> opmode is empty}; Log3($name, 1, "$name - $err"); setReadingErrorState ($hash, $err); return}; + my $method = $paref->{method} // do {my $err = qq{internal ERROR -> method is empty}; Log3($name, 1, "$name - $err"); setReadingErrorState ($hash, $err); return}; + my $userid = $paref->{userid} // do {my $err = qq{internal ERROR -> userid is empty}; Log3($name, 1, "$name - $err"); setReadingErrorState ($hash, $err); return}; + my $text = $paref->{text}; + my $fileUrl = $paref->{fileUrl}; + my $channel = $paref->{channel}; + my $attachment = $paref->{attachment}; + + if(!$text && $opmode !~ /chatUserlist|chatChannellist|apiInfo/x) { + my $err = qq{can't add message to queue: "text" is empty}; + Log3($name, 2, "$name - ERROR - $err"); + + setReadingErrorState ($hash, $err); + + return; + } + + my $entry = { + 'opmode' => $opmode, + 'method' => $method, + 'userid' => $userid, + 'channel' => $channel, + 'text' => $text, + 'attachment' => $attachment, + 'fileUrl' => $fileUrl, + 'retryCount' => 0 + }; + + __addSendqueueEntry ($hash, $entry); # den Datensatz zur Sendqueue hinzufügen + +return; +} + +############################################################################################# +# fügt den Eintrag $entry zur Sendequeue hinzu +############################################################################################# +sub __addSendqueueEntry { + my $hash = shift // carp $carpnohash && return; + my $entry = shift // carp "got no entry for adding to send queue" && return; + my $name = $hash->{NAME}; + + my $type = $hash->{TYPE}; + + $data{$type}{$name}{sendqueue}{index}++; + my $index = $data{$type}{$name}{sendqueue}{index}; + + Log3($name, 5, "$name - Add Item to queue - Index $index: \n".Dumper $entry); + + $data{$type}{$name}{sendqueue}{entries}{$index} = $entry; + + updQueueLength ($hash, "", 0); # update Länge der Sendequeue ohne Event + +return; +} + +############################################################################################# +# liefert aktuelle Einträge der Sendequeue zurück +############################################################################################# +sub listSendqueue { + my $paref = shift; + my $hash = $paref->{hash} // carp $carpnohash && return; + my $name = $paref->{name} // carp $carpnoname && return; + + my $type = $hash->{TYPE}; + + my $sub = sub { + my $idx = shift; + my $ret; + for my $key (reverse sort keys %{$data{$type}{$name}{sendqueue}{entries}{$idx}}) { + $ret .= ", " if($ret); + $ret .= $key."=>".$data{$type}{$name}{sendqueue}{entries}{$idx}{$key}; + } + return $ret; + }; + + if (!keys %{$data{$type}{$name}{sendqueue}{entries}}) { + return qq{SendQueue is empty.}; + } + + my $sq; + for my $idx (sort{$a<=>$b} keys %{$data{$type}{$name}{sendqueue}{entries}}) { + $sq .= $idx." => ".$sub->($idx)."\n"; + } + +return $sq; +} + +############################################################################################# +# Funktion Zeitplan löschen und neu planen +# $rst = Zeit für Funktionseinplanung +# $startfn = Funktion (Name incl. Paket) deren Timer gelöscht und neu gestartet wird +# $arg = Argument für die Timer Funktion +############################################################################################# +sub startFunctionDelayed { + my $name = shift // carp $carpnoname && return; + my $rst = shift // carp "got no restart Timer value" && return; + my $startfn = shift // carp $carpnotfn && return; + my $arg = shift // carp $carpnotfarg && return; + + RemoveInternalTimer ($arg, $startfn); + InternalTimer ($rst, $startfn, $arg, 0); + +return; +} + +############################################################################################# +# Erfolg der Abarbeitung eines Queueeintrags checken und ggf. Retry ausführen +# bzw. den SendQueue-Eintrag bei Erfolg löschen +# $name = Name des Devices +# $retry = 0 -> Opmode erfolgreich (DS löschen), +# 1 -> Opmode nicht erfolgreich (Abarbeitung nach ckeck errorcode +# eventuell verzögert wiederholen) +# $startfn = Funktion (Name incl. Paket) die nach Check ggf. gestartet werden soll +############################################################################################# +sub checkSendRetry { + my $name = shift // carp $carpnoname && return; + my $retry = shift // carp "got no opmode state" && return; + my $startfn = shift // carp $carpnotfn && return; + my $hash = $defs{$name}; + my $idx = $hash->{OPIDX}; + my $opmode = $hash->{OPMODE}; + my $type = $hash->{TYPE}; + + $hash->{OPMODE} = q{}; + + my $forbidSend = q{}; + my $startfnref = \&{$startfn}; + + my @forbidlist = qw(100 101 103 117 120 400 401 407 408 409 410 414 418 419 420 800 900 + 1000 1001 1002 1003 1004 1006 1007 1100 1101 1200 1300 1301 1400 + 1401 1402 1403 1404 1405 1800 1801 1802 1803 1804 1805 2000 2001 + 2002 9002); # bei diesen Errorcodes den Queueeintrag nicht wiederholen, da dauerhafter Fehler ! + + if(!keys %{$data{$type}{$name}{sendqueue}{entries}}) { + Log3($name, 4, "$name - SendQueue is empty. Nothing to do ..."); + updQueueLength ($hash); + return; + } + + if(!$retry) { # Befehl erfolgreich, Senden nur neu starten wenn weitere Einträge in SendQueue + delete $hash->{OPIDX}; + delete $data{$type}{$name}{sendqueue}{entries}{$idx}; + Log3($name, 4, qq{$name - Opmode "$opmode" finished successfully, Sendqueue index "$idx" deleted.}); + updQueueLength ($hash); + + if(keys %{$data{$type}{$name}{sendqueue}{entries}}) { + Log3($name, 4, "$name - Start next SendQueue entry..."); + return &$startfnref ($name); # nächsten Eintrag abarbeiten (wenn SendQueue nicht leer) + } + } + else { # Befehl nicht erfolgreich, (verzögertes) Senden einplanen + $data{$type}{$name}{sendqueue}{entries}{$idx}{retryCount}++; + my $rc = $data{$type}{$name}{sendqueue}{entries}{$idx}{retryCount}; + + my $errorcode = ReadingsVal($name, "Errorcode", 0); + + if($errorcode =~ /119/x) { # Session wird neu requestet und Queue-Eintrag wiederholt + delete $hash->{HELPER}{SID}; + } + + if(grep { $_ eq $errorcode } @forbidlist) { + $forbidSend = expErrors($hash,$errorcode); # Fehlertext zum Errorcode ermitteln + $data{$type}{$name}{sendqueue}{entries}{$idx}{forbidSend} = $forbidSend; + + Log3($name, 2, qq{$name - ERROR - "$opmode" SendQueue index "$idx" not executed. It seems to be a permanent error. Exclude it from new send attempt !}); + + delete $hash->{OPIDX}; + + updQueueLength ($hash); # updaten Länge der Sendequeue + + return &$startfnref ($name); # nächsten Eintrag abarbeiten (wenn SendQueue nicht leer); + } + + if(!$forbidSend) { + my $rs = 0; + $rs = $rc <= 1 ? 5 + : $rc < 3 ? 20 + : $rc < 5 ? 60 + : $rc < 7 ? 1800 + : $rc < 30 ? 3600 + : 86400 + ; + + Log3($name, 2, qq{$name - ERROR - "$opmode" SendQueue index "$idx" not executed. Restart SendQueue in $rs s (retryCount $rc).}); + + my $rst = gettimeofday()+$rs; # resend Timer + updQueueLength ($hash, $rst); # updaten Länge der Sendequeue mit resend Timer + startFunctionDelayed ($name, $rst, $startfn, $name); + } + } + +return +} + +############################################################################################# +# löscht Einträge aus der Sendequeue +############################################################################################# +sub purgeSendqueue { + my $paref = shift; + my $hash = $paref->{hash} // carp $carpnohash && return; + my $name = $paref->{name} // carp $carpnoname && return; + my $prop = $paref->{prop} // carp "got no purgeSendqueue argument" && return; + + my $type = $hash->{TYPE}; + my $ret = q{}; + + if($prop eq "-all-") { + delete $hash->{OPIDX}; + delete $data{$type}{$name}{sendqueue}{entries}; + $data{$type}{$name}{sendqueue}{index} = 0; + $ret = "All entries of SendQueue are deleted"; + } + elsif($prop eq "-permError-") { + for my $idx (keys %{$data{$type}{$name}{sendqueue}{entries}}) { + delete $data{$type}{$name}{sendqueue}{entries}{$idx} + if($data{$type}{$name}{sendqueue}{entries}{$idx}{forbidSend}); + } + $ret = qq{All entries with state "permanent send error" are deleted}; + } + else { + delete $data{$type}{$name}{sendqueue}{entries}{$prop}; + $ret = qq{SendQueue entry with index "$prop" deleted}; + } + + updQueueLength ($hash); + +return $ret; +} + +############################################################################################# +# Länge Senedequeue updaten +# $rst: Resend Timestamp +# $evtt: Eventtyp 0 - kein Event +# 1 - immer Event (Standard) +# 2 - Event nur bei fallendem QueueLength-Zähler +# 3 - Event nur bei steigendem QueueLength-Zähler +############################################################################################# +sub updQueueLength { + my $hash = shift // carp $carpnohash && return; + my $rst = shift; + my $evtt = shift // 1; + + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $ql = keys %{$data{$type}{$name}{sendqueue}{entries}}; + + readingsDelete ($hash, "QueueLenth"); # entferne Reading mit Typo + + my $evt = $evtt; + my $oql = ReadingsVal($name, "QueueLength", 0); + + if ($evtt == 2) { # Events nur bei Herabzählen der Queue + $evt = $oql > $ql ? 1 : 0; + } + + if ($evtt == 3) { # Events nur bei Heraufzählen der Queue + $evt = $ql > $oql ? 1 : 0; + } + + readingsBeginUpdate ($hash); + readingsBulkUpdateIfChanged ($hash, "QueueLength", $ql); # Länge Sendqueue updaten + readingsEndUpdate ($hash, $evt); + + my $head = "next planned SendQueue start:"; + + $hash->{RESEND} = $rst ? $head." ".FmtDateTime($rst) : $head." immediately by next entry"; + +return; +} + +1; \ No newline at end of file