# $Id$ ############################################################################## # # 70_INDEGO.pm # An FHEM Perl module for controlling a Bosch Indego. # # Copyright by Ulf von Mersewsky # e-mail: umersewsky at gmail.com # # This file is part of fhem. # # Fhem 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 # (at your option) any later version. # # Fhem 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. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # # # Version: 0.2.9 # ############################################################################## package main; use 5.012; use strict; use warnings; use Time::HiRes qw(gettimeofday); use HttpUtils; use JSON qw(decode_json encode_json); use Encode qw(encode_utf8); sub INDEGO_Set($@); sub INDEGO_Get($@); sub INDEGO_GetStatus($;$); sub INDEGO_Define($$); sub INDEGO_Undefine($$); ################################### sub INDEGO_Initialize($) { my ($hash) = @_; Log3 $hash, 5, "INDEGO_Initialize: Entering"; $hash->{GetFn} = "INDEGO_Get"; $hash->{SetFn} = "INDEGO_Set"; $hash->{DefFn} = "INDEGO_Define"; $hash->{UndefFn} = "INDEGO_Undefine"; $hash->{AttrList} = "disable:0,1 " . "actionInterval " . $readingFnAttributes; return; } ##################################### sub INDEGO_GetStatus($;$) { my ( $hash, $update ) = @_; my $name = $hash->{NAME}; my $interval = $hash->{INTERVAL}; Log3 $name, 5, "INDEGO $name: called function INDEGO_GetStatus()"; # use actionInterval if state is busy, paused, or returning $interval = AttrVal($name, "actionInterval", $interval) if (ReadingsVal($name, "state_id", "0") =~ /^[57]\d\d$/); RemoveInternalTimer($hash); InternalTimer( gettimeofday() + $interval, "INDEGO_GetStatus", $hash, 0 ); return if ( AttrVal($name, "disable", 0) == 1 ); # check device availability if (!$update) { INDEGO_SendCommand( $hash, "state" ); } return; } ################################### sub INDEGO_Get($@) { my ( $hash, @a ) = @_; my $name = $hash->{NAME}; my $what; Log3 $name, 5, "INDEGO $name: called function INDEGO_Get()"; return "argument is missing" if ( int(@a) < 2 ); $what = $a[1]; if ( $what =~ /^(mapsvgcache)$/ ) { my $value = ReadingsVal($name, $what, ""); if ($value eq "") { $value = ReadingsVal($name, ".$what", ""); eval { require Compress::Zlib; }; unless($@) { $value = Compress::Zlib::uncompress($value); } } if ( $value ne "" ) { return $value; } else { return "no such reading: $what"; } } else { return "Unknown argument $what, choose one of mapsvgcache:noArg"; } } ################################### sub INDEGO_Set($@) { my ( $hash, @a ) = @_; my $name = $hash->{NAME}; Log3 $name, 5, "INDEGO $name: called function INDEGO_Set()"; return "No Argument given" if ( !defined( $a[1] ) ); my $usage = "Unknown argument " . $a[1]; $usage .= ", choose one of password renewContext:noArg mow:noArg pause:noArg returnToDock:noArg reloadMap:noArg smartMode:on,off"; $usage .= " deleteAlert:noArg" if (ReadingsVal($name, "alert_id", "-") ne "-"); $usage .= " calendar:0,1,2,3,4,5"; # mow if ( $a[1] eq "mow" ) { Log3 $name, 2, "INDEGO set $name " . $a[1]; INDEGO_SendCommand( $hash, "state", "mow" ); readingsSingleUpdate($hash, "state", "Set_Mowing", 1); } # pause elsif ( $a[1] eq "pause" ) { Log3 $name, 2, "INDEGO set $name " . $a[1]; INDEGO_SendCommand( $hash, "state", "pause" ); readingsSingleUpdate($hash, "state", "Set_Paused", 1); } # returnToDock elsif ( $a[1] eq "returnToDock" ) { Log3 $name, 2, "INDEGO set $name " . $a[1]; INDEGO_SendCommand( $hash, "state", "returnToDock" ); readingsSingleUpdate($hash, "state", "Set_Returning", 1); } # reloadMap elsif ( $a[1] eq "reloadMap" ) { Log3 $name, 2, "INDEGO set $name " . $a[1]; INDEGO_SendCommand( $hash, "map" ); } # renewContext elsif ( $a[1] eq "renewContext" ) { Log3 $name, 2, "INDEGO set $name " . $a[1]; INDEGO_SendCommand( $hash, "authenticate" ); } # deleteAlert elsif ( $a[1] eq "deleteAlert" ) { Log3 $name, 2, "INDEGO set $name " . $a[1]; INDEGO_SendCommand( $hash, "deleteAlert" ); } # selectCalendar elsif ( $a[1] eq "calendar" ) { Log3 $name, 2, "INDEGO set $name " . $a[1] . " " . $a[2]; return "No argument given" if ( !defined( $a[2] ) ); INDEGO_SendCommand( $hash, "setCalendar", $a[2] ); } # smartMode elsif ( $a[1] eq "smartMode" ) { Log3 $name, 2, "INDEGO set $name " . $a[1] . " " . $a[2]; return "No argument given" if ( !defined( $a[2] ) ); INDEGO_SendCommand( $hash, "smartMode", $a[2] ); } # password elsif ( $a[1] eq "password") { Log3 $name, 2, "INDEGO set $name " . $a[1]; return "No password given" if ( !defined( $a[2] ) ); INDEGO_StorePassword( $hash, $a[2] ); } # return usage hint else { return $usage; } return; } ################################### sub INDEGO_Define($$) { my ( $hash, $def ) = @_; my @a = split( "[ \t][ \t]*", $def ); my $name = $hash->{NAME}; Log3 $name, 5, "INDEGO $name: called function INDEGO_Define()"; if ( int(@a) < 3 ) { my $msg = "Wrong syntax: define INDEGO []"; Log3 $name, 4, $msg; return $msg; } $hash->{TYPE} = "INDEGO"; my $email = $a[2]; $hash->{helper}{EMAIL} = $email; # use interval of 300 sec if not defined my $interval = 300; if (defined($a[3])) { if ($a[3] =~ /^[0-9]+$/ and not defined($a[4])) { $interval = $a[3]; } else { INDEGO_StorePassword($hash, $a[3]); $interval = $a[4] if (defined($a[4])); } } $hash->{INTERVAL} = $interval; unless ( defined( AttrVal( $name, "webCmd", undef ) ) ) { $attr{$name}{webCmd} = 'mow:pause:returnToDock'; } # start the status update timer RemoveInternalTimer($hash); InternalTimer( gettimeofday() + 2, "INDEGO_GetStatus", $hash, 1 ); INDEGO_addExtension($name, "INDEGO_GetMap", "INDEGO/$name/map"); return; } ################################### sub INDEGO_Undefine($$) { my ( $hash, $arg ) = @_; my $name = $hash->{NAME}; Log3 $name, 5, "INDEGO $name: called function INDEGO_Undefine()"; # De-Authenticate INDEGO_SendCommand($hash, "deauthenticate"); # Stop the internal GetStatus-Loop and exit RemoveInternalTimer($hash); INDEGO_removeExtension("INDEGO/$name/map"); return; } ############################################################################################################ # # Begin of helper functions # ############################################################################################################ ######################### sub INDEGO_addExtension($$$) { my ( $name, $func, $link ) = @_; my $url = "/$link"; Log3 $name, 2, "Registering INDEGO $name for URL $url..."; $data{FWEXT}{$url}{deviceName} = $name; $data{FWEXT}{$url}{FUNC} = $func; $data{FWEXT}{$url}{LINK} = $link; } ######################### sub INDEGO_removeExtension($) { my ($link) = @_; my $url = "/$link"; my $name = $data{FWEXT}{$url}{deviceName}; Log3 $name, 2, "Unregistering INDEGO $name for URL $url..."; delete $data{FWEXT}{$url}; } ################################### sub INDEGO_SendCommand($$;$) { my ( $hash, $service, $type ) = @_; my $name = $hash->{NAME}; my $email = $hash->{helper}{EMAIL}; my $password = INDEGO_ReadPassword($hash); my $timestamp = gettimeofday(); my $timeout = 30; my $header; my $data; my $method = "GET"; Log3 $name, 5, "INDEGO $name: called function INDEGO_SendCommand()"; my $URL = "https://api.indego.iot.bosch-si.com/api/v1/"; if ($service ne "authenticate") { return if !INDEGO_CheckContext($hash); } Log3 $name, 4, "INDEGO $name: REQ $service"; if ($service eq "authenticate") { $URL .= $service; $header = "Content-Type: application/json"; $header .= "\r\nAuthorization: Basic "; $header .= encode_base64("$email:$password",""); $data = "{\"device\":\"\", \"os_type\":\"Android\", \"os_version\":\"4.0\", \"dvc_manuf\":\"unknown\", \"dvc_type\":\"unknown\"}"; $method = "POST"; } elsif ($service eq "deauthenticate") { $URL .= $service; $header = "x-im-context-id: ".ReadingsVal($name, "contextId", ""); $method = "DELETE"; } elsif ($service eq "alerts") { $URL .= $service; $header = "x-im-context-id: ".ReadingsVal($name, "contextId", ""); } elsif ($service eq "deleteAlert") { my $id = ReadingsVal($name, "alert_id", "-"); return undef if ($id eq "-"); $URL .= "alerts/$id"; $header = "x-im-context-id: ".ReadingsVal($name, "contextId", ""); $method = "DELETE"; } elsif ($service eq "state") { $URL .= "alms/"; $URL .= ReadingsVal($name, "alm_sn", ""); $URL .= "/$service"; $header = "x-im-context-id: ".ReadingsVal($name, "contextId", ""); if (defined($type)) { $header .= "\r\nContent-Type: application/json"; $data = "{\"state\":\"".$type."\"}"; $method = "PUT"; } } elsif ($service eq "longpollState") { $URL .= "alms/"; $URL .= ReadingsVal($name, "alm_sn", "0"); $URL .= "/state?longpoll=true&timeout=3600&last="; $URL .= ReadingsVal($name, "state_id", "0"); $header = "x-im-context-id: ".ReadingsVal($name, "contextId", ""); $timeout = 3600; $hash->{LONGPOLL} = time(); } elsif ($service eq "setCalendar") { $URL .= "alms/"; $URL .= ReadingsVal($name, "alm_sn", ""); $URL .= "/calendar"; $header = "x-im-context-id: ".ReadingsVal($name, "contextId", ""); $header .= "\r\nContent-Type: application/json"; $data = INDEGO_BuildCalendar($hash, $type); $method = "PUT"; } elsif ($service eq "firmware") { $URL .= "alms/"; $URL .= ReadingsVal($name, "alm_sn", ""); $header = "x-im-context-id: ".ReadingsVal($name, "contextId", ""); } elsif ($service eq "smartMode") { my $smartMode = (defined($type) && $type eq "on") ? "true" : "false"; $URL .= "alms/"; $URL .= ReadingsVal($name, "alm_sn", ""); $URL .= "/predictive"; $header = "x-im-context-id: ".ReadingsVal($name, "contextId", ""); $header .= "\r\nContent-Type: application/json"; $data = "{\"enabled\":".$smartMode."}"; $method = "PUT"; } else { $URL .= "alms/"; $URL .= ReadingsVal($name, "alm_sn", ""); $URL .= "/$service"; $header = "x-im-context-id: ".ReadingsVal($name, "contextId", ""); } # send request via HTTP method Log3 $name, 5, "INDEGO $name: $method $URL (" . urlDecode($data) . ")" if ( defined($data) ); Log3 $name, 5, "INDEGO $name: $method $URL" if ( !defined($data) ); Log3 $name, 5, "INDEGO $name: header $header" if ( defined($header) ); if ( defined($type) && $type eq "blocking" ) { my ($err, $data) = HttpUtils_BlockingGet( { url => $URL, timeout => 15, noshutdown => 1, header => $header, data => $data, method => $method, hash => $hash, service => $service, timestamp => $timestamp, } ); return $data; } else { HttpUtils_NonblockingGet( { url => $URL, timeout => $timeout, noshutdown => 1, header => $header, data => $data, method => $method, hash => $hash, service => $service, cmd => $type, timestamp => $timestamp, callback => \&INDEGO_ReceiveCommand, } ); } return; } ################################### sub INDEGO_ReceiveCommand($$$) { my ( $param, $err, $data ) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; my $service = $param->{service}; my $cmd = $param->{cmd}; my $rc = ( $param->{buf} ) ? $param->{buf} : $param; my $return; Log3 $name, 5, "INDEGO $name: called function INDEGO_ReceiveCommand() rc: $rc err: $err data: $data "; readingsBeginUpdate($hash); # device not reachable if ($err) { if ( !defined($cmd) || $cmd eq "" ) { Log3 $name, 4, "INDEGO $name:$service RCV $err"; } else { Log3 $name, 4, "INDEGO $name:$service/$cmd RCV $err"; } # keep last state #INDEGO_ReadingsBulkUpdateIfChanged( $hash, "state", "Error" ); } # data received elsif ($data) { if ( !defined($cmd) ) { Log3 $name, 4, "INDEGO $name: RCV $service"; } else { Log3 $name, 4, "INDEGO $name: RCV $service/$cmd"; } if ( $data ne "" ) { if ( $data =~ /^{/ || $data =~ /^\[/ ) { if ( !defined($cmd) || $cmd eq "" ) { Log3 $name, 4, "INDEGO $name: RES $service - $data"; } else { Log3 $name, 4, "INDEGO $name: RES $service/$cmd - $data"; } $return = decode_json( encode_utf8($data) ); } elsif ( $service = "map" ) { if ( !defined($cmd) || $cmd eq "" ) { Log3 $name, 4, "INDEGO $name: RES $service - $data"; } else { Log3 $name, 4, "INDEGO $name: RES $service/$cmd - $data"; } $return = $data; } else { Log3 $name, 3, "INDEGO $name: RES ERROR $service\n" . $data; if ( !defined($cmd) || $cmd eq "" ) { Log3 $name, 5, "INDEGO $name: RES ERROR $service\n$data"; } else { Log3 $name, 5, "INDEGO $name: RES ERROR $service/$cmd\n$data"; } return undef; } } # state if ( $service eq "state" or $service eq "longpollState") { if ( ref($return) eq "HASH" and !defined($cmd)) { INDEGO_ReadingsBulkUpdateIfChanged($hash, "state", INDEGO_BuildState($hash, $return->{state})) if (defined($return->{state})); INDEGO_ReadingsBulkUpdateIfChanged($hash, "state_id", $return->{state}) if (defined($return->{state})); INDEGO_ReadingsBulkUpdateIfChanged($hash, "mowed", $return->{mowed}) if (defined($return->{mowed})); INDEGO_ReadingsBulkUpdateIfChanged($hash, "mowed_ts", FmtDateTime(int($return->{mowed_ts}/1000))) if (defined($return->{mowed_ts})); #INDEGO_ReadingsBulkUpdateIfChanged($hash, "mapsvgcache_ts", FmtDateTime(int($return->{mapsvgcache_ts}/1000))) if (defined($return->{mapsvgcache_ts})); #INDEGO_ReadingsBulkUpdateIfChanged($hash, "map_update_available", $return->{map_update_available}); if ( ref($return->{runtime}) eq "HASH" ) { my $runtime = $return->{runtime}; if ( ref($runtime->{total}) eq "HASH" ) { my $total = $runtime->{total}; my $operate = $total->{operate}; my $charge = $total->{charge}; INDEGO_ReadingsBulkUpdateIfChanged($hash, "totalOperate", INDEGO_GetDuration($hash, $operate)); INDEGO_ReadingsBulkUpdateIfChanged($hash, "totalCharge", INDEGO_GetDuration($hash, $charge)); } if ( ref($runtime->{session}) eq "HASH" ) { my $session = $runtime->{session}; my $operate = $session->{operate}; my $charge = $session->{charge}; INDEGO_ReadingsBulkUpdateIfChanged($hash, "sessionOperate", INDEGO_GetDuration($hash, $operate)); INDEGO_ReadingsBulkUpdateIfChanged($hash, "sessionCharge", INDEGO_GetDuration($hash, $charge)); } } readingsEndUpdate( $hash, 1 ); INDEGO_CheckLongpoll($hash) if ($service eq "state"); INDEGO_SendCommand($hash, "longpollState") if ($service eq "longpollState"); INDEGO_SendCommand($hash, "alerts"); INDEGO_SendCommand($hash, "location"); INDEGO_SendCommand($hash, "predictive"); INDEGO_SendCommand($hash, "predictive/nextcutting"); INDEGO_SendCommand($hash, "predictive/useradjustment"); INDEGO_SendCommand($hash, "predictive/useradjustment?withProposal=true"); INDEGO_SendCommand($hash, "map") if ($return->{map_update_available}); } } # firmware elsif ( $service eq "firmware" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "alm_name", $return->{alm_name}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "service_counter", $return->{service_counter}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "bareToolnumber", $return->{bareToolnumber}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alm_firmware_version", $return->{alm_firmware_version}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "model", INDEGO_GetModel($hash, $return->{bareToolnumber})) if (defined($return->{bareToolnumber})); readingsEndUpdate( $hash, 1 ); } } # alerts elsif ( $service eq "alerts" ) { if ( ref($return) eq "ARRAY" and scalar(@{$return}) > 0) { my $date; my $alert; foreach $alert (@{$return}) { my $current_date = time_str2num(substr($alert->{date}, 0, 19)); if (!defined($date) or $date < $current_date) { $date = $current_date; INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_number", scalar(@{$return})); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_id", $alert->{alert_id}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_headline", $alert->{headline}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_date", FmtDateTime($current_date + fhemTzOffset($current_date))); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_message", $alert->{message}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_flag", $alert->{flag}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_status", $alert->{read_status}); } } } readingsEndUpdate( $hash, 1 ); } # updates elsif ( $service eq "updates" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "updates", $return->{available} ? "available" : "unavailable"); readingsEndUpdate( $hash, 1 ); } } # security elsif ( $service eq "security" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "security", $return->{enabled} ? "enabled" : "disabled"); INDEGO_ReadingsBulkUpdateIfChanged($hash, "autolock", $return->{autolock} ? "true" : "false"); readingsEndUpdate( $hash, 1 ); } } # automaticUpdate elsif ( $service eq "automaticUpdate" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "allow_automatic_update", $return->{allow_automatic_update} ? "true" : "false"); readingsEndUpdate( $hash, 1 ); } } # location elsif ( $service eq "location" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "latitude", $return->{latitude}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "longitude", $return->{longitude}); readingsEndUpdate( $hash, 1 ); } } # predictive/nextcutting elsif ( $service eq "predictive" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "fc_enabled", $return->{enabled}); readingsEndUpdate( $hash, 1 ); } } # predictive/location elsif ( $service eq "predictive/location" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "fc_loc_latitude", $return->{latitude}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "fc_loc_longitude", $return->{longitude}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "fc_loc_timezone", $return->{timezone}); readingsEndUpdate( $hash, 1 ); } } # predictive/nextcutting elsif ( $service eq "predictive/nextcutting" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "mow_next", $return->{mow_next}); readingsEndUpdate( $hash, 1 ); } } # predictive/useradjustment elsif ( $service eq "predictive/useradjustment" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "user_adjustment", $return->{user_adjustment}); readingsEndUpdate( $hash, 1 ); } } # predictive/useradjustment?withProposal=true elsif ( $service eq "predictive/useradjustment?withProposal=true" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "user_adjustment_proposed", $return->{user_adjustment}); readingsEndUpdate( $hash, 1 ); } } # calendar elsif ( $service eq "calendar" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "cal", $return->{sel_cal}); my %currentCals; foreach ( keys %{ $hash->{READINGS} } ) { $currentCals{$_} = 1 if ( $_ =~ /^cal\d_.*/ ); } if ( ref($return->{cals}) eq "ARRAY" ) { my @cals = @{$return->{cals}}; my $cal; foreach $cal (@cals) { my @days = @{$cal->{days}}; my $day; for $day (@days) { my $schedule; my @slots = @{$day->{slots}}; my $slot; for $slot (@slots) { if ($slot->{En}) { my $slotStr = INDEGO_GetSlotFormatted($hash, $slot); if (defined($schedule)) { $schedule .= " ".$slotStr; } else { $schedule = $slotStr; } } } if (defined($schedule)) { my $reading = "cal".$cal->{cal}."_".$day->{day}."_".INDEGO_GetDay($hash, $day->{day}); INDEGO_ReadingsBulkUpdateIfChanged($hash, $reading, $schedule) ; delete $currentCals{$reading}; } } } } #remove outdated calendar information foreach ( keys %currentCals ) { delete( $hash->{READINGS}{$_} ); } readingsEndUpdate( $hash, 1 ); } } # predictive/calendar elsif ( $service eq "predictive/calendar" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "fc_cal", $return->{sel_cal}); my %currentCals; foreach ( keys %{ $hash->{READINGS} } ) { $currentCals{$_} = 1 if ( $_ =~ /^fc_cal\d_.*/ ); } if ( ref($return->{cals}) eq "ARRAY" ) { my @cals = @{$return->{cals}}; my $cal; foreach $cal (@cals) { my @days = @{$cal->{days}}; my $day; for $day (@days) { my $schedule; my @slots = @{$day->{slots}}; my $slot; for $slot (@slots) { if ($slot->{En}) { my $slotStr = INDEGO_GetSlotFormatted($hash, $slot); if (defined($schedule)) { $schedule .= " ".$slotStr; } else { $schedule = $slotStr; } } } if (defined($schedule)) { my $reading = "fc_cal".$cal->{cal}."_".$day->{day}."_".INDEGO_GetDay($hash, $day->{day}); INDEGO_ReadingsBulkUpdateIfChanged($hash, $reading, $schedule) ; delete $currentCals{$reading}; } } } } #remove outdated calendar information foreach ( keys %currentCals ) { delete( $hash->{READINGS}{$_} ); } readingsEndUpdate( $hash, 1 ); } } # predictive/weather elsif ( $service eq "predictive/weather" ) { if ( ref($return) eq "HASH") { if ( ref($return->{LocationWeather}) eq "HASH" ) { my $weather = $return->{LocationWeather}; if ( ref($weather->{location}) eq "HASH" ) { my $location = $weather->{location}; INDEGO_ReadingsBulkUpdateIfChanged($hash, "fc_loc_name", $location->{name}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "fc_loc_country", $location->{country}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "fc_loc_dtz", $location->{dtz}); } } readingsEndUpdate( $hash, 1 ); } } # map elsif ( $service eq "map" ) { if ( defined($return) and !ref($return)) { my $map = $return; eval { require Compress::Zlib; }; unless($@) { $map = Compress::Zlib::compress($map); } INDEGO_ReadingsBulkUpdateIfChanged($hash, ".mapsvgcache", $map ); readingsEndUpdate( $hash, 1 ); } } # authenticate elsif ( $service eq "authenticate" ) { if ( ref($return) eq "HASH") { INDEGO_ReadingsBulkUpdateIfChanged($hash, "contextId", $return->{contextId}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "userId", $return->{userId}); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alm_sn", $return->{alm_sn}); readingsEndUpdate( $hash, 1 ); # new context received - reload state INDEGO_SendCommand($hash, "state"); INDEGO_TriggerFullDataUpdate($hash); # re-execute previous command if (defined($cmd)) { if ($cmd =~ /(\w+)\/(\w+)/) { INDEGO_SendCommand($hash, $1, $2); } else { INDEGO_SendCommand($hash, $cmd); } } } } # all other command results else { Log3 $name, 2, "INDEGO $name: ERROR: method to handle response of $service not implemented"; } } else { if ($rc =~ /401/) { Log3 $name, 4, "INDEGO $name: authentication context invalidated"; if ( $service =~ /deleteAlert|setCalendar/) { INDEGO_SendCommand($hash, "authenticate", "$service"); } elsif ($service eq "state" and defined($cmd)) { INDEGO_SendCommand($hash, "authenticate", "$service/$cmd"); } else { readingsSingleUpdate($hash, "contextId", "", 1); } $hash->{LONGPOLL} = 0 if ($service eq "longpollState"); } # no alerts elsif ( $service eq "alerts" and $rc =~ /204 User found but no alerts were found/) { INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_number", 0); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_id", "-"); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_headline", "-"); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_date", "-"); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_message", "-"); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_flag", "-"); INDEGO_ReadingsBulkUpdateIfChanged($hash, "alert_status", "-"); } # deleteAlert elsif ( $service eq "deleteAlert" ) { INDEGO_SendCommand($hash, "alerts"); } # setCalendar elsif ( $service eq "setCalendar" ) { INDEGO_SendCommand($hash, "calendar"); } # smartMode elsif ( $service eq "smartMode" ) { INDEGO_ReadingsBulkUpdateIfChanged($hash, "fc_enabled", ($rc->{cmd} eq "on") ? 1 : 0) if ($rc->{httpheader} =~ /HTTP\/1.\d 200/); readingsEndUpdate( $hash, 1 ); } } return; } sub INDEGO_CheckContext($) { my ($hash) = @_; my $name = $hash->{NAME}; my $contextId = ReadingsVal($name, "contextId", ""); my $contextAge = ReadingsAge($name, "contextId", 0); if ($contextId eq "" or $contextAge > 7200) { INDEGO_SendCommand($hash, "authenticate"); return; } return $contextId; } sub INDEGO_CheckLongpoll($) { my ($hash) = @_; my $name = $hash->{NAME}; return if ( AttrVal($name, "disable", 0) == 1 ); if (!defined($hash->{LONGPOLL}) or time() - $hash->{LONGPOLL} > 3600) { Log3 $name, 4, "INDEGO $name: Request GET state (longPoll)"; INDEGO_SendCommand($hash, "longpollState"); } } sub INDEGO_TriggerFullDataUpdate($) { my ( $hash ) = @_; INDEGO_SendCommand($hash, "firmware"); INDEGO_SendCommand($hash, "automaticUpdate"); INDEGO_SendCommand($hash, "calendar"); INDEGO_SendCommand($hash, "updates"); INDEGO_SendCommand($hash, "security"); INDEGO_SendCommand($hash, "predictive/calendar"); INDEGO_SendCommand($hash, "predictive/location"); INDEGO_SendCommand($hash, "predictive/weather"); INDEGO_SendCommand($hash, "map"); } sub INDEGO_ReadingsBulkUpdateIfChanged($$$) { my ($hash,$reading,$value) = @_; my $name = $hash->{NAME}; $value = "" if (!defined($value)); readingsBulkUpdate($hash, $reading, $value) if (ReadingsVal($name, $reading, "") ne $value); } sub INDEGO_GetSlotFormatted($$) { my ($hash,$slot) = @_; return sprintf("%02d:%02d-%02d:%02d", $slot->{StHr}, $slot->{StMin}, $slot->{EnHr}, $slot->{EnMin}); } sub INDEGO_GetDuration($$) { my ($hash,$duration) = @_; return sprintf("%d:%02d", int($duration/60), $duration-int($duration/60)*60); } sub INDEGO_GetDay($$) { my ($hash,$day) = @_; my $days = { '0' => "Mon", '1' => "Tue", '2' => "Wed", '3' => "Thu", '4' => "Fri", '5' => "Sat", '6' => "Sun", }; return $days->{$day}; } sub INDEGO_GetModel($$) { my ($hash,$baretool) = @_; my $models = { "3600HA2300" => "1000", "3600HA2301" => "1200", "3600HA2302" => "1100", "3600HA2303" => "13C", "3600HA2304" => "10C", "3600HB0100" => "350", "3600HB0101" => "400" }; if (defined( $models->{$baretool})) { return $models->{$baretool}; } else { return $models; } } sub INDEGO_BuildState($$) { my ($hash,$state) = @_; my $states = { '0' => "Reading status", '257' => "Charging", '258' => "Docked", '259' => "Docked - Software update", '260' => "Docked", '261' => "Docked", '262' => "Docked - Loading map", '263' => "Docked - Saving map", '512' => "Leaving dock", '513' => "Mowing", '514' => "Relocalising", '515' => "Loading map", '516' => "Learning lawn", '517' => "Paused", '518' => "Border cut", '519' => "Idle in lawn", '520' => "Learning lawn", '768' => "Returning to dock", '769' => "Returning to dock", '770' => "Returning to dock", '771' => "Returning to dock - Battery low", '772' => "Returning to dock - Calendar timeslot ended", '773' => "Returning to dock - Battery temp range", '774' => "Returning to dock", '775' => "Returning to dock - Lawn complete", '776' => "Returning to dock - Relocalising", '1025' => "Diagnostic mode", '1026' => "End of live", '1281' => "Software update", '1537' => "Low power mode - Check PIN request on display", '64513' => "Docked - Waking up" }; if (defined( $states->{$state})) { return $states->{$state}; } else { return $state; } } sub INDEGO_BuildCalendar($$) { my ($hash,$selected) = @_; my $name = $hash->{NAME}; # create calendar object my @cals; for (my $i=1; $i<=5; $i++) { my @days; for (my $j=0; $j<=6; $j++) { my @slots; for (my $k=0; $k<2; $k++) { my %slot = ( "En" => \0, "StHr" => 0, "StMin" => 0, "EnHr" => 0, "EnMin" => 0 ); push(@slots, \%slot); } my %day = ( "day" => $j, "slots" => \@slots ); push(@days, \%day); } my %cal = ( "cal" => $i, "days" => \@days ); push(@cals, \%cal); } # set current data foreach ( keys %{ $hash->{READINGS} } ) { if ( $_ =~ /^cal(\d)_(\d)_.*/ ) { my $calnr = $1; $calnr--; # array starts with 0 my $daynr = $2; my $value = ReadingsVal($name, $_, ""); Log3 $name, 3, "--> $value"; if ($value =~ /^(\d{2}):(\d{2})-(\d{2}):(\d{2}) (\d{2}):(\d{2})-(\d{2}):(\d{2})$/) { my $slot1 = $cals[$calnr]->{days}[$daynr]->{slots}[0]; $slot1->{En} = \1; $slot1->{StHr} = int($1); $slot1->{StMin} = int($2); $slot1->{EnHr} = int($3); $slot1->{EnMin} = int($4); my $slot2 = $cals[$calnr]->{days}[$daynr]->{slots}[1]; $slot2->{En} = \1; $slot2->{StHr} = int($5); $slot2->{StMin} = int($6); $slot2->{EnHr} = int($7); $slot2->{EnMin} = int($8); } elsif ($value =~ /^(\d{2}):(\d{2})-(\d{2}):(\d{2})$/) { my $slot1 = $cals[$calnr]->{days}[$daynr]->{slots}[0]; $slot1->{En} = \1; $slot1->{StHr} = int($1); $slot1->{StMin} = int($2); $slot1->{EnHr} = int($3); $slot1->{EnMin} = int($4); } } } my %calendar = ( "sel_cal" => int($selected), "cals" => \@cals ); return encode_json(\%calendar); } sub INDEGO_StorePassword($$) { my ($hash, $password) = @_; my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; my $key = getUniqueId().$index; my $enc_pwd = ""; if(eval "use Digest::MD5;1") { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } for my $char (split //, $password) { my $encode=chop($key); $enc_pwd.=sprintf("%.2x",ord($char)^ord($encode)); $key=$encode.$key; } my $err = setKeyValue($index, $enc_pwd); return "error while saving the password - $err" if(defined($err)); return "password successfully saved"; } sub INDEGO_ReadPassword($) { my ($hash) = @_; my $name = $hash->{NAME}; my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; my $key = getUniqueId().$index; my ($password, $err); Log3 $name, 4, "INDEGO $name: Read password from file"; ($err, $password) = getKeyValue($index); if ( defined($err) ) { Log3 $name, 3, "INDEGO $name: unable to read password from file: $err"; return undef; } if ( defined($password) ) { if ( eval "use Digest::MD5;1" ) { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } my $dec_pwd = ''; for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { my $decode=chop($key); $dec_pwd.=chr(ord($char)^ord($decode)); $key=$decode.$key; } return $dec_pwd; } else { Log3 $name, 3, "INDEGO $name: No password in file"; return undef; } } sub INDEGO_ShowMap($;$$) { my ($name,$width,$height) = @_; my $hash = $main::defs{$name}; my $compress = 0; eval { require Compress::Zlib; }; unless($@) { $compress = 1; } my $map = ReadingsVal($name, ".mapsvgcache", ""); my $data = $map; $width = 800 if (!defined($width)); if ($map eq "") { $map = INDEGO_SendCommand($hash, "map", "blocking"); $data = $map; $map = Compress::Zlib::compress($map) if ($compress); readingsSingleUpdate($hash, ".mapsvgcache", $map, 1); } else { $data = Compress::Zlib::uncompress($data) if ($compress); } if (defined($data) and $data ne "") { if (!defined($height) and $data =~ /viewBox="0 0 (\d+) (\d+)"/) { my $factor = $1/$width; $height = int($2/$factor); } my $html = '

INDEGO

    This module controls a Bosch Indego.

    Define
=end html =begin html_DE

INDEGO

    Diese Module dient zur Steuerung eines Bosch Indego

    Define
=end html_DE =cut