############################################## # $Id$ # ABU 20160307 First release # ABU 20160321 Added first parsing algos # ABU 20160311 Migrated to JSON # ABU 20160412 Integrated basic set-mechanism, added internals, changed callback-routine # ABU 20160413 Renamed to robonect, added status offline # ABU 20160414 Changed final two Automower to Robonect # ABU 20160414 Renamed private fn "decode" # ABU 20160415 Renamed private fn "decode" again, set eventonchangedreading, removed debug, increased interval to 90 # ABU 20160416 Added logs, removed eventonchangedreading, added debug # ABU 20160416 Removed logs, added eventonchangedreading, removed debug, added 1=true for json data # ABU 20160426 Fixed decode warning, added error support # ABU 20160515 Added splitFn and doku package main; use strict; use warnings; use HttpUtils; use JSON; #available get cmds my %gets = ( "status" => "noArg" ); my $EOD = "feierabend"; my $HOME = "home"; my $AUTO = "auto"; my $MANUAL = "manuell"; my $START = "start"; my $STOP = "stop"; my $OFFLINE = "offline"; #available set cmds my %sets = ( $EOD => "noArg", $HOME => "noArg", $AUTO => "noArg", $MANUAL => "noArg", $START => "noArg", $STOP => "noArg" ); my %commands = ( GET_STATUS => "cmd=status", SET_MODE => {$HOME=>"cmd=mode&mode=home", $MANUAL=>"cmd=mode&mode=man", $AUTO=>"cmd=mode&mode=auto", $EOD=>"cmd=mode&mode=eod", $STOP=>"cmd=stop", $START=>"cmd=start"} ); #set to 1 for debug my $debug = 0; #elements within group next my %elements = ( # "robonect" => # { "successful" => {ALIAS=>"kommunikation", "true"=>"erfolgreich", "false"=>"fehlgeschlagen", 1=>"erfolgreich", 0=>"fehlgeschlagen"}, "status" => { ALIAS => "allgemein", "status" => {ALIAS=>"status", 0=>"schlafen", 1=>"parken", 2=>"maehen", 3=>"suche-base", 4=>"laden", 5=>"suche", 7=>"fehler", 8=>"schleife-fehlt"}, "mode" => {ALIAS=>"modus", 0=>"automatik", 1=>"manuell", 2=>"home", 3=>"demo"}, "battery" => {ALIAS=>"batteriezustand"}, "hours" => {ALIAS=>"betriebsstunden"} }, "timer" => { ALIAS => "timer", "status" => {ALIAS=>"status", 0=>"deaktiviert", 1=>"aktiv", 2=>"standby"}, "next" => { ALIAS => "timer", "date" => {ALIAS=>"startdatum"}, "time" => {ALIAS=>"startzeit"}, #"date" => {ALIAS=>"start-unix"}, } }, "error" => { ALIAS => "fehler", "error_code" => {ALIAS=>"code"}, "error_message" => {ALIAS=>"nachricht"}, "date" => {ALIAS=>"datum"}, "time" => {ALIAS=>"zeit"} } # } ); #Init this device #This declares the interface to fhem ############################# sub Robonect_Initialize($) { my ($hash) = @_; $hash->{DefFn} = 'Robonect_Define'; $hash->{UndefFn} = 'Robonect_Undef'; $hash->{SetFn} = 'Robonect_Set'; $hash->{GetFn} = 'Robonect_Get'; $hash->{AttrFn} = 'Robonect_Attr'; $hash->{ShutdownFn} = 'Robonect_Shutdown'; $hash->{ReadyFn} = 'Robonect_Ready'; $hash->{DbLog_splitFn} = 'Robonect_DbLog_split'; $hash->{AttrList} = "do_not_notify:1,0 " . #supress any notification (including log) "showtime:1,0 " . #shows time instead of received value in state "credentials " . #user/password combination for authentication in mower, stored in a credentials file "basicAuth " . #user/password combination for authentication in mower "pollInterval " . #interval to poll in seconds "$readingFnAttributes "; #standard attributes } #Define this device #Is called at every define ############################# sub Robonect_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); #device name my $name = $a[0]; #set verbose to 5, if debug enabled $attr{$name}{verbose} = 5 if ($debug eq 1); my $tempStr = join (", ", @a); Log3 ($name, 5, "define $name: enter $hash, attributes: $tempStr"); #too less arguments return "wrong syntax - define Robonect [ ]" if (int(@a) < 3); #check IP my $ip = $a[2]; #remove whitespaces $ip =~ s/^\s+|\s+$//g; #Syntax ok if ($ip =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) { my @octs = split (".", $ip); foreach my $octet (@octs) { return "wrong syntax - $octet has an invalid range. Allowed is 0..255" if (($octet >= 256) or ($octet <= -1)); } } else { return "wrong syntax - IP must be supplied correctly <0..254>.<0..254>.<0..254>.<0..254>"; } #wrong syntax for IP return "wrong syntax - IP must be supplied correctly <0..254>.<0..254>.<0..254>.<000..254>" if (int(@a) < 3); #assign name and port $hash->{NAME} = $name; $hash->{IP} = $ip; #backup name for a later rename $hash->{DEVNAME} = $name; #get first info and launch timer InternalTimer(gettimeofday(), "Robonect_GetUpdate", $hash, 0); #finally create decice #defptr is needed to supress FHEM input $modules{Robonect}{defptr}{$name} = $hash; #default event-on-changed-reading for all readings $attr{$name}{"event-on-change-reading"} = ".*"; #defaul poll-interval $attr{$name}{"pollInterval"} = 90; Log3 ($name, 5, "exit define"); return undef; } #Release this device #Is called at every delete / shutdown ############################# sub Robonect_Undef($$) { my ($hash, $name) = @_; Log3 ($name, 5, "enter undef $name: hash: $hash name: $name"); #kill interval timer RemoveInternalTimer($hash); #close port Robonect_Shutdown ($hash); #remove module. Refer to DevName, because module may be renamed delete $modules{KNX}{defptr}{$hash->{DEVNAME}}; #remove name delete $hash->{NAME}; #remove backuped name delete $hash->{DEVNAME}; Log3 ($name, 5, "exit undef"); return undef; } #Release this device #Is called at every delete / shutdown ############################# sub Robonect_Shutdown($) { my ($hash, $name) = @_; Log3 ($name, 5, "enter shutdown $name: hash: $hash name: $name"); Log3 ($name, 5, "exit shutdown"); return undef; } #This function is called from fhem from rime to time ############################# sub Robonect_Ready($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 ($name, 5, "enter ready $name: hash: $hash name: $name"); Log3 ($name, 5, "exit ready"); return undef; } #Reads info from the mower ############################# sub Robonect_Get($@) { my ($hash, @a) = @_; my $name = $hash->{NAME}; my $tempStr = join (", ", @a); Log3 ($name, 5, "enter get $name: $name hash: $hash, attributes: $tempStr"); #backup cmd my $cmd = $a[1]; #response for gui return "Unknown argument, choose one of " . join(" ", " ", sort keys %gets) if(!defined($cmd)); return "Unknown argument $cmd, choose one of " . join(" ", " ", sort keys %gets) if(!defined($gets{$cmd})); #lower cmd $cmd = lc($cmd); my ($userName, $passWord) = getCredentials ($hash); #basic url my $url = "http://" . $hash->{IP} . "/json?"; #append userdata, if given $url = $url . "user=" . $userName . "&pass=" . $passWord . "&" if (defined ($userName) and defined ($passWord)); #append command $url = $url . $commands{GET_STATUS}; my $httpData; $httpData->{url} = $url; $httpData->{loglevel} = AttrVal ($name, "verbose", 2); $httpData->{loglevel} = 5; $httpData->{hideurl} = 0; $httpData->{callback} = \&callback; $httpData->{hash} = $hash; $httpData->{cmd} = $commands{GET_STATUS}; HttpUtils_NonblockingGet($httpData); return $httpData->{err}; Log3 ($name, 5, "exit get"); return undef; } #Sends commands to the mower ############################# sub Robonect_Set($@) { my ($hash, @a) = @_; my $name = $hash->{NAME}; my $tempStr = join (", ", @a); Log3 ($name, 5, "enter set $name: $name hash: $hash, attributes: $tempStr"); #backup cmd my $cmd = $a[1]; #response for gui return "Unknown argument, choose one of " . join(" ", " ", sort keys %sets) if(!defined($cmd)); return "Unknown argument $cmd, choose one of " . join(" ", " ", sort keys %sets) if(!defined($sets{$cmd})); #lower cmd $cmd = lc($cmd); my ($userName, $passWord) = getCredentials ($hash); my $decodedCmd = $commands{SET_MODE}{$cmd}; #execute it if (defined ($decodedCmd)) { my $url = "http://" . $hash->{IP} . "/json?"; #append userdata, if given $url = $url . "user=" . $userName . "&pass=" . $passWord . "&" if (defined ($userName) and defined ($passWord)); #append command $url = $url . $decodedCmd; print "URL: $url\n"; my $httpData; $httpData->{url} = $url; $httpData->{loglevel} = AttrVal ($name, "verbose", 2); $httpData->{loglevel} = 5; $httpData->{hideurl} = 0; $httpData->{callback} = \&callback; $httpData->{hash} = $hash; $httpData->{cmd} = $decodedCmd; HttpUtils_NonblockingGet($httpData); return $httpData->{err}; Robonect_GetUpdate($hash); } Log3 ($name, 5, "exit set"); return; } #called on every mod of the attributes ############################# sub Robonect_Attr(@) { my ($cmd,$name,$attr_name,$attr_value) = @_; Log3 ($name, 5, "enter attr $name: $name, attrName: $attr_name"); #if($cmd eq "set") #{ # if(($attr_name eq "debug") and (($attr_value eq "1") or ($attr_value eq "true"))) # { # #todo # } #} Log3 ($name, 5, "exit attr"); return undef; } #Split reading for DBLOG ############################# sub Robonect_DbLog_split($) { my ($event) = @_; my ($reading, $value, $unit); my $tempStr = join (", ", @_); Log (5, "splitFn - enter, attributes: $tempStr"); #detect reading - real reading or state? my $isReading = "false"; $isReading = "true" if ($event =~ m/: /); #split input-string my @strings = split (" ", $event); my $startIndex = undef; $unit = ""; return undef if (not defined ($strings[0])); #real reading? if ($isReading =~ m/true/) { #first one is always reading $reading = $strings[0]; $reading =~ s/:?$//; $startIndex = 1; } #plain state else { #for reading state nothing is supplied $reading = "state"; $startIndex = 0; } return undef if (not defined ($strings[$startIndex])); #per default join all single pieces $value = join(" ", @strings[$startIndex..(int(@strings) - 1)]); #numeric value? #if ($strings[$startIndex] =~ /^[+-]?\d*[.,]?\d+/) if ($strings[$startIndex] =~ /^[+-]?\d*[.,]?\d+$/) { $value = $strings[$startIndex]; #single numeric value? Assume second par is unit... if ((defined ($strings[$startIndex + 1])) && !($strings[$startIndex+1] =~ /^[+-]?\d*[.,]?\d+/)) { $unit = $strings[$startIndex + 1] if (defined ($strings[$startIndex + 1])); } } #numeric value? #if ($strings[$startIndex] =~ /^[+-]?\d*[.,]?\d+/) #{ # $value = $strings[$startIndex]; # $unit = $strings[$startIndex + 1] if (defined ($strings[$startIndex + 1])); #} #string or raw #else #{ # $value = join(" ", @strings[$startIndex..(int(@strings) - 1)]); #} Log (5, "splitFn - READING: $reading, VALUE: $value, UNIT: $unit"); return ($reading, $value, $unit); } #Called on the interval timer, if enabled ############################# sub Robonect_GetUpdate($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 ($name, 5, "enter update $name: $name"); #get status my @callAttr; $callAttr[0] = $name; $callAttr[1] = "status"; Robonect_Get ($hash, @callAttr); my $interval = AttrVal($name,"pollInterval",90); #reset timer InternalTimer(gettimeofday() + $interval, "Robonect_GetUpdate", $hash, 1) if ($interval > 0); Log3 ($name, 5, "exit update"); } #Private function which handles http-responses ############################# sub callback ($) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; #wenn ein Fehler bei der HTTP Abfrage aufgetreten ist if($err ne "") { Log3 ($name, 3, "callback - error while requesting ".$param->{url}." - $err"); $hash->{LAST_COMM_STATUS} = $err; #set reading with failure - notify only, when state has not changed readingsSingleUpdate($hash, "state", $OFFLINE, 1); } #wenn die Abfrage erfolgreich war ($data enthält die Ergebnisdaten des HTTP Aufrufes) elsif($data ne "") { Log3 ($name, 3, "callback - url ".$param->{url}." returned: $data"); #repair V5.0b $data =~ s/:"/,"/g; my $answer = decode_json $data; Log3 ($name, 3, "callback - url ".$param->{url}." repaired: $data"); my ($key, $value) = decodeContent ($hash, $answer, "successful", undef, undef); $hash->{LAST_CMD} = $param->{cmd}; $hash->{LAST_COMM_STATUS} = "success: " . $value; Log3 ($name, 5, "callback - communication ok"); #my %tmp = %$answer; #print "answer: ", %tmp, "\n"; #status-readings if ($answer->{successful} =~ m/(true)|(1)/) { Log3 ($name, 5, "callback - update readings"); readingsBeginUpdate($hash); #($key, $value) = decodeContent ($hash, $answer, "successful", undef); #readingsBulkUpdate($hash, $key, $value); ($key, $value) = decodeContent ($hash, $answer, "status", "status", undef); readingsBulkUpdate($hash, $key, $value); readingsBulkUpdate($hash, "state", $value); ($key, $value) = decodeContent ($hash, $answer, "status", "mode", undef); readingsBulkUpdate($hash, $key, $value); ($key, $value) = decodeContent ($hash, $answer, "status", "battery", undef); readingsBulkUpdate($hash, $key, $value); ($key, $value) = decodeContent ($hash, $answer, "status", "hours", undef); readingsBulkUpdate($hash, $key, $value); ($key, $value) = decodeContent ($hash, $answer, "timer", "status", undef); readingsBulkUpdate($hash, $key, $value); ($key, $value) = decodeContent ($hash, $answer, "timer", "next", "date"); readingsBulkUpdate($hash, $key, $value); ($key, $value) = decodeContent ($hash, $answer, "timer", "next", "time"); readingsBulkUpdate($hash, $key, $value); readingsEndUpdate($hash, 1); } #error? my $errorOccured = $answer->{status}->{status}; if (defined ($errorOccured) and ($errorOccured =~ m/7/)) { readingsSingleUpdate($hash, "fehler_aktuell", $answer->{error}->{error_message}, 1); } #no error elsif (defined ($errorOccured)) { my $hashref = $hash->{READINGS}; my %readings = %$hashref; #delete readings foreach my $key (keys %readings) { #delete $readings{$key} if ($key =~ m/^fehler.*/); delete $hash->{READINGS}{$key} if ($key =~ m/^fehler.*/); #delete $hash->{READINGS}{$key} if ($key =~ m/^fehler-aktuell.*/); } } } } #Private function to get json-content ############################# sub decodeContent ($$$$$) { my ($hash, $msg, $key1, $key2, $key3) = @_; my $name = $hash->{NAME}; my $rdName = undef; my $rdValue = undef; my $template = undef; if (defined ($key2) && defined ($key3)) { $template = $elements{$key1}{$key2}{$key3}; $rdName = $elements{$key1}{$key2}{ALIAS} . "-" . $template->{ALIAS}; $rdValue = $msg->{$key1}->{$key2}->{$key3}; } elsif (defined ($key2)) { $template = $elements{$key1}{$key2}; $rdName = $elements{$key1}{ALIAS} . "-" . $template->{ALIAS}; $rdValue = $msg->{$key1}->{$key2}; } else { $template = $elements{$key1}; $rdValue = $msg->{$key1}; $rdName = $template->{ALIAS}; } $rdValue = "undef" if (not defined ($rdValue)); $rdValue = $template->{$rdValue} if (defined ($template->{$rdValue})); Log3 ($name, 5, "decodeContent - NAME: $rdName, VALUE: $rdValue"); return $rdName, $rdValue; } #Private function to evaluate credentials ############################# sub decodeAnswer ($$$) { my ($hash, $getCmd, @readings) = @_; my $name = $hash->{NAME}; my @list; foreach my $reading (@readings) { my $answer = undef; my $transval = undef; my ($header, $key, $value) = undef; $header = $reading->{header}; $key = $reading->{key}; $value = $reading->{value}; if ($header =~ m/robonect/i) { $answer->{name} = $getCmd . "-" . $elements{$header}{$key}{ALIAS}; $transval = $elements{$header}{$key}{$value}; } else { $answer->{name} = $getCmd . "-" . $elements{"robonect"}{$header}{ALIAS} . "-" . $elements{"robonect"}{$header}{$key}{ALIAS}; $transval = $elements{"robonect"}{$header}{$key}{$value}; } if (defined($transval)) { $answer->{value} = $transval; } else { $answer->{value} = $value; } #$answer->{name} = $getCmd . "-" . $answer->{name}; Log3 ($name, 5, "decodeAnswer - NAME: $answer->{name}, VALUE: $answer->{value}"); push (@list, $answer); } return @list; } #Private function to evaluate credentials ############################# sub getCredentials ($) { my ($hash) = @_; my $name = $hash->{NAME}; my $userName = undef; my $passWord = undef; #parse basicAuth my $basicAuth = AttrVal ($name, "basicAuth", undef); if (defined ($basicAuth)) { #if the string does NOT contain a ":", assume base64-encoded data if (not ($basicAuth =~ m/:/)) { $basicAuth = decode_base64 ($basicAuth); Log3 ($name, 5, "credentials - found encrypted data"); } #try to split my @plainAuth = split (":", $basicAuth); #found plain authentication if (int(@plainAuth) == 2) { $userName = $plainAuth[0]; $passWord = $plainAuth[1]; Log3 ($name, 5, "credentials - found plain data"); } else { Log3 ($name, 0, "credentials - user/pw combination not correct"); } } #parse credential-File - overrules basicAuth my $credentials = AttrVal ($name, "credentials", undef); if(defined($credentials)) { #cannot open file if(!open(CFG, $credentials)) { Log3 ($name, 0, "cannot open credentials file: $credentials") ; } #read it else { my @cfg = ; close(CFG); my %creds; eval join("", @cfg); #extract it $userName =~ $creds{$name}{username}; $passWord =~ $creds{$name}{password}; Log3 ($name, 5, "credentials - found in file"); } } return $userName, $passWord; } 1; =pod =begin html

Robonect

    Robonect is a after-market wifi-module for robomowers based on the husky G3-control. It was developed by Fabian H. and can be optained at www.robonect.de. This module gives you access to the basic commands. This module will not work without libjson-perl! Do not forget to install it first!

    Define

      define <name> Robonect <ip-adress> [<user> <password>]

      The authentication can be supplied in the definition as plaine text or in a differen way - see the attributes. Per default the status is polled every 90s.

      Example:

            define myMower Robonect 192.168.13.5 test tmySecret
            

    Set

      Switch the mower to automatic-timer: set <name> auto Send the mower home - prevents further runs triggered by timer (persistent): set <name> home Sends the mower home for the actual timer-slot. The next timer-slot starts the mower again: set <name> feierabend Start the mower (only needed after a manual stop: set <name> start Stop the mower immediately: set <name> stop

    Get

      Gets the actual state of the mower - normally not needed, because the status is polled cyclic.

    Attributes

    credentials

      If you supply a valid path to a credentials file, this combination is used to log in at robonect. This mechism overrules basicAuth.

    basicAuth

      You can supply username and password plain or base-64-encoded. For a base64-encoder, use google.

      Example:

            define myMower 192.168.5.1
      	  attr myMower basicAuth me:mySecret
            
            define myMower 192.168.5.1
      	  attr myMower basicAuth bWU6bXlTZWNyZXQ=
            

    pollInterval

      Supplies the interval top poll the robonect in seconds. Per default 90s is set.
=end html =begin html_DE

Robonect

    Robonect ist ein Nachrüstmodul für automower, die auf der Husky-G3-Steuerung basieren. Es wurde von Fabian H. entwickelt und kann unter www.robonect.de bezogen werden. Dieses Modul gibt Euch Zugriff auf die nötigsten Kommandos. Dieses Modul benötigt libjson-perl. Bitte NICHT VERGESSEN zu installieren!

    Define

      define <name> Robonect <ip-adress> [<user> <password>]

      Die Zugangsinformationen können im Klartext bei der Definition angegeben werden. Wahlweise auch per Attribut. Standardmäßig wird der Status vom RObonect alle 90s aktualisiert.

      Beispiel:

            define myMower Robonect 192.168.13.5 test tmySecret
            

    Set

      Versetzt den Mäher in den timerbasierten Automatikmodus: set <name> auto Schickt den Mäher nach hause. Ein erneutes Starten per Timer wird verhindert (persistent): set <name> home Schickt den Mäher nach Hause. Beim nächsten Timerstart fährt der Mäher wieder regulär: set <name> feierabend Startet den Mäher (wird nur nach einem manuellen Stop benötigt): set <name> start Stoppt den Mäher: set <name> stop

    Get

      Holt den aktuellen Status des Mähers. Wird normalerweise nicht benötigt, da automatisch gepolled wird.

    Attributes

    credentials

      Hier kann ein Link auf ein credentials-file angegeben werden. Die Zugansinformationen werden dann aus der Datei geholt. Dieser Mechanismus überschreibt basicAuth.

    basicAuth

      Hier werden die Zugangsinformationen entweder im Klartext oder base-64-codiert übergeben. Base64-encoder gibts bei google.

      Example:

            define myMower 192.168.5.1
      	  attr myMower basicAuth me:mySecret
            
            define myMower 192.168.5.1
      	  attr myMower basicAuth bWU6bXlTZWNyZXQ=
            

    pollInterval

      Hier kann das polling-interval in Sekunden angegeben werden. Default sind 90s.
=end html_DE =cut