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:
parent
b2a640e92a
commit
e7380089ed
@ -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,
|
||||||
|
@ -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{<name>}}</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{<name>}}</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 <client secret></code>
|
<code>set myMower <client secret></code>
|
||||||
<br><br>
|
<br><br>
|
||||||
</ul>
|
</ul>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<a id="AutomowerConnectSet"></a>
|
<a id="AutomowerConnectSet"></a>
|
||||||
@ -836,6 +955,10 @@ __END__
|
|||||||
<code>set <name> sendScheduleFromAttributeToMower</code><br>
|
<code>set <name> 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 <name> 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 <name> </code><br>
|
<code>set <name> </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 <name> numberOfWayPointsToDisplay <number of way points></code><br>
|
<code>attr <name> numberOfWayPointsToDisplay <number of way points></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 <name> weekdaysToResetWayPoints <any combination of weekday numbers, space or minus [0123456 -]></code><br>
|
<code>attr <name> weekdaysToResetWayPoints <any combination of weekday numbers, space or minus [0123456 -]></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 <name> scaleToMeterXY <scale factor longitude><seperator><scale factor latitude></code><br>
|
<code>attr <name> scaleToMeterXY <scale factor longitude><seperator><scale factor latitude></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{<device name>}}</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{<device name>}}</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 <name> sendScheduleFromAttributeToMower</code><br>
|
<code>set <name> 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 <name> 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 <name> </code><br>
|
<code>set <name> </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 <name> numberOfWayPointsToDisplay <number of way points></code><br>
|
<code>attr <name> numberOfWayPointsToDisplay <number of way points></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 <name> weekdaysToResetWayPoints <any combination of weekday numbers, space or minus [0123456 -]></code><br>
|
<code>attr <name> weekdaysToResetWayPoints <any combination of weekday numbers, space or minus [0123456 -]></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 <name> scaleToMeterXY <scale factor longitude><seperator><scale factor latitude></code><br>
|
<code>attr <name> scaleToMeterXY <scale factor longitude><seperator><scale factor latitude></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>
|
||||||
|
@ -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>}  </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>}  </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>}  </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>}  </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}  </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>}  </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>}  </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>}  </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>}  </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>}  </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>  </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>}  </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>}  </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>}  </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>}  </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>}  </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>}  </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>}  </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>}  </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>}  </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>}  </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>}  </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>}  </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>}  </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>}  </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>}  </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} ) ) {
|
||||||
|
|
||||||
|
@ -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' );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user