2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-02-25 03:44:52 +00:00

74_AutomowerConnect: Common.pm, automowerconnect.js, use of websocket for realtime events, Forum: https://forum.fhem.de/index.php?topic=131661.msg1276678#msg1276678

git-svn-id: https://svn.fhem.de/fhem/trunk@27611 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
Ellert 2023-05-23 15:24:21 +00:00
parent b2a640e92a
commit e7380089ed
4 changed files with 475 additions and 294 deletions

View File

@ -1,5 +1,8 @@
# Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # 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. # Do not insert empty lines here, update check depends on it.
- change: 74_AutomowerConnect: Common.pm, automowerconnect.js
use of websocket for realtime events, more at
https://forum.fhem.de/index.php?topic=131661.msg1276678#msg1276678
- change: 50_SSChatBot: compatibility to DSM starting with DSM 7.2 RC, - change: 50_SSChatBot: compatibility to DSM starting with DSM 7.2 RC,
Forum: https://forum.fhem.de/index.php?msg=1276303 Forum: https://forum.fhem.de/index.php?msg=1276303
- change: 75_AutomowerConnectDevice: deleted, - change: 75_AutomowerConnectDevice: deleted,

View File

@ -35,7 +35,7 @@ use GPUtils qw(:all);
use FHEM::Core::Authentication::Passwords qw(:ALL); use FHEM::Core::Authentication::Passwords qw(:ALL);
use Time::HiRes qw(gettimeofday); use Time::HiRes qw(gettimeofday);
use Blocking; use DevIo;
use Storable qw(dclone retrieve store); use Storable qw(dclone retrieve store);
# Import der FHEM Funktionen # Import der FHEM Funktionen
@ -69,6 +69,11 @@ BEGIN {
attr attr
modules modules
devspec2array devspec2array
DevIo_IsOpen
DevIo_CloseDev
DevIo_OpenDev
DevIo_SimpleRead
DevIo_Ping
) )
); );
} }
@ -88,6 +93,7 @@ require FHEM::Devices::AMConnect::Common;
use constant AUTHURL => 'https://api.authentication.husqvarnagroup.dev/v1'; use constant AUTHURL => 'https://api.authentication.husqvarnagroup.dev/v1';
use constant APIURL => 'https://api.amc.husqvarna.dev/v1'; use constant APIURL => 'https://api.amc.husqvarna.dev/v1';
use constant WSDEVICENAME => 'wss:ws.openapi.husqvarna.dev:443/v1';
############################################################## ##############################################################
sub Initialize() { sub Initialize() {
@ -99,10 +105,11 @@ sub Initialize() {
$hash->{DeleteFn} = \&FHEM::Devices::AMConnect::Common::Delete; $hash->{DeleteFn} = \&FHEM::Devices::AMConnect::Common::Delete;
$hash->{RenameFn} = \&FHEM::Devices::AMConnect::Common::Rename; $hash->{RenameFn} = \&FHEM::Devices::AMConnect::Common::Rename;
$hash->{FW_detailFn}= \&FHEM::Devices::AMConnect::Common::FW_detailFn; $hash->{FW_detailFn}= \&FHEM::Devices::AMConnect::Common::FW_detailFn;
$hash->{ReadFn} = \&wsRead;
$hash->{ReadyFn} = \&wsReady;
$hash->{SetFn} = \&Set; $hash->{SetFn} = \&Set;
$hash->{AttrFn} = \&Attr; $hash->{AttrFn} = \&Attr;
$hash->{AttrList} = "interval " . $hash->{AttrList} = "disable:1,0 " .
"disable:1,0 " .
"debug:1,0 " . "debug:1,0 " .
"disabledForIntervals " . "disabledForIntervals " .
"mapImagePath " . "mapImagePath " .
@ -138,7 +145,7 @@ sub Initialize() {
############################################################## ##############################################################
sub APIAuth { sub APIAuth {
my ($hash, $update) = @_; my ( $hash, $update ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $type = $hash->{TYPE}; my $type = $hash->{TYPE};
my $iam = "$type $name APIAuth:"; my $iam = "$type $name APIAuth:";
@ -147,7 +154,10 @@ sub APIAuth {
if ( IsDisabled($name) ) { if ( IsDisabled($name) ) {
readingsSingleUpdate($hash,'state','disabled',1) if( ReadingsVal($name,'state','') ne 'disabled' ); 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 ); RemoveInternalTimer( $hash, \&APIAuth );
InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 ); InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 );
@ -157,21 +167,25 @@ sub APIAuth {
if ( !$update && $::init_done ) { if ( !$update && $::init_done ) {
if ( ReadingsVal( $name,'.access_token','' ) and gettimeofday() < (ReadingsVal($name, '.expires', 0) - $hash->{helper}{interval} - 60)) { if ( ReadingsVal( $name,'.access_token','' ) and gettimeofday() < (ReadingsVal( $name, '.expires', 0 ) - 45 ) ) {
readingsSingleUpdate( $hash, 'state', 'update', 1 ); $hash->{header} = { "Authorization", "Bearer ". ReadingsVal( $name,'.access_token','' ) };
readingsSingleUpdate( $hash, 'device_state', 'update', 1 );
getMower( $hash ); getMower( $hash );
} else { } else {
readingsSingleUpdate( $hash, 'state', 'authentification', 1 ); 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_id = $hash->{helper}->{client_id};
my $client_secret = $hash->{helper}->{passObj}->getReadPassword($name); my $client_secret = $hash->{helper}->{passObj}->getReadPassword( $name );
my $grant_type = $hash->{helper}->{grant_type}; my $grant_type = $hash->{helper}->{grant_type};
my $header = "Content-Type: application/x-www-form-urlencoded\r\nAccept: application/json"; 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; my $data = 'grant_type=' . $grant_type.'&client_id=' . $client_id . '&client_secret=' . $client_secret;
::HttpUtils_NonblockingGet({ ::HttpUtils_NonblockingGet( {
url => AUTHURL . '/oauth2/token', url => AUTHURL . '/oauth2/token',
timeout => 5, timeout => 5,
hash => $hash, hash => $hash,
@ -179,12 +193,12 @@ sub APIAuth {
header => $header, header => $header,
data => $data, data => $data,
callback => \&APIAuthResponse, callback => \&APIAuthResponse,
}); } );
} }
} else { } else {
RemoveInternalTimer( $hash, \&APIAuth); RemoveInternalTimer( $hash, \&APIAuth );
InternalTimer(gettimeofday() + 20, \&APIAuth, $hash, 0); InternalTimer( gettimeofday() + 20, \&APIAuth, $hash, 0 );
} }
return undef; return undef;
@ -208,11 +222,12 @@ sub APIAuthResponse {
if ($@) { if ($@) {
Log3 $name, 2, "$iam JSON error [ $@ ]"; Log3 $name, 2, "$iam JSON error [ $@ ]";
readingsSingleUpdate( $hash, 'state', 'error JSON', 1 ); readingsSingleUpdate( $hash, 'device_state', 'error JSON', 1 );
} else { } else {
$hash->{helper}->{auth} = $result; $hash->{helper}->{auth} = $result;
$hash->{header} = { "Authorization", "Bearer $hash->{helper}{auth}{access_token}" };
# Update readings # Update readings
readingsBeginUpdate($hash); readingsBeginUpdate($hash);
@ -227,7 +242,7 @@ sub APIAuthResponse {
my $expire_date = FmtDateTime($hash->{helper}{auth}{expires}); my $expire_date = FmtDateTime($hash->{helper}{auth}{expires});
readingsBulkUpdateIfChanged($hash,'api_token_expires',$expire_date ); readingsBulkUpdateIfChanged($hash,'api_token_expires',$expire_date );
readingsBulkUpdateIfChanged($hash,'state', 'authenticated'); readingsBulkUpdateIfChanged($hash,'device_state', 'authenticated');
readingsBulkUpdateIfChanged($hash,'mower_commandStatus', 'cleared'); readingsBulkUpdateIfChanged($hash,'mower_commandStatus', 'cleared');
readingsEndUpdate($hash, 1); readingsEndUpdate($hash, 1);
@ -237,8 +252,7 @@ sub APIAuthResponse {
} else { } else {
readingsSingleUpdate( $hash, 'device_state', "error statuscode $statuscode", 1 );
readingsSingleUpdate( $hash, 'state', "error statuscode $statuscode", 1 );
Log3 $name, 1, "\n$iam\n\$statuscode [$statuscode]\n\$err [$err],\n\$data [$data]\n\$param->url $param->{url}"; Log3 $name, 1, "\n$iam\n\$statuscode [$statuscode]\n\$err [$err],\n\$data [$data]\n\$param->url $param->{url}";
} }
@ -258,7 +272,7 @@ sub APIAuthResponse {
sub getMower { sub getMower {
my ($hash) = @_; my ( $hash ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $type = $hash->{TYPE}; my $type = $hash->{TYPE};
my $iam = "$type $name getMower:"; my $iam = "$type $name getMower:";
@ -285,12 +299,11 @@ sub getMower {
######################### #########################
sub getMowerResponse { sub getMowerResponse {
my ($param, $err, $data) = @_; my ( $param, $err, $data ) = @_;
my $hash = $param->{hash}; my $hash = $param->{hash};
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $type = $hash->{TYPE}; my $type = $hash->{TYPE};
my $statuscode = $param->{code}; my $statuscode = $param->{code};
my $interval = $hash->{helper}{interval};
my $iam = "$type $name getMowerResponse:"; my $iam = "$type $name getMowerResponse:";
my $mowerNumber = $hash->{helper}{mowerNumber}; my $mowerNumber = $hash->{helper}{mowerNumber};
@ -320,20 +333,22 @@ sub getMowerResponse {
return undef; return undef;
} }
my $foundMower .= '0 => '.$hash->{helper}{mowers}[0]{attributes}{system}{name};
my $foundMower .= '0 => ' . $hash->{helper}{mowers}[0]{attributes}{system}{name} . ' ' . $hash->{helper}{mowers}[0]{id};
for (my $i = 1; $i < $maxMower; $i++) { for (my $i = 1; $i < $maxMower; $i++) {
$foundMower .= ' | '.$i.' => '.$hash->{helper}{mowers}[$i]{attributes}{system}{name};
$foundMower .= "\n" . $i .' => '. $hash->{helper}{mowers}[$i]{attributes}{system}{name} . ' ' . $hash->{helper}{mowers}[$i]{id};
} }
Log3 $name, 5, "$iam found $foundMower "; Log3 $name, 5, "$iam found $foundMower ";
if ( defined ($hash->{helper}{mower}{id}) ){ # update dataset if ( defined ($hash->{helper}{mower}{id}) ) { # update dataset
$hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}; $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}{mower}{activity} = $hash->{helper}{mower}{attributes}{mower}{activity};
} else { # first data set } else { # first data set
$hash->{helper}{searchpos} = [ dclone( $hash->{helper}{mowers}[$mowerNumber]{attributes}{positions}[0] ), dclone( $hash->{helper}{mowers}[$mowerNumber]{attributes}{positions}[1] ) ];
$hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} = $hash->{helper}{mowers}[$mowerNumber]{attributes}{metadata}{statusTimestamp}; $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}{mower}{activity} = $hash->{helper}{mowers}[$mowerNumber]{attributes}{mower}{activity};
@ -344,17 +359,11 @@ sub getMowerResponse {
} }
$hash->{helper}{mower} = dclone( $hash->{helper}{mowers}[$mowerNumber] ); $hash->{helper}{mower} = dclone( $hash->{helper}{mowers}[$mowerNumber] );
# add alignment data set (last matched search positions) to the end $hash->{helper}{mower}{attributes}{positions}[0]{getMower} = 'from polling';
push( @{ $hash->{helper}{mower}{attributes}{positions} }, @{ dclone( $hash->{helper}{searchpos} ) } ); $hash->{helper}{mower_id} = $hash->{helper}{mower}{id};
$hash->{helper}{newdatasets} = 0; $hash->{helper}{newdatasets} = 0;
$hash->{helper}{storediff} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp}; $hash->{helper}{storediff} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp};
if ($hash->{helper}{storediff}) {
::FHEM::Devices::AMConnect::Common::AlignArray( $hash );
::FHEM::Devices::AMConnect::Common::FW_detailFn_Update ($hash) if (AttrVal($name,'showMap',1));
}
# Update readings # Update readings
readingsBeginUpdate($hash); readingsBeginUpdate($hash);
@ -364,12 +373,16 @@ sub getMowerResponse {
readingsEndUpdate($hash, 1); readingsEndUpdate($hash, 1);
::FHEM::Devices::AMConnect::Common::calculateStatistics( $hash ); readingsSingleUpdate($hash, 'device_state', 'connected', 1 );
readingsSingleUpdate($hash, 'state', 'connected', 1 ); # initialize statistics
::FHEM::Devices::AMConnect::Common::initStatistics($hash);
# schedule new access token
RemoveInternalTimer( $hash, \&APIAuth ); RemoveInternalTimer( $hash, \&APIAuth );
InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 ); InternalTimer( ReadingsVal($name, '.expires', 600)-37, \&APIAuth, $hash, 0 );
# Websocket initialisieren, schedule ping, reopen
wsReopen( $hash );
return undef; return undef;
@ -378,16 +391,143 @@ sub getMowerResponse {
} else { } else {
readingsSingleUpdate( $hash, 'state', "error statuscode $statuscode", 1 ); 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}"; Log3 $name, 1, "\ndebug $iam \n\$statuscode [$statuscode]\n\$err [$err],\n \$data [$data] \n\$param->url $param->{url}";
} }
RemoveInternalTimer( $hash, \&APIAuth ); RemoveInternalTimer( $hash, \&APIAuth );
InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 ); InternalTimer( gettimeofday() + $hash->{helper}{interval}, \&APIAuth, $hash, 0 );
return undef; 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};
}
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 );
::FHEM::Devices::AMConnect::Common::FW_detailFn_Update ($hash) if (AttrVal($name,'showMap',1));
}
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 { sub Set {
@ -426,7 +566,7 @@ sub Set {
CommandAttr( $hash, "$name mapZones $tpl" ); CommandAttr( $hash, "$name mapZones $tpl" );
return undef; return undef;
} elsif ( ReadingsVal( $name, 'state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName eq 'mowerScheduleToAttribute' ) { } 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}) }; my $calendarjson = eval { JSON::XS->new->pretty(1)->encode ($hash->{helper}{mower}{attributes}{calendar}{tasks}) };
if ( $@ ) { if ( $@ ) {
@ -446,7 +586,7 @@ sub Set {
return undef; return undef;
} }
} elsif ( ReadingsVal( $name, 'state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName =~ /^(Start|Park|cuttingHeight)$/ ) { } elsif ( ReadingsVal( $name, 'device_state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName =~ /^(Start|Park|cuttingHeight)$/ ) {
if ( $setVal =~ /^(\d+)$/) { if ( $setVal =~ /^(\d+)$/) {
::FHEM::Devices::AMConnect::Common::CMD($hash ,$setName, $setVal); ::FHEM::Devices::AMConnect::Common::CMD($hash ,$setName, $setVal);
@ -454,7 +594,7 @@ sub Set {
} }
} elsif ( ReadingsVal( $name, 'state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName eq 'headlight' ) { } 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)$/) { if ( $setVal =~ /^(ALWAYS_OFF|ALWAYS_ON|EVENING_ONLY|EVENING_AND_NIGHT)$/) {
::FHEM::Devices::AMConnect::Common::CMD($hash ,$setName, $setVal); ::FHEM::Devices::AMConnect::Common::CMD($hash ,$setName, $setVal);
@ -466,7 +606,7 @@ sub Set {
readingsBeginUpdate($hash); readingsBeginUpdate($hash);
readingsBulkUpdateIfChanged( $hash, '.access_token', '', 0 ); readingsBulkUpdateIfChanged( $hash, '.access_token', '', 0 );
readingsBulkUpdateIfChanged( $hash, 'state', 'initialized'); readingsBulkUpdateIfChanged( $hash, 'device_state', 'initialized');
readingsBulkUpdateIfChanged( $hash, 'mower_commandStatus', 'cleared'); readingsBulkUpdateIfChanged( $hash, 'mower_commandStatus', 'cleared');
readingsEndUpdate($hash, 1); readingsEndUpdate($hash, 1);
@ -474,7 +614,7 @@ sub Set {
APIAuth($hash); APIAuth($hash);
return undef; return undef;
} elsif (ReadingsVal( $name, 'state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName =~ /ParkUntilFurtherNotice|ParkUntilNextSchedule|Pause|ResumeSchedule|sendScheduleFromAttributeToMower/) { } elsif (ReadingsVal( $name, 'device_state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName =~ /ParkUntilFurtherNotice|ParkUntilNextSchedule|Pause|ResumeSchedule|sendScheduleFromAttributeToMower/) {
::FHEM::Devices::AMConnect::Common::CMD($hash,$setName); ::FHEM::Devices::AMConnect::Common::CMD($hash,$setName);
return undef; return undef;
@ -572,21 +712,6 @@ sub Attr {
} }
########## ##########
} elsif( $attrName eq "interval" ) {
if( $cmd eq "set" ) {
return "$iam $cmd $attrName $attrVal Interval must be greater than 0, recommended 420" unless($attrVal > 0);
$hash->{helper}->{interval} = $attrVal;
Log3 $name, 3, "$iam $cmd $attrName $attrVal";
} elsif( $cmd eq "del" ) {
$hash->{helper}->{interval} = 420;
Log3 $name, 3, "$iam $cmd $attrName and set default 420";
}
##########
} elsif( $attrName eq "mapImageCoordinatesUTM" ) { } elsif( $attrName eq "mapImageCoordinatesUTM" ) {
if( $cmd eq "set" ) { if( $cmd eq "set" ) {
@ -692,7 +817,9 @@ sub Attr {
for ( keys %{$perl} ) { for ( keys %{$perl} ) {
my $cond = eval "($perl->{$_}{condition})"; $perl->{$_}{zoneCnt} = 0;
$perl->{$_}{zoneLength} = 0;
my $cond = eval "($perl->{$_}{condition})";
if ($@) { if ($@) {
return "$iam $cmd $attrName syntax error in condition: $@ \n $perl->{$_}{condition}"; return "$iam $cmd $attrName syntax error in condition: $@ \n $perl->{$_}{condition}";
@ -749,14 +876,6 @@ __END__
<li>Cutting height can be set for each zone differently. </li> <li>Cutting height can be set for each zone differently. </li>
<li>All API data is stored in the device hash. Use <code>{Dumper $defs{&lt;name&gt;}}</code> in the commandline to find the data and build userReadings out of it.</li><br> <li>All API data is stored in the device hash. Use <code>{Dumper $defs{&lt;name&gt;}}</code> in the commandline to find the data and build userReadings out of it.</li><br>
</ul> </ul>
<u><b>Limits for the Automower Connect API</b></u>
<br><br>
<ul>
<li>Max 1 request per second and application key.</li>
<li>Max 10 000 request per month and application key.</li>
<li>'There is a timeout of 10 minutes in the mower to preserve data traffic and save battery...'</li>
<li>This results in a recommended interval of 600 seconds.</li><br>
</ul>
<u><b>Requirements</b></u> <u><b>Requirements</b></u>
<br><br> <br><br>
<ul> <ul>
@ -774,7 +893,7 @@ __END__
It has to be set a <b>client_secret</b>. It's the application secret from the <a target="_blank" href="https://developer.husqvarnagroup.cloud/docs/get-started">Husqvarna Developer Portal</a>.<br> It has to be set a <b>client_secret</b>. It's the application secret from the <a target="_blank" href="https://developer.husqvarnagroup.cloud/docs/get-started">Husqvarna Developer Portal</a>.<br>
<code>set myMower &lt;client secret&gt;</code> <code>set myMower &lt;client secret&gt;</code>
<br><br> <br><br>
</ul> </ul>
<br> <br>
<a id="AutomowerConnectSet"></a> <a id="AutomowerConnectSet"></a>
@ -836,6 +955,10 @@ __END__
<code>set &lt;name&gt; sendScheduleFromAttributeToMower</code><br> <code>set &lt;name&gt; sendScheduleFromAttributeToMower</code><br>
Sends the schedule to the mower. NOTE: Do not use for 550 EPOS and Ceora.</li> Sends the schedule to the mower. NOTE: Do not use for 550 EPOS and Ceora.</li>
<li><a id='AutomowerConnect-set-mapZonesTemplateToAttribute'>mapZonesTemplateToAttribute</a><br>
<code>set &lt;name&gt; mapZonesTemplateToAttribute</code><br>
Load the command reference example into the attribute mapZones.</li>
<li><a id='AutomowerConnect-set-'></a><br> <li><a id='AutomowerConnect-set-'></a><br>
<code>set &lt;name&gt; </code><br> <code>set &lt;name&gt; </code><br>
@ -949,13 +1072,11 @@ __END__
<li><a id='AutomowerConnect-attr-numberOfWayPointsToDisplay'>numberOfWayPointsToDisplay</a><br> <li><a id='AutomowerConnect-attr-numberOfWayPointsToDisplay'>numberOfWayPointsToDisplay</a><br>
<code>attr &lt;name&gt; numberOfWayPointsToDisplay &lt;number of way points&gt;</code><br> <code>attr &lt;name&gt; numberOfWayPointsToDisplay &lt;number of way points&gt;</code><br>
Set the number of way points stored and displayed, default 5000. Set the number of way points stored and displayed, default and at least 5000. The way points are shifted through the dedicated stack.</li>
While in activity MOWING every 30 s a geo data set is generated.
While in activity PARKED_IN_CS/CHARGING every 42 min a geo data set is generated.</li>
<li><a id='AutomowerConnect-attr-weekdaysToResetWayPoints'>weekdaysToResetWayPoints</a><br> <li><a id='AutomowerConnect-attr-weekdaysToResetWayPoints'>weekdaysToResetWayPoints</a><br>
<code>attr &lt;name&gt; weekdaysToResetWayPoints &lt;any combination of weekday numbers, space or minus [0123456 -]&gt;</code><br> <code>attr &lt;name&gt; weekdaysToResetWayPoints &lt;any combination of weekday numbers, space or minus [0123456 -]&gt;</code><br>
A combination of weekday numbers when the way point stack will be reset. No reset for space or minus. The way points are shifted through the dedicated stack. Default 1.</li> A combination of weekday numbers when the way point stack will be reset. No reset for space or minus. Default 1.</li>
<li><a id='AutomowerConnect-attr-scaleToMeterXY'>scaleToMeterXY</a><br> <li><a id='AutomowerConnect-attr-scaleToMeterXY'>scaleToMeterXY</a><br>
<code>attr &lt;name&gt; scaleToMeterXY &lt;scale factor longitude&gt;&lt;seperator&gt;&lt;scale factor latitude&gt;</code><br> <code>attr &lt;name&gt; scaleToMeterXY &lt;scale factor longitude&gt;&lt;seperator&gt;&lt;scale factor latitude&gt;</code><br>
@ -1026,8 +1147,10 @@ __END__
<li>api_token_expires - date when session of Husqvarna Cloud expires</li> <li>api_token_expires - date when session of Husqvarna Cloud expires</li>
<li>batteryPercent - battery state of charge in percent</li> <li>batteryPercent - battery state of charge in percent</li>
<li>mower_activity - current activity "UNKNOWN" | "NOT_APPLICABLE" | "MOWING" | "GOING_HOME" | "CHARGING" | "LEAVING" | "PARKED_IN_CS" | "STOPPED_IN_GARDEN"</li> <li>mower_activity - current activity "UNKNOWN" | "NOT_APPLICABLE" | "MOWING" | "GOING_HOME" | "CHARGING" | "LEAVING" | "PARKED_IN_CS" | "STOPPED_IN_GARDEN"</li>
<li>mower_commandSend - Last successfull sent command</li>
<li>mower_commandStatus - Status of the last sent command cleared each status update</li> <li>mower_commandStatus - Status of the last sent command cleared each status update</li>
<li>mower_currentZone - Zone name with activity MOWING in the last status time stamp interval and number of way points in parenthesis.</li> <li>mower_currentZone - Zone name with activity MOWING in the last status time stamp interval and number of way points in parenthesis.</li>
<li>mower_wsEvent - websocket connection events (status-event, positions-event, settings-event)</li>
<li>mower_errorCode - last error code</li> <li>mower_errorCode - last error code</li>
<li>mower_errorCodeTimestamp - last error code time stamp</li> <li>mower_errorCodeTimestamp - last error code time stamp</li>
<li>mower_errorDescription - error description</li> <li>mower_errorDescription - error description</li>
@ -1036,12 +1159,13 @@ __END__
<li>planner_nextStart - next start time</li> <li>planner_nextStart - next start time</li>
<li>planner_restrictedReason - reason for parking NOT_APPLICABLE, NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST</li> <li>planner_restrictedReason - reason for parking NOT_APPLICABLE, NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST</li>
<li>planner_overrideAction - reason for override a planned action NOT_ACTIVE, FORCE_PARK, FORCE_MOW</li> <li>planner_overrideAction - reason for override a planned action NOT_ACTIVE, FORCE_PARK, FORCE_MOW</li>
<li>state - status of connection FHEM to Husqvarna Cloud API and device state(e.g. defined, authorization, authorized, connected, error, update)</li> <li>state - state of websocket connection</li>
<li>device_state - status of connection FHEM to Husqvarna Cloud API and device state(e.g. defined, authorization, authorized, connected, error, update)</li>
<li>settings_cuttingHeight - actual cutting height from API</li> <li>settings_cuttingHeight - actual cutting height from API</li>
<li>settings_headlight - actual headlight mode from API</li> <li>settings_headlight - actual headlight mode from API</li>
<li>statistics_newGeoDataSets - number of new data sets between the last two different time stamps</li> <li>statistics_newGeoDataSets - number of new data sets between the last two different time stamps</li>
<li>statistics_numberOfCollisions - Number of Collisions</li> <li>statistics_numberOfCollisions - Number of Collisions</li>
<li>status_connected - state of connetion between mower and Husqvarna Cloud, (1 => CONNECTED, 0 => OFFLINE)</li> <li>status_connected - state of connetion between mower and Husqvarna Cloud.</li>
<li>status_statusTimestamp - local time of last change of the API content</li> <li>status_statusTimestamp - local time of last change of the API content</li>
<li>status_statusTimestampDiff - time difference in seconds between the last and second last change of the API content</li> <li>status_statusTimestampDiff - time difference in seconds between the last and second last change of the API content</li>
<li>system_name - name of the mower</li> <li>system_name - name of the mower</li>
@ -1059,13 +1183,13 @@ __END__
<h3>AutomowerConnect</h3> <h3>AutomowerConnect</h3>
<ul> <ul>
<u><b>FHEM-FORUM:</b></u> <a target="_blank" href="https://forum.fhem.de/index.php/topic,131661.0.html"> AutomowerConnect</a><br> <u><b>FHEM-FORUM:</b></u> <a target="_blank" href="https://forum.fhem.de/index.php/topic,131661.0.html"> AutomowerConnect</a><br>
<u><b>FHEM-Wiki:</b></u> <a target="_blank" href="https://wiki.fhem.de/wiki/AutomowerConnect"> AutomowerConnect : Wie erstellt man eine Karte des Mähbereiches?</a> <u><b>FHEM-Wiki:</b></u> <a target="_blank" href="https://wiki.fhem.de/wiki/AutomowerConnect"> AutomowerConnect: Wie erstellt man eine Karte des Mähbereiches?</a>
<br><br> <br><br>
<u><b>Einleitung</b></u> <u><b>Einleitung</b></u>
<br><br> <br><br>
<ul> <ul>
<li>Dieses Modul etabliert eine Kommunikation zwischen der Husqvarna Cloud and FHEM, um einen Husqvarna Automower zu steuern, der mit einem Connect Modul (SIM) ausgerüstet ist.</li> <li>Dieses Modul etabliert eine Kommunikation zwischen der Husqvarna Cloud and FHEM, um einen Husqvarna Automower zu steuern, der mit einem Connect Modul (SIM) ausgerüstet ist.</li>
<li>Es arbeitet als Device für einen Mähroboter. Für zusätzliche in der API registrierte Mähroboter ist für jeden Mäher ein extra Appilcation Key mit Application Secret zu verwenden.</li> <li>Es arbeitet als Device für einen Mähroboter. Für jeden in der API registrierten Mähroboter ist ein extra Appilcation Key mit Application Secret zu verwenden.</li>
<li>Der Pfad des Mähroboters wird in der Detailansicht des FHEMWEB Frontends angezeigt.</li> <li>Der Pfad des Mähroboters wird in der Detailansicht des FHEMWEB Frontends angezeigt.</li>
<li>Der Pfad kann mit einer beliebigen Karte hinterlegt werden.</li> <li>Der Pfad kann mit einer beliebigen Karte hinterlegt werden.</li>
<li>Die Karte muss als Rasterbild im webp, png oder jpg Format vorliegen.</li> <li>Die Karte muss als Rasterbild im webp, png oder jpg Format vorliegen.</li>
@ -1074,14 +1198,6 @@ __END__
<li>Die Schnitthöhe kann je selbstdefinierter Zone eingestellt werden. </li> <li>Die Schnitthöhe kann je selbstdefinierter Zone eingestellt werden. </li>
<li>Die Daten aus der API sind im Gerätehash gespeichert, Mit <code>{Dumper $defs{&lt;device name&gt;}}</code> in der Befehlezeile können die Daten angezeigt werden und daraus userReadings erstellt werden.</li><br> <li>Die Daten aus der API sind im Gerätehash gespeichert, Mit <code>{Dumper $defs{&lt;device name&gt;}}</code> in der Befehlezeile können die Daten angezeigt werden und daraus userReadings erstellt werden.</li><br>
</ul> </ul>
<u><b>Limit Automower Connect API</b></u>
<br><br>
<ul>
<li>Maximal 1 Request pro Sekunde und Application Key.</li>
<li>Maximal 10 000 Requests pro Monat und Application Key.</li>
<li>'Der Mäher sendet seine Daten nur alle 10 Minuten, um den Datenverkehr zu begrenzen und Batterie zu sparen...' </li>
<li>Daraus ergibt sich ein empfohlenes Abfrageinterval von 600 Sekunden</li><br>
</ul>
<u><b>Anforderungen</b></u> <u><b>Anforderungen</b></u>
<br><br> <br><br>
<ul> <ul>
@ -1161,6 +1277,10 @@ __END__
<code>set &lt;name&gt; sendScheduleFromAttributeToMower</code><br> <code>set &lt;name&gt; sendScheduleFromAttributeToMower</code><br>
Sendet den Mähplan zum Mäher. HINWEIS: Nicht für 550 EPOS und Ceora geeignet.</li> Sendet den Mähplan zum Mäher. HINWEIS: Nicht für 550 EPOS und Ceora geeignet.</li>
<li><a id='AutomowerConnect-set-mapZonesTemplateToAttribute'>mapZonesTemplateToAttribute</a><br>
<code>set &lt;name&gt; mapZonesTemplateToAttribute</code><br>
Läd das Beispiel aus der Befehlsreferenz in das Attribut mapZones.</li>
<li><a id='AutomowerConnect-set-'></a><br> <li><a id='AutomowerConnect-set-'></a><br>
<code>set &lt;name&gt; </code><br> <code>set &lt;name&gt; </code><br>
</li> </li>
@ -1276,12 +1396,11 @@ __END__
<li><a id='AutomowerConnect-attr-numberOfWayPointsToDisplay'>numberOfWayPointsToDisplay</a><br> <li><a id='AutomowerConnect-attr-numberOfWayPointsToDisplay'>numberOfWayPointsToDisplay</a><br>
<code>attr &lt;name&gt; numberOfWayPointsToDisplay &lt;number of way points&gt;</code><br> <code>attr &lt;name&gt; numberOfWayPointsToDisplay &lt;number of way points&gt;</code><br>
Legt die Anzahl der gespeicherten und und anzuzeigenden Wegpunkte fest, default 5000. Legt die Anzahl der gespeicherten und und anzuzeigenden Wegpunkte fest, Standart und Mindestwert 5000. Die Wegpunkte werden durch den zugeteilten Wegpunktspeicher geschoben.</li>
Während der Aktivität MOWING wird ca. alle 30 s und während PARKED_IN_CS/CHARGING wird alle 42 min ein Geodatensatz erzeugt.</li>
<li><a id='AutomowerConnect-attr-weekdaysToResetWayPoints'>weekdaysToResetWayPoints</a><br> <li><a id='AutomowerConnect-attr-weekdaysToResetWayPoints'>weekdaysToResetWayPoints</a><br>
<code>attr &lt;name&gt; weekdaysToResetWayPoints &lt;any combination of weekday numbers, space or minus [0123456 -]&gt;</code><br> <code>attr &lt;name&gt; weekdaysToResetWayPoints &lt;any combination of weekday numbers, space or minus [0123456 -]&gt;</code><br>
Eine Kombination von Wochentagnummern an denen der Wegpunktspeicher gelöscht wird. Keine Löschung bei Leer- oder Minuszeichen, die Wegpunkte werden durch den zugeteilten Wegpunktspeicher geschoben. Standard 1.</li> Eine Kombination von Wochentagnummern an denen der Wegpunktspeicher gelöscht wird. Keine Löschung bei Leer- oder Minuszeichen, Standard 1.</li>
<li><a id='AutomowerConnect-attr-scaleToMeterXY'>scaleToMeterXY</a><br> <li><a id='AutomowerConnect-attr-scaleToMeterXY'>scaleToMeterXY</a><br>
<code>attr &lt;name&gt; scaleToMeterXY &lt;scale factor longitude&gt;&lt;seperator&gt;&lt;scale factor latitude&gt;</code><br> <code>attr &lt;name&gt; scaleToMeterXY &lt;scale factor longitude&gt;&lt;seperator&gt;&lt;scale factor latitude&gt;</code><br>
@ -1354,8 +1473,10 @@ __END__
<li>api_token_expires - Datum wann die Session der Husqvarna Cloud abläuft</li> <li>api_token_expires - Datum wann die Session der Husqvarna Cloud abläuft</li>
<li>batteryPercent - Batterieladung in Prozent</li> <li>batteryPercent - Batterieladung in Prozent</li>
<li>mower_activity - aktuelle Aktivität "UNKNOWN" | "NOT_APPLICABLE" | "MOWING" | "GOING_HOME" | "CHARGING" | "LEAVING" | "PARKED_IN_CS" | "STOPPED_IN_GARDEN"</li> <li>mower_activity - aktuelle Aktivität "UNKNOWN" | "NOT_APPLICABLE" | "MOWING" | "GOING_HOME" | "CHARGING" | "LEAVING" | "PARKED_IN_CS" | "STOPPED_IN_GARDEN"</li>
<li>mower_commandSend - Letzter erfolgreich gesendeter Befehl.</li>
<li>mower_commandStatus - Status des letzten uebermittelten Kommandos wird duch Statusupdate zurückgesetzt.</li> <li>mower_commandStatus - Status des letzten uebermittelten Kommandos wird duch Statusupdate zurückgesetzt.</li>
<li>mower_currentZone - Name der Zone im aktuell abgefragten Intervall der Statuszeitstempel , in der der Mäher gemäht hat und Anzahl der Wegpunkte in der Zone in Klammern.</li> <li>mower_currentZone - Name der Zone im aktuell abgefragten Intervall der Statuszeitstempel , in der der Mäher gemäht hat und Anzahl der Wegpunkte in der Zone in Klammern.</li>
<li>mower_wsEvent - Events der Websocketverbindung (status-event, positions-event, settings-event)</li>
<li>mower_errorCode - last error code</li> <li>mower_errorCode - last error code</li>
<li>mower_errorCodeTimestamp - last error code time stamp</li> <li>mower_errorCodeTimestamp - last error code time stamp</li>
<li>mower_errorDescription - error description</li> <li>mower_errorDescription - error description</li>
@ -1364,12 +1485,13 @@ __END__
<li>planner_nextStart - nächste Startzeit</li> <li>planner_nextStart - nächste Startzeit</li>
<li>planner_restrictedReason - Grund für Parken NOT_APPLICABLE, NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST</li> <li>planner_restrictedReason - Grund für Parken NOT_APPLICABLE, NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST</li>
<li>planner_overrideAction - Grund für vorrangige Aktion NOT_ACTIVE, FORCE_PARK, FORCE_MOW</li> <li>planner_overrideAction - Grund für vorrangige Aktion NOT_ACTIVE, FORCE_PARK, FORCE_MOW</li>
<li>state - Status der Verbindung des FHEM-Gerätes zur Husqvarna Cloud API (defined, authentification, authentified, connected, error, update).</li> <li>state - Status der Websocketverbindung der Husqvarna API.</li>
<li>device_state - Status der Verbindung des FHEM-Gerätes zur Husqvarna Cloud API (defined, authentification, authentified, connected, error, update).</li>
<li>settings_cuttingHeight - aktuelle Schnitthöhe aus der API</li> <li>settings_cuttingHeight - aktuelle Schnitthöhe aus der API</li>
<li>settings_headlight - aktueller Scheinwerfermode aus der API</li> <li>settings_headlight - aktueller Scheinwerfermode aus der API</li>
<li>statistics_newGeoDataSets - Anzahl der neuen Datensätze zwischen den letzten zwei unterschiedlichen Zeitstempeln</li> <li>statistics_newGeoDataSets - Anzahl der neuen Datensätze zwischen den letzten zwei unterschiedlichen Zeitstempeln</li>
<li>statistics_numberOfCollisions - Anzahl der Kollisionen</li> <li>statistics_numberOfCollisions - Anzahl der Kollisionen</li>
<li>status_connected - Status der Verbindung zwischen dem Automower und der Husqvarna Cloud, (1 => CONNECTED, 0 => OFFLINE)</li> <li>status_connected - Status der Verbindung zwischen dem Automower und der Husqvarna Cloud.</li>
<li>status_statusTimestamp - Lokalzeit der letzten Änderung der Daten in der API</li> <li>status_statusTimestamp - Lokalzeit der letzten Änderung der Daten in der API</li>
<li>status_statusTimestampDiff - Zeitdifferenz zwischen den beiden letzten Änderungen im Inhalt der Daten aus der API</li> <li>status_statusTimestampDiff - Zeitdifferenz zwischen den beiden letzten Änderungen im Inhalt der Daten aus der API</li>
<li>system_name - Name des Automowers</li> <li>system_name - Name des Automowers</li>

View File

@ -34,7 +34,7 @@ use POSIX;
use GPUtils qw(:all); use GPUtils qw(:all);
use Time::HiRes qw(gettimeofday); use Time::HiRes qw(gettimeofday);
# use Blocking; use DevIo;
use Storable qw(dclone retrieve store); use Storable qw(dclone retrieve store);
# Import der FHEM Funktionen # Import der FHEM Funktionen
@ -67,6 +67,8 @@ BEGIN {
attr attr
modules modules
devspec2array devspec2array
DevIo_IsOpen
DevIo_CloseDev
) )
); );
} }
@ -85,6 +87,8 @@ $errorjson = undef;
use constant AUTHURL => 'https://api.authentication.husqvarnagroup.dev/v1'; use constant AUTHURL => 'https://api.authentication.husqvarnagroup.dev/v1';
use constant APIURL => 'https://api.amc.husqvarna.dev/v1'; use constant APIURL => 'https://api.amc.husqvarna.dev/v1';
use constant WSDEVICENAME => 'wss:ws.openapi.husqvarna.dev:443/v1';
############################################################## ##############################################################
# #
@ -100,28 +104,13 @@ sub Define{
my $iam = "$type $name Define:"; my $iam = "$type $name Define:";
my $client_id = ''; my $client_id = '';
my $mowerNumber = 0; my $mowerNumber = 0;
my $hostname ='';
return "$iam Cannot define $type device. Perl modul $missingModul is missing." if ( $missingModul ); return "$iam Cannot define $type device. Perl modul $missingModul is missing." if ( $missingModul );
if ( $type eq 'AutomowerConnect' ) { return "$iam too few parameters: define <NAME> $type <client_id> [<mower number>]" if( @val < 3 );
return "$iam too few parameters: define <NAME> $type <client_id> [<mower number>]" if( @val < 3 ); $client_id =$val[2];
$mowerNumber = $val[3] ? $val[3] : 0;
$client_id =$val[2];
$mowerNumber = $val[3] ? $val[3] : 0;
} elsif ( $type eq 'AutomowerConnectDevice' ) {
return "$iam too few parameters: define <NAME> $type <host name> <mower number>" if( @val < 4 );
$hostname = $val[2];
$mowerNumber = $val[3];
::notifyRegexpChanged($hash, $hostname.':state:.connected');
}
my $mapAttr = 'areaLimitsColor="#ff8000" my $mapAttr = 'areaLimitsColor="#ff8000"
areaLimitsLineWidth="1" areaLimitsLineWidth="1"
@ -153,37 +142,26 @@ mowingPathLineDash="6,2"
mowingPathLineWidth="1"'; mowingPathLineWidth="1"';
my $mapZonesTpl = '{ my $mapZonesTpl = '{
"A_Zone_1" : { "01_oben" : {
"condition" : "<condition to separate Zone_1 from other zones>", "condition" : "$latitude > 52.6484600648553 || $longitude > 9.54799477359984 && $latitude > 52.64839739580418",
"cuttingHeight" : "<cutting height for the first zone>" "cuttingHeight" : "7"
}, },
"B_Zone_2" : { "02_unten" : {
"condition" : "<condition to separate Zone_2 from other zones, except myZone_1>", "condition" : "undef",
"cuttingHeight" : "<cutting height for the second zone>" "cuttingHeight" : "3"
},
"C_Zone_3" : {
"condition" : "<condition to separate Zone_3 from other zones, except myZone_1 and myZone_2>",
"cuttingHeight" : "<cutting height for the third zone>"
},
"D_Zone_x" : {
"condition" : "<condition to separate Zone_x from other zones ,except the zones already seperated>",
"cuttingHeight" : "<cutting height for the nth-1 zone>"
},
"E_LastZone" : {
"condition" : "Use undef because the last zone remains.",
"cuttingHeight" : "<cutting height for the nth zone>"
} }
}'; }';
%$hash = (%$hash, %$hash = (%$hash,
helper => { helper => {
passObj => FHEM::Core::Authentication::Passwords->new($type), passObj => FHEM::Core::Authentication::Passwords->new($type),
interval => 420, interval => 420,
interval_auth => 86345,
interval_ws => 7110,
interval_ping => 60,
client_id => $client_id, client_id => $client_id,
grant_type => 'client_credentials', grant_type => 'client_credentials',
mowerNumber => $mowerNumber, mowerNumber => $mowerNumber,
hostname => $hostname,
scaleToMeterLongitude => 67425, scaleToMeterLongitude => 67425,
scaleToMeterLatitude => 108886, scaleToMeterLatitude => 108886,
minLon => 180, minLon => 180,
@ -202,8 +180,6 @@ my $mapZonesTpl = '{
MAP_CACHE => '', MAP_CACHE => '',
cspos => [], cspos => [],
areapos => [], areapos => [],
searchpos => [],
timestamps => [],
lasterror => { lasterror => {
positions => [], positions => [],
timestamp => 0, timestamp => 0,
@ -234,7 +210,7 @@ my $mapZonesTpl = '{
maxLength => 5000, maxLength => 5000,
maxLengthDefault => 5000, maxLengthDefault => 5000,
cnt => 0, cnt => 0,
callFn => \&FHEM::Devices::AMConnect::Common::AreaStatistics callFn => ''
}, },
GOING_HOME => { GOING_HOME => {
short => 'G', short => 'G',
@ -248,7 +224,7 @@ my $mapZonesTpl = '{
arrayName => 'cspos', arrayName => 'cspos',
maxLength => 100, maxLength => 100,
cnt => 0, cnt => 0,
callFn => \&FHEM::Devices::AMConnect::Common::ChargingStationPosition callFn => ''
}, },
LEAVING => { LEAVING => {
short => 'L', short => 'L',
@ -262,11 +238,11 @@ my $mapZonesTpl = '{
arrayName => 'cspos', arrayName => 'cspos',
maxLength => 100, maxLength => 100,
cnt => 0, cnt => 0,
callFn => \&FHEM::Devices::AMConnect::Common::ChargingStationPosition callFn => ''
}, },
STOPPED_IN_GARDEN => { STOPPED_IN_GARDEN => {
short => 'S', short => 'S',
arrayName => 'otherpos', arrayName => '',
maxLength => 50, maxLength => 50,
cnt => 0, cnt => 0,
callFn => '' callFn => ''
@ -275,12 +251,16 @@ my $mapZonesTpl = '{
currentSpeed => 0, currentSpeed => 0,
currentDayTrack => 0, currentDayTrack => 0,
currentDayArea => 0, currentDayArea => 0,
currentDayTime => 0,
lastDayTrack => 0, lastDayTrack => 0,
lastDayArea => 0, lastDayArea => 0,
lastDaytime => 0,
currentWeekTrack => 0, currentWeekTrack => 0,
currentWeekArea => 0, currentWeekArea => 0,
currentWeekTime => 0,
lastWeekTrack => 0, lastWeekTrack => 0,
lastWeekArea => 0 lastWeekArea => 0,
lastWeekTime => 0
} }
} }
); );
@ -290,35 +270,23 @@ my $mapZonesTpl = '{
$attr{$name}{room} = 'AutomowerConnect' if( !defined( $attr{$name}{room} ) ); $attr{$name}{room} = 'AutomowerConnect' if( !defined( $attr{$name}{room} ) );
$attr{$name}{icon} = 'automower' if( !defined( $attr{$name}{icon} ) ); $attr{$name}{icon} = 'automower' if( !defined( $attr{$name}{icon} ) );
( $hash->{LIBRARY_VERSION} ) = $cvsid =~ /\.pm (.*)Z/; ( $hash->{LIBRARY_VERSION} ) = $cvsid =~ /\.pm (.*)Z/;
$hash->{Host} = 'ws.openapi.husqvarna.dev';
$hash->{Port} = '443/v1';
AddExtension( $name, \&GetMap, "$type/$name/map" ); AddExtension( $name, \&GetMap, "$type/$name/map" );
if ( $type eq 'AutomowerConnect' ) { if( $hash->{helper}->{passObj}->getReadPassword($name) ) {
if( $hash->{helper}->{passObj}->getReadPassword($name) ) {
RemoveInternalTimer($hash);
InternalTimer( gettimeofday() + 2, \&::FHEM::AutomowerConnect::APIAuth, $hash, 1);
InternalTimer( gettimeofday() + 30, \&readMap, $hash, 0);
readingsSingleUpdate( $hash, 'state', 'defined', 1 );
} else {
readingsSingleUpdate( $hash, 'state', 'defined - client_secret missing', 1 );
}
} elsif ( $type eq 'AutomowerConnectDevice' ) {
$hash->{HINWEIS1} = 'Dieses Modul nicht mehr verwenden, die Entwicklung ist eingestellt.';
$hash->{HINWEIS2} = 'Bestehende Instanzen muessen umgehend auf AutomowerConnect umgestellt werden.';
$hash->{HINWEIS3} = 'Für jedes Geraet ist ein extra Application Key zu verwenden.';
RemoveInternalTimer($hash); RemoveInternalTimer($hash);
InternalTimer( gettimeofday() + 25, \&readMap, $hash, 0); InternalTimer( gettimeofday() + 2, \&::FHEM::AutomowerConnect::APIAuth, $hash, 1);
InternalTimer( gettimeofday() + 30, \&readMap, $hash, 0);
readingsSingleUpdate( $hash, 'state', 'defined', 1 ); readingsSingleUpdate( $hash, 'device_state', 'defined', 1 );
} else {
readingsSingleUpdate( $hash, 'device_state', 'defined - client_secret missing', 1 );
} }
@ -331,8 +299,10 @@ sub Undefine {
my ( $hash, $arg ) = @_; my ( $hash, $arg ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $type = $hash->{TYPE}; my $type = $hash->{TYPE};
RemoveInternalTimer($hash); DevIo_CloseDev( $hash ) if ( DevIo_IsOpen( $hash ) );
RemoveInternalTimer( $hash );
::FHEM::Devices::AMConnect::Common::RemoveExtension("$type/$name/map"); ::FHEM::Devices::AMConnect::Common::RemoveExtension("$type/$name/map");
return undef; return undef;
} }
@ -576,15 +546,8 @@ sub CMD {
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $type = $hash->{TYPE}; my $type = $hash->{TYPE};
my $iam = "$type $name CMD:"; my $iam = "$type $name CMD:";
my $hostname = $hash->{helper}{hostname} ? $hash->{helper}{hostname} : $name; $hash->{helper}{mower_commandSend} = $cmd[ 0 ] . ' ' . ( $cmd[ 1 ] ? $cmd[ 1 ] : '' );
my $hosthash = $defs{$hostname};
if ( IsDisabled($hostname) ) {
Log3 $name, 3, "$iam Host $hostname disabled";
return undef
}
if ( IsDisabled($name) ) { if ( IsDisabled($name) ) {
Log3 $name, 3, "$iam disabled"; Log3 $name, 3, "$iam disabled";
@ -592,9 +555,9 @@ sub CMD {
} }
my $client_id = $hosthash->{helper}->{client_id}; my $client_id = $hash->{helper}->{client_id};
my $token = ReadingsVal($hostname,".access_token",""); my $token = ReadingsVal($name,".access_token","");
my $provider = ReadingsVal($hostname,".provider",""); my $provider = ReadingsVal($name,".provider","");
my $mower_id = $hash->{helper}{mower}{id}; my $mower_id = $hash->{helper}{mower}{id};
my $json = ''; my $json = '';
@ -630,7 +593,7 @@ my $header = "Accept: application/vnd.api+json\r\nX-Api-Key: ".$client_id."\r\nA
::HttpUtils_NonblockingGet({ ::HttpUtils_NonblockingGet({
url => APIURL . "/mowers/". $mower_id . "/".$post, url => APIURL . "/mowers/". $mower_id . "/".$post,
timeout => 10, timeout => 15,
hash => $hash, hash => $hash,
method => "POST", method => "POST",
header => $header, header => $header,
@ -674,7 +637,13 @@ sub CMDResponse {
} }
readingsSingleUpdate($hash, 'mower_commandStatus', $hash->{helper}->{mower_commandStatus} ,1); readingsBeginUpdate($hash);
readingsBulkUpdateIfChanged( $hash, 'mower_commandStatus', $hash->{helper}{mower_commandStatus}, 1 );
readingsBulkUpdateIfChanged( $hash, 'mower_commandSend', $hash->{helper}{mower_commandSend}, 1 );
readingsEndUpdate($hash, 1);
return undef; return undef;
} }
@ -683,7 +652,13 @@ sub CMDResponse {
} }
readingsSingleUpdate($hash, 'mower_commandStatus', "ERROR statuscode $statuscode" ,1); readingsBeginUpdate($hash);
readingsBulkUpdateIfChanged( $hash, 'mower_commandStatus', "ERROR statuscode $statuscode", 1 );
readingsBulkUpdateIfChanged( $hash, 'mower_commandSend', $hash->{helper}{mower_commandSend}, 1 );
readingsEndUpdate($hash, 1);
Log3 $name, 2, "\n$iam \n\$statuscode [$statuscode]\n\$err [$err],\n\$data [$data]\n\$param->url $param->{url}"; Log3 $name, 2, "\n$iam \n\$statuscode [$statuscode]\n\$err [$err],\n\$data [$data]\n\$param->url $param->{url}";
return undef; return undef;
} }
@ -694,83 +669,76 @@ sub AlignArray {
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $act = $hash->{helper}{mower}{attributes}{mower}{activity}; my $act = $hash->{helper}{mower}{attributes}{mower}{activity};
my $actold = $hash->{helper}{mowerold}{attributes}{mower}{activity}; my $actold = $hash->{helper}{mowerold}{attributes}{mower}{activity};
my $searchlen = 2; my $cnt = @{ $hash->{helper}{mower}{attributes}{positions} };
my $cnt = 0;
my $tmp = []; my $tmp = [];
my $poslen = @{ $hash->{helper}{mower}{attributes}{positions} }; if ( $cnt > 0 ) {
my @searchposlon = ( $hash->{helper}{searchpos}[0]{longitude}, $hash->{helper}{searchpos}[1]{longitude} );
my @searchposlat = ( $hash->{helper}{searchpos}[0]{latitude}, $hash->{helper}{searchpos}[1]{latitude} );
for ( $cnt = 0; $cnt < $poslen-1; $cnt++ ) { # <-1 due to 2 alignment data sets at the end my @ar = @{ $hash->{helper}{mower}{attributes}{positions} };
my $deltaTime = $hash->{helper}{positionsTime} - $hash->{helper}{statusTime};
if ( $searchposlon[ 0 ] == $hash->{helper}{mower}{attributes}{positions}[ $cnt ]{longitude} # if encounter positions shortly after status event old activity is assigned to positions
&& $searchposlat[ 0 ] == $hash->{helper}{mower}{attributes}{positions}[ $cnt ]{latitude} if ( $cnt > 1 && $deltaTime > 0 && $deltaTime < 0.29 ) {
&& $searchposlon[ 1 ] == $hash->{helper}{mower}{attributes}{positions}[ $cnt+1 ]{longitude}
&& $searchposlat[ 1 ] == $hash->{helper}{mower}{attributes}{positions}[ $cnt+1 ]{latitude} ) {
if ( $cnt > 0 ) { map { $_->{act} = $hash->{helper}{$actold}{short} } @ar;
my @ar = @{ $hash->{helper}{mower}{attributes}{positions} }[ 0 .. $cnt-1 ]; @ar = reverse @ar if ( $cnt > 1 ); # positions seem to be in reversed order
map { $_->{act} = $hash->{helper}{$act}{short} } @ar; } else {
$tmp = dclone( \@ar ); map { $_->{act} = $hash->{helper}{$act}{short} } @ar;
if ( @{ $hash->{helper}{areapos} } ) { @ar = reverse @ar if ( $cnt > 1 ); # positions seem to be in reversed order
unshift ( @{ $hash->{helper}{areapos} }, @$tmp ); }
} else { $tmp = dclone( \@ar );
$hash->{helper}{areapos} = $tmp; if ( @{ $hash->{helper}{areapos} } ) {
} unshift ( @{ $hash->{helper}{areapos} }, @$tmp );
while ( @{ $hash->{helper}{areapos} } > $hash->{helper}{MOWING}{maxLength} ) { } else {
pop ( @{ $hash->{helper}{areapos}} ); # reduce to max allowed length $hash->{helper}{areapos} = $tmp;
$hash->{helper}{areapos}[0]{start} = 'first value';
} }
posMinMax( $hash, $tmp ); while ( @{ $hash->{helper}{areapos} } > $hash->{helper}{MOWING}{maxLength} ) {
if ( $act =~ /^(MOWING)$/ && $actold =~ /^(MOWING|LEAVING|PARKED_IN_CS|CHARGING)$/ ) { pop ( @{ $hash->{helper}{areapos}} ); # reduce to max allowed length
AreaStatistics ( $hash, $cnt ); }
} posMinMax( $hash, $tmp );
if ( AttrVal($name, 'mapZones', 0) && $act =~ /^(MOWING)$/ && $actold =~ /^(MOWING|LEAVING|PARKED_IN_CS|CHARGING)$/ if ( $act =~ /^(MOWING)$/ ) {
|| $act =~ /^(GOING_HOME|PARKED_IN_CS|CHARGING)$/ && $actold =~ /^(MOWING)$/ ) {
$tmp = dclone( \@ar ); AreaStatistics ( $hash, $cnt );
ZoneHandling ( $hash, $tmp, $cnt );
} }
# set cutting height per zone
if ( AttrVal($name, 'mapZones', 0) && $act =~ /^MOWING$/ && $actold =~ /^MOWING$/
&& defined( $hash->{helper}{currentZone} ) && defined( $hash->{helper}{mapZones}{$hash->{helper}{currentZone}}{cuttingHeight} )) {
CMD( $hash ,'cuttingHeight', $hash->{helper}{mapZones}{$hash->{helper}{currentZone}}{cuttingHeight} ) if ( AttrVal($name, 'mapZones', 0) && $act =~ /^(MOWING)$/ ) {
if ( $hash->{helper}{mapZones}{$hash->{helper}{currentZone}}{cuttingHeight} != $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} );
} $tmp = dclone( \@ar );
ZoneHandling ( $hash, $tmp, $cnt );
if ( $act =~ /^(CHARGING|PARKED_IN_CS)$/ && $actold =~ /^(PARKED_IN_CS|CHARGING)$/ ) { }
# set cutting height per zone
if ( AttrVal($name, 'mapZones', 0) && $act =~ /^MOWING$/
&& defined( $hash->{helper}{currentZone} )
&& defined( $hash->{helper}{mapZones}{$hash->{helper}{currentZone}}{cuttingHeight} )) {
$tmp = dclone( \@ar ); RemoveInternalTimer( $hash, \&setCuttingHeight );
ChargingStationPosition ( $hash, $tmp, $cnt ); InternalTimer( gettimeofday() + 11, \&setCuttingHeight, $hash, 0 )
}
} # if ( $act =~ /^(CHARGING|PARKED_IN_CS)$/ && $actold =~ /^(PARKED_IN_CS|CHARGING)$/ ) {
if ( $act =~ /^(CHARGING|PARKED_IN_CS)$/ ) {
} else { $tmp = dclone( \@ar );
ChargingStationPosition ( $hash, $tmp, $cnt );
$cnt = 0;
}
last;
} }
@ -781,7 +749,6 @@ sub AlignArray {
resetLastErrorIfCorrected($hash); resetLastErrorIfCorrected($hash);
$hash->{helper}{newdatasets} = $cnt; $hash->{helper}{newdatasets} = $cnt;
$hash->{helper}{searchpos} = [ dclone( $hash->{helper}{mower}{attributes}{positions}[0] ), dclone( $hash->{helper}{mower}{attributes}{positions}[1] ) ];
return undef; return undef;
} }
@ -968,12 +935,16 @@ sub AreaStatistics {
my $activity = 'MOWING'; my $activity = 'MOWING';
my $lsum = calcPathLength( $hash, 0, $i ); my $lsum = calcPathLength( $hash, 0, $i );
my $asum = 0; my $asum = 0;
my $atim = 0;
$asum = $lsum * AttrVal($name,'mowerCuttingWidth',0.24); $asum = $lsum * AttrVal($name,'mowerCuttingWidth',0.24);
$atim = $i*30; # seconds
$hash->{helper}{$activity}{track} = $lsum; $hash->{helper}{$activity}{track} = $lsum;
$hash->{helper}{$activity}{area} = $asum; $hash->{helper}{$activity}{area} = $asum;
$hash->{helper}{$activity}{time} = $atim;
$hash->{helper}{statistics}{currentDayTrack} += $lsum; $hash->{helper}{statistics}{currentDayTrack} += $lsum;
$hash->{helper}{statistics}{currentDayArea} += $asum; $hash->{helper}{statistics}{currentDayArea} += $asum;
$hash->{helper}{statistics}{currentDayTime} += $atim;
return undef; return undef;
} }
@ -1050,6 +1021,16 @@ sub readMap {
} }
} }
#########################
sub setCuttingHeight {
my ( $hash ) = @_;
RemoveInternalTimer( $hash, \&setCuttingHeight );
CMD( $hash ,'cuttingHeight', $hash->{helper}{mapZones}{$hash->{helper}{currentZone}}{cuttingHeight} )
if ( $hash->{helper}{mapZones}{$hash->{helper}{currentZone}}{cuttingHeight} != $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} );
return undef;
}
######################### #########################
sub posMinMax { sub posMinMax {
my ($hash, $poshash) = @_; my ($hash, $poshash) = @_;
@ -1070,7 +1051,7 @@ sub posMinMax {
$hash->{helper}{minLat} = $minLat; $hash->{helper}{minLat} = $minLat;
$hash->{helper}{maxLat} = $maxLat; $hash->{helper}{maxLat} = $maxLat;
$hash->{helper}{posMinMax} = "$minLon $maxLat\n$maxLon $minLat"; $hash->{helper}{posMinMax} = "$minLon $maxLat\n$maxLon $minLat";
$hash->{helper}{imageWidthHeight} = int($hash->{helper}{imageHeight} * ($maxLon-$minLon) / ($maxLat-$minLat)) . ' ' . $hash->{helper}{imageHeight} if ($maxLon-$minLon); $hash->{helper}{imageWidthHeight} = int($hash->{helper}{imageHeight} * ($maxLon-$minLon) / ($maxLat-$minLat)) . ' ' . $hash->{helper}{imageHeight} if ($maxLat-$minLat);
return undef; return undef;
} }
@ -1080,12 +1061,15 @@ sub fillReadings {
my ( $hash ) = @_; my ( $hash ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
readingsBulkUpdateIfChanged($hash, '.mower_id', $hash->{helper}{mower}{id}, 0 );
readingsBulkUpdateIfChanged($hash, "batteryPercent", $hash->{helper}{mower}{attributes}{battery}{batteryPercent} ); readingsBulkUpdateIfChanged($hash, "batteryPercent", $hash->{helper}{mower}{attributes}{battery}{batteryPercent} );
my $pref = 'mower'; my $pref = 'mower';
readingsBulkUpdateIfChanged($hash, $pref.'_mode', $hash->{helper}{mower}{attributes}{$pref}{mode} ); readingsBulkUpdateIfChanged($hash, $pref.'_mode', $hash->{helper}{mower}{attributes}{$pref}{mode} );
readingsBulkUpdateIfChanged($hash, $pref.'_activity', $hash->{helper}{mower}{attributes}{$pref}{activity} ); readingsBulkUpdateIfChanged($hash, $pref.'_activity', $hash->{helper}{mower}{attributes}{$pref}{activity} );
readingsBulkUpdateIfChanged($hash, $pref.'_state', $hash->{helper}{mower}{attributes}{$pref}{state} ); readingsBulkUpdateIfChanged($hash, $pref.'_state', $hash->{helper}{mower}{attributes}{$pref}{state} );
readingsBulkUpdateIfChanged($hash, $pref.'_commandStatus', 'cleared' ); readingsBulkUpdateIfChanged($hash, $pref.'_commandStatus', 'cleared' );
readingsBulkUpdateIfChanged($hash, $pref.'_commandSend', ( $hash->{helper}{mower_commandSend} ? $hash->{helper}{mower_commandSend} : '-' ) );
readingsBulkUpdateIfChanged($hash, $pref.'_wsEvent', $hash->{helper}{wsResult}{type} );
if ( AttrVal($name, 'mapZones', 0) && $hash->{helper}{currentZone} && $hash->{helper}{mapZones}{$hash->{helper}{currentZone}}{curZoneCnt} ) { if ( AttrVal($name, 'mapZones', 0) && $hash->{helper}{currentZone} && $hash->{helper}{mapZones}{$hash->{helper}{currentZone}}{curZoneCnt} ) {
my $curZon = $hash->{helper}{currentZone}; my $curZon = $hash->{helper}{currentZone};
@ -1120,91 +1104,103 @@ sub fillReadings {
readingsBulkUpdateIfChanged($hash, $pref."_numberOfCollisions", $hash->{helper}->{mower}{attributes}{$pref}{numberOfCollisions} ); readingsBulkUpdateIfChanged($hash, $pref."_numberOfCollisions", $hash->{helper}->{mower}{attributes}{$pref}{numberOfCollisions} );
readingsBulkUpdateIfChanged($hash, $pref."_newGeoDataSets", $hash->{helper}{newdatasets} ); readingsBulkUpdateIfChanged($hash, $pref."_newGeoDataSets", $hash->{helper}{newdatasets} );
$pref = 'settings'; $pref = 'settings';
readingsBulkUpdateIfChanged($hash, $pref."_headlight", $hash->{helper}->{mower}{attributes}{$pref}{headlight}{mode} ); readingsBulkUpdateIfChanged($hash, $pref."_headlight", $hash->{helper}{mower}{attributes}{$pref}{headlight}{mode} );
readingsBulkUpdateIfChanged($hash, $pref."_cuttingHeight", $hash->{helper}->{mower}{attributes}{$pref}{cuttingHeight} ); readingsBulkUpdateIfChanged($hash, $pref."_cuttingHeight", $hash->{helper}{mower}{attributes}{$pref}{cuttingHeight} );
$pref = 'status'; $pref = 'status';
my $connected = $hash->{helper}{mower}{attributes}{metadata}{connected}; my $connected = $hash->{helper}{mower}{attributes}{metadata}{connected};
readingsBulkUpdateIfChanged($hash, $pref."_connected", ( $connected ? "CONNECTED($connected)" : "OFFLINE($connected)") ); readingsBulkUpdateIfChanged($hash, $pref."_connected", ( $connected ? "CONNECTED($connected)" : "OFFLINE($connected)") );
readingsBulkUpdateIfChanged($hash, $pref."_Timestamp", FmtDateTime( $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}/1000 )); readingsBulkUpdateIfChanged($hash, $pref."_Timestamp", FmtDateTime( $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}/1000 )); # verschieben nach websocket fill
readingsBulkUpdateIfChanged($hash, $pref."_TimestampDiff", $hash->{helper}{storediff}/1000 ); readingsBulkUpdateIfChanged($hash, $pref."_TimestampDiff", $hash->{helper}{storediff}/1000 );# verschieben nach websocket fill
return undef; return undef;
} }
#########################
sub initStatistics {
my ( $hash ) = @_;
my ( @tim ) = localtime(time);
$tim[ 0 ] = 0;
$tim[ 1 ] = 0;
$tim[ 2 ] = 0;
my $ret = ::timelocal( @tim ) + 86417;
RemoveInternalTimer( $hash, \&calculateStatistics );
InternalTimer( $ret, \&calculateStatistics, $hash, 0 );
return undef;
}
######################### #########################
sub calculateStatistics { sub calculateStatistics {
my ( $hash ) = @_; my ( $hash ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my @time = localtime(); my @time = localtime();
my $secs = ( $time[2] * 3600 ) + ( $time[1] * 60 ) + $time[0];
my $interval = $hash->{helper}->{interval};
# do at midnight
if ( $secs <= $interval ) {
$hash->{helper}{statistics}{lastDayTrack} = $hash->{helper}{statistics}{currentDayTrack}; $hash->{helper}{statistics}{lastDayTrack} = $hash->{helper}{statistics}{currentDayTrack};
$hash->{helper}{statistics}{lastDayArea} = $hash->{helper}{statistics}{currentDayArea}; $hash->{helper}{statistics}{lastDayArea} = $hash->{helper}{statistics}{currentDayArea};
$hash->{helper}{statistics}{currentWeekTrack} += $hash->{helper}{statistics}{currentDayTrack}; $hash->{helper}{statistics}{lastDayTime} = $hash->{helper}{statistics}{currentDayTime};
$hash->{helper}{statistics}{currentWeekArea} += $hash->{helper}{statistics}{currentDayArea}; $hash->{helper}{statistics}{currentWeekTrack} += $hash->{helper}{statistics}{currentDayTrack};
$hash->{helper}{statistics}{currentDayTrack} = 0; $hash->{helper}{statistics}{currentWeekArea} += $hash->{helper}{statistics}{currentDayArea};
$hash->{helper}{statistics}{currentDayArea} = 0; $hash->{helper}{statistics}{currentWeekTime} += $hash->{helper}{statistics}{currentDayTime};
$hash->{helper}{statistics}{currentDayTrack} = 0;
$hash->{helper}{statistics}{currentDayArea} = 0;
$hash->{helper}{statistics}{currentDayTime} = 0;
if ( AttrVal($name, 'mapZones', 0) && defined( $hash->{helper}{mapZones} ) ) {
my @zonekeys = sort (keys %{$hash->{helper}{mapZones}});
my $sumCurrentWeekCnt=0;
my $sumCurrentWeekArea=0;
map {
$hash->{helper}{mapZones}{$_}{currentWeekCnt} += $hash->{helper}{mapZones}{$_}{zoneCnt};
$sumCurrentWeekCnt += $hash->{helper}{mapZones}{$_}{currentWeekCnt};
$hash->{helper}{mapZones}{$_}{currentWeekArea} += $hash->{helper}{mapZones}{$_}{zoneLength};
$sumCurrentWeekArea += $hash->{helper}{mapZones}{$_}{currentWeekArea};
$hash->{helper}{mapZones}{$_}{zoneCnt} = 0;
$hash->{helper}{mapZones}{$_}{zoneLength} = 0;
} @zonekeys;
map {
$hash->{helper}{mapZones}{$_}{lastDayCntPct} = $hash->{helper}{mapZones}{$_}{currentDayCntPct};
$hash->{helper}{mapZones}{$_}{currentWeekCntPct} = ( $sumCurrentWeekCnt ? sprintf( "%.0f", $hash->{helper}{mapZones}{$_}{currentWeekCnt} / $sumCurrentWeekCnt * 100 ) : '' );
$hash->{helper}{mapZones}{$_}{lastDayAreaPct} = $hash->{helper}{mapZones}{$_}{currentDayAreaPct};
$hash->{helper}{mapZones}{$_}{currentWeekAreaPct} = ( $sumCurrentWeekArea ? sprintf( "%.0f", $hash->{helper}{mapZones}{$_}{currentWeekArea} / $sumCurrentWeekArea * 100 ) : '' );
$hash->{helper}{mapZones}{$_}{currentDayCntPct} = '';
$hash->{helper}{mapZones}{$_}{currentDayAreaPct} = '';
} @zonekeys;
}
# do on days
if ( $time[6] == 1 ) {
$hash->{helper}{statistics}{lastWeekTrack} = $hash->{helper}{statistics}{currentWeekTrack};
$hash->{helper}{statistics}{lastWeekArea} = $hash->{helper}{statistics}{currentWeekArea};
$hash->{helper}{statistics}{lastWeekTime} = $hash->{helper}{statistics}{currentWeekTime};
$hash->{helper}{statistics}{currentWeekTrack} = 0;
$hash->{helper}{statistics}{currentWeekArea} = 0;
$hash->{helper}{statistics}{currentWeekTime} = 0;
if ( AttrVal($name, 'mapZones', 0) && defined( $hash->{helper}{mapZones} ) ) { if ( AttrVal($name, 'mapZones', 0) && defined( $hash->{helper}{mapZones} ) ) {
my @zonekeys = sort (keys %{$hash->{helper}{mapZones}}); my @zonekeys = sort (keys %{$hash->{helper}{mapZones}});
my $sumCurrentWeekCnt=0;
my $sumCurrentWeekArea=0;
map { map {
$hash->{helper}{mapZones}{$_}{currentWeekCnt} += $hash->{helper}{mapZones}{$_}{zoneCnt}; $hash->{helper}{mapZones}{$_}{lastWeekCntPct} = $hash->{helper}{mapZones}{$_}{currentWeekCntPct};
$sumCurrentWeekCnt += $hash->{helper}{mapZones}{$_}{currentWeekCnt}; $hash->{helper}{mapZones}{$_}{lastWeekAreaPct} = $hash->{helper}{mapZones}{$_}{currentWeekAreaPct};
$hash->{helper}{mapZones}{$_}{currentWeekArea} += $hash->{helper}{mapZones}{$_}{zoneLength}; $hash->{helper}{mapZones}{$_}{currentWeekCntPct} = '';
$sumCurrentWeekArea += $hash->{helper}{mapZones}{$_}{currentWeekArea}; $hash->{helper}{mapZones}{$_}{currentWeekAreaPct} = '';
$hash->{helper}{mapZones}{$_}{zoneCnt} = 0;
$hash->{helper}{mapZones}{$_}{zoneLength} = 0;
} @zonekeys; } @zonekeys;
map {
$hash->{helper}{mapZones}{$_}{lastDayCntPct} = $hash->{helper}{mapZones}{$_}{currentDayCntPct};
$hash->{helper}{mapZones}{$_}{currentWeekCntPct} = ( $sumCurrentWeekCnt ? sprintf( "%.0f", $hash->{helper}{mapZones}{$_}{currentWeekCnt} / $sumCurrentWeekCnt * 100 ) : '' );
$hash->{helper}{mapZones}{$_}{lastDayAreaPct} = $hash->{helper}{mapZones}{$_}{currentDayAreaPct};
$hash->{helper}{mapZones}{$_}{currentWeekAreaPct} = ( $sumCurrentWeekArea ? sprintf( "%.0f", $hash->{helper}{mapZones}{$_}{currentWeekArea} / $sumCurrentWeekArea * 100 ) : '' );
$hash->{helper}{mapZones}{$_}{currentDayCntPct} = '';
$hash->{helper}{mapZones}{$_}{currentDayAreaPct} = '';
} @zonekeys;
}
# do on days
if ( $time[6] == 1 ) {
$hash->{helper}{statistics}{lastWeekTrack} = $hash->{helper}{statistics}{currentWeekTrack};
$hash->{helper}{statistics}{lastWeekArea} = $hash->{helper}{statistics}{currentWeekArea};
$hash->{helper}{statistics}{currentWeekTrack} = 0;
$hash->{helper}{statistics}{currentWeekArea} = 0;
if ( AttrVal($name, 'mapZones', 0) && defined( $hash->{helper}{mapZones} ) ) {
my @zonekeys = sort (keys %{$hash->{helper}{mapZones}});
map {
$hash->{helper}{mapZones}{$_}{lastWeekCntPct} = $hash->{helper}{mapZones}{$_}{currentWeekCntPct};
$hash->{helper}{mapZones}{$_}{lastWeekAreaPct} = $hash->{helper}{mapZones}{$_}{currentWeekAreaPct};
$hash->{helper}{mapZones}{$_}{currentWeekCntPct} = '';
$hash->{helper}{mapZones}{$_}{currentWeekAreaPct} = '';
} @zonekeys;
}
}
#clear position arrays
if ( AttrVal( $name, 'weekdaysToResetWayPoints', 1 ) =~ $time[6] ) {
$hash->{helper}{areapos} = [];
$hash->{helper}{otherpos} = [];
} }
} }
#clear position arrays
if ( AttrVal( $name, 'weekdaysToResetWayPoints', 1 ) =~ $time[6] ) {
$hash->{helper}{areapos} = [];
}
initStatistics( $hash );
return undef; return undef;
} }
@ -1227,15 +1223,19 @@ sub listStatisticsData {
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{mower}{attributes}{statistics}{<b>totalRunningTime</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} / 3600 ) . '<sup>1</sup> </td><td> h </td></tr>'; $cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{mower}{attributes}{statistics}{<b>totalRunningTime</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} / 3600 ) . '<sup>1</sup> </td><td> h </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{mower}{attributes}{statistics}{<b>totalSearchingTime</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} / 3600 ) . ' </td><td> h </td></tr>'; $cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{mower}{attributes}{statistics}{<b>totalSearchingTime</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} / 3600 ) . ' </td><td> h </td></tr>';
# $cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{currentSpeed} &emsp;</td><td> ' . $hash->{helper}{statistics}{currentSpeed} . ' </td><td> m/s </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>currentDayTrack</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentDayTrack} ) . ' </td><td> m </td></tr>'; $cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>currentDayTrack</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentDayTrack} ) . ' </td><td> m </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>currentDayArea</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentDayArea} ) . ' </td><td> qm </td></tr>'; $cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>currentDayArea</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentDayArea} ) . ' </td><td> qm </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>currentDayTime</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentDayTime} ) . ' </td><td> s </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> <b>calculated speed</b> &emsp;</td><td> ' . sprintf( "%.2f", $hash->{helper}{statistics}{currentDayTrack} / $hash->{helper}{statistics}{currentDayTime} ) . ' </td><td> m/s </td></tr>' if ( $hash->{helper}{statistics}{currentDayTime} );
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>lastDayTrack</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastDayTrack} ) . ' </td><td> m </td></tr>'; $cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>lastDayTrack</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastDayTrack} ) . ' </td><td> m </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>lastDayArea</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastDayArea} ) . ' </td><td> qm </td></tr>'; $cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>lastDayArea</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastDayArea} ) . ' </td><td> qm </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>lastDayTime</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastDayTime} ) . ' </td><td> s </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>currentWeekTrack</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentWeekTrack} ) . ' </td><td> m </td></tr>'; $cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>currentWeekTrack</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentWeekTrack} ) . ' </td><td> m </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>currentWeekArea</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentWeekArea} ) . ' </td><td> qm </td></tr>'; $cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>currentWeekArea</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentWeekArea} ) . ' </td><td> qm </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>currentWeekTime</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{currentWeekTime} ) . ' </td><td> s </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>lastWeekTrack</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastWeekTrack} ) . ' </td><td> m </td></tr>'; $cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>lastWeekTrack</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastWeekTrack} ) . ' </td><td> m </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>lastWeekArea</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastWeekArea} ) . ' </td><td> qm </td></tr>'; $cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>lastWeekArea</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastWeekArea} ) . ' </td><td> qm </td></tr>';
$cnt++;$ret .= '<tr class="column '.( $cnt % 2 ? 'odd' : 'even' ).'"><td> $hash->{helper}{statistics}{<b>lastWeekTime</b>} &emsp;</td><td> ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastWeekTime} ) . ' </td><td> s </td></tr>';
if ( AttrVal($name, 'mapZones', 0) && defined( $hash->{helper}{mapZones} ) ) { if ( AttrVal($name, 'mapZones', 0) && defined( $hash->{helper}{mapZones} ) ) {

View File

@ -175,35 +175,91 @@ function AutomowerConnectDrawPath ( ctx, div, pos, type ) {
} }
function AutomowerConnectDrawPathColor ( ctx, div, pos, colorat ) { function AutomowerConnectDrawPathColorRev ( ctx, div, pos, colorat ) {
// draw path // draw path
var type = colorat[ pos[ 2 ] ]; var type = colorat[ pos[ 2 ] ];
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = div.getAttribute( 'data-'+ type + 'LineColor' ); ctx.strokeStyle = div.getAttribute( 'data-'+ type + 'LineColor' );
ctx.lineWidth=div.getAttribute( 'data-'+ type + 'LineWidth' ); ctx.lineWidth=div.getAttribute( 'data-'+ type + 'LineWidth' );
ctx.setLineDash( div.getAttribute( 'data-'+ type + 'LineDash' ).split(",") ); ctx.setLineDash( div.getAttribute( 'data-'+ type + 'LineDash' ).split(",") );
ctx.moveTo( parseInt( pos[0] ), parseInt( pos[ 1 ] ) ); ctx.moveTo( parseInt( pos[ 0 ] ), parseInt( pos[ 1 ] ) );
var i = 0;
for (var i=3;i<pos.length;i+=3){ for ( i = 3; i<pos.length; i+=3 ){
ctx.lineTo( parseInt( pos[ i ] ),parseInt( pos[ i+1 ] ) ); ctx.lineTo( parseInt( pos[ i ] ),parseInt( pos[ i + 1 ] ) );
if ( colorat[ pos[ i+2 ] ] != type ){ if ( colorat[ pos[ i + 2 ] ] != type ){
ctx.stroke(); ctx.stroke();
var type = colorat[ pos[ i + 2 ] ]; type = colorat[ pos[ i + 2 ] ];
ctx.beginPath(); ctx.beginPath();
ctx.moveTo( parseInt( pos[ i ] ), parseInt( pos[ i + 1 ] ) );
ctx.strokeStyle = div.getAttribute( 'data-'+ type + 'LineColor' ); ctx.strokeStyle = div.getAttribute( 'data-'+ type + 'LineColor' );
ctx.lineWidth=div.getAttribute( 'data-'+ type + 'LineWidth' ); ctx.lineWidth=div.getAttribute( 'data-'+ type + 'LineWidth' );
ctx.setLineDash( div.getAttribute( 'data-'+ type + 'LineDash' ).split(",") ); ctx.setLineDash( div.getAttribute( 'data-'+ type + 'LineDash' ).split( "," ) );
} }
} }
ctx.stroke(); ctx.stroke();
} }
function AutomowerConnectDrawPathColor ( ctx, div, pos, colorat ) {
// draw path
var type = colorat[ pos[ pos.length-1 ] ];
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[ pos.length-3 ] ), parseInt( pos[ pos.length-2 ] ) );
var i = 0;
for ( i = pos.length-3; i>-1; i-=3 ){
ctx.lineTo( parseInt( pos[ i ] ),parseInt( pos[ i + 1 ] ) );
if ( colorat[ pos[ i + 2 ] ] != type ){
ctx.stroke();
type = colorat[ pos[ i + 2 ] ];
ctx.beginPath();
ctx.moveTo( parseInt( pos[ i ] ), parseInt( pos[ i + 1 ] ) );
ctx.strokeStyle = div.getAttribute( 'data-'+ type + 'LineColor' );
ctx.lineWidth=div.getAttribute( 'data-'+ type + 'LineWidth' );
ctx.setLineDash( div.getAttribute( 'data-'+ type + 'LineDash' ).split( "," ) );
}
}
ctx.stroke();
}
function AutomowerConnectTor ( x0, y0, x1, y1 ) {
var dy = y0-y1;
var dx = x0-x1;
var dyx = dx ? Math.abs( dy / dx ) : 999;
var ret = '';
// position of icon relative to path end point
if ( dx >= 0 && dy >= 0 && Math.abs( dyx ) >= 1 ) ret = 'top';
if ( dx >= 0 && dy >= 0 && Math.abs( dyx ) < 1 ) ret = 'left';
if ( dx < 0 && dy >= 0 && Math.abs( dyx ) >= 1 ) ret = 'top';
if ( dx < 0 && dy >= 0 && Math.abs( dyx ) < 1 ) ret = 'right';
if ( dx >= 0 && dy < 0 && Math.abs( dyx ) >= 1 ) ret = 'bottom';
if ( dx >= 0 && dy < 0 && Math.abs( dyx ) < 1 ) ret = 'left';
if ( dx < 0 && dy < 0 && Math.abs( dyx ) >= 1 ) ret = 'bottom';
if ( dx < 0 && dy < 0 && Math.abs( dyx ) < 1 ) ret = 'right';
//~ log ('AUTOMOWERCONNECTTOR:');
//~ log ('dx: ' + dx);
//~ log ('dy: ' + dy);
//~ log ('dyx: ' + dyx);
//~ log ('ret: ' + ret);
return ret;
}
//AutomowerConnectUpdateDetail (<devicename>, <type> <background-image path>, <imagesize x>, <imagesize y>, <relative position of CS marker>,<scale x>, <error description>, <path array>, <area limits array>, <property limits array>, <error array>) //AutomowerConnectUpdateDetail (<devicename>, <type> <background-image path>, <imagesize x>, <imagesize y>, <relative position of CS marker>,<scale x>, <error description>, <path array>, <area limits array>, <property limits array>, <error array>)
function AutomowerConnectUpdateDetail (dev, type, imgsrc, picx, picy, csx, csy, csrel, scalx, errdesc, pos, lixy, plixy, erray) { function AutomowerConnectUpdateDetail (dev, type, imgsrc, picx, picy, csx, csy, csrel, scalx, errdesc, pos, lixy, plixy, erray) {
const colorat = { const colorat = {
@ -251,7 +307,7 @@ function AutomowerConnectUpdateDetail (dev, type, imgsrc, picx, picy, csx, csy,
} }
// draw mower icon // draw mower icon
AutomowerConnectIcon( ctx, pos[0], pos[1], 'bottom', 'M' ); AutomowerConnectIcon( ctx, pos[0], pos[1], AutomowerConnectTor ( pos[3], pos[4], pos[0], pos[1] ), 'M' );
} }