diff --git a/fhem/CHANGED b/fhem/CHANGED index 586208c55..2bb1bbd66 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,8 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - change: 74_AutomowerConnect: Common.pm, automowerconnect.js + shift subs to library, mowing path: display only way point dots + alternatively - bugfix: 59_Weather: wunderground API fix lost temperature reading - bugfix: 88_HMCCU: Fixed scaling of non numeric reading values - bugfix: 74_AutomowerConnect: improved background image loading diff --git a/fhem/FHEM/74_AutomowerConnect.pm b/fhem/FHEM/74_AutomowerConnect.pm index 28a959bfe..c2c24984d 100644 --- a/fhem/FHEM/74_AutomowerConnect.pm +++ b/fhem/FHEM/74_AutomowerConnect.pm @@ -25,59 +25,13 @@ ################################################################################ package FHEM::AutomowerConnect; -my $cvsid = '$Id$'; +our $cvsid = '$Id$'; use strict; use warnings; use POSIX; # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt use GPUtils qw(:all); -use FHEM::Core::Authentication::Passwords qw(:ALL); - -use Time::HiRes qw(gettimeofday); -use Time::Local; -use DevIo; -use Storable qw(dclone retrieve store); - -# Import der FHEM Funktionen -BEGIN { - GP_Import( - qw( - AttrVal - CommandAttr - CommandDeleteReading - FmtDateTime - getKeyValue - InternalTimer - InternalVal - IsDisabled - Log3 - Log - minNum - maxNum - readingFnAttributes - readingsBeginUpdate - readingsBulkUpdate - readingsBulkUpdateIfChanged - readingsDelete - readingsEndUpdate - ReadingsNum - readingsSingleUpdate - ReadingsVal - RemoveInternalTimer - setKeyValue - defs - attr - modules - devspec2array - DevIo_IsOpen - DevIo_CloseDev - DevIo_OpenDev - DevIo_SimpleRead - DevIo_Ping - ) - ); -} GP_Export( qw( @@ -85,17 +39,8 @@ GP_Export( ) ); -my $missingModul = ""; - -eval "use JSON;1" or $missingModul .= "JSON "; -require HttpUtils; - require FHEM::Devices::AMConnect::Common; -use constant AUTHURL => 'https://api.authentication.husqvarnagroup.dev/v1'; -use constant APIURL => 'https://api.amc.husqvarna.dev/v1'; -use constant WSDEVICENAME => 'wss:ws.openapi.husqvarna.dev:443/v1'; - ############################################################## sub Initialize() { my ($hash) = @_; @@ -107,10 +52,10 @@ sub Initialize() { $hash->{ShutdownFn} = \&FHEM::Devices::AMConnect::Common::Shutdown; $hash->{RenameFn} = \&FHEM::Devices::AMConnect::Common::Rename; $hash->{FW_detailFn}= \&FHEM::Devices::AMConnect::Common::FW_detailFn; - $hash->{ReadFn} = \&wsRead; - $hash->{ReadyFn} = \&wsReady; - $hash->{SetFn} = \&Set; - $hash->{AttrFn} = \&Attr; + $hash->{ReadFn} = \&FHEM::Devices::AMConnect::Common::wsRead; + $hash->{ReadyFn} = \&FHEM::Devices::AMConnect::Common::wsReady; + $hash->{SetFn} = \&FHEM::Devices::AMConnect::Common::Set; + $hash->{AttrFn} = \&FHEM::Devices::AMConnect::Common::Attr; $hash->{AttrList} = "disable:1,0 " . "debug:1,0 " . "disabledForIntervals " . @@ -132,774 +77,13 @@ sub Initialize() { "propertyLimits:textField-long " . "weekdaysToResetWayPoints " . "numberOfWayPointsToDisplay " . - $readingFnAttributes; + $::readingFnAttributes; $::data{FWEXT}{AutomowerConnect}{SCRIPT} = "automowerconnect.js"; return undef; } - -############################################################## -# -# API AUTHENTICATION -# -############################################################## - -sub APIAuth { - my ( $hash, $update ) = @_; - my $name = $hash->{NAME}; - my $type = $hash->{TYPE}; - my $iam = "$type $name APIAuth:"; - my $interval = $hash->{helper}{interval}; - ( $hash->{VERSION} ) = $cvsid =~ /\.pm (.*)Z/ if ( !$hash->{VERSION} ); - - if ( IsDisabled($name) ) { - - readingsSingleUpdate( $hash,'device_state','disabled',1) if ( ReadingsVal( $name, 'device_state', '' ) ne 'disabled' ); - RemoveInternalTimer( $hash, \&wsReopen ); - RemoveInternalTimer( $hash, \&wsKeepAlive ); - DevIo_CloseDev( $hash ) if ( DevIo_IsOpen( $hash ) ); - RemoveInternalTimer( $hash, \&APIAuth ); - InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 ); - - return undef; - - } - - if ( !$update && $::init_done ) { - - if ( ReadingsVal( $name,'.access_token','' ) and gettimeofday() < (ReadingsVal( $name, '.expires', 0 ) - 45 ) ) { - - $hash->{header} = { "Authorization", "Bearer ". ReadingsVal( $name,'.access_token','' ) }; - readingsSingleUpdate( $hash, 'device_state', 'update', 1 ); - getMower( $hash ); - - } else { - - readingsSingleUpdate( $hash, 'device_state', 'authentification', 1 ); - RemoveInternalTimer( $hash, \&wsReopen ); - RemoveInternalTimer( $hash, \&wsKeepAlive ); - DevIo_CloseDev( $hash ) if ( DevIo_IsOpen( $hash ) ); - my $client_id = $hash->{helper}->{client_id}; - my $client_secret = $hash->{helper}->{passObj}->getReadPassword( $name ); - my $grant_type = $hash->{helper}->{grant_type}; - - my $header = "Content-Type: application/x-www-form-urlencoded\r\nAccept: application/json"; - my $data = 'grant_type=' . $grant_type.'&client_id=' . $client_id . '&client_secret=' . $client_secret; - ::HttpUtils_NonblockingGet( { - url => AUTHURL . '/oauth2/token', - timeout => 5, - hash => $hash, - method => 'POST', - header => $header, - data => $data, - callback => \&APIAuthResponse, - } ); - } - } else { - - RemoveInternalTimer( $hash, \&APIAuth ); - InternalTimer( gettimeofday() + 10, \&APIAuth, $hash, 0 ); - - } - return undef; -} - -######################### -sub APIAuthResponse { - my ($param, $err, $data) = @_; - my $hash = $param->{hash}; - my $name = $hash->{NAME}; - my $type = $hash->{TYPE}; - my $statuscode = $param->{code} // ''; - my $interval = $hash->{helper}{interval}; - my $iam = "$type $name APIAuthResponse:"; - - Log3 $name, 1, "\ndebug $iam \n\$statuscode [$statuscode]\n\$err [$err],\n \$data [$data] \n\$param->url $param->{url}" if ( AttrVal($name, 'debug', '') ); - - if( !$err && $statuscode == 200 && $data) { - - my $result = eval { decode_json($data) }; - if ($@) { - - Log3 $name, 2, "$iam JSON error [ $@ ]"; - readingsSingleUpdate( $hash, 'device_state', 'error JSON', 1 ); - - } else { - - $hash->{helper}->{auth} = $result; - $hash->{header} = { "Authorization", "Bearer $hash->{helper}{auth}{access_token}" }; - - # Update readings - readingsBeginUpdate($hash); - readingsBulkUpdateIfChanged($hash,'.access_token',$hash->{helper}{auth}{access_token},0 ); - readingsBulkUpdateIfChanged($hash,'.provider',$hash->{helper}{auth}{provider},0 ); - readingsBulkUpdateIfChanged($hash,'.user_id',$hash->{helper}{auth}{user_id},0 ); - - # refresh token between 00:00 and 01:00 - my $expire = $result->{expires_in} + gettimeofday(); - my ( @tim ) = localtime( $expire ); - my $seconds = $tim[0] + $tim[1] * 60 + $tim[2] * 3600; - if ($seconds > 3600) { - $tim[ 0 ] = 0; - $tim[ 1 ] = 0; - $tim[ 2 ] = 1; - $expire = timelocal( @tim ); - } - - $hash->{helper}{auth}{expires} = $expire; - readingsBulkUpdateIfChanged($hash,'.expires',$hash->{helper}{auth}{expires},0 ); - readingsBulkUpdateIfChanged($hash,'.scope',$hash->{helper}{auth}{scope},0 ); - readingsBulkUpdateIfChanged($hash,'.token_type',$hash->{helper}{auth}{token_type},0 ); - - my $expire_date = FmtDateTime($hash->{helper}{auth}{expires}); - readingsBulkUpdateIfChanged($hash,'api_token_expires',$expire_date ); - readingsBulkUpdateIfChanged($hash,'device_state', 'authenticated'); - readingsBulkUpdateIfChanged($hash,'mower_commandStatus', 'cleared'); - readingsEndUpdate($hash, 1); - - getMower( $hash ); - return undef; - } - - } else { - - readingsSingleUpdate( $hash, 'device_state', "error statuscode $statuscode", 1 ); - Log3 $name, 1, "\n$iam\n\$statuscode [$statuscode]\n\$err [$err],\n\$data [$data]\n\$param->url $param->{url}"; - - } - - RemoveInternalTimer( $hash, \&APIAuth ); - InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 ); - return undef; - -} - - -############################################################## -# -# GET MOWERS -# -############################################################## - -sub getMower { - - my ( $hash ) = @_; - my $name = $hash->{NAME}; - my $type = $hash->{TYPE}; - my $iam = "$type $name getMower:"; - my $access_token = ReadingsVal($name,".access_token",""); - my $provider = ReadingsVal($name,".provider",""); - my $client_id = $hash->{helper}->{client_id}; - - my $header = "Accept: application/vnd.api+json\r\nX-Api-Key: " . $client_id . "\r\nAuthorization: Bearer " . $access_token . "\r\nAuthorization-Provider: " . $provider; - Log3 $name, 5, "$iam header [ $header ]"; - - ::HttpUtils_NonblockingGet({ - url => APIURL . "/mowers", - timeout => 5, - hash => $hash, - method => "GET", - header => $header, - callback => \&getMowerResponse, - }); - - - return undef; -} - -######################### -sub getMowerResponse { - - my ( $param, $err, $data ) = @_; - my $hash = $param->{hash}; - my $name = $hash->{NAME}; - my $type = $hash->{TYPE}; - my $statuscode = $param->{code}; - my $iam = "$type $name getMowerResponse:"; - my $mowerNumber = $hash->{helper}{mowerNumber}; - - Log3 $name, 1, "\ndebug $iam \n\$statuscode [$statuscode]\n\$err [$err],\n \$data [$data] \n\$param->url $param->{url}" if ( AttrVal($name, 'debug', '') ); - - if( !$err && $statuscode == 200 && $data) { - - if ( $data eq "[]" ) { - - Log3 $name, 2, "$iam no mower data present"; - - } else { - - my $result = eval { decode_json($data) }; - if ($@) { - - Log3( $name, 2, "$iam - JSON error while request: $@"); - - } else { - - $hash->{helper}{mowers} = $result->{data}; - my $maxMower = 0; - $maxMower = @{$hash->{helper}{mowers}} if ( ref ( $hash->{helper}{mowers} ) eq 'ARRAY' ); - if ($maxMower <= $mowerNumber || $mowerNumber < 0 ) { - - Log3 $name, 2, "$iam wrong mower number $mowerNumber ($maxMower mower available). Change definition of $name."; - return undef; - - } - - my $foundMower .= '0 => ' . $hash->{helper}{mowers}[0]{attributes}{system}{name} . ' ' . $hash->{helper}{mowers}[0]{id}; - for (my $i = 1; $i < $maxMower; $i++) { - - $foundMower .= "\n" . $i .' => '. $hash->{helper}{mowers}[$i]{attributes}{system}{name} . ' ' . $hash->{helper}{mowers}[$i]{id}; - - } - Log3 $name, 5, "$iam found $foundMower "; - - if ( defined ($hash->{helper}{mower}{id}) ) { # update dataset - - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}; - $hash->{helper}{mowerold}{attributes}{mower}{activity} = $hash->{helper}{mower}{attributes}{mower}{activity}; - $hash->{helper}{mowerold}{attributes}{statistics}{numberOfCollisions} = $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions}; - - } else { # first data set - - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} = $hash->{helper}{mowers}[$mowerNumber]{attributes}{metadata}{statusTimestamp}; - $hash->{helper}{mowerold}{attributes}{mower}{activity} = $hash->{helper}{mowers}[$mowerNumber]{attributes}{mower}{activity}; - $hash->{helper}{mowerold}{attributes}{statistics}{numberOfCollisions} = $hash->{helper}{mowers}[$mowerNumber]{attributes}{statistics}{numberOfCollisions}; - - if ( AttrVal( $name, 'mapImageCoordinatesToRegister', '' ) eq '' ) { - ::FHEM::Devices::AMConnect::Common::posMinMax( $hash, $hash->{helper}{mowers}[$mowerNumber]{attributes}{positions} ); - } - - } - - $hash->{helper}{mower} = dclone( $hash->{helper}{mowers}[$mowerNumber] ); - $hash->{helper}{mower}{attributes}{positions}[0]{getMower} = 'from polling'; - $hash->{helper}{mower_id} = $hash->{helper}{mower}{id}; - $hash->{helper}{newdatasets} = 0; - - $hash->{helper}{storediff} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp}; - - ::FHEM::Devices::AMConnect::Common::calculateStatistics($hash); - - # Update readings - readingsBeginUpdate($hash); - - readingsBulkUpdateIfChanged($hash, 'api_MowerFound', $foundMower ); - ::FHEM::Devices::AMConnect::Common::fillReadings( $hash ); - - readingsEndUpdate($hash, 1); - - readingsSingleUpdate($hash, 'device_state', 'connected', 1 ); - - # schedule new access token - RemoveInternalTimer( $hash, \&APIAuth ); - InternalTimer( ReadingsVal($name, '.expires', 600)-37, \&APIAuth, $hash, 0 ); - - # Websocket initialisieren, schedule ping, reopen - RemoveInternalTimer( $hash, \&wsReopen ); - InternalTimer( gettimeofday() + 1.5, \&wsReopen, $hash, 0 ); - - return undef; - - } - } - - } else { - - readingsSingleUpdate( $hash, 'device_state', "error statuscode $statuscode", 1 ); - Log3 $name, 1, "\ndebug $iam \n\$statuscode [$statuscode]\n\$err [$err],\n \$data [$data] \n\$param->url $param->{url}"; - - } - RemoveInternalTimer( $hash, \&APIAuth ); - InternalTimer( gettimeofday() + $hash->{helper}{interval}, \&APIAuth, $hash, 0 ); - return undef; - -} - -######################### -sub wsKeepAlive { - my ($hash) = @_; - RemoveInternalTimer( $hash, \&wsKeepAlive); - DevIo_Ping($hash); - InternalTimer(gettimeofday() + 60, \&wsKeepAlive, $hash, 0); - -} - -######################### -sub wsInit { - - my ( $hash ) = @_; - $hash->{First_Read} = 1; - RemoveInternalTimer( $hash, \&wsReopen ); - RemoveInternalTimer( $hash, \&wsKeepAlive ); - InternalTimer( gettimeofday() + 7110, \&wsReopen, $hash, 0 ); - InternalTimer( gettimeofday() + 60, \&wsKeepAlive, $hash, 0 ); - return undef; - -} - -######################### -sub wsCb { - my ($hash, $error) = @_; - my $name = $hash->{NAME}; - my $type = $hash->{TYPE}; - my $iam = "$type $name wsCb:"; - Log3 $name, 2, "$iam failed with error: $error" if( $error ); - return undef; - -} - -######################### -sub wsReopen { - my ( $hash ) = @_; - RemoveInternalTimer( $hash, \&wsReopen ); - RemoveInternalTimer( $hash, \&wsKeepAlive ); - DevIo_CloseDev( $hash ) if ( DevIo_IsOpen( $hash ) ); - $hash->{DeviceName} = WSDEVICENAME; - DevIo_OpenDev( $hash, 0, \&wsInit, \&wsCb ); - -} - -######################### -sub wsRead { - my ($hash) = @_; - my $name = $hash->{NAME}; - my $type = $hash->{TYPE}; - my $iam = "$type $name wsRead:"; - - my $buf = DevIo_SimpleRead( $hash ); - return "" if ( !defined($buf) ); - - if ( $buf ) { - - my $result = eval { decode_json($buf) }; - - if ( $@ ) { - - Log3( $name, 2, "$iam - JSON error while request: $@"); - - } else { - - $hash->{helper}{wsResult} = $result; - - if ( defined( $result->{type} && $result->{id} eq $hash->{helper}{mower_id}) ) { - - if ( $result->{type} eq "status-event" ) { - - $hash->{helper}{statusTime} = gettimeofday(); - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}; - $hash->{helper}{mowerold}{attributes}{mower}{activity} = $hash->{helper}{mower}{attributes}{mower}{activity}; - $hash->{helper}{mower}{attributes}{battery} = dclone( $result->{attributes}{battery} ); - $hash->{helper}{mower}{attributes}{metadata} = dclone( $result->{attributes}{metadata} ); - $hash->{helper}{mower}{attributes}{mower} = dclone( $result->{attributes}{mower} ); - $hash->{helper}{mower}{attributes}{planner} = dclone( $result->{attributes}{planner} ); - $hash->{helper}{storediff} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp}; - - $hash->{helper}{detailFnNewPos} = 0; - ::FHEM::Devices::AMConnect::Common::isErrorThanPrepare( $hash ); - ::FHEM::Devices::AMConnect::Common::resetLastErrorIfCorrected( $hash ); - - } - - if ( $result->{type} eq "positions-event" ) { - - $hash->{helper}{positionsTime} = gettimeofday(); - # for ( my $i=0;$i<@{$result->{attributes}{positions}};$i++ ) { - # $result->{attributes}{positions}[ $i ]->{nr}=$i; - # }; - $hash->{helper}{mower}{attributes}{positions} = dclone( $result->{attributes}{positions} ); - - ::FHEM::Devices::AMConnect::Common::AlignArray( $hash ); - - my $deltaTime = $hash->{helper}{positionsTime} - $hash->{helper}{statusTime}; - - # if encounter positions shortly after status-event count it as error positions - if ( $hash->{helper}{mower}{attributes}{mower}{errorCode} && $deltaTime > 0 && $deltaTime < 0.29 && @{ $result->{attributes}{positions} } < 3) { - - $hash->{helper}{areapos}[ 0 ]{act} = 'N'; - $hash->{helper}{areapos}[ 1 ]{act} = 'N'; - $hash->{helper}{lasterror}{positions} = [dclone( $hash->{helper}{areapos}[ 0 ] ), dclone( $hash->{helper}{areapos}[ 1 ] ) ]; - $hash->{helper}{errorstack}[0]{positions} = [dclone( $hash->{helper}{areapos}[ 0 ] ), dclone( $hash->{helper}{areapos}[ 1 ] ) ]; - - } - - $hash->{helper}{detailFnNewPos} = scalar @{ $result->{attributes}{positions} }; - ::FHEM::Devices::AMConnect::Common::FW_detailFn_Update ($hash); - - } - - if ( $result->{type} eq "settings-event" ) { - - $hash->{helper}{mower}{attributes}{calendar} = dclone( $result->{attributes}{calendar} ) if ( defined ( $result->{attributes}{calendar} ) ); - $hash->{helper}{mower}{attributes}{settings}{headlight} = $result->{attributes}{headlight} if ( defined ( $result->{attributes}{headlight} ) ); - $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} = $result->{attributes}{cuttingHeight} if ( defined ( $result->{attributes}{cuttingHeight} ) ); - - } - - # Update readings - readingsBeginUpdate($hash); - - ::FHEM::Devices::AMConnect::Common::fillReadings( $hash ); - - readingsEndUpdate($hash, 1); - - } - - } - - } - Log3 $name, 4, "$iam received websocket data: >$buf<"; - - $hash->{First_Read} = 0; - return; - -} - -######################### -sub wsReady { - my ($hash ) = @_; - RemoveInternalTimer( $hash, \&wsReopen); - RemoveInternalTimer( $hash, \&wsKeepAlive); - return DevIo_OpenDev( $hash, 1, \&wsInit, \&wsCb ); - -} - -######################### -sub Set { - my ($hash,@val) = @_; - my $type = $hash->{TYPE}; - - return "$type $hash->{NAME} Set: needs at least one argument" if ( @val < 2 ); - - my ($name,$setName,$setVal,$setVal2,$setVal3) = @val; - my $iam = "$type $name Set:"; - - Log3 $name, 4, "$iam called with $setName " . ($setVal ? $setVal : "") if ($setName !~ /^(\?|client_secret)$/); - - if ( !IsDisabled($name) && $setName eq 'getUpdate' ) { - - RemoveInternalTimer($hash, \&APIAuth); - APIAuth($hash); - return undef; - - } elsif ( $setName eq 'chargingStationPositionToAttribute' ) { - - my $xm = $hash->{helper}{chargingStation}{longitude} // 10.1165; - my $ym = $hash->{helper}{chargingStation}{latitude} // 51.28; - CommandAttr( $hash, "$name chargingStationCoordinates $xm $ym" ); - return undef; - - } elsif ( $setName eq 'defaultDesignAttributesToAttribute' ) { - - my $design = $hash->{helper}{mapdesign}; - CommandAttr( $hash, "$name mapDesignAttributes $design" ); - return undef; - - } elsif ( $setName eq 'mapZonesTemplateToAttribute' ) { - - my $tpl = $hash->{helper}{mapZonesTpl}; - CommandAttr( $hash, "$name mapZones $tpl" ); - return undef; - - } elsif ( ReadingsVal( $name, 'device_state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName eq 'mowerScheduleToAttribute' ) { - - my $calendarjson = eval { JSON::XS->new->pretty(1)->encode ($hash->{helper}{mower}{attributes}{calendar}{tasks}) }; - if ( $@ ) { - return "$iam $@"; - } - CommandAttr($hash,"$name mowerSchedule $calendarjson"); - return undef; - - } elsif ( $setName eq 'client_secret' ) { - if ( $setVal ) { - - my ($passResp, $passErr) = $hash->{helper}->{passObj}->setStorePassword($name, $setVal); - Log3 $name, 1, "$iam error: $passErr" if ($passErr); - return "$iam $passErr" if( $passErr ); - - readingsBeginUpdate($hash); - readingsBulkUpdateIfChanged( $hash, '.access_token', '', 0 ); - readingsBulkUpdateIfChanged( $hash, 'device_state', 'initialized'); - readingsBulkUpdateIfChanged( $hash, 'mower_commandStatus', 'cleared'); - readingsEndUpdate($hash, 1); - - RemoveInternalTimer($hash, \&APIAuth); - APIAuth($hash); - return undef; - } - - } elsif ( ReadingsVal( $name, 'device_state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName =~ /^(Start|Park|cuttingHeight)$/ ) { - if ( $setVal =~ /^(\d+)$/) { - - ::FHEM::Devices::AMConnect::Common::CMD($hash ,$setName, $setVal); - return undef; - - } - - } elsif ( ReadingsVal( $name, 'device_state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName eq 'headlight' ) { - if ( $setVal =~ /^(ALWAYS_OFF|ALWAYS_ON|EVENING_ONLY|EVENING_AND_NIGHT)$/) { - - ::FHEM::Devices::AMConnect::Common::CMD($hash ,$setName, $setVal); - - return undef; - } - - } elsif ( !IsDisabled($name) && $setName eq 'getNewAccessToken' ) { - - readingsBeginUpdate($hash); - readingsBulkUpdateIfChanged( $hash, '.access_token', '', 0 ); - readingsBulkUpdateIfChanged( $hash, 'device_state', 'initialized'); - readingsBulkUpdateIfChanged( $hash, 'mower_commandStatus', 'cleared'); - readingsEndUpdate($hash, 1); - - RemoveInternalTimer($hash, \&APIAuth); - APIAuth($hash); - return undef; - - } elsif (ReadingsVal( $name, 'device_state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName =~ /ParkUntilFurtherNotice|ParkUntilNextSchedule|Pause|ResumeSchedule|sendScheduleFromAttributeToMower/) { - - ::FHEM::Devices::AMConnect::Common::CMD($hash,$setName); - return undef; - - } - my $ret = " getNewAccessToken:noArg ParkUntilFurtherNotice:noArg ParkUntilNextSchedule:noArg Pause:noArg Start:selectnumbers,60,60,600,0,lin Park:selectnumbers,60,60,600,0,lin ResumeSchedule:noArg getUpdate:noArg client_secret "; - $ret .= "chargingStationPositionToAttribute:noArg headlight:ALWAYS_OFF,ALWAYS_ON,EVENING_ONLY,EVENING_AND_NIGHT cuttingHeight:1,2,3,4,5,6,7,8,9 mowerScheduleToAttribute:noArg "; - $ret .= "sendScheduleFromAttributeToMower:noArg defaultDesignAttributesToAttribute:noArg mapZonesTemplateToAttribute:noArg "; - return "Unknown argument $setName, choose one of".$ret; - -} - -######################### -sub Attr { - - my ( $cmd, $name, $attrName, $attrVal ) = @_; - my $hash = $defs{$name}; - my $type = $hash->{TYPE}; - my $iam = "$type $name Attr:"; - ########## - if( $attrName eq "disable" ) { - if( $cmd eq "set" and $attrVal eq "1" ) { - - Log3 $name, 3, "$iam $cmd $attrName disabled"; - - } elsif( $cmd eq "del" or $cmd eq 'set' and !$attrVal ) { - - Log3 $name, 3, "$iam $cmd $attrName enabled"; - - } - - ########## - } elsif ( $attrName eq 'mapImagePath' ) { - - if( $cmd eq "set") { - - if ($attrVal =~ '(webp|png|jpg|jpeg)$' ) { - - $hash->{helper}{MAP_PATH} = $attrVal; - $hash->{helper}{MAP_MIME} = "image/".$1; - ::FHEM::Devices::AMConnect::Common::readMap( $hash ); - - if ( $attrVal =~ /(\d+)x(\d+)/ ) { - $attr{$name}{mapImageWidthHeight} = "$1 $2"; - } - - Log3 $name, 3, "$iam $cmd $attrName $attrVal"; - - } else { - - return "$iam $cmd $attrName wrong image type, use webp, png, jpeg or jpg"; - Log3 $name, 3, "$iam $cmd $attrName wrong image type, use webp, png, jpeg or jpg"; - - } - - } elsif( $cmd eq "del" ) { - - $hash->{helper}{MAP_PATH} = ''; - $hash->{helper}{MAP_CACHE} = ''; - $hash->{helper}{MAP_MIME} = ''; - Log3 $name, 3, "$iam $cmd $attrName"; - - } - - ########## - } elsif( $attrName eq "weekdaysToResetWayPoints" ) { - - if( $cmd eq "set" ) { - - return "$iam $attrName is invalid, enter a combination of weekday numbers, space or - [0123456 -]" unless( $attrVal =~ /0|1|2|3|4|5|6| |-/ ); - Log3 $name, 4, "$iam $cmd $attrName $attrVal"; - - } elsif( $cmd eq "del" ) { - - Log3 $name, 3, "$iam $cmd $attrName and set default to 1"; - - } - ########## - } elsif ( $attrName eq 'numberOfWayPointsToDisplay' ) { - - my $icurr = @{$hash->{helper}{areapos}}; - if( $cmd eq "set" && $attrVal =~ /\d+/ && $attrVal > $hash->{helper}{MOWING}{maxLengthDefault}) { - - # reduce array - $hash->{helper}{MOWING}{maxLength} = $attrVal; - for ( my $i = $icurr; $i > $attrVal; $i-- ) { - pop @{$hash->{helper}{areapos}}; - } - Log3 $name, 3, "$iam $cmd $attrName $attrVal"; - - } elsif( $cmd eq "del" ) { - - # reduce array - my $imax = $hash->{helper}{MOWING}{maxLengthDefault}; - $hash->{helper}{MOWING}{maxLength} = $imax; - for ( my $i = $icurr; $i > $imax; $i-- ) { - pop @{$hash->{helper}{areapos}}; - } - Log3 $name, 3, "$iam $cmd $attrName $attrName and set default $imax"; - - } - ########## - } elsif( $attrName eq "mapImageCoordinatesUTM" ) { - - if( $cmd eq "set" ) { - - if ( AttrVal( $name,'mapImageCoordinatesToRegister', '' ) && $attrVal =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)(\R|\s)(-?\d*\.?\d+)\s(-?\d*\.?\d+)/ ) { - - my ( $x1, $y1, $x2, $y2 ) = ( $1, $2, $4, $5 ); - AttrVal( $name,'mapImageCoordinatesToRegister', '' ) =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)(\R|\s)(-?\d*\.?\d+)\s(-?\d*\.?\d+)/; - my ( $lo1, $la1, $lo2, $la2 ) = ( $1, $2, $4, $5 ); - - return "$iam $attrName illegal value 0 for the difference of longitudes." unless ( $lo1 - $lo2 ); - return "$iam $attrName illegal value 0 for the difference of latitudes." unless ( $la1 - $la2 ); - - my $scx = int( ( $x1 - $x2) / ( $lo1 - $lo2 ) ); - my $scy = int( ( $y1 - $y2 ) / ( $la1 - $la2 ) ); - $attr{$name}{scaleToMeterXY} = "$scx $scy"; - - } else { - return "$iam $attrName has a wrong format use linewise pairs or the attribute mapImageCoordinatesToRegister was not set before."; - } - Log3 $name, 3, "$iam $cmd $attrName $attrVal"; - - } elsif( $cmd eq "del" ) { - - Log3 $name, 3, "$iam $cmd $attrName and set default 0 9090 0"; - - } - ########## - } elsif( $attrName eq "mapImageCoordinatesToRegister" ) { - - if( $cmd eq "set" ) { - - return "$iam $attrName has a wrong format use linewise pairs " unless( $attrVal =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)(\R|\s)(-?\d*\.?\d+)\s(-?\d*\.?\d+)/ ); - my ( $lo1, $la1, $lo2, $la2 ) = ( $1, $2, $4, $5 ); - return "$iam $attrName illegal value 0 for the difference of longitudes." unless ( $lo1 - $lo2 ); - return "$iam $attrName illegal value 0 for the difference of latitudes." unless ( $la1 - $la2 ); - - - - Log3 $name, 3, "$iam $cmd $attrName $attrVal"; - - } elsif( $cmd eq "del" ) { - - Log3 $name, 3, "$iam $cmd $attrName and set default 0 9090 0"; - - } - ########## - } elsif( $attrName eq "chargingStationCoordinates" ) { - - if( $cmd eq "set" ) { - - return "$iam $attrName has a wrong format use " unless( $attrVal =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)/ ); - Log3 $name, 3, "$iam $cmd $attrName $attrVal"; - - } elsif( $cmd eq "del" ) { - - Log3 $name, 3, "$iam $cmd $attrName and set default 10.1165 51.28"; - - } - ########## - } elsif( $attrName eq "mapImageWidthHeight" ) { - - if( $cmd eq "set" ) { - - return "$iam $attrName has a wrong format use " unless( $attrVal =~ /(\d+)\s(\d+)/ ); - Log3 $name, 3, "$iam $cmd $attrName $attrVal"; - - } elsif( $cmd eq "del" ) { - - Log3 $name, 3, "$iam $cmd $attrName and set default 100 200"; - - } - ########## - } elsif( $attrName eq "scaleToMeterXY" ) { - - if( $cmd eq "set" ) { - - return "$iam $attrName has a wrong format use " unless( $attrVal =~ /(-?\d+)\s(-?\d+)/ ); - Log3 $name, 3, "$iam $cmd $attrName $attrVal"; - - } elsif( $cmd eq "del" ) { - - Log3 $name, 3, "$iam $cmd $attrName and set default $hash->{helper}{scaleToMeterLongitude} $hash->{helper}{scaleToMeterLatitude}"; - - } - ########## - } elsif( $attrName eq "mowerSchedule" ) { - if( $cmd eq "set" ) { - - my $perl = eval { decode_json ($attrVal) }; - - if ($@) { - return "$iam $cmd $attrName decode error: $@ \n $perl"; - } - my $json = eval { encode_json ($perl) }; - if ($@) { - return "$iam $cmd $attrName encode error: $@ \n $json"; - } - Log3 $name, 4, "$iam $cmd $attrName mower schedule array"; - - } - ########## - } elsif( $attrName eq "mapZones" ) { - if( $cmd eq "set" ) { - - my $longitude = 10; - my $latitude = 52; - my $perl = eval { decode_json ($attrVal) }; - - if ($@) { - return "$iam $cmd $attrName decode error: $@ \n $attrVal"; - } - - for ( keys %{$perl} ) { - - $perl->{$_}{zoneCnt} = 0; - $perl->{$_}{zoneLength} = 0; - my $cond = eval "($perl->{$_}{condition})"; - - if ($@) { - return "$iam $cmd $attrName syntax error in condition: $@ \n $perl->{$_}{condition}"; - } - - } - - Log3 $name, 4, "$iam $cmd $attrName"; - $hash->{helper}{mapZones} = $perl; - - } elsif( $cmd eq "del" ) { - - delete $hash->{helper}{mapZones}; - delete $hash->{helper}{currentZone}; - CommandDeleteReading( $hash, "$name mower_currentZone" ); - Log3 $name, 3, "$iam $cmd $attrName"; - - } - } - return undef; -} - ############################################################## diff --git a/fhem/lib/FHEM/Devices/AMConnect/Common.pm b/fhem/lib/FHEM/Devices/AMConnect/Common.pm index 0598f8032..a5c437225 100644 --- a/fhem/lib/FHEM/Devices/AMConnect/Common.pm +++ b/fhem/lib/FHEM/Devices/AMConnect/Common.pm @@ -32,6 +32,7 @@ use POSIX; # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt use GPUtils qw(:all); +use FHEM::Core::Authentication::Passwords qw(:ALL); use Time::HiRes qw(gettimeofday); use Time::Local; @@ -44,6 +45,7 @@ BEGIN { qw( AttrVal CommandAttr + CommandDeleteReading FmtDateTime getKeyValue InternalTimer @@ -71,6 +73,9 @@ BEGIN { DevIo_IsOpen DevIo_CloseDev DevIo_setStates + DevIo_OpenDev + DevIo_SimpleRead + DevIo_Ping ) ); } @@ -78,6 +83,7 @@ BEGIN { my $missingModul = ""; eval "use JSON;1" or $missingModul .= "JSON "; +require HttpUtils; my $errorjson = '{"23":"Wheel drive problem, left","24":"Cutting system blocked","123":"Destination not reachable","710":"SIM card locked","50":"Guide 1 not found","717":"SMS could not be sent","108":"Folding cutting deck sensor defect","4":"Loop sensor problem - front","15":"Lifted","29":"Slope too steep","1":"Outside working area","45":"Cutting height problem - dir","52":"Guide 3 not found","28":"Memory circuit problem","95":"Folding sensor activated","9":"Trapped","114":"Too high discharge current","103":"Cutting drive motor 2 defect","65":"Temporary battery problem","119":"Zone generator problem","6":"Loop sensor problem - left","82":"Wheel motor blocked - rear right","714":"Geofence problem","703":"Connectivity problem","708":"SIM card locked","75":"Connection changed","7":"Loop sensor problem - right","35":"Wheel motor overloaded - right","3":"Wrong loop signal","117":"High internal power loss","0":"Unexpected error","80":"Cutting system imbalance - Warning","110":"Collision sensor error","100":"Ultrasonic Sensor 3 defect","79":"Invalid battery combination - Invalid combination of different battery types.","724":"Communication circuit board SW must be updated","86":"Wheel motor overloaded - rear right","81":"Safety function faulty","78":"Slipped - Mower has Slipped. Situation not solved with moving pattern","107":"Docking sensor defect","33":"Mower tilted","69":"Alarm! Mower switched off","68":"Temporary battery problem","34":"Cutting stopped - slope too steep","127":"Battery problem","73":"Alarm! Mower in motion","74":"Alarm! Outside geofence","713":"Geofence problem","87":"Wheel motor overloaded - rear left","120":"Internal voltage error","39":"Cutting motor problem","704":"Connectivity problem","63":"Temporary battery problem","109":"Loop sensor defect","38":"Electronic problem","64":"Temporary battery problem","113":"Complex working area","93":"No accurate position from satellites","104":"Cutting drive motor 3 defect","709":"SIM card not found","94":"Reference station communication problem","43":"Cutting height problem - drive","13":"No drive","44":"Cutting height problem - curr","118":"Charging system problem","14":"Mower lifted","57":"Guide calibration failed","707":"SIM card requires PIN","99":"Ultrasonic Sensor 2 defect","98":"Ultrasonic Sensor 1 defect","51":"Guide 2 not found","56":"Guide calibration accomplished","49":"Ultrasonic problem","2":"No loop signal","124":"Destination blocked","25":"Cutting system blocked","19":"Collision sensor problem, front","18":"Collision sensor problem - rear","48":"No response from charger","105":"Lift Sensor defect","111":"No confirmed position","10":"Upside down","40":"Limited cutting height range","716":"Connectivity problem","27":"Settings restored","90":"No power in charging station","21":"Wheel motor blocked - left","26":"Invalid sub-device combination","92":"Work area not valid","702":"Connectivity settings restored","125":"Battery needs replacement","5":"Loop sensor problem - rear","12":"Empty battery","55":"Difficult finding home","42":"Limited cutting height range","30":"Charging system problem","72":"Alarm! Mower tilted","85":"Wheel drive problem - rear left","8":"Wrong PIN code","62":"Temporary battery problem","102":"Cutting drive motor 1 defect","116":"High charging power loss","122":"CAN error","60":"Temporary battery problem","705":"Connectivity problem","711":"SIM card locked","70":"Alarm! Mower stopped","32":"Tilt sensor problem","37":"Charging current too high","89":"Invalid system configuration","76":"Connection NOT changed","71":"Alarm! Mower lifted","88":"Angular sensor problem","701":"Connectivity problem","715":"Connectivity problem","61":"Temporary battery problem","66":"Battery problem","106":"Collision sensor defect","67":"Battery problem","112":"Cutting system major imbalance","83":"Wheel motor blocked - rear left","84":"Wheel drive problem - rear right","126":"Battery near end of life","77":"Com board not available","36":"Wheel motor overloaded - left","31":"STOP button problem","17":"Charging station blocked","54":"Weak GPS signal","47":"Cutting height problem","53":"GPS navigation problem","121":"High internal temerature","97":"Left brush motor overloaded","712":"SIM card locked","20":"Wheel motor blocked - right","91":"Switch cord problem","96":"Right brush motor overloaded","58":"Temporary battery problem","59":"Temporary battery problem","22":"Wheel drive problem - right","706":"Poor signal quality","41":"Unexpected cutting height adj","46":"Cutting height blocked","11":"Low battery","16":"Stuck in charging station","101":"Ultrasonic Sensor 4 defect","115":"Too high internal current"}'; @@ -129,19 +135,25 @@ errorPathLineWidth="2" chargingStationPathLineColor="#999999" chargingStationPathLineDash="6,2" chargingStationPathLineWidth="1" +chargingStationPathDotWidth="2" otherActivityPathLineColor="#999999" otherActivityPathLineDash="6,2" otherActivityPathLineWidth="1" +otherActivityPathDotWidth="4" leavingPathLineColor="#33cc33" leavingPathLineDash="6,2" leavingPathLineWidth="2" +leavingPathDotWidth="4" goingHomePathLineColor="#0099ff" goingHomePathLineDash="6,2" goingHomePathLineWidth="2" +goingHomePathDotWidth="4" mowingPathDisplayStart="" mowingPathLineColor="#ff0000" mowingPathLineDash="6,2" -mowingPathLineWidth="1"'; +mowingPathLineWidth="1" +mowingPathDotWidth="4" +mowingPathUseDots="0"'; my $mapZonesTpl = '{ "01_oben" : { @@ -268,7 +280,7 @@ my $mapZonesTpl = '{ ); $hash->{MODEL} = ''; - $hash->{VERSION} = ''; + ( $hash->{VERSION} ) = $::FHEM::AutomowerConnect::cvsid =~ /\.pm (.*)Z/; $attr{$name}{room} = 'AutomowerConnect' if( !defined( $attr{$name}{room} ) ); $attr{$name}{icon} = 'automower' if( !defined( $attr{$name}{icon} ) ); ( $hash->{LIBRARY_VERSION} ) = $cvsid =~ /\.pm (.*)Z/; @@ -295,7 +307,7 @@ my $mapZonesTpl = '{ if( $hash->{helper}->{passObj}->getReadPassword($name) ) { RemoveInternalTimer($hash); - InternalTimer( gettimeofday() + 2, \&::FHEM::AutomowerConnect::APIAuth, $hash, 1); + InternalTimer( gettimeofday() + 2, \&APIAuth, $hash, 1); readingsSingleUpdate( $hash, 'device_state', 'defined', 1 ); @@ -326,7 +338,7 @@ sub Undefine { my $type = $hash->{TYPE}; RemoveInternalTimer( $hash ); - ::FHEM::Devices::AMConnect::Common::RemoveExtension("$type/$name/map"); + RemoveExtension("$type/$name/map"); return undef; } @@ -377,32 +389,32 @@ sub Get { if ( $setName eq 'html' ) { - my $ret = '' . ::FHEM::Devices::AMConnect::Common::FW_detailFn( undef, $name, undef, undef) . ''; + my $ret = '' . FW_detailFn( undef, $name, undef, undef) . ''; return $ret; } elsif ( $setName eq 'errorCodes' ) { - my $ret = ::FHEM::Devices::AMConnect::Common::listErrorCodes(); + my $ret = listErrorCodes(); return $ret; } elsif ( $setName eq 'InternalData' ) { - my $ret = ::FHEM::Devices::AMConnect::Common::listInternalData($hash); + my $ret = listInternalData($hash); return $ret; } elsif ( $setName eq 'MowerData' ) { - my $ret = ::FHEM::Devices::AMConnect::Common::listMowerData($hash); + my $ret = listMowerData($hash); return $ret; } elsif ( $setName eq 'StatisticsData' ) { - my $ret = ::FHEM::Devices::AMConnect::Common::listStatisticsData($hash); + my $ret = listStatisticsData($hash); return $ret; } elsif ( $setName eq 'errorStack' ) { - my $ret = ::FHEM::Devices::AMConnect::Common::listErrorStack($hash); + my $ret = listErrorStack($hash); return $ret; } else { @@ -418,7 +430,9 @@ sub FW_detailFn { my $hash = $defs{$name}; my $type = $hash->{TYPE}; return '' if( AttrVal($name, 'disable', 0) || !AttrVal($name, 'showMap', 1) ); + if ( $hash->{helper} && $hash->{helper}{mower} && $hash->{helper}{mower}{attributes} && $hash->{helper}{mower}{attributes}{positions} && @{$hash->{helper}{mower}{attributes}{positions}} > 0 ) { + my $img = "./fhem/$type/$name/map"; my $zoom=AttrVal( $name,"mapImageZoom", 0.7 ); my $backgroundcolor = AttrVal($name, 'mapBackgroundColor',''); @@ -514,8 +528,11 @@ sub FW_detailFn { InternalTimer( gettimeofday() + 1.5, \&FW_detailFn_Update, $hash, 0 ); return $ret; + } + return ''; + } ######################### @@ -587,6 +604,284 @@ sub FW_detailFn_Update { return undef; } +############################################################## +# +# API AUTHENTICATION +# +############################################################## + +sub APIAuth { + my ( $hash, $update ) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name APIAuth:"; + my $interval = $hash->{helper}{interval}; + # ( $hash->{VERSION} ) = $cvsid =~ /\.pm (.*)Z/ if ( !$hash->{VERSION} ); + + + if ( IsDisabled($name) ) { + + readingsSingleUpdate( $hash,'device_state','disabled',1) if ( ReadingsVal( $name, 'device_state', '' ) ne 'disabled' ); + RemoveInternalTimer( $hash, \&wsReopen ); + RemoveInternalTimer( $hash, \&wsKeepAlive ); + DevIo_CloseDev( $hash ) if ( DevIo_IsOpen( $hash ) ); + RemoveInternalTimer( $hash, \&APIAuth ); + InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 ); + + return undef; + + } + + if ( !$update && $::init_done ) { + + if ( ReadingsVal( $name,'.access_token','' ) and gettimeofday() < (ReadingsVal( $name, '.expires', 0 ) - 45 ) ) { + + $hash->{header} = { "Authorization", "Bearer ". ReadingsVal( $name,'.access_token','' ) }; + readingsSingleUpdate( $hash, 'device_state', 'update', 1 ); + getMower( $hash ); + + } else { + + readingsSingleUpdate( $hash, 'device_state', 'authentification', 1 ); + RemoveInternalTimer( $hash, \&wsReopen ); + RemoveInternalTimer( $hash, \&wsKeepAlive ); + DevIo_CloseDev( $hash ) if ( DevIo_IsOpen( $hash ) ); + my $client_id = $hash->{helper}->{client_id}; + my $client_secret = $hash->{helper}->{passObj}->getReadPassword( $name ); + my $grant_type = $hash->{helper}->{grant_type}; + + my $header = "Content-Type: application/x-www-form-urlencoded\r\nAccept: application/json"; + my $data = 'grant_type=' . $grant_type.'&client_id=' . $client_id . '&client_secret=' . $client_secret; + ::HttpUtils_NonblockingGet( { + url => AUTHURL . '/oauth2/token', + timeout => 5, + hash => $hash, + method => 'POST', + header => $header, + data => $data, + callback => \&APIAuthResponse, + } ); + } + } else { + + RemoveInternalTimer( $hash, \&APIAuth ); + InternalTimer( gettimeofday() + 10, \&APIAuth, $hash, 0 ); + + } + return undef; +} + +######################### +sub APIAuthResponse { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $statuscode = $param->{code} // ''; + my $interval = $hash->{helper}{interval}; + my $iam = "$type $name APIAuthResponse:"; + + Log3 $name, 1, "\ndebug $iam \n\$statuscode [$statuscode]\n\$err [$err],\n \$data [$data] \n\$param->url $param->{url}" if ( AttrVal($name, 'debug', '') ); + + if( !$err && $statuscode == 200 && $data) { + + my $result = eval { decode_json($data) }; + if ($@) { + + Log3 $name, 2, "$iam JSON error [ $@ ]"; + readingsSingleUpdate( $hash, 'device_state', 'error JSON', 1 ); + + } else { + + $hash->{helper}->{auth} = $result; + $hash->{header} = { "Authorization", "Bearer $hash->{helper}{auth}{access_token}" }; + + # Update readings + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged($hash,'.access_token',$hash->{helper}{auth}{access_token},0 ); + readingsBulkUpdateIfChanged($hash,'.provider',$hash->{helper}{auth}{provider},0 ); + readingsBulkUpdateIfChanged($hash,'.user_id',$hash->{helper}{auth}{user_id},0 ); + + # refresh token between 00:00 and 01:00 + my $expire = $result->{expires_in} + gettimeofday(); + my ( @tim ) = localtime( $expire ); + my $seconds = $tim[0] + $tim[1] * 60 + $tim[2] * 3600; + if ($seconds > 3600) { + $tim[ 0 ] = 0; + $tim[ 1 ] = 0; + $tim[ 2 ] = 1; + $expire = timelocal( @tim ); + } + + $hash->{helper}{auth}{expires} = $expire; + readingsBulkUpdateIfChanged($hash,'.expires',$hash->{helper}{auth}{expires},0 ); + readingsBulkUpdateIfChanged($hash,'.scope',$hash->{helper}{auth}{scope},0 ); + readingsBulkUpdateIfChanged($hash,'.token_type',$hash->{helper}{auth}{token_type},0 ); + + my $expire_date = FmtDateTime($hash->{helper}{auth}{expires}); + readingsBulkUpdateIfChanged($hash,'api_token_expires',$expire_date ); + readingsBulkUpdateIfChanged($hash,'device_state', 'authenticated'); + readingsBulkUpdateIfChanged($hash,'mower_commandStatus', 'cleared'); + readingsEndUpdate($hash, 1); + + getMower( $hash ); + return undef; + } + + } else { + + readingsSingleUpdate( $hash, 'device_state', "error statuscode $statuscode", 1 ); + Log3 $name, 1, "\n$iam\n\$statuscode [$statuscode]\n\$err [$err],\n\$data [$data]\n\$param->url $param->{url}"; + + } + + RemoveInternalTimer( $hash, \&APIAuth ); + InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 ); + return undef; + +} + + +############################################################## +# +# GET MOWERS +# +############################################################## + +sub getMower { + + my ( $hash ) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name getMower:"; + my $access_token = ReadingsVal($name,".access_token",""); + my $provider = ReadingsVal($name,".provider",""); + my $client_id = $hash->{helper}->{client_id}; + + my $header = "Accept: application/vnd.api+json\r\nX-Api-Key: " . $client_id . "\r\nAuthorization: Bearer " . $access_token . "\r\nAuthorization-Provider: " . $provider; + Log3 $name, 5, "$iam header [ $header ]"; + + ::HttpUtils_NonblockingGet({ + url => APIURL . "/mowers", + timeout => 5, + hash => $hash, + method => "GET", + header => $header, + callback => \&getMowerResponse, + }); + + + return undef; +} + +######################### +sub getMowerResponse { + + my ( $param, $err, $data ) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $statuscode = $param->{code}; + my $iam = "$type $name getMowerResponse:"; + my $mowerNumber = $hash->{helper}{mowerNumber}; + + Log3 $name, 1, "\ndebug $iam \n\$statuscode [$statuscode]\n\$err [$err],\n \$data [$data] \n\$param->url $param->{url}" if ( AttrVal($name, 'debug', '') ); + + if( !$err && $statuscode == 200 && $data) { + + if ( $data eq "[]" ) { + + Log3 $name, 2, "$iam no mower data present"; + + } else { + + my $result = eval { decode_json($data) }; + if ($@) { + + Log3( $name, 2, "$iam - JSON error while request: $@"); + + } else { + + $hash->{helper}{mowers} = $result->{data}; + my $maxMower = 0; + $maxMower = @{$hash->{helper}{mowers}} if ( ref ( $hash->{helper}{mowers} ) eq 'ARRAY' ); + if ($maxMower <= $mowerNumber || $mowerNumber < 0 ) { + + Log3 $name, 2, "$iam wrong mower number $mowerNumber ($maxMower mower available). Change definition of $name."; + return undef; + + } + + my $foundMower .= '0 => ' . $hash->{helper}{mowers}[0]{attributes}{system}{name} . ' ' . $hash->{helper}{mowers}[0]{id}; + for (my $i = 1; $i < $maxMower; $i++) { + + $foundMower .= "\n" . $i .' => '. $hash->{helper}{mowers}[$i]{attributes}{system}{name} . ' ' . $hash->{helper}{mowers}[$i]{id}; + + } + Log3 $name, 5, "$iam found $foundMower "; + + if ( defined ($hash->{helper}{mower}{id}) ) { # update dataset + + $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}; + $hash->{helper}{mowerold}{attributes}{mower}{activity} = $hash->{helper}{mower}{attributes}{mower}{activity}; + $hash->{helper}{mowerold}{attributes}{statistics}{numberOfCollisions} = $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions}; + + } else { # first data set + + $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} = $hash->{helper}{mowers}[$mowerNumber]{attributes}{metadata}{statusTimestamp}; + $hash->{helper}{mowerold}{attributes}{mower}{activity} = $hash->{helper}{mowers}[$mowerNumber]{attributes}{mower}{activity}; + $hash->{helper}{mowerold}{attributes}{statistics}{numberOfCollisions} = $hash->{helper}{mowers}[$mowerNumber]{attributes}{statistics}{numberOfCollisions}; + + if ( AttrVal( $name, 'mapImageCoordinatesToRegister', '' ) eq '' ) { + ::FHEM::Devices::AMConnect::Common::posMinMax( $hash, $hash->{helper}{mowers}[$mowerNumber]{attributes}{positions} ); + } + + } + + $hash->{helper}{mower} = dclone( $hash->{helper}{mowers}[$mowerNumber] ); + $hash->{helper}{mower}{attributes}{positions}[0]{getMower} = 'from polling'; + $hash->{helper}{mower_id} = $hash->{helper}{mower}{id}; + $hash->{helper}{newdatasets} = 0; + + $hash->{helper}{storediff} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp}; + + ::FHEM::Devices::AMConnect::Common::calculateStatistics($hash); + + # Update readings + readingsBeginUpdate($hash); + + readingsBulkUpdateIfChanged($hash, 'api_MowerFound', $foundMower ); + ::FHEM::Devices::AMConnect::Common::fillReadings( $hash ); + + readingsEndUpdate($hash, 1); + + readingsSingleUpdate($hash, 'device_state', 'connected', 1 ); + + # schedule new access token + RemoveInternalTimer( $hash, \&APIAuth ); + InternalTimer( ReadingsVal($name, '.expires', 600)-37, \&APIAuth, $hash, 0 ); + + # Websocket initialisieren, schedule ping, reopen + RemoveInternalTimer( $hash, \&wsReopen ); + InternalTimer( gettimeofday() + 1.5, \&wsReopen, $hash, 0 ); + + return undef; + + } + } + + } else { + + readingsSingleUpdate( $hash, 'device_state', "error statuscode $statuscode", 1 ); + Log3 $name, 1, "\ndebug $iam \n\$statuscode [$statuscode]\n\$err [$err],\n \$data [$data] \n\$param->url $param->{url}"; + + } + RemoveInternalTimer( $hash, \&APIAuth ); + InternalTimer( gettimeofday() + $hash->{helper}{interval}, \&APIAuth, $hash, 0 ); + return undef; + +} + ############################################################## # # SEND COMMAND @@ -715,6 +1010,341 @@ sub CMDResponse { return undef; } +######################### +sub Set { + my ($hash,@val) = @_; + my $type = $hash->{TYPE}; + + return "$type $hash->{NAME} Set: needs at least one argument" if ( @val < 2 ); + + my ($name,$setName,$setVal,$setVal2,$setVal3) = @val; + my $iam = "$type $name Set:"; + + Log3 $name, 4, "$iam called with $setName " . ($setVal ? $setVal : "") if ($setName !~ /^(\?|client_secret)$/); + + if ( !IsDisabled($name) && $setName eq 'getUpdate' ) { + + RemoveInternalTimer($hash, \&APIAuth); + APIAuth($hash); + return undef; + + } elsif ( $setName eq 'chargingStationPositionToAttribute' ) { + + my $xm = $hash->{helper}{chargingStation}{longitude} // 10.1165; + my $ym = $hash->{helper}{chargingStation}{latitude} // 51.28; + CommandAttr( $hash, "$name chargingStationCoordinates $xm $ym" ); + return undef; + + } elsif ( $setName eq 'defaultDesignAttributesToAttribute' ) { + + my $design = $hash->{helper}{mapdesign}; + CommandAttr( $hash, "$name mapDesignAttributes $design" ); + return undef; + + } elsif ( $setName eq 'mapZonesTemplateToAttribute' ) { + + my $tpl = $hash->{helper}{mapZonesTpl}; + CommandAttr( $hash, "$name mapZones $tpl" ); + return undef; + + } elsif ( ReadingsVal( $name, 'device_state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName eq 'mowerScheduleToAttribute' ) { + + my $calendarjson = eval { JSON::XS->new->pretty(1)->encode ($hash->{helper}{mower}{attributes}{calendar}{tasks}) }; + if ( $@ ) { + return "$iam $@"; + } + CommandAttr($hash,"$name mowerSchedule $calendarjson"); + return undef; + + } elsif ( $setName eq 'client_secret' ) { + if ( $setVal ) { + + my ($passResp, $passErr) = $hash->{helper}->{passObj}->setStorePassword($name, $setVal); + Log3 $name, 1, "$iam error: $passErr" if ($passErr); + return "$iam $passErr" if( $passErr ); + + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged( $hash, '.access_token', '', 0 ); + readingsBulkUpdateIfChanged( $hash, 'device_state', 'initialized'); + readingsBulkUpdateIfChanged( $hash, 'mower_commandStatus', 'cleared'); + readingsEndUpdate($hash, 1); + + RemoveInternalTimer($hash, \&APIAuth); + APIAuth($hash); + return undef; + } + + } elsif ( ReadingsVal( $name, 'device_state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName =~ /^(Start|Park|cuttingHeight)$/ ) { + if ( $setVal =~ /^(\d+)$/) { + + CMD($hash ,$setName, $setVal); + return undef; + + } + + } elsif ( ReadingsVal( $name, 'device_state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName eq 'headlight' ) { + if ( $setVal =~ /^(ALWAYS_OFF|ALWAYS_ON|EVENING_ONLY|EVENING_AND_NIGHT)$/) { + + CMD($hash ,$setName, $setVal); + + return undef; + } + + } elsif ( !IsDisabled($name) && $setName eq 'getNewAccessToken' ) { + + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged( $hash, '.access_token', '', 0 ); + readingsBulkUpdateIfChanged( $hash, 'device_state', 'initialized'); + readingsBulkUpdateIfChanged( $hash, 'mower_commandStatus', 'cleared'); + readingsEndUpdate($hash, 1); + + RemoveInternalTimer($hash, \&APIAuth); + APIAuth($hash); + return undef; + + } elsif (ReadingsVal( $name, 'device_state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName =~ /ParkUntilFurtherNotice|ParkUntilNextSchedule|Pause|ResumeSchedule|sendScheduleFromAttributeToMower/) { + + CMD($hash,$setName); + return undef; + + } + my $ret = " getNewAccessToken:noArg ParkUntilFurtherNotice:noArg ParkUntilNextSchedule:noArg Pause:noArg Start:selectnumbers,60,60,600,0,lin Park:selectnumbers,60,60,600,0,lin ResumeSchedule:noArg getUpdate:noArg client_secret "; + $ret .= "chargingStationPositionToAttribute:noArg headlight:ALWAYS_OFF,ALWAYS_ON,EVENING_ONLY,EVENING_AND_NIGHT cuttingHeight:1,2,3,4,5,6,7,8,9 mowerScheduleToAttribute:noArg "; + $ret .= "sendScheduleFromAttributeToMower:noArg defaultDesignAttributesToAttribute:noArg mapZonesTemplateToAttribute:noArg "; + return "Unknown argument $setName, choose one of".$ret; + +} + +######################### +sub Attr { + + my ( $cmd, $name, $attrName, $attrVal ) = @_; + my $hash = $defs{$name}; + my $type = $hash->{TYPE}; + my $iam = "$type $name Attr:"; + ########## + if( $attrName eq "disable" ) { + if( $cmd eq "set" and $attrVal eq "1" ) { + + Log3 $name, 3, "$iam $cmd $attrName disabled"; + + } elsif( $cmd eq "del" or $cmd eq 'set' and !$attrVal ) { + + Log3 $name, 3, "$iam $cmd $attrName enabled"; + + } + + ########## + } elsif ( $attrName eq 'mapImagePath' ) { + + if( $cmd eq "set") { + + if ($attrVal =~ '(webp|png|jpg|jpeg)$' ) { + + $hash->{helper}{MAP_PATH} = $attrVal; + $hash->{helper}{MAP_MIME} = "image/".$1; + ::FHEM::Devices::AMConnect::Common::readMap( $hash ); + + if ( $attrVal =~ /(\d+)x(\d+)/ ) { + $attr{$name}{mapImageWidthHeight} = "$1 $2"; + } + + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } else { + + return "$iam $cmd $attrName wrong image type, use webp, png, jpeg or jpg"; + Log3 $name, 3, "$iam $cmd $attrName wrong image type, use webp, png, jpeg or jpg"; + + } + + } elsif( $cmd eq "del" ) { + + $hash->{helper}{MAP_PATH} = ''; + $hash->{helper}{MAP_CACHE} = ''; + $hash->{helper}{MAP_MIME} = ''; + Log3 $name, 3, "$iam $cmd $attrName"; + + } + + ########## + } elsif( $attrName eq "weekdaysToResetWayPoints" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName is invalid, enter a combination of weekday numbers, space or - [0123456 -]" unless( $attrVal =~ /0|1|2|3|4|5|6| |-/ ); + Log3 $name, 4, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default to 1"; + + } + ########## + } elsif ( $attrName eq 'numberOfWayPointsToDisplay' ) { + + my $icurr = @{$hash->{helper}{areapos}}; + if( $cmd eq "set" && $attrVal =~ /\d+/ && $attrVal > $hash->{helper}{MOWING}{maxLengthDefault}) { + + # reduce array + $hash->{helper}{MOWING}{maxLength} = $attrVal; + for ( my $i = $icurr; $i > $attrVal; $i-- ) { + pop @{$hash->{helper}{areapos}}; + } + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + # reduce array + my $imax = $hash->{helper}{MOWING}{maxLengthDefault}; + $hash->{helper}{MOWING}{maxLength} = $imax; + for ( my $i = $icurr; $i > $imax; $i-- ) { + pop @{$hash->{helper}{areapos}}; + } + Log3 $name, 3, "$iam $cmd $attrName $attrName and set default $imax"; + + } + ########## + } elsif( $attrName eq "mapImageCoordinatesUTM" ) { + + if( $cmd eq "set" ) { + + if ( AttrVal( $name,'mapImageCoordinatesToRegister', '' ) && $attrVal =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)(\R|\s)(-?\d*\.?\d+)\s(-?\d*\.?\d+)/ ) { + + my ( $x1, $y1, $x2, $y2 ) = ( $1, $2, $4, $5 ); + AttrVal( $name,'mapImageCoordinatesToRegister', '' ) =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)(\R|\s)(-?\d*\.?\d+)\s(-?\d*\.?\d+)/; + my ( $lo1, $la1, $lo2, $la2 ) = ( $1, $2, $4, $5 ); + + return "$iam $attrName illegal value 0 for the difference of longitudes." unless ( $lo1 - $lo2 ); + return "$iam $attrName illegal value 0 for the difference of latitudes." unless ( $la1 - $la2 ); + + my $scx = int( ( $x1 - $x2) / ( $lo1 - $lo2 ) ); + my $scy = int( ( $y1 - $y2 ) / ( $la1 - $la2 ) ); + $attr{$name}{scaleToMeterXY} = "$scx $scy"; + + } else { + return "$iam $attrName has a wrong format use linewise pairs or the attribute mapImageCoordinatesToRegister was not set before."; + } + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default 0 9090 0"; + + } + ########## + } elsif( $attrName eq "mapImageCoordinatesToRegister" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName has a wrong format use linewise pairs " unless( $attrVal =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)(\R|\s)(-?\d*\.?\d+)\s(-?\d*\.?\d+)/ ); + my ( $lo1, $la1, $lo2, $la2 ) = ( $1, $2, $4, $5 ); + return "$iam $attrName illegal value 0 for the difference of longitudes." unless ( $lo1 - $lo2 ); + return "$iam $attrName illegal value 0 for the difference of latitudes." unless ( $la1 - $la2 ); + + + + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default 0 9090 0"; + + } + ########## + } elsif( $attrName eq "chargingStationCoordinates" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName has a wrong format use " unless( $attrVal =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)/ ); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default 10.1165 51.28"; + + } + ########## + } elsif( $attrName eq "mapImageWidthHeight" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName has a wrong format use " unless( $attrVal =~ /(\d+)\s(\d+)/ ); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default 100 200"; + + } + ########## + } elsif( $attrName eq "scaleToMeterXY" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName has a wrong format use " unless( $attrVal =~ /(-?\d+)\s(-?\d+)/ ); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default $hash->{helper}{scaleToMeterLongitude} $hash->{helper}{scaleToMeterLatitude}"; + + } + ########## + } elsif( $attrName eq "mowerSchedule" ) { + if( $cmd eq "set" ) { + + my $perl = eval { decode_json ($attrVal) }; + + if ($@) { + return "$iam $cmd $attrName decode error: $@ \n $perl"; + } + my $json = eval { encode_json ($perl) }; + if ($@) { + return "$iam $cmd $attrName encode error: $@ \n $json"; + } + Log3 $name, 4, "$iam $cmd $attrName mower schedule array"; + + } + ########## + } elsif( $attrName eq "mapZones" ) { + if( $cmd eq "set" ) { + + my $longitude = 10; + my $latitude = 52; + my $perl = eval { decode_json ($attrVal) }; + + if ($@) { + return "$iam $cmd $attrName decode error: $@ \n $attrVal"; + } + + for ( keys %{$perl} ) { + + $perl->{$_}{zoneCnt} = 0; + $perl->{$_}{zoneLength} = 0; + my $cond = eval "($perl->{$_}{condition})"; + + if ($@) { + return "$iam $cmd $attrName syntax error in condition: $@ \n $perl->{$_}{condition}"; + } + + } + + Log3 $name, 4, "$iam $cmd $attrName"; + $hash->{helper}{mapZones} = $perl; + + } elsif( $cmd eq "del" ) { + + delete $hash->{helper}{mapZones}; + delete $hash->{helper}{currentZone}; + CommandDeleteReading( $hash, "$name mower_currentZone" ); + Log3 $name, 3, "$iam $cmd $attrName"; + + } + } + return undef; +} + ######################### sub AlignArray { my ($hash) = @_; @@ -734,16 +1364,14 @@ sub AlignArray { map { $_->{act} = $hash->{helper}{$actold}{short} } @ar; - @ar = reverse @ar if ( $cnt > 1 ); # positions seem to be in reversed order - } else { map { $_->{act} = $hash->{helper}{$act}{short} } @ar; - @ar = reverse @ar if ( $cnt > 1 ); # positions seem to be in reversed order - } + @ar = reverse @ar if ( $cnt > 1 ); # positions seem to be in reversed order + $tmp = dclone( \@ar ); if ( @{ $hash->{helper}{areapos} } ) { @@ -1262,17 +1890,19 @@ sub listStatisticsData { $cnt++;$ret .= ' $hash->{helper}{statistics}{currentDayArea}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentDayArea} ) . ' qm '; $cnt++;$ret .= ' $hash->{helper}{statistics}{currentDayTime}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentDayTime} ) . ' s '; $cnt++;$ret .= ' calculated speed   ' . sprintf( "%.2f", $hash->{helper}{statistics}{currentDayTrack} / $hash->{helper}{statistics}{currentDayTime} ) . ' m/s ' if ( $hash->{helper}{statistics}{currentDayTime} ); + $cnt++;$ret .= ' $hash->{helper}{statistics}{lastDayTrack}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastDayTrack} ) . ' m '; $cnt++;$ret .= ' $hash->{helper}{statistics}{lastDayArea}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastDayArea} ) . ' qm '; $cnt++;$ret .= ' $hash->{helper}{statistics}{lastDayTime}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastDayTime} ) . ' s '; + $cnt++;$ret .= ' $hash->{helper}{statistics}{lastDayCollisions}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastDayCollisions} ) . ' '; + $cnt++;$ret .= ' last day calculated speed   ' . sprintf( "%.2f", $hash->{helper}{statistics}{lastDayTrack} / $hash->{helper}{statistics}{lastDayTime} ) . ' m/s ' if ( $hash->{helper}{statistics}{lastDayTime} ); + $cnt++;$ret .= ' $hash->{helper}{statistics}{currentWeekTrack}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentWeekTrack} ) . ' m '; $cnt++;$ret .= ' $hash->{helper}{statistics}{currentWeekArea}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentWeekArea} ) . ' qm '; $cnt++;$ret .= ' $hash->{helper}{statistics}{currentWeekTime}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentWeekTime} ) . ' s '; $cnt++;$ret .= ' $hash->{helper}{statistics}{lastWeekTrack}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastWeekTrack} ) . ' m '; $cnt++;$ret .= ' $hash->{helper}{statistics}{lastWeekArea}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastWeekArea} ) . ' qm '; $cnt++;$ret .= ' $hash->{helper}{statistics}{lastWeekTime}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastWeekTime} ) . ' s '; - - $cnt++;$ret .= ' $hash->{helper}{statistics}{lastDayCollisions}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastDayCollisions} ) . ' '; if ( AttrVal($name, 'mapZones', 0) && defined( $hash->{helper}{mapZones} ) ) { @@ -1486,8 +2116,9 @@ sub listInternalData { $ret .= 'Rest API Data'; $cnt++;$ret .= ' Link to APIsHusqvarna Developer'; - $cnt++;$ret .= ' Authentification API URL' . ::FHEM::AutomowerConnect::AUTHURL . ''; - $cnt++;$ret .= ' Automower Connect API URL' . ::FHEM::AutomowerConnect::APIURL . ''; + $cnt++;$ret .= ' Authentification API URL' . AUTHURL . ''; + $cnt++;$ret .= ' Automower Connect API URL' . APIURL . ''; + $cnt++;$ret .= ' Websocket IO Device name' . WSDEVICENAME . ''; $cnt++;$ret .= ' Client-Id' . $hash->{helper}{client_id} . ''; $cnt++;$ret .= ' Grant-Type' . $hash->{helper}{grant_type} . ''; $cnt++;$ret .= ' User-Id' . ReadingsVal($name, '.user_id', '-') . ''; @@ -1555,6 +2186,168 @@ sub FmtDateTimeGMT { my $ret = POSIX::strftime( "%F %H:%M:%S", gmtime( $ti ) ); } +############################################################## +# +# WEBSOCKET +# +############################################################## + +sub wsKeepAlive { + my ($hash) = @_; + RemoveInternalTimer( $hash, \&wsKeepAlive); + DevIo_Ping($hash); + InternalTimer(gettimeofday() + 60, \&wsKeepAlive, $hash, 0); + +} + +######################### +sub wsInit { + + my ( $hash ) = @_; + $hash->{First_Read} = 1; + RemoveInternalTimer( $hash, \&wsReopen ); + RemoveInternalTimer( $hash, \&wsKeepAlive ); + InternalTimer( gettimeofday() + 7110, \&wsReopen, $hash, 0 ); + InternalTimer( gettimeofday() + 60, \&wsKeepAlive, $hash, 0 ); + return undef; + +} + +######################### +sub wsCb { + my ($hash, $error) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name wsCb:"; + Log3 $name, 2, "$iam failed with error: $error" if( $error ); + return undef; + +} + +######################### +sub wsReopen { + my ( $hash ) = @_; + RemoveInternalTimer( $hash, \&wsReopen ); + RemoveInternalTimer( $hash, \&wsKeepAlive ); + DevIo_CloseDev( $hash ) if ( DevIo_IsOpen( $hash ) ); + $hash->{DeviceName} = WSDEVICENAME; + DevIo_OpenDev( $hash, 0, \&wsInit, \&wsCb ); + +} + +######################### +sub wsRead { + my ($hash) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name wsRead:"; + + my $buf = DevIo_SimpleRead( $hash ); + return "" if ( !defined($buf) ); + + if ( $buf ) { + + my $result = eval { decode_json($buf) }; + + if ( $@ ) { + + Log3( $name, 2, "$iam - JSON error while request: $@"); + + } else { + + if ( defined( $result->{type} ) ) { + + $hash->{helper}{wsResult}{$result->{type}} = $result; + $hash->{helper}{wsResult}{type} = $result->{type}; + + } else { + + $hash->{helper}{wsResult}{other} = $result; + + } + + if ( defined( $result->{type} && $result->{id} eq $hash->{helper}{mower_id}) ) { + + if ( $result->{type} eq "status-event" ) { + + $hash->{helper}{statusTime} = gettimeofday(); + $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}; + $hash->{helper}{mowerold}{attributes}{mower}{activity} = $hash->{helper}{mower}{attributes}{mower}{activity}; + $hash->{helper}{mower}{attributes}{battery} = dclone( $result->{attributes}{battery} ); + $hash->{helper}{mower}{attributes}{metadata} = dclone( $result->{attributes}{metadata} ); + $hash->{helper}{mower}{attributes}{mower} = dclone( $result->{attributes}{mower} ); + $hash->{helper}{mower}{attributes}{planner} = dclone( $result->{attributes}{planner} ); + $hash->{helper}{storediff} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp}; + + $hash->{helper}{detailFnNewPos} = 0; + isErrorThanPrepare( $hash ); + resetLastErrorIfCorrected( $hash ); + + } + + if ( $result->{type} eq "positions-event" ) { + + $hash->{helper}{positionsTime} = gettimeofday(); + # for ( my $i=0;$i<@{$result->{attributes}{positions}};$i++ ) { + # $result->{attributes}{positions}[ $i ]->{nr}=$i; + # }; + $hash->{helper}{mower}{attributes}{positions} = dclone( $result->{attributes}{positions} ); + + AlignArray( $hash ); + + my $deltaTime = $hash->{helper}{positionsTime} - $hash->{helper}{statusTime}; + + # if encounter positions shortly after status-event count it as error positions + if ( $hash->{helper}{mower}{attributes}{mower}{errorCode} && $deltaTime > 0 && $deltaTime < 0.29 && @{ $result->{attributes}{positions} } < 3) { + + $hash->{helper}{areapos}[ 0 ]{act} = 'N'; + $hash->{helper}{areapos}[ 1 ]{act} = 'N'; + $hash->{helper}{lasterror}{positions} = [dclone( $hash->{helper}{areapos}[ 0 ] ), dclone( $hash->{helper}{areapos}[ 1 ] ) ]; + $hash->{helper}{errorstack}[0]{positions} = [dclone( $hash->{helper}{areapos}[ 0 ] ), dclone( $hash->{helper}{areapos}[ 1 ] ) ]; + + } + + $hash->{helper}{detailFnNewPos} = scalar @{ $result->{attributes}{positions} }; + FW_detailFn_Update ($hash); + + } + + if ( $result->{type} eq "settings-event" ) { + + $hash->{helper}{mower}{attributes}{calendar} = dclone( $result->{attributes}{calendar} ) if ( defined ( $result->{attributes}{calendar} ) ); + $hash->{helper}{mower}{attributes}{settings}{headlight} = $result->{attributes}{headlight} if ( defined ( $result->{attributes}{headlight} ) ); + $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} = $result->{attributes}{cuttingHeight} if ( defined ( $result->{attributes}{cuttingHeight} ) ); + + } + + # Update readings + readingsBeginUpdate($hash); + + fillReadings( $hash ); + + readingsEndUpdate($hash, 1); + + } + + } + + } + Log3 $name, 4, "$iam received websocket data: >$buf<"; + + $hash->{First_Read} = 0; + return; + +} + +######################### +sub wsReady { + my ($hash ) = @_; + RemoveInternalTimer( $hash, \&wsReopen); + RemoveInternalTimer( $hash, \&wsKeepAlive); + return DevIo_OpenDev( $hash, 1, \&wsInit, \&wsCb ); + +} + ############################################################## diff --git a/fhem/www/pgm2/automowerconnect.js b/fhem/www/pgm2/automowerconnect.js index 34b8d8fa5..cf7c06248 100644 --- a/fhem/www/pgm2/automowerconnect.js +++ b/fhem/www/pgm2/automowerconnect.js @@ -153,21 +153,6 @@ function AutomowerConnectIcon( ctx, csx, csy, csrel, type ) { } } -function AutomowerConnectDrawPath ( ctx, div, pos, type ) { - // draw path - ctx.beginPath(); - ctx.strokeStyle = div.getAttribute( 'data-'+ type + 'LineColor' ); - ctx.lineWidth=div.getAttribute( 'data-'+ type + 'LineWidth' ); - ctx.setLineDash( div.getAttribute( 'data-'+ type + 'LineDash' ).split(",") ); - - ctx.moveTo(parseInt(pos[0]),parseInt(pos[1])); - for (var i=2;i-1; i-=3 ){ + + ctx.fillRect( parseInt( pos[ i ] ),parseInt( pos[ i + 1 ] ), fillWidth, fillWidth ); + + if ( colorat[ pos[ i + 2 ] ] != type ){ + + ctx.stroke(); + type = colorat[ pos[ i + 2 ] ]; + //~ ctx.beginPath(); + ctx.fillRect( parseInt( pos[ i ] ), parseInt( pos[ i + 1 ] ), fillWidth, fillWidth ); + ctx.fillStyle = div.getAttribute( 'data-'+ type + 'LineColor' ); + fillWidth=div.getAttribute( 'data-'+ type + 'DotWidth' ); + //~ ctx.setLineDash( div.getAttribute( 'data-'+ type + 'LineDash' ).split( "," ) ); + + } + } + + ctx.stroke(); + +} + function AutomowerConnectTor ( x0, y0, x1, y1 ) { var dy = y0-y1; var dx = x0-x1; @@ -324,7 +342,15 @@ function AutomowerConnectUpdateDetail (dev, type, detailfnfirst, picx, picy, sca if ( pos.length > 3 ) { // draw mowing path color - AutomowerConnectDrawPathColor ( ctx, div, pos, colorat ); + if ( div.getAttribute( 'data-mowingPathUseDots' ) ) { + + AutomowerConnectDrawDotColor ( ctx, div, pos, colorat ); + + } else { + + AutomowerConnectDrawPathColor ( ctx, div, pos, colorat ); + + } // draw start if ( div.getAttribute( 'data-mowingPathDisplayStart' ) ) {