From 29d4402b63b8a7ad8e23009b0f52db2dfcc3dd76 Mon Sep 17 00:00:00 2001 From: Ellert <> Date: Mon, 29 May 2023 01:26:46 +0000 Subject: [PATCH] 74_AutomowerConnect: Common.pm, automowerconnect.js, implemented error stack and getter to show, show daily collisions, not calculate statistics solved, use only differential data to update mower path, hints at https://forum.fhem.de/index.php?topic=131661.msg1277180 git-svn-id: https://svn.fhem.de/fhem/trunk@27625 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 5 + fhem/FHEM/74_AutomowerConnect.pm | 113 ++++++-- fhem/lib/FHEM/Devices/AMConnect/Common.pm | 327 +++++++++++++--------- fhem/www/pgm2/automowerconnect.js | 89 ++++-- 4 files changed, 336 insertions(+), 198 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index 276347c33..2b13a0026 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,10 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - change: 74_AutomowerConnect: Common.pm, automowerconnect.js + implemented error stack and getter to show + show daily collisions, not calculate statistics solved + use only differential data to update mower path, hints at + https://forum.fhem.de/index.php?topic=131661.msg1277180 - feature: 76_SMAInverter: add new Inverter (STP X, SI x.xM-13), add String 3 for STP X - feature: 76_SMAInverter: add new Readings diff --git a/fhem/FHEM/74_AutomowerConnect.pm b/fhem/FHEM/74_AutomowerConnect.pm index b62022557..bac47c187 100644 --- a/fhem/FHEM/74_AutomowerConnect.pm +++ b/fhem/FHEM/74_AutomowerConnect.pm @@ -103,6 +103,7 @@ sub Initialize() { $hash->{GetFn} = \&FHEM::Devices::AMConnect::Common::Get; $hash->{UndefFn} = \&FHEM::Devices::AMConnect::Common::Undefine; $hash->{DeleteFn} = \&FHEM::Devices::AMConnect::Common::Delete; + $hash->{ShutdownFn} = \&FHEM::Devices::AMConnect::Common::Shutdown; $hash->{RenameFn} = \&FHEM::Devices::AMConnect::Common::Rename; $hash->{FW_detailFn}= \&FHEM::Devices::AMConnect::Common::FW_detailFn; $hash->{ReadFn} = \&wsRead; @@ -198,7 +199,7 @@ sub APIAuth { } else { RemoveInternalTimer( $hash, \&APIAuth ); - InternalTimer( gettimeofday() + 20, \&APIAuth, $hash, 0 ); + InternalTimer( gettimeofday() + 10, \&APIAuth, $hash, 0 ); } return undef; @@ -235,7 +236,18 @@ sub APIAuthResponse { readingsBulkUpdateIfChanged($hash,'.provider',$hash->{helper}{auth}{provider},0 ); readingsBulkUpdateIfChanged($hash,'.user_id',$hash->{helper}{auth}{user_id},0 ); - $hash->{helper}{auth}{expires} = $result->{expires_in} + gettimeofday(); + # refresh token between 00:00 and 01:00 + my $expire = $result->{expires_in} + gettimeofday(); + my ( @tim ) = localtime( $expire ); + my $seconds = $tim[0] + $tim[1] * 60 + $tim[2] * 3600; + if ($seconds > 3600) { + $tim[ 0 ] = 0; + $tim[ 1 ] = 0; + $tim[ 2 ] = 1; + $expire = ::timelocal( @tim ); + } + + $hash->{helper}{auth}{expires} = $expire; readingsBulkUpdateIfChanged($hash,'.expires',$hash->{helper}{auth}{expires},0 ); readingsBulkUpdateIfChanged($hash,'.scope',$hash->{helper}{auth}{scope},0 ); readingsBulkUpdateIfChanged($hash,'.token_type',$hash->{helper}{auth}{token_type},0 ); @@ -346,11 +358,13 @@ sub getMowerResponse { $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp}; $hash->{helper}{mowerold}{attributes}{mower}{activity} = $hash->{helper}{mower}{attributes}{mower}{activity}; + $hash->{helper}{mowerold}{attributes}{statistics}{numberOfCollisions} = $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions}; } else { # first data set $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp} = $hash->{helper}{mowers}[$mowerNumber]{attributes}{metadata}{statusTimestamp}; $hash->{helper}{mowerold}{attributes}{mower}{activity} = $hash->{helper}{mowers}[$mowerNumber]{attributes}{mower}{activity}; + $hash->{helper}{mowerold}{attributes}{statistics}{numberOfCollisions} = $hash->{helper}{mowers}[$mowerNumber]{attributes}{statistics}{numberOfCollisions}; if ( AttrVal( $name, 'mapImageCoordinatesToRegister', '' ) eq '' ) { ::FHEM::Devices::AMConnect::Common::posMinMax( $hash, $hash->{helper}{mowers}[$mowerNumber]{attributes}{positions} ); @@ -365,19 +379,18 @@ sub getMowerResponse { $hash->{helper}{storediff} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp}; + ::FHEM::Devices::AMConnect::Common::calculateStatistics($hash); + # Update readings readingsBeginUpdate($hash); - readingsBulkUpdateIfChanged($hash, 'api_MowerFound', $foundMower ); # host only + readingsBulkUpdateIfChanged($hash, 'api_MowerFound', $foundMower ); ::FHEM::Devices::AMConnect::Common::fillReadings( $hash ); readingsEndUpdate($hash, 1); readingsSingleUpdate($hash, 'device_state', 'connected', 1 ); - # initialize statistics - ::FHEM::Devices::AMConnect::Common::initStatistics($hash); - # schedule new access token RemoveInternalTimer( $hash, \&APIAuth ); InternalTimer( ReadingsVal($name, '.expires', 600)-37, \&APIAuth, $hash, 0 ); @@ -480,6 +493,10 @@ sub wsRead { $hash->{helper}{mower}{attributes}{planner} = dclone( $result->{attributes}{planner} ); $hash->{helper}{storediff} = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp}; + $hash->{helper}{detailFnNewPos} = 0; + ::FHEM::Devices::AMConnect::Common::isErrorThanPrepare( $hash ); + ::FHEM::Devices::AMConnect::Common::resetLastErrorIfCorrected( $hash ); + } if ( $result->{type} eq "positions-event" ) { @@ -489,8 +506,23 @@ sub wsRead { # $result->{attributes}{positions}[ $i ]->{nr}=$i; # }; $hash->{helper}{mower}{attributes}{positions} = dclone( $result->{attributes}{positions} ); + ::FHEM::Devices::AMConnect::Common::AlignArray( $hash ); - ::FHEM::Devices::AMConnect::Common::FW_detailFn_Update ($hash) if (AttrVal($name,'showMap',1)); + + my $deltaTime = $hash->{helper}{positionsTime} - $hash->{helper}{statusTime}; + + # if encounter positions shortly after status-event count it as error positions + if ( $hash->{helper}{mower}{attributes}{mower}{errorCode} && $deltaTime > 0 && $deltaTime < 0.29 && @{ $result->{attributes}{positions} } < 3) { + + $hash->{helper}{areapos}[ 0 ]{act} = 'N'; + $hash->{helper}{areapos}[ 1 ]{act} = 'N'; + $hash->{helper}{lasterror}{positions} = [dclone( $hash->{helper}{areapos}[ 0 ] ), dclone( $hash->{helper}{areapos}[ 1 ] ) ]; + $hash->{helper}{errorstack}[0]{positions} = [dclone( $hash->{helper}{areapos}[ 0 ] ), dclone( $hash->{helper}{areapos}[ 1 ] ) ]; + + } + + $hash->{helper}{detailFnNewPos} = scalar @{ $result->{attributes}{positions} }; + ::FHEM::Devices::AMConnect::Common::FW_detailFn_Update ($hash); } @@ -499,6 +531,7 @@ sub wsRead { $hash->{helper}{mower}{attributes}{calendar} = dclone( $result->{attributes}{calendar} ) if ( defined ( $result->{attributes}{calendar} ) ); $hash->{helper}{mower}{attributes}{settings}{headlight} = $result->{attributes}{headlight} if ( defined ( $result->{attributes}{headlight} ) ); $hash->{helper}{mower}{attributes}{settings}{cuttingHeight} = $result->{attributes}{cuttingHeight} if ( defined ( $result->{attributes}{cuttingHeight} ) ); + } # Update readings @@ -893,7 +926,7 @@ __END__ It has to be set a client_secret. It's the application secret from the Husqvarna Developer Portal.
set myMower <client secret>

- +
@@ -931,11 +964,11 @@ __END__ set <name> client_secret <application secret>
Sets the mandatory application secret (client secret) -
  • cuttingHeight
    +
  • cuttingHeight
    set <name> cuttingHeight <1..9>
    Sets the cutting height. NOTE: Do not use for 550 EPOS and Ceora.
  • -
  • getNewAccessToken
    +
  • getNewAccessToken
    set <name> getNewAccessToken
    Gets a new access token
  • @@ -943,27 +976,31 @@ __END__ set <name> getUpdate
    Gets data from the API. This is done each intervall automatically. -
  • headlight
    +
  • headlight
    set <name> headlight <ALWAYS_OFF|ALWAYS_ON|EVENIG_ONLY|EVENING_AND_NIGHT>
    +
  • - -
  • mowerScheduleToAttribute
    +
  • mowerScheduleToAttribute
    set <name> mowerScheduleToAttribute
    Writes the schedule in to the attribute moverSchedule.
  • -
  • sendScheduleFromAttributeToMower
    +
  • sendScheduleFromAttributeToMower
    set <name> sendScheduleFromAttributeToMower
    Sends the schedule to the mower. NOTE: Do not use for 550 EPOS and Ceora.
  • -
  • mapZonesTemplateToAttribute
    +
  • mapZonesTemplateToAttribute
    set <name> mapZonesTemplateToAttribute
    Load the command reference example into the attribute mapZones.
  • +
  • defaultDesignAttributesToAttribute
    + set <name> mapZonesTemplateToAttribute
    + Load default design attributes.
  • -

  • + +

  • set <name>
    -
  • - + +


    @@ -986,11 +1023,15 @@ __END__
  • StatisticsData
    get <name> StatisticsData
    - Lists statistics data with its hash path. The hash path can be used for generating userReadings. The trigger is connected.
  • + Lists statistics data with its hash path. The hash path can be used for generating userReadings. The trigger is device_state: connected.
  • errorCodes
    get <name> errorCodes
    Lists API response status codes and mower error codes
  • + +
  • errorStack
    + get <name> errorStack
    + Lists error stack.



  • @@ -1024,10 +1065,12 @@ __END__ attr <name> mapDesignAttributes <complete list of design-attributes>
    Load the list of attributes by set <name> defaultDesignAttributesToAttribute to change its values. Some default values are @@ -1043,7 +1086,7 @@ __END__ This attribute has to be set after the attribute mapImageCoordinatesToRegister. The values are used to calculate the scale factors and the attribute scaleToMeterXY is set accordingly.
  • showMap
    - attr <name> showMap <>1,0
    + attr <name> showMap <1,0>
    Shows Map on (1 default) or not (0).
  • chargingStationCoordinates
    @@ -1164,7 +1207,7 @@ __END__
  • 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
  • +
  • statistics_numberOfCollisions - Number of collisions (last day/all days)
  • status_connected - state of connetion between mower and Husqvarna Cloud.
  • 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
  • @@ -1281,6 +1324,10 @@ __END__ set <name> mapZonesTemplateToAttribute
    Läd das Beispiel aus der Befehlsreferenz in das Attribut mapZones. +
  • defaultDesignAttributesToAttribute
    + set <name> mapZonesTemplateToAttribute
    + Läd die Standartdesignattribute.
  • +

  • set <name>
  • @@ -1308,7 +1355,11 @@ __END__
  • StatisticsData
    get <name> StatisticsData
    - Listet statistische Daten mit ihrem Hashpfad auf. Der Hashpfad kann zur Erzeugung von userReadings genutzt werden, getriggert wird durch connected
  • + Listet statistische Daten mit ihrem Hashpfad auf. Der Hashpfad kann zur Erzeugung von userReadings genutzt werden, getriggert wird durch device_state: connected + +
  • errorStack
    + get <name> errorStack
    + Listet die gespeicherten Fehler auf.



  • @@ -1345,10 +1396,12 @@ __END__ attr <name> mapDesignAttributes <complete list of design-attributes>
    Lade die Attributliste mit set <name> defaultDesignAttributesToAttribute um die Werte zu ändern. Einige Vorgabewerte: @@ -1367,7 +1420,7 @@ __END__ Dieses Attribut berechnet die Skalierungsfaktoren. Das Attribut scaleToMeterXY wird entsprechend gesetzt.
  • showMap
    - attr <name> showMap <>1,0
    + attr <name> showMap <1,0>
    Zeigt die Karte an (1 default) oder nicht (0).
  • chargingStationCoordinates
    @@ -1490,7 +1543,7 @@ __END__
  • 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
  • +
  • statistics_numberOfCollisions - Anzahl der Kollisionen (letzter Tag/alle Tage)
  • status_connected - Status der Verbindung zwischen dem Automower und der Husqvarna Cloud.
  • 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
  • diff --git a/fhem/lib/FHEM/Devices/AMConnect/Common.pm b/fhem/lib/FHEM/Devices/AMConnect/Common.pm index eb5a30692..8e9945f50 100644 --- a/fhem/lib/FHEM/Devices/AMConnect/Common.pm +++ b/fhem/lib/FHEM/Devices/AMConnect/Common.pm @@ -69,6 +69,7 @@ BEGIN { devspec2array DevIo_IsOpen DevIo_CloseDev + DevIo_setStates ) ); } @@ -171,24 +172,26 @@ my $mapZonesTpl = '{ imageHeight => 650, imageWidthHeight => '350 650', mapdesign => $mapAttr, + detailFnFirst => 0, + detailFnNewPos => 0, + detailFnAttrMaxPos => 5000, mapZonesTpl => $mapZonesTpl, posMinMax => "-180 90\n180 -90", newdatasets => 0, - newzonedatasets => 0, + newzonedatasets => 0, + positionsTime => 0, + statusTime => 0, MAP_PATH => '', MAP_MIME => '', MAP_CACHE => '', cspos => [], areapos => [], + errorstack => [], lasterror => { positions => [], timestamp => 0, errordesc => '-', - errordate => '', - sizex => 0, - sizey => 0, - olLon => 0, - olLat => 0 + errordate => '' }, UNKNOWN => { short => 'U', @@ -255,6 +258,7 @@ my $mapZonesTpl = '{ lastDayTrack => 0, lastDayArea => 0, lastDaytime => 0, + lastDayCollisions => 0, currentWeekTrack => 0, currentWeekArea => 0, currentWeekTime => 0, @@ -280,13 +284,15 @@ my $mapZonesTpl = '{ RemoveInternalTimer($hash); InternalTimer( gettimeofday() + 2, \&::FHEM::AutomowerConnect::APIAuth, $hash, 1); - InternalTimer( gettimeofday() + 30, \&readMap, $hash, 0); + InternalTimer( gettimeofday() + 20, \&readMap, $hash, 0); - readingsSingleUpdate( $hash, 'device_state', 'defined', 1 ); + DevIo_setStates( $hash, "disconnected" ); + readingsSingleUpdate( $hash, 'device_state', 'defined', 1 ); } else { - readingsSingleUpdate( $hash, 'device_state', 'defined - client_secret missing', 1 ); + DevIo_setStates( $hash, "disconnected" ); + readingsSingleUpdate( $hash, 'device_state', 'defined - client_secret missing', 1 ); } @@ -294,15 +300,23 @@ my $mapZonesTpl = '{ } +######################### +sub Shutdown { + my ( $hash, $arg ) = @_; + + DevIo_CloseDev( $hash ) if ( DevIo_IsOpen( $hash ) ); + DevIo_setStates( $hash, "closed" ); + + return undef; +} + ######################### sub Undefine { my ( $hash, $arg ) = @_; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - DevIo_CloseDev( $hash ) if ( DevIo_IsOpen( $hash ) ); RemoveInternalTimer( $hash ); - ::FHEM::Devices::AMConnect::Common::RemoveExtension("$type/$name/map"); return undef; } @@ -377,9 +391,14 @@ sub Get { my $ret = ::FHEM::Devices::AMConnect::Common::listStatisticsData($hash); return $ret; + } elsif ( $setName eq 'errorStack' ) { + + my $ret = ::FHEM::Devices::AMConnect::Common::listErrorStack($hash); + return $ret; + } else { - return "Unknown argument $setName, choose one of StatisticsData:noArg MowerData:noArg InternalData:noArg errorCodes:noArg "; + return "Unknown argument $setName, choose one of StatisticsData:noArg MowerData:noArg InternalData:noArg errorCodes:noArg errorStack:noArg "; } } @@ -398,88 +417,28 @@ sub FW_detailFn { my $design = AttrVal( $name, 'mapDesignAttributes', $hash->{helper}{mapdesign} ); my @adesign = split(/\R/,$design); my $mapDesign = 'data-'.join("data-",@adesign); + 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} }; - my @posc = @{ $hash->{helper}{cspos} }; - my @poserr = @{ $hash->{helper}{lasterror}{positions} }; - 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+)/; + my $mapx = $lonlo-$lonru; + my $mapy = $latlo-$latru; AttrVal($name,'scaleToMeterXY', $hash->{helper}{scaleToMeterLongitude} . ' ' .$hash->{helper}{scaleToMeterLatitude}) =~ /(-?\d+)\s+(-?\d+)/; my $scalx = ( $lonru - $lonlo ) * $1; my $scaly = ( $latlo - $latru ) * $2; - $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"; - } - # CHARGING STATION POSITION 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 ).",'".$pos[ 0 ]{act}."'"; - for (my $i=1;$i<@pos;$i++){ - $posxy .= ",".int( ( $lonlo - $pos[ $i ]{longitude} ) * $picx / $mapx ).",".int( ( $latlo - $pos[ $i ]{latitude} ) * $picy / $mapy ).",'".$pos[ $i ]{act}."'"; - } - - } - - # CHARGING STATION PATH - my $poscxy = int( ( $lonru-$lonlo ) * $picx / $mapx ).",".int( ( $latlo - $latru ) * $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); - } - - } + my $csdata = 'data-csimgpos="'.$csimgpos.'" data-cslon="'.$cslon.'" data-cslat="'.$cslat.'"'; # AREA LIMITS my $arealimits = AttrVal($name,'mowingAreaLimits',''); @@ -491,6 +450,7 @@ sub FW_detailFn_Update { $limi .= ",".int( ( $lonlo - $lixy[ $i ] ) * $picx / $mapx).",".int( ( $latlo-$lixy[$i+1] ) * $picy / $mapy); } } + $limi = 'data-areaLimitsPath="'.$limi.'"'; # PROPERTY LIMITS my $propertylimits = AttrVal($name,'propertyLimits',''); @@ -502,36 +462,119 @@ sub FW_detailFn_Update { $propli .= ",".int(($lonlo-$propxy[$i]) * $picx / $mapx).",".int(($latlo-$propxy[$i+1]) * $picy / $mapy); } } + $propli = 'data-propertyLimitsPath="'.$propli.'"'; - # ERROR MESSAGE - my $errlon = int( ( $lonlo - $hash->{helper}{lasterror}{olLon} ) * $picx / $mapx ); - my $errlat = int( ( $latlo - $hash->{helper}{lasterror}{olLat} ) * $picy / $mapy ); - my $errx = int( $hash->{helper}{lasterror}{sizex} * $picx / -$mapx ); - my $erry = int( $hash->{helper}{lasterror}{sizey} * $picy / $mapy ); - my $errdesc = $hash->{helper}{lasterror}{errordesc}; - my $errdate = $hash->{helper}{lasterror}{errordate}; + # MOWING PATH + my @pos = @{ $hash->{helper}{areapos} }; + # my $posxy = $cslon . "," . $cslat . ",P"; + my $posxy = ''; - # ERROR PATH - my $poserrxy = int( ( $lonru-$lonlo ) / 2 * $picx / $mapx ).",".int( ( $latlo - $latru ) / 2 * $picy / $mapy );; + if ( @pos > 1 ) { - if ( @poserr > 0 ) { + $posxy = int( ( $lonlo-$pos[ 0 ]{longitude} ) * $picx / $mapx ).",".int( ( $latlo-$pos[ 0 ]{latitude} ) * $picy / $mapy ).",".$pos[ 0 ]{act}; - $poserrxy = int( ( $lonlo - $poserr[ 0 ]{longitude} ) * $picx / $mapx ) . "," . int( ( $latlo - $poserr[ 0 ]{latitude} ) * $picy / $mapy ); + for ( my $i = 1; $i < ( @pos > 5000 ? 5000 : @pos ); $i++ ){ + + $posxy .= ",".int( ( $lonlo - $pos[ $i ]{longitude} ) * $picx / $mapx ).",".int( ( $latlo - $pos[ $i ]{latitude} ) * $picy / $mapy ).",".$pos[ $i ]{act}; - for ( my $i = 1; $i < @poserr; $i++ ){ - $poserrxy .= ",".int( ( $lonlo - $poserr[ $i ]{longitude} ) * $picx / $mapx) . "," . int( ( $latlo - $poserr[ $i ]{latitude} ) * $picy / $mapy ); } } - my $erray = "$errlon,$errlat,$errx,$erry,$poserrxy"; - - # Log3 $name, 1, "AutomowerConnectUpdateDetail ( '$name', '$type', '$img', $picx, $picy, $cslon, $cslat, '$csimgpos', $scalx, '$errdesc', [ $posxy ], [ $limi ], [ $propli ], [ $erray ] )"; + $posxy = 'data-mowingPath="'.$posxy.'"'; - map { - ::FW_directNotify("#FHEMWEB:$_", "AutomowerConnectUpdateDetail ( '$name', '$type', '$img', $picx, $picy, $cslon, $cslat, '$csimgpos', $scalx, [ '$errdesc', '$errdate' ], [ $posxy ], [ $limi ], [ $propli ], [ $erray ] )",""); - } devspec2array("TYPE=FHEMWEB"); + + my $ret = ""; + $ret .= ""; + $ret .= "
    "; + $ret .= ""; + $ret .= ""; + $ret .= "
    "; + $hash->{helper}{detailFnFirst} = 1; + InternalTimer( gettimeofday() + 1.5, \&FW_detailFn_Update, $hash, 0 ); + + return $ret; } + return ''; +} + +######################### +sub FW_detailFn_Update { + my ($hash) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + return undef if( AttrVal($name, 'disable', 0) || !AttrVal($name, 'showMap', 1) ); + + my @pos = @{ $hash->{helper}{areapos} }; + my @poserr = @{ $hash->{helper}{lasterror}{positions} }; + + 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; + my $scaly = ( $latlo - $latru ) * $2; + + $picx = int($picx*$zoom); + $picy = int($picy*$zoom); + my $mapx = $lonlo-$lonru; + my $mapy = $latlo-$latru; + + # MOWING PATH + my $posxy = ''; + + if ( @pos > 0 && $hash->{helper}{detailFnNewPos}) { + + $posxy = int( ( $lonlo-$pos[ 0 ]{longitude} ) * $picx / $mapx ).",".int( ( $latlo-$pos[ 0 ]{latitude} ) * $picy / $mapy ).",'".$pos[ 0 ]{act}."'"; + my $imax = ( @pos > 5000 ? @pos - 5000 + $hash->{helper}{detailFnNewPos} : $hash->{helper}{detailFnNewPos} ); + + for ( my $i = 1; $i < $imax; $i++ ){ + + $posxy .= ",".int( ( $lonlo - $pos[ $i ]{longitude} ) * $picx / $mapx ).",".int( ( $latlo - $pos[ $i ]{latitude} ) * $picy / $mapy ).",'".$pos[ $i ]{act}."'"; + + } + + } + + # ERROR MESSAGE + my $errdesc = $hash->{helper}{lasterror}{errordesc}; + my $errdate = $hash->{helper}{lasterror}{errordate}; + + # ERROR PATH + my $poserrxy = int( ( $lonru-$lonlo ) / 2 * $picx / $mapx ).",".int( ( $latlo - $latru ) / 2 * $picy / $mapy );; + + if ( @poserr > 0 ) { + + $poserrxy = int( ( $lonlo - $poserr[ 0 ]{longitude} ) * $picx / $mapx ) . "," . int( ( $latlo - $poserr[ 0 ]{latitude} ) * $picy / $mapy ); + + for ( my $i = 1; $i < @poserr; $i++ ){ + $poserrxy .= ",".int( ( $lonlo - $poserr[ $i ]{longitude} ) * $picx / $mapx) . "," . int( ( $latlo - $poserr[ $i ]{latitude} ) * $picy / $mapy ); + } + + } + + # Log3 $name, 1, "AutomowerConnectUpdateDetail ( '$name', '$type', $detailFnFirst, $picx, $picy, $scalx, [ '$errdesc', '$errdate' ], [ $posxy ], [ $poserrxy ] )"; + my $detailFnFirst = $hash->{helper}{detailFnFirst}; + + map { + ::FW_directNotify("#FHEMWEB:$_", "AutomowerConnectUpdateDetail ( '$name', '$type', $detailFnFirst, $picx, $picy, $scalx, [ '$errdesc', '$errdate' ], [ $posxy ], [ $poserrxy ] )",""); + } devspec2array("TYPE=FHEMWEB"); + + $hash->{helper}{detailFnFirst} = 0; return undef; } @@ -669,7 +712,7 @@ sub AlignArray { my $name = $hash->{NAME}; my $act = $hash->{helper}{mower}{attributes}{mower}{activity}; my $actold = $hash->{helper}{mowerold}{attributes}{mower}{activity}; - my $cnt = @{ $hash->{helper}{mower}{attributes}{positions} }; + my $cnt = @{ $hash->{helper}{mower}{attributes}{positions} }; my $tmp = []; if ( $cnt > 0 ) { @@ -744,10 +787,6 @@ sub AlignArray { } - isErrorThanPrepare( $hash, $tmp ); - - resetLastErrorIfCorrected($hash); - $hash->{helper}{newdatasets} = $cnt; return undef; @@ -755,33 +794,27 @@ sub AlignArray { ######################### sub isErrorThanPrepare { - my ( $hash, $poshash ) = @_; + my ( $hash ) = @_; + my $name = $hash->{NAME}; + if ( $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} ) { - if ( ( $hash->{helper}{lasterror}{timestamp} != $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} ) && @$poshash) { - - my $minLon = minNum( 180, $poshash->[ 0 ]{longitude} ); - my $maxLon = maxNum( -180, $poshash->[ 0 ]{longitude} ); - my $minLat = minNum( 90, $poshash->[ 0 ]{latitude} ); - my $maxLat = maxNum( -90, $poshash->[ 0 ]{latitude} ); - - for ( @{ $poshash } ) { - $minLon = minNum( $minLon, $_->{longitude} ); - $maxLon = maxNum( $maxLon, $_->{longitude} ); - $minLat = minNum( $minLat, $_->{latitude} ); - $maxLat = maxNum( $maxLat, $_->{latitude} ); - } + if ( ( $hash->{helper}{lasterror}{timestamp} != $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} ) && @{ $hash->{helper}{areapos} } > 1) { my $ect = $hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp}; - $hash->{helper}{lasterror}{positions} = dclone $poshash; + $hash->{helper}{areapos}[ 0 ]{act} = 'N'; + $hash->{helper}{areapos}[ 1 ]{act} = 'N'; + $hash->{helper}{lasterror}{positions} = [ dclone( $hash->{helper}{areapos}[ 0 ] ), dclone( $hash->{helper}{areapos}[ 1 ] ) ]; $hash->{helper}{lasterror}{timestamp} = $ect; - $hash->{helper}{lasterror}{olLon} = $minLon; - $hash->{helper}{lasterror}{olLat} = $maxLat; - $hash->{helper}{lasterror}{sizex} = sprintf('%.7f',$maxLon - $minLon); - $hash->{helper}{lasterror}{sizey} = sprintf('%.7f',$maxLat - $minLat); my $errc = $hash->{helper}{mower}{attributes}{mower}{errorCode}; $hash->{helper}{lasterror}{errordesc} = $::FHEM::Devices::AMConnect::Common::errortable->{$errc}; $hash->{helper}{lasterror}{errordate} = FmtDateTimeGMT( $ect / 1000 ); + $hash->{helper}{lasterror}{errorzone} = $hash->{helper}{currentZone} if ( defined( $hash->{helper}{currentZone} ) ); + + my $tmp = dclone( $hash->{helper}{lasterror} ); + unshift ( @{ $hash->{helper}{errorstack} }, $tmp ); + pop ( @{ $hash->{helper}{errorstack} } ) if ( @{ $hash->{helper}{errorstack} } > 5 ); + ::FHEM::Devices::AMConnect::Common::FW_detailFn_Update ($hash); } @@ -792,17 +825,15 @@ sub isErrorThanPrepare { ######################### sub resetLastErrorIfCorrected { my ( $hash ) = @_; + my $name = $hash->{NAME}; if (!$hash->{helper}{mower}{attributes}{mower}{errorCodeTimestamp} && $hash->{helper}{lasterror}{timestamp} ) { $hash->{helper}{lasterror}{positions} = []; $hash->{helper}{lasterror}{timestamp} = 0; - $hash->{helper}{lasterror}{olLon} = 0; - $hash->{helper}{lasterror}{olLat} = 0; - $hash->{helper}{lasterror}{sizex} = 0; - $hash->{helper}{lasterror}{sizey} = 0; $hash->{helper}{lasterror}{errordesc} = '-'; $hash->{helper}{lasterror}{errordate} = ''; + ::FHEM::Devices::AMConnect::Common::FW_detailFn_Update ($hash); } @@ -1101,7 +1132,7 @@ sub fillReadings { readingsBulkUpdateIfChanged($hash, "planner_nextStart", $tstamp ? $timestamp : '-' ); $pref = 'statistics'; - readingsBulkUpdateIfChanged($hash, $pref."_numberOfCollisions", $hash->{helper}->{mower}{attributes}{$pref}{numberOfCollisions} ); + readingsBulkUpdateIfChanged($hash, $pref."_numberOfCollisions", '(' . $hash->{helper}{statistics}{lastDayCollisions} . '/' . $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} ); @@ -1116,19 +1147,6 @@ sub fillReadings { return undef; } -######################### -sub initStatistics { - my ( $hash ) = @_; - my ( @tim ) = localtime(time); - $tim[ 0 ] = 0; - $tim[ 1 ] = 0; - $tim[ 2 ] = 0; - my $ret = ::timelocal( @tim ) + 86417; - RemoveInternalTimer( $hash, \&calculateStatistics ); - InternalTimer( $ret, \&calculateStatistics, $hash, 0 ); -return undef; -} - ######################### sub calculateStatistics { my ( $hash ) = @_; @@ -1138,6 +1156,7 @@ sub calculateStatistics { $hash->{helper}{statistics}{lastDayTrack} = $hash->{helper}{statistics}{currentDayTrack}; $hash->{helper}{statistics}{lastDayArea} = $hash->{helper}{statistics}{currentDayArea}; $hash->{helper}{statistics}{lastDayTime} = $hash->{helper}{statistics}{currentDayTime}; + $hash->{helper}{statistics}{lastDayCollisions} = $hash->{helper}{mower}{attributes}{statistics}{numberOfCollisions} - $hash->{helper}{mowerold}{attributes}{statistics}{numberOfCollisions}; $hash->{helper}{statistics}{currentWeekTrack} += $hash->{helper}{statistics}{currentDayTrack}; $hash->{helper}{statistics}{currentWeekArea} += $hash->{helper}{statistics}{currentDayArea}; $hash->{helper}{statistics}{currentWeekTime} += $hash->{helper}{statistics}{currentDayTime}; @@ -1200,7 +1219,6 @@ sub calculateStatistics { } - initStatistics( $hash ); return undef; } @@ -1236,6 +1254,8 @@ sub listStatisticsData { $cnt++;$ret .= ' $hash->{helper}{statistics}{lastWeekTrack}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastWeekTrack} ) . ' m '; $cnt++;$ret .= ' $hash->{helper}{statistics}{lastWeekArea}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastWeekArea} ) . ' qm '; $cnt++;$ret .= ' $hash->{helper}{statistics}{lastWeekTime}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastWeekTime} ) . ' s '; + + $cnt++;$ret .= ' $hash->{helper}{statistics}{lastDayCollisions}   ' . sprintf( "%.0f", $hash->{helper}{statistics}{lastDayCollisions} ) . ' '; if ( AttrVal($name, 'mapZones', 0) && defined( $hash->{helper}{mapZones} ) ) { @@ -1369,6 +1389,37 @@ sub listMowerData { } } +######################### +sub listErrorStack { + my ( $hash ) = @_; + my $name = $hash->{NAME}; + my $cnt = 0; + my $ret = ''; + if ( $::init_done && defined( $hash->{helper}{mower}{type} ) && @{ $hash->{helper}{errorstack} } ) { + + $ret .= ''; + $ret .= ''; + + $ret .= ''; + + for ( my $i = 0; $i < @{ $hash->{helper}{errorstack} }; $i++ ) { + + $cnt++; $ret .= ''; + + } + + $ret .= '
    Last Errors
    Timestamp Description  Zone   Position
    ' . $hash->{helper}{errorstack}[$i]{errordate} . ' ' . $hash->{helper}{errorstack}[$i]{errordesc} . ' ' . $hash->{helper}{errorstack}[$i]{errorzone} . ' ' . $hash->{helper}{errorstack}[$i]{positions}[0]{longitude} . ' / ' . $hash->{helper}{errorstack}[$i]{positions}[0]{latitude} . '
    '; + $ret .= ''; + + return $ret; + + } else { + + return '
    No error in stack.
    '; + + } +} + ######################### sub listInternalData { my ( $hash ) = @_; diff --git a/fhem/www/pgm2/automowerconnect.js b/fhem/www/pgm2/automowerconnect.js index ee866ded4..34b8d8fa5 100644 --- a/fhem/www/pgm2/automowerconnect.js +++ b/fhem/www/pgm2/automowerconnect.js @@ -2,7 +2,6 @@ FW_version["automowerconnect.js"] = "$Id$"; function AutomowerConnectShowError( ctx, div, dev, picx, picy, errdesc, erray ) { -//~ log( 'AutomowerConnectShowError: ' + erray[0]+' '+erray[1]+' '+erray[2]+' '+erray[3]+' '+erray[4]+' '+erray[5]); // ERROR BANNER ctx.beginPath(); ctx.fillStyle = div.getAttribute( 'data-errorBackgroundColor' );; @@ -34,17 +33,11 @@ function AutomowerConnectShowError( ctx, div, dev, picx, picy, errdesc, erray ) } ctx.stroke(); + //~ log('AutomowerConnectShowError: erray '+ erray[2]+', '+erray[3]+', '+erray[0]+', '+erray[1] ); if ( erray[ 0 ] && erray[ 1 ] && erray.length > 3) { - AutomowerConnectIcon( ctx, erray[ 4 ], erray[ 5 ], 'top', 'E' ); - - } - - if ( erray.length > 8 ) { - - var pos = erray.slice(4); - AutomowerConnectDrawPath ( ctx, div, pos, 'errorPath' ); + AutomowerConnectIcon( ctx, erray[ 0 ], erray[ 1 ], AutomowerConnectTor ( erray[2], erray[3], erray[0], erray[1] ), 'E' ); } @@ -260,11 +253,11 @@ function AutomowerConnectTor ( x0, y0, x1, y1 ) { //~ log ('ret: ' + ret); return ret; } -//AutomowerConnectUpdateDetail (, , , , ,, , , , , ) -function AutomowerConnectUpdateDetail (dev, type, imgsrc, picx, picy, csx, csy, csrel, scalx, errdesc, pos, lixy, plixy, erray) { +//AutomowerConnectUpdateDetail (, , , , ,, , , ) +function AutomowerConnectUpdateDetail (dev, type, detailfnfirst, picx, picy, scalx, errdesc, pos, erray) { const colorat = { "U" : "otherActivityPath", - "N" : "otherActivityPath", + "N" : "errorPath", "S" : "otherActivityPath", "P" : "chargingStationPath", "C" : "chargingStationPath", @@ -275,20 +268,59 @@ function AutomowerConnectUpdateDetail (dev, type, imgsrc, picx, picy, csx, csy, // log('pos.length '+pos.length+' lixy.length '+lixy.length+', scalx '+scalx ); // log('loop: Start '+ type+' '+dev ); if (FW_urlParams.detail == dev || 1) { -// if (FW_urlParams.detail == dev) { - const canvas = document.getElementById(type+'_'+dev+'_canvas'); + const canvas_0 = document.getElementById(type+'_'+dev+'_canvas_0'); + const canvas = document.getElementById(type+'_'+dev+'_canvas_1'); const div = document.getElementById(type+'_'+dev+'_div'); - if (canvas) { -// log('loop: canvas true '+ type+' '+dev ); - const ctx = canvas.getContext('2d'); - ctx.clearRect(0, 0, canvas.width, canvas.height); + + if ( canvas && canvas_0 ) { + +// log('loop: canvas && canvas_0 true '+ type+' '+dev ); + + if ( detailfnfirst ) { + + const ctx0 = canvas_0.getContext( '2d' ); + ctx0.clearRect( 0, 0, canvas.width, canvas.height ); + const ctx = canvas.getContext( '2d' ); + + // draw area limits + const lixy = div.getAttribute( 'data-areaLimitsPath' ).split(","); + if ( lixy.length > 0 ) AutomowerConnectLimits( ctx0, div, lixy, 'area' ); // draw property limits - if ( lixy.length > 0 ) AutomowerConnectLimits( ctx, div, lixy, 'area' ); - // draw area limits - if ( plixy.length > 0 ) AutomowerConnectLimits( ctx, div, plixy, 'property' ); + const plixy = div.getAttribute( 'data-propertyLimitsPath' ).split( "," ); + if ( plixy.length > 0 ) AutomowerConnectLimits( ctx0, div, plixy, 'property' ); + // draw scale - AutomowerConnectScale( ctx, picx, picy, scalx ); + AutomowerConnectScale( ctx0, picx, picy, scalx ); + + // draw charging station + var csx = div.getAttribute( 'data-cslon' ); + var csy = div.getAttribute( 'data-cslat' ); + var csrel = div.getAttribute( 'data-csimgpos' ); + AutomowerConnectIcon( ctx0, csx , csy, csrel, 'CS' ); + + } + + const ctx = canvas.getContext( '2d' ); + ctx.clearRect( 0, 0, canvas.width, canvas.height ); + const attrpath = div.getAttribute( 'data-mowingPath' ); + var oldpath = ""; + + if ( attrpath ) { + + oldpath = attrpath.split( "," ); + pos.push( ...oldpath ); + + while ( pos.length > 5000 ) { + + pos.pop(); + + } + + } + + div.setAttribute( 'data-mowingPath', pos.join( "," ) ); + if ( pos.length > 3 ) { // draw mowing path color @@ -311,22 +343,19 @@ function AutomowerConnectUpdateDetail (dev, type, imgsrc, picx, picy, csx, csy, } - // draw charging station - AutomowerConnectIcon( ctx, csx, csy, csrel, 'CS' ); - // draw error icon and path - if ( errdesc[0] != '-' ) AutomowerConnectShowError( ctx, div, dev, picx, picy, errdesc, erray ); -// } -// img.src = imgsrc; + // draw error icon and path + if ( errdesc[0] != '-' ) AutomowerConnectShowError( ctx, div, dev, picx, picy, errdesc, erray ); + } else { setTimeout(()=>{ // log('loop: canvas false '+ type+' '+dev ); - AutomowerConnectUpdateDetail (dev, type, imgsrc, picx, picy, csx, csy, csrel, scalx, errdesc, pos, lixy, plixy, erray); + AutomowerConnectUpdateDetail (dev, type, detailfnfirst, picx, picy, scalx, errdesc, pos, erray); }, 100); } } else { setTimeout(()=>{ // log('loop: detail false '+ type+' '+dev ); - AutomowerConnectUpdateDetail (dev, type, imgsrc, picx, picy, csx, csy, csrel, scalx, errdesc, pos, lixy, plixy, erray); + AutomowerConnectUpdateDetail (dev, type, detailfnfirst, picx, picy, scalx, errdesc, pos, erray); }, 100); } }