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
- - mower path (activity MOWING): red
- - path in CS (activity CHARGING,PARKED_IN_CS): grey
+ - mower path for activity MOWING: red
+ - path in CS, activity CHARGING,PARKED_IN_CS: grey
+ - path for activity LEAVING: green
+ - path for activityGOING_HOME: blue
- path for interval with error (all activities with error): kind of magenta
- - all other activities: green
+ - all other activities: grey
@@ -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:
- - Pfad beim mähen (Aktivität MOWING): rot
- - In der Ladestation (Aktivität CHARGING,PARKED_IN_CS): grau
+ - Pfad beim mähen, Aktivität MOWING: rot
+ - In der Ladestation, Aktivität CHARGING,PARKED_IN_CS: grau
+ - Pfad für die Aktivität LEAVING: grün
+ - Pfad für Aktivität GOING_HOME: blau
- Pfad eines Intervalls mit Fehler (alle Aktivitäten with error): Eine Art Magenta
- - Pfad aller anderen Aktivitäten: grün
+ - Pfad aller anderen Aktivitäten: grau
@@ -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 .= 'Last Errors';
+
+ $ret .= '';
+
+ for ( my $i = 0; $i < @{ $hash->{helper}{errorstack} }; $i++ ) {
+
+ $cnt++; $ret .= ' ' . $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 .= '
';
+ $ret .= '';
+
+ return $ret;
+
+ } else {
+
+ return '';
+
+ }
+}
+
#########################
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);
}
}