############################################################################### # # $Id$ # # This script is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # any later version. # # The GNU General Public License can be found at # http://www.gnu.org/copyleft/gpl.html. # A copy is found in the textfile GPL.txt and important notices to the license # from the author is found in LICENSE.txt distributed with these scripts. # # This script is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # # # Husqvarnas Open API is used # based on some ideas from HusqvarnaAutomower and BOTVAC module # ################################################################################ package FHEM::AutomowerConnect; my $cvsid = '$Id$'; 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 IsDisabled Log3 Log minNum maxNum 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; require FHEM::Devices::AMConnect::Common; use constant AUTHURL => 'https://api.authentication.husqvarnagroup.dev/v1'; use constant APIURL => 'https://api.amc.husqvarna.dev/v1'; ############################################################## sub Initialize() { my ($hash) = @_; $hash->{DefFn} = \&FHEM::Devices::AMConnect::Common::Define; $hash->{GetFn} = \&FHEM::Devices::AMConnect::Common::Get; $hash->{UndefFn} = \&FHEM::Devices::AMConnect::Common::Undefine; $hash->{DeleteFn} = \&FHEM::Devices::AMConnect::Common::Delete; $hash->{RenameFn} = \&FHEM::Devices::AMConnect::Common::Rename; $hash->{FW_detailFn}= \&FHEM::Devices::AMConnect::Common::FW_detailFn; $hash->{SetFn} = \&Set; $hash->{AttrFn} = \&Attr; $hash->{AttrList} = "interval " . "disable:1,0 " . "debug:1,0 " . "disabledForIntervals " . "mapImagePath " . "mapImageWidthHeight " . "mapImageCoordinatesToRegister:textField-long " . "mapImageCoordinatesUTM:textField-long " . "mapImageZoom " . "mapBackgroundColor " . "mapDesignAttributes:textField-long " . "mapZones:textField-long " . "mowerActivityToHighLight:textField-long " . "showMap:1,0 " . "chargingStationCoordinates " . "chargingStationImagePosition:left,top,right,bottom,center " . "scaleToMeterXY " . "mowerCuttingWidth " . "mowerSchedule:textField-long " . "mowingAreaLimits:textField-long " . "propertyLimits:textField-long " . "weekdaysToResetWayPoints " . "numberOfWayPointsToDisplay " . $readingFnAttributes; $::data{FWEXT}{AutomowerConnect}{SCRIPT} = "automowerconnect.js"; return undef; } ############################################################## # # API AUTHENTICATION # ############################################################## sub APIAuth { my ($hash, $update) = @_; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my $iam = "$type $name APIAuth:"; my $interval = $hash->{helper}{interval}; ( $hash->{VERSION} ) = $cvsid =~ /\.pm (.*)Z/ if ( !$hash->{VERSION} ); if ( IsDisabled($name) ) { readingsSingleUpdate($hash,'state','disabled',1) if( ReadingsVal($name,'state','') ne 'disabled' ); RemoveInternalTimer( $hash, \&APIAuth ); InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 ); 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 { readingsSingleUpdate( $hash, 'state', 'authentification', 1 ); 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, }); } } 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 $statuscode = $param->{code} // ''; my $interval = $hash->{helper}{interval}; my $iam = "$type $name APIAuthResponse:"; Log3 $name, 1, "\ndebug $iam \n\$statuscode [$statuscode]\n\$err [$err],\n \$data [$data] \n\$param->url $param->{url}" if ( AttrVal($name, 'debug', '') ); if( !$err && $statuscode == 200 && $data) { my $result = eval { decode_json($data) }; if ($@) { Log3 $name, 2, "$iam JSON error [ $@ ]"; readingsSingleUpdate( $hash, 'state', 'error JSON', 1 ); } else { $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 ); 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; } } else { readingsSingleUpdate( $hash, 'state', "error statuscode $statuscode", 1 ); Log3 $name, 1, "\n$iam\n\$statuscode [$statuscode]\n\$err [$err],\n\$data [$data]\n\$param->url $param->{url}"; } RemoveInternalTimer( $hash, \&APIAuth ); InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 ); return undef; } ############################################################## # # GET MOWERS # ############################################################## sub getMower { my ($hash) = @_; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my $iam = "$type $name getMower:"; 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 $statuscode = $param->{code}; my $interval = $hash->{helper}{interval}; my $iam = "$type $name getMowerResponse:"; my $mowerNumber = $hash->{helper}{mowerNumber}; Log3 $name, 1, "\ndebug $iam \n\$statuscode [$statuscode]\n\$err [$err],\n \$data [$data] \n\$param->url $param->{url}" if ( AttrVal($name, 'debug', '') ); if( !$err && $statuscode == 200 && $data) { if ( $data eq "[]" ) { Log3 $name, 2, "$iam no mower data present"; } else { my $result = eval { decode_json($data) }; if ($@) { Log3( $name, 2, "$iam - JSON error while request: $@"); } else { $hash->{helper}{mowers} = $result->{data}; my $maxMower = 0; $maxMower = @{$hash->{helper}{mowers}} if ( ref ( $hash->{helper}{mowers} ) eq 'ARRAY' ); if ($maxMower <= $mowerNumber || $mowerNumber < 0 ) { Log3 $name, 2, "$iam wrong mower number $mowerNumber ($maxMower mower 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}) ){ # update dataset $hash->{helper}{mowerold} = dclone( $hash->{helper}{mower} ); } else { # first data set $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}{timestamps}[ 0 ] = $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp}; if ( AttrVal( $name, 'mapImageCoordinatesToRegister', '' ) eq '' ) { ::FHEM::Devices::AMConnect::Common::posMinMax( $hash, $hash->{helper}{mowerold}{attributes}{positions} ); } } $hash->{helper}{mower} = dclone( $hash->{helper}{mowers}[$mowerNumber] ); # add alignment data set (last matched search positions) to the end push( @{ $hash->{helper}{mower}{attributes}{positions} }, @{ dclone( $hash->{helper}{searchpos} ) } ); $hash->{helper}{newdatasets} = 0; my $storediff = $hash->{helper}{mower}{attributes}{metadata}{statusTimestamp} - $hash->{helper}{mowerold}{attributes}{metadata}{statusTimestamp}; if ($storediff) { ::FHEM::Devices::AMConnect::Common::AlignArray( $hash ); ::FHEM::Devices::AMConnect::Common::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.'_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' ); if ( AttrVal($name, 'mapZones', 0) && $hash->{helper}{currentZone} && $hash->{helper}{mapZones}{$hash->{helper}{currentZone}}{curZoneCnt} ) { my $curZon = $hash->{helper}{currentZone}; my $curZonCnt = $hash->{helper}{mapZones}{$curZon}{curZoneCnt}; readingsBulkUpdateIfChanged($hash, $pref.'_currentZone', $curZon . '(' . $curZonCnt . '/' . $hash->{helper}{newzonedatasets} . ')' ); } my $tstamp = $hash->{helper}{mower}{attributes}{$pref}{errorCodeTimestamp}; my $timestamp = ::FHEM::Devices::AMConnect::Common::FmtDateTimeGMT($tstamp/1000); readingsBulkUpdateIfChanged($hash, $pref."_errorCodeTimestamp", $tstamp ? $timestamp : '-' ); my $errc = $hash->{helper}{mower}{attributes}{$pref}{errorCode}; readingsBulkUpdateIfChanged($hash, $pref.'_errorCode', $tstamp ? $errc : '-'); my $errd = $::FHEM::Devices::AMConnect::Common::errortable->{$errc}; readingsBulkUpdateIfChanged($hash, $pref.'_errorDescription', $tstamp ? $errd : '-'); $pref = 'system'; readingsBulkUpdateIfChanged($hash, $pref."_name", $hash->{helper}{mower}{attributes}{$pref}{name} ); my $model = $hash->{helper}{mower}{attributes}{$pref}{model}; $model =~ s/AUTOMOWER./AM/; # $hash->{MODEL} = '' if (!defined $hash->{MODEL}); $hash->{MODEL} = $model if ( $model && $hash->{MODEL} ne $model ); $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 = ::FHEM::Devices::AMConnect::Common::FmtDateTimeGMT($tstamp/1000); readingsBulkUpdateIfChanged($hash, "planner_nextStart", $tstamp ? $timestamp : '-' ); $pref = 'statistics'; readingsBulkUpdateIfChanged($hash, $pref."_numberOfCollisions", $hash->{helper}->{mower}{attributes}{$pref}{numberOfCollisions} ); readingsBulkUpdateIfChanged($hash, $pref."_newGeoDataSets", $hash->{helper}{newdatasets} ); $pref = 'settings'; readingsBulkUpdateIfChanged($hash, $pref."_headlight", $hash->{helper}->{mower}{attributes}{$pref}{headlight}{mode} ); readingsBulkUpdateIfChanged($hash, $pref."_cuttingHeight", $hash->{helper}->{mower}{attributes}{$pref}{cuttingHeight} ); $pref = 'status'; my $connected = $hash->{helper}{mower}{attributes}{metadata}{connected}; readingsBulkUpdateIfChanged($hash, $pref."_connected", ( $connected ? "CONNECTED($connected)" : "OFFLINE($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 )); readingsEndUpdate($hash, 1); my @time = localtime(); my $secs = ( $time[2] * 3600 ) + ( $time[1] * 60 ) + $time[0]; my $interval = $hash->{helper}->{interval}; # do at midnight if ( $secs <= $interval ) { $hash->{helper}{statistics}{lastDayTrack} = $hash->{helper}{statistics}{currentDayTrack}; $hash->{helper}{statistics}{lastDayArea} = $hash->{helper}{statistics}{currentDayArea}; $hash->{helper}{statistics}{currentWeekTrack} += $hash->{helper}{statistics}{currentDayTrack}; $hash->{helper}{statistics}{currentWeekArea} += $hash->{helper}{statistics}{currentDayArea}; $hash->{helper}{statistics}{currentDayTrack} = 0; $hash->{helper}{statistics}{currentDayArea} = 0; if ( AttrVal($name, 'mapZones', 0) && defined( $hash->{helper}{mapZones} ) ) { my @zonekeys = sort (keys %{$hash->{helper}{mapZones}}); my $sumLastDayCnt=0; my $sumCurrentWeekCnt=0; map { $hash->{helper}{mapZones}{$_}{lastDayCnt} = $hash->{helper}{mapZones}{$_}{zoneCnt}; $sumLastDayCnt += $hash->{helper}{mapZones}{$_}{lastDayCnt}; $hash->{helper}{mapZones}{$_}{currentWeekCnt} += $hash->{helper}{mapZones}{$_}{lastDayCnt}; $sumCurrentWeekCnt += $hash->{helper}{mapZones}{$_}{currentWeekCnt}; $hash->{helper}{mapZones}{$_}{zoneCnt} = 0; } @zonekeys; map { $hash->{helper}{mapZones}{$_}{lastDayCntPct} = sprintf( "%.0f", $hash->{helper}{mapZones}{$_}{lastDayCnt} / $sumLastDayCnt * 100 ); } @zonekeys if( $sumLastDayCnt ); map { $hash->{helper}{mapZones}{$_}{currentWeekCntPct} = sprintf( "%.0f", $hash->{helper}{mapZones}{$_}{currentWeekCnt} / $sumCurrentWeekCnt * 100 ); } @zonekeys if( $sumCurrentWeekCnt ); } # do on days if ( $time[6] == 1 ) { $hash->{helper}{statistics}{lastWeekTrack} = $hash->{helper}{statistics}{currentWeekTrack}; $hash->{helper}{statistics}{lastWeekArea} = $hash->{helper}{statistics}{currentWeekArea}; $hash->{helper}{statistics}{currentWeekTrack} = 0; $hash->{helper}{statistics}{currentWeekArea} = 0; if ( AttrVal($name, 'mapZones', 0) && defined( $hash->{helper}{mapZones} ) ) { my @zonekeys = sort (keys %{$hash->{helper}{mapZones}}); my $sumLastWeekCnt=0; map { $hash->{helper}{mapZones}{$_}{lastWeekCnt} = $hash->{helper}{mapZones}{$_}{currentWeekCnt}; $sumLastWeekCnt += $hash->{helper}{mapZones}{$_}{lastWeekCnt}; $hash->{helper}{mapZones}{$_}{currentWeekCnt} = 0; } @zonekeys; map { $hash->{helper}{mapZones}{$_}{lastWeekCntPct} = sprintf( "%.0f", $hash->{helper}{mapZones}{$_}{lastWeekCnt} / $sumLastWeekCnt * 100 ); } @zonekeys if( $sumLastWeekCnt ); } } #clear position arrays if ( AttrVal( $name, 'weekdaysToResetWayPoints', 1 ) =~ $time[6] ) { $hash->{helper}{areapos} = []; $hash->{helper}{otherpos} = []; } } readingsSingleUpdate($hash, 'state', 'connected', 1 ); RemoveInternalTimer( $hash, \&APIAuth ); InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 ); return undef; } } } else { readingsSingleUpdate( $hash, 'state', "error statuscode $statuscode", 1 ); Log3 $name, 1, "\ndebug $iam \n\$statuscode [$statuscode]\n\$err [$err],\n \$data [$data] \n\$param->url $param->{url}"; } RemoveInternalTimer( $hash, \&APIAuth ); InternalTimer( gettimeofday() + $interval, \&APIAuth, $hash, 0 ); return undef; } ######################### sub Set { my ($hash,@val) = @_; my $type = $hash->{TYPE}; return "$type $hash->{NAME} Set: needs at least one 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 ( !IsDisabled($name) && $setName eq 'getUpdate' ) { RemoveInternalTimer($hash, \&APIAuth); APIAuth($hash); return undef; } elsif ( $setName eq 'chargingStationPositionToAttribute' ) { my $xm = $hash->{helper}{chargingStation}{longitude} // 10.1165; my $ym = $hash->{helper}{chargingStation}{latitude} // 51.28; CommandAttr( $hash, "$name chargingStationCoordinates $xm $ym" ); return undef; } elsif ( $setName eq 'defaultDesignAttributesToAttribute' ) { my $design = $hash->{helper}{mapdesign}; CommandAttr( $hash, "$name mapDesignAttributes $design" ); return undef; } elsif ( $setName eq 'mapZonesTemplateToAttribute' ) { my $tpl = $hash->{helper}{mapZonesTpl}; CommandAttr( $hash, "$name mapZones $tpl" ); return undef; } elsif ( ReadingsVal( $name, 'state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName eq 'mowerScheduleToAttribute' ) { my $calendarjson = eval { 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 ); RemoveInternalTimer($hash, \&APIAuth); APIAuth($hash); return undef; } } elsif ( ReadingsVal( $name, 'state', 'defined' ) !~ /defined|initialized|authentification|authenticated|update/ && $setName =~ /^(Start|Park|cuttingHeight)$/ ) { if ( $setVal =~ /^(\d+)$/) { ::FHEM::Devices::AMConnect::Common::CMD($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)$/) { ::FHEM::Devices::AMConnect::Common::CMD($hash ,$setName, $setVal); return undef; } } elsif ( !IsDisabled($name) && $setName eq 'getNewAccessToken' ) { 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/) { ::FHEM::Devices::AMConnect::Common::CMD($hash,$setName); return undef; } my $ret = " getNewAccessToken:noArg ParkUntilFurtherNotice:noArg ParkUntilNextSchedule:noArg Pause:noArg Start:selectnumbers,60,60,600,0,lin Park:selectnumbers,60,60,600,0,lin 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 defaultDesignAttributesToAttribute:noArg mapZonesTemplateToAttribute:noArg "; return "Unknown argument $setName, choose one of".$ret; } ######################### 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" ) { Log3 $name, 3, "$iam $cmd $attrName disabled"; } elsif( $cmd eq "del" or $cmd eq 'set' and !$attrVal ) { 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"); } ::FHEM::Devices::AMConnect::Common::readMap( $hash ); Log3 $name, 3, "$iam $cmd $attrName $attrVal"; } else { return "$iam $cmd $attrName wrong image type, use webp, png, jpeg or jpg"; 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 "weekdaysToResetWayPoints" ) { if( $cmd eq "set" ) { return "$iam $attrName is invalid, enter a combination of weekday numbers, space or - [0123456 -]" unless( $attrVal =~ /0|1|2|3|4|5|6| |-/ ); Log3 $name, 4, "$iam $cmd $attrName $attrVal"; } elsif( $cmd eq "del" ) { Log3 $name, 3, "$iam $cmd $attrName and set default to 1"; } ########## } 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 mower schedule array"; } ########## } elsif( $attrName eq "mapZones" ) { if( $cmd eq "set" ) { my $longitude = 10; my $latitude = 52; my $perl = eval { decode_json ($attrVal) }; if ($@) { return "$iam $cmd $attrName decode error: $@ \n $attrVal"; } for ( keys %{$perl} ) { my $cond = eval "($perl->{$_}{condition})"; if ($@) { return "$iam $cmd $attrName syntax error in condition: $@ \n $perl->{$_}{condition}"; } } Log3 $name, 4, "$iam $cmd $attrName"; $hash->{helper}{mapZones} = $perl; } elsif( $cmd eq "del" ) { delete $hash->{helper}{mapZones}; delete $hash->{helper}{currentZone}; CommandDeleteReading( $hash, "$name mower_currentZone" ); Log3 $name, 3, "$iam $cmd $attrName"; } ########## } elsif( $attrName eq "mowerActivityToHighLight" ) { if( $cmd eq "set" ) { my $act = 'LEAVING'; my $actold = 'LEAVING'; my $perl = eval "($attrVal)"; if ($@) { return "$iam $cmd $attrName syntax error in condition: $@ \n $attrVal"; } Log3 $name, 4, "$iam $cmd $attrName"; } elsif( $cmd eq "del" ) { Log3 $name, 3, "$iam $cmd $attrName"; } } return undef; } ############################################################## 1; __END__ =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
    FHEM-Wiki: AutomowerConnect und AutomowerConnectDevice: Wie erstellt man eine Karte des Mähbereiches?

    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.
    • 'There is a timeout of 10 minutes in the mower to preserve data traffic and save battery...'
    • 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>
    • mowerScheduleToAttribute
      set <name> mowerScheduleToAttribute
      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.
    • InternalData
      get <name> InternalData
      Lists some device internal data
    • MowerData
      get <name> MowerData
      Lists all mower data with its hash path exept positon array. The hash path can be used for generating userReadings. The trigger is connected.
      Example: created reading serialnumber with hash path $hash->{helper}{mower}{attributes}{system}{serialNumber}

      attr <name> userReadings serialnumber:connected {$defs{$name}->{helper}{mower}{attributes}{system}{serialNumber}}
    • StatisticsData
      get <name> StatisticsData
      Lists statistics data with its hash path. The hash path can be used for generating userReadings. The trigger is connected.
    • errorCodes
      get <name> errorCodes
      Lists API response status codes and mower error codes



    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.
    • mapBackgroundColor
      attr <name> mapBackgroundColor <background-color>
      The value is used as background-color.
    • mapDesignAttributes
      attr <name> mapDesignAttributes <complete list of design-attributes>
      Load the list of attributes by set <name> defaultDesignAttributesToAttribute to change its values. Some default values are
      • mower path (activity MOWING): red
      • path in CS (activity CHARGING,PARKED_IN_CS): grey
      • path for interval with error (all activities with error): kind of magenta
      • all other activities: green
    • 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 5000. While in activity MOWING every 30 s a geo data set is generated. While in activity PARKED_IN_CS/CHARGING every 42 min a geo data set is generated.
    • weekdaysToResetWayPoints
      attr <name> weekdaysToResetWayPoints <any combination of weekday numbers, space or minus [0123456 -]>
      A combination of weekday numbers when the way point stack will be reset. No reset for space or minus. The way points are shifted through the dedicated stack. Default 1.
    • 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)
    • mapZones
      attr <name> mapZones <valid perl condition to separate Zones>
      Provide the zones with conditions as JSON-String:
      The waypoints are accessable by the variables $longitude und $latitude.
      Zones have have to be separated by conditions in alphabetical order of their names.
      The last zone is determined by the remaining waypoints.
      Syntactical example:
      '{
          "<name_1>" : {
            "condition" : "<condition to separate name_1 from other zones>"
        },
          "<name_2>" : {
            "condition" : "<condition to separate name_2 from other zones, except name_1>"
        },
          "<name_3>" : {
            "condition" : "<condition to separate name_3 from other zones, except name_1 and name_2>"
        },
          "<name_n-1>" : {
            "condition" : "<condition to separate name_n-1 from other zones ,except the zones already seperated>"
        },
          "<name n>" : {
            "condition" : "Use 'undef' because the last zone remains."
        }
      }'

      Example with two Zones and virtual lines defined by latitude 52.6484600648553, 52.64839739580418 (horizontal) and longitude 9.54799477359984 (vertikal). all way points above 52.6484600648553 or all way points above 52.64839739580418 and all way points to the right of 9.54799477359984 belong to zone 01_oben. All other way points belong to zone 02_unten.
      '{
          "01_oben" : {
            "condition" : "$latitude > 52.6484600648553 || $longitude > 9.54799477359984 && $latitude > 52.64839739580418"
        },
          "02_unten" : {
            "condition" : "undef"
        }
      }'
    • mowerActivityToHighLight
      attr <name> mowerActivityToHighLight <perl condition to determine a path section>
      A perl condition to highlight a path section by mower activities.
      The current interval activity is accessible by $act. The last intervall activity is accessible by $actold.
      LineColor, LineDash and LineWidth are adjustable by the attribut mapDesignAttributes under otherActivityPath...
      Example: Highlight path when leaving charging station.
      attr <name> mowerActivityToHighLight $act =~ /MOWING|LEAVING/ && $actold =~ /LEAVING|PARKED_IN_CS|CHARGING/
      Example: Highlight path when returning to charging station.
      attr <name> mowerActivityToHighLight $act =~ /PARKED_IN_CS|CHARGING|GOING_HOME/ && $actold =~ /MOWING|GOING_HOME/
    • disable
    • disabledForIntervals

    • attr <name> <>

    Readings
    • api_MowerFound - all mower registered under the application key (client_id)
    • api_token_expires - date when session of Husqvarna Cloud expires
    • batteryPercent - battery state of charge 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_currentZone - Zone name with activity MOWING in the last status time stamp interval and number of way points in parenthesis.
    • mower_errorCode - last error code
    • mower_errorCodeTimestamp - last error code time stamp
    • mower_errorDescription - error description
    • 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 NOT_APPLICABLE, NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST
    • planner_overrideAction - reason for override a planned action NOT_ACTIVE, FORCE_PARK, FORCE_MOW
    • state - status of connection FHEM to Husqvarna Cloud API and device state(e.g. defined, authorization, authorized, connected, error, update)
    • settings_cuttingHeight - actual cutting height from API
    • settings_headlight - actual headlight mode from API
    • statistics_newGeoDataSets - number of new data sets between the last two different time stamps
    • statistics_numberOfCollisions - Number of Collisions
    • status_connected - state of connetion between mower and Husqvarna Cloud, (1 => CONNECTED, 0 => OFFLINE)
    • status_statusTimestamp - local time of last change of the API content
    • status_statusTimestampDiff - time difference in seconds between the last and second last change of the API content
    • status_statusTimestampOld - local time of second last change of the API content
    • system_name - name 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.
    • 'Der Mäher sendet seine Daten nur alle 10 Minuten, um den Datenverkehr zu begrenzen und Batterie zu sparen...'
    • 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ähernummer 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
    • mowerScheduleToAttribute
      set <name> mowerScheduleToAttribute
      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.
      • errorCodes
        get <name> errorCodes
        Listet die Statuscode der API-Anfrage und die Fehlercodes des Mähroboters auf.
      • InternalData
        get <name> InternalData
        Listet einige Daten des FHEM-Gerätes auf.
      • MowerData
        get <name> MowerData
        Listet alle Daten des Mähers einschließlich Hashpfad auf ausgenommen das Positonsarray. Der Hashpfad kann zur Erzeugung von userReadings genutzt werden, getriggert wird durch connected.
        Beispiel: erzeugen des Reading serialnumber mit dem Hashpfad $hash->{helper}{mower}{attributes}{system}{serialNumber}

        attr <name> userReadings serialnumber:connected {$defs{$name}->{helper}{mower}{attributes}{system}{serialNumber}}
      • StatisticsData
        get <name> StatisticsData
        Listet statistische Daten mit ihrem Hashpfad auf. Der Hashpfad kann zur Erzeugung von userReadings genutzt werden, getriggert wird durch connected




    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
    • mapBackgroundColor
      attr <name> mapBackgroundColor <color value>
      Der Wert wird als Hintergrungfarbe benutzt.
    • mapDesignAttributes
      attr <name> mapDesignAttributes <complete list of design-attributes>
      Lade die Attributliste mit set <name> defaultDesignAttributesToAttribute um die Werte zu ändern. Einige Vorgabewerte:
      • Pfad beim mähen (Aktivität MOWING): rot
      • In der Ladestation (Aktivität CHARGING,PARKED_IN_CS): grau
      • Pfad eines Intervalls mit Fehler (alle Aktivitäten with error): Eine Art Magenta
      • Pfad aller anderen Aktivitäten: grün
    • 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 5000. Während der Aktivität MOWING wird ca. alle 30 s und während PARKED_IN_CS/CHARGING wird alle 42 min ein Geodatensatz erzeugt.
    • weekdaysToResetWayPoints
      attr <name> weekdaysToResetWayPoints <any combination of weekday numbers, space or minus [0123456 -]>
      Eine Kombination von Wochentagnummern an denen der Wegpunktspeicher gelöscht wird. Keine Löschung bei Leer- oder Minuszeichen, die Wegpunkte werden durch den zugeteilten Wegpunktspeicher geschoben. Standard 1.
    • 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)
    • mapZones
      attr <name> mapZones <JSON string with zone names in alpabetical order and valid perl condition to seperate the zones>
      Die Wegpunkte stehen über die Perlvariablen $longitude und $latitude zur Verfügung.
      Die Zonennamen und Bedingungen müssen als JSON-String angegeben werden.
      Die Zonennamen müssen in alphabetischer Reihenfolge durch Bedingungen abgegrenzt werden.
      Die letzte Zone ergibt sich aus den übrig gebliebenen Wegpunkten.
      Syntaxbeispiel:
      '{
          "<name_1>" : {
            "condition" : "<condition to separate name_1 from other zones>"
        },
          "<name_2>" : {
            "condition" : "<condition to separate name_2 from other zones, except name_1>"
        },
          "<name_3>" : {
            "condition" : "<condition to separate name_3 from other zones, except name_1 and name_2>"
        },
          "<name_n-1>" : {
            "condition" : "<condition to separate name_n-1 from other zones ,except the zones already seperated>"
        },
          "<name n>" : {
            "condition" : "Use 'undef' because the last zone remains."
        }
      }'

      Beispiel mit zwei Zonen und gedachten Linien bestimmt durch die Punkte Latitude 52.6484600648553, 52.64839739580418 (horizontal) und 9.54799477359984 (vertikal). Alle Wegpunkte deren Latitude über einer horizontalen Linie mit der Latitude 52.6484600648553 liegen oder alle Wegpunkte deren Latitude über einer horizontalen Linie mit der Latitude 52.64839739580418 liegen und deren Longitude rechts von einer vertikale Linie mit der Longitude 9.54799477359984 liegen, gehören zur Zone 01_oben. Alle anderen Wegpunkte gehören zur Zone 02_unten. '{
          "01_oben" : {
            "condition" : "$latitude > 52.6484600648553 || $longitude > 9.54799477359984 && $latitude > 52.64839739580418"
        },
          "02_unten" : {
            "condition" : "undef"
        }
      }'
    • mowerActivityToHighLight
      attr <name> mowerActivityToHighLight <perl condition to determine a path section>
      Eine Perl Bedingung, die Aktivitäten verknüpft, um einen Pfadabschnitt festzulegen, der hervorgehoben wird.
      Die Aktivität im aktuellen Intervall steht über die Perlvariable $act und die Aktivität im letzten Intervall über $actold zur Verfügung.
      Die Farbe, Strichstärke und Muster können über das Attribut mapDesignAttributes unter otherActivityPath... eingestellt werden.
      Beispiel: Pfad beim Verlassen der Ladestation hervorheben.
      attr <name> mowerActivityToHighLight $act =~ /MOWING|LEAVING/ && $actold =~ /LEAVING|PARKED_IN_CS|CHARGING/
      Beispiel: Pfad beim Zurückkehren zur Ladestation hervorheben.
      attr <name> mowerActivityToHighLight $act =~ /PARKED_IN_CS|CHARGING|GOING_HOME/ && $actold =~ /MOWING|GOING_HOME/
    • disable
    • disabledForIntervals

    • attr <name> <>

    Readings
    • api_MowerFound - Alle Mähroboter, die unter dem genutzten Application Key (client_id) registriert sind.
    • api_token_expires - Datum wann die Session der Husqvarna Cloud abläuft
    • batteryPercent - Batterieladung 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_currentZone - Name der Zone im aktuell abgefragten Intervall der Statuszeitstempel , in der der Mäher gemäht hat und Anzahl der Wegpunkte in der Zone in Klammern.
    • mower_errorCode - last error code
    • mower_errorCodeTimestamp - last error code time stamp
    • mower_errorDescription - error description
    • 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 NOT_APPLICABLE, NONE, WEEK_SCHEDULE, PARK_OVERRIDE, SENSOR, DAILY_LIMIT, FOTA, FROST
    • planner_overrideAction - Grund für vorrangige Aktion NOT_ACTIVE, FORCE_PARK, FORCE_MOW
    • state - Status der Verbindung des FHEM-Gerätes zur Husqvarna Cloud API (defined, authentification, authentified, connected, error, update).
    • settings_cuttingHeight - aktuelle Schnitthöhe aus der API
    • settings_headlight - aktueller Scheinwerfermode aus der API
    • statistics_newGeoDataSets - Anzahl der neuen Datensätze zwischen den letzten zwei unterschiedlichen Zeitstempeln
    • statistics_numberOfCollisions - Anzahl der Kollisionen
    • status_connected - Status der Verbindung zwischen dem Automower und der Husqvarna Cloud, (1 => CONNECTED, 0 => OFFLINE)
    • status_statusTimestamp - Lokalzeit der letzten Änderung der Daten in der API
    • status_statusTimestampDiff - Zeitdifferenz zwischen den beiden letzten Änderungen im Inhalt der Daten aus der API
    • status_statusTimestampOld - Lokalzeit der vorletzten Änderung der Daten in der API
    • system_name - Name des Automowers
=end html_DE