From 4c626e3481c1a817f924a2d333bc40854a244e9e Mon Sep 17 00:00:00 2001
From: Ellert <>
Date: Sun, 5 Feb 2023 14:10:02 +0000
Subject: [PATCH] AutomowerConnect(Device): shift common subs to lib, bugfix
due to last rework, more precise alingment for gps data
git-svn-id: https://svn.fhem.de/fhem/trunk@27181 2b470e98-0d58-463d-a4d8-8e2adae1ed80
---
fhem/CHANGED | 3 +
fhem/FHEM/74_AutomowerConnect.pm | 753 ++++------------------
fhem/FHEM/75_AutomowerConnectDevice.pm | 624 ++----------------
fhem/lib/FHEM/Devices/AMConnect/Common.pm | 729 +++++++++++++++++++++
4 files changed, 900 insertions(+), 1209 deletions(-)
create mode 100644 fhem/lib/FHEM/Devices/AMConnect/Common.pm
diff --git a/fhem/CHANGED b/fhem/CHANGED
index 7744d87da..823e142f3 100644
--- a/fhem/CHANGED
+++ b/fhem/CHANGED
@@ -1,5 +1,8 @@
# Add changes at the top of the list. Keep it in ASCII, and 80-char wide.
# Do not insert empty lines here, update check depends on it.
+ - change : 74_AutomowerConnect: shift common subs to lib
+ bugfix due to last rework, more precise alingment for gps data
+ - change: 75_AutomowerConnectDevice: shift common subs to lib
- bugfix: 59_Weather: return missing perl modules message
- bugfix: 59_Weather: fix Undefined subroutine
- feature: 74_AutomowerConnect: rework detail view
diff --git a/fhem/FHEM/74_AutomowerConnect.pm b/fhem/FHEM/74_AutomowerConnect.pm
index 8aad4176b..80af8fd66 100644
--- a/fhem/FHEM/74_AutomowerConnect.pm
+++ b/fhem/FHEM/74_AutomowerConnect.pm
@@ -82,6 +82,8 @@ my $missingModul = "";
eval "use JSON;1" or $missingModul .= "JSON ";
require HttpUtils;
+require FHEM::Devices::AMConnect::Common;
+
use constant AUTHURL => 'https://api.authentication.husqvarnagroup.dev/v1';
use constant APIURL => 'https://api.amc.husqvarna.dev/v1';
@@ -89,13 +91,14 @@ use constant APIURL => 'https://api.amc.husqvarna.dev/v1';
sub Initialize() {
my ($hash) = @_;
- $hash->{SetFn} = \&Set;
- $hash->{GetFn} = \&Get;
$hash->{DefFn} = \&Define;
- $hash->{UndefFn} = \&Undefine;
- $hash->{DeleteFn} = \&Delete;
- $hash->{RenameFn} = \&Rename;
- $hash->{FW_detailFn}= \&FW_detailFn;
+ $hash->{SetFn} = \&Set;
+ $hash->{AttrFn} = \&Attr;
+ $hash->{GetFn} = \&FHEM::Devices::AMConnect::Common::Get;
+ $hash->{UndefFn} = \&FHEM::Devices::AMConnect::Common::Undefine;
+ $hash->{DeleteFn} = \&FHEM::Devices::AMConnect::Common::Delete;
+ $hash->{RenameFn} = \&FHEM::Devices::AMConnect::Common::Rename;
+ $hash->{FW_detailFn}= \&FHEM::Devices::AMConnect::Common::FW_detailFn;
$hash->{AttrFn} = \&Attr;
$hash->{AttrList} = "interval " .
"disable:1,0 " .
@@ -155,13 +158,16 @@ sub Define{
maxLat => -90,
imageHeight => 650,
imageWidthHeight => '350 650',
- posMinMax => "-180 90\n 180 -90",
+ posMinMax => "-180 90\n180 -90",
newdatasets => 0,
client_id => $client_id,
grant_type => 'client_credentials',
MAP_PATH => '',
MAP_MIME => '',
MAP_CACHE => '',
+ cspos => [],
+ areapos => [],
+ searchpos => [],
UNKNOWN => {
arrayName => '',
maxLength => 0,
@@ -176,7 +182,7 @@ sub Define{
arrayName => 'areapos',
maxLength => 500,
maxLengthDefault => 500,
- callFn => \&AreaStatistics
+ callFn => \&FHEM::Devices::AMConnect::Common::AreaStatistics
},
GOING_HOME => {
arrayName => '',
@@ -186,7 +192,7 @@ sub Define{
CHARGING => {
arrayName => 'cspos',
maxLength => 100,
- callFn => \&ChargingStationPosition
+ callFn => \&FHEM::Devices::AMConnect::Common::ChargingStationPosition
},
LEAVING => {
arrayName => '',
@@ -196,7 +202,7 @@ sub Define{
PARKED_IN_CS => {
arrayName => 'cspos',
maxLength => 100,
- callFn => \&ChargingStationPosition
+ callFn => \&FHEM::Devices::AMConnect::Common::ChargingStationPosition
},
STOPPED_IN_GARDEN => {
arrayName => '',
@@ -217,33 +223,22 @@ sub Define{
}
);
-my $errorjson = <<'EOF';
-{"23":"Wheel drive problem, left","24":"Cutting system blocked","123":"Destination not reachable","710":"SIM card locked","50":"Guide 1 not found","717":"SMS could not be sent","108":"Folding cutting deck sensor defect","4":"Loop sensor problem, front","15":"Lifted","29":"Slope too steep","1":"Outside working area","45":"Cutting height problem, dir","52":"Guide 3 not found","28":"Memory circuit problem","95":"Folding sensor activated","9":"Trapped","114":"Too high discharge current","103":"Cutting drive motor 2 defect","65":"Temporary battery problem","119":"Zone generator problem","6":"Loop sensor problem, left","82":"Wheel motor blocked, rear right","714":"Geofence problem","703":"Connectivity problem","708":"SIM card locked","75":"Connection changed","7":"Loop sensor problem, right","35":"Wheel motor overloaded, right","3":"Wrong loop signal","117":"High internal power loss","0":"Unexpected error","80":"Cutting system imbalance - Warning","110":"Collision sensor error","100":"Ultrasonic Sensor 3 defect","79":"Invalid battery combination - Invalid combination of different battery types.","724":"Communication circuit board SW must be updated","86":"Wheel motor overloaded, rear right","81":"Safety function faulty","78":"Slipped - Mower has Slipped.Situation not solved with moving pattern","107":"Docking sensor defect","33":"Mower tilted","69":"Alarm! Mower switched off","68":"Temporary battery problem","34":"Cutting stopped - slope too steep","127":"Battery problem","73":"Alarm! Mower in motion","74":"Alarm! Outside geofence","713":"Geofence problem","87":"Wheel motor overloaded, rear left","120":"Internal voltage error","39":"Cutting motor problem","704":"Connectivity problem","63":"Temporary battery problem","109":"Loop sensor defect","38":"Electronic problem","64":"Temporary battery problem","113":"Complex working area","93":"No accurate position from satellites","104":"Cutting drive motor 3 defect","709":"SIM card not found","94":"Reference station communication problem","43":"Cutting height problem, drive","13":"No drive","44":"Cutting height problem, curr","118":"Charging system problem","14":"Mower lifted","57":"Guide calibration failed","707":"SIM card requires PIN","99":"Ultrasonic Sensor 2 defect","98":"Ultrasonic Sensor 1 defect","51":"Guide 2 not found","56":"Guide calibration accomplished","49":"Ultrasonic problem","2":"No loop signal","124":"Destination blocked","25":"Cutting system blocked","19":"Collision sensor problem, front","18":"Collision sensor problem, rear","48":"No response from charger","105":"Lift Sensor defect","111":"No confirmed position","10":"Upside down","40":"Limited cutting height range","716":"Connectivity problem","27":"Settings restored","90":"No power in charging station","21":"Wheel motor blocked, left","26":"Invalid sub-device combination","92":"Work area not valid","702":"Connectivity settings restored","125":"Battery needs replacement","5":"Loop sensor problem, rear","12":"Empty battery","55":"Difficult finding home","42":"Limited cutting height range","30":"Charging system problem","72":"Alarm! Mower tilted","85":"Wheel drive problem, rear left","8":"Wrong PIN code","62":"Temporary battery problem","102":"Cutting drive motor 1 defect","116":"High charging power loss","122":"CAN error","60":"Temporary battery problem","705":"Connectivity problem","711":"SIM card locked","70":"Alarm! Mower stopped","32":"Tilt sensor problem","37":"Charging current too high","89":"Invalid system configuration","76":"Connection NOT changed","71":"Alarm! Mower lifted","88":"Angular sensor problem","701":"Connectivity problem","715":"Connectivity problem","61":"Temporary battery problem","66":"Battery problem","106":"Collision sensor defect","67":"Battery problem","112":"Cutting system major imbalance","83":"Wheel motor blocked, rear left","84":"Wheel drive problem, rear right","126":"Battery near end of life","77":"Com board not available","36":"Wheel motor overloaded, left","31":"STOP button problem","17":"Charging station blocked","54":"Weak GPS signal","47":"Cutting height problem","53":"GPS navigation problem","121":"High internal temerature","97":"Left brush motor overloaded","712":"SIM card locked","20":"Wheel motor blocked, right","91":"Switch cord problem","96":"Right brush motor overloaded","58":"Temporary battery problem","59":"Temporary battery problem","22":"Wheel drive problem, right","706":"Poor signal quality","41":"Unexpected cutting height adj","46":"Cutting height blocked","11":"Low battery","16":"Stuck in charging station","101":"Ultrasonic Sensor 4 defect","115":"Too high internal current"}
-EOF
-
- my $errortable = eval { decode_json ($errorjson) };
- if ($@) {
- return "$iam $@";
- }
-
- $hash->{helper}{errortable} = $errortable;
- $errorjson = undef;
- $errortable = undef;
-
$hash->{MODEL} = '';
$attr{$name}{room} = $type if( !defined( $attr{$name}{room} ) );
$attr{$name}{icon} = 'automower' if( !defined( $attr{$name}{icon} ) );
- if (::AnalyzeCommandChain(undef,"version 74_AutomowerConnect.pm noheader") =~ "^74_AutomowerConnect.pm (.*)Z") {
+ my ($modname) = __FILE__ =~ /(\d\d_.*\.pm)/;
+
+ if (::AnalyzeCommandChain(undef,"version $modname noheader") =~ "^$modname (.*)Z") {
$hash->{VERSION}=$1;
}
- AddExtension( $name, \&GetMap, "$type/$name/map" );
+ ::FHEM::Devices::AMConnect::Common::AddExtension( $name, \&FHEM::Devices::AMConnect::Common::GetMap, "$type/$name/map" );
if( $hash->{helper}->{passObj}->getReadPassword($name) ) {
RemoveInternalTimer($hash);
InternalTimer( gettimeofday() + 2, \&APIAuth, $hash, 1);
- InternalTimer( gettimeofday() + 30, \&readMap, $hash, 0);
+ InternalTimer( gettimeofday() + 30, \&FHEM::Devices::AMConnect::Common::readMap, $hash, 0);
readingsSingleUpdate( $hash, 'state', 'defined', 1 );
} else {
@@ -257,6 +252,88 @@ EOF
}
+#########################
+sub AlignArray {
+ my ($hash) = @_;
+ my $name = $hash->{NAME};
+ my $activity = $hash->{helper}{mower}{attributes}{mower}{activity};
+ my $arrayName = $hash->{helper}{$activity}{arrayName};
+ my $searchlen = 2;
+ my $i = 0;
+ my @temp = ();
+
+ if ( isGoodActivity( $hash ) ) {
+
+ my $k = -1;
+ my $poslen = @{$hash->{helper}{mower}{attributes}{positions}};
+ my @searchposlon = ($hash->{helper}{searchpos}[0]{longitude}, $hash->{helper}{searchpos}[1]{longitude});
+ my @searchposlat = ($hash->{helper}{searchpos}[0]{latitude}, $hash->{helper}{searchpos}[1]{latitude});
+ my $maxLength = $hash->{helper}{$activity}{maxLength};
+ for ( $i = 0; $i < $poslen-2; $i++ ) { # 2 due to 2 alignment data sets at the end
+ if ( $searchposlon[ 0 ] == $hash->{helper}{mower}{attributes}{positions}[ $i ]{longitude}
+ && $searchposlat[ 0 ] == $hash->{helper}{mower}{attributes}{positions}[ $i ]{latitude}
+ && $searchposlon[ 1 ] == $hash->{helper}{mower}{attributes}{positions}[ $i+1 ]{longitude}
+ && $searchposlat[ 1 ] == $hash->{helper}{mower}{attributes}{positions}[ $i+1 ]{latitude} ) {
+ # timediff per step
+ my $dt = 0;
+ $dt = int(($hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{$arrayName}[0]{statusTimestamp})/$i) if ( $i && @{ $hash->{helper}{$arrayName} } );
+ for ($k=$i-1;$k>-1;$k--) {
+
+ }
+ for ($k=$i-1;$k>-1;$k--) {
+
+ if ( @{ $hash->{helper}{$arrayName} } ) {
+
+ unshift (@{$hash->{helper}{$arrayName}}, dclone($hash->{helper}{mower}{attributes}{positions}[ $k ]) );
+
+ } else {
+
+ $hash->{helper}{$arrayName}[ 0 ] = dclone($hash->{helper}{mower}{attributes}{positions}[ $k ]);
+
+ }
+
+ pop (@{$hash->{helper}{$arrayName}}) if (@{$hash->{helper}{$arrayName}} > $maxLength);
+ $hash->{helper}{$arrayName}[ 0 ]{statusTimestamp} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $dt * $k;
+
+ push ( @temp, dclone( $hash->{helper}{mower}{attributes}{positions}[ $k ] ) );
+
+ }
+
+ ::FHEM::Devices::AMConnect::Common::posMinMax( $hash, \@temp );
+ #callFn if present
+ if ( $hash->{helper}{$activity}{callFn} && @{$hash->{helper}{$arrayName}} > 1) {
+
+ $hash->{helper}{$activity}{cnt} = $i;
+ no strict "refs";
+ &{$hash->{helper}{$activity}{callFn}}($hash);
+ use strict "refs";
+
+ }
+
+ last;
+
+ }
+
+ }
+
+ }
+
+ $hash->{helper}{newdatasets} = $i;
+ $hash->{helper}{searchpos} = [ dclone( $hash->{helper}{mower}{attributes}{positions}[0] ), dclone( $hash->{helper}{mower}{attributes}{positions}[1] ) ];
+ return undef;
+
+}
+
+#########################
+sub isGoodActivity {
+
+ my ( $hash ) = @_;
+ my $act = $hash->{helper}{mower}{attributes}{mower}{activity};
+ my $ret = $hash->{helper}{$act}{arrayName} && $act eq $hash->{helper}{mowerold}{attributes}{mower}{activity};
+ return $ret;
+
+}
+
##############################################################
#
# API AUTHENTICATION
@@ -380,6 +457,7 @@ sub APIAuthResponse {
##############################################################
sub getMower {
+
my ($hash) = @_;
my $name = $hash->{NAME};
my $type = $hash->{TYPE};
@@ -458,16 +536,8 @@ sub getMowerResponse {
$hash->{helper}{searchpos} = [ dclone( $hash->{helper}{mowerold}{attributes}{positions}[0] ), dclone( $hash->{helper}{mowerold}{attributes}{positions}[1] ) ];
- $hash->{helper}{areapos} = [ dclone( $hash->{helper}{mowerold}{attributes}{positions}[0] ), dclone( $hash->{helper}{mowerold}{attributes}{positions}[1] ) ];
- $hash->{helper}{areapos}[0]{statusTimestamp} = $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp};
- $hash->{helper}{areapos}[1]{statusTimestamp} = $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} - 12000;
-
- $hash->{helper}{cspos} = [ dclone( $hash->{helper}{mowerold}{attributes}{positions}[0] ), dclone( $hash->{helper}{mowerold}{attributes}{positions}[1] ) ];
- $hash->{helper}{cspos}[0]{statusTimestamp} = $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp};
- $hash->{helper}{cspos}[1]{statusTimestamp} = $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} - 600000;
-
if ( AttrVal( $name, 'mapImageCoordinatesToRegister', '' ) eq '' ) {
- posMinMax( $hash, $hash->{helper}{mowerold}{attributes}{positions} );
+ ::FHEM::Devices::AMConnect::Common::posMinMax( $hash, $hash->{helper}{mowerold}{attributes}{positions} );
}
}
@@ -475,12 +545,14 @@ sub getMowerResponse {
$hash->{helper}{mower} = dclone( $hash->{helper}{mowers}[$mowerNumber] );
# add alignment data set to the end
push( @{ $hash->{helper}{mower}{attributes}{positions} }, @{ dclone( $hash->{helper}{searchpos} ) } );
+ $hash->{helper}{newdatasets} = 0;
my $storediff = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp};
if ($storediff) {
+ # ::FHEM::Devices::AMConnect::Common::AlignArray( $hash );
AlignArray( $hash );
- FW_detailFn_Update ($hash) if (AttrVal($name,'showMap',1));
+ ::FHEM::Devices::AMConnect::Common::FW_detailFn_Update ($hash) if (AttrVal($name,'showMap',1));
}
@@ -494,13 +566,17 @@ sub getMowerResponse {
readingsBulkUpdateIfChanged($hash, $pref.'_activity', $hash->{helper}{mower}{attributes}{$pref}{activity} );
readingsBulkUpdateIfChanged($hash, $pref.'_state', $hash->{helper}{mower}{attributes}{$pref}{state} );
readingsBulkUpdateIfChanged($hash, $pref.'_commandStatus', 'cleared' );
+
my $tstamp = $hash->{helper}{mower}{attributes}{$pref}{errorCodeTimestamp};
- my $timestamp = FmtDateTime($tstamp/1000);
+ my $timestamp = FmtDateTimeGMT($tstamp/1000);
readingsBulkUpdateIfChanged($hash, $pref."_errorCodeTimestamp", $tstamp ? $timestamp : '-' );
+
my $errc = $hash->{helper}{mower}{attributes}{$pref}{errorCode};
readingsBulkUpdateIfChanged($hash, $pref.'_errorCode', $tstamp ? $errc : '-');
- my $errd = $hash->{helper}{errortable}{$errc};
+
+ my $errd = $::FHEM::Devices::AMConnect::Common::errortable->{$errc};
readingsBulkUpdateIfChanged($hash, $pref.'_errorDescription', $tstamp ? $errd : '-');
+
$pref = 'system';
readingsBulkUpdateIfChanged($hash, $pref."_name", $hash->{helper}{mower}{attributes}{$pref}{name} );
my $model = $hash->{helper}{mower}{attributes}{$pref}{model};
@@ -511,15 +587,16 @@ sub getMowerResponse {
readingsBulkUpdateIfChanged($hash, "planner_overrideAction", $hash->{helper}{mower}{attributes}{$pref}{override}{action} );
$tstamp = $hash->{helper}{mower}{attributes}{$pref}{nextStartTimestamp};
- $timestamp = FmtDateTime($tstamp/1000);
+ $timestamp = FmtDateTimeGMT($tstamp/1000);
readingsBulkUpdateIfChanged($hash, "planner_nextStart", $tstamp ? $timestamp : '-' );
$pref = 'statistics';
readingsBulkUpdateIfChanged($hash, $pref."_numberOfCollisions", $hash->{helper}->{mower}{attributes}{$pref}{numberOfCollisions} );
+ readingsBulkUpdateIfChanged($hash, $pref."_newGeoDataSets", $hash->{helper}{newdatasets} );
$pref = 'settings';
readingsBulkUpdateIfChanged($hash, $pref."_headlight", $hash->{helper}->{mower}{attributes}{$pref}{headlight}{mode} );
readingsBulkUpdateIfChanged($hash, $pref."_cuttingHeight", $hash->{helper}->{mower}{attributes}{$pref}{cuttingHeight} );
$pref = 'status';
- readingsBulkUpdateIfChanged($hash, $pref."_connected", $hash->{helper}{mower}{attributes}{metadata}{connected} );
+ readingsBulkUpdateIfChanged($hash, $pref."_connected", ( $hash->{helper}{mower}{attributes}{metadata}{connected} ? 'CONNECTED' : 'OFFLINE') );
readingsBulkUpdateIfChanged($hash, $pref."_Timestamp", FmtDateTime( $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}/1000 ));
readingsBulkUpdateIfChanged($hash, $pref."_TimestampDiff", $storediff/1000 );
readingsBulkUpdateIfChanged($hash, $pref."_TimestampOld", FmtDateTime( $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp}/1000 ));
@@ -589,7 +666,7 @@ sub sendCMD {
my $client_id = $hash->{helper}->{client_id};
my $token = ReadingsVal($name,".access_token","");
my $provider = ReadingsVal($name,".provider","");
- my $mower_id = ReadingsVal($name,"mower_id","");
+ my $mower_id = $hash->{helper}{mower}{id};
my $json = '';
my $post = '';
@@ -624,7 +701,7 @@ my $header = "Accept: application/vnd.api+json\r\nX-Api-Key: ".$client_id."\r\nA
::HttpUtils_NonblockingGet({
url => APIURL . "/mowers/". $mower_id . "/".$post,
- timeout => 10,
+ timeout => 20,
hash => $hash,
method => "POST",
header => $header,
@@ -682,51 +759,7 @@ sub CMDResponse {
return undef;
}
-#########################
-sub Get {
- my ($hash,@val) = @_;
- my $type = $hash->{TYPE};
- return "$type $hash->{NAME} Get: needs at least one argument" if ( @val < 2 );
-
- my ($name,$setName,$setVal,$setVal2,$setVal3) = @val;
- my $iam = "$type $name Get:";
-
- Log3 $name, 4, "$iam called with $setName " . ($setVal ? $setVal : "");
-
- if ( $setName eq 'html' ) {
-
- my $ret = '' . FW_detailFn( undef, $name, undef, undef) . '';
- return $ret;
-
- } elsif ( $setName eq 'errorCodes' ) {
-
- my $ret = listErrorCodes($hash);
- return $ret;
-
- } elsif ( $setName eq 'InternalData' ) {
-
- my $ret = listInternalData($hash);
- return $ret;
-
- } elsif ( $setName eq 'MowerData' ) {
-
- my $ret = listMowerData($hash);
- return $ret;
-
- } elsif ( $setName eq 'StatisticsData' ) {
-
- my $ret = listStatisticsData($hash);
- return $ret;
-
- } else {
-
- return "Unknown argument $setName, choose one of StatisticsData:noArg MowerData:noArg InternalData:noArg errorCodes:noArg ";
-
- }
-}
-
-#########################
sub Set {
my ($hash,@val) = @_;
my $type = $hash->{TYPE};
@@ -806,120 +839,6 @@ sub Set {
}
-#########################
-sub FW_detailFn {
- my ($FW_wname, $name, $room, $pageHash) = @_; # pageHash is set for summaryFn.
- my $hash = $defs{$name};
- my $type = $hash->{TYPE};
- return '' if( AttrVal($name, 'disable', 0) || !AttrVal($name, 'showMap', 1) );
- if ( $hash->{helper} && $hash->{helper}{mower} && $hash->{helper}{mower}{attributes} && $hash->{helper}{mower}{attributes}{positions} && @{$hash->{helper}{mower}{attributes}{positions}} > 0 ) {
- my $img = "./fhem/$type/$name/map";
- my $zoom=AttrVal( $name,"mapImageZoom", 0.7 );
-
- AttrVal( $name,"mapImageWidthHeight", $hash->{helper}{imageWidthHeight} ) =~ /(\d+)\s(\d+)/;
- my ($picx,$picy) = ($1, $2);
-
- $picx=int($picx*$zoom);
- $picy=int($picy*$zoom);
- my $ret = "";
- $ret .= "";
- $ret .= "
";
- $ret .= "";
- $ret .= "
";
-
- InternalTimer( gettimeofday() + 2.0, \&FW_detailFn_Update, $hash, 0 );
-
- return $ret;
- }
- return '';
-}
-
-#########################
-sub FW_detailFn_Update {
- my ($hash) = @_;
- my $name = $hash->{NAME};
- my $type = $hash->{TYPE};
- if ( $hash->{helper} && $hash->{helper}{mower} && $hash->{helper}{mower}{attributes} && $hash->{helper}{mower}{attributes}{positions} && @{$hash->{helper}{mower}{attributes}{positions}} > 0 ) {
-
- my @pos = ();
- my @posc = ();
- @pos = @{$hash->{helper}{areapos}}; # operational mode
- @posc =@{$hash->{helper}{cspos}}; # maybe operational mode
- my $img = "./fhem/$type/$name/map";
-
- AttrVal( $name,"mapImageCoordinatesToRegister",$hash->{helper}{posMinMax} ) =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)(\R|-?\s)(\d*\.?\d+)\s(-?\d*\.?\d+)/;
- my ( $lonlo, $latlo, $lonru, $latru ) = ( $1, $2, $4, $5 );
-
- my $zoom = AttrVal( $name,"mapImageZoom", 0.7 );
-
- AttrVal( $name,"mapImageWidthHeight", $hash->{helper}{imageWidthHeight} ) =~ /(\d+)\s(\d+)/;
- my ($picx,$picy) = ($1, $2);
-
- AttrVal($name,'scaleToMeterXY', $hash->{helper}{scaleToMeterLongitude} . ' ' .$hash->{helper}{scaleToMeterLatitude}) =~ /(\d+)\s+(\d+)/;
- my $scalx = ($lonru-$lonlo) * $1;
-
- $picx = int($picx*$zoom);
- $picy = int($picy*$zoom);
- my $mapx = $lonlo-$lonru;
- my $mapy = $latlo-$latru;
-
- if ( ($hash->{helper}{PARKED_IN_CS}{callFn} || $hash->{helper}{CHARGING}{callFn}) && (!$hash->{helper}{chargingStation}{longitude} || !$hash->{helper}{chargingStation}{latitude}) ) {
- no strict "refs";
- &{$hash->{helper}{PARKED_IN_CS}{callFn}}($hash);
- use strict "refs";
- }
-
- my $csimgpos = AttrVal( $name,"chargingStationImagePosition","right" );
- my $xm = $hash->{helper}{chargingStation}{longitude} // 10.1165;
- my $ym = $hash->{helper}{chargingStation}{latitude} // 51.28;
-
- AttrVal( $name,"chargingStationCoordinates","$xm $ym" ) =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)/;
- my ($cslo,$csla) = ($1, $2);
-
- my $cslon = int(($lonlo-$cslo) * $picx / $mapx);
- my $cslat = int(($latlo-$csla) * $picy / $mapy);
-
- # MOWING PATH
- my $posxy = int(($lonlo-$pos[0]{longitude}) * $picx / $mapx).",".int(($latlo-$pos[0]{latitude}) * $picy / $mapy);
- for (my $i=1;$i<@pos;$i++){
- $posxy .= ",".int(($lonlo-$pos[$i]{longitude}) * $picx / $mapx).",".int(($latlo-$pos[$i]{latitude}) * $picy / $mapy);
- }
-
- # CHARGING STATION PATH
- my $poscxy = int(($lonlo-$posc[0]{longitude}) * $picx / $mapx).",".int(($latlo-$posc[0]{latitude}) * $picy / $mapy);
- for (my $i=1;$i<@posc;$i++){
- $poscxy .= ",".int(($lonlo-$posc[$i]{longitude}) * $picx / $mapx).",".int(($latlo-$posc[$i]{latitude}) * $picy / $mapy);
- }
-
- # AREA LIMITS
- my $arealimits = AttrVal($name,'mowingAreaLimits','');
- my $limi = '';
- if ($arealimits) {
- my @lixy = (split(/\s|,|\R$/,$arealimits));
- $limi = int(($lonlo-$lixy[0]) * $picx / $mapx).",".int(($latlo-$lixy[1]) * $picy / $mapy);
- for (my $i=2;$i<@lixy;$i+=2){
- $limi .= ",".int(($lonlo-$lixy[$i]) * $picx / $mapx).",".int(($latlo-$lixy[$i+1]) * $picy / $mapy);
- }
- }
-
- # PROPERTY LIMITS
- my $propertylimits = AttrVal($name,'propertyLimits','');
- my $propli = '';
- if ($propertylimits) {
- my @propxy = (split(/\s|,|\R$/,$propertylimits));
- $propli = int(($lonlo-$propxy[0]) * $picx / $mapx).",".int(($latlo-$propxy[1]) * $picy / $mapy);
- for (my $i=2;$i<@propxy;$i+=2){
- $propli .= ",".int(($lonlo-$propxy[$i]) * $picx / $mapx).",".int(($latlo-$propxy[$i+1]) * $picy / $mapy);
- }
- }
-
- map {
- ::FW_directNotify("#FHEMWEB:$_", $type . "UpdateDetail ( '$name', '$type', '$img', $picx, $picy, $cslon, $cslat, '$csimgpos', $scalx, [ $posxy ], [ $limi ], [ $propli ], [ $poscxy ] )","");
- } devspec2array("TYPE=FHEMWEB");
- }
- return undef;
-}
-
#########################
sub Attr {
@@ -951,7 +870,7 @@ sub Attr {
CommandAttr($hash,"$name mapImageWidthHeight $1 $2");
}
- readMap( $hash );
+ ::FHEM::Devices::AMConnect::Common::readMap( $hash );
Log3 $name, 3, "$iam $cmd $attrName $attrVal";
} else {
return "$iam $cmd $attrName wrong image type, use webp, png, jpeg or jpg";
@@ -1074,7 +993,7 @@ sub Attr {
if( $cmd eq "set" ) {
- return "$iam $attrName has a wrong format use " unless( $attrVal =~ /(\d+)\s(\d+)/ );
+ return "$iam $attrName has a wrong format use " unless( $attrVal =~ /(-?\d+)\s(-?\d+)/ );
Log3 $name, 3, "$iam $cmd $attrName $attrVal";
} elsif( $cmd eq "del" ) {
@@ -1103,430 +1022,16 @@ sub Attr {
}
#########################
-sub Undefine {
- my ( $hash, $arg ) = @_;
- my $name = $hash->{NAME};
- my $type = $hash->{TYPE};
-
- RemoveInternalTimer($hash);
- RemoveExtension("$type/$name/map");
- return undef;
-}
-
-##########################
-sub Delete {
- my ( $hash, $arg ) = @_;
- my $name = $hash->{NAME};
- my $type = $hash->{TYPE};
- my $iam ="$type $name Delete: ";
- Log3( $name, 5, "$iam called" );
-
- my ($passResp,$passErr) = $hash->{helper}->{passObj}->setDeletePassword($name);
- Log3( $name, 1, "$iam error: $passErr" ) if ($passErr);
-
- return;
-}
-
-##########################
-sub Rename {
- my ( $newname, $oldname ) = @_;
- my $hash = $defs{$newname};
-
- my ( $passResp, $passErr ) = $hash->{helper}->{passObj}->setRename( $newname, $oldname );
- Log3 $newname, 2, "$newname password rename error: $passErr" if ($passErr);
-
- return undef;
-}
-
-
-###############################################################################
-#
-# HELPER FUINCTION
-#
-###############################################################################
-
-sub AlignArray {
- my ($hash) = @_;
- my $name = $hash->{NAME};
- if ($hash->{helper}{searchpos} && $hash->{helper}{cspos} && $hash->{helper}{areapos} && @{$hash->{helper}{searchpos}} > 1 && @{$hash->{helper}{cspos}} > 1 && @{$hash->{helper}{areapos}} > 1) {
- my $i = 0;
- my $k = -1;
- my $poslen = @{$hash->{helper}{mower}{attributes}{positions}};
- my $searchlen = 2;
- my @searchposlon = ($hash->{helper}{searchpos}[0]{longitude}, $hash->{helper}{searchpos}[1]{longitude});
- my @searchposlat = ($hash->{helper}{searchpos}[0]{latitude}, $hash->{helper}{searchpos}[1]{latitude});
- my $activity = $hash->{helper}{mower}{attributes}{mower}{activity};
- my $arrayName = $hash->{helper}{$activity}{arrayName};
- my $maxLength = $hash->{helper}{$activity}{maxLength};
- for ( $i = 0; $i < $poslen-3; $i++ ) { # 3 instead of 1 due to new alignment
- if ( $searchposlon[0] == $hash->{helper}{mower}{attributes}{positions}[ $i ]{longitude}
- && $searchposlat[0] == $hash->{helper}{mower}{attributes}{positions}[ $i ]{latitude}
- && $searchposlon[1] == $hash->{helper}{mower}{attributes}{positions}[ $i+1 ]{longitude}
- && $searchposlat[1] == $hash->{helper}{mower}{attributes}{positions}[ $i+1 ]{latitude} ) {
- # || $i == $poslen-2 ) { # not nessecary due to new alignment
- # $i++ if ( $i == $poslen-2 ); # not nessecary due to new alignment
- # timediff per step
- my $dt = 0;
- $dt = int(($hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{$arrayName}[0]{statusTimestamp})/$i) if ($i);
- for ($k=$i-1;$k>-1;$k--) {
-
- unshift (@{$hash->{helper}{$arrayName}}, dclone($hash->{helper}{mower}{attributes}{positions}[ $k ]) );
- pop (@{$hash->{helper}{$arrayName}}) if (@{$hash->{helper}{$arrayName}} > $maxLength);
- $hash->{helper}{$arrayName}[0]{statusTimestamp} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $dt * $k;
-
- unshift (@{$hash->{helper}{searchpos}}, dclone($hash->{helper}{mower}{attributes}{positions}[ $k ]) );
- pop (@{$hash->{helper}{searchpos}}) if (@{$hash->{helper}{searchpos}} > $searchlen);
-
- my @temp = ();
- unshift ( @temp, dclone( $hash->{helper}{mower}{attributes}{positions}[ $k ] ) );
- posMinMax( $hash, \@temp );
-
- }
-
- #callFn if present
- if ($hash->{helper}{$activity}{callFn}) {
-
- $hash->{helper}{$activity}{cnt} = $i;
- no strict "refs";
- &{$hash->{helper}{$activity}{callFn}}($hash);
- use strict "refs";
-
- }
-
- $hash->{helper}{newdatasets} = $i;
- readingsSingleUpdate($hash, "statistics_newGeoDataSets", $i, 1);
- last;
-
- }
-
- }
- }
-}
-
-#########################
-sub ChargingStationPosition {
- my ($hash) = @_;
- my $n = @{$hash->{helper}{cspos}};
- my $xm = 0;
- map { $xm += $_->{longitude} } @{$hash->{helper}{cspos}};
- $xm = $xm/$n;
- my $ym = 0;
- map { $ym += $_->{latitude} } @{$hash->{helper}{cspos}};
- $ym = $ym/$n;
- $hash->{helper}{chargingStation}{longitude} = sprintf("%.8f",$xm);
- $hash->{helper}{chargingStation}{latitude} = sprintf("%.8f",$ym);
- return undef;
-}
-
-#########################
-sub AreaStatistics {
- my ($hash) = @_;
- my $name = $hash->{NAME};
- my $activity = 'MOWING';
- my $i = $hash->{helper}{$activity}{cnt};
- my $k = 0;
- my @xyarr = @{$hash->{helper}{areapos}};# areapos
- my $n = @xyarr;
- AttrVal($name,'scaleToMeterXY', $hash->{helper}{scaleToMeterLongitude} . ' ' .$hash->{helper}{scaleToMeterLatitude}) =~ /(\d+)\s+(\d+)/;
- my ($sclon, $sclat) = ($1, $2);
- my $lsum = 0;
- my $asum = 0;
- my $vm = 0;
-
- for ( $k = 0; $k <= $i-1; $k++) {
-
- $lsum += ((($xyarr[ $k ]{longitude} - $xyarr[ $k+1 ]{longitude}) * $sclon)**2 + (($xyarr[ $k ]{latitude} - $xyarr[ $k+1 ]{latitude}) * $sclat)**2)**0.5;
-
- }
-
- $asum = $lsum * AttrVal($name,'mowerCuttingWidth',0.24);
- my $td = $xyarr[ 0 ]{storedTimestamp} - $xyarr[ $k ]{storedTimestamp};
- $vm = sprintf( '%.6f', $lsum / $td ) * 1000 if ($td); # m/s
- $hash->{helper}{$activity}{speed} = $vm;
- $hash->{helper}{$activity}{track} = $lsum;
- $hash->{helper}{$activity}{area} = $asum;
- $hash->{helper}{statistics}{currentSpeed} = $vm;
- $hash->{helper}{statistics}{currentDayTrack} += $lsum;
- $hash->{helper}{statistics}{currentDayArea} += $asum;
- $hash->{helper}{statistics}{currentSpeed} = $vm;
-
- return undef;
-}
-
-#########################
-sub AddExtension {
- my ( $name, $func, $link ) = @_;
- my $hash = $defs{$name};
- my $type = $hash->{TYPE};
-
- my $url = "/$link";
- Log3( $name, 2, "Registering $type $name for URL $url..." );
- $::data{FWEXT}{$url}{deviceName} = $name;
- $::data{FWEXT}{$url}{FUNC} = $func;
- $::data{FWEXT}{$url}{LINK} = $link;
-
- return;
-}
-
-#########################
-sub RemoveExtension {
- my ($link) = @_;
- my $url = "/$link";
- my $name = $::data{FWEXT}{$url}{deviceName};
- my $hash = $defs{$name};
- my $type = $hash->{TYPE};
-
- Log3( $name, 2, "Unregistering $type $name for URL $url..." );
- delete $::data{FWEXT}{$url};
-
- return;
-}
-
-#########################
-sub GetMap() {
- my ($request) = @_;
-
- if ( $request =~ /^\/AutomowerConnect\/(\w+)\/map/ ) {
- my $name = $1;
- my $hash = $::defs{$name};
- return ( "text/plain; charset=utf-8","AutomowerConnect: No MAP_MIME for webhook $request" ) if ( !defined $hash->{helper}{MAP_MIME} || !$hash->{helper}{MAP_MIME} );
- return ( "text/plain; charset=utf-8","AutomowerConnect: No MAP_CACHE for webhook $request" ) if ( !defined $hash->{helper}{MAP_CACHE} || !$hash->{helper}{MAP_CACHE} );
- my $mapMime = $hash->{helper}{MAP_MIME};
- my $mapData = $hash->{helper}{MAP_CACHE};
- return ( $mapMime, $mapData );
- }
- return ( "text/plain; charset=utf-8","No AutomowerConnect device for webhook $request" );
-
-}
-
-#########################
-sub readMap {
- my ($hash) = @_;
- my $name = $hash->{NAME};
- my $type = $hash->{TYPE};
- my $iam = "$type $name readMap:";
- RemoveInternalTimer( $hash, \&readMap );
- my $filename = $hash->{helper}{MAP_PATH};
-
- if ( $filename and -e $filename ) {
- open my $fh, '<:raw', $filename or die $!;
- my $content = '';
- while (1) {
- my $success = read $fh, $content, 1024, length($content);
- die $! if not defined $success;
- last if not $success;
- }
- close $fh;
- $hash->{helper}{MAP_CACHE} = $content;
- Log3 $name, 5, "$iam file \"$filename\" content length: ".length($content);
- } else {
- Log3 $name, 2, "$iam file \"$filename\" does not exist.";
- }
-}
-
-#########################
-sub posMinMax {
- my ($hash, $poshash) = @_;
- my $minLon = $hash->{helper}{minLon};
- my $maxLon = $hash->{helper}{maxLon};
- my $minLat = $hash->{helper}{minLat};
- my $maxLat = $hash->{helper}{maxLat};
-
- for ( @{$poshash} ) {
- $minLon = minNum( $minLon,$_->{longitude} );
- $maxLon = maxNum( $maxLon,$_->{longitude} );
- $minLat = minNum( $minLat,$_->{latitude} );
- $maxLat = maxNum( $maxLat,$_->{latitude} );
- }
-
- $hash->{helper}{minLon} = $minLon;
- $hash->{helper}{maxLon} = $maxLon;
- $hash->{helper}{minLat} = $minLat;
- $hash->{helper}{maxLat} = $maxLat;
- $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);
-
- return undef;
-}
-
-#########################
-sub listStatisticsData {
- my ( $hash ) = @_;
- my $name = $hash->{NAME};
- my $cnt = 0;
- my $ret = '';
- $ret .= '';
- $ret .= 'Statistics Data';
-
- $ret .= '';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} | ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} | ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} . ' | s |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} . ' | s |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} . '1 | s |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} . ' | s |
';
-
- $cnt++;$ret .= ' $hash->{helper}{statistics}{currentSpeed} | ' . $hash->{helper}{statistics}{currentSpeed} . ' | m/s |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{currentDayTrack} | ' . $hash->{helper}{statistics}{currentDayTrack} . ' | m |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{currentDayArea} | ' . $hash->{helper}{statistics}{currentDayArea} . ' | qm |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{lastDayTrack} | ' . $hash->{helper}{statistics}{lastDayTrack} . ' | m |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{lastDayArea} | ' . $hash->{helper}{statistics}{lastDayArea} . ' | qm |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{currentWeekTrack} | ' . $hash->{helper}{statistics}{currentWeekTrack} . ' | m |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{currentWeekArea} | ' . $hash->{helper}{statistics}{currentWeekArea} . ' | qm |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{lastWeekTrack} | ' . $hash->{helper}{statistics}{lastWeekTrack} . ' | m |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{lastWeekArea} | ' . $hash->{helper}{statistics}{lastWeekArea} . ' | qm |
';
-
- $ret .= '
';
- $ret .= '1 totalRunningTime = totalCuttingTime + totalSearchingTime';
- $ret .= '';
-
- return $ret;
-}
-
-#########################
-sub listMowerData {
- my ( $hash ) = @_;
- my $name = $hash->{NAME};
- my $cnt = 0;
- my $ret = '';
- $ret .= '
';
- $ret .= 'Mower Data';
-
- $ret .= '';
- $cnt++;$ret .= ' $hash->{helper}{mower}{type} | ' . $hash->{helper}{mower}{type} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{id} | ' . $hash->{helper}{mower}{id} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{system}{name} | ' . $hash->{helper}{mower}{attributes}{system}{name} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{system}{model} | ' . $hash->{helper}{mower}{attributes}{system}{model} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{system}{serialNumber} | ' . $hash->{helper}{mower}{attributes}{system}{serialNumber} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{battery}{batteryPercent} | ' . $hash->{helper}{mower}{attributes}{battery}{batteryPercent} . ' | % |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{mode} | ' . $hash->{helper}{mower}{attributes}{mower}{mode} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{activity} | ' . $hash->{helper}{mower}{attributes}{mower}{activity} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{state} | ' . $hash->{helper}{mower}{attributes}{mower}{state} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{errorCode} | ' . $hash->{helper}{mower}{attributes}{mower}{errorCode} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} | ' . $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} . ' | ms |
';
-
- my $calendarjson = eval { JSON::XS->new->pretty(1)->encode ($hash->{helper}{mower}{attributes}{calendar}{tasks}) };
-
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{calendar}{tasks} | ' . ($@ ? $@ : $calendarjson) . ' |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{planner}{nextStartTimestamp} | ' . $hash->{helper}{mower}{attributes}{planner}{nextStartTimestamp} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{planner}{override}{action} | ' . $hash->{helper}{mower}{attributes}{planner}{override}{action} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{planner}{restrictedReason} | ' . $hash->{helper}{mower}{attributes}{planner}{restrictedReason} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{metadata}{connected} | ' . $hash->{helper}{mower}{attributes}{metadata}{connected} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} | ' . $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} . ' | ms |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{positions}[0]{longitude} | ' . $hash->{helper}{mower}{attributes}{positions}[0]{longitude} . ' | decimal degree |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{positions}[0]{latitude} | ' . $hash->{helper}{mower}{attributes}{positions}[0]{latitude} . ' | decimal degree |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} | ' . $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{settings}{headlight}{mode} | ' . $hash->{helper}{mower}{attributes}{settings}{headlight}{mode} . ' | |
';
-# $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{cuttingBladeUsageTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{cuttingBladeUsageTime} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} | ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} | ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} . ' | s |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} . ' | s |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} . '1 | s |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} . ' | s |
';
-
- $ret .= '
';
- $ret .= '1 totalRunningTime = totalCuttingTime + totalSearchingTime';
- $ret .= '';
-
- return $ret;
-}
-
-#########################
-sub listInternalData {
- my ( $hash ) = @_;
- my $name = $hash->{NAME};
- my $rowCount = 1;
- my $ret = '
';
- $ret .= 'Calculated Coordinates For Automatic Registration';
-
- $hash->{helper}{posMinMax} =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)(\R|-?\s)(\d*\.?\d+)\s(-?\d*\.?\d+)/;
-
- my $xm = $hash->{helper}{chargingStation}{longitude} // 0;
- my $ym = $hash->{helper}{chargingStation}{latitude} // 0;
- my $csnr = scalar @{$hash->{helper}{cspos}};
- my $csnrmax = $hash->{helper}{PARKED_IN_CS}{maxLength};
- my $arnr = 0;
- $arnr = scalar @{$hash->{helper}{areapos}} if( scalar @{$hash->{helper}{areapos}} > 2 );
- my $arnrmax = $hash->{helper}{MOWING}{maxLength};
-
- $ret .= '';
- $ret .= ' ' . ($csnr + $arnr) . ' ( ' . ($csnrmax + $arnrmax) . ' ) | Upper Left | ' . $1 . ' | ' . $2 . ' |
';
- $ret .= ' Lower Right | ' . $4 . ' | ' . $5 . ' |
';
-
- $ret .= '
';
- $ret .= '
';
- $ret .= 'Calculated Charging Station Coordinates';
-
- $ret .= '';
- $ret .= ' ' . $csnr . ' ( ' . $csnrmax . ' ) | ' . $xm . ' | ' . $ym . ' |
';
-
- $ret .= '
';
- $ret .= '
';
- $ret .= 'Way Point Stacks';
-
- $ret .= '';
- $ret .= 'PARKED_IN_CS, CHARGING | cspos | ' . $csnr . ' | ' . $csnrmax . ' |
';
- $ret .= 'MOWING | areapos | ' . $arnr . ' | ' . $arnrmax . ' |
';
-
- $ret .= '
';
- if ( $hash->{TYPE} eq 'AutomowerConnect' ) {
-
- $ret .= '';
- $ret .= 'Authentification Data';
-
- $ret .= ' Authentification URL | ' . AUTHURL . ' |
';
- $ret .= ' Client-Id | ' . $hash->{helper}{client_id} . ' |
';
- $ret .= ' Grant-Type | ' . $hash->{helper}{grant_type} . ' |
';
- $ret .= ' User-Id | ' . ReadingsVal($name, '.user_id', '-') . ' |
';
- $ret .= ' Provider | ' . ReadingsVal($name, '.provider', '-') . ' |
';
- $ret .= ' Scope | ' . ReadingsVal($name, '.scope', '-') . ' |
';
- $ret .= ' Token Type | ' . ReadingsVal($name, '.token_type', '-') . ' |
';
- $ret .= ' Token Expires | ' . FmtDateTime( ReadingsVal($name, '.expires', '0') ) . ' |
';
- $ret .= ' Access Token | ' . ReadingsVal($name, '.access_token', '0') . ' |
';
-
- $ret .= '
';
-
- }
- $ret .= '';
-
- return $ret;
-}
-
-#########################
-sub listErrorCodes {
- my ( $hash ) = @_;
- my $rowCount = 1;
- my %ec = ();
- my $ec = \%ec;
- for ( keys %{$hash->{helper}{errortable}} ) {
- $ec->{sprintf("%03d",$_)} = $hash->{helper}{errortable}{$_} ;
- }
- my $ret = '';
- $ret .= 'API-Response Status Codes';
- $ret .= '200, 201, 202 204 | response o.k. |
';
- $ret .= '400, 401, 402 403, 404, 415 500, 503 | error, detailed information see logfile |
';
- $ret .= '
';
- $ret .= 'Mower Error Table';
- for (sort keys %{$ec}) {
- $ret .= '{$_};
- $ret .= '
';
- $rowCount++;
- }
- $ret .= '
';
-
- return $ret;
+sub FmtDateTimeGMT {
+ my $ret = POSIX::strftime( "%F %H:%M:%S", gmtime( shift ) );
}
##############################################################
+
1;
+__END__
=pod
@@ -1777,14 +1282,14 @@ sub listErrorCodes {
mower_mode - current working mode "MAIN_AREA" | "SECONDARY_AREA" | "HOME" | "DEMO" | "UNKNOWN"
mower_state - current status "UNKNOWN" | "NOT_APPLICABLE" | "PAUSED" | "IN_OPERATION" | "WAIT_UPDATING" | "WAIT_POWER_UP" | "RESTRICTED" | "OFF" | "STOPPED" | "ERROR" | "FATAL_ERROR" |"ERROR_AT_POWER_UP"
planner_nextStart - next start time
- planner_restrictedReason - reason for parking NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST
+ planner_restrictedReason - reason for parking NOT_APPLICABLE, NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST
planner_overrideAction - reason for override a planned action NOT_ACTIVE, FORCE_PARK, FORCE_MOW
state - status of connection FHEM to Husqvarna Cloud API and device state(e.g. defined, authorization, authorized, connected, error, update)
settings_cuttingHeight - actual cutting height from API
settings_headlight - actual headlight mode from API
statistics_newGeoDataSets - number of new data sets between the last two different time stamps
statistics_numberOfCollisions - Number of Collisions
- status_connected - state of connetion between mower and Husqvarna Cloud, (1 => true, 0 => false)
+ status_connected - state of connetion between mower and Husqvarna Cloud, (1 => CONNECTED, 0 => OFFLINE)
status_statusTimestamp - local time of last change of the API content
status_statusTimestampDiff - time difference in seconds between the last and second last change of the API content
status_statusTimestampOld - local time of second last change of the API content
@@ -2042,14 +1547,14 @@ sub listErrorCodes {
mower_mode - aktueller Arbeitsmodus "MAIN_AREA" | "SECONDARY_AREA" | "HOME" | "DEMO" | "UNKNOWN"
mower_state - aktueller Status "UNKNOWN" | "NOT_APPLICABLE" | "PAUSED" | "IN_OPERATION" | "WAIT_UPDATING" | "WAIT_POWER_UP" | "RESTRICTED" | "OFF" | "STOPPED" | "ERROR" | "FATAL_ERROR" |"ERROR_AT_POWER_UP"
planner_nextStart - nächste Startzeit
- planner_restrictedReason - Grund für Parken NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST
+ planner_restrictedReason - Grund für Parken NOT_APPLICABLE, NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST
planner_overrideAction - Grund für vorrangige Aktion NOT_ACTIVE, FORCE_PARK, FORCE_MOW
state - Status der Verbindung des FHEM-Gerätes zur Husqvarna Cloud API (defined, authentification, authentified, connected, error, update).
settings_cuttingHeight - aktuelle Schnitthöhe aus der API
settings_headlight - aktueller Scheinwerfermode aus der API
statistics_newGeoDataSets - Anzahl der neuen Datensätze zwischen den letzten zwei unterschiedlichen Zeitstempeln
statistics_numberOfCollisions - Anzahl der Kollisionen
- status_connected - Status der Verbindung zwischen dem Automower und der Husqvarna Cloud, (1 => true, 0 => false)
+ status_connected - Status der Verbindung zwischen dem Automower und der Husqvarna Cloud, (1 => CONNECTED, 0 => OFFLINE)
status_statusTimestamp - Lokalzeit der letzten Änderung der Daten in der API
status_statusTimestampDiff - Zeitdifferenz zwischen den beiden letzten Änderungen im Inhalt der Daten aus der API
status_statusTimestampOld - Lokalzeit der vorletzten Änderung der Daten in der API
diff --git a/fhem/FHEM/75_AutomowerConnectDevice.pm b/fhem/FHEM/75_AutomowerConnectDevice.pm
index 5bbd0745f..d520348b9 100644
--- a/fhem/FHEM/75_AutomowerConnectDevice.pm
+++ b/fhem/FHEM/75_AutomowerConnectDevice.pm
@@ -78,7 +78,8 @@ my $missingModul = "";
eval "use JSON;1" or $missingModul .= "JSON ";
require HttpUtils;
-use constant AUTHURL => 'https://api.authentication.husqvarnagroup.dev/v1';
+require FHEM::Devices::AMConnect::Common;
+
use constant APIURL => 'https://api.amc.husqvarna.dev/v1';
##############################################################
@@ -87,11 +88,11 @@ sub Initialize() {
my ($hash) = @_;
$hash->{SetFn} = \&Set;
- $hash->{GetFn} = \&Get;
+ $hash->{GetFn} = \&FHEM::Devices::AMConnect::Common::Get;
$hash->{DefFn} = \&Define;
- $hash->{UndefFn} = \&Undef;
+ $hash->{UndefFn} = \&FHEM::Devices::AMConnect::Common::Undefine;
$hash->{NotifyFn} = \&Notify;
- $hash->{FW_detailFn}= \&FW_detailFn;
+ $hash->{FW_detailFn}= \&FHEM::Devices::AMConnect::Common::FW_detailFn;
$hash->{AttrFn} = \&Attr;
$hash->{AttrList} = "disable:1,0 " .
"debug:1,0 " .
@@ -148,11 +149,14 @@ sub Define{
maxLat => -90,
imageHeight => 650,
imageWidthHeight => '350 650',
- posMinMax => "-180 90\n 180 -90",
+ posMinMax => "-180 90\n180 -90",
newdatasets => 0,
MAP_PATH => '',
MAP_MIME => '',
MAP_CACHE => '',
+ cspos => [],
+ areapos => [],
+ searchpos => [],
UNKNOWN => {
arrayName => '',
maxLength => 0,
@@ -167,7 +171,7 @@ sub Define{
arrayName => 'areapos',
maxLength => 500,
maxLengthDefault => 500,
- callFn => \&AreaStatistics
+ callFn => \&FHEM::Devices::AMConnect::Common::AreaStatistics
},
GOING_HOME => {
arrayName => '',
@@ -177,7 +181,7 @@ sub Define{
CHARGING => {
arrayName => 'cspos',
maxLength => 100,
- callFn => \&ChargingStationPosition
+ callFn => \&FHEM::Devices::AMConnect::Common::ChargingStationPosition
},
LEAVING => {
arrayName => '',
@@ -187,7 +191,7 @@ sub Define{
PARKED_IN_CS => {
arrayName => 'cspos',
maxLength => 100,
- callFn => \&ChargingStationPosition
+ callFn => \&FHEM::Devices::AMConnect::Common::ChargingStationPosition
},
STOPPED_IN_GARDEN => {
arrayName => '',
@@ -209,29 +213,19 @@ sub Define{
);
-my $errorjson = <<'EOF';
-{"23":"Wheel drive problem, left","24":"Cutting system blocked","123":"Destination not reachable","710":"SIM card locked","50":"Guide 1 not found","717":"SMS could not be sent","108":"Folding cutting deck sensor defect","4":"Loop sensor problem, front","15":"Lifted","29":"Slope too steep","1":"Outside working area","45":"Cutting height problem, dir","52":"Guide 3 not found","28":"Memory circuit problem","95":"Folding sensor activated","9":"Trapped","114":"Too high discharge current","103":"Cutting drive motor 2 defect","65":"Temporary battery problem","119":"Zone generator problem","6":"Loop sensor problem, left","82":"Wheel motor blocked, rear right","714":"Geofence problem","703":"Connectivity problem","708":"SIM card locked","75":"Connection changed","7":"Loop sensor problem, right","35":"Wheel motor overloaded, right","3":"Wrong loop signal","117":"High internal power loss","0":"Unexpected error","80":"Cutting system imbalance - Warning","110":"Collision sensor error","100":"Ultrasonic Sensor 3 defect","79":"Invalid battery combination - Invalid combination of different battery types.","724":"Communication circuit board SW must be updated","86":"Wheel motor overloaded, rear right","81":"Safety function faulty","78":"Slipped - Mower has Slipped.Situation not solved with moving pattern","107":"Docking sensor defect","33":"Mower tilted","69":"Alarm! Mower switched off","68":"Temporary battery problem","34":"Cutting stopped - slope too steep","127":"Battery problem","73":"Alarm! Mower in motion","74":"Alarm! Outside geofence","713":"Geofence problem","87":"Wheel motor overloaded, rear left","120":"Internal voltage error","39":"Cutting motor problem","704":"Connectivity problem","63":"Temporary battery problem","109":"Loop sensor defect","38":"Electronic problem","64":"Temporary battery problem","113":"Complex working area","93":"No accurate position from satellites","104":"Cutting drive motor 3 defect","709":"SIM card not found","94":"Reference station communication problem","43":"Cutting height problem, drive","13":"No drive","44":"Cutting height problem, curr","118":"Charging system problem","14":"Mower lifted","57":"Guide calibration failed","707":"SIM card requires PIN","99":"Ultrasonic Sensor 2 defect","98":"Ultrasonic Sensor 1 defect","51":"Guide 2 not found","56":"Guide calibration accomplished","49":"Ultrasonic problem","2":"No loop signal","124":"Destination blocked","25":"Cutting system blocked","19":"Collision sensor problem, front","18":"Collision sensor problem, rear","48":"No response from charger","105":"Lift Sensor defect","111":"No confirmed position","10":"Upside down","40":"Limited cutting height range","716":"Connectivity problem","27":"Settings restored","90":"No power in charging station","21":"Wheel motor blocked, left","26":"Invalid sub-device combination","92":"Work area not valid","702":"Connectivity settings restored","125":"Battery needs replacement","5":"Loop sensor problem, rear","12":"Empty battery","55":"Difficult finding home","42":"Limited cutting height range","30":"Charging system problem","72":"Alarm! Mower tilted","85":"Wheel drive problem, rear left","8":"Wrong PIN code","62":"Temporary battery problem","102":"Cutting drive motor 1 defect","116":"High charging power loss","122":"CAN error","60":"Temporary battery problem","705":"Connectivity problem","711":"SIM card locked","70":"Alarm! Mower stopped","32":"Tilt sensor problem","37":"Charging current too high","89":"Invalid system configuration","76":"Connection NOT changed","71":"Alarm! Mower lifted","88":"Angular sensor problem","701":"Connectivity problem","715":"Connectivity problem","61":"Temporary battery problem","66":"Battery problem","106":"Collision sensor defect","67":"Battery problem","112":"Cutting system major imbalance","83":"Wheel motor blocked, rear left","84":"Wheel drive problem, rear right","126":"Battery near end of life","77":"Com board not available","36":"Wheel motor overloaded, left","31":"STOP button problem","17":"Charging station blocked","54":"Weak GPS signal","47":"Cutting height problem","53":"GPS navigation problem","121":"High internal temerature","97":"Left brush motor overloaded","712":"SIM card locked","20":"Wheel motor blocked, right","91":"Switch cord problem","96":"Right brush motor overloaded","58":"Temporary battery problem","59":"Temporary battery problem","22":"Wheel drive problem, right","706":"Poor signal quality","41":"Unexpected cutting height adj","46":"Cutting height blocked","11":"Low battery","16":"Stuck in charging station","101":"Ultrasonic Sensor 4 defect","115":"Too high internal current"}
-EOF
- my $errortable = eval { decode_json ($errorjson) };
- if ($@) {
- return "$iam $@";
- }
-
- $hash->{helper}{errortable} = $errortable;
- $errorjson = undef;
- $errortable = undef;
-
$hash->{MODEL} = '';
$attr{$name}{room} = 'AutomowerConnect' if( !defined( $attr{$name}{room} ) );
$attr{$name}{icon} = 'automower' if( !defined( $attr{$name}{icon} ) );
- if (::AnalyzeCommandChain(undef,"version 75_AutomowerConnectDevice.pm noheader") =~ "^75_AutomowerConnectDevice.pm (.*)Z") {
+ my ($modname) = __FILE__ =~ /(\d\d_.*\.pm)/;
+
+ if (::AnalyzeCommandChain(undef,"version $modname noheader") =~ "^$modname (.*)Z") {
$hash->{VERSION}=$1;
}
RemoveInternalTimer($hash);
- InternalTimer( gettimeofday() + 25, \&readMap, $hash, 0);
+ InternalTimer( gettimeofday() + 25, \&FHEM::Devices::AMConnect::Common::readMap, $hash, 0);
- AddExtension( $name, \&GetMap, "$type/$name/map" );
+ ::FHEM::Devices::AMConnect::Common::AddExtension( $name, \&FHEM::Devices::AMConnect::Common::GetMap, "$type/$name/map" );
readingsSingleUpdate( $hash, 'state', 'defined', 1 );
@@ -285,16 +279,8 @@ sub Notify {
$hash->{helper}{searchpos} = [ dclone( $hash->{helper}{mowerold}{attributes}{positions}[0] ), dclone( $hash->{helper}{mowerold}{attributes}{positions}[1] ) ];
- $hash->{helper}{areapos} = [ dclone( $hash->{helper}{mowerold}{attributes}{positions}[0] ), dclone( $hash->{helper}{mowerold}{attributes}{positions}[1] ) ];
- $hash->{helper}{areapos}[0]{statusTimestamp} = $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp};
- $hash->{helper}{areapos}[1]{statusTimestamp} = $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} - 12000;
-
- $hash->{helper}{cspos} = [ dclone( $hash->{helper}{mowerold}{attributes}{positions}[0] ), dclone( $hash->{helper}{mowerold}{attributes}{positions}[1] ) ];
- $hash->{helper}{cspos}[0]{statusTimestamp} = $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp};
- $hash->{helper}{cspos}[1]{statusTimestamp} = $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} - 600000;
-
if ( AttrVal( $name, 'mapImageCoordinatesToRegister', '' ) eq '' ) {
- posMinMax( $hash, $hash->{helper}{mowerold}{attributes}{positions} );
+ ::FHEM::Devices::AMConnect::Common::posMinMax( $hash, $hash->{helper}{mowerold}{attributes}{positions} );
}
}
@@ -302,11 +288,13 @@ sub Notify {
$hash->{helper}{mower} = $myMower;
# add alignment data set to the end
push( @{ $hash->{helper}{mower}{attributes}{positions} }, @{ dclone( $hash->{helper}{searchpos} ) } );
+ $hash->{helper}{newdatasets} = 0;
my $storediff = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp};
if ($storediff) {
- AlignArray( $hash );
- FW_detailFn_Update ($hash) if (AttrVal($name,'showMap',1));
+
+ ::FHEM::Devices::AMConnect::Common::AlignArray( $hash );
+ ::FHEM::Devices::AMConnect::Common::FW_detailFn_Update ($hash) if (AttrVal($name,'showMap',1));
}
@@ -316,18 +304,21 @@ sub Notify {
readingsBulkUpdateIfChanged($hash, "batteryPercent", $hash->{helper}{mower}{attributes}{battery}{batteryPercent} );
my $pref = 'mower';
- readingsBulkUpdateIfChanged($hash, $pref.'_id', $hash->{helper}{mower}{id} );
readingsBulkUpdateIfChanged($hash, $pref.'_mode', $hash->{helper}{mower}{attributes}{$pref}{mode} );
readingsBulkUpdateIfChanged($hash, $pref.'_activity', $hash->{helper}{mower}{attributes}{$pref}{activity} );
readingsBulkUpdateIfChanged($hash, $pref.'_state', $hash->{helper}{mower}{attributes}{$pref}{state} );
readingsBulkUpdateIfChanged($hash, $pref.'_commandStatus', 'cleared' );
+
my $tstamp = $hash->{helper}{mower}{attributes}{$pref}{errorCodeTimestamp};
- my $timestamp = FmtDateTime($tstamp/1000);
+ my $timestamp = FmtDateTimeGMT($tstamp/1000);
readingsBulkUpdateIfChanged($hash, $pref."_errorCodeTimestamp", $tstamp ? $timestamp : '-' );
+
my $errc = $hash->{helper}{mower}{attributes}{$pref}{errorCode};
readingsBulkUpdateIfChanged($hash, $pref.'_errorCode', $tstamp ? $errc : '-');
- my $errd = $hash->{helper}{errortable}{$errc};
+
+ my $errd = $::FHEM::Devices::AMConnect::Common::errortable->{$errc};
readingsBulkUpdateIfChanged($hash, $pref.'_errorDescription', $tstamp ? $errd : '-');
+
$pref = 'system';
readingsBulkUpdateIfChanged($hash, $pref."_name", $hash->{helper}{mower}{attributes}{$pref}{name} );
my $model = $hash->{helper}{mower}{attributes}{$pref}{model};
@@ -340,20 +331,20 @@ sub Notify {
readingsBulkUpdateIfChanged($hash, "planner_overrideAction", $hash->{helper}{mower}{attributes}{$pref}{override}{action} );
$tstamp = $hash->{helper}{mower}{attributes}{$pref}{nextStartTimestamp};
- $timestamp = FmtDateTime($tstamp/1000);
+ $timestamp = FmtDateTimeGMT($tstamp/1000);
readingsBulkUpdateIfChanged($hash, "planner_nextStart", $tstamp ? $timestamp : '-' );
$pref = 'statistics';
readingsBulkUpdateIfChanged($hash, $pref."_numberOfCollisions", $hash->{helper}->{mower}{attributes}{$pref}{numberOfCollisions} );
+ readingsBulkUpdateIfChanged($hash, $pref."_newGeoDataSets", $hash->{helper}{newdatasets} );
$pref = 'settings';
readingsBulkUpdateIfChanged($hash, $pref."_headlight", $hash->{helper}->{mower}{attributes}{$pref}{headlight}{mode} );
readingsBulkUpdateIfChanged($hash, $pref."_cuttingHeight", $hash->{helper}->{mower}{attributes}{$pref}{cuttingHeight} );
$pref = 'status';
- readingsBulkUpdateIfChanged($hash, $pref."_connected", $hash->{helper}{mower}{attributes}{metadata}{connected} );
+ readingsBulkUpdateIfChanged($hash, $pref."_connected", ( $hash->{helper}{mower}{attributes}{metadata}{connected} ? 'CONNECTED' : 'OFFLINE' ) );
readingsBulkUpdateIfChanged($hash, $pref."_Timestamp", FmtDateTime( $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}/1000 ));
readingsBulkUpdateIfChanged($hash, $pref."_TimestampDiff", $storediff/1000 );
readingsBulkUpdateIfChanged($hash, $pref."_TimestampOld", FmtDateTime( $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp}/1000 ));
$pref = 'positions';
- readingsBulkUpdateIfChanged($hash, $pref."_lastLonLat", $hash->{helper}{mower}{attributes}{$pref}[0]{longitude} . ' ' . $hash->{helper}{mower}{attributes}{$pref}[0]{latitude} );
readingsBulkUpdate($hash, 'state', 'connected',1);
readingsEndUpdate($hash, 1);
@@ -511,50 +502,6 @@ sub CMDResponse {
return undef;
}
-##############################################################
-sub Get {
- my ($hash,@val) = @_;
- my $type = $hash->{TYPE};
-
- return "$type $hash->{NAME} Get: needs at least an argument" if ( @val < 2 );
-
- my ($name,$setName,$setVal,$setVal2,$setVal3) = @val;
- my $iam = "$type $name Get:";
-
- Log3 $name, 4, "$iam called with $setName " . ($setVal ? $setVal : "");
-
- if ( $setName eq 'html' ) {
-
- my $ret = '' . FW_detailFn( undef, $name, undef, undef) . '';
- return $ret;
-
- } elsif ( $setName eq 'errorCodes' ) {
-
- my $ret = listErrorCodes($hash);
- return $ret;
-
- } elsif ( $setName eq 'InternalData' ) {
-
- my $ret = listInternalData($hash);
- return $ret;
-
- } elsif ( $setName eq 'MowerData' ) {
-
- my $ret = listMowerData($hash);
- return $ret;
-
- } elsif ( $setName eq 'StatisticsData' ) {
-
- my $ret = listStatisticsData($hash);
- return $ret;
-
- } else {
-
- return "Unknown argument $setName, choose one of StatisticsData:noArg MowerData:noArg InternalData:noArg errorCodes:noArg ";
-
- }
-}
-
##############################################################
sub Set {
my ($hash,@a) = @_;
@@ -609,121 +556,6 @@ sub Set {
}
-#########################
-sub FW_detailFn {
- my ($FW_wname, $name, $room, $pageHash) = @_; # pageHash is set for summaryFn.
- my $hash = $defs{$name};
- my $type = $hash->{TYPE};
- return '' if( IsDisabled($name) || !AttrVal($name, 'showMap', 1) );
- if ( $hash->{helper} && $hash->{helper}{mower} && $hash->{helper}{mower}{attributes} && $hash->{helper}{mower}{attributes}{positions} && @{$hash->{helper}{mower}{attributes}{positions}} > 0 ) {
- my $img = "./fhem/$type/$name/map";
- my $zoom=AttrVal( $name,"mapImageZoom", 0.7 );
-
- AttrVal( $name,"mapImageWidthHeight", $hash->{helper}{imageWidthHeight} ) =~ /(\d+)\s(\d+)/;
- my ($picx,$picy) = ($1, $2);
-
- $picx=int($picx*$zoom);
- $picy=int($picy*$zoom);
- my $ret = "";
- $ret .= "";
- $ret .= "";
- $ret .= "";
- $ret .= "
";
-
- InternalTimer( gettimeofday() + 2.0, \&FW_detailFn_Update, $hash, 0 );
-
- return $ret;
- }
- return '';
-}
-
-#########################
-sub FW_detailFn_Update {
- my ($hash) = @_;
- my $name = $hash->{NAME};
- my $type = $hash->{TYPE};
- if ( $hash->{helper} && $hash->{helper}{mower} && $hash->{helper}{mower}{attributes} && $hash->{helper}{mower}{attributes}{positions} && @{$hash->{helper}{mower}{attributes}{positions}} > 0 ) {
-
- my @pos = ();
- my @posc = ();
- @pos = @{$hash->{helper}{areapos}}; # operational mode
- @posc =@{$hash->{helper}{cspos}}; # maybe operational mode
- my $img = "./fhem/$type/$name/map";
-
- AttrVal( $name,"mapImageCoordinatesToRegister",$hash->{helper}{posMinMax} ) =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)(\R|-?\s)(\d*\.?\d+)\s(-?\d*\.?\d+)/;
- my ( $lonlo, $latlo, $lonru, $latru ) = ($1, $2, $4, $5);
-
- my $zoom = AttrVal( $name,"mapImageZoom",0.7 );
-
- AttrVal( $name,"mapImageWidthHeight", $hash->{helper}{imageWidthHeight} ) =~ /(\d+)\s(\d+)/;
- my ($picx,$picy) = ($1, $2);
-
- AttrVal( $name,'scaleToMeterXY', $hash->{helper}{scaleToMeterLongitude} . ' ' .$hash->{helper}{scaleToMeterLatitude} ) =~ /(\d+)\s+(\d+)/;
- my $scalx = ($lonru-$lonlo) * $1;
-
- $picx = int($picx*$zoom);
- $picy = int($picy*$zoom);
- my $mapx = $lonlo-$lonru;
- my $mapy = $latlo-$latru;
-
- if ( ($hash->{helper}{PARKED_IN_CS}{callFn} || $hash->{helper}{CHARGING}{callFn}) && (!$hash->{helper}{chargingStation}{longitude} || !$hash->{helper}{chargingStation}{latitude}) ) {
- no strict "refs";
- &{$hash->{helper}{PARKED_IN_CS}{callFn}}( $hash );
- use strict "refs";
- }
-
- my $csimgpos = AttrVal( $name,"chargingStationImagePosition","right" );
- my $xm = $hash->{helper}{chargingStation}{longitude} // 10.1165;
- my $ym = $hash->{helper}{chargingStation}{latitude} // 51.28;
-
- AttrVal( $name,"chargingStationCoordinates","$xm $ym" ) =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)/;
- my ($cslo,$csla) = ($1, $2);
-
- my $cslon = int(($lonlo-$cslo) * $picx / $mapx);
- my $cslat = int(($latlo-$csla) * $picy / $mapy);
-
- # MOWING PATH
- my $posxy = int(($lonlo-$pos[0]{longitude}) * $picx / $mapx).",".int(($latlo-$pos[0]{latitude}) * $picy / $mapy);
- for (my $i=1;$i<@pos;$i++){
- $posxy .= ",".int(($lonlo-$pos[$i]{longitude}) * $picx / $mapx).",".int(($latlo-$pos[$i]{latitude}) * $picy / $mapy);
- }
-
- # CHARGING STATION PATH
- my $poscxy = int(($lonlo-$posc[0]{longitude}) * $picx / $mapx).",".int(($latlo-$posc[0]{latitude}) * $picy / $mapy);
- for (my $i=1;$i<@posc;$i++){
- $poscxy .= ",".int(($lonlo-$posc[$i]{longitude}) * $picx / $mapx).",".int(($latlo-$posc[$i]{latitude}) * $picy / $mapy);
- }
-
- # AREA LIMITS
- my $arealimits = AttrVal($name,'mowingAreaLimits','');
- my $limi = '';
- if ($arealimits) {
- my @lixy = (split(/\s|,|\R$/,$arealimits));
- $limi = int(($lonlo-$lixy[0]) * $picx / $mapx).",".int(($latlo-$lixy[1]) * $picy / $mapy);
- for (my $i=2;$i<@lixy;$i+=2){
- $limi .= ",".int(($lonlo-$lixy[$i]) * $picx / $mapx).",".int(($latlo-$lixy[$i+1]) * $picy / $mapy);
- }
- }
-
- # PROPERTY LIMITS
- my $propertylimits = AttrVal($name,'propertyLimits','');
- my $propli = '';
- if ($propertylimits) {
- my @propxy = (split(/\s|,|\R$/,$propertylimits));
- $propli = int(($lonlo-$propxy[0]) * $picx / $mapx).",".int(($latlo-$propxy[1]) * $picy / $mapy);
- for (my $i=2;$i<@propxy;$i+=2){
- $propli .= ",".int(($lonlo-$propxy[$i]) * $picx / $mapx).",".int(($latlo-$propxy[$i+1]) * $picy / $mapy);
- }
- }
-
- map {
- ::FW_directNotify("#FHEMWEB:$_", "AutomowerConnectUpdateDetail ('$name', '$type', '$img', $picx, $picy, $cslon, $cslat, '$csimgpos', $scalx, [ $posxy ], [ $limi ], [ $propli ], [ $poscxy ] )","");
- } devspec2array("TYPE=FHEMWEB");
- }
- return undef;
-}
-
-
#########################
sub Attr {
@@ -761,7 +593,7 @@ sub Attr {
CommandAttr($hash,"$name mapImageWidthHeight $1 $2");
}
- readMap($hash);
+ ::FHEM::Devices::AMConnect::Common::readMap($hash);
Log3 $name, 3, "$iam $cmd $attrName $attrVal";
} else {
return "$iam $attrName wrong image type, use webp, png, jpeg or jpg";
@@ -897,395 +729,17 @@ sub Attr {
return undef;
}
-
#########################
-sub Undef {
- my ( $hash, $arg ) = @_;
- my $name = $hash->{NAME};
- my $type = $hash->{TYPE};
- RemoveExtension("$type/$name/map");
- return undef;
+sub FmtDateTimeGMT {
+ my $ret = POSIX::strftime( "%F %H:%M:%S", gmtime( shift ) );
}
-###############################################################################
-#
-# HELPER FUINCTION
-#
-###############################################################################
-
-sub AlignArray {
- my ($hash) = @_;
- my $name = $hash->{NAME};
- if ($hash->{helper}{searchpos} && $hash->{helper}{cspos} && $hash->{helper}{areapos} && @{$hash->{helper}{searchpos}} > 1 && @{$hash->{helper}{cspos}} > 1 && @{$hash->{helper}{areapos}} > 1) {
- my $i = 0;
- my $k = -1;
- my $poslen = @{$hash->{helper}{mower}{attributes}{positions}};
- my $searchlen = 2;
- my @searchposlon = ($hash->{helper}{searchpos}[0]{longitude}, $hash->{helper}{searchpos}[1]{longitude});
- my @searchposlat = ($hash->{helper}{searchpos}[0]{latitude}, $hash->{helper}{searchpos}[1]{latitude});
- my $activity = $hash->{helper}{mower}{attributes}{mower}{activity};
- my $arrayName = $hash->{helper}{$activity}{arrayName};
- my $maxLength = $hash->{helper}{$activity}{maxLength};
- for ( $i = 0; $i < $poslen-3; $i++ ) { # 3 instead of 1 due to new alignment
- if ( $searchposlon[0] == $hash->{helper}{mower}{attributes}{positions}[ $i ]{longitude}
- && $searchposlat[0] == $hash->{helper}{mower}{attributes}{positions}[ $i ]{latitude}
- && $searchposlon[1] == $hash->{helper}{mower}{attributes}{positions}[ $i+1 ]{longitude}
- && $searchposlat[1] == $hash->{helper}{mower}{attributes}{positions}[ $i+1 ]{latitude} ) {
- # || $i == $poslen-2 ) { # not nessecary due to new alignment
- # $i++ if ( $i == $poslen-2 ); # not nessecary due to new alignment
- # timediff per step
- my $dt = 0;
- $dt = int(($hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{$arrayName}[0]{statusTimestamp})/$i) if ($i);
- for ($k=$i-1;$k>-1;$k--) {
-
- unshift (@{$hash->{helper}{$arrayName}}, dclone($hash->{helper}{mower}{attributes}{positions}[ $k ]) );
- pop (@{$hash->{helper}{$arrayName}}) if (@{$hash->{helper}{$arrayName}} > $maxLength);
- $hash->{helper}{$arrayName}[0]{statusTimestamp} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $dt * $k;
-
- unshift (@{$hash->{helper}{searchpos}}, dclone($hash->{helper}{mower}{attributes}{positions}[ $k ]) );
- pop (@{$hash->{helper}{searchpos}}) if (@{$hash->{helper}{searchpos}} > $searchlen);
-
- my @temp = ();
- unshift ( @temp, dclone( $hash->{helper}{mower}{attributes}{positions}[ $k ] ) );
- posMinMax( $hash, \@temp );
-
- }
-
- #callFn if present
- if ($hash->{helper}{$activity}{callFn}) {
-
- $hash->{helper}{$activity}{cnt} = $i;
- no strict "refs";
- &{$hash->{helper}{$activity}{callFn}}($hash);
- use strict "refs";
-
- }
-
- $hash->{helper}{newdatasets} = $i;
- readingsSingleUpdate($hash, "statistics_newGeoDataSets", $i, 1);
- last;
-
- }
- }
- }
-}
-
-#########################
-sub ChargingStationPosition {
- my ($hash) = @_;
- my $n = @{$hash->{helper}{cspos}};
- my $xm = 0;
- map { $xm += $_->{longitude} } @{$hash->{helper}{cspos}};
- $xm = $xm/$n;
- my $ym = 0;
- map { $ym += $_->{latitude} } @{$hash->{helper}{cspos}};
- $ym = $ym/$n;
- $hash->{helper}{chargingStation}{longitude} = sprintf("%.8f",$xm);
- $hash->{helper}{chargingStation}{latitude} = sprintf("%.8f",$ym);
- return undef;
-}
-
-#########################
-sub AreaStatistics {
- my ($hash) = @_;
- my $name = $hash->{NAME};
- my $activity = 'MOWING';
- my $i = $hash->{helper}{MOWING}{cnt};
- my $k = 0;
- my @xyarr = @{$hash->{helper}{areapos}};# areapos
- my $n = @xyarr;
-
- AttrVal($name,'scaleToMeterXY', $hash->{helper}{scaleToMeterLongitude} . ' ' .$hash->{helper}{scaleToMeterLatitude}) =~ /(\d+)\s+(\d+)/;
- my ($sclon, $sclat) = ($1, $2);
-
- my $lsum = 0;
- my $asum = 0;
- my $vm = 0;
-
- for ( $k = 0; $k <= $i-1; $k++) {
- $lsum += ((($xyarr[ $k ]{longitude} - $xyarr[ $k+1 ]{longitude}) * $sclon)**2 + (($xyarr[ $k ]{latitude} - $xyarr[ $k+1 ]{latitude}) * $sclat)**2)**0.5; # m
- }
- $asum = $lsum * AttrVal($name,'mowerCuttingWidth',0.24); # qm
- my $td = $xyarr[ 0 ]{storedTimestamp} - $xyarr[ $k ]{storedTimestamp};
- $vm = sprintf( '%.6f', $lsum / $td ) * 1000 if ($td); # m/s
- $hash->{helper}{$activity}{speed} = $vm;
- $hash->{helper}{$activity}{track} = $lsum;
- $hash->{helper}{$activity}{area} = $asum;
- $hash->{helper}{statistics}{currentSpeed} = $vm;
- $hash->{helper}{statistics}{currentDayTrack} += $lsum;
- $hash->{helper}{statistics}{currentDayArea} += $asum;
- $hash->{helper}{statistics}{currentSpeed} = $vm;
- return undef;
-}
-
-#########################
-sub AddExtension {
- my ( $name, $func, $link ) = @_;
- my $hash = $defs{$name};
- my $type = $hash->{TYPE};
-
- my $url = "/$link";
- Log3( $name, 2, "Registering $type $name for URL $url..." );
- $::data{FWEXT}{$url}{deviceName} = $name;
- $::data{FWEXT}{$url}{FUNC} = $func;
- $::data{FWEXT}{$url}{LINK} = $link;
-
- return;
-}
-
-#########################
-sub RemoveExtension {
- my ($link) = @_;
- my $url = "/$link";
- my $name = $::data{FWEXT}{$url}{deviceName};
- my $hash = $defs{$name};
- my $type = $hash->{TYPE};
-
- Log3( $name, 2, "Unregistering $type $name for URL $url..." );
- delete $::data{FWEXT}{$url};
-
- return;
-}
-
-#########################
-sub GetMap() {
- my ($request) = @_;
-
- if ( $request =~ /^\/AutomowerConnectDevice\/(\w+)\/map/ ) {
- my $name = $1;
- my $hash = $::defs{$name};
- return ( "text/plain; charset=utf-8","AutomowerConnectDevice: No MAP_MIME for webhook $request" ) if ( !defined $hash->{helper}{MAP_MIME} || !$hash->{helper}{MAP_MIME} );
- return ( "text/plain; charset=utf-8","AutomowerConnectDevice: No MAP_CACHE for webhook $request" ) if ( !defined $hash->{helper}{MAP_CACHE} || !$hash->{helper}{MAP_CACHE} );
- my $mapMime = $hash->{helper}{MAP_MIME};
- my $mapData = $hash->{helper}{MAP_CACHE};
- return ( $mapMime, $mapData );
- }
- return ( "text/plain; charset=utf-8","No AutomowerConnectDevice device for webhook $request" );
-
-}
-
-#########################
-sub readMap {
- my ($hash) = @_;
- my $name = $hash->{NAME};
- my $type = $hash->{TYPE};
- my $iam = "$type $name readMap:";
- RemoveInternalTimer( $hash, \&readMap );
- my $filename = $hash->{helper}{MAP_PATH};
-
- if ( $filename and -e $filename ) {
- open my $fh, '<:raw', $filename or die $!;
- my $content = '';
- while (1) {
- my $success = read $fh, $content, 1024, length($content);
- die $! if not defined $success;
- last if not $success;
- }
- close $fh;
- $hash->{helper}{MAP_CACHE} = $content;
- Log3 $name, 5, "$iam file \"$filename\" content length: ".length($content);
- } else {
- Log3 $name, 2, "$iam file \"$filename\" does not exist.";
- }
-}
-
-#########################
-sub posMinMax {
- my ($hash, $poshash) = @_;
- my $minLon = $hash->{helper}{minLon};
- my $maxLon = $hash->{helper}{maxLon};
- my $minLat = $hash->{helper}{minLat};
- my $maxLat = $hash->{helper}{maxLat};
-
- for ( @{$poshash} ) {
- $minLon = minNum( $minLon,$_->{longitude} );
- $maxLon = maxNum( $maxLon,$_->{longitude} );
- $minLat = minNum( $minLat,$_->{latitude} );
- $maxLat = maxNum( $maxLat,$_->{latitude} );
- }
-
- $hash->{helper}{minLon} = $minLon;
- $hash->{helper}{maxLon} = $maxLon;
- $hash->{helper}{minLat} = $minLat;
- $hash->{helper}{maxLat} = $maxLat;
- $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);
-
- return undef;
-}
-
-#########################
-sub listStatisticsData {
- my ( $hash ) = @_;
- my $name = $hash->{NAME};
- my $cnt = 0;
- my $ret = '';
- $ret .= '';
- $ret .= 'Statistics Data';
-
- $ret .= '';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} | ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} | ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} . ' | s |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} . ' | s |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} . '1 | s |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} . ' | s |
';
-
- $cnt++;$ret .= ' $hash->{helper}{statistics}{currentSpeed} | ' . $hash->{helper}{statistics}{currentSpeed} . ' | m/s |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{currentDayTrack} | ' . $hash->{helper}{statistics}{currentDayTrack} . ' | m |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{currentDayArea} | ' . $hash->{helper}{statistics}{currentDayArea} . ' | qm |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{lastDayTrack} | ' . $hash->{helper}{statistics}{lastDayTrack} . ' | m |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{lastDayArea} | ' . $hash->{helper}{statistics}{lastDayArea} . ' | qm |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{currentWeekTrack} | ' . $hash->{helper}{statistics}{currentWeekTrack} . ' | m |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{currentWeekArea} | ' . $hash->{helper}{statistics}{currentWeekArea} . ' | qm |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{lastWeekTrack} | ' . $hash->{helper}{statistics}{lastWeekTrack} . ' | m |
';
- $cnt++;$ret .= ' $hash->{helper}{statistics}{lastWeekArea} | ' . $hash->{helper}{statistics}{lastWeekArea} . ' | qm |
';
-
- $ret .= '
';
- $ret .= '1 totalRunningTime = totalCuttingTime + totalSearchingTime';
- $ret .= '';
-
- return $ret;
-}
-
-#########################
-sub listMowerData {
- my ( $hash ) = @_;
- my $name = $hash->{NAME};
- my $cnt = 0;
- my $ret = '';
- $ret .= '
';
- $ret .= 'Mower Data';
-
- $ret .= '';
- $cnt++;$ret .= ' $hash->{helper}{mower}{type} | ' . $hash->{helper}{mower}{type} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{id} | ' . $hash->{helper}{mower}{id} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{system}{name} | ' . $hash->{helper}{mower}{attributes}{system}{name} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{system}{model} | ' . $hash->{helper}{mower}{attributes}{system}{model} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{system}{serialNumber} | ' . $hash->{helper}{mower}{attributes}{system}{serialNumber} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{battery}{batteryPercent} | ' . $hash->{helper}{mower}{attributes}{battery}{batteryPercent} . ' | % |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{mode} | ' . $hash->{helper}{mower}{attributes}{mower}{mode} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{activity} | ' . $hash->{helper}{mower}{attributes}{mower}{activity} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{state} | ' . $hash->{helper}{mower}{attributes}{mower}{state} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{errorCode} | ' . $hash->{helper}{mower}{attributes}{mower}{errorCode} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} | ' . $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} . ' | ms |
';
-
- my $calendarjson = eval { JSON::XS->new->pretty(1)->encode ($hash->{helper}{mower}{attributes}{calendar}{tasks}) };
-
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{calendar}{tasks} | ' . ($@ ? $@ : $calendarjson) . ' |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{planner}{nextStartTimestamp} | ' . $hash->{helper}{mower}{attributes}{planner}{nextStartTimestamp} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{planner}{override}{action} | ' . $hash->{helper}{mower}{attributes}{planner}{override}{action} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{planner}{restrictedReason} | ' . $hash->{helper}{mower}{attributes}{planner}{restrictedReason} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{metadata}{connected} | ' . $hash->{helper}{mower}{attributes}{metadata}{connected} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} | ' . $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} . ' | ms |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{positions}[0]{longitude} | ' . $hash->{helper}{mower}{attributes}{positions}[0]{longitude} . ' | decimal degree |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{positions}[0]{latitude} | ' . $hash->{helper}{mower}{attributes}{positions}[0]{latitude} . ' | decimal degree |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} | ' . $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{settings}{headlight}{mode} | ' . $hash->{helper}{mower}{attributes}{settings}{headlight}{mode} . ' | |
';
-# $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{cuttingBladeUsageTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{cuttingBladeUsageTime} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} | ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} | ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} . ' | |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} . ' | s |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} . ' | s |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} . '1 | s |
';
- $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} . ' | s |
';
-
- $ret .= '
';
- $ret .= '1 totalRunningTime = totalCuttingTime + totalSearchingTime';
- $ret .= '';
-
- return $ret;
-}
-
-#########################
-sub listInternalData {
- my ( $hash ) = @_;
- my $name = $hash->{NAME};
- my $rowCount = 1;
- my $ret = '
';
- $ret .= 'Calculated Coordinates For Automatic Registration';
-
- $hash->{helper}{posMinMax} =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)(\R|-?\s)(\d*\.?\d+)\s(-?\d*\.?\d+)/;
-
- my $xm = $hash->{helper}{chargingStation}{longitude} // 0;
- my $ym = $hash->{helper}{chargingStation}{latitude} // 0;
- my $csnr = scalar @{$hash->{helper}{cspos}};
- my $csnrmax = $hash->{helper}{PARKED_IN_CS}{maxLength};
- my $arnr = 0;
- $arnr = scalar @{$hash->{helper}{areapos}} if( scalar @{$hash->{helper}{areapos}} > 2 );
- my $arnrmax = $hash->{helper}{MOWING}{maxLength};
-
- $ret .= '';
- $ret .= ' ' . ($csnr + $arnr) . ' ( ' . ($csnrmax + $arnrmax) . ' ) | Upper Left | ' . $1 . ' | ' . $2 . ' |
';
- $ret .= ' Lower Right | ' . $4 . ' | ' . $5 . ' |
';
-
- $ret .= '
';
- $ret .= '
';
- $ret .= 'Calculated Charging Station Coordinates';
-
- $ret .= '';
- $ret .= ' ' . $csnr . ' ( ' . $csnrmax . ' ) | ' . $xm . ' | ' . $ym . ' |
';
-
- $ret .= '
';
- $ret .= '
';
- $ret .= 'Way Point Stacks';
-
- $ret .= '';
- $ret .= 'PARKED_IN_CS, CHARGING | cspos | ' . $csnr . ' | ' . $csnrmax . ' |
';
- $ret .= 'MOWING | areapos | ' . $arnr . ' | ' . $arnrmax . ' |
';
-
- $ret .= '
';
- if ( $hash->{TYPE} eq 'AutomowerConnect' ) {
-
- $ret .= '';
- $ret .= 'Access Token ( expires: ' . ReadingsVal( $name, 'api_token_expires', 'none') . ' ) ';
-
- $ret .= '' . ReadingsVal( $name, '.access_token', 'none') . ' |
';
-
- $ret .= '
';
-
- }
- $ret .= '';
-
- return $ret;
-}
-
-#########################
-sub listErrorCodes {
- my ( $hash ) = @_;
- my $rowCount = 1;
- my %ec = ();
- my $ec = \%ec;
- for ( keys %{$hash->{helper}{errortable}} ) {
- $ec->{sprintf("%03d",$_)} = $hash->{helper}{errortable}{$_} ;
- }
- my $ret = '';
- $ret .= 'API-Response Status Codes';
- $ret .= '200, 201, 202 204 | response o.k. |
';
- $ret .= '400, 401, 402 403, 404, 415 500, 503 | error, detailed information see logfile |
';
- $ret .= '
';
- $ret .= 'Mower Error Table';
- for (sort keys %{$ec}) {
- $ret .= '{$_};
- $ret .= '
';
- $rowCount++;
- }
- $ret .= '
';
-
- return $ret;
-}
-
##############################################################
1;
-
+__END__
=pod
=item device
@@ -1501,7 +955,7 @@ sub listErrorCodes {
mower_mode - current working mode "MAIN_AREA" | "SECONDARY_AREA" | "HOME" | "DEMO" | "UNKNOWN"
mower_state - current status "UNKNOWN" | "NOT_APPLICABLE" | "PAUSED" | "IN_OPERATION" | "WAIT_UPDATING" | "WAIT_POWER_UP" | "RESTRICTED" | "OFF" | "STOPPED" | "ERROR" | "FATAL_ERROR" |"ERROR_AT_POWER_UP"
planner_nextStart - next start time
- planner_restrictedReason - reason for parking NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST
+ planner_restrictedReason - reason for parking NOT_APPLICABLE, NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST
planner_overrideAction - reason for override a planned action NOT_ACTIVE, FORCE_PARK, FORCE_MOW
state - status of connection FHEM to Husqvarna Cloud API and device state (e.g. defined, connected, error)
status_statusTimestampOld - local time of second last change of the API content
@@ -1509,7 +963,7 @@ sub listErrorCodes {
settings_headlight - actual headlight mode from API
statistics_newGeoDataSets - number of new data sets between the last two different time stamps
statistics_numberOfCollisions - number of collisions
- status_connected - state of connetion between mower and Husqvarna Cloud, (1 => true, 0 => false)
+ status_connected - state of connetion between mower and Husqvarna Cloud, (1 => CONNECTED, 0 => OFFLINE)
status_statusTimestamp - local time of last change of the API content
status_statusTimestampDiff - time difference in seconds between the last and second last change of the API content
status_statusTimestampOld - local time of second last change of the API content
@@ -1740,14 +1194,14 @@ sub listErrorCodes {
mower_mode - aktueller Arbeitsmodus "MAIN_AREA" | "SECONDARY_AREA" | "HOME" | "DEMO" | "UNKNOWN"
mower_state - aktueller Status "UNKNOWN" | "NOT_APPLICABLE" | "PAUSED" | "IN_OPERATION" | "WAIT_UPDATING" | "WAIT_POWER_UP" | "RESTRICTED" | "OFF" | "STOPPED" | "ERROR" | "FATAL_ERROR" |"ERROR_AT_POWER_UP"
planner_nextStart - nächste Startzeit
- planner_restrictedReason - Grund für Parken NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST
+ planner_restrictedReason - Grund für Parken NOT_APPLICABLE, NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST
planner_overrideAction - Grund für vorrangige Aktion NOT_ACTIVE, FORCE_PARK, FORCE_MOW
state - Status der Verbindung des FHEM-Gerätes zur Husqvarna Cloud API (defined, connected, error).
settings_cuttingHeight - aktuelle Schnitthöhe aus der API
settings_headlight - aktueller Scheinwerfermode aus der API
statistics_newGeoDataSets - Anzahl der neuen Datensätze zwischen den letzten zwei unterschiedlichen Zeitstempeln
statistics_numberOfCollisions - Anzahl der Kollisionen
- status_connected - Status der Verbindung zwischen dem Automower und der Husqvarna Cloud, (1 => true, 0 => false)
+ status_connected - Status der Verbindung zwischen dem Automower und der Husqvarna Cloud, (1 => CONNECTED, 0 => OFFLINE)
status_statusTimestamp - Lokalzeit der letzten Änderung der Daten in der API
status_statusTimestampDiff - Zeitdifferenz zwichen den beiden letzten Änderungen im Inhalt der Daten aus der API
status_statusTimestampOld - Lokalzeit der vorletzten Änderung der Daten in der API
diff --git a/fhem/lib/FHEM/Devices/AMConnect/Common.pm b/fhem/lib/FHEM/Devices/AMConnect/Common.pm
new file mode 100644
index 000000000..ff7e5bd66
--- /dev/null
+++ b/fhem/lib/FHEM/Devices/AMConnect/Common.pm
@@ -0,0 +1,729 @@
+###############################################################################
+#
+# $Id$
+#
+# This script is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# any later version.
+#
+# The GNU General Public License can be found at
+# http://www.gnu.org/copyleft/gpl.html.
+# A copy is found in the textfile GPL.txt and important notices to the license
+# from the author is found in LICENSE.txt distributed with these scripts.
+#
+# This script is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+#
+#
+# Husqvarnas Open API is used
+# based on some ideas from HusqvarnaAutomower and BOTVAC module
+#
+################################################################################
+
+package FHEM::Devices::AMConnect::Common;
+use strict;
+use warnings;
+use POSIX;
+
+# wird für den Import der FHEM Funktionen aus der fhem.pl benötigt
+use GPUtils qw(:all);
+
+use Time::HiRes qw(gettimeofday);
+use Blocking;
+use Storable qw(dclone retrieve store);
+
+# Import der FHEM Funktionen
+BEGIN {
+ GP_Import(
+ qw(
+ AttrVal
+ CommandAttr
+ FmtDateTime
+ getKeyValue
+ InternalTimer
+ InternalVal
+ IsDisabled
+ Log3
+ Log
+ minNum
+ maxNum
+ readingFnAttributes
+ readingsBeginUpdate
+ readingsBulkUpdate
+ readingsBulkUpdateIfChanged
+ readingsDelete
+ readingsEndUpdate
+ ReadingsNum
+ readingsSingleUpdate
+ ReadingsVal
+ RemoveInternalTimer
+ setKeyValue
+ defs
+ attr
+ modules
+ devspec2array
+ )
+ );
+}
+
+my $missingModul = "";
+
+eval "use JSON;1" or $missingModul .= "JSON ";
+
+my $errorjson = '{"23":"Wheel drive problem, left","24":"Cutting system blocked","123":"Destination not reachable","710":"SIM card locked","50":"Guide 1 not found","717":"SMS could not be sent","108":"Folding cutting deck sensor defect","4":"Loop sensor problem, front","15":"Lifted","29":"Slope too steep","1":"Outside working area","45":"Cutting height problem, dir","52":"Guide 3 not found","28":"Memory circuit problem","95":"Folding sensor activated","9":"Trapped","114":"Too high discharge current","103":"Cutting drive motor 2 defect","65":"Temporary battery problem","119":"Zone generator problem","6":"Loop sensor problem, left","82":"Wheel motor blocked, rear right","714":"Geofence problem","703":"Connectivity problem","708":"SIM card locked","75":"Connection changed","7":"Loop sensor problem, right","35":"Wheel motor overloaded, right","3":"Wrong loop signal","117":"High internal power loss","0":"Unexpected error","80":"Cutting system imbalance - Warning","110":"Collision sensor error","100":"Ultrasonic Sensor 3 defect","79":"Invalid battery combination - Invalid combination of different battery types.","724":"Communication circuit board SW must be updated","86":"Wheel motor overloaded, rear right","81":"Safety function faulty","78":"Slipped - Mower has Slipped.Situation not solved with moving pattern","107":"Docking sensor defect","33":"Mower tilted","69":"Alarm! Mower switched off","68":"Temporary battery problem","34":"Cutting stopped - slope too steep","127":"Battery problem","73":"Alarm! Mower in motion","74":"Alarm! Outside geofence","713":"Geofence problem","87":"Wheel motor overloaded, rear left","120":"Internal voltage error","39":"Cutting motor problem","704":"Connectivity problem","63":"Temporary battery problem","109":"Loop sensor defect","38":"Electronic problem","64":"Temporary battery problem","113":"Complex working area","93":"No accurate position from satellites","104":"Cutting drive motor 3 defect","709":"SIM card not found","94":"Reference station communication problem","43":"Cutting height problem, drive","13":"No drive","44":"Cutting height problem, curr","118":"Charging system problem","14":"Mower lifted","57":"Guide calibration failed","707":"SIM card requires PIN","99":"Ultrasonic Sensor 2 defect","98":"Ultrasonic Sensor 1 defect","51":"Guide 2 not found","56":"Guide calibration accomplished","49":"Ultrasonic problem","2":"No loop signal","124":"Destination blocked","25":"Cutting system blocked","19":"Collision sensor problem, front","18":"Collision sensor problem, rear","48":"No response from charger","105":"Lift Sensor defect","111":"No confirmed position","10":"Upside down","40":"Limited cutting height range","716":"Connectivity problem","27":"Settings restored","90":"No power in charging station","21":"Wheel motor blocked, left","26":"Invalid sub-device combination","92":"Work area not valid","702":"Connectivity settings restored","125":"Battery needs replacement","5":"Loop sensor problem, rear","12":"Empty battery","55":"Difficult finding home","42":"Limited cutting height range","30":"Charging system problem","72":"Alarm! Mower tilted","85":"Wheel drive problem, rear left","8":"Wrong PIN code","62":"Temporary battery problem","102":"Cutting drive motor 1 defect","116":"High charging power loss","122":"CAN error","60":"Temporary battery problem","705":"Connectivity problem","711":"SIM card locked","70":"Alarm! Mower stopped","32":"Tilt sensor problem","37":"Charging current too high","89":"Invalid system configuration","76":"Connection NOT changed","71":"Alarm! Mower lifted","88":"Angular sensor problem","701":"Connectivity problem","715":"Connectivity problem","61":"Temporary battery problem","66":"Battery problem","106":"Collision sensor defect","67":"Battery problem","112":"Cutting system major imbalance","83":"Wheel motor blocked, rear left","84":"Wheel drive problem, rear right","126":"Battery near end of life","77":"Com board not available","36":"Wheel motor overloaded, left","31":"STOP button problem","17":"Charging station blocked","54":"Weak GPS signal","47":"Cutting height problem","53":"GPS navigation problem","121":"High internal temerature","97":"Left brush motor overloaded","712":"SIM card locked","20":"Wheel motor blocked, right","91":"Switch cord problem","96":"Right brush motor overloaded","58":"Temporary battery problem","59":"Temporary battery problem","22":"Wheel drive problem, right","706":"Poor signal quality","41":"Unexpected cutting height adj","46":"Cutting height blocked","11":"Low battery","16":"Stuck in charging station","101":"Ultrasonic Sensor 4 defect","115":"Too high internal current"}';
+
+our $errortable = eval { decode_json ( $errorjson ) };
+if ($@) {
+ return "FHEM::Devices::AMConnect::Common \$errortable: $@";
+}
+$errorjson = undef;
+
+#########################
+sub Undefine {
+ my ( $hash, $arg ) = @_;
+ my $name = $hash->{NAME};
+ my $type = $hash->{TYPE};
+
+ RemoveInternalTimer($hash);
+ ::FHEM::Devices::AMConnect::Common::RemoveExtension("$type/$name/map");
+ return undef;
+}
+
+##########################
+sub Delete {
+ my ( $hash, $arg ) = @_;
+ my $name = $hash->{NAME};
+ my $type = $hash->{TYPE};
+ my $iam ="$type $name Delete: ";
+ Log3( $name, 5, "$iam called" );
+
+ my ($passResp,$passErr) = $hash->{helper}->{passObj}->setDeletePassword($name);
+ Log3( $name, 1, "$iam error: $passErr" ) if ($passErr);
+
+ return;
+}
+
+##########################
+sub Rename {
+ my ( $newname, $oldname ) = @_;
+ my $hash = $defs{$newname};
+
+ my ( $passResp, $passErr ) = $hash->{helper}->{passObj}->setRename( $newname, $oldname );
+ Log3 $newname, 2, "$newname password rename error: $passErr" if ($passErr);
+
+ return undef;
+}
+
+#########################
+
+sub Get {
+ my ($hash,@val) = @_;
+ my $type = $hash->{TYPE};
+
+ return "$type $hash->{NAME} Get: needs at least one argument" if ( @val < 2 );
+
+ my ($name,$setName,$setVal,$setVal2,$setVal3) = @val;
+ my $iam = "$type $name Get:";
+
+ Log3 $name, 4, "$iam called with $setName " . ($setVal ? $setVal : "");
+
+ if ( $setName eq 'html' ) {
+
+ my $ret = '' . ::FHEM::Devices::AMConnect::Common::FW_detailFn( undef, $name, undef, undef) . '';
+ return $ret;
+
+ } elsif ( $setName eq 'errorCodes' ) {
+
+ my $ret = ::FHEM::Devices::AMConnect::Common::listErrorCodes();
+ return $ret;
+
+ } elsif ( $setName eq 'InternalData' ) {
+
+ my $ret = ::FHEM::Devices::AMConnect::Common::listInternalData($hash);
+ return $ret;
+
+ } elsif ( $setName eq 'MowerData' ) {
+
+ my $ret = ::FHEM::Devices::AMConnect::Common::listMowerData($hash);
+ return $ret;
+
+ } elsif ( $setName eq 'StatisticsData' ) {
+
+ my $ret = ::FHEM::Devices::AMConnect::Common::listStatisticsData($hash);
+ return $ret;
+
+ } else {
+
+ return "Unknown argument $setName, choose one of StatisticsData:noArg MowerData:noArg InternalData:noArg errorCodes:noArg ";
+
+ }
+}
+
+#########################
+sub FW_detailFn {
+ my ($FW_wname, $name, $room, $pageHash) = @_; # pageHash is set for summaryFn.
+ my $hash = $defs{$name};
+ my $type = $hash->{TYPE};
+ return '' if( AttrVal($name, 'disable', 0) || !AttrVal($name, 'showMap', 1) );
+ if ( $hash->{helper} && $hash->{helper}{mower} && $hash->{helper}{mower}{attributes} && $hash->{helper}{mower}{attributes}{positions} && @{$hash->{helper}{mower}{attributes}{positions}} > 0 ) {
+ my $img = "./fhem/$type/$name/map";
+ my $zoom=AttrVal( $name,"mapImageZoom", 0.7 );
+
+ my ($picx,$picy) = AttrVal( $name,"mapImageWidthHeight", $hash->{helper}{imageWidthHeight} ) =~ /(\d+)\s(\d+)/;
+
+ $picx=int($picx*$zoom);
+ $picy=int($picy*$zoom);
+ my $ret = "";
+ $ret .= "";
+ $ret .= "";
+ $ret .= "";
+ $ret .= "
";
+
+ InternalTimer( gettimeofday() + 2.0, \&FW_detailFn_Update, $hash, 0 );
+
+ return $ret;
+ }
+ return '';
+}
+
+#########################
+sub FW_detailFn_Update {
+ my ($hash) = @_;
+ my $name = $hash->{NAME};
+ my $type = $hash->{TYPE};
+ if ( $hash->{helper} && $hash->{helper}{mower} && $hash->{helper}{mower}{attributes} && $hash->{helper}{mower}{attributes}{positions} && @{$hash->{helper}{mower}{attributes}{positions}} > 0 ) {
+
+ my @pos = @{$hash->{helper}{areapos}}; # operational mode
+ my @posc = @{$hash->{helper}{cspos}}; # maybe operational mode
+ my $img = "./fhem/$type/$name/map";
+
+ my ( $lonlo, $latlo, $dummy, $lonru, $latru ) = AttrVal( $name,"mapImageCoordinatesToRegister",$hash->{helper}{posMinMax} ) =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)(\R|\s)(-?\d*\.?\d+)\s(-?\d*\.?\d+)/;
+
+ my $zoom = AttrVal( $name,"mapImageZoom", 0.7 );
+
+ my ($picx,$picy) = AttrVal( $name,"mapImageWidthHeight", $hash->{helper}{imageWidthHeight} ) =~ /(\d+)\s(\d+)/;
+
+ AttrVal($name,'scaleToMeterXY', $hash->{helper}{scaleToMeterLongitude} . ' ' .$hash->{helper}{scaleToMeterLatitude}) =~ /(-?\d+)\s+(-?\d+)/;
+ my $scalx = ($lonru-$lonlo) * $1;
+
+ $picx = int($picx*$zoom);
+ $picy = int($picy*$zoom);
+ my $mapx = $lonlo-$lonru;
+ my $mapy = $latlo-$latru;
+
+ if ( ($hash->{helper}{PARKED_IN_CS}{callFn} || $hash->{helper}{CHARGING}{callFn}) && (!$hash->{helper}{chargingStation}{longitude} || !$hash->{helper}{chargingStation}{latitude}) ) {
+ no strict "refs";
+ &{$hash->{helper}{PARKED_IN_CS}{callFn}}($hash);
+ use strict "refs";
+ }
+
+ my $csimgpos = AttrVal( $name,"chargingStationImagePosition","right" );
+ my $xm = $hash->{helper}{chargingStation}{longitude} // 10.1165;
+ my $ym = $hash->{helper}{chargingStation}{latitude} // 51.28;
+
+ my ($cslo,$csla) = AttrVal( $name,"chargingStationCoordinates","$xm $ym" ) =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)/;
+
+ my $cslon = int(($lonlo-$cslo) * $picx / $mapx);
+ my $cslat = int(($latlo-$csla) * $picy / $mapy);
+
+ # MOWING PATH
+ my $posxy = int($lonlo * $picx / $mapx).",".int($latlo * $picy / $mapy);
+ if ( @pos > 1 ) {
+
+ $posxy = int(($lonlo-$pos[0]{longitude}) * $picx / $mapx).",".int(($latlo-$pos[0]{latitude}) * $picy / $mapy);
+ for (my $i=1;$i<@pos;$i++){
+ $posxy .= ",".int(($lonlo-$pos[$i]{longitude}) * $picx / $mapx).",".int(($latlo-$pos[$i]{latitude}) * $picy / $mapy);
+ }
+
+ }
+
+ # CHARGING STATION PATH
+ my $poscxy = int( $lonlo * $picx / $mapx ).",".int( $latlo * $picy / $mapy );
+ if ( @posc > 1 ) {
+
+ $poscxy = int( ( $lonlo-$posc[0]{longitude} ) * $picx / $mapx ).",".int( ( $latlo-$posc[0]{latitude} ) * $picy / $mapy );
+ for (my $i=1;$i<@posc;$i++){
+ $poscxy .= ",".int(($lonlo-$posc[$i]{longitude}) * $picx / $mapx).",".int(($latlo-$posc[$i]{latitude}) * $picy / $mapy);
+ }
+
+ }
+
+ # AREA LIMITS
+ my $arealimits = AttrVal($name,'mowingAreaLimits','');
+ my $limi = '';
+ if ($arealimits) {
+ my @lixy = (split(/\s|,|\R$/,$arealimits));
+ $limi = int(($lonlo-$lixy[0]) * $picx / $mapx).",".int(($latlo-$lixy[1]) * $picy / $mapy);
+ for (my $i=2;$i<@lixy;$i+=2){
+ $limi .= ",".int(($lonlo-$lixy[$i]) * $picx / $mapx).",".int(($latlo-$lixy[$i+1]) * $picy / $mapy);
+ }
+ }
+
+ # PROPERTY LIMITS
+ my $propertylimits = AttrVal($name,'propertyLimits','');
+ my $propli = '';
+ if ($propertylimits) {
+ my @propxy = (split(/\s|,|\R$/,$propertylimits));
+ $propli = int(($lonlo-$propxy[0]) * $picx / $mapx).",".int(($latlo-$propxy[1]) * $picy / $mapy);
+ for (my $i=2;$i<@propxy;$i+=2){
+ $propli .= ",".int(($lonlo-$propxy[$i]) * $picx / $mapx).",".int(($latlo-$propxy[$i+1]) * $picy / $mapy);
+ }
+ }
+
+ map {
+ ::FW_directNotify("#FHEMWEB:$_", "AutomowerConnectUpdateDetail ( '$name', '$type', '$img', $picx, $picy, $cslon, $cslat, '$csimgpos', $scalx, [ $posxy ], [ $limi ], [ $propli ], [ $poscxy ] )","");
+ } devspec2array("TYPE=FHEMWEB");
+ }
+ return undef;
+}
+
+#########################
+sub AlignArray {
+ my ($hash) = @_;
+ my $name = $hash->{NAME};
+ my $activity = $hash->{helper}{mower}{attributes}{mower}{activity};
+ my $arrayName = $hash->{helper}{$activity}{arrayName};
+ my $searchlen = 2;
+ my $i = 0;
+ my @temp = ();
+
+ if ( isGoodActivity( $hash ) ) {
+
+ my $k = -1;
+ my $poslen = @{$hash->{helper}{mower}{attributes}{positions}};
+ my @searchposlon = ($hash->{helper}{searchpos}[0]{longitude}, $hash->{helper}{searchpos}[1]{longitude});
+ my @searchposlat = ($hash->{helper}{searchpos}[0]{latitude}, $hash->{helper}{searchpos}[1]{latitude});
+ my $maxLength = $hash->{helper}{$activity}{maxLength};
+ for ( $i = 0; $i < $poslen-3; $i++ ) { # 3 due to 2 alignment data sets at the end
+ if ( $searchposlon[ 0 ] == $hash->{helper}{mower}{attributes}{positions}[ $i ]{longitude}
+ && $searchposlat[ 0 ] == $hash->{helper}{mower}{attributes}{positions}[ $i ]{latitude}
+ && $searchposlon[ 1 ] == $hash->{helper}{mower}{attributes}{positions}[ $i+1 ]{longitude}
+ && $searchposlat[ 1 ] == $hash->{helper}{mower}{attributes}{positions}[ $i+1 ]{latitude} ) {
+ # timediff per step
+ my $dt = 0;
+ $dt = int(($hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{$arrayName}[0]{statusTimestamp})/$i) if ( $i && @{ $hash->{helper}{$arrayName} } );
+ for ($k=$i-1;$k>-1;$k--) {
+
+ }
+ for ($k=$i-1;$k>-1;$k--) {
+
+ if ( @{ $hash->{helper}{$arrayName} } ) {
+
+ unshift (@{$hash->{helper}{$arrayName}}, dclone( $hash->{helper}{mower}{attributes}{positions}[ $k ] ) );
+
+ } else {
+
+ $hash->{helper}{$arrayName}[ 0 ] = dclone( $hash->{helper}{mower}{attributes}{positions}[ $k ] );
+
+ }
+
+ pop (@{$hash->{helper}{$arrayName}}) if (@{$hash->{helper}{$arrayName}} > $maxLength);
+ $hash->{helper}{$arrayName}[ 0 ]{statusTimestamp} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $dt * $k;
+
+ push ( @temp, dclone( $hash->{helper}{mower}{attributes}{positions}[ $k ] ) );
+
+ }
+
+ posMinMax( $hash, \@temp );
+ #callFn if present
+ if ( $hash->{helper}{$activity}{callFn} && @{$hash->{helper}{$arrayName}} > 1) {
+
+ $hash->{helper}{$activity}{cnt} = $i;
+ no strict "refs";
+ &{$hash->{helper}{$activity}{callFn}}($hash);
+ use strict "refs";
+
+ }
+
+ last;
+
+ }
+
+ }
+
+ }
+
+ $hash->{helper}{newdatasets} = $i;
+ $hash->{helper}{searchpos} = [ dclone( $hash->{helper}{mower}{attributes}{positions}[0] ), dclone( $hash->{helper}{mower}{attributes}{positions}[1] ) ];
+ return undef;
+
+}
+
+#########################
+sub isGoodActivity {
+
+ my ( $hash ) = @_;
+ my $act = $hash->{helper}{mower}{attributes}{mower}{activity};
+ my $actold = $hash->{helper}{mowerold}{attributes}{mower}{activity};
+
+ my $ret = $hash->{helper}{$act}{arrayName} && $act eq $actold
+ || $act =~ /^(CHARGING|PARKED_IN_CS)$/ && $actold =~ /^(PARKED_IN_CS|CHARGING)$/;
+ return $ret;
+
+}
+
+#########################
+sub ChargingStationPosition {
+ my ($hash) = @_;
+ my $n = @{$hash->{helper}{cspos}};
+ if ( $n > 0 ) {
+ my $xm = 0;
+ map { $xm += $_->{longitude} } @{$hash->{helper}{cspos}};
+ $xm = $xm/$n;
+ my $ym = 0;
+ map { $ym += $_->{latitude} } @{$hash->{helper}{cspos}};
+ $ym = $ym/$n;
+ $hash->{helper}{chargingStation}{longitude} = sprintf("%.8f",$xm);
+ $hash->{helper}{chargingStation}{latitude} = sprintf("%.8f",$ym);
+ }
+ return undef;
+}
+
+#########################
+sub AreaStatistics {
+ my ($hash) = @_;
+ my $name = $hash->{NAME};
+ my $activity = 'MOWING';
+ my $i = $hash->{helper}{$activity}{cnt};
+ my $k = 0;
+ my @xyarr = @{$hash->{helper}{areapos}};# areapos
+ my $n = @xyarr;
+ my ($sclon, $sclat) = AttrVal($name,'scaleToMeterXY', $hash->{helper}{scaleToMeterLongitude} . ' ' .$hash->{helper}{scaleToMeterLatitude}) =~ /(-?\d+)\s+(-?\d+)/;
+ my $lsum = 0;
+ my $asum = 0;
+ my $vm = 0;
+
+ for ( $k = 0; $k <= $i-1; $k++) {
+
+ $lsum += ((($xyarr[ $k ]{longitude} - $xyarr[ $k+1 ]{longitude}) * $sclon)**2 + (($xyarr[ $k ]{latitude} - $xyarr[ $k+1 ]{latitude}) * $sclat)**2)**0.5;
+
+ }
+
+ $asum = $lsum * AttrVal($name,'mowerCuttingWidth',0.24);
+ my $td = $xyarr[ 0 ]{storedTimestamp} - $xyarr[ $k ]{storedTimestamp};
+ $vm = sprintf( '%.6f', $lsum / $td ) * 1000 if ($td); # m/s
+ $hash->{helper}{$activity}{speed} = $vm;
+ $hash->{helper}{$activity}{track} = $lsum;
+ $hash->{helper}{$activity}{area} = $asum;
+ $hash->{helper}{statistics}{currentSpeed} = $vm;
+ $hash->{helper}{statistics}{currentDayTrack} += $lsum;
+ $hash->{helper}{statistics}{currentDayArea} += $asum;
+ $hash->{helper}{statistics}{currentSpeed} = $vm;
+
+ return undef;
+}
+
+#########################
+sub AddExtension {
+ my ( $name, $func, $link ) = @_;
+ my $hash = $defs{$name};
+ my $type = $hash->{TYPE};
+
+ my $url = "/$link";
+ Log3( $name, 2, "Registering $type $name for URL $url..." );
+ $::data{FWEXT}{$url}{deviceName} = $name;
+ $::data{FWEXT}{$url}{FUNC} = $func;
+ $::data{FWEXT}{$url}{LINK} = $link;
+
+ return;
+}
+
+#########################
+sub RemoveExtension {
+ my ($link) = @_;
+ my $url = "/$link";
+ my $name = $::data{FWEXT}{$url}{deviceName};
+ my $hash = $defs{$name};
+ my $type = $hash->{TYPE};
+
+ Log3( $name, 2, "Unregistering $type $name for URL $url..." );
+ delete $::data{FWEXT}{$url};
+
+ return;
+}
+
+#########################
+sub GetMap() {
+ my ($request) = @_;
+
+ if ( $request =~ /^\/(AutomowerConnectDevice|AutomowerConnect)\/(\w+)\/map/ ) {
+
+ my $type = $1;
+ my $name = $2;
+ my $hash = $::defs{$name};
+ return ( "text/plain; charset=utf-8","${type} ${name}: No MAP_MIME for webhook $request" ) if ( !defined $hash->{helper}{MAP_MIME} || !$hash->{helper}{MAP_MIME} );
+ return ( "text/plain; charset=utf-8","${type} ${name}: No MAP_CACHE for webhook $request" ) if ( !defined $hash->{helper}{MAP_CACHE} || !$hash->{helper}{MAP_CACHE} );
+ my $mapMime = $hash->{helper}{MAP_MIME};
+ my $mapData = $hash->{helper}{MAP_CACHE};
+ return ( $mapMime, $mapData );
+
+ }
+ return ( "text/plain; charset=utf-8","No AutomowerConnect(Device) device for webhook $request" );
+
+}
+
+#########################
+sub readMap {
+ my ($hash) = @_;
+ my $name = $hash->{NAME};
+ my $type = $hash->{TYPE};
+ my $iam = "$type $name readMap:";
+ RemoveInternalTimer( $hash, \&::FHEM::Devices::AMConnect::Common::readMap );
+ my $filename = $hash->{helper}{MAP_PATH};
+
+ if ( $filename and -e $filename ) {
+ open my $fh, '<:raw', $filename or die $!;
+ my $content = '';
+ while (1) {
+ my $success = read $fh, $content, 1024, length($content);
+ die $! if not defined $success;
+ last if not $success;
+ }
+ close $fh;
+ $hash->{helper}{MAP_CACHE} = $content;
+ Log3 $name, 5, "$iam file \"$filename\" content length: ".length($content);
+ } else {
+ Log3 $name, 2, "$iam file \"$filename\" does not exist.";
+ }
+}
+
+#########################
+sub posMinMax {
+ my ($hash, $poshash) = @_;
+ my $minLon = $hash->{helper}{minLon};
+ my $maxLon = $hash->{helper}{maxLon};
+ my $minLat = $hash->{helper}{minLat};
+ my $maxLat = $hash->{helper}{maxLat};
+
+ for ( @{$poshash} ) {
+ $minLon = minNum( $minLon,$_->{longitude} );
+ $maxLon = maxNum( $maxLon,$_->{longitude} );
+ $minLat = minNum( $minLat,$_->{latitude} );
+ $maxLat = maxNum( $maxLat,$_->{latitude} );
+ }
+
+ $hash->{helper}{minLon} = $minLon;
+ $hash->{helper}{maxLon} = $maxLon;
+ $hash->{helper}{minLat} = $minLat;
+ $hash->{helper}{maxLat} = $maxLat;
+ $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);
+
+ return undef;
+}
+
+#########################
+sub listStatisticsData {
+ my ( $hash ) = @_;
+ if ( $::init_done && $hash->{helper}{statistics} ) {
+
+ my $name = $hash->{NAME};
+ my $cnt = 0;
+ my $ret = '';
+ $ret .= '';
+ $ret .= 'Statistics Data';
+
+ $ret .= '';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} | ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} | ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} . ' | s |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} . ' | s |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} . '1 | s |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} . ' | s |
';
+
+ $cnt++;$ret .= ' $hash->{helper}{statistics}{currentSpeed} | ' . $hash->{helper}{statistics}{currentSpeed} . ' | m/s |
';
+ $cnt++;$ret .= ' $hash->{helper}{statistics}{currentDayTrack} | ' . $hash->{helper}{statistics}{currentDayTrack} . ' | m |
';
+ $cnt++;$ret .= ' $hash->{helper}{statistics}{currentDayArea} | ' . $hash->{helper}{statistics}{currentDayArea} . ' | qm |
';
+ $cnt++;$ret .= ' $hash->{helper}{statistics}{lastDayTrack} | ' . $hash->{helper}{statistics}{lastDayTrack} . ' | m |
';
+ $cnt++;$ret .= ' $hash->{helper}{statistics}{lastDayArea} | ' . $hash->{helper}{statistics}{lastDayArea} . ' | qm |
';
+ $cnt++;$ret .= ' $hash->{helper}{statistics}{currentWeekTrack} | ' . $hash->{helper}{statistics}{currentWeekTrack} . ' | m |
';
+ $cnt++;$ret .= ' $hash->{helper}{statistics}{currentWeekArea} | ' . $hash->{helper}{statistics}{currentWeekArea} . ' | qm |
';
+ $cnt++;$ret .= ' $hash->{helper}{statistics}{lastWeekTrack} | ' . $hash->{helper}{statistics}{lastWeekTrack} . ' | m |
';
+ $cnt++;$ret .= ' $hash->{helper}{statistics}{lastWeekArea} | ' . $hash->{helper}{statistics}{lastWeekArea} . ' | qm |
';
+
+ $ret .= '
';
+ $ret .= '1 totalRunningTime = totalCuttingTime + totalSearchingTime';
+ $ret .= '';
+
+ return $ret;
+
+ } else {
+
+ return '
error codes are not yet available |
';
+
+ }
+}
+
+#########################
+sub listMowerData {
+ my ( $hash ) = @_;
+ my $name = $hash->{NAME};
+ my $cnt = 0;
+ my $ret = '';
+ if ( $::init_done && defined( $hash->{helper}{mower}{type} ) ) {
+
+ $ret .= '';
+ $ret .= 'Mower Data';
+
+ $ret .= '';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{type} | ' . $hash->{helper}{mower}{type} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{id} | ' . $hash->{helper}{mower}{id} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{system}{name} | ' . $hash->{helper}{mower}{attributes}{system}{name} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{system}{model} | ' . $hash->{helper}{mower}{attributes}{system}{model} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{system}{serialNumber} | ' . $hash->{helper}{mower}{attributes}{system}{serialNumber} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{battery}{batteryPercent} | ' . $hash->{helper}{mower}{attributes}{battery}{batteryPercent} . ' | % |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{mode} | ' . $hash->{helper}{mower}{attributes}{mower}{mode} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{activity} | ' . $hash->{helper}{mower}{attributes}{mower}{activity} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{state} | ' . $hash->{helper}{mower}{attributes}{mower}{state} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{errorCode} | ' . $hash->{helper}{mower}{attributes}{mower}{errorCode} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} | ' . $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} . ' | ms |
';
+
+ my $calendarjson = eval { JSON::XS->new->pretty(1)->encode ($hash->{helper}{mower}{attributes}{calendar}{tasks}) };
+
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{calendar}{tasks} | ' . ($@ ? $@ : $calendarjson) . ' |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{planner}{nextStartTimestamp} | ' . $hash->{helper}{mower}{attributes}{planner}{nextStartTimestamp} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{planner}{override}{action} | ' . $hash->{helper}{mower}{attributes}{planner}{override}{action} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{planner}{restrictedReason} | ' . $hash->{helper}{mower}{attributes}{planner}{restrictedReason} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{metadata}{connected} | ' . $hash->{helper}{mower}{attributes}{metadata}{connected} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} | ' . $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} . ' | ms |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{positions}[0]{longitude} | ' . $hash->{helper}{mower}{attributes}{positions}[0]{longitude} . ' | decimal degree |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{positions}[0]{latitude} | ' . $hash->{helper}{mower}{attributes}{positions}[0]{latitude} . ' | decimal degree |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} | ' . $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{settings}{headlight}{mode} | ' . $hash->{helper}{mower}{attributes}{settings}{headlight}{mode} . ' | |
';
+ # $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{cuttingBladeUsageTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{cuttingBladeUsageTime} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} | ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} | ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} . ' | |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} . ' | s |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} . ' | s |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} . '1 | s |
';
+ $cnt++;$ret .= ' $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} | ' . $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} . ' | s |
';
+
+ $ret .= '
';
+ $ret .= '1 totalRunningTime = totalCuttingTime + totalSearchingTime';
+ $ret .= '';
+
+ return $ret;
+
+ } else {
+
+ return '
mower data is not yet available |
';
+
+ }
+}
+
+#########################
+sub listInternalData {
+ my ( $hash ) = @_;
+ my $name = $hash->{NAME};
+ my $cnt = 0;
+ my $ret = '';
+ $ret .= 'Calculated Coordinates For Automatic Registration';
+
+ my $xm = $hash->{helper}{chargingStation}{longitude} // 0;
+ my $ym = $hash->{helper}{chargingStation}{latitude} // 0;
+ my $csnr = scalar @{$hash->{helper}{cspos}};
+ my $csnrmax = $hash->{helper}{PARKED_IN_CS}{maxLength};
+ my $arnr = 0;
+ $arnr = scalar @{$hash->{helper}{areapos}} if( scalar @{$hash->{helper}{areapos}} > 2 );
+ my $arnrmax = $hash->{helper}{MOWING}{maxLength};
+
+ $hash->{helper}{posMinMax} =~ /(-?\d*\.?\d+)\s(-?\d*\.?\d+)(\R|\s)(-?\d*\.?\d+)\s(-?\d*\.?\d+)/;
+
+ if ( $::init_done && $1 && $2 && $4 && $5 ) {
+
+ $ret .= '';
+ $ret .= ' ' . ($csnr + $arnr) . ' ( ' . ($csnrmax + $arnrmax) . ' ) | Upper Left | ' . $1 . ' | ' . $2 . ' |
';
+ $ret .= ' Lower Right | ' . $4 . ' | ' . $5 . ' |
';
+
+ $ret .= '
';
+ $ret .= '
';
+ $ret .= 'Calculated Charging Station Coordinates';
+
+ $ret .= '';
+ $ret .= ' ' . $csnr . ' ( ' . $csnrmax . ' ) | ' . $xm . ' | ' . $ym . ' |
';
+
+ $ret .= '
';
+ $ret .= '
';
+ $ret .= 'Way Point Stacks';
+
+ $ret .= '';
+ $ret .= 'PARKED_IN_CS, CHARGING | cspos | ' . $csnr . ' | ' . $csnrmax . ' |
';
+ $ret .= 'MOWING | areapos | ' . $arnr . ' | ' . $arnrmax . ' |
';
+
+ $ret .= '
';
+ if ( $hash->{TYPE} eq 'AutomowerConnect' ) {
+
+ $ret .= '';
+ $ret .= 'Rest API Data';
+
+ $cnt++;$ret .= ' Link to APIs | Husqvarna Developer |
';
+ $cnt++;$ret .= ' Authentification API URL | ' . ::FHEM::AutomowerConnect::AUTHURL . ' |
';
+ $cnt++;$ret .= ' Automower Connect API URL | ' . ::FHEM::AutomowerConnect::APIURL . ' |
';
+ $cnt++;$ret .= ' Client-Id | ' . $hash->{helper}{client_id} . ' |
';
+ $cnt++;$ret .= ' Grant-Type | ' . $hash->{helper}{grant_type} . ' |
';
+ $cnt++;$ret .= ' User-Id | ' . ReadingsVal($name, '.user_id', '-') . ' |
';
+ $cnt++;$ret .= ' Provider | ' . ReadingsVal($name, '.provider', '-') . ' |
';
+ $cnt++;$ret .= ' Scope | ' . ReadingsVal($name, '.scope', '-') . ' |
';
+ $cnt++;$ret .= ' Token Type | ' . ReadingsVal($name, '.token_type', '-') . ' |
';
+ $cnt++;$ret .= ' Token Expires | ' . FmtDateTime( ReadingsVal($name, '.expires', '0') ) . ' |
';
+ $cnt++;$ret .= ' Access Token | ' . ReadingsVal($name, '.access_token', '0') . ' |
';
+
+ $ret .= '
';
+
+ }
+
+ $ret .= '';
+ return $ret;
+
+ } else {
+
+ return 'Internal data is not yet available |
';
+
+ }
+}
+
+#########################
+sub listErrorCodes {
+ if ($::init_done) {
+
+ my $rowCount = 1;
+ my %ec = ();
+ my $ec = \%ec;
+ for ( keys %{$errortable} ) {
+ $ec->{sprintf("%03d",$_)} = $errortable->{$_} ;
+ }
+ my $ret = '';
+ $ret .= 'API-Response Status Codes';
+ $ret .= '200, 201, 202 204 | response o.k. |
';
+ $ret .= '400, 401, 402 403, 404, 415 500, 503 | error, detailed information see logfile |
';
+ $ret .= '
';
+ $ret .= 'Mower Error Table';
+ for (sort keys %{$ec}) {
+ $ret .= '{$_};
+ $ret .= '
';
+ $rowCount++;
+ }
+
+ $ret .= '
';
+ return $ret;
+
+ } else {
+
+ return 'error codes are not yet available |
';
+
+ }
+}
+
+##############################################################
+
+1;
+