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 .= ''; - - $ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - - $ret .= '
Statistics Data
Hash Path Value Unit
$hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles}   ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} . '
$hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions}   ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} . '
$hash->{helper}{mower}{attributes}{statistics}{totalChargingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} . ' s
$hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} . ' s
$hash->{helper}{mower}{attributes}{statistics}{totalRunningTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} . '1 s
$hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} . ' s
$hash->{helper}{statistics}{currentSpeed}   ' . $hash->{helper}{statistics}{currentSpeed} . ' m/s
$hash->{helper}{statistics}{currentDayTrack}   ' . $hash->{helper}{statistics}{currentDayTrack} . ' m
$hash->{helper}{statistics}{currentDayArea}   ' . $hash->{helper}{statistics}{currentDayArea} . ' qm
$hash->{helper}{statistics}{lastDayTrack}   ' . $hash->{helper}{statistics}{lastDayTrack} . ' m
$hash->{helper}{statistics}{lastDayArea}   ' . $hash->{helper}{statistics}{lastDayArea} . ' qm
$hash->{helper}{statistics}{currentWeekTrack}   ' . $hash->{helper}{statistics}{currentWeekTrack} . ' m
$hash->{helper}{statistics}{currentWeekArea}   ' . $hash->{helper}{statistics}{currentWeekArea} . ' qm
$hash->{helper}{statistics}{lastWeekTrack}   ' . $hash->{helper}{statistics}{lastWeekTrack} . ' m
$hash->{helper}{statistics}{lastWeekArea}   ' . $hash->{helper}{statistics}{lastWeekArea} . ' qm
'; - $ret .= '

1 totalRunningTime = totalCuttingTime + totalSearchingTime'; - $ret .= ''; - - return $ret; -} - -######################### -sub listMowerData { - my ( $hash ) = @_; - my $name = $hash->{NAME}; - my $cnt = 0; - my $ret = ''; - $ret .= ''; - $ret .= ''; - - $ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - - my $calendarjson = eval { JSON::XS->new->pretty(1)->encode ($hash->{helper}{mower}{attributes}{calendar}{tasks}) }; - - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; -# $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - - $ret .= '
Mower Data
Hash Path Value Unit
$hash->{helper}{mower}{type}   ' . $hash->{helper}{mower}{type} . '
$hash->{helper}{mower}{id}   ' . $hash->{helper}{mower}{id} . '
$hash->{helper}{mower}{attributes}{system}{name}   ' . $hash->{helper}{mower}{attributes}{system}{name} . '
$hash->{helper}{mower}{attributes}{system}{model}   ' . $hash->{helper}{mower}{attributes}{system}{model} . '
$hash->{helper}{mower}{attributes}{system}{serialNumber}   ' . $hash->{helper}{mower}{attributes}{system}{serialNumber} . '
$hash->{helper}{mower}{attributes}{battery}{batteryPercent}   ' . $hash->{helper}{mower}{attributes}{battery}{batteryPercent} . ' %
$hash->{helper}{mower}{attributes}{mower}{mode}   ' . $hash->{helper}{mower}{attributes}{mower}{mode} . '
$hash->{helper}{mower}{attributes}{mower}{activity}   ' . $hash->{helper}{mower}{attributes}{mower}{activity} . '
$hash->{helper}{mower}{attributes}{mower}{state}   ' . $hash->{helper}{mower}{attributes}{mower}{state} . '
$hash->{helper}{mower}{attributes}{mower}{errorCode}   ' . $hash->{helper}{mower}{attributes}{mower}{errorCode} . '
$hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp}   ' . $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} . ' ms
$hash->{helper}{mower}{attributes}{calendar}{tasks}   ' . ($@ ? $@ : $calendarjson) . '
$hash->{helper}{mower}{attributes}{planner}{nextStartTimestamp}   ' . $hash->{helper}{mower}{attributes}{planner}{nextStartTimestamp} . '
$hash->{helper}{mower}{attributes}{planner}{override}{action}   ' . $hash->{helper}{mower}{attributes}{planner}{override}{action} . '
$hash->{helper}{mower}{attributes}{planner}{restrictedReason}   ' . $hash->{helper}{mower}{attributes}{planner}{restrictedReason} . '
$hash->{helper}{mower}{attributes}{metadata}{connected}   ' . $hash->{helper}{mower}{attributes}{metadata}{connected} . '
$hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}   ' . $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} . ' ms
$hash->{helper}{mower}{attributes}{positions}[0]{longitude}   ' . $hash->{helper}{mower}{attributes}{positions}[0]{longitude} . ' decimal degree
$hash->{helper}{mower}{attributes}{positions}[0]{latitude}   ' . $hash->{helper}{mower}{attributes}{positions}[0]{latitude} . ' decimal degree
$hash->{helper}{mower}{attributes}{settings}{cuttingHeight}   ' . $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} . '
$hash->{helper}{mower}{attributes}{settings}{headlight}{mode}   ' . $hash->{helper}{mower}{attributes}{settings}{headlight}{mode} . '
$hash->{helper}{mower}{attributes}{statistics}{cuttingBladeUsageTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{cuttingBladeUsageTime} . '
$hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles}   ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} . '
$hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions}   ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} . '
$hash->{helper}{mower}{attributes}{statistics}{totalChargingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} . ' s
$hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} . ' s
$hash->{helper}{mower}{attributes}{statistics}{totalRunningTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} . '1 s
$hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} . ' s
'; - $ret .= '

1 totalRunningTime = totalCuttingTime + totalSearchingTime'; - $ret .= ''; - - return $ret; -} - -######################### -sub listInternalData { - my ( $hash ) = @_; - my $name = $hash->{NAME}; - my $rowCount = 1; - my $ret = ''; - $ret .= ''; - - $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 .= ''; - $ret .= ''; - - $ret .= '
Calculated Coordinates For Automatic Registration
Data Sets ( max )  Corner Longitude Latitude
' . ($csnr + $arnr) . ' ( ' . ($csnrmax + $arnrmax) . ' )  Upper Left ' . $1 . ' ' . $2 . '
Lower Right ' . $4 . ' ' . $5 . '

'; - $ret .= ''; - $ret .= ''; - - $ret .= ''; - $ret .= ''; - - $ret .= '
Calculated Charging Station Coordinates
Data Sets (max)  Longitude  Latitude 
' . $csnr . ' ( ' . $csnrmax . ' )  ' . $xm . ' ' . $ym . ' 

'; - $ret .= ''; - $ret .= ''; - - $ret .= ''; - $ret .= ''; - $ret .= ''; - - $ret .= '
Way Point Stacks
Used For Action  Stack Name  Current Size  Max Size 
PARKED_IN_CS, CHARGING  cspos  ' . $csnr . ' ' . $csnrmax . ' 
MOWING  areapos  ' . $arnr . ' ' . $arnrmax . ' 
'; - if ( $hash->{TYPE} eq 'AutomowerConnect' ) { - - $ret .= '

'; - $ret .= ''; - - $ret .= ''; - $ret .= ''; - $ret .= ''; - $ret .= ''; - $ret .= ''; - $ret .= ''; - $ret .= ''; - $ret .= ''; - $ret .= ''; - - $ret .= '
Authentification Data
Authentification URL' . AUTHURL . '
Client-Id' . $hash->{helper}{client_id} . '
Grant-Type' . $hash->{helper}{grant_type} . '
User-Id' . ReadingsVal($name, '.user_id', '-') . '
Provider' . ReadingsVal($name, '.provider', '-') . '
Scope' . ReadingsVal($name, '.scope', '-') . '
Token Type' . ReadingsVal($name, '.token_type', '-') . '
Token Expires ' . FmtDateTime( ReadingsVal($name, '.expires', '0') ) . '
Access Token' . ReadingsVal($name, '.access_token', '0') . '
'; - - } - $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 .= ''; - $ret .= ''; - $ret .= ''; - $ret .= '
API-Response Status Codes
200, 201, 202
204
response o.k.
400, 401, 402
403, 404, 415
500, 503
error, detailed information see logfile

'; - $ret .= ''; - for (sort keys %{$ec}) { - $ret .= '{$_}; - $ret .= ''; - $rowCount++; - } - $ret .= '
Mower Error Table
'; - - 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 .= ''; - - $ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - - $ret .= '
    Statistics Data
    Hash Path Value Unit
    $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles}   ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} . '
    $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions}   ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} . '
    $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} . ' s
    $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} . ' s
    $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} . '1 s
    $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} . ' s
    $hash->{helper}{statistics}{currentSpeed}   ' . $hash->{helper}{statistics}{currentSpeed} . ' m/s
    $hash->{helper}{statistics}{currentDayTrack}   ' . $hash->{helper}{statistics}{currentDayTrack} . ' m
    $hash->{helper}{statistics}{currentDayArea}   ' . $hash->{helper}{statistics}{currentDayArea} . ' qm
    $hash->{helper}{statistics}{lastDayTrack}   ' . $hash->{helper}{statistics}{lastDayTrack} . ' m
    $hash->{helper}{statistics}{lastDayArea}   ' . $hash->{helper}{statistics}{lastDayArea} . ' qm
    $hash->{helper}{statistics}{currentWeekTrack}   ' . $hash->{helper}{statistics}{currentWeekTrack} . ' m
    $hash->{helper}{statistics}{currentWeekArea}   ' . $hash->{helper}{statistics}{currentWeekArea} . ' qm
    $hash->{helper}{statistics}{lastWeekTrack}   ' . $hash->{helper}{statistics}{lastWeekTrack} . ' m
    $hash->{helper}{statistics}{lastWeekArea}   ' . $hash->{helper}{statistics}{lastWeekArea} . ' qm
    '; - $ret .= '

    1 totalRunningTime = totalCuttingTime + totalSearchingTime'; - $ret .= ''; - - return $ret; -} - -######################### -sub listMowerData { - my ( $hash ) = @_; - my $name = $hash->{NAME}; - my $cnt = 0; - my $ret = ''; - $ret .= ''; - $ret .= ''; - - $ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - - my $calendarjson = eval { JSON::XS->new->pretty(1)->encode ($hash->{helper}{mower}{attributes}{calendar}{tasks}) }; - - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; -# $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - $cnt++;$ret .= ''; - - $ret .= '
    Mower Data
    Hash Path Value Unit
    $hash->{helper}{mower}{type}   ' . $hash->{helper}{mower}{type} . '
    $hash->{helper}{mower}{id}   ' . $hash->{helper}{mower}{id} . '
    $hash->{helper}{mower}{attributes}{system}{name}   ' . $hash->{helper}{mower}{attributes}{system}{name} . '
    $hash->{helper}{mower}{attributes}{system}{model}   ' . $hash->{helper}{mower}{attributes}{system}{model} . '
    $hash->{helper}{mower}{attributes}{system}{serialNumber}   ' . $hash->{helper}{mower}{attributes}{system}{serialNumber} . '
    $hash->{helper}{mower}{attributes}{battery}{batteryPercent}   ' . $hash->{helper}{mower}{attributes}{battery}{batteryPercent} . ' %
    $hash->{helper}{mower}{attributes}{mower}{mode}   ' . $hash->{helper}{mower}{attributes}{mower}{mode} . '
    $hash->{helper}{mower}{attributes}{mower}{activity}   ' . $hash->{helper}{mower}{attributes}{mower}{activity} . '
    $hash->{helper}{mower}{attributes}{mower}{state}   ' . $hash->{helper}{mower}{attributes}{mower}{state} . '
    $hash->{helper}{mower}{attributes}{mower}{errorCode}   ' . $hash->{helper}{mower}{attributes}{mower}{errorCode} . '
    $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp}   ' . $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} . ' ms
    $hash->{helper}{mower}{attributes}{calendar}{tasks}   ' . ($@ ? $@ : $calendarjson) . '
    $hash->{helper}{mower}{attributes}{planner}{nextStartTimestamp}   ' . $hash->{helper}{mower}{attributes}{planner}{nextStartTimestamp} . '
    $hash->{helper}{mower}{attributes}{planner}{override}{action}   ' . $hash->{helper}{mower}{attributes}{planner}{override}{action} . '
    $hash->{helper}{mower}{attributes}{planner}{restrictedReason}   ' . $hash->{helper}{mower}{attributes}{planner}{restrictedReason} . '
    $hash->{helper}{mower}{attributes}{metadata}{connected}   ' . $hash->{helper}{mower}{attributes}{metadata}{connected} . '
    $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}   ' . $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} . ' ms
    $hash->{helper}{mower}{attributes}{positions}[0]{longitude}   ' . $hash->{helper}{mower}{attributes}{positions}[0]{longitude} . ' decimal degree
    $hash->{helper}{mower}{attributes}{positions}[0]{latitude}   ' . $hash->{helper}{mower}{attributes}{positions}[0]{latitude} . ' decimal degree
    $hash->{helper}{mower}{attributes}{settings}{cuttingHeight}   ' . $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} . '
    $hash->{helper}{mower}{attributes}{settings}{headlight}{mode}   ' . $hash->{helper}{mower}{attributes}{settings}{headlight}{mode} . '
    $hash->{helper}{mower}{attributes}{statistics}{cuttingBladeUsageTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{cuttingBladeUsageTime} . '
    $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles}   ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} . '
    $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions}   ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} . '
    $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} . ' s
    $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} . ' s
    $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} . '1 s
    $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} . ' s
    '; - $ret .= '

    1 totalRunningTime = totalCuttingTime + totalSearchingTime'; - $ret .= ''; - - return $ret; -} - -######################### -sub listInternalData { - my ( $hash ) = @_; - my $name = $hash->{NAME}; - my $rowCount = 1; - my $ret = ''; - $ret .= ''; - - $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 .= ''; - $ret .= ''; - - $ret .= '
    Calculated Coordinates For Automatic Registration
    Data Sets ( max )  Corner Longitude Latitude
    ' . ($csnr + $arnr) . ' ( ' . ($csnrmax + $arnrmax) . ' )  Upper Left ' . $1 . ' ' . $2 . '
    Lower Right ' . $4 . ' ' . $5 . '

    '; - $ret .= ''; - $ret .= ''; - - $ret .= ''; - $ret .= ''; - - $ret .= '
    Calculated Charging Station Coordinates
    Data Sets (max)  Longitude  Latitude 
    ' . $csnr . ' ( ' . $csnrmax . ' )  ' . $xm . ' ' . $ym . ' 

    '; - $ret .= ''; - $ret .= ''; - - $ret .= ''; - $ret .= ''; - $ret .= ''; - - $ret .= '
    Way Point Stacks
    Used For Action  Stack Name  Current Size  Max Size 
    PARKED_IN_CS, CHARGING  cspos  ' . $csnr . ' ' . $csnrmax . ' 
    MOWING  areapos  ' . $arnr . ' ' . $arnrmax . ' 
    '; - if ( $hash->{TYPE} eq 'AutomowerConnect' ) { - - $ret .= '

    '; - $ret .= ''; - - $ret .= ''; - - $ret .= '
    Access Token ( expires: ' . ReadingsVal( $name, 'api_token_expires', 'none') . ' )
    ' . ReadingsVal( $name, '.access_token', 'none') . '
    '; - - } - $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 .= ''; - $ret .= ''; - $ret .= ''; - $ret .= '
    API-Response Status Codes
    200, 201, 202
    204
    response o.k.
    400, 401, 402
    403, 404, 415
    500, 503
    error, detailed information see logfile

    '; - $ret .= ''; - for (sort keys %{$ec}) { - $ret .= '{$_}; - $ret .= ''; - $rowCount++; - } - $ret .= '
    Mower Error Table
    '; - - 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 .= ''; + + $ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + + $ret .= '
    Statistics Data
    Hash Path Value Unit
    $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles}   ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} . '
    $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions}   ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} . '
    $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} . ' s
    $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} . ' s
    $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} . '1 s
    $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} . ' s
    $hash->{helper}{statistics}{currentSpeed}   ' . $hash->{helper}{statistics}{currentSpeed} . ' m/s
    $hash->{helper}{statistics}{currentDayTrack}   ' . $hash->{helper}{statistics}{currentDayTrack} . ' m
    $hash->{helper}{statistics}{currentDayArea}   ' . $hash->{helper}{statistics}{currentDayArea} . ' qm
    $hash->{helper}{statistics}{lastDayTrack}   ' . $hash->{helper}{statistics}{lastDayTrack} . ' m
    $hash->{helper}{statistics}{lastDayArea}   ' . $hash->{helper}{statistics}{lastDayArea} . ' qm
    $hash->{helper}{statistics}{currentWeekTrack}   ' . $hash->{helper}{statistics}{currentWeekTrack} . ' m
    $hash->{helper}{statistics}{currentWeekArea}   ' . $hash->{helper}{statistics}{currentWeekArea} . ' qm
    $hash->{helper}{statistics}{lastWeekTrack}   ' . $hash->{helper}{statistics}{lastWeekTrack} . ' m
    $hash->{helper}{statistics}{lastWeekArea}   ' . $hash->{helper}{statistics}{lastWeekArea} . ' qm
    '; + $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 .= ''; + + $ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + + my $calendarjson = eval { JSON::XS->new->pretty(1)->encode ($hash->{helper}{mower}{attributes}{calendar}{tasks}) }; + + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + # $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + + $ret .= '
    Mower Data
    Hash Path Value Unit
    $hash->{helper}{mower}{type}   ' . $hash->{helper}{mower}{type} . '
    $hash->{helper}{mower}{id}   ' . $hash->{helper}{mower}{id} . '
    $hash->{helper}{mower}{attributes}{system}{name}   ' . $hash->{helper}{mower}{attributes}{system}{name} . '
    $hash->{helper}{mower}{attributes}{system}{model}   ' . $hash->{helper}{mower}{attributes}{system}{model} . '
    $hash->{helper}{mower}{attributes}{system}{serialNumber}   ' . $hash->{helper}{mower}{attributes}{system}{serialNumber} . '
    $hash->{helper}{mower}{attributes}{battery}{batteryPercent}   ' . $hash->{helper}{mower}{attributes}{battery}{batteryPercent} . ' %
    $hash->{helper}{mower}{attributes}{mower}{mode}   ' . $hash->{helper}{mower}{attributes}{mower}{mode} . '
    $hash->{helper}{mower}{attributes}{mower}{activity}   ' . $hash->{helper}{mower}{attributes}{mower}{activity} . '
    $hash->{helper}{mower}{attributes}{mower}{state}   ' . $hash->{helper}{mower}{attributes}{mower}{state} . '
    $hash->{helper}{mower}{attributes}{mower}{errorCode}   ' . $hash->{helper}{mower}{attributes}{mower}{errorCode} . '
    $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp}   ' . $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} . ' ms
    $hash->{helper}{mower}{attributes}{calendar}{tasks}   ' . ($@ ? $@ : $calendarjson) . '
    $hash->{helper}{mower}{attributes}{planner}{nextStartTimestamp}   ' . $hash->{helper}{mower}{attributes}{planner}{nextStartTimestamp} . '
    $hash->{helper}{mower}{attributes}{planner}{override}{action}   ' . $hash->{helper}{mower}{attributes}{planner}{override}{action} . '
    $hash->{helper}{mower}{attributes}{planner}{restrictedReason}   ' . $hash->{helper}{mower}{attributes}{planner}{restrictedReason} . '
    $hash->{helper}{mower}{attributes}{metadata}{connected}   ' . $hash->{helper}{mower}{attributes}{metadata}{connected} . '
    $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}   ' . $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} . ' ms
    $hash->{helper}{mower}{attributes}{positions}[0]{longitude}   ' . $hash->{helper}{mower}{attributes}{positions}[0]{longitude} . ' decimal degree
    $hash->{helper}{mower}{attributes}{positions}[0]{latitude}   ' . $hash->{helper}{mower}{attributes}{positions}[0]{latitude} . ' decimal degree
    $hash->{helper}{mower}{attributes}{settings}{cuttingHeight}   ' . $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} . '
    $hash->{helper}{mower}{attributes}{settings}{headlight}{mode}   ' . $hash->{helper}{mower}{attributes}{settings}{headlight}{mode} . '
    $hash->{helper}{mower}{attributes}{statistics}{cuttingBladeUsageTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{cuttingBladeUsageTime} . '
    $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles}   ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfChargingCycles} . '
    $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions}   ' . $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} . '
    $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalChargingTime} . ' s
    $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalCuttingTime} . ' s
    $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalRunningTime} . '1 s
    $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime}   ' . $hash->{helper}{mower}{attributes}{statistics}{totalSearchingTime} . ' s
    '; + $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 .= ''; + + 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 .= ''; + $ret .= ''; + + $ret .= '
    Calculated Coordinates For Automatic Registration
    Data Sets ( max )  Corner Longitude Latitude
    ' . ($csnr + $arnr) . ' ( ' . ($csnrmax + $arnrmax) . ' )  Upper Left ' . $1 . ' ' . $2 . '
    Lower Right ' . $4 . ' ' . $5 . '

    '; + $ret .= ''; + $ret .= ''; + + $ret .= ''; + $ret .= ''; + + $ret .= '
    Calculated Charging Station Coordinates
    Data Sets (max)  Longitude  Latitude 
    ' . $csnr . ' ( ' . $csnrmax . ' )  ' . $xm . ' ' . $ym . ' 

    '; + $ret .= ''; + $ret .= ''; + + $ret .= ''; + $ret .= ''; + $ret .= ''; + + $ret .= '
    Way Point Stacks
    Used For Action  Stack Name  Current Size  Max Size 
    PARKED_IN_CS, CHARGING  cspos  ' . $csnr . ' ' . $csnrmax . ' 
    MOWING  areapos  ' . $arnr . ' ' . $arnrmax . ' 
    '; + if ( $hash->{TYPE} eq 'AutomowerConnect' ) { + + $ret .= '

    '; + $ret .= ''; + + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + $cnt++;$ret .= ''; + + $ret .= '
    Rest API Data
    Link to APIsHusqvarna Developer
    Authentification API URL' . ::FHEM::AutomowerConnect::AUTHURL . '
    Automower Connect API URL' . ::FHEM::AutomowerConnect::APIURL . '
    Client-Id' . $hash->{helper}{client_id} . '
    Grant-Type' . $hash->{helper}{grant_type} . '
    User-Id' . ReadingsVal($name, '.user_id', '-') . '
    Provider' . ReadingsVal($name, '.provider', '-') . '
    Scope' . ReadingsVal($name, '.scope', '-') . '
    Token Type' . ReadingsVal($name, '.token_type', '-') . '
    Token Expires ' . FmtDateTime( ReadingsVal($name, '.expires', '0') ) . '
    Access Token' . ReadingsVal($name, '.access_token', '0') . '
    '; + + } + + $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 .= ''; + $ret .= ''; + $ret .= ''; + $ret .= '
    API-Response Status Codes
    200, 201, 202
    204
    response o.k.
    400, 401, 402
    403, 404, 415
    500, 503
    error, detailed information see logfile

    '; + $ret .= ''; + for (sort keys %{$ec}) { + $ret .= '{$_}; + $ret .= ''; + $rowCount++; + } + + $ret .= '
    Mower Error Table
    '; + return $ret; + + } else { + + return '
    error codes are not yet available
    '; + + } +} + +############################################################## + +1; +