#=============================================================================== # $Id: 98_TadoAPI.pm 102 2019-12-27 15:58:26Z psycho160 $ # # FILE: 98_TadoAPI.pm # # USAGE: Module for FHEM # Info: Turn verbose on for debugging # # REQUIREMENTS: Below modules should be pre-installed. # HTTP::Request::Common # HTTP::Headers # Data::Dumper; # JSON # # BUGS: --- # NOTES: --- # AUTHOR: Philipp Wolfmajer # ORGANIZATION: # VERSION: 0.6 # CREATED: 04/12/2019 # REVISION: 12/10/2019 #=============================================================================== package main; use strict; use warnings; use utf8; use HTTP::Request::Common qw (POST GET PUT); use HTTP::Headers; use JSON; use POSIX qw(strftime); ####DEFAULTS############ my $client_id='public-api-preview'; my $client_secret='4HJGRffVR8xb3XdEUQpjgZ1VplJi6Xgw'; my $scope='home.user'; my $AuthURL = qq{https://auth.tado.com/oauth/token}; my $DataURL = qq{https://my.tado.com/api/v2/me}; my $QueryURL = qq{https://my.tado.com/api/v2/homes}; my $tokenFile = "./FHEM/FhemUtils/TadoAPI_token"; my $header = {}; my $data = {}; my $TokenData = {}; my $reqDebug = 5; # helpers my $apiStatus = 1; my %sets = ( "zoneUpdate" => "", "refreshToken" => "noArg", "password" => "", "update" => "noArg", "setGeo" => "", "setZoneOverlay" => "", "timedZoneOverlay" => "", "updateAllOverlays" => "noArg", "setAllOverlays" => "" ); my %gets = ( "getZoneDevices" => "noArg", "getZoneInfo" => "noArg", "getGeo" => "", #"getXTest" => "", "getMobileDevices" => "noArg" ); sub TadoAPI_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "TadoAPI_Define"; $hash->{InitFn} = "TadoAPI_Init"; $hash->{SetFn} = "TadoAPI_Set"; $hash->{GetFn} = "TadoAPI_Get"; $hash->{AttrList} = "homeID " . "mobileID " . "showPosData:0,1 " . "updateIntervall " . $main::readingFnAttributes; } sub TadoAPI_Init($$) { my ($hash,$args) = @_; my $u = "wrong syntax: define TadoAPI []"; return $u if(int(@$args) < 2); return undef; } sub TadoAPI_Define($$) { my ($hash, $def) = @_; my @a = split( "[ \t]+", $def ); my $name = shift @a; my $type = shift @a; my $tokenFileName = $tokenFile."_".$name; return "Invalid number of arguments: " . "define TadoAPI []" if ( int(@a) < 1 ); my ( $user, $homeID ) = @a; Log3 $name, 3, "TadoAPI_Define $name: called "; $hash->{STATE}="defined"; # Initialize the device return $@ unless ( FHEM::Meta::SetInternals($hash) ); $hash->{TADO_USER} = $user; $hash->{TOKEN_FILE} = $tokenFileName; my @args = ($homeID); if ($main::init_done) { # do something? return TadoAPI_Catch($@) if $@; } my $password = TadoAPI_readPassword($name); if (defined($password)){ TadoAPI_CheckStatus($hash); TadoAPI_LoadToken($hash); # start the status update timer RemoveInternalTimer($hash); InternalTimer( gettimeofday() + 15, "TadoAPI_Update", $hash, 0 ); if ( defined($homeID) && $homeID ne "" ) { $attr{$name}{homeID} = $homeID; } else{ my $id = TadoAPI_GetHomeId($hash); if ( defined($id) && $id ne "" ) { $attr{$name}{homeID} = $id; } } }else{ $hash->{STATE}="no password set"; } return undef; } sub TadoAPI_Set(@) { my ($hash, @a) = @_; return "Need at least one parameters" if(@a < 2); my $cmd = $a[1]; my $value = $a[2]; my $name = $hash->{NAME}; my $subcmd; my $message = undef; if(!defined($sets{$cmd})) { my @cmds = (); foreach my $key (sort keys %sets) { push @cmds, $sets{$key} ? $key.":".join(",",$sets{$key}) : $key; } return "Unknown argument $a[1], choose one of " . join(" ", @cmds); } if (($cmd ne "password")) { my $pwd = TadoAPI_readPassword($name); unless (defined $pwd) { $message = "Error: no tado password set. Please define it with 'set $name password Your_tado_Password'"; Log3 $name,2,"$name, $message"; $hash->{STATE}="no password set"; return $message; } } if( $cmd eq 'setGeo' ) { return "Need at least two parameters (mobileID, Setting)" if(@a < 4); if( $a[3] eq "on" ) { Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)"; TadoAPI_SetGeoById($hash, $value, 1); } else { Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)"; TadoAPI_SetGeoById($hash, $value, 0); } TadoAPI_GetGeoById($hash, $value); } elsif( $cmd eq 'setZoneOverlay' ) { Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)"; return "Need at least two parameters [ZoneID] [Setting] (duration in sec); Setting Info: remove=delete overlay; 0=heating power off; 1<=desired temperature (overlay)" if(@a < 4); if( $a[3] eq "remove" ) { TadoAPI_SetZoneOverlayById($hash, $value, "remove"); } elsif (defined($a[4])) { TadoAPI_SetZoneOverlayById($hash, $value, $a[3], $a[4]); } elsif ($a[3] >= 0) { TadoAPI_SetZoneOverlayById($hash, $value, $a[3]); } Log3 $name, 4, "TadoAPI $name" . ": " . "$cmd finished"; } elsif( $cmd eq 'timedZoneOverlay' ) { Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)"; return "Need at least three parameters [ZoneID] [Duration (sec)] [Setting]" if(@a < 4); if( defined($a[4]) ) { TadoAPI_SetTimedZoneOverlay($hash, $value, $a[3], $a[4]); } Log3 $name, 4, "TadoAPI $name" . ": " . "$cmd finished"; } elsif( $cmd eq 'setAllOverlays' ) { Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)"; return "Need at least one parameter (Setting) - Setting: remove=delete overlay; 0=heating power off; 1<=desired temperature (overlay)" if(@a < 3); if( $value eq "remove" ) { TadoAPI_SetAllOverlays($hash, "remove"); } elsif ($value >= 0) { TadoAPI_SetAllOverlays($hash, $value); } Log3 $name, 4, "TadoAPI $name" . ": " . "$cmd finished\n"; } elsif( $cmd eq 'updateAllOverlays' ) { Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)"; TadoAPI_GetAllZoneOverlays($hash); Log3 $name, 4, "TadoAPI $name" . ": " . "$cmd finished\n"; } elsif( $cmd eq 'refreshToken' ) { Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)"; RemoveInternalTimer($hash); InternalTimer( gettimeofday() + 10, "TadoAPI_Update", $hash, 0 ); TadoAPI_LoadToken($hash); Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; } elsif( $cmd eq 'update' ) { Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)"; TadoAPI_UpdateFn($hash); Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; } elsif( $cmd eq 'zoneUpdate' ) { Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)"; return "ZoneID as parameter needed" if (!$value); if( $value >= 1 ) { my ($temperature, $humidity, $desiredTemp, $currentHeatingPower, $overlay ) = TadoAPI_GetZoneReadingsById($hash, $value); my $zoneName = TadoAPI_GetZoneNameById($hash, $value); readingsBeginUpdate($hash); readingsBulkUpdate($hash, "Temperatur_" . $zoneName, $temperature); readingsBulkUpdate($hash, "Luftfeuchtigkeit_" . $zoneName, $humidity); readingsBulkUpdate($hash, "Heizleistung_" . $zoneName, $currentHeatingPower); readingsBulkUpdate($hash, "OverlayType_" . $zoneName, $overlay); readingsBulkUpdate($hash, "DesiredTemp_" . $zoneName, $desiredTemp); readingsEndUpdate($hash, 1); $message = "OK"; } else { return "Wrong ZoneID"; } Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; } elsif( $cmd eq 'password' ) { Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)"; # name und cmd überspringen shift @a; shift @a; # den Rest der das passwort enthält, als ein String $subcmd = join(" ",@a); $message = TadoAPI_storePassword($name,$subcmd); # start the status update timer RemoveInternalTimer($hash); InternalTimer( gettimeofday() + 10, "TadoAPI_Update", $hash, 0 ); Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; } return $message if $message; return TadoAPI_Catch($@) if $@; } sub TadoAPI_Get(@) { my ($hash, @a) = @_; return "Need at least one parameters" if(@a < 2); my $cmd = $a[1]; my $value = $a[2]; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $message = undef; if(!defined($gets{$cmd})) { my @cmds = (); foreach my $key (sort keys %gets) { push @cmds, $gets{$key} ? $key.":".join(",",$gets{$key}) : $key; } return "Unknown argument $a[1], choose one of " . join(" ", @cmds); } my $pwd = TadoAPI_readPassword($name); unless (defined $pwd) { $message = "Error: no tado password set. Please define it with 'set $name password Your_tado_Password'"; Log3 $name,2,"$name, $message"; $hash->{STATE}="no password set"; return $message; } if($cmd =~ /\Qget\E/) { COMMAND_HANDLER: { $cmd eq "getGeo" and do { return "Need at least one parameter (mobileID)" if(@a < 3); return "Wrong MobileID" if (length($value) < 6); Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)"; TadoAPI_GetGeoById($hash, $value); Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; last; }; $cmd eq "getMobileDevices" and do { Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)"; my @data = TadoAPI_GetMobileDevices($hash); $message = "Device List:\n"; foreach my $item ( @data ){ $message .= $item->{'name'} . ": " . $item->{'id'} . "\n"; }; Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished"; last; }; # only for testing $cmd eq "getXTest" and do { Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)"; my $zoneName = TadoAPI_GetZoneNameById($hash, $value); $zoneName = "wrong Zone ID" unless $zoneName; $message = "Name: " . $zoneName; Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; last; }; $cmd eq "getZoneDevices" and do { Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)"; my @devArr = TadoAPI_GetTadoDevices($hash); my $devicecount = 0; $message = "Tado-Device(s):\n"; for (my $i=0; $i < @devArr; $i++){ my $tadodevices = $devArr[$i]->{'devices'}; $message .= "ZoneID: " . ($devArr[$i]->{'id'}); my $spacer = 0; foreach my $item ( @$tadodevices ){ $message .= "\t " if ($spacer > 0); $message .= " " . $item->{'serialNo'} . " Battery: " . $item->{'batteryState'} . "\n"; $devicecount++; $spacer++; } } $message .= "There are $devicecount Tado-Device(s) in this HomeID(" . $attr{$name}{homeID} . ")." ; Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; last; }; $cmd eq "getZoneInfo" and do { Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)"; TadoAPI_GetZoneInfo($hash); my $zonecount = TadoAPI_GetZoneCount($hash); $message = "You have $zonecount Zones in Home " . $attr{$name}{homeID} . ".\n"; my @devArr = TadoAPI_GetTadoDevices($hash); for (my $i=0; $i < @devArr; $i++){ my $zoneid = $devArr[$i]->{'id'}; $message .= "Zone ID $zoneid: " . TadoAPI_GetZoneNameById($hash, $zoneid) . "\n"; } $message .= "See Logfile for more Info"; Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; last; }; } return $message if $message; return TadoAPI_Catch($@) if $@; return undef; } } sub TadoAPI_Catch($) { my $exception = shift; if ($exception) { $exception =~ /^(.*)( at.*FHEM.*)$/; return $1; } return undef; } sub TadoAPI_Undefine($$) { my ( $hash, $name ) = @_; RemoveInternalTimer($hash); #todo remove tokenfile return undef; } sub TadoAPI_CheckStatus(@){ my ($hash) = @_; my $name = $hash->{NAME}; # test api status my $param = { url => $AuthURL, timeout => 5, hash => $hash, method => "GET", header => "", callback => \&TadoAPI_callback }; #test if api is reachable Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $AuthURL"; HttpUtils_NonblockingGet($param); return undef; } sub TadoAPI_LoadToken(@){ my ($hash) = @_; my $name = $hash->{NAME}; my $tokenFileName = $tokenFile."_".$name; my $tokenLifeTime = $hash->{TOKEN_LIFETIME}; $tokenLifeTime = 0 if(!defined $tokenLifeTime || $tokenLifeTime eq ''); my $Token = undef; if($apiStatus){ eval { open(TOKENFILE, '<', $tokenFileName) or die("ERROR: $!"); $Token = decode_json()}; if($@ || $tokenLifeTime < gettimeofday()){ Log3 $name, 5, "TadoAPI $name" . ": " . "Error while loading: $@ ,requesting new one" if $@; Log3 $name, 5, "TadoAPI $name" . ": " . "Token is expired, requesting new one" if $tokenLifeTime < gettimeofday(); $Token = TadoAPI_NewTokenRequest($hash); TadoAPI_CheckStatus($hash); }else{ Log3 $name, 5, "TadoAPI $name" . ": " . "Token expires at " . localtime($tokenLifeTime); # if token is about to expire, refresh him if(($tokenLifeTime-45) < gettimeofday()){ Log3 $name, 5, "TadoAPI $name" . ": " . "Token will expire soon, refreshing"; $Token = TadoAPI_TokenRefresh($hash); } } close(TOKENFILE); return $Token if $Token; } TadoAPI_CheckStatus($hash); return undef; } sub TadoAPI_NewTokenRequest(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $username = $hash->{TADO_USER}; my $password = TadoAPI_readPassword($name); my $tokenFileName = $tokenFile."_".$name; Log3 $name, 5, "TadoAPI $name" . ": " . "calling NewTokenRequest()"; $data = { client_id => $client_id, client_secret => $client_secret, username => $username, password => $password, scope => $scope, grant_type=>'password' }; my $param = { url => $AuthURL, method => 'POST', timeout => 5, hash => $hash, data => $data }; #Log3 $name, 5, 'Blocking GET: ' . Dumper($param); Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $AuthURL"; my ($err, $data) = HttpUtils_BlockingGet($param); if($err ne "") { Log3 $name, 3, "TadoAPI $name" . ": " . "NewTokenRequest: Error while requesting ".$param->{url}." - $err"; } elsif($data ne "") { Log3 $name, 5, "url ".$param->{url}." returned: $data"; my $decoded_data = eval { decode_json($data) }; if ($@){ Log3 $name, 3, "TadoAPI $name" . ": " . "NewTokenRequest: decode_json failed, invalid json. error: $@ "; }else{ #write token data in file open(TOKENFILE,">$tokenFileName") or die("ERROR: $!"); print TOKENFILE $data . "\n"; close(TOKENFILE); # token lifetime management $hash->{TOKEN_LIFETIME} = gettimeofday() + $decoded_data->{'expires_in'}; $hash->{TOKEN_LIFETIME_HR} = localtime($hash->{TOKEN_LIFETIME}); Log3 $name, 5, "TadoAPI $name" . ": " . "Retrived new authentication token successfully. Valid until " . localtime($hash->{TOKEN_LIFETIME}); $hash->{STATE}="reachable"; return $decoded_data; } } } sub TadoAPI_TokenRefresh(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $tokenFileName = $tokenFile."_".$name; # load token eval { open(TOKENFILE, '<', $hash->{TOKEN_FILE}) or die("ERROR: $!"); $TokenData = decode_json()}; $data = { client_id => $client_id, client_secret => $client_secret, scope => $scope, grant_type=>'refresh_token', refresh_token => $TokenData->{'refresh_token'} }; my $param = { url => $AuthURL, method => 'POST', timeout => 5, hash => $hash, data => $data }; #Log3 $name, 5, 'Blocking GET TokenRefresh: ' . Dumper($param); Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $AuthURL"; my ($err, $data) = HttpUtils_BlockingGet($param); if($err ne "") { Log3 $name, 3, "TadoAPI $name" . ": " . "TokenRefresh: Error in token retrival while requesting ".$param->{url}." - $err"; $hash->{STATE}="error"; } elsif($data ne "") { Log3 $name, 5, "url ".$param->{url}." returned: $data"; my $decoded_data = eval{decode_json($data);}; if ($@){ Log3 $name, 3, "TadoAPI $name" . ": " . "TokenRefresh: decode_json failed, invalid json. error:$@\n" if $@; $hash->{STATE}="error"; }else{ #write token data in file open(TOKENFILE,">$tokenFileName") or die("ERROR: $!"); print TOKENFILE $data . "\n"; close(TOKENFILE); # token lifetime management $hash->{TOKEN_LIFETIME} = gettimeofday() + $decoded_data->{'expires_in'}; $hash->{TOKEN_LIFETIME_HR} = localtime($hash->{TOKEN_LIFETIME}); Log3 $name, 5, "TadoAPI $name" . ": " . "TokenRefresh: Refreshed authentication token successfully. Valid until " . localtime($hash->{TOKEN_LIFETIME}); $hash->{STATE}="reachable"; return $decoded_data; } } } sub TadoAPI_Update(@){ my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 5, "TadoAPI $name" . ": " . "TadoAPI_Update called"; my $nextTimer = "none"; my $intervall = 300; $intervall = $attr{$name}{updateIntervall} if (defined($attr{$name}{updateIntervall}) && $attr{$name}{updateIntervall} =~ m/^-?\d+$/); $nextTimer = gettimeofday() + $intervall; $hash->{NEXT_UPDATE} = localtime($nextTimer); Log3 $name, 5, "TadoAPI $name" . ": " . "Next Timer = $nextTimer"; RemoveInternalTimer($hash); InternalTimer( $nextTimer, "TadoAPI_Update", $hash, 0 ); # update subs TadoAPI_UpdateFn($hash); return undef; } ######################## tado methods ######################## ############################################################## sub TadoAPI_SetZoneOverlayById(@){ my ($hash, $zoneID, $setting, $duration) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL = $QueryURL . qq{/$homeID/zones/$zoneID/overlay}; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ my $method = ""; my $myjson =undef; Log3 $name, 5, "TadoAPI $name" . ": SetOverlay for Zone $zoneID (Setting: " . $setting . ") - " . "query-URL: $URL"; my $dt = time(); $dt += $duration if defined($duration); # remove overlay if($setting eq "remove"){ $method = "DELETE"; Log3 $name, 3, "TadoAPI $name" . ": " . "Deleting Overlay for Zone $zoneID"; } # turn heating of elsif($setting == 0){ # turn off for timer if(defined($duration) && $duration > 0){ $method = "PUT"; $myjson = { type => "MANUAL", setting => { type => "HEATING", power => "OFF" }, termination => { type => "TIMER", durationInSeconds => $duration, expiry => strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt)) } }; Log3 $name, 3, "TadoAPI $name" . ": " . "Timer Overlay for Zone $zoneID . Power off for: $duration seconds"; } # infinite off else{ $method = "PUT"; $myjson = { setting => { type => "HEATING", power => "OFF", }, termination => { type => "MANUAL" }, }; } } # infinite temperature overlay elsif($setting > 0){ # timed overlay if(defined($duration) && $duration > 0){ $method = "PUT"; $myjson = { type => "MANUAL", setting => { type => "HEATING", power => "ON", temperature => { celsius => $setting } }, termination => { type => "TIMER", durationInSeconds => $duration, expiry => strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($dt)), } }; Log3 $name, 3, "TadoAPI $name" . ": " . "Set Timer Overlay for Zone $zoneID with $duration seconds expire."; } else{ # infinite setting $method = "PUT"; $myjson = { setting => { type => "HEATING", power => "ON", temperature => { celsius => $setting }, }, termination => { type => "MANUAL" }, }; } } $myjson = encode_json($myjson) if (defined($myjson)); my $request = { url => $URL, header => { "Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}" }, method => $method, timeout => 5, callback => \&Tado_UpdateZoneOverlayCallback, hash => $hash, setting => $setting, zoneID => $zoneID, data => $myjson }; Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL"; HttpUtils_NonblockingGet($request); } } sub TadoAPI_SetAllOverlays(@){ my ($hash, $setting, $duration) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my @zones = TadoAPI_GetTadoDevices($hash); for (my $i=0; $i < @zones; $i++) { my $zoneid = $zones[$i]->{'id'}; if(defined($duration) && $duration > 0){ TadoAPI_SetZoneOverlayById($hash, $zoneid, $setting, $duration); }else{ TadoAPI_SetZoneOverlayById($hash, $zoneid, $setting); } } } sub TadoAPI_GetAllZoneOverlays(@){ my ($hash) = @_; my $name = $hash->{NAME}; my @zones = TadoAPI_GetTadoDevices($hash); foreach my $zone ( @zones ){ my $zoneId = $zone->{'id'}; my $zoneName = TadoAPI_ReplaceUmlaute($zone->{'name'}); my ($temperature, $humidity, $desiredTemp, $currentHeatingPower, $overlay ) = TadoAPI_GetZoneReadingsById($hash, $zoneId); readingsSingleUpdate($hash, "OverlayType_" . $zoneName, $overlay, 1); } } sub TadoAPI_UpdateFn(@){ my ($hash) = @_; my $name = $hash->{NAME}; my $TokenData = TadoAPI_LoadToken($hash); my $homeID = $attr{$name}{homeID}; if($apiStatus == 1 && defined($TokenData)){ # zone specific updates my $URL = $QueryURL.qq{/$homeID/zones}; my $request = { url => $URL, header => { "Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}" }, method => 'GET', timeout => 25, incrementalTimout => 1, hash => $hash, callback => \&TadoAPI_UpdateAllZoneReadingsCallback }; Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "UpdFN: Request $URL"; HttpUtils_NonblockingGet($request); # mobile devices $URL=$QueryURL . qq{/$homeID/mobileDevices}; $request = { url => $URL, header => { "Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}" }, method => 'GET', timeout => 7, incrementalTimout => 1, callback => \&TadoAPI_UpdateMobileReadingsCallback, hash => $hash }; Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL"; HttpUtils_NonblockingGet($request); } } ######################################################################################################################################################################## # Callback Subs ######################################################################################################################################################################## sub TadoAPI_callback($){ my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; $param->{code} = 0 unless defined $param->{code}; if($param->{code} == 401 || $param->{code} == 400){ $apiStatus = 1; $hash->{STATE}="reachable"; Log3 $name, 5, "TadoAPI $name" . ": " . "API is reachable. Callback Status: " . $param->{code}; }else{ $apiStatus = 0; $hash->{STATE}="error"; Log3 $name, 3, "TadoAPI $name" . ": " . "API error: apiStatus $apiStatus ($err)"; } return undef; } sub TadoAPI_UpdateAllZoneReadingsCallback($){ my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; if($err ne "") { Log3 $name, 3, "Error in TadoAPI_UpdateZoneCallback while requesting ".$param->{url}." - $err"; return undef; } elsif($data ne "") { Log3 $name, 5, "url ".$param->{url}." returned: $data"; my $decoded_data = eval { decode_json($data) }; # if api returns error eval { my $error = @$decoded_data; }; if ($@){ Log3 $name, 3, "TadoAPI $name" . ": " . "UpdateAllZonesCallback: decode_json failed, invalid json. error:$@\n" if $@; Log3 $name, 3, "TadoAPI $name" . ": " . "UpdateAllZonesCallback: Error in decoded data, Code: " . $decoded_data->{'errors'}->[0]->{'code'} if (exists($decoded_data->{'errors'}->[0]->{'code'})); $hash->{LastRequest}="error"; }else{ readingsBeginUpdate($hash); foreach my $zone ( @$decoded_data ){ my $zoneid = $zone->{'id'}; my $zoneName = TadoAPI_ReplaceUmlaute($zone->{'name'}); Log3 $name, 5, "TadoAPI $name" . ": " . "Set Reading Update for Zone $zoneid "; my ($temperature, $humidity, $desiredTemp, $currentHeatingPower, $overlay ) = TadoAPI_GetZoneReadingsById($hash, $zoneid); # updates zone readings readingsBulkUpdate($hash, "Temperatur_" . $zoneName, $temperature); readingsBulkUpdate($hash, "Luftfeuchtigkeit_" . $zoneName, $humidity); readingsBulkUpdate($hash, "Heizleistung_" . $zoneName, $currentHeatingPower); readingsBulkUpdate($hash, "OverlayType_" . $zoneName, $overlay); readingsBulkUpdate($hash, "DesiredTemp_" . $zoneName, $desiredTemp); # iterate through all devices in zone my $devices = $zone->{'devices'}; foreach my $device ( @$devices ){ readingsBulkUpdate($hash, "Battery_" . $device->{'serialNo'}, $device->{'batteryState'}); } } readingsEndUpdate( $hash, 1 ); } my $zonecount = TadoAPI_GetZoneCount($hash); readingsBeginUpdate($hash); readingsBulkUpdate($hash, "ActiveZones", $zonecount); readingsEndUpdate( $hash, 0 ); $hash->{LastRequest}="OK"; } } sub Tado_UpdateZoneOverlayCallback($) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; my $zoneId = $param->{zoneID}; my $setting = $param->{setting}; if($err ne "") { Log3 $name, 3, "Error in UpdateZoneOverlayCallback while requesting ".$param->{url}." - $err"; return undef; } elsif($data ne "") { Log3 $name, 5, "url ".$param->{url}." returned: $data"; Log3 $name, 3, "TadoAPI $name" . ": " . "set (async) Overlay for Zone $zoneId to: $setting"; } # finaly update readings my ($temperature, $humidity, $desiredTemp, $currentHeatingPower, $overlay ) = TadoAPI_GetZoneReadingsById($hash, $zoneId); my $zoneName = TadoAPI_GetZoneNameById($hash, $zoneId); # updates zone readings readingsBeginUpdate($hash); # readingsBulkUpdate($hash, "Temperatur_" . $zoneName, $temperature); # readingsBulkUpdate($hash, "Luftfeuchtigkeit_" . $zoneName, $humidity); # readingsBulkUpdate($hash, "Heizleistung_" . $zoneName, $currentHeatingPower); readingsBulkUpdate($hash, "OverlayType_" . $zoneName, $overlay); readingsBulkUpdate($hash, "DesiredTemp_" . $zoneName, $desiredTemp); readingsEndUpdate( $hash, 1 ); } sub TadoAPI_LogInfoCallback($){ my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; if($err ne "") { Log3 $name, 3, "Error in LogInfoCallback while requesting ".$param->{url}." - $err"; return undef; } elsif($data ne "") { Log3 $name, 3, "TadoAPI $name" . ": " . $param->{infotext} .":\n" . $data . "\n"; } } sub TadoAPI_SetGeoByIdCallback($){ my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; if($err ne "") { Log3 $name, 3, "Error in TadoAPI_SetGeoByIdCallback while requesting ".$param->{url}." - $err"; } elsif($data ne "") { Log3 $name, 3, "url ".$param->{url}." returned: $data"; } } sub TadoAPI_UpdateMobileReadingsCallback($){ my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; if($err ne "") { Log3 $name, 3, "Error in UpdateMobileReadingsCallback while requesting ".$param->{url}." - $err"; } elsif($data ne "") { Log3 $name, 5, "url ".$param->{url}." returned: $data"; my $decoded_data = eval { decode_json($data) }; # if api returns error eval { my $error = @$decoded_data; }; if ($@){ Log3 $name, 3, "TadoAPI $name" . ": " . "Decode_json failed, invalid json. error:$@\n" if $@; Log3 $name, 3, "TadoAPI $name" . ": " . "Error in UpdateMobileReadingsCallback, Code: " . $decoded_data->{'errors'}->[0]->{'code'}; $hash->{LastRequest}="error"; }else{ foreach my $item ( @$decoded_data ){ TadoAPI_GetGeoById($hash, $item->{'id'}, $item); } } } } sub TadoAPI_GetZoneInfo(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ # HomeInfo my $URL = qq{https://my.tado.com/api/v2/me}; my $request = { url => $URL, header => { "Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}" }, method => 'GET', timeout => 8, infotext => "HomeInfos", hash => $hash, callback => \&TadoAPI_LogInfoCallback }; HttpUtils_NonblockingGet($request); # TadoDevicesInfo $URL = $QueryURL . qq{/$homeID/zones}; $request = { url => $URL, header => { "Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}" }, method => 'GET', timeout => 3, infotext => "Tado Devices Info", hash => $hash, callback => \&TadoAPI_LogInfoCallback }; Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL"; HttpUtils_NonblockingGet($request); # Mobileinfo $URL = $QueryURL . qq{/$homeID/mobileDevices}; $request = { url => $URL, header => { "Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}" }, method => 'GET', timeout => 3, infotext => "Mobile Devices Info", hash => $hash, callback => \&TadoAPI_LogInfoCallback }; HttpUtils_NonblockingGet($request); my @mobDev = TadoAPI_GetMobileDevices($hash); for (my $i=0; $i < @mobDev; $i++) { my $mobileID = $mobDev[$i]->{'id'}; $URL=$QueryURL . qq{/$homeID/mobileDevices/$mobileID/settings}; $request = { url => $URL, header => { "Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}" }, method => 'GET', timeout => 3, infotext => "Mobile Device $mobileID", hash => $hash, callback => \&TadoAPI_LogInfoCallback }; Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL"; HttpUtils_NonblockingGet($request); } # zones my @devArr = TadoAPI_GetTadoDevices($hash); for (my $i=0; $i < @devArr; $i++) { my $zoneid = $devArr[$i]->{'id'}; my $URL=$QueryURL . qq{/$homeID/zones/$zoneid/state}; my $infotext = "ZoneID $zoneid (" . TadoAPI_GetZoneNameById($hash, $zoneid) . ") Status"; my $request = { url => $URL, header => { "Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}" }, method => 'GET', timeout => 3, infotext => $infotext, hash => $hash, callback => \&TadoAPI_LogInfoCallback }; Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL"; HttpUtils_NonblockingGet($request); } } return undef; } sub TadoAPI_SetGeoById(@){ my ($hash, $mobileID, $geo) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL . qq{/$homeID/mobileDevices/$mobileID/settings}; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ if($geo){ my $data = { geoTrackingEnabled=>"true" }; }else{ my $data = { geoTrackingEnabled=>"false" }; } $data = encode_json($data); my $request = { url => $URL, header => { "Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}" }, method => 'PUT', timeout => 3, mobileID => $mobileID, data => $data, hash => $hash, callback => \&TadoAPI_SetGeoByIdCallback }; Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL"; HttpUtils_NonblockingGet($request); } } ###################################### ############ Helpers ################# ###################################### sub TadoAPI_ReplaceUmlaute(@) { my ($string) = @_; my %umlaute = ("ä" => "ae", "Ä" => "Ae", "ü" => "ue", "Ü" => "Ue", "ö" => "oe", "Ö" => "Oe", "ß" => "ss" ); my $umlautkeys = join ("|", keys(%umlaute)); $string =~ s/($umlautkeys)/$umlaute{$1}/g; return $string; } # helper sub for fhem tablet-ui thermostat widget: set timedZoneOverlay sub TadoAPI_SetTimedZoneOverlay(@){ my ($hash, $zoneid, $duration, $setting) = @_; TadoAPI_SetZoneOverlayById($hash, $zoneid, $setting, $duration); } sub TadoAPI_GetHomeId(@){ # returns first home id only my ($hash) = @_; my $name = $hash->{NAME}; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)) { my $param = { url => $DataURL, header => {"Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"}, method => 'GET', timeout => 2, hash => $hash, }; #Log3 $name, 5, 'Blocking GET: ' . Dumper($param); Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $DataURL"; my ($err, $data) = HttpUtils_BlockingGet($param); if($err ne "") { Log3 $name, 3, "TadoAPI $name" . ": " . "GetHomeId: Error while requesting ".$param->{url}." - $err"; } elsif($data ne "") { Log3 $name, 5, "url ".$param->{url}." returned: $data"; my $decoded_data = eval { decode_json($data) }; if ($@){ Log3 $name, 3, "TadoAPI $name" . ": " . "GetHomeId: Decode_json failed, invalid json. error:$@" if $@; $hash->{LastRequest}="error"; }else{ $hash->{LastRequest}="OK"; return $decoded_data->{'homes'}->[0]->{'id'} if (exists($decoded_data->{'homes'})); } } } return undef; } sub TadoAPI_GetGeoById(@){ # returns geo setting and distance from home; takes an item object or querys itself my ($hash, $mobileID, $item) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL.qq{/$homeID/mobileDevices}; if(!defined($item)){ my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ my $param = { url => $URL, header => {"Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"}, method => 'GET', timeout => 4, hash => $hash, }; #Log3 $name, 5, 'Blocking GET: ' . Dumper($param); Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL"; my ($err, $data) = HttpUtils_BlockingGet($param); if($err ne "") { Log3 $name, 3, "TadoAPI $name" . ": " . "GetGeoById: Error while requesting ".$param->{url}." - $err"; } elsif($data ne "") { Log3 $name, 5, "url ".$param->{url}." returned: $data"; my $decoded_data = eval { decode_json($data) }; if ($@){ Log3 $name, 3, "TadoAPI $name" . ": " . "GetGeoById: Decode_json failed, invalid json. error:$@\n" if $@; Log3 $name, 3, "TadoAPI $name" . ": " . "GetGeoById: Error in UpdateMobileReadingsCallback, Code: " . $decoded_data->{'errors'}->[0]->{'code'}; $hash->{LastRequest}="error"; }else{ foreach my $item ( @$decoded_data ){ if($item->{'id'} eq $mobileID){ return my ($setting, $distance) = TadoAPI_ParseMobileItem($hash, $item); } } } } } }elsif(defined($item)){ Log3 $name, 5, "TadoAPI $name" . ": " . "GetGeoById: parsing passed item"; return my ($setting, $distance) = TadoAPI_ParseMobileItem($hash, $item); } return undef; } sub TadoAPI_ParseMobileItem(@){ my ($hash, $item) = @_; my $name = $hash->{NAME}; my $setting = 0; $setting = 1 if $item->{'settings'}->{'geoTrackingEnabled'}; my $distance = "-"; $distance = $item->{'location'}->{'relativeDistanceFromHomeFence'} if $setting; readingsBeginUpdate($hash); readingsBulkUpdate($hash, "GeoTracking_" . $item->{'id'}, $item->{'settings'}->{'geoTrackingEnabled'}); if(defined($item->{'location'}->{'atHome'}) && $item->{'location'}->{'atHome'}){ # present readingsBulkUpdate($hash, "GeoLocation_" . $item->{'id'}, "present"); }elsif(defined($item->{'location'}->{'atHome'})){ # away readingsBulkUpdate($hash, "GeoLocation_" . $item->{'id'}, "away"); }else{ # no state readingsDelete($hash, "GeoLocation_" . $item->{'id'}); } readingsBulkUpdate($hash, "GeoDistance_" . $item->{'id'}, $distance) if $setting && $attr{$name}{showPosData}; readingsDelete($hash, "GeoDistance_" . $item->{'id'}) if !defined($attr{$name}{showPosData}) || $attr{$name}{showPosData} == 0 || !$setting; readingsEndUpdate( $hash, 1 ); $hash->{LastRequest}="OK"; return ($setting, $distance); } sub TadoAPI_GetMobileDevices(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL . qq{/$homeID/mobileDevices}; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ my $param = { url => $URL, header => {"Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"}, method => 'GET', timeout => 2, hash => $hash }; #Log3 $name, 5, 'Blocking GET: ' . Dumper($param); Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL"; my ($err, $data) = HttpUtils_BlockingGet($param); if($err ne "") { Log3 $name, 3, "TadoAPI $name" . ": " . "GetMobileDevices: Error while requesting ".$param->{url}." - $err"; } elsif($data ne "") { my @devices = (); Log3 $name, 5, "url ".$param->{url}." returned: $data"; my $decoded_data = eval { decode_json($data) }; if ($@){ Log3 $name, 3, "TadoAPI $name" . ": " . "GetMobileDevices: decode_json failed, invalid json. error:$@\n"; }else{ if(ref($decoded_data) eq 'ARRAY'){ foreach my $item( @$decoded_data ) { push @devices, $item; } # default case return @devices; }elsif(ref($decoded_data) eq 'HASH'){ # error, api response is a hash in case of error Log3 $name, 3, "TadoAPI $name" . ": " . "GetMobileDevices: " . $decoded_data->{'errors'}->[0]->{'code'} if (exists($decoded_data->{'errors'})); } } } } return undef; } sub TadoAPI_GetZoneCount(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL.qq{/$homeID/zones}; my $zonecount = 0; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ my $param = { url => $URL, header => {"Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"}, method => 'GET', timeout => 2, hash => $hash }; #Log3 $name, 5, 'Blocking GET: ' . Dumper($param); Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL"; my ($err, $data) = HttpUtils_BlockingGet($param); if($err ne "") { Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneCount: Error while requesting ".$param->{url}." - $err"; } elsif($data ne "") { my @devices = (); Log3 $name, 5, "url ".$param->{url}." returned: $data"; my $decoded_data = eval { decode_json($data) }; if ($@){ Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneCount: decode_json failed, invalid json. error:$@\n"; }else{ if(ref($decoded_data) eq 'ARRAY'){ foreach my $item( @$decoded_data ) { $zonecount++; } return $zonecount; }elsif(ref($decoded_data) eq 'HASH'){ Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneCount: " . $decoded_data->{'errors'}->[0]->{'code'} if (exists($decoded_data->{'errors'})); } } } } return undef; } sub TadoAPI_GetZoneNameById(@) { my ($hash, $zoneID) = @_; my $name = $hash->{NAME}; my $zoneName = undef; my @zones = TadoAPI_GetTadoDevices($hash); foreach my $zone ( @zones ){ if ($zone->{'id'} == $zoneID){ $zoneName = TadoAPI_ReplaceUmlaute($zone->{'name'}); return $zoneName; } } Log3 $name, 3, "TadoAPI $name" . ": " . "Error GetZoneNameById: Wrong zone ID ($zoneID)"; return undef; } sub TadoAPI_GetZoneReadingsById(@){ my ($hash, $zoneID) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL.qq{/$homeID/zones/$zoneID/state}; my $temperature = 0; my $humidity = 0; my $desiredTemp = 0; my $currentHeatingPower = 0; my $overlay = 0; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ my $param = { url => $URL, header => {"Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"}, method => 'GET', timeout => 4, hash => $hash }; #Log3 $name, 5, 'Blocking GET: ' . Dumper($param); Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL"; my ($err, $data) = HttpUtils_BlockingGet($param); if($err ne "") { Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneReadingsById: Error while requesting ".$param->{url}." - $err"; } elsif($data ne "") { Log3 $name, 5, "url ".$param->{url}." returned: $data"; my $decoded_data = eval { decode_json($data) }; if ($@){ Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneReadingsById: Zone $zoneID decode_json failed, invalid json. error:$@\n"; }else{ $temperature = sprintf("%.1f", $decoded_data->{'sensorDataPoints'}->{'insideTemperature'}->{'celsius'}); $humidity = $decoded_data->{'sensorDataPoints'}->{'humidity'}->{'percentage'}; if($decoded_data->{'setting'}->{'power'} eq "OFF"){ $desiredTemp = "OFF"; }else{ $desiredTemp = $decoded_data->{'setting'}->{'temperature'}->{'celsius'}; } $currentHeatingPower = $decoded_data->{'activityDataPoints'}->{'heatingPower'}->{'percentage'}; $overlay = $decoded_data->{'overlayType'}; if (!defined $overlay) {$overlay = "no overlay"}; return ($temperature, $humidity, $desiredTemp, $currentHeatingPower, $overlay ); } } } return undef; } sub TadoAPI_GetTadoDevices(@) { # returns array with zonenames and zone devices my ($hash) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL . qq{/$homeID/zones}; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ my $param = { url => $URL, header => {"Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"}, method => 'GET', timeout => 5, hash => $hash }; #Log3 $name, 5, 'Blocking GET: ' . Dumper($param); Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL"; my ($err, $data) = HttpUtils_BlockingGet($param); if($err ne "") { Log3 $name, 3, "TadoAPI $name" . ": " . "RequestTadoDevices: Error while requesting ".$param->{url}." - $err"; } elsif($data ne "") { Log3 $name, 5, "url ".$param->{url}." returned: $data"; my $decoded_data = eval { decode_json($data) }; if ($@){ Log3 $name, 3, "TadoAPI $name" . ": " . "RequestTadoDevices: decode_json failed, invalid json. error:$@\n"; }else{ if(ref($decoded_data) eq 'ARRAY'){ my @devices = (); foreach my $dev (@$decoded_data){ push @devices, $dev; } return @devices; } } } } return undef; } ###################################################### # storePW & readPW Code geklaut aus 96_SIP.pm :) ###################################################### sub TadoAPI_storePassword($$) { my ($name, $password) = @_; my $index = "TadoAPI_".$name."_passwd"; my $key = getUniqueId().$index; my $e_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); $e_pwd.=sprintf("%.2x",ord($char)^ord($encode)); $key=$encode.$key; } my $error = setKeyValue($index, $e_pwd); return "error while saving TadoAPI password : $error" if(defined($error)); return "TadoAPI password successfully saved in FhemUtils/uniqueID Key $index"; } sub TadoAPI_readPassword($) { my ($name) = @_; my $index = "TadoAPI_".$name."_passwd"; my $key = getUniqueId().$index; my ($password, $error); #Log3 $name,5,"$name, read user password from FhemUtils/uniqueID Key $key"; ($error, $password) = getKeyValue($index); if ( defined($error) ) { Log3 $name,3, "$name, cant't read Tado password from FhemUtils/uniqueID: $error"; 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,"$name, no Tado password found in FhemUtils/uniqueID"; return undef; } } 1; =pod =item device =item summary integration of the Tado API =item summary_DE Anbindung der Tado Heizungssteuerung über API =begin html

TadoAPI

    The TadoAPI module connects your tado devices to FHEM. Most zone readings are shown and desired temperature for a zone can be set.
    TadoAPI makes use of the (unofficial) tado api and does NOT rely on any addition local client installed.
    Notes:
    • JSON has to be installed on the FHEM host.
      Please install the module (e.g. with sudo apt-get install libjson-perl) or the correct method for the underlying platform/system.
    Define
      The username and password must match the username and password used on the Tado website.
      After successful define, store PASSWORD with set <name> password <your-tado-password>.
      Note: Password is encrypted and saved in FHEM uniqueID file. All requests to the API are handeld via oauth2 token.
      Examples:
        define <name> TadoAPI mail@example.com [<homeID>]

    Set
    • set <name> <tado password>
      Stores password from tado account encrypted in FHEM.
      Without stored password all functions are blocked !
      IMPORTANT : if you rename the fhem Device you must set the password again!
    • set <name> update
      Reloads all information from the tado installation (devices, battery state, geolocation,...).
    • set <name> setZoneOverlay <zoneID> <setting> [<duration>]
      Setting: remove = delete overlay; 0 = heating power off; > 1 sets desired temperature to given value (overlay)
    • set <name> setAllOverlays <setting>
      Same as above, but for all zones
      Setting: remove = delete overlay; 0 = heating power off; > 1 sets desired temperature to given value (overlay)


    Attributes
    • homeID
      Home ID that will be used for API querys.
    • updateIntervall
      Intervall (in seconds) that is used for polling the tado API to update readings.
    • showPosData
      If set to 1 readings with relative distance to tado home are shown.

=end html # Ende der Commandref =cut