#!/usr/bin/env perl #=============================================================================== # # FILE: 98_TadoAPI_API.pm # # USAGE: Module for FHEM # Info: Turn $debug on for debugging # # REQUIREMENTS: Below modules should be pre-installed. # HTTP::Request::Common # HTTP::Headers # LWP::UserAgent::Paranoid # Data::Dumper; # JSON # # BUGS: --- # NOTES: --- # AUTHOR: Philipp Wolfmajer # ORGANIZATION: # VERSION: 1.0 # CREATED: 04/12/2019 07:55:44 PM # REVISION: 11/15/2019 05:17:22 PM #=============================================================================== package main; use strict; use warnings; use utf8; use FHEM::Meta; use HTTP::Request::Common qw (POST GET PUT); use HTTP::Headers; use LWP::UserAgent::Paranoid; use JSON; ####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 $debug = 0; my $header = {}; my $data = {}; my $TokenData = {}; my $apiStatus = 1; ##################################### #Request data for HTTP Request & Response my $Request=undef; my $Response=undef; #Empty variable for LWP User Agent for sending HTTP Request my $UserAgent = undef; my %sets = ( "getTemperature" => "", "refreshToken" => "noArg", "password" => "", "update" => "noArg", "setGeo" => "", "setZoneOverlay" => "", "setAllOverlays" => "" ); my %gets = ( "getZoneDevices" => "noArg", "getZoneInfo" => "noArg", "getGeo" => "", #"getXTest" => "noArg", "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 " . "debug:1,0 " . $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; return "Invalid number of arguments: " . "define TadoAPI []" if ( int(@a) < 2 ); my ( $user, $homeID, $mobileID ) = @a; Log3 $name, 3, "TadoAPI_Define $name: called "; $hash->{STATE}="defined"; # Initialize the device return $@ unless ( FHEM::Meta::SetInternals($hash) ); $hash->{TADO_USER} = $user; if ( defined($homeID) && $homeID ne "" ) { $attr{$name}{homeID} = $homeID; } if ( defined($mobileID) && $mobileID ne "" ) { $main::attr{$a[0]}{"mobileID"}= $mobileID; } my @args = ($homeID, $mobileID); if ($main::init_done) { # do something? return TadoAPI_Catch($@) if $@; } # start the status update timer RemoveInternalTimer($hash); InternalTimer( gettimeofday() + 10, "TadoAPI_Update", $hash, 0 ); 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; #debug $debug = $attr{$name}{debug}; 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 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); return undef; } elsif( $cmd eq 'setZoneOverlay' ) { Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)" if $debug; return "Need at least two parameters (ID, Setting)" if(@a < 4); if( $a[3] > 10 ) { TadoAPI_SetZoneOverlay($hash, $value, $a[3]); } else { TadoAPI_SetZoneOverlay($hash, $value, "off"); } Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; return undef; } elsif( $cmd eq 'setAllOverlays' ) { Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)" if $debug; return "Need at least one parameter (Setting)" if(@a < 3); if( $value > 1 ) { TadoAPI_SetAllOverlays($hash, $value); } else { TadoAPI_SetAllOverlays($hash, "off"); } Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; return undef; } elsif( $cmd eq 'refreshToken' ) { Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)"; RemoveInternalTimer($hash); InternalTimer( gettimeofday() + 10, "TadoAPI_Update", $hash, 0 ); TadoAPI_Connect($hash); return undef; } elsif( $cmd eq 'update' ) { Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)"; TadoAPI_UpdateFn($hash); return undef; } elsif( $cmd eq 'getTemperature' ) { Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)" if $debug; return "ZoneID as parameter needed" if (!$value); if( $value >= 1 ) { TadoAPI_GetZoneReadingsById($hash, $value); } else { return "Wrong ZoneID"; } Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; return undef; } elsif( $cmd eq 'password' ) { Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)" if $debug; # name und cmd überspringen shift @a; shift @a; # den Rest der das passwort enthält, als ein String $subcmd = join(" ",@a); return TadoAPI_storePassword($name,$subcmd); #Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; return undef; } 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 $mobileID = $attr{$name}{mobileID}; my $message = undef; #debug $debug = $attr{$name}{debug}; 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); } 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)" if $debug; 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)" if $debug; my @data = TadoAPI_RequestMobileDevices($hash); $message = "Device List:\n"; foreach my $item ( @data ){ print "\n"; $message .= $item->{'name'} . ": " . $item->{'id'} . "\n"; }; Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; last; }; $cmd eq "getXTest" and do { Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)" if $debug; TadoAPI_UpdateFn($hash); Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; last; }; $cmd eq "getZoneDevices" and do { Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)" if $debug; TadoAPI_Connect($hash); my @devArr = TadoAPI_RequestTadoDevices($hash); my $devicecount = 0; $message = "Tado-Device(s):\n"; for (my $i=0; $i < @devArr; $i++){ my $tadodevices = $devArr[$i]->{'devices'}; $message .= "ZoneID:" . ($i+1); 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)" if $debug; TadoAPI_GetZoneInfo($hash); my $zonecount = TadoAPI_GetZoneCount($hash); $message = "You have $zonecount Zones.\n"; for (my $i=1; $i <= $zonecount; $i++) { $message .= "Zone ID:$i: " . TadoAPI_GetZoneNameById($hash, $i) . "\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_callback($$$){ my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; $param = 0 unless defined $param; if($param->{code} == 401 || $param->{code} == 400){ $apiStatus = 1; $hash->{STATE}="reachable"; Log3 $name, 3, "TadoAPI $name" . ": " . "API is reachable. Callback Status: " . $param->{code} if $debug; }else{ $apiStatus = 0; $hash->{STATE}="error"; Log3 $name, 3, "TadoAPI $name" . ": " . "API error: apiStatus $apiStatus ($err)" if $debug; } return undef; } sub TadoAPI_Status(@){ 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 HttpUtils_NonblockingGet($param); return undef; } sub TadoAPI_Connect(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $tokenFileName = $tokenFile."_".$name; my $tokenLifeTime = $hash->{TOKEN_LIFETIME}; $tokenLifeTime = 0 if(!defined $tokenLifeTime || $tokenLifeTime eq ''); #debug $debug = $attr{$name}{debug}; #load existing token, or try to refresh old one Log3 $name, 3, "TadoAPI $name" . ": " . "Loading Token Data from file: $tokenFileName." if $debug; TadoAPI_Status($hash); if($apiStatus){ eval { open(TOKENFILE, '<', $tokenFileName) or die("ERROR: $!"); $TokenData = decode_json()}; if($@ || $tokenLifeTime < gettimeofday()){ Log3 $name, 3, "TadoAPI $name" . ": " . "Error while loading: $@" if $debug && $@; Log3 $name, 3, "TadoAPI $name" . ": " . "Token expired, requesting new one" if $debug && $tokenLifeTime < gettimeofday(); TadoAPI_requestNewToken($hash); }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "Token expires at " . localtime($tokenLifeTime) if $debug; # if token is about to expire, refresh him if (($tokenLifeTime-60) < gettimeofday()){ Log3 $name, 3, "TadoAPI $name" . ": " . "Token will expire soon, refreshing" if $debug; TadoAPI_refreshToken($hash); } } close(TOKENFILE); } return undef; } sub TadoAPI_requestNewToken(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $username = $hash->{TADO_USER}; my $password = TadoAPI_readPassword($name); my $tokenFileName = $tokenFile."_".$name; Log3 $name, 3, "TadoAPI $name" . ": " . "Requesting new Token (TadoAPI_requestNewToken)" if $debug; $data = { client_id => $client_id, client_secret => $client_secret, username => $username, password => $password, scope => $scope, grant_type=>'password' }; my $req = POST($AuthURL,$data); my $ua = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); my $res = $ua->request($req); if($res->is_success){ $TokenData = decode_json($res->content); #write token data in file open(TOKENFILE,">$tokenFileName") or die("ERROR: $!"); print TOKENFILE $res->content."\n"; close(TOKENFILE); # token time management $hash->{TOKEN_LIFETIME} = gettimeofday() + $TokenData->{'expires_in'}; Log3 $name, 3, "TadoAPI $name" . ": " . "Retrived new authentication token successfully. Valid until " . localtime($hash->{TOKEN_LIFETIME}) if $debug; #return to apistatus $hash->{STATE}="reachable"; return 1; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "Error in token retrival [Authentication Error]"; print $Response->status_line."\n" if $debug; #apiStatus down $hash->{STATE}="error"; return 0; } return undef; } sub TadoAPI_refreshToken(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $tokenFileName = $tokenFile."_".$name; Log3 $name, 3, "TadoAPI $name" . ": " . "calling TadoAPI_refreshToken()" if $debug; $data = { client_id => $client_id, client_secret => $client_secret, scope => $scope, grant_type=>'refresh_token', refresh_token => $TokenData->{'refresh_token'} }; $Request = POST($AuthURL,$data); $UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 3,); $Response = $UserAgent->request($Request); $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8"); if($Response->is_success){ Log3 $name, 3, "TadoAPI $name" . ": " . "refreshed authentication token successfully." if $debug; $TokenData = decode_json($Response->content); Log3 $name, 3, "TadoAPI $name" . ": " . "writting refreshed Token Data to file $tokenFileName" if $debug; open(TOKENFILE,">$tokenFileName") or die("ERROR: $!"); print TOKENFILE $Response->content."\n"; close(TOKENFILE); #Log3 $name, 3, "TadoAPI $name" . ": " . "refreshed Token successful written" if $debug; return 1; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "token expired, requesting new token"; Log3 $name, 3, "TadoAPI $name" . ": " . $Response->status_line if $debug; Log3 $name, 3, "TadoAPI $name" . ": " . "response code: " . $Response->code if $debug; #if token is expired and connection up, request new one if($Response->code == 500){ Log3 $name, 3, "TadoAPI $name" . ": " . "error: not connected - response: 500" if $debug; $apiStatus = 0; $hash->{STATE}="error"; } else{ TadoAPI_requestNewToken($hash); } return 0; } return undef; } sub TadoAPI_Update(@){ my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 3, "TadoAPI $name" . ": " . "TadoAPI_Update called" if $debug; # timer loop # my $nextTimer = "none"; # if api online, try again in 5 minutes if ( $apiStatus ) { $nextTimer = gettimeofday() + 300; } RemoveInternalTimer($hash); InternalTimer( $nextTimer, "TadoAPI_Update", $hash, 0 ); # update subs TadoAPI_UpdateFn($hash); return undef; } ######################## tado methods ######################## ############################################################## sub TadoAPI_SetZoneOverlay(@){ my ($hash, $zoneID, $setting) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL = $QueryURL . qq{/$homeID/zones/$zoneID/overlay}; Log3 $name, 3, "TadoAPI $name" . ": SetOverlay for Zone $zoneID (Setting: " . $setting . ") - " . "query-URL: $URL" if $debug; if($apiStatus == 1){ TadoAPI_Connect($hash); my $zoneName = TadoAPI_GetZoneNameById($hash, $zoneID); $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); $UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $UserAgent->default_headers($header); my $req = undef; my $res = undef; my $message = ""; if ($setting eq "off"){ # Delete overlay $req = HTTP::Request->new( 'DELETE', $URL ); $req->content_type('application/json'); Log3 $name, 3, "TadoAPI $name" . ": " . "Deleting Overlay (off)" if $debug; $message = "no overlay"; }elsif($setting > 10){ $req = HTTP::Request->new( 'PUT', $URL ); $req->content_type('application/json'); my $message = { setting => { type => "HEATING", power => "ON", temperature => { celsius => $setting }, }, termination => { type => "MANUAL" }, }; my $myjson = encode_json($message); #print Dumper($myjson); $req->content($myjson); $message = "MANUAL"; } $UserAgent->default_headers($header); $res = $UserAgent->request($req); if($res->is_success){ print "\n Retriving State:\n" if $debug; Log3 $name, 3, "TadoAPI $name" . ": " . "Retriving state:\n" . $res->content if $debug; Log3 $name, 3, "TadoAPI $name" . ": " . "Set Overlay for Zone $zoneID to: $setting"; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "Error in SetOverlay()"; Log3 $name, 3, "TadoAPI $name" . ": " . "Status (setOverlay): " . $res->status_line if $debug; } # finaly update readings TadoAPI_GetZoneReadingsById($hash, $zoneID); } } sub TadoAPI_SetAllOverlays(@){ my ($hash, $setting) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; if($apiStatus == 1){ TadoAPI_Connect($hash); my $zonecount = TadoAPI_RequestZones($hash); my $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); my $ua = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $ua->default_headers($header); my $req = undef; my $res = undef; my $message = ""; for (my $i=1; $i <= $zonecount; $i++) { my $URL = $QueryURL . qq{/$homeID/zones/$i/overlay}; if ($setting eq "off"){ # Delete overlay $req = HTTP::Request->new( 'DELETE', $URL ); $req->content_type('application/json'); Log3 $name, 3, "TadoAPI $name" . ": " . "Deleting Overlay (off)" if $debug; $message = "no overlay"; }elsif($setting > 10){ $req = HTTP::Request->new( 'PUT', $URL ); $req->content_type('application/json'); $message = { setting => { type => "HEATING", power => "ON", temperature => { celsius => $setting }, }, termination => { type => "MANUAL" }, }; my $myjson = encode_json($message); #print Dumper($myjson); $req->content($myjson); $message = "MANUAL"; } $ua->default_headers($header); $res = $ua->request($req); if($res->is_success){ print "\n Retriving State:\n" if $debug; Log3 $name, 3, "TadoAPI $name" . ": " . "Retriving state:\n" . $res->content if $debug; Log3 $name, 3, "TadoAPI $name" . ": " . "Set Overlay for Zone $i to: $setting"; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "Error in SetOverlay()"; Log3 $name, 3, "TadoAPI $name" . ": " . "Status (setOverlay): " . $res->status_line if $debug; } } # finaly update readings #TadoAPI_UpdateFn($hash); } } sub TadoAPI_UpdateFn(@){ my ($hash) = @_; my $name = $hash->{NAME}; TadoAPI_Connect($hash); # zone specific updates if($apiStatus == 1){ my @zones = TadoAPI_RequestZones($hash); my @devices = TadoAPI_RequestTadoDevices($hash); my $zonecount = @zones; for (my $i=0; $i < $zonecount; $i++) { print "HomeMode_" . encode("UTF-8", $devices[$i]->{'name'}) . $zones[$i]->{'tadoMode'} . "\n" if $debug; my $overlay = $zones[$i]->{'overlayType'}; if (!defined $overlay) {$overlay = "no overlay"}; readingsBeginUpdate($hash); readingsBulkUpdate($hash, "HomeMode_" . TadoAPI_ReplaceUmlaute($devices[$i]->{'name'}), $zones[$i]->{'tadoMode'}); readingsBulkUpdate($hash, "Temperatur_" . TadoAPI_ReplaceUmlaute($devices[$i]->{'name'}), $zones[$i]->{'sensorDataPoints'}->{'insideTemperature'}->{'celsius'}); readingsBulkUpdate($hash, "Luftfeuchtigkeit_" . TadoAPI_ReplaceUmlaute($devices[$i]->{'name'}), $zones[$i]->{'sensorDataPoints'}->{'humidity'}->{'percentage'}); readingsBulkUpdate($hash, "Heizleistung_" . TadoAPI_ReplaceUmlaute($devices[$i]->{'name'}), $zones[$i]->{'activityDataPoints'}->{'heatingPower'}->{'percentage'}); readingsBulkUpdate($hash, "OverlayType_" . TadoAPI_ReplaceUmlaute($devices[$i]->{'name'}), $overlay); readingsBulkUpdate($hash, "DesiredTemp_" . TadoAPI_ReplaceUmlaute($devices[$i]->{'name'}), $zones[$i]->{'setting'}->{'temperature'}->{'celsius'}); readingsEndUpdate( $hash, 1 ); } # mobile devices my @mobDev = TadoAPI_RequestMobileDevices($hash); my $mobDevCount = @mobDev; for (my $i=0; $i < $mobDevCount; $i++) { print "Geolocation_" . encode("UTF-8", $mobDev[$i]->{'name'}) . $mobDev[$i]->{'settings'}->{'geoTrackingEnabled'} . "\n" if $debug; readingsBeginUpdate($hash); readingsBulkUpdate($hash, "Geolocation_" . encode("UTF-8", $mobDev[$i]->{'id'}), $mobDev[$i]->{'settings'}->{'geoTrackingEnabled'}); readingsEndUpdate( $hash, 1 ); } # tado devices for (my $i=0; $i < $zonecount; $i++) { my $deviceList = $devices[$i]->{'devices'}; foreach my $dev ( @$deviceList ){ print "Battery_" . encode("UTF-8", $dev->{'serialNo'}) . $dev->{'batteryState'} . "\n" if $debug; readingsBeginUpdate($hash); readingsBulkUpdate($hash, "Battery_" . $dev->{'serialNo'}, $dev->{'batteryState'}); readingsEndUpdate( $hash, 1 ); } } readingsBeginUpdate($hash); readingsBulkUpdate($hash, "ActiveZones", $zonecount); readingsEndUpdate( $hash, 1 ); } return undef; } sub TadoAPI_GetGeoById(@){ my ($hash, $mobileID) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL.qq{/$homeID/mobileDevices/$mobileID/settings}; Log3 $name, 3, "TadoAPI $name" . ": " . "query-URL: $URL" if $debug; TadoAPI_Connect($hash); if($apiStatus == 1){ my $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); my $ua = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $ua->default_headers($header); my $req = GET($URL); my $res = $ua->request($req); if($res->is_success){ my $ResponseData = decode_json($res->content); my $setting = $ResponseData->{'geoTrackingEnabled'}; Log3 $name, 3, "TadoAPI $name" . ": " . "Actual geo setting for $mobileID is: $setting" if $debug; readingsBeginUpdate($hash); readingsBulkUpdate($hash, "Geolocation_" . $mobileID, $setting); readingsEndUpdate( $hash, 1 ); return $setting; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "[Authentication Error in TadoAPI_GetGeoById( $mobileID )]"; Log3 $name, 3, "TadoAPI $name" . ": " . "Error: ". $res->status_line if $debug; } } } sub TadoAPI_GetZoneInfo(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL.qq{/$homeID/zones/1/state}; if($apiStatus == 1){ TadoAPI_Connect($hash); $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); $UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $UserAgent->default_headers($header); # HomeInfo $URL = qq{https://my.tado.com/api/v2/me}; my $req = GET($URL); my $res = $UserAgent->request($req); if($res->is_success){ Log3 $name, 3, "TadoAPI $name" . ": " . "Home Info:\n" . $res->content . "\n"; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneInfo: [Authentication Error]". $res->status_line; } # TadoDevicesInfo $URL = $QueryURL.qq{/$homeID/zones}; $req = GET($URL); $res = $UserAgent->request($req); if($res->is_success){ Log3 $name, 3, "TadoAPI $name" . ": " . "Tado Devices:\n" . $res->content . "\n"; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneInfo: [Authentication Error]". $res->status_line; } # Mobileinfo $URL = $QueryURL . qq{/$homeID/mobileDevices}; $req = GET($URL); $res = $UserAgent->request($req); if($res->is_success){ Log3 $name, 3, "TadoAPI $name" . ": " . "Mobile Devices:\n" . $res->content . "\n"; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneInfo: [Authentication Error]". $res->status_line; } my $mobileID = $attr{$name}{mobileID}; $URL=$QueryURL.qq{/$homeID/mobileDevices/$mobileID/settings}; $req = GET($URL); $res = $UserAgent->request($req); if($res->is_success){ Log3 $name, 3, "TadoAPI $name" . ": " . "Mobile Device $mobileID :\n" . $res->content . "\n"; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneInfo: [Authentication Error]". $res->status_line; } # zones for (my $i=1; $i <= TadoAPI_GetZoneCount($hash); $i++) { $URL=$QueryURL.qq{/$homeID/zones/$i/state}; $req = GET($URL); $res = $UserAgent->request($req); if($res->is_success){ print "\n"; Log3 $name, 3, "TadoAPI $name" . ": " . "ZoneID $i (" . TadoAPI_GetZoneNameById($hash, $i) . ") Status:\n" . $res->content; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneInfo [Authentication Error]". $res->status_line; } } } return undef; } sub TadoAPI_SetGeo(@){ my ($hash, $geo) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $mobileID = $attr{$name}{mobileID}; my $URL=$QueryURL.qq{/$homeID/mobileDevices/$mobileID/settings}; Log3 $name, 3, "TadoAPI $name" . ": " . "homeID: $homeID" if $debug; Log3 $name, 3, "TadoAPI $name" . ": " . "geo: $geo" if $debug; Log3 $name, 3, "TadoAPI $name" . ": " . "query-URL: $URL" if $debug; if($apiStatus == 1){ TadoAPI_Connect($hash); $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); $UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $UserAgent->default_headers($header); $Request = HTTP::Request->new('PUT',$URL); $Request->content_type('application/json'); if($geo){ $Request->content('{"geoTrackingEnabled":"true"}'); }else{ $Request->content('{"geoTrackingEnabled":"false"}'); } $Response = $UserAgent->request($Request); if($Response->is_success){ print "\n Retriving State:\n" if $debug; Log3 $name, 3, "TadoAPI $name" . ": " . "Retriving state:\n" . $Response->content if $debug; Log3 $name, 3, "TadoAPI $name" . ": " . "Set geo setting for $mobileID to: $geo"; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "Error in setGeo()"; Log3 $name, 3, "TadoAPI $name" . ": " . "Status: " . $Response->status_line if $debug; } } } sub TadoAPI_SetGeoById(@){ my ($hash, $mobID, $geo) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL.qq{/$homeID/mobileDevices/$mobID/settings}; TadoAPI_Connect($hash); if($apiStatus == 1){ my $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); my $ua = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $ua->default_headers($header); my $req = HTTP::Request->new('PUT',$URL); $req->content_type('application/json'); if($geo){ $req->content('{"geoTrackingEnabled":"true"}'); }else{ $req->content('{"geoTrackingEnabled":"false"}'); } my $res = $ua->request($req); if($res->is_success){ print "\n Retriving State:\n" if $debug; Log3 $name, 3, "TadoAPI $name" . ": " . "Retriving state:\n" . $res->content if $debug; Log3 $name, 3, "TadoAPI $name" . ": " . "Set geo setting for $mobID to: $geo"; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "Error in setGeo()"; Log3 $name, 3, "TadoAPI $name" . ": " . "Status: " . $res->status_line if $debug; } } } ####################################################################################################### # API Subs ########### sub TadoAPI_RequestHome(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $URL=qq{https://my.tado.com/api/v2/me}; if($apiStatus == 1){ TadoAPI_Connect($hash); my $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); my $ua = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $ua->default_headers($header); my $req = GET($URL); my $res = $ua->request($req); if($res->is_success){ Log3 $name, 3, "TadoAPI $name" . ": " . "TadoAPI_RequestHome:\n" . $res->content . "\n" if $debug; my $data = $res->content; # validate response from api eval { decode_json($data) }; if ($@) { Log3 $name, 3, "TadoAPI $name" . ": " . "TadoAPI_RequestHome: decode_json failed, invalid json. error:$@\n"; }else{ # returns json return $data; } }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "TadoAPI_RequestHome: [Authentication Error]". $res->status_line; } } return undef; } sub TadoAPI_RequestTadoDevices(@) { # returns array with zonenames and zone devices my ($hash) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL.qq{/$homeID/zones}; if($apiStatus == 1){ TadoAPI_Connect($hash); my $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); my $ua = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $ua->default_headers($header); my $req = GET($URL); my $res = $ua->request($req); if($res->is_success){ Log3 $name, 3, "TadoAPI $name" . ": " . "RequestTadoDevices:\n" . $res->content . "\n" if $debug; my $data = $res->content; # validate response from api my $decoded_data = eval { decode_json($data) }; if ($@) { Log3 $name, 3, "TadoAPI $name" . ": " . "RequestTadoDevices: decode_json failed, invalid json. error:$@\n"; }else{ my @devices = (); foreach my $dev (@$decoded_data){ push @devices, $dev; } return @devices; } }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "RequestTadoDevices: [Authentication Error]". $res->status_line; } } return undef; } sub TadoAPI_RequestZones(@) { # returns array with state of all zones my ($hash) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; if($apiStatus == 1){ TadoAPI_Connect($hash); my $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); my $ua = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $ua->default_headers($header); my @zones = (); for (my $i=1; $i <= TadoAPI_GetZoneCount($hash); $i++) { my $URL=$QueryURL.qq{/$homeID/zones/$i/state}; my $req = GET($URL); my $res = $UserAgent->request($req); if($res->is_success){ Log3 $name, 3, "TadoAPI $name" . ": " . "RequestZones: Zone $i Status:\n" . $res->content if $debug; my $data = $res->content; # validate response from api my $decoded_data = eval { decode_json($data) }; if ($@) { Log3 $name, 3, "TadoAPI $name" . ": " . "RequestZones: Zone $i decode_json failed, invalid json. error:$@\n"; }else{ # returns a json object with zone i push @zones, $decoded_data; } }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "RequestZones: [Authentication Error]". $res->status_line; } } return @zones; } return undef; } sub TadoAPI_RequestMobileDevices(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL . qq{/$homeID/mobileDevices}; if($apiStatus == 1){ TadoAPI_Connect($hash); my @devices = (); my $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); my $ua = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $ua->default_headers($header); my $req = GET($URL); my $res = $ua->request($req); if($res->is_success){ my $data = $res->content; # validate response from api my $decoded_data = eval { decode_json($data) }; if ($@) { Log3 $name, 3, "TadoAPI $name" . ": " . "RequestMobileDevice: decode_json failed, invalid json. error:$@\n"; }else{ foreach my $item( @$decoded_data ) { push @devices, $item; } } }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "RequestMobileDevices: [Authentication Error]". $res->status_line; } return @devices; } return undef; } ###################################### ############ 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; } sub TadoAPI_GetZoneCount(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL.qq{/$homeID/zones}; my $zonecount = 0; $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); $UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $UserAgent->default_headers($header); $Request = GET($URL); $Response = $UserAgent->request($Request); if($Response->is_success){ my $ResponseData = decode_json($Response->content); foreach my $item( @$ResponseData ) { $zonecount++; } return $zonecount; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneCount: [Authentication Error]". $Response->status_line; } return undef; } sub TadoAPI_GetZoneNameById(@) { my ($hash, $zoneID) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL.qq{/$homeID/zones}; $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); $UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $UserAgent->default_headers($header); $Request = GET($URL); $Response = $UserAgent->request($Request); if($Response->is_success){ my $ResponseData = decode_json($Response->content); my $zoneName = TadoAPI_ReplaceUmlaute(@$ResponseData[$zoneID - 1]->{'name'}); $zoneName = encode("UTF-8", $zoneName); return $zoneName; }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneNameById: [Authentication Error]". $Response->status_line; } return undef; } sub TadoAPI_GetZoneTemperatureById(@){ 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; $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); $UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $UserAgent->default_headers($header); $Request = GET($URL); $Response = $UserAgent->request($Request); if($Response->is_success){ my $ResponseData = decode_json($Response->content); $temperature = $ResponseData->{'sensorDataPoints'}->{'insideTemperature'}->{'celsius'}; $humidity = $ResponseData->{'sensorDataPoints'}->{'humidity'}->{'percentage'}; Log3 $name, 3, "TadoAPI $name" . ": " . "Temperature: $temperature Humidity: $humidity\n" if $debug; return ($temperature, $humidity); }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneTemperatureById: [Authentication Error]". $Response->status_line; } 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 $header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}"); my $ua = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,); $ua->default_headers($header); my $req = GET($URL); my $res = $ua->request($req); if($res->is_success){ my $ResponseData = decode_json($res->content); $temperature = $ResponseData->{'sensorDataPoints'}->{'insideTemperature'}->{'celsius'}; $humidity = $ResponseData->{'sensorDataPoints'}->{'humidity'}->{'percentage'}; $desiredTemp = $ResponseData->{'setting'}->{'temperature'}->{'celsius'}; $currentHeatingPower = $ResponseData->{'activityDataPoints'}->{'heatingPower'}->{'percentage'}; $overlay = $ResponseData->{'overlayType'}; if (!defined $overlay) {$overlay = "no overlay"}; my $zoneName = TadoAPI_GetZoneNameById($hash, $zoneID); 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 ); return ($temperature, $humidity, $desiredTemp, $currentHeatingPower, $overlay ); }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "GetZoneReadingsById: [Authentication Error]". $Response->status_line; } 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; # Beginn der Commandref =pod =item [device] =item summary integration of the Tado API =item summary_DE Anbindung der Tado Heizungssteuerung über API =begin html - =end html =begin html_DE - =end html # Ende der Commandref =cut