diff --git a/FHEM/73_GardenaSmartBridge.pm b/FHEM/73_GardenaSmartBridge.pm index 88bdc0b..19297c3 100644 --- a/FHEM/73_GardenaSmartBridge.pm +++ b/FHEM/73_GardenaSmartBridge.pm @@ -1,8 +1,8 @@ ############################################################################### # -# Developed with Kate +# Developed with VSCodium and richterger perl plugin. # -# (c) 2017-2021 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) +# (c) 2017-2022 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) # All rights reserved # # Special thanks goes to comitters: @@ -57,22 +57,20 @@ package FHEM::GardenaSmartBridge; use GPUtils qw(GP_Import GP_Export); - use strict; use warnings; use POSIX; use FHEM::Meta; -#use Data::Dumper; - use HttpUtils; my $missingModul = ''; -eval "use Encode qw(encode encode_utf8 decode_utf8);1" +eval { use Encode qw /encode_utf8 decode_utf8/; 1 } or $missingModul .= "Encode "; # eval "use JSON;1" || $missingModul .= 'JSON '; -eval "use IO::Socket::SSL;1" or $missingModul .= 'IO::Socket::SSL '; +eval { use IO::Socket::SSL; 1 } + or $missingModul .= 'IO::Socket::SSL '; # try to use JSON::MaybeXS wrapper # for chance of better performance + open code @@ -80,15 +78,11 @@ eval { require JSON::MaybeXS; import JSON::MaybeXS qw( decode_json encode_json ); 1; -}; - -if ($@) { - $@ = undef; +} or do { # try to use JSON wrapper # for chance of better performance eval { - # JSON preference order local $ENV{PERL_JSON_BACKEND} = 'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP' @@ -97,10 +91,7 @@ if ($@) { require JSON; import JSON qw( decode_json encode_json ); 1; - }; - - if ($@) { - $@ = undef; + } or do { # In rare cases, Cpanel::JSON::XS may # be installed but JSON|JSON::MaybeXS not ... @@ -108,10 +99,7 @@ if ($@) { require Cpanel::JSON::XS; import Cpanel::JSON::XS qw(decode_json encode_json); 1; - }; - - if ($@) { - $@ = undef; + } or do { # In rare cases, JSON::XS may # be installed but JSON not ... @@ -119,10 +107,7 @@ if ($@) { require JSON::XS; import JSON::XS qw(decode_json encode_json); 1; - }; - - if ($@) { - $@ = undef; + } or do { # Fallback to built-in JSON which SHOULD # be available since 5.014 ... @@ -130,20 +115,17 @@ if ($@) { require JSON::PP; import JSON::PP qw(decode_json encode_json); 1; - }; - - if ($@) { - $@ = undef; + } or do { # Fallback to JSON::backportPP in really rare cases require JSON::backportPP; import JSON::backportPP qw(decode_json encode_json); 1; - } - } - } - } -} + }; + }; + }; + }; +}; ## Import der FHEM Funktionen #-- Run before package compilation @@ -183,7 +165,7 @@ BEGIN { #-- Export to main context with different name GP_Export( qw( - Initialize + Initialize ) ); @@ -237,12 +219,10 @@ sub Define { my $name = shift @$aArg; $hash->{BRIDGE} = 1; $hash->{URL} = - AttrVal( $name, 'gardenaBaseURL', - 'https://smart.gardena.com' ) - . '/v1'; + AttrVal( $name, 'gardenaBaseURL', 'https://smart.gardena.com' ) . '/v1'; $hash->{VERSION} = version->parse($VERSION)->normal; $hash->{INTERVAL} = 60; - $hash->{NOTIFYDEV} = "global,$name"; + $hash->{NOTIFYDEV} = "global,$name"; CommandAttr( undef, $name . ' room GardenaSmart' ) if ( AttrVal( $name, 'room', 'none' ) eq 'none' ); @@ -261,7 +241,7 @@ sub Undef { my $hash = shift; my $name = shift; - RemoveInternalTimer($hash, "FHEM::GardenaSmartBridge::getDevices"); + RemoveInternalTimer( $hash, "FHEM::GardenaSmartBridge::getDevices" ); delete $modules{GardenaSmartBridge}{defptr}{BRIDGE} if ( defined( $modules{GardenaSmartBridge}{defptr}{BRIDGE} ) ); @@ -282,7 +262,8 @@ sub Attr { if ( $attrName eq 'disable' ) { if ( $cmd eq 'set' && $attrVal eq '1' ) { - RemoveInternalTimer($hash, "FHEM::GardenaSmartBridge::getDevices"); + RemoveInternalTimer( $hash, + "FHEM::GardenaSmartBridge::getDevices" ); readingsSingleUpdate( $hash, 'state', 'inactive', 1 ); Log3 $name, 3, "GardenaSmartBridge ($name) - disabled"; } @@ -307,13 +288,15 @@ sub Attr { if ( $cmd eq 'set' ) { return 'Interval must be greater than 0' if ( $attrVal == 0 ); - RemoveInternalTimer($hash, "FHEM::GardenaSmartBridge::getDevices"); + RemoveInternalTimer( $hash, + "FHEM::GardenaSmartBridge::getDevices" ); $hash->{INTERVAL} = $attrVal; Log3 $name, 3, "GardenaSmartBridge ($name) - set interval: $attrVal"; } elsif ( $cmd eq 'del' ) { - RemoveInternalTimer($hash, "FHEM::GardenaSmartBridge::getDevices"); + RemoveInternalTimer( $hash, + "FHEM::GardenaSmartBridge::getDevices" ); $hash->{INTERVAL} = 60; Log3 $name, 3, "GardenaSmartBridge ($name) - delete User interval and set default: 60"; @@ -360,14 +343,9 @@ sub Notify { ) ) - || ( - $devtype eq 'GardenaSmartBridge' - && ( - grep /^gardenaAccountPassword.+/, - @{$events} - ) - ) - && $init_done + || ( $devtype eq 'GardenaSmartBridge' + && ( grep /^gardenaAccountPassword.+/, @{$events} ) ) + && $init_done ); getDevices($hash) @@ -387,7 +365,7 @@ sub Notify { && ( grep /^state:.Connected$/, @{$events} or grep /^lastRequestState:.request_error$/, - @{$events} + @{$events} ) ) { @@ -399,6 +377,7 @@ sub Notify { return; } + sub Get { my $hash = shift // return; my $aArg = shift // return; @@ -410,15 +389,16 @@ sub Get { if ( lc $cmd eq 'debug_devices_list' ) { my $device = shift @$aArg; $hash->{helper}{debug_device} = $device; - Write($hash, undef, undef, undef, undef); - return undef; - } else { + Write( $hash, undef, undef, undef, undef ); + return; + } + else { my $list = ""; - $list .= " debug_devices_list:" - .join( ',', @{ $hash->{helper}{deviceList} }) - if ( AttrVal( $name, "debugDEVICE", "none") ne "none" - && exists($hash->{helper}{deviceList}) ); - return "Unknown argument $cmd,choose one of $list"; + $list .= + " debug_devices_list:" . join( ',', @{ $hash->{helper}{deviceList} } ) + if ( AttrVal( $name, "debugDEVICE", "none" ) ne "none" + && exists( $hash->{helper}{deviceList} ) ); + return "Unknown argument $cmd,choose one of $list"; } } @@ -427,8 +407,9 @@ sub Set { my $aArg = shift // return; my $name = shift @$aArg // return; - my $cmd = shift @$aArg // return qq{"set $name" needs at least one argument}; - + my $cmd = shift @$aArg + // return qq{"set $name" needs at least one argument}; + # Das Argument für das Passwort, also das Passwort an sich darf keine = enthalten!!! if ( lc $cmd eq 'getdevicesstate' ) { @@ -478,21 +459,22 @@ sub Write { my ( $session_id, $header, $uri, $method ); ( $payload, $session_id, $header, $uri, $method, $deviceId, $service_id ) = - createHttpValueStrings( $hash, $payload, $deviceId, $abilities, $service_id ); + createHttpValueStrings( $hash, $payload, $deviceId, $abilities, + $service_id ); HttpUtils_NonblockingGet( { - url => $hash->{URL} . $uri, - timeout => 15, + url => $hash->{URL} . $uri, + timeout => 15, incrementalTimeout => 1, - hash => $hash, - device_id => $deviceId, - data => $payload, - method => $method, - header => $header, - doTrigger => 1, - cl => $hash->{CL}, - callback => \&ErrorHandling + hash => $hash, + device_id => $deviceId, + data => $payload, + method => $method, + header => $header, + doTrigger => 1, + cl => $hash->{CL}, + callback => \&ErrorHandling } ); @@ -500,8 +482,8 @@ sub Write { "GardenaSmartBridge ($name) - Send with URL: $hash->{URL}$uri, HEADER: secret!, DATA: secret!, METHOD: $method" ); - # Log3($name, 3, - # "GardenaSmartBridge ($name) - Send with URL: $hash->{URL}$uri, HEADER: $header, DATA: $payload, METHOD: $method"); +# Log3($name, 3, +# "GardenaSmartBridge ($name) - Send with URL: $hash->{URL}$uri, HEADER: $header, DATA: $payload, METHOD: $method"); return; } @@ -519,7 +501,7 @@ sub ErrorHandling { if ( defined( $param->{'device_id'} ) ); my $dname = $dhash->{NAME}; - + Log3 $name, 4, "GardenaSmartBridge ($name) - Request: $data"; my $decode_json = eval { decode_json($data) } if ( length($data) > 0 ); @@ -702,6 +684,11 @@ sub ErrorHandling { . $param->{code}; } + if ( !defined( $hash->{helper}{session_id} ) ) { + readingsSingleUpdate( $hash, 'token', 'none', 1 ); + InternalTimer( gettimeofday() + 5, + "FHEM::GardenaSmartBridge::getToken", $hash ); + } readingsEndUpdate( $dhash, 1 ); Log3 $dname, 5, @@ -712,60 +699,70 @@ sub ErrorHandling { delete $dhash->{helper}{deviceAction} if ( defined( $dhash->{helper}{deviceAction} ) ); - readingsSingleUpdate( $hash, 'token', 'none', 1 ) - if ( !defined( $hash->{helper}{session_id} ) ); - - getToken($hash) - if ( !defined( $hash->{helper}{session_id} ) ); return; } - elsif (defined ($decode_json->{message}) - && $decode_json->{message} eq 'Unauthorized') { - Log3 $name, 3, - "GardenaSmartBridge ($name) - Unauthorized -> fetch new token "; - getToken($hash); - return; + elsif ( defined( $decode_json->{message} ) + && $decode_json->{message} eq 'Unauthorized' ) + { + Log3 $name, 3, + "GardenaSmartBridge ($name) - Unauthorized -> fetch new token "; + + getToken($hash); + + return; } - if (defined($hash->{helper}{debug_device}) - && $hash->{helper}{debug_device} ne 'none' - ){ - Log3 $name, 4, "GardenaSmartBridge DEBUG Device"; - delete $hash->{helper}{debug_device}; - my @device_spec = ("name", "id", "category"); - my $devJson=$decode_json->{devices}; - my $output = '.:{ DEBUG OUTPUT for '.$devJson->{name}.' }:. \n'; - for my $spec (@device_spec) { - $output .= "$spec : $devJson->{$spec} \n"; - } - #settings - $output .= '\n=== Settings \n'; - my $i = 0; - for my $dev_settings ( @ { $devJson->{settings} } ) { - $output .= "[".$i++."]id: $dev_settings->{id} \n"; - $output .= "name: $dev_settings->{name} "; - if (ref ($dev_settings->{value}) eq 'ARRAY' - || ref ($dev_settings->{value}) eq 'HASH'){ - $output .= 'N/A \n'; - } else { - $output .= "value: $dev_settings->{value} \n"; + if ( defined( $hash->{helper}{debug_device} ) + && $hash->{helper}{debug_device} ne 'none' ) + { + Log3 $name, 4, "GardenaSmartBridge DEBUG Device"; + delete $hash->{helper}{debug_device}; + + my @device_spec = ( "name", "id", "category" ); + my $devJson = $decode_json->{devices}; + my $output = '.:{ DEBUG OUTPUT for ' . $devJson->{name} . ' }:. \n'; + + for my $spec (@device_spec) { + $output .= "$spec : $devJson->{$spec} \n"; } - } - $output .= '\n=== Abilities \n'; - $i = 0; - for my $dev_settings ( @ { $devJson->{abilities} } ) { - $output .= "[".$i++."]id: $dev_settings->{id} \n"; - $output .= "name: $dev_settings->{name} "; - if (ref ($dev_settings->{value}) eq 'ARRAY' - || ref ($dev_settings->{value}) eq 'HASH'){ - $output .= 'N/A \n'; - } else { - $output .= "value: $dev_settings->{value} \n"; + + #settings + $output .= '\n=== Settings \n'; + my $i = 0; + for my $dev_settings ( @{ $devJson->{settings} } ) { + $output .= "[" . $i++ . "]id: $dev_settings->{id} \n"; + $output .= "name: $dev_settings->{name} "; + if ( ref( $dev_settings->{value} ) eq 'ARRAY' + || ref( $dev_settings->{value} ) eq 'HASH' ) + { + $output .= 'N/A \n'; + } + else { + $output .= "value: $dev_settings->{value} \n"; + } } - } - $hash->{helper}{debug_device_output} = $output; - asyncOutput($param->{cl}, $hash->{helper}{debug_device_output}); - return; + + $output .= '\n=== Abilities \n'; + $i = 0; + + for my $dev_settings ( @{ $devJson->{abilities} } ) { + $output .= "[" . $i++ . "]id: $dev_settings->{id} \n"; + $output .= "name: $dev_settings->{name} "; + + if ( ref( $dev_settings->{value} ) eq 'ARRAY' + || ref( $dev_settings->{value} ) eq 'HASH' ) + { + $output .= 'N/A \n'; + } + else { + $output .= "value: $dev_settings->{value} \n"; + } + } + + $hash->{helper}{debug_device_output} = $output; + asyncOutput( $param->{cl}, $hash->{helper}{debug_device_output} ); + + return; } readingsSingleUpdate( $hash, 'state', 'Connected', 1 ) if ( defined( $hash->{helper}{locations_id} ) ); @@ -794,18 +791,20 @@ sub ResponseProcessing { } } - # print Dumper $decode_json; + if ( defined( $decode_json->{data} ) + && $decode_json->{data} + && ref( $decode_json->{data} ) eq 'HASH' + && !defined( $hash->{helper}->{user_id} ) ) + { - if ( defined( $decode_json->{data} ) && $decode_json->{data} - && ref($decode_json->{data}) eq 'HASH' - && !defined( $hash->{helper}->{user_id})) { + $hash->{helper}{session_id} = $decode_json->{data}{id}; + $hash->{helper}{user_id} = $decode_json->{data}{attributes}->{user_id}; + $hash->{helper}{refresh_token} = + $decode_json->{data}{attributes}->{refresh_token}; + $hash->{helper}{token_expired} = + gettimeofday() + $decode_json->{data}{attributes}->{expires_in}; - $hash->{helper}{session_id} = $decode_json->{data}{id}; - $hash->{helper}{user_id} = $decode_json->{data}{attributes}->{user_id}; - $hash->{helper}{refresh_token} = $decode_json->{data}{attributes}->{refresh_token}; - $hash->{helper}{token_expired} = gettimeofday() + $decode_json->{data}{attributes}->{expires_in}; - - InternalTimer($hash->{helper}{token_expired}, + InternalTimer( $hash->{helper}{token_expired}, "FHEM::GardenaSmartBridge::getToken", $hash ); Write( $hash, undef, undef, undef ); @@ -838,51 +837,36 @@ sub ResponseProcessing { && ref( $decode_json->{devices} ) eq 'ARRAY' && scalar( @{ $decode_json->{devices} } ) > 0 ) { - my @buffer = split( '"devices":\[', $json ); - my ( $json, $tail ) = ParseJSON( $hash, $buffer[1] ); + require SubProcess; - while ($json) { + my $subprocess = + SubProcess->new( { onRun => \&ResponseSubprocessing } ); + $subprocess->{buffer} = $buffer[1]; - Log3 $name, 5, - "GardenaSmartBridge ($name) - Decoding JSON message. Length: " - . length($json) - . " Content: " - . $json; - Log3 $name, 5, - "GardenaSmartBridge ($name) - Vor Sub: Laenge JSON: " - . length($json) - . " Content: " - . $json - . " Tail: " - . $tail; + my $pid = $subprocess->run(); - if ( defined($tail) and $tail ) { - $decode_json = eval { decode_json($json) }; - if ($@) { - Log3 $name, 5, -"GardenaSmartBridge ($name) - JSON error while request: $@"; - } + if ( !defined($pid) ) { + Log3( $name, 1, +qq{GardenaSmartBridge ($name) - Cannot execute parse json asynchronously} + ); - Dispatch( $hash, $json, undef ) - if ( $decode_json->{category} ne 'gateway' ); - WriteReadings( $hash, $decode_json ) - if ( defined( $decode_json->{category} ) - && $decode_json->{category} eq 'gateway' ); - } - - ( $json, $tail ) = ParseJSON( $hash, $tail ); - - Log3 $name, 5, - "GardenaSmartBridge ($name) - Nach Sub: Laenge JSON: " - . length($json) - . " Content: " - . $json - . " Tail: " - . $tail; + CleanSubprocess($hash); + readingsSingleUpdate( $hash, 'state', + 'Cannot execute parse json asynchronously', 1 ); + return; } + Log3( $name, 4, +qq{GardenaSmartBridge ($name) - execute parse json asynchronously (PID="$pid")} + ); + + $hash->{".fhem"}{subprocess} = $subprocess; + + InternalTimer( gettimeofday() + 1, + "FHEM::GardenaSmartBridge::PollChild", $hash ); + return; } @@ -891,12 +875,148 @@ sub ResponseProcessing { return; } +sub ResponseProcessingFinalFromSubProcessing { + my $hash = shift; + my $response = shift; + + my $name = $hash->{NAME}; + + my @response = split '\|,', $response; + + Log3( $name, 4, + qq{GardenaSmartBridge ($name) - got result from asynchronous parsing} ); + + my $decode_json; + + Log3( $name, 4, qq{GardenaSmartBridge ($name) - asynchronous finished.} ); + + if ( scalar(@response) > 0 ) { + for my $json (@response) { + + ################# + $decode_json = eval { decode_json($json) }; + if ($@) { + Log3 $name, 5, + "GardenaSmartBridge ($name) - JSON error while request: $@"; + } + + Dispatch( $hash, $json, undef ) + if ( $decode_json->{category} ne 'gateway' ); + WriteReadings( $hash, $decode_json ) + if ( defined( $decode_json->{category} ) + && $decode_json->{category} eq 'gateway' ); + } + } + + return; +} + +sub PollChild { + my $hash = shift; + + my $name = $hash->{NAME}; + + if ( defined( $hash->{".fhem"}{subprocess} ) ) { + my $subprocess = $hash->{".fhem"}{subprocess}; + my $response = $subprocess->readFromChild(); + + if ( defined($response) ) { + ResponseProcessingFinalFromSubProcessing( $hash, $response ); + $subprocess->wait(); + CleanSubprocess($hash); + } + + Log3( $name, 4, +qq{GardenaSmartBridge ($name) - still waiting ($subprocess->{lasterror}).} + ); + + InternalTimer( gettimeofday() + 1, + "FHEM::GardenaSmartBridge::PollChild", $hash ); + return; + } + + return; +} + +# ResponseSubprocessin muss in eine async ausgelagert werden +###################################### +# Begin Childprozess +###################################### +sub ResponseSubprocessing { + my $subprocess = shift; + my $buffer = $subprocess->{buffer}; + my @response = (); + + my ( $json, $tail ) = ParseJSON($buffer); + + while ($json) { + if ( defined($tail) and $tail ) { + push @response, $json; + } + + ( $json, $tail ) = ParseJSON($tail); + } + + $subprocess->writeToParent( join '|', @response ); + + return; +} + +sub ParseJSON { + my $buffer = shift; + + my $open = 0; + my $close = 0; + my $msg = ''; + my $tail = ''; + + if ($buffer) { + for my $c ( split //, $buffer ) { + if ( $open == $close && $open > 0 ) { + $tail .= $c; + } + else { + + if ( $c eq '{' ) { + + $open++; + + } + elsif ( $c eq '}' ) { + + $close++; + } + + $msg .= $c; + } + } + + if ( $open != $close ) { + + $tail = $msg; + $msg = ''; + } + } + + return ( $msg, $tail ); +} +###################################### +# End Childprozess +###################################### + +sub CleanSubprocess { + my $hash = shift; + + my $name = $hash->{NAME}; + + delete( $hash->{".fhem"}{subprocess} ); + Log3( $name, 4, qq{GardenaSmartBridge ($name) - clean Subprocess} ); +} + sub WriteReadings { my $hash = shift; my $decode_json = shift; - # print Dumper $decode_json; - my $name = $hash->{NAME}; if ( defined( $decode_json->{id} ) @@ -940,8 +1060,9 @@ sub WriteReadings { $decode_json->{abilities}[0]{properties}[$properties] {name} . '-' . $t, $v - ) - if ($decode_json->{abilities}[0]{properties}[$properties]{name} !~ /ethernet_status|wifi_status/ ); + ) + if ( $decode_json->{abilities}[0]{properties}[$properties] + {name} !~ /ethernet_status|wifi_status/ ); if ( ( $decode_json->{abilities}[0]{properties} @@ -968,8 +1089,8 @@ sub WriteReadings { [$properties]{name} eq 'wifi_status' ) { readingsBulkUpdateIfChanged( $hash, - 'wifi_status-ssid', $v->{ssid} ) - if (ref($v->{ssid}) ne 'HASH'); + 'wifi_status-ssid', $v->{ssid} ) + if ( ref( $v->{ssid} ) ne 'HASH' ); readingsBulkUpdateIfChanged( $hash, 'wifi_status-mac', $v->{mac} ); readingsBulkUpdateIfChanged( $hash, @@ -1002,25 +1123,23 @@ sub getDevices { my $hash = shift; my $name = $hash->{NAME}; - RemoveInternalTimer($hash, "FHEM::GardenaSmartBridge::getDevices"); + RemoveInternalTimer( $hash, "FHEM::GardenaSmartBridge::getDevices" ); if ( not IsDisabled($name) ) { delete $hash->{helper}{deviceList}; my @list; @list = devspec2array('TYPE=GardenaSmartDevice'); - for my $gardenaDev (@list){ - push( @{ $hash->{helper}{deviceList} }, $gardenaDev ); + for my $gardenaDev (@list) { + push( @{ $hash->{helper}{deviceList} }, $gardenaDev ); } - if ( AttrVal( $name, 'gardenaAccountEmail', 'none' ) ne 'none' - && ( - defined( ReadPassword( $hash, $name ) ) - )) + if ( AttrVal( $name, 'gardenaAccountEmail', 'none' ) ne 'none' + && ( defined( ReadPassword( $hash, $name ) ) ) ) { - Write( $hash, undef, undef, undef ); - Log3 $name, 4, - "GardenaSmartBridge ($name) - fetch device list and device states"; - } # fi gardenaAccountEmail + Write( $hash, undef, undef, undef ); + Log3 $name, 4, +"GardenaSmartBridge ($name) - fetch device list and device states"; + } # fi gardenaAccountEmail } else { readingsSingleUpdate( $hash, 'state', 'disabled', 1 ); @@ -1044,27 +1163,29 @@ sub getToken { readingsSingleUpdate( $hash, 'state', 'get token', 1 ); delete $hash->{helper}{session_id} - if ( defined( $hash->{helper}{session_id} ) - && $hash->{helper}{session_id} ); + if ( exists( $hash->{helper}{session_id} ) ); delete $hash->{helper}{user_id} - if ( defined( $hash->{helper}{user_id} ) && $hash->{helper}{user_id} ); + if ( exists( $hash->{helper}{user_id} ) ); delete $hash->{helper}{locations_id} - if ( defined( $hash->{helper}{locations_id} ) - && $hash->{helper}{locations_id} ); + if ( exists( $hash->{helper}{locations_id} ) ); Write( - $hash, - '"data": {"type":"token", "attributes":{"username": "' - . AttrVal( $name, 'gardenaAccountEmail', 'none' ) - . '","password": "' - . ReadPassword( $hash, $name ) . '", "client_id":"smartgarden-jwt-client"}}', - undef, - undef - ); + $hash, + '"data": {"type":"token", "attributes":{"username": "' + . AttrVal( $name, 'gardenaAccountEmail', 'none' ) + . '","password": "' + . ReadPassword( $hash, $name ) + . '", "client_id":"smartgarden-jwt-client"}}', + undef, + undef + ); - Log3 $name, 4, '"data": {"type":"token", "attributes":{"username": "' - .AttrVal( $name, 'gardenaAccountEmail', 'none' ) . '","password": "' - .ReadPassword( $hash, $name ) . '", "client_id":"smartgarden-jwt-client"}}'; + Log3 $name, 4, + '"data": {"type":"token", "attributes":{"username": "' + . AttrVal( $name, 'gardenaAccountEmail', 'none' ) + . '","password": "' + . ReadPassword( $hash, $name ) + . '", "client_id":"smartgarden-jwt-client"}}'; Log3 $name, 3, "GardenaSmartBridge ($name) - send credentials to fetch Token and locationId"; @@ -1115,7 +1236,7 @@ sub ReadPassword { Log3 $name, 3, "GardenaSmartBridge ($name) - unable to read password from file: $err"; - return undef; + return; } @@ -1141,7 +1262,7 @@ sub ReadPassword { else { Log3 $name, 3, "GardenaSmartBridge ($name) - No password in file"; - return undef; + return; } return; @@ -1159,59 +1280,6 @@ sub Rename { return; } -sub ParseJSON { - my $hash = shift; - my $buffer = shift; - - my $name = $hash->{NAME}; - my $open = 0; - my $close = 0; - my $msg = ''; - my $tail = ''; - - if ($buffer) { - for my $c ( split //, $buffer ) { - if ( $open == $close && $open > 0 ) { - $tail .= $c; - Log3 $name, 5, - "GardenaSmartBridge ($name) - $open == $close and $open > 0"; - - } - elsif ( ( $open == $close ) && ( $c ne '{' ) ) { - - Log3 $name, 5, -"GardenaSmartBridge ($name) - Garbage character before message: " - . $c; - - } - else { - - if ( $c eq '{' ) { - - $open++; - - } - elsif ( $c eq '}' ) { - - $close++; - } - - $msg .= $c; - } - } - - if ( $open != $close ) { - - $tail = $msg; - $msg = ''; - } - } - - Log3 $name, 5, - "GardenaSmartBridge ($name) - return msg: $msg and tail: $tail"; - return ( $msg, $tail ); -} - sub createHttpValueStrings { my ( $hash, $payload, $deviceId, $abilities, $service_id ) = @_; @@ -1220,17 +1288,17 @@ sub createHttpValueStrings { my $uri = ''; my $method = 'POST'; $header .= "\r\nAuthorization: Bearer $session_id" - if ( defined($hash->{helper}{session_id}) ); + if ( defined( $hash->{helper}{session_id} ) ); $header .= "\r\nAuthorization-Provider: husqvarna" - if ( defined($hash->{helper}{session_id}) ); + if ( defined( $hash->{helper}{session_id} ) ); # $header .= "\r\nx-api-key: $session_id" # if ( defined( $hash->{helper}{session_id} ) ); $payload = '{' . $payload . '}' if ( defined($payload) ); - $payload = '{}' if ( !defined($payload) ); + $payload = '{}' if ( !defined($payload) ); if ( $payload eq '{}' ) { - $method = 'GET' if (defined( $hash->{helper}{session_id} ) ); + $method = 'GET' if ( defined( $hash->{helper}{session_id} ) ); $payload = ''; $uri .= '/locations?locatioId=null&user_id=' . $hash->{helper}{user_id} if ( exists( $hash->{helper}{user_id} ) @@ -1243,8 +1311,10 @@ sub createHttpValueStrings { && defined( $hash->{helper}{locations_id} ) ); } - $uri = '/devices/'.InternalVal($hash->{helper}{debug_device}, 'DEVICEID', 0 ) if ( defined ($hash->{helper}{debug_device}) - && defined( $hash->{helper}{locations_id} ) ); + $uri = + '/devices/' . InternalVal( $hash->{helper}{debug_device}, 'DEVICEID', 0 ) + if ( defined( $hash->{helper}{debug_device} ) + && defined( $hash->{helper}{locations_id} ) ); $uri = '/auth/token' if ( !defined( $hash->{helper}{session_id} ) ); if ( defined( $hash->{helper}{locations_id} ) ) { @@ -1252,17 +1322,13 @@ sub createHttpValueStrings { $method = 'PUT'; my $dhash = $modules{GardenaSmartDevice}{defptr}{$deviceId}; - - $uri .= - '/devices/' - . $deviceId - . '/settings/' - . $service_id + + $uri .= '/devices/' . $deviceId . '/settings/' . $service_id if ( defined($abilities) && defined($payload) && $abilities =~ /.*_settings/ ); - } # park until next schedules or override + } # park until next schedules or override elsif (defined($abilities) && defined($payload) && $abilities eq 'mower_timer' ) @@ -1285,7 +1351,7 @@ sub createHttpValueStrings { my $valve_id; if ( $payload =~ m#watering_timer_(\d)# ) { - $method = 'PUT'; + $method = 'PUT'; $valve_id = $1; } $uri .= @@ -1293,7 +1359,11 @@ sub createHttpValueStrings { . $deviceId . '/abilities/' . $abilities - . ( defined($valve_id) ? '/properties/watering_timer_'. $valve_id : '/command') + . ( + defined($valve_id) + ? '/properties/watering_timer_' . $valve_id + : '/command' + ); } elsif (defined($abilities) @@ -1507,9 +1577,9 @@ sub DeletePassword { ], "release_status": "stable", "license": "GPL_2", - "version": "v2.4.6", + "version": "v2.4.7", "author": [ - "Marko Oldenburg " + "Marko Oldenburg " ], "x_fhem_maintainer": [ "CoolTux" diff --git a/FHEM/74_GardenaSmartDevice.pm b/FHEM/74_GardenaSmartDevice.pm index 801e015..fac69b5 100644 --- a/FHEM/74_GardenaSmartDevice.pm +++ b/FHEM/74_GardenaSmartDevice.pm @@ -1,8 +1,8 @@ ############################################################################### # -# Developed with Kate +# Developed with VSCodium and richterger perl plugin. # -# (c) 2017-2021 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) +# (c) 2017-2022 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) # All rights reserved # # Special thanks goes to comitters: @@ -72,15 +72,11 @@ eval { require JSON::MaybeXS; import JSON::MaybeXS qw( decode_json encode_json ); 1; -}; - -if ($@) { - $@ = undef; +} or do { # try to use JSON wrapper # for chance of better performance eval { - # JSON preference order local $ENV{PERL_JSON_BACKEND} = 'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP' @@ -89,10 +85,7 @@ if ($@) { require JSON; import JSON qw( decode_json encode_json ); 1; - }; - - if ($@) { - $@ = undef; + } or do { # In rare cases, Cpanel::JSON::XS may # be installed but JSON|JSON::MaybeXS not ... @@ -100,10 +93,7 @@ if ($@) { require Cpanel::JSON::XS; import Cpanel::JSON::XS qw(decode_json encode_json); 1; - }; - - if ($@) { - $@ = undef; + } or do { # In rare cases, JSON::XS may # be installed but JSON not ... @@ -111,10 +101,7 @@ if ($@) { require JSON::XS; import JSON::XS qw(decode_json encode_json); 1; - }; - - if ($@) { - $@ = undef; + } or do { # Fallback to built-in JSON which SHOULD # be available since 5.014 ... @@ -122,20 +109,17 @@ if ($@) { require JSON::PP; import JSON::PP qw(decode_json encode_json); 1; - }; - - if ($@) { - $@ = undef; + } or do { # Fallback to JSON::backportPP in really rare cases require JSON::backportPP; import JSON::backportPP qw(decode_json encode_json); 1; - } - } - } - } -} + }; + }; + }; + }; +}; ## Import der FHEM Funktionen #-- Run before package compilation @@ -164,7 +148,7 @@ BEGIN { #-- Export to main context with different name GP_Export( qw( - Initialize + Initialize ) ); @@ -181,7 +165,7 @@ sub Initialize { $hash->{AttrFn} = \&Attr; $hash->{AttrList} = "readingValueLanguage:de,en " - . "model:watering_computer,sensor,mower,ic24,power,electronic_pressure_pump " + . "model:watering_computer,sensor,sensor2,mower,ic24,power,electronic_pressure_pump " . "IODev " . $readingFnAttributes; $hash->{parseParams} = 1; @@ -204,22 +188,23 @@ sub Define { my $deviceId = $aArg->[2]; my $category = $aArg->[3]; - $hash->{DEVICEID} = $deviceId; - $hash->{VERSION} = version->parse($VERSION)->normal; - $hash->{helper}{STARTINGPOINTID} = ''; - $hash->{helper}{schedules_paused_until_id} = ''; - $hash->{helper}{eco_mode_id} = ''; - $hash->{helper}{button_config_time_id} = ''; - $hash->{helper}{winter_mode_id} = ''; - - $hash->{helper}{_id} = ''; + $hash->{DEVICEID} = $deviceId; + $hash->{VERSION} = version->parse($VERSION)->normal; + $hash->{helper}{STARTINGPOINTID} = ''; + $hash->{helper}{schedules_paused_until_id} = ''; + $hash->{helper}{eco_mode_id} = ''; + $hash->{helper}{button_config_time_id} = ''; + $hash->{helper}{winter_mode_id} = ''; + + $hash->{helper}{_id} = ''; + # IrrigationControl valve control max 6 - $hash->{helper}{schedules_paused_until_1_id} = ''; - $hash->{helper}{schedules_paused_until_2_id} = ''; - $hash->{helper}{schedules_paused_until_3_id} = ''; - $hash->{helper}{schedules_paused_until_4_id} = ''; - $hash->{helper}{schedules_paused_until_5_id} = ''; - $hash->{helper}{schedules_paused_until_6_id} = ''; + $hash->{helper}{schedules_paused_until_1_id} = ''; + $hash->{helper}{schedules_paused_until_2_id} = ''; + $hash->{helper}{schedules_paused_until_3_id} = ''; + $hash->{helper}{schedules_paused_until_4_id} = ''; + $hash->{helper}{schedules_paused_until_5_id} = ''; + $hash->{helper}{schedules_paused_until_6_id} = ''; CommandAttr( undef, "$name IODev $modules{GardenaSmartBridge}{defptr}{BRIDGE}->{NAME}" ) @@ -283,16 +268,23 @@ sub Attr { } sub Set { - my $hash = shift // return; - my $aArg = shift // return; + my $hash = shift // return; + my $aArg = shift // return; - my $name = shift @$aArg; - my $cmd = shift @$aArg // return qq{"set $name" needs at least one argument}; + my $name = shift @$aArg; + my $cmd = shift @$aArg + // return qq{"set $name" needs at least one argument}; my $payload; my $abilities; my $service_id; - my $mainboard_version = ReadingsVal( $name, 'mower_type-mainboard_version', 0.0 ); + my $mainboard_version = + ReadingsVal( $name, 'mower_type-mainboard_version', 0.0 ); + + my ($Sekunden, $Minuten, $Stunden, $Monatstag, $Monat, + $Jahr, $Wochentag, $Jahrestag, $Sommerzeit) = localtime(time); + + my $timezone_offset = $Sommerzeit ? 0 : ( Time::Piece->new )->tzoffset; #set default abilitie ... overwrite in cmd to change $abilities = 'mower' @@ -305,51 +297,62 @@ sub Set { $abilities = 'manual_watering' if ( AttrVal( $name, 'model', 'unknown' ) eq 'electronic_pressure_pump' ); - ### mower + ### mower # service_id (eco, parkuntilfurhternotice, startpoints) if ( lc $cmd eq 'parkuntilfurthernotice' ) { $payload = '"name":"park_until_further_notice"'; if ( $mainboard_version > 10.30 ) { - $payload = ' "settings":{"name":"schedules_paused_until","value":"2038-01-18T00:00:00.000Z","device":"'.$hash->{DEVICEID}.'"}'; - $abilities = 'mower_settings' ; - $service_id = $hash->{helper}{schedules_paused_until_id}; + $payload = +' "settings":{"name":"schedules_paused_until","value":"2038-01-18T00:00:00.000Z","device":"' + . $hash->{DEVICEID} . '"}'; + $abilities = 'mower_settings'; + $service_id = $hash->{helper}{schedules_paused_until_id}; } } - elsif ( lc $cmd eq 'parkuntilnexttimer' ) { + elsif ( lc $cmd eq 'parkuntilnexttimer' ) { $payload = '"name":"park_until_next_timer"'; - if ( $mainboard_version > 10.30 ){ - $payload = '"properties":{"name":"mower_timer","value":0}' ; - $abilities = 'mower_timer'; + if ( $mainboard_version > 10.30 ) { + $payload = '"properties":{"name":"mower_timer","value":0}'; + $abilities = 'mower_timer'; } } elsif ( lc $cmd eq 'startresumeschedule' ) { $payload = '"name":"start_resume_schedule"'; if ( $mainboard_version > 10.30 ) { - $payload = ' "settings":{"name":"schedules_paused_until","value":"","device":"'.$hash->{DEVICEID}.'"}'; - $abilities = 'mower_settings' ; - $service_id = $hash->{helper}{schedules_paused_until_id}; + $payload = +' "settings":{"name":"schedules_paused_until","value":"","device":"' + . $hash->{DEVICEID} . '"}'; + $abilities = 'mower_settings'; + $service_id = $hash->{helper}{schedules_paused_until_id}; } } elsif ( lc $cmd eq 'startoverridetimer' ) { $payload = '"name":"start_override_timer","parameters":{"duration":' . $aArg->[0] * 60 . '}'; - if ( $mainboard_version > 10.30 ){ - $payload = '"properties":{"name":"mower_timer","value":'.$aArg->[0] * 60 .'}'; - $abilities = 'mower_timer'; + if ( $mainboard_version > 10.30 ) { + $payload = '"properties":{"name":"mower_timer","value":' + . $aArg->[0] * 60 . '}'; + $abilities = 'mower_timer'; } } elsif ( lc $cmd eq 'startpoint' ) { my $err; - ( $err, $payload, $abilities ) = SetPredefinedStartPoints( $hash, $aArg ); + ( $err, $payload, $abilities ) = + SetPredefinedStartPoints( $hash, $aArg ); $service_id = $hash->{helper}{STARTINGPOINTID}; return $err if ( defined($err) ); } elsif ( lc $cmd eq 'eco' ) { - $payload = '"settings": {"name": "eco_mode", "value": '.$aArg->[0].', "device": "'.$hash->{DEVICEID}.'"}'; - $abilities = 'mower_settings' if ( $mainboard_version > 10.30 ); + $payload = + '"settings": {"name": "eco_mode", "value": ' + . $aArg->[0] + . ', "device": "' + . $hash->{DEVICEID} . '"}'; + $abilities = 'mower_settings' if ( $mainboard_version > 10.30 ); $service_id = $hash->{helper}{eco_mode_id}; - #$abilities['service_id'] = $hash->{helper}{SCHEDULESID} if ( $mainboard_version > 10.30 ); + +#$abilities['service_id'] = $hash->{helper}{SCHEDULESID} if ( $mainboard_version > 10.30 ); } ### electronic_pressure_pump elsif ( lc $cmd eq 'pumptimer' ) { @@ -365,16 +368,15 @@ sub Set { . $aArg->[0] * 60 . ',"valve_id":1}}'; } - elsif ( lc $cmd eq 'manualbuttontime'){ - $service_id = $hash->{helper}{button_config_time_id}; - $payload= - '"properties":{"name":"button_config_time",' - .'"value":' - . $aArg->[0] * 60 - . ',"timestamp":"2021-05-26T19:06:23.680Z"' - . ',"at_bound":null,"unit":"seconds","ability":"' - . $service_id - .'"}'; + elsif ( lc $cmd eq 'manualbuttontime' ) { + $service_id = $hash->{helper}{button_config_time_id}; + $payload = + '"properties":{"name":"button_config_time",' + . '"value":' + . $aArg->[0] * 60 + . ',"timestamp":"2021-05-26T19:06:23.680Z"' + . ',"at_bound":null,"unit":"seconds","ability":"' + . $service_id . '"}'; $abilities = 'watering_button_config'; } elsif ( $cmd =~ m{\AcancelOverride}xms ) { @@ -393,26 +395,40 @@ sub Set { . ',"valve_id":' . $valve_id . '}}'; } - elsif ( $cmd =~ /.*Schedule/ ){ - my $duration = (( defined($aArg->[0]) ? ( ((Time::Piece->new)+(ONE_HOUR * $aArg->[0]) - (Time::Piece->new)->tzoffset )->datetime ).'.000Z' : '2038-01-18T00:00:00.000Z')); - - $abilities = 'wateringcomputer_settings'; - $service_id = $hash->{helper}->{'schedules_paused_until_id'}; - $payload = '"settings":{"name":"schedules_paused_until"' - . ', "value":"' - . ($cmd eq 'resumeSchedule' ? '' : $duration ) - . '","device":"' - . $hash->{DEVICEID} - . '"}'; + elsif ( $cmd =~ /.*Schedule$/ ) { + my $duration = ( + ( + defined( $aArg->[0] ) + ? ( + ( + ( Time::Piece->new ) + + ( ONE_HOUR * $aArg->[0] ) - + $timezone_offset + )->datetime + ) + . '.000Z' + : '2038-01-18T00:00:00.000Z' + ) + ); + + $abilities = 'wateringcomputer_settings'; + $service_id = $hash->{helper}->{'schedules_paused_until_id'}; + $payload = + '"settings":{"name":"schedules_paused_until"' + . ', "value":"' + . ( $cmd eq 'resumeSchedule' ? '' : $duration ) + . '","device":"' + . $hash->{DEVICEID} . '"}'; } elsif ( lc $cmd eq 'on' || lc $cmd eq 'off' || lc $cmd eq 'on-for-timer' ) { my $val = ( - scalar(!@$aArg == 0) && ref($aArg) eq 'ARRAY' + scalar( !@$aArg == 0 ) && ref($aArg) eq 'ARRAY' ? $aArg->[0] * 60 : lc $cmd ); - $payload = '"properties":{"name":"power_timer", "value":"' . $val . '"}'; + $payload = + '"properties":{"name":"power_timer", "value":"' . $val . '"}'; } ### Watering ic24 elsif ( $cmd =~ m{\AmanualDurationValve\d\z}xms ) { @@ -430,34 +446,51 @@ sub Set { . ',"valve_id":' . $valve_id . '}}'; } - elsif ( $cmd eq 'closeAllValves' ){ - $payload = '"name":"close_all_valves","parameters":{}'; + elsif ( $cmd eq 'closeAllValves' ) { + $payload = '"name":"close_all_valves","parameters":{}'; } - elsif ( $cmd =~ /.*ScheduleValve/ ){ - my $valve_id = $aArg->[0]; - my $duration = (( defined($aArg->[1]) ? ( ((Time::Piece->new)+(ONE_HOUR * $aArg->[1]) - (Time::Piece->new)->tzoffset )->datetime ).'.000Z' : '2038-01-18T00:00:00.000Z')); - - $abilities = 'irrigation_settings'; - $service_id = $hash->{helper}->{'schedules_paused_until_'.$valve_id.'_id'}; - $payload = '"settings":{"name":"schedules_paused_until_' - . $valve_id - . '", "value":"' - . ($cmd eq 'resumeScheduleValve' ? '' : $duration ) - . '","device":"' - . $hash->{DEVICEID} - . '"}'; + elsif ( $cmd =~ /.*ScheduleValve$/ ) { + my $valve_id = $aArg->[0]; + my $duration = ( + ( + defined( $aArg->[1] ) + ? ( + ( + ( Time::Piece->new ) + + ( ONE_HOUR * $aArg->[1] ) - + $timezone_offset + )->datetime + ) + . '.000Z' + : '2038-01-18T00:00:00.000Z' + ) + ); + + $abilities = 'irrigation_settings'; + $service_id = + $hash->{helper}->{ 'schedules_paused_until_' . $valve_id . '_id' }; + $payload = + '"settings":{"name":"schedules_paused_until_' + . $valve_id + . '", "value":"' + . ( $cmd eq 'resumeScheduleValve' ? '' : $duration ) + . '","device":"' + . $hash->{DEVICEID} . '"}'; } ### Sensors elsif ( lc $cmd eq 'refresh' ) { my $sensname = $aArg->[0]; if ( lc $sensname eq 'temperature' ) { - if ( ReadingsVal( $name, 'device_info-category', 'sensor' ) eq 'sensor') { - $payload = '"name":"measure_ambient_temperature"'; - $abilities = 'ambient_temperature'; - } else { - $payload = '"name":"measure_soil_temperature"'; - $abilities = 'soil_temperature'; + if ( ReadingsVal( $name, 'device_info-category', 'sensor' ) eq + 'sensor' ) + { + $payload = '"name":"measure_ambient_temperature"'; + $abilities = 'ambient_temperature'; + } + else { + $payload = '"name":"measure_soil_temperature"'; + $abilities = 'soil_temperature'; } } elsif ( lc $sensname eq 'light' ) { @@ -471,13 +504,13 @@ sub Set { } } ## winter sleep - elsif ( lc $cmd eq 'winter_mode') { - $payload = '"settings":{"name":"winter_mode","value":"' - . $aArg->[0] - .'","device":"' - . $hash->{DEVICEID} - .'"}'; - $abilities = 'winter_settings'; + elsif ( lc $cmd eq 'winter_mode' ) { + $payload = + '"settings":{"name":"winter_mode","value":"' + . $aArg->[0] + . '","device":"' + . $hash->{DEVICEID} . '"}'; + $abilities = 'winter_settings'; $service_id = $hash->{helper}->{'winter_mode_id'}; } else { @@ -488,7 +521,8 @@ sub Set { 'parkUntilFurtherNotice:noArg parkUntilNextTimer:noArg startResumeSchedule:noArg startOverrideTimer:slider,0,1,240 startpoint' if ( AttrVal( $name, 'model', 'unknown' ) eq 'mower' ); - $list .= 'manualOverride:slider,1,1,59 cancelOverride:noArg resumeSchedule:noArg stopSchedule manualButtonTime:slider,0,2,100' + $list .= +'manualOverride:slider,1,1,59 cancelOverride:noArg resumeSchedule:noArg stopSchedule manualButtonTime:slider,0,2,100' if ( AttrVal( $name, 'model', 'unknown' ) eq 'watering_computer' ); $list .= @@ -496,14 +530,17 @@ sub Set { if ( AttrVal( $name, 'model', 'unknown' ) eq 'ic24' ); $list .= 'refresh:temperature,humidity' - if ( AttrVal( $name, 'model', 'unknown' ) eq 'sensor' ); + if ( AttrVal( $name, 'model', 'unknown' ) =~ /sensor.?/ ); + # add light for old sensors $list .= ',light' - if ( AttrVal( $name, 'model', 'unknown' ) eq 'sensor' - && ReadingsVal($name, 'device_info-category', 'unknown') eq 'sensor' ); + if ( AttrVal( $name, 'model', 'unknown' ) eq 'sensor' + && ReadingsVal( $name, 'device_info-category', 'unknown' ) eq + 'sensor' ); $list .= 'on:noArg off:noArg on-for-timer:slider,0,1,720' if ( AttrVal( $name, 'model', 'unknown' ) eq 'power' ); + # all devices has abilitie to fall a sleep $list .= ' winter_mode:awake,hibernate'; return "Unknown argument $cmd, choose one of $list"; @@ -584,25 +621,30 @@ sub WriteReadings { for my $propertie ( @{ $decode_json->{abilities}[$abilities]{properties} } ) { - if ( exists($decode_json->{abilities}[$abilities]{name}) - && ( - $decode_json->{abilities}[$abilities]{name} eq 'watering' ) - ) { - if ( $propertie->{name} eq 'button_config_time' ) - { - if ( $hash->{helper}{$propertie->{name}.'_id'} ne - $decode_json->{abilities}[$abilities]{id} ) - { - $hash->{helper}{$propertie->{name}.'_id'} = - $decode_json->{abilities}[$abilities]{id}; - } - readingsBulkUpdateIfChanged( - $hash, - 'manualButtonTime', - (RigReadingsValue( $hash, $propertie->{value} / 60) ) - ); - next; - } + if ( + exists( $decode_json->{abilities}[$abilities]{name} ) + && ( $decode_json->{abilities}[$abilities]{name} eq + 'watering' ) + ) + { + if ( $propertie->{name} eq 'button_config_time' ) { + if ( $hash->{helper}{ $propertie->{name} . '_id' } ne + $decode_json->{abilities}[$abilities]{id} ) + { + $hash->{helper}{ $propertie->{name} . '_id' } = + $decode_json->{abilities}[$abilities]{id}; + } + readingsBulkUpdateIfChanged( + $hash, + 'manualButtonTime', + ( + RigReadingsValue( + $hash, $propertie->{value} / 60 + ) + ) + ); + next; + } } readingsBulkUpdateIfChanged( @@ -653,21 +695,22 @@ sub WriteReadings { || $decode_json->{abilities}[$abilities]{name} . '-' . $propertie->{name} eq 'light-light' ) ); - + readingsBulkUpdateIfChanged( $hash, $decode_json->{abilities}[$abilities]{name} . '-' - . $propertie->{name} + . $propertie->{name} . '_timestamp', - Time::Piece->strptime(RigReadingsValue( $hash, $propertie->{timestamp} ), "%Y-%m-%d %H:%M:%S")->strftime('%s') - + Time::Piece->strptime( + RigReadingsValue( $hash, $propertie->{timestamp} ), + "%Y-%m-%d %H:%M:%S" )->strftime('%s') + ) if ( - defined( $propertie->{value} ) + defined( $propertie->{value} ) && ( $decode_json->{abilities}[$abilities]{name} . '-' - . $propertie->{name} eq 'mower_timer-mower_timer' - ) - ); + . $propertie->{name} eq 'mower_timer-mower_timer' ) + ); readingsBulkUpdateIfChanged( $hash, @@ -706,32 +749,45 @@ sub WriteReadings { $abilities--; } while ( $abilities >= 0 ); - do { - #Log3 $name, 1, "Settings pro Device : ".$decode_json->{settings}[$settings]{name}; - #Log3 $name, 1, " - KEIN ARRAY" if ( ref( $decode_json->{settings}[$settings]{value} ) ne "ARRAY"); - #Log3 $name, 1, " - IST ARRAY" if ( ref( $decode_json->{settings}[$settings]{value} ) eq "ARRAY"); + my $winter_mode; - if ( exists($decode_json->{settings}[$settings]{name}) - && ( - $decode_json->{settings}[$settings]{name} =~ /schedules_paused_until_?\d?$/ - || $decode_json->{settings}[$settings]{name} eq 'eco_mode' - || $decode_json->{settings}[$settings]{name} eq 'winter_mode' ) - ) - { - if ( $hash->{helper}{$decode_json->{settings}[$settings]{name}.'_id'} ne + do { +#Log3 $name, 1, "Settings pro Device : ".$decode_json->{settings}[$settings]{name}; +#Log3 $name, 1, " - KEIN ARRAY" if ( ref( $decode_json->{settings}[$settings]{value} ) ne "ARRAY"); +#Log3 $name, 1, " - IST ARRAY" if ( ref( $decode_json->{settings}[$settings]{value} ) eq "ARRAY"); + + if ( + exists( $decode_json->{settings}[$settings]{name} ) + && ( $decode_json->{settings}[$settings]{name} =~ + /schedules_paused_until_?\d?$/ + || $decode_json->{settings}[$settings]{name} eq 'eco_mode' + || $decode_json->{settings}[$settings]{name} eq 'winter_mode' ) + ) + { + if ( $hash->{helper} + { $decode_json->{settings}[$settings]{name} . '_id' } ne $decode_json->{settings}[$settings]{id} ) { - $hash->{helper}{$decode_json->{settings}[$settings]{name}.'_id'} = + $hash->{helper} + { $decode_json->{settings}[$settings]{name} . '_id' } = $decode_json->{settings}[$settings]{id}; } + # check watering controler single schedules pause until + if ( $decode_json->{settings}[$settings]{name} eq 'schedules_paused_until' ) { + readingsBulkUpdateIfChanged( $hash, 'scheduling-schedules_paused_until', + $decode_json->{settings}[$settings]{value} ); + } + # save winter mode as reading - readingsBulkUpdateIfChanged( - $hash, - 'winter_mode', - $decode_json->{settings}[$settings]{value} - ) if ($decode_json->{settings}[$settings]{name} eq 'winter_mode'); + + if ( $decode_json->{settings}[$settings]{name} eq 'winter_mode' ) { + readingsBulkUpdateIfChanged( $hash, 'winter_mode', + $decode_json->{settings}[$settings]{value} ); + + $winter_mode = $decode_json->{settings}[$settings]{value}; + } } - + if ( ref( $decode_json->{settings}[$settings]{value} ) eq "ARRAY" && $decode_json->{settings}[$settings]{name} eq 'starting_points' ) { @@ -763,36 +819,111 @@ sub WriteReadings { $settings--; } while ( $settings >= 0 ); - - my $online_state = ReadingsVal($name , 'device_info-connection_status', 'unknown'); - + if ( $winter_mode ne 'hibernate' ) { + setState($hash); + } + else { + readingsBulkUpdate( $hash, 'state', + RigReadingsValue( $hash, 'hibernate' ) ); + } + + readingsEndUpdate( $hash, 1 ); + + Log3 $name, 4, "GardenaSmartDevice ($name) - readings was written"; + + return; +} + +sub setState { + my $hash = shift; + my $name = $hash->{NAME}; + + my $online_state = + ReadingsVal( $name, 'device_info-connection_status', 'unknown' ); + + #online state mower readingsBulkUpdate( $hash, 'state', - $online_state eq 'online' ? - ReadingsVal( $name, 'mower-status', 'readingsValError') : 'offline' - ) + $online_state eq 'online' + ? ReadingsVal( $name, 'mower-status', 'readingsValError' ) + : 'offline' ) if ( AttrVal( $name, 'model', 'unknown' ) eq 'mower' ); - readingsBulkUpdate( - $hash, 'state', - ( - ReadingsVal( $name, 'watering-watering_timer_1_duration', 0 ) - =~ m{\A[1-9]([0-9]+)?\z}xms - ? RigReadingsValue( $hash, 'open' ) - : RigReadingsValue( $hash, 'closed' ) - ) - ) if ( AttrVal( $name, 'model', 'unknown' ) eq 'watering_computer' ); + #online state water control + # zeitplan -> dauert pausiert wenn 2038-01-18T00:00:00.000Z - if ( AttrVal( $name, 'model', 'unknown' ) eq 'sensor' ) { - my $state_string = ( ReadingsVal($name, 'device_info-category', 'unknown') eq 'sensor') ? 'T: ' .ReadingsVal( $name, 'ambient_temperature-temperature', 'readingsValError' ) . '°C, ' : 'T: ' .ReadingsVal( $name, 'soil_temperature-temperature', 'readingsValError' ) . '°C, ' ; - $state_string .= 'H: '. ReadingsVal( $name, 'humidity-humidity', 'readingsValError' ). '%'; - $state_string .= ', L: ' . ReadingsVal( $name, 'light-light', 'readingsValError' ) . 'lux' if (ReadingsVal($name, 'device_info-category', 'unknown') eq 'sensor'); - - # if ( $online_state eq 'offline') { - # readingsBulkUpdate( $hash, 'humidity-humidity', '-1' ); - # readingsBulkUpdate( $hash, 'ambient_temperature-temperature', '-1' ) if (ReadingsVal($name, 'device_info-category', 'unknown') eq 'sensor'); - # readingsBulkUpdate( $hash, 'light-light', '-1' ) if (ReadingsVal($name, 'device_info-category', 'unknown') eq 'sensor'); - # } - readingsBulkUpdate($hash, 'state', $online_state eq 'online' ? $state_string : 'offline' ) + # watering-watering_timer_1_state idle | scheduled | manual + # watering-watering_timer_1_duration 0 in sec + # scheduling-scheduled_watering_next_start XXX + # scheduling-scheduled_watering_end | end < NOW && duration = 0 => abbruch manuell + # scheduling-schedules_paused_until + + + # 1. Ventil geschlossen, Zeitplan pausiert. + # App zeigt: nichts (wenn vorher ein Zeitplan abgebrochen wurde, steht da "Unterbrochen xx:yy - zz:aa") + # 2. Ventil geschlossen, Zeitplan aktiv. + # App zeigt: "Nächste Bewässerung heute um xx:yy Uhr" (wenn vorher ein Zeitplan abgebrochen wurde, steht da vorher auch "Unterbrochen xx:yy - zz:aa") + # 3. Ventil manuell geoeffnet, späterer Zeitplan aktiv. + # Wird bewässert xx Minuten verbleibend" und "Nächste Bewässerung heute um xx:yy Uhr" + # 4. Ventil manuell geoeffnet, Zeitpläne deaktiviert. + # App zeigt: "Wird bewässert xx Minuten verbleibend" + if ( AttrVal( $name, 'model', 'unknown' ) eq 'watering_computer' ){ + + my $state_string = ReadingsVal( $name, 'watering-watering_timer_1_duration', 0 ) =~ + m{\A[1-9]([0-9]+)?\z}xms + # offen + ? + ( ReadingsVal($name, 'scheduling-schedules_paused_until', '' ) eq '' ) + # leer ( zeitplan aktiv ... ) + ? sprintf( (RigReadingsValue($hash, 'will be irrigated %.f minutes remaining.').' '.RigReadingsValue($hash, 'next watering: %s')), (ReadingsVal( $name, 'watering-watering_timer_1_duration', 0 )/60), RigReadingsValue($hash, ReadingsVal($name, 'scheduling-scheduled_watering_next_start', '')) ) + # zeitplan pausiert + : + ( ReadingsVal($name, 'scheduling-schedules_paused_until', '') eq '2038-01-18T00:00:00.000Z') + # pause bis dauerhaft + ? sprintf( (RigReadingsValue($hash, 'will be irrigated %.f minutes remaining.').' '.RigReadingsValue($hash , 'schedule permanently paused')), (ReadingsVal( $name, 'watering-watering_timer_1_duration', 0 )/60) ) + # naechter termin + : sprintf( RigReadingsValue($hash , 'paused until %s'), RigReadingsValue($hash, ReadingsVal($name, 'scheduling-schedules_paused_until', '')) ) + # zu + : + ( ReadingsVal($name, 'scheduling-schedules_paused_until', '' ) eq '' ) + # zeitplan aktiv + ? sprintf( (RigReadingsValue($hash, 'closed') .'. '.RigReadingsValue($hash, 'next watering: %s')), RigReadingsValue($hash, ReadingsVal($name, 'scheduling-scheduled_watering_next_start', '') ) ) + # zeitplan pausiert + : RigReadingsValue($hash, 'closed') + ; + # state offline | override + $state_string = 'offline' if ($online_state eq 'offline'); + + readingsBulkUpdate( + $hash, 'state', RigReadingsValue( $hash, $state_string ) ); + } + + if ( AttrVal( $name, 'model', 'unknown' ) =~ /sensor.?/ ) { + my $state_string = + ( ReadingsVal( $name, 'device_info-category', 'unknown' ) eq + 'sensor' ) + ? 'T: ' + . ReadingsVal( $name, 'ambient_temperature-temperature', + 'readingsValError' ) + . '°C, ' + : 'T: ' + . ReadingsVal( $name, 'soil_temperature-temperature', + 'readingsValError' ) + . '°C, '; + $state_string .= 'H: ' + . ReadingsVal( $name, 'humidity-humidity', 'readingsValError' ) . '%'; + $state_string .= ', L: ' + . ReadingsVal( $name, 'light-light', 'readingsValError' ) . 'lux' + if ( ReadingsVal( $name, 'device_info-category', 'unknown' ) eq + 'sensor' ); + +# if ( $online_state eq 'offline') { +# readingsBulkUpdate( $hash, 'humidity-humidity', '-1' ); +# readingsBulkUpdate( $hash, 'ambient_temperature-temperature', '-1' ) if (ReadingsVal($name, 'device_info-category', 'unknown') eq 'sensor'); +# readingsBulkUpdate( $hash, 'light-light', '-1' ) if (ReadingsVal($name, 'device_info-category', 'unknown') eq 'sensor'); +# } + #online state sensor I II + readingsBulkUpdate( $hash, 'state', + $online_state eq 'online' ? RigReadingsValue( $hash, $state_string) : RigReadingsValue( $hash, 'offline') ); } readingsBulkUpdate( @@ -810,10 +941,6 @@ sub WriteReadings { ReadingsVal( $name, 'power-power_timer', 'no info from power-timer' ) ) if ( AttrVal( $name, 'model', 'unknown' ) eq 'power' ); - readingsEndUpdate( $hash, 1 ); - - Log3 $name, 4, "GardenaSmartDevice ($name) - readings was written"; - return; } @@ -827,22 +954,21 @@ sub ReadingLangGerman { my $name = $hash->{NAME}; my %langGermanMapp = ( - 'ok_cutting' => 'mähen', - 'paused' => 'pausiert', - 'ok_searching' => 'suche Ladestation', - 'ok_charging' => 'lädt', - 'ok_leaving' => 'unterwegs zum Startpunkt', - 'wait_updating' => 'wird aktualisiert ...', - 'wait_power_up' => 'wird eingeschaltet ...', - 'parked_timer' => 'geparkt nach Zeitplan', - 'parked_park_selected' => 'geparkt', - 'off_disabled' => 'der Mäher ist ausgeschaltet', - 'off_hatch_open' => - 'deaktiviert. Abdeckung ist offen oder PIN-Code erforderlich', - 'unknown' => 'unbekannter Status', - 'error' => 'Fehler', - 'error_at_power_up' => 'Neustart ...', - 'off_hatch_closed' => 'Deaktiviert. Manueller Start erforderlich', + 'ok_cutting' => 'mähen', + 'paused' => 'pausiert', + 'ok_searching' => 'suche Ladestation', + 'ok_charging' => 'lädt', + 'ok_leaving' => 'unterwegs zum Startpunkt', + 'wait_updating' => 'wird aktualisiert ...', + 'wait_power_up' => 'wird eingeschaltet ...', + 'parked_timer' => 'geparkt nach Zeitplan', + 'parked_park_selected' => 'geparkt', + 'off_disabled' => 'der Mäher ist ausgeschaltet', + 'off_hatch_open' => 'deaktiviert. Abdeckung ist offen oder PIN-Code erforderlich', + 'unknown' => 'unbekannter Status', + 'error' => 'Fehler', + 'error_at_power_up' => 'Neustart ...', + 'off_hatch_closed' => 'Deaktiviert. Manueller Start erforderlich', 'ok_cutting_timer_overridden' => 'manuelles mähen', 'parked_autotimer' => 'geparkt durch SensorControl', 'parked_daily_limit_reached' => 'abgeschlossen', @@ -880,50 +1006,54 @@ sub ReadingLangGerman { 'guide_2_not_found' => 'SK 2 nicht gefunden', 'guide_3_not_found' => 'SK 3 nicht gefunden', 'difficult_finding_home' => 'Problem die Ladestation zu finden', - 'guide_calibration_accomplished' => - 'Kalibrierung des Suchkabels beendet', - 'guide_calibration_failed' => - 'Kalibrierung des Suchkabels fehlgeschlagen', - 'temporary_battery_problem' => 'kurzzeitiges Batterieproblem', - 'battery_problem' => 'Batterieproblem', - 'alarm_mower_switched_off' => 'Alarm! Mäher ausgeschalten', - 'alarm_mower_stopped' => 'Alarm! Mäher gestoppt', - 'alarm_mower_lifted' => 'Alarm! Mäher angehoben', - 'alarm_mower_tilted' => 'Alarm! Mäher gekippt', - 'connection_changed' => 'Verbindung geändert', - 'connection_not_changed' => 'Verbindung nicht geändert', - 'com_board_not_available' => 'COM Board nicht verfügbar', - 'slipped' => 'rutscht', - 'out_of_operation' => 'ausser Betrieb', - 'replace_now' => 'kritischer Batteriestand, wechseln Sie jetzt', - 'low' => 'niedrig', - 'ok' => 'ok', - 'no_source' => 'ok', - 'mower_charging' => 'Mäher wurde geladen', - 'completed_cutting_autotimer' => 'Sensor Control erreicht', - 'week_timer' => 'Wochentimer erreicht', - 'countdown_timer' => 'Stoppuhr Timer', - 'undefined' => 'unklar', - 'unknown' => 'unklar', - 'status_device_unreachable' => 'Gerät ist nicht in Reichweite', - 'status_device_alive' => 'Gerät ist in Reichweite', - 'bad' => 'schlecht', - 'poor' => 'schwach', - 'good' => 'gut', - 'undefined' => 'unklar', - 'idle' => 'nichts zu tun', - 'firmware_cancel' => 'Firmwareupload unterbrochen', - 'firmware_upload' => 'Firmwareupload', - 'unsupported' => 'nicht unterstützt', - 'up_to_date' => 'auf dem neusten Stand', - 'mower' => 'Mäher', - 'watering_computer' => 'Bewässerungscomputer', - 'no_frost' => 'kein Frost', - 'open' => 'offen', - 'closed' => 'geschlossen', - 'included' => 'inbegriffen', - 'active' => 'aktiv', - 'inactive' => 'nicht aktiv' + 'guide_calibration_accomplished' => 'Kalibrierung des Suchkabels beendet', + 'guide_calibration_failed' => 'Kalibrierung des Suchkabels fehlgeschlagen', + 'temporary_battery_problem' => 'kurzzeitiges Batterieproblem', + 'battery_problem' => 'Batterieproblem', + 'alarm_mower_switched_off' => 'Alarm! Mäher ausgeschalten', + 'alarm_mower_stopped' => 'Alarm! Mäher gestoppt', + 'alarm_mower_lifted' => 'Alarm! Mäher angehoben', + 'alarm_mower_tilted' => 'Alarm! Mäher gekippt', + 'connection_changed' => 'Verbindung geändert', + 'connection_not_changed' => 'Verbindung nicht geändert', + 'com_board_not_available' => 'COM Board nicht verfügbar', + 'slipped' => 'rutscht', + 'out_of_operation' => 'ausser Betrieb', + 'replace_now' => 'kritischer Batteriestand, wechseln Sie jetzt', + 'low' => 'niedrig', + 'ok' => 'ok', + 'no_source' => 'ok', + 'mower_charging' => 'Mäher wurde geladen', + 'completed_cutting_autotimer' => 'Sensor Control erreicht', + 'week_timer' => 'Wochentimer erreicht', + 'countdown_timer' => 'Stoppuhr Timer', + 'undefined' => 'unklar', + 'unknown' => 'unklar', + 'status_device_unreachable' => 'Gerät ist nicht in Reichweite', + 'status_device_alive' => 'Gerät ist in Reichweite', + 'bad' => 'schlecht', + 'poor' => 'schwach', + 'good' => 'gut', + 'undefined' => 'unklar', + 'idle' => 'nichts zu tun', + 'firmware_cancel' => 'Firmwareupload unterbrochen', + 'firmware_upload' => 'Firmwareupload', + 'unsupported' => 'nicht unterstützt', + 'up_to_date' => 'auf dem neusten Stand', + 'mower' => 'Mäher', + 'watering_computer' => 'Bewässerungscomputer', + 'no_frost' => 'kein Frost', + 'open' => 'offen', + 'closed' => 'geschlossen', + 'included' => 'inbegriffen', + 'active' => 'aktiv', + 'inactive' => 'nicht aktiv', + 'hibernate' => 'Winterschlaf', + 'awake' => 'Aufgewacht', + 'schedule permanently paused' => 'Zeitplan dauerhaft pausiert', + 'paused until %s' => 'pausiert bis %s', + 'will be irrigated %.f minutes remaining.'=> 'Wird bewässert. %.f Minuten verbleibend.', + 'next watering: %s' => 'Nächste Bewässerung: %s', ); if ( @@ -977,7 +1107,7 @@ sub Zulu2LocalString { return ( sprintf( "%04d-%02d-%02d %02d:%02d:%s", - $lyear, $lmonth, $lday, + $lyear, $lmonth, $lday, $lhour, $datemin, substr( $rest, 0, 2 ) ) ); @@ -998,10 +1128,10 @@ sub Zulu2LocalString { } sub SetPredefinedStartPoints { - my $hash = shift; - my $aArg = shift; + my $hash = shift; + my $aArg = shift; - my ($startpoint_state,$startpoint_num,@morestartpoints) = @{$aArg}; + my ( $startpoint_state, $startpoint_num, @morestartpoints ) = @{$aArg}; my $name = $hash->{NAME}; my $payload; @@ -1049,6 +1179,7 @@ sub SetPredefinedStartPoints { $payload = '"settings": ' . encode_json($decode_json_settings); $abilities = 'mower_settings'; + #$abilities['service_id'] = $hash->{helper}{STARTINGPOINTID}; } else { @@ -1404,9 +1535,9 @@ sub SetPredefinedStartPoints { ], "release_status": "stable", "license": "GPL_2", - "version": "v2.4.2", + "version": "v2.4.3", "author": [ - "Marko Oldenburg " + "Marko Oldenburg " ], "x_fhem_maintainer": [ "CoolTux" diff --git a/controls_GardenaSmartDevice.txt b/controls_GardenaSmartDevice.txt index 94b7649..3810d2c 100644 --- a/controls_GardenaSmartDevice.txt +++ b/controls_GardenaSmartDevice.txt @@ -1,2 +1,2 @@ -UPD 2021-09-27_10:50:10 48067 FHEM/73_GardenaSmartBridge.pm -UPD 2021-09-27_10:50:10 56217 FHEM/74_GardenaSmartDevice.pm +UPD 2022-03-29_13:42:09 49520 FHEM/73_GardenaSmartBridge.pm +UPD 2022-04-12_08:40:29 61278 FHEM/74_GardenaSmartDevice.pm