diff --git a/fhem/CHANGED b/fhem/CHANGED index 6f7864ad2..51ae260ea 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. + - new: 74_AutomowerConnect: Control Automower + Host + - new: 75_AutomowerConnectDevice: Control Automower Client + - new: automowerconnect.js: Frontend helper - change: 93_Log2Syslog: Adapt to change Logging in fhem.pl, Forum:#131790 - change: 93_DbLog: Syntaxcheck of DbLogValueFn attribute, Forum:#131777 - change: 72_FRITZBOX: our Deklaration nach my (package global) diff --git a/fhem/FHEM/74_AutomowerConnect.pm b/fhem/FHEM/74_AutomowerConnect.pm new file mode 100644 index 000000000..cad0350c0 --- /dev/null +++ b/fhem/FHEM/74_AutomowerConnect.pm @@ -0,0 +1,1762 @@ +############################################################################### +# +# 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. +# +# +# $Id$ +# +# +# Husqvarnas Open API is used +# based on some ideas from HusqvarnaAutomower and BOTVAC module +# +################################################################################ + +package FHEM::AutomowerConnect; +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 FHEM::Core::Authentication::Passwords 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 + Log3 + Log + readingFnAttributes + readingsBeginUpdate + readingsBulkUpdate + readingsBulkUpdateIfChanged + readingsDelete + readingsEndUpdate + ReadingsNum + readingsSingleUpdate + ReadingsVal + RemoveInternalTimer + setKeyValue + defs + attr + modules + devspec2array + ) + ); +} + +GP_Export( + qw( + Initialize + ) +); + +my $missingModul = ""; + +eval "use JSON;1" or $missingModul .= "JSON "; +require HttpUtils; + +use constant AUTHURL => 'https://api.authentication.husqvarnagroup.dev/v1'; +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->{AttrFn} = \&Attr; + $hash->{AttrList} = "interval " . + "disable:1,0 " . + "mapImagePath " . + "mapImageWidthHeight " . + "mapImageCoordinatesToRegister:textField-long " . + "mapImageCoordinatesUTM:textField-long " . + "mapImageZoom " . + "showMap:1,0 " . + "chargingStationCoordinates " . + "chargingStationImagePosition:left,top,right,bottom,center " . + "scaleToMeterXY " . + "mowerCuttingWidth " . + "mowerSchedule:textField-long " . + "mowingAreaLimits:textField-long " . + "propertyLimits:textField-long " . + "numberOfWayPointsToDisplay " . + $readingFnAttributes; + + $::data{FWEXT}{AutomowerConnect}{SCRIPT} = "automowerconnect.js"; + + return undef; +} + + +############################################################## +# +# DEFINE +# +############################################################## + +sub Define{ + my ( $hash, $def ) = @_; + my @val = split( "[ \t]+", $def ); + my $name = $val[0]; + my $type = $val[1]; + my $iam = "$type $name Define:"; + + return "$iam too few parameters: define $type []" if( @val < 3 ) ; + return "$iam Cannot define $type device. Perl modul $missingModul is missing." if ( $missingModul ); + + my $client_id =$val[2]; + my $mowerNumber = $val[3] ? $val[3] : 0; + + %$hash = (%$hash, + helper => { + passObj => FHEM::Core::Authentication::Passwords->new($type), + interval => 600, + mowerNumber => $mowerNumber, + scaleToMeterLongitude => 67425, + scaleToMeterLatitude => 108886, + client_id => $client_id, + grant_type => 'client_credentials', + MAP_PATH => '', + MAP_MIME => '', + MAP_CACHE => '', + UNKNOWN => { + arrayName => '', + maxLength => 0, + callFn => '' + }, + NOT_APPLICABLE => { + arrayName => '', + maxLength => 0, + callFn => '' + }, + MOWING => { + arrayName => 'areapos', + maxLength => 500, + maxLengthDefault => 500, + callFn => \&AreaStatistics + }, + GOING_HOME => { + arrayName => '', + maxLength => 0, + callFn => '' + }, + CHARGING => { + arrayName => 'cspos', + maxLength => 500, + callFn => \&ChargingStationPosition + }, + LEAVING => { + arrayName => '', + maxLength => 0, + callFn => '' + }, + PARKED_IN_CS => { + arrayName => 'cspos', + maxLength => 50, + callFn => \&ChargingStationPosition + }, + STOPPED_IN_GARDEN => { + arrayName => '', + maxLength => 0, + callFn => '' + } + } + ); + + # my $helper = retrieve( $name.'_helper' ); + # my $hashhelper = $hash->{helper}; + # %$hashhelper = (%$helper, %$hashhelper); + +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; + + $attr{$name}{room} = $type if( !defined( $attr{$name}{room} ) ); + $attr{$name}{icon} = 'automower' if( !defined( $attr{$name}{icon} ) ); + + RemoveInternalTimer($hash); + InternalTimer( gettimeofday() + 2, \&APIAuth, $hash, 1); + InternalTimer( gettimeofday() + 30, \&readMap, $hash, 0); + AddExtension( $name, \&GetMap, "$type/$name/map" ); + + readingsSingleUpdate( $hash, 'state', 'defined', 1 ); + + return undef; + +} + + +############################################################## +# +# API AUTHENTICATION +# +############################################################## + +sub APIAuth { + my ($hash, $update) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name APIAuth:"; + + if ( AttrVal($name, 'disable', '') ) { + + Log3 $name, 3, "$iam disabled"; + return undef + + } + + if ( !$update && $::init_done ) { + + if ( ReadingsVal( $name,'.access_token','' ) and gettimeofday() < (ReadingsVal($name, '.expires', 0) - $hash->{helper}{interval} - 60)) { + + readingsSingleUpdate( $hash, 'state', 'update', 1 ); + getMower( $hash ); + + } else { + + my $client_id = $hash->{helper}->{client_id}; + my $client_secret = $hash->{helper}->{passObj}->getReadPassword($name); + my $grant_type = $hash->{helper}->{grant_type}; + + my $header = "Content-Type: application/x-www-form-urlencoded\r\nAccept: application/json"; + my $data = 'grant_type=' . $grant_type.'&client_id=' . $client_id . '&client_secret=' . $client_secret; + ::HttpUtils_NonblockingGet({ + url => AUTHURL . '/oauth2/token', + timeout => 5, + hash => $hash, + method => 'POST', + header => $header, + data => $data, + callback => \&APIAuthResponse, + }); + readingsSingleUpdate( $hash, 'state', 'authentification', 1 ); + } + } else { + RemoveInternalTimer( $hash, \&APIAuth); + InternalTimer(gettimeofday() + 20, \&APIAuth, $hash, 0); + + } + return undef; +} + +######################### +sub APIAuthResponse { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name APIAuthResponse:"; + + if($err) { + + Log3 $name, 2, "$iam error [$err], data [$data] for url $param->{url}"; + readingsSingleUpdate( $hash, 'state', 'error', 1 ); + + } elsif( $data ) { + + my $result = eval { decode_json($data) }; + if ($@) { + Log3 $name, 2, "$iam JSON error [ $@ ]"; + readingsSingleUpdate( $hash, 'state', 'error', 1 ); + return undef; + } + + if ($result->{errors} || $result->{message}) { + Log3 $name, 2, "$iam error [ $data ]"; + readingsSingleUpdate( $hash, 'state', 'error', 1 ); + + } else { + Log3 $name, 5, "$iam $data"; + + $hash->{helper}->{auth} = $result; + + # Update readings + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged($hash,'.access_token',$hash->{helper}{auth}{access_token},0 ); + 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(); + 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 ); + readingsBulkUpdateIfChanged($hash,'.provider',$hash->{helper}{auth}{provider} ); + + my $tok = substr($hash->{helper}{auth}{access_token},0,15).'...'.substr($hash->{helper}{auth}{access_token},-15); + readingsBulkUpdateIfChanged($hash,'api_token',$tok ); + + my $expire_date = FmtDateTime($hash->{helper}{auth}{expires}); + readingsBulkUpdateIfChanged($hash,'api_token_expires',$expire_date ); + readingsBulkUpdateIfChanged($hash,'state', 'authenticated'); + readingsBulkUpdateIfChanged($hash,'mower_commandStatus', 'cleared'); + readingsEndUpdate($hash, 1); + + getMower( $hash ); + } + } +return undef; +} + + +############################################################## +# +# GET MOWERS +# +############################################################## + +sub getMower { + my ($hash) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name getMower:"; + + if ( AttrVal($name, 'disable', '') ) { + Log3 $name, 3, "$iam disabled"; + return undef + } + + my $access_token = ReadingsVal($name,".access_token",""); + my $provider = ReadingsVal($name,".provider",""); + my $client_id = $hash->{helper}->{client_id}; + + my $header = "Accept: application/vnd.api+json\r\nX-Api-Key: " . $client_id . "\r\nAuthorization: Bearer " . $access_token . "\r\nAuthorization-Provider: " . $provider; + Log3 $name, 5, "$iam header [ $header ]"; + + ::HttpUtils_NonblockingGet({ + url => APIURL . "/mowers", + timeout => 5, + hash => $hash, + method => "GET", + header => $header, + callback => \&getMowerResponse, + }); + + + return undef; +} + +######################### +sub getMowerResponse { + + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name getMowerResponse:"; + my $mowerNumber = $hash->{helper}{mowerNumber}; + + if($err ne "") { + + Log3 $name, 2, "$iam request error ".$param->{url}." - $err"; + + } elsif ( $data ) { + if ( $data eq "[]" ) { + + Log3 $name, 2, "$iam Please register an automower first"; + + } else { + + #Log3 $name, 5, $data; + + my $result = eval { decode_json($data) }; + if ($@) { + Log3( $name, 2, "$iam - JSON error while request: $@"); + return undef; + } + if ($result->{message}) { + Log3( $name, 2, "$iam - API error while request: $data"); + return undef; + } + $hash->{helper}{mowers} = $result->{data}; + my $maxMower = @{$hash->{helper}{mowers}}; + if ($maxMower <= $mowerNumber || $mowerNumber < 0 ) { + + Log3 $name, 2, "$iam mower number $mowerNumber not available. Change definition of $name."; + return undef; + + } + my $foundMower .= '0 => '.$hash->{helper}{mowers}[0]{attributes}{system}{name}; + for (my $i = 1; $i < $maxMower; $i++) { + $foundMower .= ' | '.$i.' => '.$hash->{helper}{mowers}[$i]{attributes}{system}{name}; + } + Log3 $name, 5, "$iam found $foundMower "; + + if ( defined ($hash->{helper}{mower}{id}) ){ + + $hash->{helper}{mowerold} = dclone( $hash->{helper}{mower} ); + + } else { + + $hash->{helper}{mowerold} = dclone( $hash->{helper}{mowers}[$mowerNumber] ); + + $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; + } + + $hash->{helper}{mower} = dclone( $hash->{helper}{mowers}[$mowerNumber] ); + 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)); + + } + + # Update readings + readingsBeginUpdate($hash); + + readingsBulkUpdateIfChanged($hash, "batteryPercent", $hash->{helper}{mower}{attributes}{battery}{batteryPercent} ); + readingsBulkUpdateIfChanged($hash, 'api_MowerFound', $foundMower ); + 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); + 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}; + 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}; + $model =~ s/AUTOMOWER./AUTOMOWER®/; + readingsBulkUpdateIfChanged($hash, $pref."_model", $model ); + readingsBulkUpdateIfChanged($hash, $pref."_serialNumber", $hash->{helper}{mower}{attributes}{$pref}{serialNumber} ); + $pref = 'planner'; + readingsBulkUpdateIfChanged($hash, "planner_restrictedReason", $hash->{helper}{mower}{attributes}{$pref}{restrictedReason} ); + readingsBulkUpdateIfChanged($hash, "planner_overrideAction", $hash->{helper}{mower}{attributes}{$pref}{override}{action} ); + + $tstamp = $hash->{helper}{mower}{attributes}{$pref}{nextStartTimestamp}; + $timestamp = FmtDateTime($tstamp/1000); + readingsBulkUpdateIfChanged($hash, "planner_nextStart", $tstamp ? $timestamp : '-' ); + $pref = 'statistics'; + readingsBulkUpdateIfChanged($hash, $pref."_numberOfChargingCycles", $hash->{helper}->{mower}{attributes}{$pref}{numberOfChargingCycles} ); + readingsBulkUpdateIfChanged($hash, $pref."_totalCuttingTime", $hash->{helper}->{mower}{attributes}{$pref}{totalCuttingTime} ); + readingsBulkUpdateIfChanged($hash, $pref."_totalChargingTime", $hash->{helper}->{mower}{attributes}{$pref}{totalChargingTime} ); + readingsBulkUpdateIfChanged($hash, $pref."_totalSearchingTime", $hash->{helper}->{mower}{attributes}{$pref}{totalSearchingTime} ); + readingsBulkUpdateIfChanged($hash, $pref."_numberOfCollisions", $hash->{helper}->{mower}{attributes}{$pref}{numberOfCollisions} ); + readingsBulkUpdateIfChanged($hash, $pref."_totalRunningTime", $hash->{helper}->{mower}{attributes}{$pref}{totalRunningTime} ); + $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."_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."_lastLatitude", $hash->{helper}{mower}{attributes}{$pref}[0]{latitude} ); + readingsBulkUpdateIfChanged($hash, $pref."_lastLongitude", $hash->{helper}{mower}{attributes}{$pref}[0]{longitude} ); + readingsBulkUpdateIfChanged($hash, 'state', 'connected' ); + + my @time = localtime(); + my $secs = ( $time[2] * 3600 ) + ( $time[1] * 60 ) + $time[0]; + my $interval = $hash->{helper}->{interval}; + # do at midnight + if ( $secs <= $interval ) { + + readingsBulkUpdateIfChanged( $hash, 'statistics_lastDayTrack', ReadingsNum( $name, 'statistics_currentDayTrack', 0 )); + readingsBulkUpdateIfChanged( $hash, 'statistics_lastDayArea', ReadingsNum( $name, 'statistics_currentDayArea', 0 )); + readingsBulkUpdateIfChanged( $hash, 'statistics_currentWeekTrack', ReadingsNum( $name, 'statistics_currentWeekTrack', 0 ) + ReadingsNum( $name, 'statistics_currentDayTrack', 0 )); + readingsBulkUpdateIfChanged( $hash, 'statistics_currentWeekArea', ReadingsNum( $name, 'statistics_currentWeekArea', 0 ) + ReadingsNum( $name, 'statistics_currentDayArea', 0 )); + readingsBulkUpdateIfChanged( $hash, 'statistics_currentDayTrack', 0, 0); + readingsBulkUpdateIfChanged( $hash, 'statistics_currentDayArea', 0, 0); + # do on mondays + if ( $time[6] == 1 && $secs <= $interval ) { + + readingsBulkUpdateIfChanged( $hash, 'statistics_lastWeekTrack', ReadingsNum( $name, 'statistics_currentWeekTrack', 0 )); + readingsBulkUpdateIfChanged( $hash, 'statistics_lastWeekArea', ReadingsNum( $name, 'statistics_currentWeekArea', 0 )); + readingsBulkUpdateIfChanged( $hash, 'statistics_currentWeekTrack', 0, 0); + readingsBulkUpdateIfChanged( $hash, 'statistics_currentWeekArea', 0, 0); + + } + } + readingsEndUpdate($hash, 1); + + RemoveInternalTimer( $hash, \&APIAuth ); + InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 ); + + } + + } + + return undef; + +} + + +############################################################## +# +# SEND COMMAND +# +############################################################## + +sub sendCMD { + my ($hash,@cmd) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name sendCMD:"; + + if ( AttrVal($name, 'disable', '') ) { + Log3 $name, 3, "$iam disabled"; + return undef + } + + 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 $json = ''; + my $post = ''; + + +my $header = "Accept: application/vnd.api+json\r\nX-Api-Key: ".$client_id."\r\nAuthorization: Bearer " . $token . "\r\nAuthorization-Provider: " . $provider . "\r\nContent-Type: application/vnd.api+json"; + + + if ($cmd[0] eq "ParkUntilFurtherNotice") { $json = '{"data":{"type":"'.$cmd[0].'"}}'; $post = 'actions' } + elsif ($cmd[0] eq "ParkUntilNextSchedule") { $json = '{"data": {"type":"'.$cmd[0].'"}}'; $post = 'actions' } + elsif ($cmd[0] eq "ResumeSchedule") { $json = '{"data": {"type":"'.$cmd[0].'"}}'; $post = 'actions' } + elsif ($cmd[0] eq "Pause") { $json = '{"data": {"type":"'.$cmd[0].'"}}'; $post = 'actions' } + elsif ($cmd[0] eq "Park") { $json = '{"data": {"type":"'.$cmd[0].'","attributes":{"duration":'.$cmd[1].'}}}'; $post = 'actions' } + elsif ($cmd[0] eq "Start") { $json = '{"data": {"type":"'.$cmd[0].'","attributes":{"duration":'.$cmd[1].'}}}'; $post = 'actions' } + elsif ($cmd[0] eq "headlight") { $json = '{"data": {"type":"settings","attributes":{"'.$cmd[0].'": {"mode": "'.$cmd[1].'"}}}}'; $post = 'settings' } + elsif ($cmd[0] eq "cuttingHeight") { $json = '{"data": {"type":"settings","attributes":{"'.$cmd[0].'": '.$cmd[1].'}}}'; $post = 'settings' } + elsif ($cmd[0] eq "sendScheduleFromAttributeToMower" && AttrVal( $name, 'mowerSchedule', '')) { + + my $perl = eval { decode_json (AttrVal( $name, 'mowerSchedule', '')) }; + if ($@) { + return "$iam decode error: $@ \n $perl"; + } + my $jsonSchedule = eval { encode_json ($perl) }; + if ($@) { + return "$iam encode error: $@ \n $json"; + } + $json = '{"data":{"type": "calendar","attributes":{"tasks":'.$jsonSchedule.'}}}'; + $post = 'calendar'; + } + + Log3 $name, 5, "$iam $header \n $cmd[0] \n $json"; + + ::HttpUtils_NonblockingGet({ + url => APIURL . "/mowers/". $mower_id . "/".$post, + timeout => 10, + hash => $hash, + method => "POST", + header => $header, + data => $json, + callback => \&CMDResponse, + }); + +} + +######################### +sub CMDResponse { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name CMDResponse:"; + + if($err ne "") { + readingsSingleUpdate( $hash, 'state', 'error', 1 ); + Log3 $name, 2, "$iam error while requesting ".$param->{url}." - $err"; + + } elsif($data ne "") { + + my $result = eval { decode_json($data) }; + if ($@) { + Log3( $name, 2, "$iam - JSON error while request: $@"); + return; + } + + $hash->{helper}{CMDResponse} = $result; + if ($result->{message}) { + readingsSingleUpdate( $hash, 'state', 'error', 1 ); + Log3 $name, 2, "$iam" . $data; + $hash->{helper}->{mower_commandStatus} = $result->{message}; + + } elsif ($result->{errors}) { + Log3 $name, 2, "$iam" . $data; + readingsSingleUpdate( $hash, 'state', 'error', 1 ); + $hash->{helper}->{mower_commandStatus} = 'ERROR - '. $result->{errors}[0]{title}; + } elsif ($result->{data}) { + Log3 $name, 5, $data; + if ( ref ($result->{data}) eq 'ARRAY') { + $hash->{helper}->{mower_commandStatus} = 'OK - '. $result->{data}[0]{type}; + } else { + $hash->{helper}->{mower_commandStatus} = 'OK - '. $result->{data}{type}; + } + + } + + readingsSingleUpdate($hash, 'mower_commandStatus', $hash->{helper}->{mower_commandStatus} ,1); + + } + +} + +######################### +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; + + } +} + +######################### +sub Set { + my ($hash,@val) = @_; + my $type = $hash->{TYPE}; + + return "$type $hash->{NAME} Set: needs at least an argument" if ( @val < 2 ); + + my ($name,$setName,$setVal,$setVal2,$setVal3) = @val; + my $iam = "$type $name Set:"; + + Log3 $name, 4, "$iam called with $setName " . ($setVal ? $setVal : "") if ($setName !~ /^(\?|client_secret)$/); + + if ( $setName eq 'getUpdate' ) { + RemoveInternalTimer($hash, \&APIAuth); + APIAuth($hash); + return undef; + + } elsif ( $setName eq 'chargingStationPositionToAttribute' ) { + + my ($xm, $ym, $n) = split(/,\s/,ReadingsVal($name,'status_calcChargingStationPositionXYn','10.1165, 51.28, 0')); + CommandAttr($hash,"$name chargingStationCoordinates $xm $ym"); + return undef; + + } elsif ( ReadingsVal( $name, 'state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName eq 'mowerScheduleToAttribute' ) { + + my $calendarjson = JSON::XS->new->pretty(1)->encode ($hash->{helper}{mower}{attributes}{calendar}{tasks}); + if ( $@ ) { + return "$iam $@"; + } + CommandAttr($hash,"$name mowerSchedule $calendarjson"); + return undef; + + } elsif ( $setName eq 'client_secret' ) { + if ( $setVal ) { + + my ($passResp, $passErr) = $hash->{helper}->{passObj}->setStorePassword($name, $setVal); + Log3 $name, 1, "$iam error: $passErr" if ($passErr); + return "$iam $passErr" if( $passErr ); + return undef; + } + + } elsif ( ReadingsVal( $name, 'state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName =~ /^(Start|Park|cuttingHeight)$/ ) { + if ( $setVal =~ /^(\d+)$/) { + sendCMD($hash ,$setName, $setVal); + return undef; + } + + } elsif ( ReadingsVal( $name, 'state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName eq 'headlight' ) { + if ( $setVal =~ /^(ALWAYS_OFF|ALWAYS_ON|EVENING_ONLY|EVENING_AND_NIGHT)$/) { + sendCMD($hash ,$setName, $setVal); + return undef; + } + + } elsif ( $setName eq 'getNewAccessToken' ) { + if ( $setVal ) { + + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged( $hash, '.access_token', '', 0 ); + readingsBulkUpdateIfChanged( $hash, 'state', 'initialized'); + readingsBulkUpdateIfChanged( $hash, 'mower_commandStatus', 'cleared'); + readingsEndUpdate($hash, 1); + + RemoveInternalTimer($hash, \&APIAuth); + APIAuth($hash); + return undef; + } + + } elsif (ReadingsVal( $name, 'state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName =~ /ParkUntilFurtherNotice|ParkUntilNextSchedule|Pause|ResumeSchedule|sendScheduleFromAttributeToMower/) { + sendCMD($hash,$setName); + return undef; + } + my $ret = " getNewAccessToken:noArg ParkUntilFurtherNotice:noArg ParkUntilNextSchedule:noArg Pause:noArg Start Park ResumeSchedule:noArg getUpdate:noArg client_secret "; + $ret .= "chargingStationPositionToAttribute:noArg headlight:ALWAYS_OFF,ALWAYS_ON,EVENING_ONLY,EVENING_AND_NIGHT cuttingHeight:1,2,3,4,5,6,7,8,9 mowerScheduleToAttribute:noArg "; + $ret .= "sendScheduleFromAttributeToMower:noArg "; + return "Unknown argument $setName, choose one of".$ret; + +} + +######################### +sub FW_detailFn { + my ($FW_wname, $name, $room, $pageHash) = @_; # pageHash is set for summaryFn. + my $hash = $defs{$name}; + my $type = $hash->{TYPE}; + return undef 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",'100 200') =~ /(\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 undef; +} + +######################### +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}{mower}{attributes}{positions}}; # developement mode + @pos = @{$hash->{helper}{areapos}}; # operational mode + @posc =@{$hash->{helper}{cspos}}; # maybe operational mode + my $img = "./fhem/$type/$name/map"; + + AttrVal($name,"mapImageCoordinatesToRegister","0 90\n90 0") =~ /(\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",'100 200') =~ /(\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"); + + AttrVal($name,"chargingStationCoordinates",'10.1165 51.28') =~ /(\d*\.?\d+)\s(\d*\.?\d+)/; + my ($cslo,$csla) = ($1, $2); + + my $cslon = int(($lonlo-$cslo) * $picx / $mapx); + my $cslat = int(($latlo-$csla) * $picy / $mapy); + # my $lon = int(($lonlo-$pos[0]{longitude}) * $picx / $mapx); + # my $lat = int(($latlo-$pos[0]{latitude}) * $picy / $mapy); + # my $lastx = int(($lonlo-$pos[$#pos]{longitude}) * $picx / $mapx); + # my $lasty = int(($latlo-$pos[$#pos]{latitude}) * $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 { + + my ( $cmd, $name, $attrName, $attrVal ) = @_; + my $hash = $defs{$name}; + my $type = $hash->{TYPE}; + my $iam = "$type $name Attr:"; + ########## + if( $attrName eq "disable" ) { + if( $cmd eq "set" and $attrVal eq "1" ) { + + RemoveInternalTimer( $hash ); + readingsSingleUpdate ( $hash, "state", "disabled", 1 ); + Log3 $name, 3, "$iam $cmd $attrName disabled"; + + } elsif( $cmd eq "del" or $cmd eq 'set' and !$attrVal ) { + + readingsSingleUpdate ( $hash, "state", "initialized", 1 ); + readMap( $hash ); + RemoveInternalTimer( $hash, \&APIAuth ); + InternalTimer( gettimeofday() + 2, \&APIAuth, $hash, 0 ); + Log3 $name, 3, "$iam $cmd $attrName enabled"; + + } + + ########## + } elsif ( $attrName eq 'mapImagePath' ) { + + if( $cmd eq "set") { + if ($attrVal =~ '(webp|png|jpg|jpeg)$' ) { + $hash->{helper}{MAP_PATH} = $attrVal; + $hash->{helper}{MAP_MIME} = "image/".$1; + + if ($attrVal =~ /(\d+)x(\d+)/) { + CommandAttr($hash,"$name mapImageWidthHeight $1 $2"); + } + + readMap( $hash ); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + } else { + return "$iam $cmd $attrName wrong image type, use webp, png, jpeg or jpg"; + Log3 $name, 3, "$iam $cmd $attrName wrong image type, use webp, png, jpeg or jpg"; + } + + } elsif( $cmd eq "del" ) { + + $hash->{helper}{MAP_PATH} = ''; + $hash->{helper}{MAP_CACHE} = ''; + $hash->{helper}{MAP_MIME} = ''; + Log3 $name, 3, "$iam $cmd $attrName"; + + } + + ########## + } elsif ( $attrName eq 'numberOfWayPointsToDisplay' ) { + + my $icurr = @{$hash->{helper}{areapos}}; + if( $cmd eq "set" && $attrVal =~ /\d+/ && $attrVal > $hash->{helper}{MOWING}{maxLengthDefault}) { + + # reduce array + $hash->{helper}{MOWING}{maxLength} = $attrVal; + for ( my $i = $icurr; $i > $attrVal; $i-- ) { + pop @{$hash->{helper}{areapos}}; + } + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + # reduce array + my $imax = $hash->{helper}{MOWING}{maxLengthDefault}; + $hash->{helper}{MOWING}{maxLength} = $imax; + for ( my $i = $icurr; $i > $imax; $i-- ) { + pop @{$hash->{helper}{areapos}}; + } + Log3 $name, 3, "$iam $cmd $attrName $attrName and set default $imax"; + + } + ########## + } elsif( $attrName eq "interval" ) { + + if( $cmd eq "set" ) { + + return "$iam $cmd $attrName $attrVal Interval must be greater than 0, recommended 600" unless($attrVal > 0); + $hash->{helper}->{interval} = $attrVal; + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + $hash->{helper}->{interval} = 600; + Log3 $name, 3, "$iam $cmd $attrName and set default 600"; + + } + ########## + } elsif( $attrName eq "mapImageCoordinatesUTM" ) { + + if( $cmd eq "set" ) { + + if ( AttrVal( $name,'mapImageCoordinatesToRegister', '' ) && $attrVal =~ /(\d*\.?\d+)\s(\d*\.?\d+)(\R|\s)(\d*\.?\d+)\s(\d*\.?\d+)/ ) { + + my ( $x1, $y1, $x2, $y2 ) = ( $1, $2, $4, $5 ); + AttrVal( $name,'mapImageCoordinatesToRegister', '' ) =~ /(\d*\.?\d+)\s(\d*\.?\d+)(\R|\s)(\d*\.?\d+)\s(\d*\.?\d+)/; + my ( $lo1, $la1, $lo2, $la2 ) = ( $1, $2, $4, $5 ); + my $scx = int( ( $x1 - $x2) / ( $lo1 - $lo2 ) ); + my $scy = int( ( $y1 - $y2 ) / ( $la1 - $la2 ) ); + CommandAttr($hash,"$name scaleToMeterXY $scx $scy"); + + } else { + return "$iam $attrName has a wrong format use linewise pairs or the attribute mapImageCoordinatesToRegister was not set before."; + } + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default 0 9090 0"; + + } + ########## + } elsif( $attrName eq "mapImageCoordinatesToRegister" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName has a wrong format use linewise pairs " unless($attrVal =~ /(\d*\.?\d+)\s(\d*\.?\d+)(\R|\s)(\d*\.?\d+)\s(\d*\.?\d+)/); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default 0 9090 0"; + + } + ########## + } elsif( $attrName eq "chargingStationCoordinates" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName has a wrong format use " unless($attrVal =~ /(\d*\.?\d+)\s(\d*\.?\d+)/); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default 10.1165 51.28"; + + } + ########## + } elsif( $attrName eq "mapImageWidthHeight" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName has a wrong format use " unless($attrVal =~ /(\d+)\s(\d+)/); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default 100 200"; + + } + ########## + } elsif( $attrName eq "scaleToMeterXY" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName has a wrong format use " unless($attrVal =~ /(\d+)\s(\d+)/); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default $hash->{helper}{scaleToMeterLongitude} $hash->{helper}{scaleToMeterLatitude}"; + + } + ########## + } elsif( $attrName eq "mowerSchedule" ) { + if( $cmd eq "set" ) { + + my $perl = eval { decode_json ($attrVal) }; + + if ($@) { + return "$iam $cmd $attrName decode error: $@ \n $perl"; + } + my $json = eval { encode_json ($perl) }; + if ($@) { + return "$iam $cmd $attrName encode error: $@ \n $json"; + } + Log3 $name, 4, "$iam $cmd $attrName array"; + + } + } + return undef; +} + +######################### +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-1; $i++ ) { + 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 ) { + $i++ if ($i == $poslen-2); + # 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); + } + #callFn if present + if ($hash->{helper}{$activity}{callFn}) { + $hash->{helper}{$activity}{cnt} = $i; + no strict "refs"; + &{$hash->{helper}{$activity}{callFn}}($hash); + use strict "refs"; + } + 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} = $xm; + $hash->{helper}{chargingStation}{latitude} = $ym; + readingsSingleUpdate($hash, "statistics_ChargingStationPositionXYn", (int($xm * 10000000 + 0.5) / 10000000).", ".(int($ym * 10000000 + 0.5) / 10000000).", ".$n, 0); + return undef; +} + + +######################### +sub AreaStatistics { + my ($hash) = @_; + my $name = $hash->{NAME}; + 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; + } + $asum = $lsum * AttrVal($name,'mowerCuttingWidth',0.24); + my $td = $xyarr[ 0 ]{storedTimestamp} - $xyarr[ $k ]{storedTimestamp}; + $vm = int($lsum / $td * 1000000 + 0.5)/1000 if ($td); + $lsum += int( ReadingsNum( $name, 'statistics_currentDayTrack', 0 ) ); + $asum += int( ReadingsNum( $name, 'statistics_currentDayArea', 0 ) ); + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged($hash,'statistics_currentDayTrack', int($lsum)); # m + readingsBulkUpdateIfChanged($hash,'statistics_currentDayArea', int($asum)); # qm + readingsBulkUpdateIfChanged($hash,'statistics_lastIntervalMowerSpeed', $vm); # m/s + readingsBulkUpdateIfChanged($hash,'statistics_lastIntervalNumberOfWayPoints', $i-1); # m/s + readingsEndUpdate($hash,1); + 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."; + } +} + +############################################################## + +1; + + +=pod + +=item device +=item summary Module to control Husqvarnas robotic lawn mowers with Connect Module (SIM) +=item summary_DE Modul zur Steuerung von Husqvarnas Mähroboter mit Connect Modul (SIM) + +=begin html + + +

AutomowerConnect

+
    + FHEM-FORUM: AutomowerConnect und AutomowerConnectDevice +

    + Introduction +

    +
      +
    • This module allows the communication between the Husqvarna Cloud and FHEM to control Husqvarna Automower equipped with a Connect Module (SIM).
    • +
    • It acts as Device for one mower and it acts as host for aditional mower registered in the API.
    • +
    • Additional mower have to be defined with the modul AutomowerConnectDevice.
    • +
    • The mower path is shown in the detail view.
    • +
    • An arbitrary map can be used as background for the mower path.
    • +
    • The map has to be a raster image in webp, png or jpg format.
    • +
    • It's possible to control everything the API offers, e.g. schedule, headlight, cutting height and actions like start, pause, park etc.
    • +
    • All API data is stored in the device hash, the last and the second last one. Use {Dumper $defs{<name>}} in the commandline to find the data and build userReadings out of it.

    • +
    + Limits for the Automower Connect API +

    +
      +
    • Max 1 request per second and application key.
    • +
    • Max 10 000 request per month and application key.
    • +
    • This results in a recommended interval of 600 seconds.

    • +
    + Requirements +

    +
      +
    • To get access to the API an application has to be created in the Husqvarna Developer Portal.
    • +
    • During registration an application key (client_id) and an application secret (client secret) is provided. Use these for for the module. The module uses client credentials as grant type for authorization.
    • +
    • The module uses client credentials as grant type for authorization.
    • +
    +
    + + Define +
      + define <device name> AutomowerConnect <application key> [<mower number>]
      + Example:
      + define myMower AutomowerConnect 123456789012345678901234567890123456 First device: the default mower number is 0.
      + It has to be set a client_secret. It's the application secret from the Husqvarna Developer Portal. + set myMower <client secret> +

      + Additional mower devices
      + define <device name> AutomowerConnectDevice <host name> <mower number>
      + Example:
      + define myAdditionalMower AutomowerConnectDevice MyMower 1 Second device with host name myMower and mower number 1 +

      +
    +
    + + + Set +
      +
    • Park
      + set <name> Park <number of minutes>
      + Parks mower in charging station for <number of minutes>
    • +
    • ParkUntilFurtherNotice
      + set <name> ParkUntilFurtherNotice
      + Parks mower in charging station until further notice
    • +
    • ParkUntilNextSchedule
      + set <name> ParkUntilNextSchedule
      + Parks mower in charging station and starts with next planned start
    • +
    • Pause
      + set <name> Pause
      + Pauses mower immediately at current position
    • +
    • ResumeSchedule
      + set <name> ResumeSchedule
      + Starts immediately if in planned intervall, otherwise with next scheduled start>
    • +
    • Start
      + set <name> Start <number of minutes>
      + Starts immediately for <number of minutes>
    • +
    • chargingStationPositionToAttribute
      + set <name> chargingStationPositionToAttribute
      + Sets the calculated charging station coordinates to the corresponding attributes.
    • +
    • client_secret
      + set <name> client_secret <application secret>
      + Sets the mandatory application secret (client secret)
    • +
    • cuttingHeight
      + set <name> cuttingHeight <1..9>
      + Sets the cutting height. NOTE: Do not use for 550 EPOS and Ceora.
    • +
    • getNewAccessToken
      + set <name> getNewAccessToken
      + Gets a new access token
    • +
    • getUpdate
      + set <name> getUpdate
      + Gets data from the API. This is done each intervall automatically.
    • +
    • headlight
      + set <name> headlight <ALWAYS_OFF|ALWAYS_ON|EVENIG_ONLY|EVENING_AND_NIGHT>
      +
    • +
    • mowerScheduleToAttrbute
      + set <name> mowerScheduleToAttrbute
      + Writes the schedule in to the attribute moverSchedule.
    • +
    • sendScheduleFromAttributeToMower
      + set <name> sendScheduleFromAttributeToMower
      + Sends the schedule to the mower. NOTE: Do not use for 550 EPOS and Ceora.
    • + +

    • + set <name>
      +
    • + +
    +
    + + + Get +
      +
    • html
      + get <name> html
      + Returns the mower area image as html code. For use in uiTable, TabletUI, Floorplan, readingsGroup, weblink etc.
    • +

      +
    +
    + + + Attributes +
      +
    • interval
      + attr <name> interval <time in seconds>
      + Time in seconds that is used to get new data from Husqvarna Cloud. Default: 600
    • + +
    • mapImagePath
      + attr <name> mapImagePath <path to image>
      + Path of a raster image file for an area the mower path has to be drawn to.
      + If the image name implies the image size by containing a part which matches /(\d+)x(\d+)/
      + the corresponding attribute will be set to mapImageWidthHeight = '$1 $2'
      + Image name example: map740x1300.webp
    • + +
    • mapImageWidthHeight
      + attr <name> mapImageWidthHeight <width in pixel><separator><height in pixel>
      + Width and Height in pixel of a raster image file for an area image the mower path has to be drawn to. <separator> is one space character.
    • + +
    • mapImageZoom
      + attr <name> mapImageZoom <zoom factor>
      + Zoom of a raster image for an area the mower path has to be drawn to. Default: 0.5
    • + +
    • mapImageCoordinatesToRegister
      + attr <name> mapImageCoordinatesToRegister <upper left longitude><space><upper left latitude><line feed><lower right longitude><space><lower right latitude>
      + Upper left and lower right coordinates to register (or to fit to earth) the image. Format: linewise longitude and latitude values separated by 1 space.
      + The lines are splitted by (/\s|\R$/). Use WGS84 (GPS) coordinates in decimal degree notation.
    • + +
    • mapImageCoordinatesUTM
      + attr <name> mapImageCoordinatesUTM <upper left longitude><space><upper left latitude><line feed><lower right longitude><space><lower right latitude>
      + Upper left and lower right coordinates to register (or to fit to earth) the image. Format: linewise longitude and latitude values separated by 1 space.
      + The lines are splitted by (/\s|\R$/). Use UTM coordinates in meter notation.
      + 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
      + Shows Map on (1 default) or not (0).
    • + +
    • chargingStationCoordinates
      + attr <name> chargingStationCoordinates <longitude><separator><latitude>
      + Longitude and latitude of the charging station. Use WGS84 (GPS) coordinates in decimal degree notation. <separator> is one space character
    • + +
    • chargingStationImagePosition
      + attr <name> chargingStationImagePosition <right, bottom, left, top, center>
      + Position of the charging station image relative to its coordinates.
    • + +
    • mowerCuttingWidth
      + attr <name> mowerCuttingWidth <cutting width>
      + mower cutting width in meter to calculate the mowed area. default: 0.24
    • + +
    • mowerSchedule
      + attr <name> mowerSchedule <schedule array>
      + This attribute provides the possebility to edit the mower schedule in form of an JSON array.
      The actual schedule can be loaded with the command set <name> mowerScheduleToAttribute.
      The command set <name> sendScheduleFromAttributeToMower sends the schedule to the mower. The maximum of array elements is 14 and 2 each day, so every day of a week can have 2 time spans. Each array element consists of 7 unsorted day values (monday to sunday) which can be true or false, a start and duration value in minutes. Start time counts from midnight. NOTE: Do not use for 550 EPOS and Ceora. Delete the attribute after the schedule is successfully uploaded.
    • + +
    • mowingAreaLimits
      + attr <name> mowingAreaLimits <positions list>
      + List of position describing the area to mow. Format: linewise longitude and latitude values separated by 1 space. The lines are splitted by (/\s|\R$/).
      The position values could be taken from Google Earth KML file, but whithout the altitude values.
    • + +
    • propertyLimits
      + attr <name> propertyLimits <positions list>
      + List of position describing the property limits. Format: linewise of longitude and latitude values separated by 1 space. The lines are splitted by (/\s|\R$/).The position values could be taken from . For converting UTM32 meter to ETRS89 / WGS84 decimal degree you can use the BKG-Geodatenzentrum BKG-Geodatenzentrum.
    • + +
    • numberOfWayPointsToDisplay
      + attr <name> numberOfWayPointsToDisplay <number of way points>
      + Set the number of way points stored and displayed, default 500
    • + +
    • scaleToMeterXY
      + attr <name> scaleToMeterXY <scale factor longitude><seperator><scale factor latitude>
      + The scale factor depends from the Location on earth, so it has to be calculated for short ranges only. <seperator> is one space character.
      + Longitude: (LongitudeMeter_1 - LongitudeMeter_2) / (LongitudeDegree_1 - LongitudeDegree _2)
      + Latitude: (LatitudeMeter_1 - LatitudeMeter_2) / (LatitudeDegree_1 - LatitudeDegree _2)
    • + + + +

    • + attr <name> <>
      +
    • +
    +
    + + + Readings +
      +
    • api_MowerFound - all mower registered under the application key (client_id)
    • +
    • api_token_expires - date when session of Husqvarna Cloud expires
    • +
    • api_access_token - current session token (shortend) of Husqvarna Cloud
    • +
    • batteryPercent - Battery power in percent
    • +
    • mower_activity - current activity "UNKNOWN" | "NOT_APPLICABLE" | "MOWING" | "GOING_HOME" | "CHARGING" | "LEAVING" | "PARKED_IN_CS" | "STOPPED_IN_GARDEN"
    • +
    • mower_commandStatus - Status of the last sent command cleared each status update
    • +
    • mower_errorCode - last error code
    • +
    • mower_errorCodeTimestamp - last error code time stamp
    • +
    • mower_errorDescription - error description
    • +
    • mower_id - ID of the mower
    • +
    • 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_overrideAction - reason for override a planned action NOT_ACTIVE, FORCE_PARK, FORCE_MOW
    • +
    • positions_lastLatitude - last known position (latitude)
    • +
    • positions_lastLongitude - last known position (longitude)
    • +
    • state - status of connection FHEM to Husqvarna Cloud API and device state(e.g. defined, authorization, authorized, connected, error, update)
    • +
    • status_statusTimestampOld - local time of second last change of the API content
    • +
    • settings_cuttingHeight - actual cutting height from API
    • +
    • settings_headlight - actual headlight mode from API
    • +
    • statistics_ChargingStationPositionXYn - calculated position of the carging station (longitude, latitude, number of datasets) during mower_activity PARKED_IN_CS and CHARGING
    • +
    • statistics_numberOfChargingCycles - number of charging cycles
    • +
    • statistics_numberOfCollisions - number of collisions
    • +
    • statistics_totalChargingTime - total charging time in hours
    • +
    • statistics_totalCuttingTime - total cutting time in hours
    • +
    • statistics_totalRunningTime - total running time in hours
    • +
    • statistics_totalSearchingTime - total searching time in hours
    • +
    • statistics_currentDayTrack - calculated mowed track length in meter during mower_activity MOWING since midnight
    • +
    • statistics_currentDayArea - calculated mowed area in square meter during mower_activity MOWING since midnight
    • +
    • statistics_lastIntervalNumberOfWayPoints - last Intervals Number of way points
    • +
    • statistics_currentMowerSpeed - calculated mower speed in meter per second during mower_activity MOWING for the last interval
    • +
    • statistics_lastDayTrack - calculated mowed track length in meter during mower_activity MOWING for yesterday
    • +
    • statistics_lastDayArea - calculated mowed area in square meter during mower_activity MOWING for yesterday
    • +
    • statistics_currentWeekTrack - calculated mowed track length in meter during mower_activity MOWING of the current week
    • +
    • statistics_currentWeekArea - calculated mowed area in square meter during mower_activity MOWING of the current week
    • +
    • statistics_lastWeekTrack - calculated mowed track length in meter during mower_activity MOWING of the last week
    • +
    • statistics_lastWeekArea - calculated mowed area in square meter during mower_activity MOWING of the last week
    • +
    • status_connected - state of connetion between mower and Husqvarna Cloud, (1 => true, 0 => false)
    • +
    • 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
    • +
    • system_name - name of the mower
    • +
    • system_model - model of the mower
    • +
    • system_serialNumber - serial number of the mower
    • + +
    +
+ +=end html + + + +=begin html_DE + + +

AutomowerConnect

+
    + FHEM-FORUM: AutomowerConnect und AutomowerConnectDevice
    + FHEM-Wiki: AutomowerConnect und AutomowerConnectDevice: Wie erstellt man eine Karte des Mähbereiches? +

    + Einleitung +

    +
      +
    • Dieses Modul etabliert eine Kommunikation zwischen der Husqvarna Cloud and FHEM, um einen Husqvarna Automower zu steuern, der mit einem Connect Modul (SIM) ausgerüstet ist.
    • +
    • Es arbeitet als Device für einen Mähroboter und übernimmt die Rolle als Host für zusätzliche in der API registrierte Mähroboter.
    • +
    • Zusätzliche Mähroboter sollten mit dem Modul AutomowerConnectDevice definiert werden..
    • +
    • Der Pfad des Mähroboters wird in der Detailansicht des FHEMWEB Frontends angezeigt.
    • +
    • Der Pfad kann mit einer beliebigen Karte hinterlegt werden.
    • +
    • Die Karte muss als Rasterbild im webp, png oder jpg Format vorliegen.
    • +
    • Es ist möglich alles was die API anbietet zu steuern, z.B. Mähplan,Scheinwerfer, Schnitthöhe und Aktionen wie, Start, Pause, Parken usw.
    • +
    • Die letzten und vorletzten Daten aus der API sind im Gerätehash gespeichert, Mit {Dumper $defs{<device name>}} in der Befehlezeile können die Daten angezeigt werden und daraus userReadings erstellt werden.

    • +
    + Limit Automower Connect API +

    +
      +
    • Maximal 1 Request pro Sekunde und Application Key.
    • +
    • Maximal 10 000 Requests pro Monat und Application Key.
    • +
    • Daraus ergibt sich ein empfohlenes Abfrageinterval von 600 Sekunden

    • +
    + Anforderungen +

    +
      +
    • Für den Zugriff auf die API muss eine Application angelegt werden, im Husqvarna Developer Portal.
    • +
    • Währenddessen wird ein Application Key (client_id) und ein Application Secret (client secret) bereitgestellt. Diese sind für dieses Modul zu nutzen.
    • +
    • Das Modul nutzt Client Credentials als Granttype zur Authorisierung.
    • +
    +
    + + Define +
      + define <device name> AutomowerConnect <application key> [<mower number>]
      + Beispiel:
      + define myMower AutomowerConnect 123456789012345678901234567890123456 Erstes Gerät: die Defaultmähernummer ist 0.
      + Es muss ein client_secret gesetzt werden. Es ist das Application Secret vom Husqvarna Developer Portal. + set myMower <client secret>
      +
      + Zusätzlicher Mähroboter
      + define <device name> AutomowerConnectDevice <host name> <mower number>
      + Beispiel:
      + define myAdditionalMower AutomowerConnectDevice MyMower 1 Zweites Gerät mit Hostname myMower und Mähernumme 1 +

      +
    +
    + + + Set +
      +
    • Park
      + set <name> Park <number of minutes>
      + Parkt den Mäher in der Ladestation (LS) für <number of minutes>
    • +
    • ParkUntilFurtherNotice
      + set <name> ParkUntilFurtherNotice
      + Parkt den Mäher bis auf Weiteres in der LS
    • +
    • ParkUntilNextSchedule
      + set <name> ParkUntilNextSchedule
      + Parkt den Mäher bis auf Weiteres in der LS und startet zum nächsten geplanten Zeitpunkt
    • +
    • Pause
      + set <name> Pause
      + Pausiert den Mäher sofort am aktuellen Standort
    • +
    • ResumeSchedule
      + set <name> ResumeSchedule
      + Startet im geplanten Interval den Mäher sofort, sonst zum nächsten geplanten Zeitpunkt
    • +
    • Start
      + set <name> Start <number of minutes>
      + Startet sofort für <number of minutes>
    • +
    • chargingStationPositionToAttribute
      + set <name> chargingStationPositionToAttribute
      + Setzt die berechneten Koordinaten der LS in das entsprechende Attribut.
    • +
    • client_secret
      + set <name> client_secret <application secret>
      + Setzt das erforderliche Application Secret (client secret)
    • +
    • cuttingHeight
      + set <name> cuttingHeight <1..9>
      + Setzt die Schnitthöhe. HINWEIS: Nicht für 550 EPOS und Ceora geeignet.
    • +
    • getNewAccessToken
      + set <name> getNewAccessToken
      + Holt ein neues Access Token.
    • +
    • getUpdate
      + set <name> getUpdate
      + Liest die Daten von der API. Das passiert jedes Interval automatisch.
    • +
    • headlight
      + set <name> headlight <ALWAYS_OFF|ALWAYS_ON|EVENIG_ONLY|EVENING_AND_NIGHT>
      + Setzt den Scheinwerfermode
    • +
    • mowerScheduleToAttrbute
      + set <name> mowerScheduleToAttrbute
      + Schreibt den Mähplan ins Attribut moverSchedule.
    • +
    • sendScheduleFromAttributeToMower
      + set <name> sendScheduleFromAttributeToMower
      + Sendet den Mähplan zum Mäher. HINWEIS: Nicht für 550 EPOS und Ceora geeignet.
    • + +

    • + set <name>
      +
    • + + + Get +
        +
      • html
        + get <name> html
        + Gibt das Bild des Mäherbereiches html kodiert zurück, zur Verwendung in uiTable, TabletUI, Floorplan, readingsGroup, weblink usw.
      • +

        +
      +
      + +
    +
    + + Attributes +
      +
    • interval
      + attr <name> interval <time in seconds>
      + Zeit in Sekunden nach denen neue Daten aus der Husqvarna Cloud abgerufen werden. Standard: 600
    • + +
    • mapImagePath
      + attr <name> mapImagePath <path to image>
      + Pfad zur Bilddatei. Auf das Bild werden Pfad, Anfangs- u. Endpunkte gezeichnet.
      + Wenn der Bildname die Bildgröße impliziert indem er zu dem regulären Ausdruck /(\d+)x(\d+)/ passt,
      + wird das zugehörige Attribut gesetzt mapImageWidthHeight = '$1 $2'
      + Beispiel Bildname: map740x1300.webp
    • + +
    • mapImageWidthHeight
      + attr <name> mapImageWidthHeight <width in pixel><separator><height in pixel>
      + Bildbreite in Pixel des Bildes auf das Pfad, Anfangs- u. Endpunkte gezeichnet werden. <separator> ist 1 Leerzeichen.
    • + +
    • mapImageZoom
      + attr <name> mapImageZoom <zoom factor>
      + Zoomfaktor zur Salierung des Bildes auf das Pfad, Anfangs- u. Endpunkte gezeichnet werden. Standard: 0.5
    • + +
    • mapImageCoordinatesToRegister
      + attr <name> mapImageCoordinatesToRegister <upper left longitude><space><upper left latitude><line feed><lower right longitude><space><lower right latitude>
      + Obere linke und untere rechte Ecke der Fläche auf der Erde, die durch das Bild dargestellt wird um das Bild auf der Fläche zu registrieren (oder einzupassen).
      + Format: Zeilenweise Paare von Longitude- u. Latitudewerten getrennt durch 1 Leerzeichen. Die Zeilen werden aufgeteilt durch (/\s|\R$/).
      + Angabe der WGS84 (GPS) Koordinaten muss als Dezimalgrad erfolgen.
    • + +
    • mapImageCoordinatesUTM
      + attr <name> mapImageCoordinatesUTM <upper left longitude><space><upper left latitude><line feed><lower right longitude><space><lower right latitude>
      + Obere linke und untere rechte Ecke der Fläche auf der Erde, die durch das Bild dargestellt wird um das Bild auf der Fläche zu registrieren (oder einzupassen).
      + Format: Zeilenweise Paare von Longitude- u. Latitudewerten getrennt durch 1 Leerzeichen. Die Zeilen werden aufgeteilt durch (/\s|\R$/).
      + Die Angabe der UTM Koordinaten muss als Dezimalzahl in Meter erfolgen.
      + Das Attribut muss nach dem Attribut mapImageCoordinatesToRegister gesetzt werden.
      + Dieses Attribut berechnet die Skalierungsfaktoren. Das Attribut scaleToMeterXY wird entsprechend gesetzt
    • + +
    • showMap
      + attr <name> showMap <>1,0
      + Zeigt die Karte an (1 default) oder nicht (0).
    • + +
    • chargingStationCoordinates
      + attr <name> chargingStationCoordinates <longitude><separator><latitude>
      + Longitude und Latitude der Ladestation als WGS84 (GPS) Koordinaten als Deimalzahl. <separator> ist 1 Leerzeichen
    • + +
    • chargingStationImagePosition
      + attr <name> chargingStationImagePosition <right, bottom, left, top, center>
      + Position der Ladestation relativ zu ihren Koordinaten.
    • + +
    • mowerCuttingWidth
      + attr <name> mowerCuttingWidth <cutting width>
      + Schnittbreite in Meter zur Berechnung der gemähten Fläche. default: 0.24
    • + +
    • mowerSchedule
      + attr <name> mowerSchedule <schedule array>
      + Dieses Attribut bietet die Möglichkeit den Mähplan zu ändern, er liegt als JSON Array vor.
      Der aktuelleMähplan kann mit dem Befehl set <name> mowerScheduleToAttrbute ins Attribut geschrieben werden.
      Der Befehl set <name> sendScheduleFromAttributeToMower sendet den Mähplan an den Mäher. Das Maximum der Arrayelemente beträgt 14, 2 für jeden Tag, so daß jeden Tag zwei Intervalle geplant werden können. Jedes Arrayelement besteht aus 7 unsortierten Tageswerten (monday bis sunday) die auf true oder false gesetzt werden können, einen start Wert und einen duration Wert in Minuten. Die Startzeit start wird von Mitternacht an gezählt. HINWEIS: Nicht für 550 EPOS und Ceora geeignet.
    • + +
    • mowingAreaLimits
      + attr <name> mowingAreaLimits <positions list>
      + Liste von Positionen, die den Mähbereich beschreiben. Format: Zeilenweise Paare von Longitude- u. Latitudewerten getrennt durch 1 Leerzeichen. Die Zeilen werden aufgeteilt durch (/\s|\R$/).
      Die Liste der Positionen kann aus einer mit Google Earth erzeugten KML-Datei entnommen werden, aber ohne Höhenangaben
    • + +
    • propertyLimits
      + attr <name> propertyLimits <positions list>
      + Liste von Positionen, um die Grundstücksgrenze zu beschreiben. Format: Zeilenweise Paare von Longitude- u. Latitudewerten getrennt durch 1 Leerzeichen. Eine Zeile wird aufgeteilt durch (/\s|\R$/).
      Die genaue Position der Grenzpunkte kann man über die Geoportale der Länder finden. Eine Umrechnung der UTM32 Daten in Meter nach ETRS89 in Dezimalgrad kann über das BKG-Geodatenzentrum erfolgen.
    • + +
    • numberOfWayPointsToDisplay
      + attr <name> numberOfWayPointsToDisplay <number of way points>
      + Legt die Anzahl der gespeicherten und und anzuzeigenden Wegpunkte fest, default 500
    • + +
    • scaleToMeterXY
      + attr <name> scaleToMeterXY <scale factor longitude><seperator><scale factor latitude>
      + Der Skalierfaktor hängt vom Standort ab und muss daher für kurze Strecken berechnet werden. <seperator> ist 1 Leerzeichen.
      + Longitude: (LongitudeMeter_1 - LongitudeMeter_2) / (LongitudeDegree_1 - LongitudeDegree _2)
      + Latitude: (LatitudeMeter_1 - LatitudeMeter_2) / (LatitudeDegree_1 - LatitudeDegree _2)
    • + + +

    • + attr <name> <>
      +
    • + +
    +
    + + + Readings +
      +
    • api_MowerFound - Alle Mährobuter, die unter dem genutzten Application Key (client_id) registriert sind.
    • +
    • api_token_expires - Datum wann die Session der Husqvarna Cloud abläuft
    • +
    • api_access_token - aktueller Sitzungstoken (gekürzt) für die Husqvarna Cloud
    • +
    • batteryPercent - Batteryladung in Prozent
    • +
    • mower_activity - aktuelle Aktivität "UNKNOWN" | "NOT_APPLICABLE" | "MOWING" | "GOING_HOME" | "CHARGING" | "LEAVING" | "PARKED_IN_CS" | "STOPPED_IN_GARDEN"
    • +
    • mower_commandStatus - Status des letzten uebermittelten Kommandos wird duch Statusupdate zurückgesetzt.
    • +
    • mower_errorCode - last error code
    • +
    • mower_errorCodeTimestamp - last error code time stamp
    • +
    • mower_errorDescription - error description
    • +
    • mower_id - ID des Automowers
    • +
    • 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_overrideAction - Grund für vorrangige Aktion NOT_ACTIVE, FORCE_PARK, FORCE_MOW
    • +
    • positions_lastLatitude - letzte bekannte Position (Breitengrad)
    • +
    • positions_lastLongitude - letzte bekannte Position (Längengrad)
    • +
    • state - Status der Verbindung des FHEM-Gerätes zur Husqvarna Cloud API (defined, authorization, authorized, connected, error, update).
    • +
    • settings_cuttingHeight - aktuelle Schnitthöhe aus der API
    • +
    • settings_headlight - aktueller Scheinwerfermode aus der API
    • +
    • statistics_ChargingStationPositionXYn - berechnete Position der Ladestation mit den Werten Longitude, Latitude und Anzahl der verwendeten Datensätze wähend der Mower_activity PARKED_IN_CS und CHARGING
    • +
    • statistics_numberOfChargingCycles - Anzahl der Ladezyklen
    • +
    • statistics_numberOfCollisions - Anzahl der Kollisionen
    • +
    • statistics_totalChargingTime - Gesamtladezeit in Stunden
    • +
    • statistics_totalCuttingTime - Gesamtschneidezeit in Stunden
    • +
    • statistics_totalRunningTime - Gesamtlaufzeit in Stunden
    • +
    • statistics_totalSearchingTime - Gesamtsuchzeit in Stunden
    • +
    • statistics_currentDayTrack - berechnete gefahrene Strecke in Meter bei_Activity MOWING seit Mitternacht
    • +
    • statistics_currentDayArea - berechnete übermähte Fläche in Quadratmeter bei der Activity MOWING seit Mitternacht
    • +
    • statistics_lastIntervalNumberOfWayPoints - Anzahl der Wegpunkte im letzten Interval
    • +
    • statistics_currentMowerSpeed - berechnet Geschwindigkeit in Meter pro Sekunde bei der_Activity MOWING im letzten Interval
    • +
    • statistics_lastDayTrack - berechnete gefahrene Strecke in Meter bei_Activity MOWING des letzten Tages
    • +
    • statistics_lastDayArea - berechnete übermähte Fläche in Quadratmeter bei der Activity MOWING des letzten Tages
    • +
    • statistics_currentWeekTrack - berechnete gefahrene Strecke in Meter bei_Activity MOWING
    • +
    • statistics_currentWeekArea - berechnete übermähte Fläche in Quadratmeter bei der Activity MOWING der laufenden Woche
    • +
    • statistics_lastWeekTrack - berechnete gefahrene Strecke in Meter bei_Activity MOWING der letzten Woche
    • +
    • statistics_lastWeekArea - berechnete übermähte Fläche in Quadratmeter bei der Activity MOWING der letzten Woche
    • +
    • status_connected - Status der Verbindung zwischen dem Automower und der Husqvarna Cloud, (1 => true, 0 => false)
    • +
    • 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
    • +
    • system_name - Name des Automowers
    • +
    • system_model - Model des Automowers
    • +
    • system_serialNumber - Seriennummer des Automowers
    • +
    +
+ +=end html_DE diff --git a/fhem/FHEM/75_AutomowerConnectDevice.pm b/fhem/FHEM/75_AutomowerConnectDevice.pm new file mode 100644 index 000000000..e231e4227 --- /dev/null +++ b/fhem/FHEM/75_AutomowerConnectDevice.pm @@ -0,0 +1,1463 @@ +############################################################################### +# +# 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. +# +# +# $Id$ +# +# +################################################################################ + +package FHEM::AutomowerConnectDevice; +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 store retrieve); + +# Import der FHEM Funktionen +BEGIN { + GP_Import( + qw( + AttrVal + CommandAttr + fhemTzOffset + FmtDateTime + getKeyValue + InternalTimer + InternalVal + Log3 + readingFnAttributes + readingsBeginUpdate + readingsBulkUpdate + readingsBulkUpdateIfChanged + readingsDelete + readingsEndUpdate + ReadingsNum + readingsSingleUpdate + ReadingsVal + RemoveInternalTimer + setKeyValue + defs + attr + modules + deviceEvents + devspec2array + ) + ); +} + +GP_Export( + qw( + Initialize + ) +); + +my $missingModul = ""; + +eval "use JSON;1" or $missingModul .= "JSON "; +require HttpUtils; + +use constant AUTHURL => 'https://api.authentication.husqvarnagroup.dev/v1'; +use constant APIURL => 'https://api.amc.husqvarna.dev/v1'; + +############################################################## + +sub Initialize() { + my ($hash) = @_; + + $hash->{SetFn} = \&Set; + $hash->{GetFn} = \&Get; + $hash->{DefFn} = \&Define; + $hash->{UndefFn} = \&Undef; + $hash->{NotifyFn} = \&Notify; + $hash->{FW_detailFn}= \&FW_detailFn; + $hash->{AttrFn} = \&Attr; + $hash->{AttrList} = "disable:1,0 " . + "mapImagePath " . + "mapImageWidthHeight " . + "mapImageCoordinatesToRegister:textField-long " . + "mapImageCoordinatesUTM:textField-long " . + "mapImageZoom " . + "showMap:1,0 " . + "chargingStationCoordinates " . + "chargingStationImagePosition:left,top,right,bottom,center " . + "scaleToMeterXY " . + "mowerCuttingWidth " . + "mowerSchedule:textField-long " . + "mowingAreaLimits:textField-long " . + "propertyLimits:textField-long " . + "numberOfWayPointsToDisplay " . + $readingFnAttributes; + + return undef; +} + + +############################################################## +# +# DEFINE +# +############################################################## + +sub Define{ + my ( $hash, $def ) = @_; + my @val = split( "[ \t]+", $def ); + my $name = $val[0]; + my $type = $val[1]; + my $iam = "$type $name Define:"; + + return "$iam too few parameters: define $type " if( @val < 4 ) ; + return "$iam Cannot define $type device. Perl modul $missingModul is missing." if ( $missingModul ); + my $hostname =$val[2]; + my $mowerNumber = $val[3]; + + ::notifyRegexpChanged($hash, $hostname.':state:.connected'); + + %$hash = (%$hash, + helper => { + hostname => $hostname, + mowerNumber => $mowerNumber, + scaleToMeterLongitude => 67425, + scaleToMeterLatitude => 108886, + MAP_PATH => '', + MAP_MIME => '', + MAP_CACHE => '', + UNKNOWN => { + arrayName => '', + maxLength => 0, + callFn => '' + }, + NOT_APPLICABLE => { + arrayName => '', + maxLength => 0, + callFn => '' + }, + MOWING => { + arrayName => 'areapos', + maxLength => 500, + maxLengthDefault => 500, + callFn => \&AreaStatistics + }, + GOING_HOME => { + arrayName => '', + maxLength => 0, + callFn => '' + }, + CHARGING => { + arrayName => 'cspos', + maxLength => 500, + callFn => \&ChargingStationPosition + }, + LEAVING => { + arrayName => '', + maxLength => 0, + callFn => '' + }, + PARKED_IN_CS => { + arrayName => 'cspos', + maxLength => 50, + callFn => \&ChargingStationPosition + }, + STOPPED_IN_GARDEN => { + arrayName => '', + maxLength => 0, + callFn => '' + } + } + ); + + +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; + + $attr{$name}{room} = $type if( !defined( $attr{$name}{room} ) ); + $attr{$name}{icon} = 'automower' if( !defined( $attr{$name}{icon} ) ); + + RemoveInternalTimer($hash); + InternalTimer( gettimeofday() + 25, \&readMap, $hash, 0); + + AddExtension( $name, \&GetMap, "$type/$name/map" ); + + + readingsSingleUpdate( $hash, 'state', 'defined', 1 ); + + return undef; + +} + + +############################################################## +# +# GET MOWER +# +############################################################## + +sub Notify { + + my ($hash,$hosthash) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name Notify:"; + my $mowerNumber = $hash->{helper}{mowerNumber}; + my $events = ::deviceEvents($hosthash,1); + + if(grep /^state:.connected$/, @{$events}) { + + my $maxMower = @{$hosthash->{helper}{mowers}}; + if ($maxMower <= $mowerNumber || $mowerNumber < 0 ) { + + Log3 $name, 2, "$iam mower number $mowerNumber not available. Change definition of $name."; + return undef; + + } + + my $mowerhash = $hosthash->{helper}{mowers}[$mowerNumber]; + my $myMower = dclone( $mowerhash ); + if ( defined ($hash->{helper}{mower}{id}) ){ + + $hash->{helper}{mowerold} = dclone( $hash->{helper}{mower} ); + + } else { + + $hash->{helper}{mowerold} = $myMower; + + $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; + } + + $hash->{helper}{mower} = $myMower; + 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)); + + } + + + + readingsBeginUpdate($hash); + + 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); + 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}; + 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}; + $model =~ s/AUTOMOWER./AUTOMOWER®/; + readingsBulkUpdateIfChanged($hash, $pref."_model", $model ); + readingsBulkUpdateIfChanged($hash, $pref."_serialNumber", $hash->{helper}{mower}{attributes}{$pref}{serialNumber} ); + $pref = 'planner'; + readingsBulkUpdateIfChanged($hash, "planner_restrictedReason", $hash->{helper}{mower}{attributes}{$pref}{restrictedReason} ); + readingsBulkUpdateIfChanged($hash, "planner_overrideAction", $hash->{helper}{mower}{attributes}{$pref}{override}{action} ); + + $tstamp = $hash->{helper}{mower}{attributes}{$pref}{nextStartTimestamp}; + $timestamp = FmtDateTime($tstamp/1000); + readingsBulkUpdateIfChanged($hash, "planner_nextStart", $tstamp ? $timestamp : '-' ); + $pref = 'statistics'; + readingsBulkUpdateIfChanged($hash, $pref."_numberOfChargingCycles", $hash->{helper}->{mower}{attributes}{$pref}{numberOfChargingCycles} ); + readingsBulkUpdateIfChanged($hash, $pref."_totalCuttingTime", $hash->{helper}->{mower}{attributes}{$pref}{totalCuttingTime} ); + readingsBulkUpdateIfChanged($hash, $pref."_totalChargingTime", $hash->{helper}->{mower}{attributes}{$pref}{totalChargingTime} ); + readingsBulkUpdateIfChanged($hash, $pref."_totalSearchingTime", $hash->{helper}->{mower}{attributes}{$pref}{totalSearchingTime} ); + readingsBulkUpdateIfChanged($hash, $pref."_numberOfCollisions", $hash->{helper}->{mower}{attributes}{$pref}{numberOfCollisions} ); + readingsBulkUpdateIfChanged($hash, $pref."_totalRunningTime", $hash->{helper}->{mower}{attributes}{$pref}{totalRunningTime} ); + $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."_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."_lastLatitude", $hash->{helper}{mower}{attributes}{$pref}[0]{latitude} ); + readingsBulkUpdateIfChanged($hash, $pref."_lastLongitude", $hash->{helper}{mower}{attributes}{$pref}[0]{longitude} ); + readingsBulkUpdateIfChanged($hash, 'state', 'connected' ); + + my @time = localtime(); + my $secs = ( $time[2] * 3600 ) + ( $time[1] * 60 ) + $time[0]; + my $interval = $hosthash->{helper}->{interval}; + # do at midnight + if ( $secs <= $interval ) { + + readingsBulkUpdateIfChanged( $hash, 'statistics_lastDayTrack', ReadingsNum( $name, 'statistics_currentDayTrack', 0 )); + readingsBulkUpdateIfChanged( $hash, 'statistics_lastDayArea', ReadingsNum( $name, 'statistics_currentDayArea', 0 )); + readingsBulkUpdateIfChanged( $hash, 'statistics_currentWeekTrack', ReadingsNum( $name, 'statistics_currentWeekTrack', 0 ) + ReadingsNum( $name, 'statistics_currentDayTrack', 0 )); + readingsBulkUpdateIfChanged( $hash, 'statistics_currentWeekArea', ReadingsNum( $name, 'statistics_currentWeekArea', 0 ) + ReadingsNum( $name, 'statistics_currentDayArea', 0 )); + readingsBulkUpdateIfChanged( $hash, 'statistics_currentDayTrack', 0, 0); + readingsBulkUpdateIfChanged( $hash, 'statistics_currentDayArea', 0, 0); + # do on mondays + if ( $time[6] == 1 && $secs <= $interval ) { + + readingsBulkUpdateIfChanged( $hash, 'statistics_lastWeekTrack', ReadingsNum( $name, 'statistics_currentWeekTrack', 0 )); + readingsBulkUpdateIfChanged( $hash, 'statistics_lastWeekArea', ReadingsNum( $name, 'statistics_currentWeekArea', 0 )); + readingsBulkUpdateIfChanged( $hash, 'statistics_currentWeekTrack', 0, 0); + readingsBulkUpdateIfChanged( $hash, 'statistics_currentWeekArea', 0, 0); + + } + } + readingsEndUpdate($hash, 1); + } + + return undef; + +} + + +############################################################## +# +# SEND COMMAND +# +############################################################## + +sub CMD { + my ($hash,@cmd) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name CMD:"; + my $hostname = $hash->{helper}{hostname}; + my $hosthash = $defs{$hostname}; + + if ( AttrVal($name, 'disable', '') ) { + + Log3 $name, 3, "$iam disabled"; + return undef + + } + + my $client_id = $hosthash->{helper}->{client_id}; + my $token = ReadingsVal($hostname,".access_token",""); + my $provider = ReadingsVal($hostname,".provider",""); + my $mower_id = $hash->{helper}{mower}{id}; + + my $json = ''; + my $post = ''; + + +my $header = "Accept: application/vnd.api+json\r\nX-Api-Key: ".$client_id."\r\nAuthorization: Bearer " . $token . "\r\nAuthorization-Provider: " . $provider . "\r\nContent-Type: application/vnd.api+json"; + + + if ($cmd[0] eq "ParkUntilFurtherNotice") { $json = '{"data":{"type":"'.$cmd[0].'"}}'; $post = 'actions' } + elsif ($cmd[0] eq "ParkUntilNextSchedule") { $json = '{"data": {"type":"'.$cmd[0].'"}}'; $post = 'actions' } + elsif ($cmd[0] eq "ResumeSchedule") { $json = '{"data": {"type":"'.$cmd[0].'"}}'; $post = 'actions' } + elsif ($cmd[0] eq "Pause") { $json = '{"data": {"type":"'.$cmd[0].'"}}'; $post = 'actions' } + elsif ($cmd[0] eq "Park") { $json = '{"data": {"type":"'.$cmd[0].'","attributes":{"duration":'.$cmd[1].'}}}'; $post = 'actions' } + elsif ($cmd[0] eq "Start") { $json = '{"data": {"type":"'.$cmd[0].'","attributes":{"duration":'.$cmd[1].'}}}'; $post = 'actions' } + elsif ($cmd[0] eq "headlight") { $json = '{"data": {"type":"settings","attributes":{"'.$cmd[0].'": {"mode": "'.$cmd[1].'"}}}}'; $post = 'settings' } + elsif ($cmd[0] eq "cuttingHeight") { $json = '{"data": {"type":"settings","attributes":{"'.$cmd[0].'": '.$cmd[1].'}}}'; $post = 'settings' } + elsif ($cmd[0] eq "sendScheduleFromAttributeToMower" && AttrVal( $name, 'mowerSchedule', '')) { + + my $perl = eval { decode_json (AttrVal( $name, 'mowerSchedule', '')) }; + if ($@) { + return "$iam decode error: $@ \n $perl"; + } + my $jsonSchedule = eval { encode_json ($perl) }; + if ($@) { + return "$iam encode error: $@ \n $json"; + } + $json = '{"data":{"type": "calendar","attributes":{"tasks":'.$jsonSchedule.'}}}'; + $post = 'calendar'; + } + + Log3 $name, 5, "$iam $header \n $cmd[0] \n $json"; + + ::HttpUtils_NonblockingGet({ + url => APIURL . "/mowers/". $mower_id . "/".$post, + timeout => 10, + hash => $hash, + method => "POST", + header => $header, + data => $json, + callback => \&CMDResponse, + }); + +} + +############################################################## +sub CMDResponse { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $iam = "$type $name CMDResponse:"; + + if($err ne "") { + readingsSingleUpdate( $hash, 'state', 'error', 1 ); + Log3 $name, 2, "$iam error while requesting ".$param->{url}." - $err"; + + } elsif($data ne "") { + + my $result = eval { decode_json($data) }; + if ($@) { + Log3( $name, 2, "$iam - JSON error while request: $@"); + return; + } + + $hash->{helper}{CMDResponse} = $result; + if ($result->{message}) { + readingsSingleUpdate( $hash, 'state', 'error', 1 ); + Log3 $name, 2, "$iam" . $data; + $hash->{helper}->{mower_commandStatus} = $result->{message}; + + } elsif ($result->{errors}) { + Log3 $name, 2, "$iam" . $data; + readingsSingleUpdate( $hash, 'state', 'error', 1 ); + $hash->{helper}->{mower_commandStatus} = 'ERROR - '. $result->{errors}[0]{title}; + } elsif ($result->{data}) { + Log3 $name, 5, $data; + if ( ref ($result->{data}) eq 'ARRAY') { + $hash->{helper}->{mower_commandStatus} = 'OK - '. $result->{data}[0]{type}; + } else { + $hash->{helper}->{mower_commandStatus} = 'OK - '. $result->{data}{type}; + } + + } + + readingsSingleUpdate($hash, 'mower_commandStatus', $hash->{helper}->{mower_commandStatus} ,1); + + } + +} + +############################################################## +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; + + } +} + +############################################################## +sub Set { + my ($hash,@a) = @_; + my $type = $hash->{TYPE}; + + return "$type $hash->{NAME} Set: needs at least an argument" if ( @a < 2 ); + my ($name,$setName,$setVal,$setVal2,$setVal3) = @a; + my $iam = "$type $name Set:"; + + Log3 $name, 4, "$iam set called with $setName " . ($setVal ? $setVal : "") if ($setName !~ /^\?$/); + + if ( $setName eq 'chargingStationPositionToAttribute' ) { + my ($xm, $ym, $n) = split(/,\s/,ReadingsVal($name,'status_calcChargingStationPositionXYn','10.1165, 51.28, 0')); + CommandAttr($hash,"$name chargingStationCoordinates $xm $ym"); + return undef; + + ################ + } elsif ( ReadingsVal( $name, 'state', 'defined' ) !~ /defined/ && $setName eq 'mowerScheduleToAttribute' ) { + + my $calendarjson = JSON::XS->new->pretty(1)->encode ($hash->{helper}{mower}{attributes}{calendar}{tasks}); + if ( $@ ) { + return "$iam $@"; + } + CommandAttr($hash,"$name mowerSchedule $calendarjson"); + return undef; + + ################ + } elsif ( ReadingsVal( $name, 'state', 'defined' ) !~ /defined/ && $setName =~ /^(Start|Park|cuttingHeight)$/ ) { + if ( $setVal =~ /^(\d+)$/) { + CMD($hash ,$setName, $setVal); + return undef; + } + + ################ + } elsif ( ReadingsVal( $name, 'state', 'defined' ) !~ /defined|/ && $setName eq 'headlight' ) { + if ( $setVal =~ /^(ALWAYS_OFF|ALWAYS_ON|EVENING_ONLY|EVENING_AND_NIGHT)$/) { + CMD($hash ,$setName, $setVal); + return undef; + } + + ################ + } elsif (ReadingsVal( $name, 'state', 'defined' ) !~ /defined/ && $setName =~ /ParkUntilFurtherNotice|ParkUntilNextSchedule|Pause|ResumeSchedule|sendScheduleFromAttributeToMower/) { + CMD($hash,$setName); + return undef; + } + my $ret = " ParkUntilFurtherNotice:noArg ParkUntilNextSchedule:noArg Pause:noArg Start Park ResumeSchedule:noArg "; + $ret .= "chargingStationPositionToAttribute:noArg headlight:ALWAYS_OFF,ALWAYS_ON,EVENING_ONLY,EVENING_AND_NIGHT cuttingHeight:1,2,3,4,5,6,7,8,9 mowerScheduleToAttribute:noArg "; + $ret .= "sendScheduleFromAttributeToMower:noArg "; + return "Unknown argument $setName, choose one of".$ret; + +} + +######################### +sub FW_detailFn { + my ($FW_wname, $name, $room, $pageHash) = @_; # pageHash is set for summaryFn. + my $hash = $defs{$name}; + my $type = $hash->{TYPE}; + return undef 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",'100 200') =~ /(\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 undef; +} + +######################### +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}{mower}{attributes}{positions}}; # developement mode + @pos = @{$hash->{helper}{areapos}}; # operational mode + @posc =@{$hash->{helper}{cspos}}; # maybe operational mode + my $img = "./fhem/$type/$name/map"; + + AttrVal($name,"mapImageCoordinatesToRegister","0 90\n90 0") =~ /(\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",'100 200') =~ /(\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"); + + AttrVal($name,"chargingStationCoordinates",'10.1165 51.28') =~ /(\d*\.?\d+)\s(\d*\.?\d+)/; + my ($cslo,$csla) = ($1, $2); + + my $cslon = int(($lonlo-$cslo) * $picx / $mapx); + my $cslat = int(($latlo-$csla) * $picy / $mapy); + # my $lon = int(($lonlo-$pos[0]{longitude}) * $picx / $mapx); + # my $lat = int(($latlo-$pos[0]{latitude}) * $picy / $mapy); + # my $lastx = int(($lonlo-$pos[$#pos]{longitude}) * $picx / $mapx); + # my $lasty = int(($latlo-$pos[$#pos]{latitude}) * $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 { + + my ( $cmd, $name, $attrName, $attrVal ) = @_; + my $hash = $defs{$name}; + my $type = $hash->{TYPE}; + my $iam = "$type $name Attr:"; + ########## + if( $attrName eq "disable" ) { + if( $cmd eq "set" and $attrVal eq "1" ) { + + ::setDisableNotifyFn($hash, 1); + readingsSingleUpdate ( $hash, "state", "disabled", 1 ); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" or $cmd eq 'set' and !$attrVal ) { + + my $hostname = $hash->{helper}{hostname}; + readMap($hash); + ::notifyRegexpChanged($hash, $hostname.':state:.connected'); + readingsSingleUpdate ( $hash, "state", "initialized", 1 ); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } + + ########## + } elsif ( $attrName eq 'mapImagePath' ) { + + if( $cmd eq "set") { + if ($attrVal =~ '(webp|png|jpg)$' ) { + $hash->{helper}{MAP_PATH} = $attrVal; + $hash->{helper}{MAP_MIME} = "image/".$1; + + if ($attrVal =~ /(\d+)x(\d+)/) { + CommandAttr($hash,"$name mapImageWidthHeight $1 $2"); + } + + readMap($hash); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + } else { + return "$iam $attrName wrong image type, use webp, png, jpeg or jpg"; + Log3 $name, 3, "$iam wrong image type, use webp, png, jpeg or jpg"; + } + + } elsif( $cmd eq "del" ) { + + $hash->{helper}{MAP_PATH} = ''; + $hash->{helper}{MAP_CACHE} = ''; + $hash->{helper}{MAP_MIME} = ''; + Log3 $name, 3, "$iam $cmd $attrName"; + + } + + ########## + } elsif ( $attrName eq 'numberOfWayPointsToDisplay' ) { + + my $icurr = @{$hash->{helper}{areapos}}; + if( $cmd eq "set" && $attrVal =~ /\d+/ && $attrVal > $hash->{helper}{MOWING}{maxLengthDefault}) { + + # reduce array + $hash->{helper}{MOWING}{maxLength} = $attrVal; + for ( my $i = $icurr; $i > $attrVal; $i-- ) { + pop @{$hash->{helper}{areapos}}; + } + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + # reduce array + my $imax = $hash->{helper}{MOWING}{maxLengthDefault}; + $hash->{helper}{MOWING}{maxLength} = $imax; + for ( my $i = $icurr; $i > $imax; $i-- ) { + pop @{$hash->{helper}{areapos}}; + } + Log3 $name, 3, "$iam $cmd $attrName default $imax"; + + } + ########## + } elsif( $attrName eq "mapImageCoordinatesUTM" ) { + + if( $cmd eq "set" ) { + + if ( AttrVal( $name,'mapImageCoordinatesToRegister', '' ) && $attrVal =~ /(\d*\.?\d+)\s(\d*\.?\d+)(\R|\s)(\d*\.?\d+)\s(\d*\.?\d+)/ ) { + + my ( $x1, $y1, $x2, $y2 ) = ( $1, $2, $4, $5 ); + AttrVal( $name,'mapImageCoordinatesToRegister', '' ) =~ /(\d*\.?\d+)\s(\d*\.?\d+)(\R|\s)(\d*\.?\d+)\s(\d*\.?\d+)/; + my ( $lo1, $la1, $lo2, $la2 ) = ( $1, $2, $4, $5 ); + my $scx = int( ( $x1 - $x2) / ( $lo1 - $lo2 ) ); + my $scy = int( ( $y1 - $y2 ) / ( $la1 - $la2 ) ); + CommandAttr($hash,"$name scaleToMeterXY $scx $scy"); + + } else { + return "$iam $attrName has a wrong format use linewise pairs or the attribute mapImageCoordinatesToRegister was not set before."; + } + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default 0 9090 0"; + + } + ########## + } elsif( $attrName eq "mapImageCoordinatesToRegister" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName has a wrong format use linewise pairs " unless($attrVal =~ /(\d*\.?\d+)\s(\d*\.?\d+)(\R|\s)(\d*\.?\d+)\s(\d*\.?\d+)/); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default 0 9090 0"; + + } + ########## + } elsif( $attrName eq "chargingStationCoordinates" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName has a wrong format use " unless($attrVal =~ /(\d*\.?\d+)\s(\d*\.?\d+)/); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default 10.1165 51.28"; + + } + ########## + } elsif( $attrName eq "mapImageWidthHeight" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName has a wrong format use " unless($attrVal =~ /(\d+)\s(\d+)/); + Log3 $name, 3, "$iam $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set default 100 200"; + + } + ########## + } elsif( $attrName eq "scaleToMeterXY" ) { + + if( $cmd eq "set" ) { + + return "$iam $attrName has a wrong format use " unless($attrVal =~ /(\d+)\s(\d+)/); + Log3 $name, 3, "$iam - $cmd $attrName $attrVal"; + + } elsif( $cmd eq "del" ) { + + Log3 $name, 3, "$iam $cmd $attrName and set to default: $hash->{helper}{scaleToMeterLongitude} $hash->{helper}{scaleToMeterLatitude}"; + + } + ########## + } elsif( $attrName eq "mowerSchedule" ) { + if( $cmd eq "set" ) { + + my $perl = eval { decode_json ($attrVal) }; + + if ($@) { + return "$iam $attrName decode error: $@ \n $perl"; + } + my $json = eval { encode_json ($perl) }; + if ($@) { + return "$iam $attrName encode error: $@ \n $json"; + } + Log3 $name, 4, "$iam $cmd $attrName array"; + + } + } + return undef; +} + + +######################### +sub Undef { + my ( $hash, $arg ) = @_; + my $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + RemoveExtension("$type/$name/map"); + 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-1; $i++ ) { + 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 ) { + $i++ if ($i == $poslen-2); + # 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); + } + #callFn if present + if ($hash->{helper}{$activity}{callFn}) { + $hash->{helper}{$activity}{cnt} = $i; + no strict "refs"; + &{$hash->{helper}{$activity}{callFn}}($hash); + use strict "refs"; + } + 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} = $xm; + $hash->{helper}{chargingStation}{latitude} = $ym; + readingsSingleUpdate($hash, "statistics_ChargingStationPositionXYn", (int($xm * 10000000 + 0.5) / 10000000).", ".(int($ym * 10000000 + 0.5) / 10000000).", ".$n, 0); + return undef; +} + +######################### +sub AreaStatistics { + my ($hash) = @_; + my $name = $hash->{NAME}; + 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; + } + $asum = $lsum * AttrVal($name,'mowerCuttingWidth',0.24); + my $td = $xyarr[ 0 ]{storedTimestamp} - $xyarr[ $k ]{storedTimestamp}; + $vm = int($lsum / $td * 1000000 + 0.5)/1000 if ($td); + $lsum += int( ReadingsNum( $name, 'statistics_currentDayTrack', 0 ) ); + $asum += int( ReadingsNum( $name, 'statistics_currentDayArea', 0 ) ); + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged($hash,'statistics_currentDayTrack', int($lsum)); # m + readingsBulkUpdateIfChanged($hash,'statistics_currentDayArea', int($asum)); # qm + readingsBulkUpdateIfChanged($hash,'statistics_lastIntervalMowerSpeed', $vm); # m/s + readingsBulkUpdateIfChanged($hash,'statistics_lastIntervalNumberOfWayPoints', $i-1); # m/s + readingsEndUpdate($hash,1); + 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."; + } +} + +############################################################## + +1; + + +=pod + +=item device +=item summary Module to control Husqvarnas robotic lawn mowers with Connect Module (SIM) +=item summary_DE Modul zur Steuerung von Husqvarnas Mähroboter mit Connect Modul (SIM) + +=begin html + + +

AutomowerConnectDevice

+
    + FHEM-FORUM: AutomowerConnect und AutomowerConnectDevice +

    + Introduction +

    +
      +
    • This module uses an entity of the AutomowerConnect Module as host, to control additional Husqvarna Automower equipped with Connect Module (SIM) and if their data hosted there.
    • +
    • The entities of this module are FHEM devices of additional mowers.
    • +
    • This module is nessesary only when additional mowers registered under the same application key.
    • +
    • The mower path is shown in the detail view.
    • +
    • The number of way points to display is customizable.
    • +
    • An arbitrary map can be used as background for the mower path.
    • +
    • The map has to be a raster image in webp, png or jpg format.
    • +
    • It's possible to control everything the API offers, e.g. schedule, headlight, cutting height and actions like start, pause, park etc.
    • +
    • All API data is stored in the device hash, the last and the second last one. Use {Dumper $defs{<name>}} in the commandline to find the data and build userReadings out of it.

    • +
    + Requirements +

    +
      +
    • An active entity (device, host) of the AutomowerConnect module is required It has to run with an application key under which more than one mower is registered.
    • +
    • Readings and state connected are shown after a host's update.
    • +
    +
    + + Define +
      + define <device name> AutomowerConnectDevice <host name> <mower number>
      + Example:
      + define myAdditionalMower AutomowerConnectDevice myMower 1 the host name is myMower and the mower number is 1.
      +

      +
    +
    + + + Set +
      +
    • Park
      + set <name> Park <number of minutes>
      + Parks mower in charging station for <number of minutes>
    • +
    • ParkUntilFurtherNotice
      + set <name> ParkUntilFurtherNotice
      + Parks mower in charging station until further notice
    • +
    • ParkUntilNextSchedule
      + set <name> ParkUntilNextSchedule
      + Parks mower in charging station and starts with next planned start
    • +
    • Pause
      + set <name> Pause
      + Pauses mower immediately at current position
    • +
    • ResumeSchedule
      + set <name> ResumeSchedule
      + Starts immediately if in planned intervall, otherwise with next scheduled start>
    • +
    • Start
      + set <name> Start <number of minutes>
      + Starts immediately for <number of minutes>
    • +
    • chargingStationPositionToAttribute
      + set <name> chargingStationPositionToAttribute
      + Sets the calculated charging station coordinates to the corresponding attributes.
    • +
    • cuttingHeight
      + set <name> cuttingHeight <1..9>
      + Sets the cutting height. NOTE: Do not use for 550 EPOS and Ceora.
    • +
    • headlight
      + set <name> headlight <ALWAYS_OFF|ALWAYS_ON|EVENIG_ONLY|EVENING_AND_NIGHT>
      +
    • +
    • mowerScheduleToAttrbute
      + set <name> mowerScheduleToAttrbute
      + Writes the schedule in to the attribute moverSchedule.
    • +
    • sendScheduleFromAttributeToMower
      + set <name> sendScheduleFromAttributeToMower
      + Sends the schedule to the mower. NOTE: Do not use for 550 EPOS and Ceora.
    • + +

    • + set <name>
      +
    • + +
    +
    + + + Get +
      +
    • html
      + get <name> html
      + Returns the mower area image as html code. For use in uiTable, TabletUI, Floorplan, readingsGroup, weblink etc.
    • +

      +
    +
    + + + Attributes +
      + +
    • mapImagePath
      + attr <name> mapImagePath <path to image>
      + Path of a raster image file for an area the mower path has to be drawn to.
      + If the image name implies the image size by containing a part which matches /(\d+)x(\d+)/
      + the corresponding attribute will be set to mapImageWidthHeight = '$1 $2'
      + Image name example: map740x1300.webp
    • + +
    • mapImageWidthHeight
      + attr <name> mapImageWidthHeight <width in pixel><separator><height in pixel>
      + Width and Height in pixel of a raster image file for an area image the mower path has to be drawn to. <separator> is one space character.
    • + +
    • mapImageZoom
      + attr <name> mapImageZoom <height in pixel>
      + Zoom of a raster image for an area the mower path has to be drawn to. Default: 0.5
    • + +
    • mapImageCoordinatesToRegister
      + attr <name> mapImageCoordinatesToRegister <upper left longitude><space><upper left latitude><line feed><lower right longitude><space><lower right latitude>
      + Upper left and lower right coordinates to register (or to fit to earth) the image. Format: linewise longitude and latitude values separated by 1 space.
      + The lines are splitted by (/\s|\R$/). Use WGS84 (GPS) coordinates in decimal degree notation.
    • + +
    • mapImageCoordinatesUTM
      + attr <name> mapImageCoordinatesUTM <upper left longitude><space><upper left latitude><line feed><lower right longitude><space><lower right latitude>
      + Upper left and lower right coordinates to register (or to fit to earth) the image. Format: linewise longitude and latitude values separated by 1 space.
      + The lines are splitted by (/\s|\R$/). Use UTM coordinates in meter notation.
      + 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
      + Shows Map on (1 default) or not (0).
    • + +
    • chargingStationCoordinates
      + attr <name> chargingStationCoordinates <longitude><separator><latitude>
      + Longitude and latitude of the charging station. Use WGS84 (GPS) coordinates in decimal degree notation. <separator> is one space character
    • + +
    • chargingStationImagePosition
      + attr <name> chargingStationImagePosition <right, bottom, left, top, center>
      + Position of the charging station image relative to its coordinates.
    • + +
    • mowerCuttingWidth
      + attr <name> mowerCuttingWidth <cutting width>
      + mower cutting width in meter to calculate the mowed area. default: 0.24
    • + +
    • mowerSchedule
      + attr <name> mowerSchedule <schedule array>
      + This attribute provides the possebility to edit the mower schedule in form of an JSON array.
      The actual schedule can be loaded with the command set <name> mowerScheduleToAttribute.
      The command set <name> sendScheduleFromAttributeToMower sends the schedule to the mower. The maximum of array elements is 14 and 2 each day, so every day of a week can have 2 time spans. Each array element consists of 7 unsorted day values (monday to sunday) which can be true or false, a start and duration value in minutes. Start time counts from midnight. NOTE: Do not use for 550 EPOS and Ceora. Delete the attribute after the schedule is successfully uploaded.
    • + +
    • mowingAreaLimits
      + attr <name> mowingAreaLimits <positions list>
      + List of position describing the area to mow. Format: linewise pairs of longitude and latitude values separated by 1 space. The lines are splitted by (/\s|\R$/).
      The position values could be taken from Google Earth KML file, but whithout the altitude values.
    • + +
    • propertyLimits
      + attr <name> propertyLimits <positions list>
      + List of position describing the property limits. Format: linewise pairs of longitude and latitude values separated by 1 space. The lines are splitted by (/\s|\R$/).The position values could be taken from . For converting UTM32 meter to ETRS89 decimal degree you can use the BKG-Geodatenzentrum .
    • + +
    • numberOfWayPointsToDisplay
      + attr <name> numberOfWayPointsToDisplay <number of way points>
      + Set the number of way points stored and displayed, default 500
    • + +
    • scaleToMeterXY
      + attr <name> scaleToMeterXY <scale factor longitude><seperator><scale factor latitude>
      + The scale factor depends from the Location on earth, so it has to be calculated for short ranges only. <seperator> is one space character.
      + Longitude: (LongitudeMeter_1 - LongitudeMeter_2) / (LongitudeDegree_1 - LongitudeDegree _2)
      + Latitude: (LatitudeMeter_1 - LatitudeMeter_2) / (LatitudeDegree_1 - LatitudeDegree _2)
    • + + +

    • + attr <name> <>
      +
    • +
    +
    + + + Readings +
      +
    • batteryPercent - Battery power in percent
    • +
    • mower_activity - current activity "UNKNOWN" | "NOT_APPLICABLE" | "MOWING" | "GOING_HOME" | "CHARGING" | "LEAVING" | "PARKED_IN_CS" | "STOPPED_IN_GARDEN"
    • +
    • mower_commandStatus - Status of the last sent command cleared each status update
    • +
    • mower_errorCode - last error code
    • +
    • mower_errorCodeTimestamp - last error code time stamp
    • +
    • mower_errorDescription - error description
    • +
    • mower_id - ID of the mower
    • +
    • 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_overrideAction - reason for override a planned action NOT_ACTIVE, FORCE_PARK, FORCE_MOW
    • +
    • positions_lastLatitude - last known position (latitude)
    • +
    • positions_lastLongitude - last known position (longitude)
    • +
    • state - status of connection FHEM to Husqvarna Cloud API and device state(e.g. defined, authorization, authorized, connected, error, update)
    • +
    • status_statusTimestampOld - local time of second last change of the API content
    • +
    • settings_cuttingHeight - actual cutting height from API
    • +
    • settings_headlight - actual headlight mode from API
    • +
    • statistics_ChargingStationPositionXYn - calculated position of the carging station (longitude, latitude, number of datasets) during mower_activity PARKED_IN_CS and CHARGING
    • +
    • statistics_numberOfChargingCycles - number of charging cycles
    • +
    • statistics_numberOfCollisions - number of collisions
    • +
    • statistics_totalChargingTime - total charging time in hours
    • +
    • statistics_totalCuttingTime - total cutting time in hours
    • +
    • statistics_totalRunningTime - total running time in hours
    • +
    • statistics_totalSearchingTime - total searching time in hours
    • +
    • statistics_currentDayTrack - calculated mowed track length in meter during mower_activity MOWING since midnight
    • +
    • statistics_currentDayArea - calculated mowed area in square meter during mower_activity MOWING since midnight
    • +
    • statistics_lastIntervalNumberOfWayPoints - last Intervals Number of way points
    • +
    • statistics_currentMowerSpeed - calculated mower speed in meter per second during mower_activity MOWING for the last interval
    • +
    • statistics_lastDayTrack - calculated mowed track length in meter during mower_activity MOWING for yesterday
    • +
    • statistics_lastDayArea - calculated mowed area in square meter during mower_activity MOWING for yesterday
    • +
    • statistics_currentWeekTrack - calculated mowed track length in meter during mower_activity MOWING of the current week
    • +
    • statistics_currentWeekArea - calculated mowed area in square meter during mower_activity MOWING of the current week
    • +
    • statistics_lastWeekTrack - calculated mowed track length in meter during mower_activity MOWING of the last week
    • +
    • statistics_lastWeekArea - calculated mowed area in square meter during mower_activity MOWING of the last week
    • +
    • status_connected - state of connetion between mower and Husqvarna Cloud, (1 => true, 0 => false)
    • +
    • 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
    • +
    • system_name - name of the mower
    • +
    • system_model - model of the mower
    • +
    • system_serialNumber - serial number of the mower
    • + +
    +
+ +=end html + + + +=begin html_DE + + +

AutomowerConnectDevice

+
    + FHEM-FORUM: AutomowerConnect und AutomowerConnectDevice
    + FHEM-Wiki: AutomowerConnect und AutomowerConnectDevice: Wie erstellt man eine Karte des Mähbereiches? + +

    + Einleitung +

    +
      +
    • Dieses Modul nutzt eine Istanz des AutomowerConnect Moduls als Host, um einen weiteren Husqvarna Automower, dessen Daten dort gehostet werden und der mit einem Connect Modul (SIM) ausgerüstet ist, zu steuern.
    • +
    • Die Instanzen dieses Moduls bilden die FHEM-Geräte weiterer Mähroboter.
    • +
    • Dieses Modul wird also erst benötigt, wenn mehrere Mähroboter unter einem Application Key registriert sind.
    • +
    • Der Pfad des Mähroboters wird in der Detailansicht des FHEMWEB Frontends angezeigt.
    • +
    • Die Zahl der anzuzeigenden Wegpunkte des Pfades kann frei gewählt werden.
    • +
    • Der Pfad kann mit einer beliebigen Karte hinterlegt werden.
    • +
    • Die Karte muss als Rasterbild im webp, png oder jpg Format vorliegen.
    • +
    • Es ist möglich alles was Die API anbietet zu steuern, z.B. Mähplan,Scheinwerfer, Schnitthöhe und Aktionen wie, Start, Pause, Parken usw.
    • +
    • Die letzten und vorletzten Daten aus dem Host sind im Gerätehash gespeichert, Mit {Dumper $defs{<device name>}} in der Befehlszeile können die Daten angezeigt werden und daraus userReadings erstellt werden.

    • +
    + Anforderungen +

    +
      +
    • Es wird eine aktive Instanz (Device, FHEM-Gerät) des Moduls AutomowerConnect vorausgesetzt, die mit einem Application Key läuft unter dem mindestens zwei Husqvarna Automower registriert sind..
    • +
    • Readings und der Status connected wird erst angezeigt, wenn in dem Hostgerät ein Update erfolgt ist..
    • +
    +
    + + Define +
      + define <device name> AutomowerConnectDevice <host name> <mower number>
      + Beispiel:
      + define myAdditionalMower AutomowerConnectDevice myMower 1 myMower ist der Hostname und 1 ist die Nummer des anzuzeigenden Mähers.
      +

      +
    +
    + + + Set +
      +
    • Park
      + set <name> Park <number of minutes>
      + Parkt den Mäher in der Ladestation (LS) für <number of minutes>
    • +
    • ParkUntilFurtherNotice
      + set <name> ParkUntilFurtherNotice
      + Parkt den Mäher bis auf Weiteres in der LS
    • +
    • ParkUntilNextSchedule
      + set <name> ParkUntilNextSchedule
      + Parkt den Mäher bis auf Weiteres in der LS und startet zum nächsten geplanten Zeitpunkt
    • +
    • Pause
      + set <name> Pause
      + Pausiert den Mäher sofort am aktuellen Standort
    • +
    • ResumeSchedule
      + set <name> ResumeSchedule
      + Startet im geplanten Interval den Mäher sofort, sonst zum nächsten geplanten Zeitpunkt
    • +
    • Start
      + set <name> Start <number of minutes>
      + Startet sofort für <number of minutes>
    • +
    • chargingStationPositionToAttribute
      + set <name> chargingStationPositionToAttribute
      + Setzt die berechneten Koordinaten der LS in das entsprechende Attribut.
    • +
    • cuttingHeight
      + set <name> cuttingHeight <1..9>
      + Setzt die Schnitthöhe. HINWEIS: Nicht für 550 EPOS und Ceora geeignet.
    • +
    • headlight
      + set <name> headlight <ALWAYS_OFF|ALWAYS_ON|EVENIG_ONLY|EVENING_AND_NIGHT>
      + Setzt den Scheinwerfermode
    • +
    • mowerScheduleToAttrbute
      + set <name> mowerScheduleToAttrbute
      + Schreibt den Mähplan ins Attribut moverSchedule.
    • +
    • sendScheduleFromAttributeToMower
      + set <name> sendScheduleFromAttributeToMower
      + Sendet den Mähplan zum Mäher. HINWEIS: Nicht für 550 EPOS und Ceora geeignet.
    • + +

    • + set <name>
      +
    • + + + Get +
        +
      • html
        + get <name> html
        + Gibt das Bild des Mäherbereiches html kodiert zurück, zur Verwendung in uiTable, TabletUI, Floorplan, readingsGroup, weblink usw.
      • +

        +
      +
      + +
    +
    + + Attributes +
      + +
    • mapImagePath
      + attr <name> mapImagePath <path to image>
      + Pfad zur Bilddatei. Auf das Bild werden Pfad, Anfangs- u. Endpunkte gezeichnet.
      + Wenn der Bildname die Bildgröße impliziert indem er zu dem regulären Ausdruck /(\d+)x(\d+)/ passt,
      + wird das zugehörige Attribut gesetzt mapImageWidthHeight = '$1 $2'
      + Beispiel Bildname: map740x1300.webp
    • + +
    • mapImageWidthHeight
      + attr <name> mapImageWidthHeight <width in pixel><separator><height in pixel>
      + Bildbreite in Pixel des Bildes auf das Pfad, Anfangs- u. Endpunkte gezeichnet werden. <separator> ist 1 Leerzeichen.
    • + +
    • mapImageHeight
      + attr <name> <>
      + Bildhöhe in Pixel des Bildes auf das Pfad, Anfangs- u. Endpunkte gezeichnet werden.
    • + +
    • mapImageZoom
      + attr <name> mapImageHeight <height in pixel>
      + Zoomfaktor zur Salierung des Bildes auf das Pfad, Anfangs- u. Endpunkte gezeichnet werden. Standard: 0.5
    • + +
    • mapImageCoordinatesToRegister
      + attr <name> mapImageCoordinatesToRegister <upper left longitude><space><upper left latitude><line feed><lower right longitude><space><lower right latitude>
      + Obere linke und untere rechte Ecke der Fläche auf der Erde, die durch das Bild dargestellt wird um das Bild auf der Fläche zu registrieren (oder einzupassen).
      + Format: Zeilenweise Paare von Longitude- u. Latitudewerten getrennt durch 1 Leerzeichen. Die Zeilen werden aufgeteilt durch (/\s|\R$/).
      + Angabe der WGS84 (GPS) Koordinaten als Deimalgrad.
    • + +
    • mapImageCoordinatesUTM
      + attr <name> mapImageCoordinatesUTM <upper left longitude><space><upper left latitude><line feed><lower right longitude><space><lower right latitude>
      + Obere linke und untere rechte Ecke der Fläche auf der Erde, die durch das Bild dargestellt wird um das Bild auf der Fläche zu registrieren (oder einzupassen).
      + Format: Zeilenweise Paare von Longitude- u. Latitudewerten getrennt durch 1 Leerzeichen. Die Zeilen werden aufgeteilt durch (/\s|\R$/).
      + Die Angabe der UTM Koordinaten muss als Dezimalzahl in Meter erfolgen.
      + Das Attribut muss nach dem Attribut mapImageCoordinatesToRegister gesetzt werden.
      + Dieses Attribut berechnet die Skalierungsfaktoren. Das Attribut scaleToMeterXY wird entsprechend gesetzt
    • + +
    • showMap
      + attr <name> showMap <>1,0
      + Zeigt die Karte an (1 default) oder nicht (0).
    • + +
    • chargingStationCoordinates
      + attr <name> chargingStationCoordinates <longitude><separator><latitude>
      + Longitude und Latitude der Ladestation als WGS84 (GPS) Koordinaten als Deimalgrad. <separator> ist 1 Leerzeichen
    • + +
    • chargingStationImagePosition
      + attr <name> chargingStationImagePosition <right, bottom, left, top, center>
      + Position der Ladestation relativ zu ihren Koordinaten.
    • + +
    • mowerCuttingWidth
      + attr <name> mowerCuttingWidth <cutting width>
      + Schnittbreite in Meter zur Berechnung der gemähten Fläche. default: 0.24
    • + +
    • mowerSchedule
      + attr <name> mowerSchedule <schedule array>
      + Dieses Attribut bietet die Möglichkeit den Mähplan zu ändern, er liegt als JSON Array vor.
      Der aktuelleMähplan kann mit dem Befehl set <name> mowerScheduleToAttrbute ins Attribut geschrieben werden.
      Der Befehl set <name> sendScheduleFromAttributeToMower sendet den Mähplan an den Mäher. Das Maximum der Arrayelemente beträgt 14, 2 für jeden Tag, so daß jeden Tag zwei Intervalle geplant werden können. Jedes Arrayelement besteht aus 7 unsortierten Tageswerten (monday bis sunday) die auf true oder false gesetzt werden können, einen start Wert und einen duration Wert in Minuten. Die Startzeit start wird von Mitternacht an gezählt. HINWEIS: Nicht für 550 EPOS und Ceora geeignet.
    • + +
    • mowingAreaLimits
      + attr <name> mowingAreaLimits <positions list>
      + Liste von Positionen, die den Mähbereich beschreiben. Format: Zeilenweise Paare von Longitude- u. Latitudewerten getrennt durch 1 Leerzeichen. Die Zeilen werden aufgeteilt durch (/\s|,|\R$/).
      Die Liste der Positionen kann aus einer mit Google Earth erzeugten KML-Datei entnommen werden, ohne Höhenangaben zu übernehmen
    • + +
    • propertyLimits
      + attr <name> propertyLimits <positions list>
      + Liste von Positionen, um die Grundstücksgrenze zu beschreiben. Format: Zeilenweise Paare von Longitude- u. Latitudewerten getrennt durch 1 Leerzeichen. Eine Zeile wird aufgeteilt durch (/\s|,|\R$/).
      Die genaue Position der Grenzpunkte kann man über die Geoportale der Länder finden. Eine Umrechnung der UTM32 Daten in Meter nach ETRS89 in Dezimalgrad kann über das BKG-Geodatenzentrum erfolgen.
    • + +
    • numberOfWayPointsToDisplay
      + attr <name> numberOfWayPointsToDisplay <number of way points>
      + Legt die Anzahl der gespeicherten und anzuzeigenden Wegpunkte fest, default 500
    • + +
    • scaleToMeterXY
      + attr <name> scaleToMeterXY <scale factor longitude><seperator><scale factor latitude>
      + Der Skalierfaktor hängt vom Standort ab und muss daher für kurze Strecken berechnet werden. <seperator> ist 1 Leerzeichen.
      + Longitude: (LongitudeMeter_1 - LongitudeMeter_2) / (LongitudeDegree_1 - LongitudeDegree _2)
      + Latitude: (LatitudeMeter_1 - LatitudeMeter_2) / (LatitudeDegree_1 - LatitudeDegree _2)
    • + + +

    • + attr <name> <>
      +
    • + +
    +
    + + + Readings +
      +
    • batteryPercent - Batteryladung in Prozent (ohne %-Zeichen)
    • +
    • mower_activity - aktuelle Aktivität "UNKNOWN" | "NOT_APPLICABLE" | "MOWING" | "GOING_HOME" | "CHARGING" | "LEAVING" | "PARKED_IN_CS" | "STOPPED_IN_GARDEN"
    • +
    • mower_commandStatus - Status des letzten uebermittelten Kommandos wird duch Statusupdate zurückgesetzt.
    • +
    • mower_errorCode - last error code
    • +
    • mower_errorCodeTimestamp - last error code time stamp
    • +
    • mower_errorDescription - error description
    • +
    • mower_id - ID des Automowers
    • +
    • 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_overrideAction - Grund für vorrangige Aktion NOT_ACTIVE, FORCE_PARK, FORCE_MOW
    • +
    • positions_lastLatitude - letzte bekannte Position (Breitengrad)
    • +
    • positions_lastLongitude - letzte bekannte Position (Längengrad)
    • +
    • state - Status der Verbindung des FHEM-Gerätes zur Husqvarna Cloud API (defined, connected).
    • +
    • settings_cuttingHeight - aktuelle Schnitthöhe aus der API
    • +
    • settings_headlight - aktueller Scheinwerfermode aus der API
    • +
    • statistics_ChargingStationPositionXYn - berechnete Position der Ladestation mit den Werten Longitude, Latitude und Anzahl der verwendeten Datensätze wähend der Mower_activity PARKED_IN_CS und CHARGING
    • +
    • statistics_numberOfChargingCycles - Anzahl der Ladezyklen
    • +
    • statistics_numberOfCollisions - Anzahl der Kollisionen
    • +
    • statistics_totalChargingTime - Gesamtladezeit in Stunden
    • +
    • statistics_totalCuttingTime - Gesamtschneidezeit in Stunden
    • +
    • statistics_totalRunningTime - Gesamtlaufzeit in Stunden
    • +
    • statistics_totalSearchingTime - Gesamtsuchzeit in Stunden
    • +
    • statistics_currentDayTrack - berechnete gefahrene Strecke in Meter bei_Activity MOWING seit Mitternacht
    • +
    • statistics_currentDayArea - berechnete übermähte Fläche in Quadratmeter bei der Activity MOWING seit Mitternacht
    • +
    • statistics_lastIntervalNumberOfWayPoints - Anzahl der Wegpunkte im letzten Interval
    • +
    • statistics_currentMowerSpeed - berechnet Geschwindigkeit in Meter pro Sekunde bei der_Activity MOWING im letzten Interval
    • +
    • statistics_lastDayTrack - berechnete gefahrene Strecke in Meter bei_Activity MOWING des letzten Tages
    • +
    • statistics_lastDayArea - berechnete übermähte Fläche in Quadratmeter bei der Activity MOWING des letzten Tages
    • +
    • statistics_currentWeekTrack - berechnete gefahrene Strecke in Meter bei_Activity MOWING
    • +
    • statistics_currentWeekArea - berechnete übermähte Fläche in Quadratmeter bei der Activity MOWING der laufenden Woche
    • +
    • statistics_lastWeekTrack - berechnete gefahrene Strecke in Meter bei_Activity MOWING der letzten Woche
    • +
    • statistics_lastWeekArea - berechnete übermähte Fläche in Quadratmeter bei der Activity MOWING der letzten Woche
    • +
    • status_connected - Status der Verbindung zwischen dem Automower und der Husqvarna Cloud, (1 => true, 0 => false)
    • +
    • 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
    • +
    • system_name - Name des Automowers
    • +
    • system_model - Model des Automowers
    • +
    • system_serialNumber - Seriennummer des Automowers
    • +
    +
+ +=end html_DE diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index dfb302d18..055062f65 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -404,6 +404,7 @@ FHEM/73_NUKIBridge.pm CoolTux Sonstige Systeme FHEM/73_PRESENCE.pm JoWiemann Unterstützende Dienste FHEM/73_WaterCalculator Sailor Unterstützende Dienste https://forum.fhem.de/index.php/topic,58579.0.html FHEM/74_AMADDevice.pm CoolTux Sonstige Systeme +FHEM/74_AutomowerConnect.pm Ellert Sonstige Systeme https://forum.fhem.de/index.php/topic,131661.0.html FHEM/74_GardenaSmartDevice.pm CoolTux Sonstige Systeme FHEM/74_HOMBOT.pm CoolTux sonstige Systeme FHEM/74_HusqvarnaAutomower.pm krannich Sonstige Systeme @@ -416,6 +417,7 @@ FHEM/74_UnifiClient.pm wuehler Automatisierung FHEM/74_UnifiProtect.pm justme1968 Sonstiges FHEM/74_UnifiVideo.pm justme1968 Sonstiges FHEM/74_XiaomiBTLESens CoolTux Sonstige Systeme +FHEM/75_AutomowerConnectDevice.pm Ellert Sonstige Systeme https://forum.fhem.de/index.php/topic,131661.0.html FHEM/75_MSG.pm loredo Automatisierung FHEM/75_msgConfig.pm loredo Automatisierung FHEM/76_msgDialog.pm orphan/Beta-User Frontends/Sprachsteuerung https://forum.fhem.de/index.php/topic,125710.0.html @@ -503,7 +505,7 @@ FHEM/98_CustomReadings.pm HCS Unterstützende Dienste FHEM/98_dewpoint.pm hotbso Automatisierung FHEM/98_DLNARenderer.pm dominikkarall Multimedia FHEM/98_DOIF.pm damian-s Automatisierung/DOIF -FHEM/98_DOIFtools.pm Ellert Automatisierung/DOIF +FHEM/98_DOIFtools.pm Ellert Automatisierung/DOIF https://forum.fhem.de/index.php/topic,63938.0.html FHEM/98_Dooya.pm Jarnsen/ralf9/darkmission Sonstige Systeme FHEM/98_dummy.pm rudolfkoenig Automatisierung FHEM/98_DSBMobile KernSani Codeschnipsel https://forum.fhem.de/index.php/topic,107104.0.html @@ -667,6 +669,7 @@ contrib/Widgets/DateTimePicker* Matscher Frontends contrib/Wzut/* Wzut MAX contrib/YAF/* MarcP Frontends +www/pgm2/automowerconnect.js Ellert Frontends www/codemirror/* rapster Frontends www/frontend/* johannnes Frontends www/gplot/* rudolfkoenig Frontends/SVG/Plots/logProxy diff --git a/fhem/www/pgm2/automowerconnect.js b/fhem/www/pgm2/automowerconnect.js new file mode 100644 index 000000000..68ab1fcf1 --- /dev/null +++ b/fhem/www/pgm2/automowerconnect.js @@ -0,0 +1,218 @@ + +FW_version["automowerconnect.js"] = "$Id$"; + +function AutomowerConnectLimits( ctx, pos, format ) { +// log("array length: "+pos.length); + if ( pos.length > 3 ) { + // draw limits + ctx.beginPath(); + + if ( format == 0 ) { + ctx.lineWidth=1; + ctx.strokeStyle = '#ff8000'; + ctx.setLineDash([]); + } + if ( format == 1 ) { + ctx.lineWidth=1; + ctx.strokeStyle = '#33cc33'; + ctx.setLineDash([]); + } + + ctx.moveTo(parseInt(pos[0]),parseInt(pos[1])); + for (var i=2;i < pos.length - 1; i+=2 ) { + ctx.lineTo(parseInt(pos[i]),parseInt(pos[i+1])); + } + ctx.lineTo(parseInt(pos[0]),parseInt(pos[1])); + ctx.stroke(); + + // limits connector + if ( format == 1 ) { + for (var i=0;i < pos.length - 1; i+=2 ) { + ctx.beginPath(); + ctx.setLineDash([]); + ctx.lineWidth=1; + ctx.strokeStyle = '#33cc33'; + ctx.fillStyle= 'white'; + ctx.moveTo(parseInt(pos[i]),parseInt(pos[i+1])); + ctx.arc(parseInt(pos[i]), parseInt(pos[i+1]), 2, 0, 2 * Math.PI, false); + ctx.fill(); + ctx.stroke(); + } + } + } +} + +function AutomowerConnectScale( ctx, picx, picy, scalx ) { + // draw scale + ctx.beginPath(); + ctx.lineWidth=2; + ctx.setLineDash([]); + const l = 10; + const scam = picx / scalx; + ctx.moveTo(picx-l*scam-30, picy-30); + ctx.lineTo(picx-l*scam-30,picy-20); + ctx.lineTo(picx-30,picy-20); + ctx.moveTo(picx-30, picy-30); + ctx.lineTo(picx-30,picy-20); + ctx.moveTo(picx-(l/2)*scam-30, picy-26); + ctx.lineTo(picx-(l/2)*scam-30, picy-20); + ctx.strokeStyle = '#ff8000'; + ctx.stroke(); + ctx.beginPath(); + ctx.lineWidth = 1; + for (var i=1;i 0 && parseInt(csy) > 0) { + // draw chargingstation + ctx.beginPath(); + ctx.setLineDash([]); + ctx.lineWidth=3; + ctx.strokeStyle = '#ffffff'; + ctx.fillStyle= '#3d3d3d'; + if (csrel == 'right') ctx.arc(parseInt(csx)+13, parseInt(csy), 13, 0, 2 * Math.PI, false); + if (csrel == 'bottom') ctx.arc(parseInt(csx), parseInt(csy)+13, 13, 0, 2 * Math.PI, false); + if (csrel == 'left') ctx.arc(parseInt(csx)-13, parseInt(csy), 13, 0, 2 * Math.PI, false); + if (csrel == 'top') ctx.arc(parseInt(csx), parseInt(csy)-13, 13, 0, 2 * Math.PI, false); + if (csrel == 'center') ctx.arc(parseInt(csx), parseInt(csy), 13, 0, 2 * Math.PI, false); + ctx.fill(); + ctx.stroke(); + + ctx.font = "16px Arial"; + ctx.fillStyle = "#f15422"; + ctx.textAlign = "center"; + if (csrel == 'right') ctx.fillText("CS", parseInt(csx)+13, parseInt(csy)+6); + if (csrel == 'bottom') ctx.fillText("CS", parseInt(csx), parseInt(csy)+6+13); + if (csrel == 'left') ctx.fillText("CS", parseInt(csx)-13, parseInt(csy)+6); + if (csrel == 'top') ctx.fillText("CS", parseInt(csx), parseInt(csy)+6-13); + if (csrel == 'center') ctx.fillText("CS", parseInt(csx), parseInt(csy)+6); + + // draw mark + ctx.beginPath(); + ctx.setLineDash([]); + ctx.lineWidth=1; + ctx.strokeStyle = '#f15422'; + ctx.fillStyle= '#3d3d3d'; + ctx.arc( parseInt(csx), parseInt(csy), 2, 0, 2 * Math.PI, false); + ctx.fill(); + ctx.stroke(); + } +} + +function AutomowerConnectChargingStationPath ( ctx, pos ) { + // draw path + ctx.beginPath(); + ctx.lineWidth=1; + ctx.setLineDash([6, 2]); + ctx.strokeStyle = '#999999'; + ctx.moveTo(parseInt(pos[0]),parseInt(pos[1])); + for (var i=2;i, , , , ,scalx , , ) +function AutomowerConnectUpdateDetail (dev, type, imgsrc, picx, picy, csx, csy, csrel, scalx, pos, lixy, plixy, posc) { +// 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'); + if (canvas) { +// log('loop: canvas true '+ type+' '+dev ); + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // draw limits + if ( lixy.length > 0 ) AutomowerConnectLimits( ctx, lixy, 0 ); + if ( plixy.length > 0 ) AutomowerConnectLimits( ctx, plixy, 1 ); + // draw scale + AutomowerConnectScale( ctx, picx, picy, scalx ); + // draw charging station path + AutomowerConnectChargingStationPath ( ctx, posc ); + + if ( pos.length > 4 ) { + // draw path + ctx.beginPath(); + ctx.lineWidth=1; + ctx.setLineDash([6, 2]); + ctx.strokeStyle = '#ff0000'; + ctx.moveTo(parseInt(pos[2]),parseInt(pos[3])); + for (var i=4;i{ +// log('loop: canvas false '+ type+' '+dev ); + AutomowerConnectUpdateDetail (dev, type, imgsrc, picx, picy, csx, csy, csrel, scalx, pos, lixy, plixy); + }, 100); + } + } else { + setTimeout(()=>{ +// log('loop: detail false '+ type+' '+dev ); + AutomowerConnectUpdateDetail (dev, type, imgsrc, picx, picy, csx, csy, csrel, scalx, pos, lixy, plixy); + }, 100); + } +}