#!/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" => "", "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; my $tokenFileName = $tokenFile."_".$name; 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; $hash->{TOKEN_FILE} = $tokenFileName; 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 (ZoneID, Setting)" if(@a < 4); if( $a[3] eq "off" ) { TadoAPI_SetZoneOverlayById($hash, $value, "off"); } elsif ($a[3] > 1) { TadoAPI_SetZoneOverlayById($hash, $value, $a[3]); } 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 eq "off" ) { TadoAPI_SetAllOverlays($hash, "off"); } elsif ($value > 1) { TadoAPI_SetAllOverlays($hash, $value); } 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_LoadToken($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 $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_ZoneRequest($hash); Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; last; }; $cmd eq "getZoneDevices" and do { Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)" if $debug; 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_LoadToken(@){ 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}; TadoAPI_Status($hash); my $Token = undef; if($apiStatus){ eval { open(TOKENFILE, '<', $tokenFileName) or die("ERROR: $!"); $Token = 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_NewTokenRequest($hash); }else{ Log3 $name, 3, "TadoAPI $name" . ": " . "Token expires at " . localtime($tokenLifeTime) if $debug; # if token is about to expire, refresh him if (($tokenLifeTime-15) < gettimeofday()){ Log3 $name, 3, "TadoAPI $name" . ": " . "Token will expire soon, refreshing" if $debug; TadoAPI_TokenRefresh($hash); } } close(TOKENFILE); return $Token; } return undef; } sub TadoAPI_NewTokenRequest(@) { my ($hash) = @_; my $name = $hash->{NAME}; my $username = $hash->{TADO_USER}; my $password = TadoAPI_readPassword($name); Log3 $name, 3, "TadoAPI $name" . ": " . "calling TadoAPI_NewTokenRequest()" if $debug; $data = { client_id => $client_id, client_secret => $client_secret, username => $username, password => $password, scope => $scope, grant_type=>'password' }; my $request = { url => $AuthURL, method => 'POST', timeout => 5, callback => \&Tado_StoreTokenCallback, refreshToken => 0, hash => $hash, data => $data }; HttpUtils_NonblockingGet($request); } sub TadoAPI_TokenRefresh(@) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 $name, 3, "TadoAPI $name" . ": " . "calling TokenRefresh()" if $debug; # 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 $request = { url => $AuthURL, method => 'POST', timeout => 5, callback => \&Tado_StoreTokenCallback, hash => $hash, refreshToken => 1, data => $data }; HttpUtils_NonblockingGet($request); } sub Tado_StoreTokenCallback($) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; my $tokenFileName = $hash->{TOKEN_FILE}; if($err ne "") { Log3 $name, 3, "TadoAPI $name" . ": " . "Error in token retrival while requesting ".$param->{url}." - $err"; $hash->{STATE}="error"; } elsif($data ne "") { Log3 $name, 3, "url ".$param->{url}." returned: $data" if $debug; my $json_data =""; eval{ $json_data = decode_json($data); }; # todo error handling if ($param->{refreshToken} == 1 && exists($json_data->{error})) { # do if refresh sub was not successful (maybe token expired) Log3 $name, 3, "TadoAPI $name" . ": " . "Token expired, requesting new one"; TadoAPI_NewTokenRequest($hash); } else { eval{ $TokenData = decode_json($data); }; # todo error handling #write token data in file open(TOKENFILE,">$tokenFileName") or die("ERROR: $!"); print TOKENFILE $data . "\n"; close(TOKENFILE); # token lifetime management $hash->{TOKEN_LIFETIME} = gettimeofday() + $TokenData->{'expires_in'}; $hash->{TOKEN_LIFETIME_HR} = localtime($hash->{TOKEN_LIFETIME}); Log3 $name, 3, "TadoAPI $name" . ": " . "Retrived new authentication token successfully. Valid until " . localtime($hash->{TOKEN_LIFETIME}); #if $debug; $hash->{STATE}="reachable"; } } } 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_SetZoneOverlayById(@){ my ($hash, $zoneID, $setting) = @_; 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 =""; Log3 $name, 3, "TadoAPI $name" . ": SetOverlay for Zone $zoneID (Setting: " . $setting . ") - " . "query-URL: $URL" if $debug; if ($setting eq "off"){ $method = "DELETE"; Log3 $name, 3, "TadoAPI $name" . ": " . "Deleting Overlay for Zone $zoneID"; }elsif($setting > 10){ $method = "PUT"; $myjson = { setting => { type => "HEATING", power => "ON", temperature => { celsius => $setting }, }, termination => { type => "MANUAL" }, }; $myjson = encode_json($myjson); } my $request = { url => $URL, header => { "Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}" }, method => $method, timeout => 3, callback => \&Tado_UpdateZoneOverlayCallback, hash => $hash, setting => $setting, zoneID => $zoneID, data => $myjson }; #Log3 $name, 3, 'NonBlocking Request: ' . Dumper($request); HttpUtils_NonblockingGet($request); } } 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, 3, "url ".$param->{url}." returned: $data" if $debug; Log3 $name, 3, "TadoAPI $name" . ": " . "Set Overlay for Zone $zoneID to: $setting"; } # 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){ my $zonecount = TadoAPI_ZoneRequest($hash); my $message = ""; my $method = ""; my $myjson =""; for (my $i=1; $i <= $zonecount; $i++) { my $URL = $QueryURL . qq{/$homeID/zones/$i/overlay}; if ($setting eq "off"){ $method = "DELETE"; Log3 $name, 3, "TadoAPI $name" . ": " . "Deleting Overlay for Zone $i"; }elsif($setting > 10){ $method = "PUT"; $myjson = { setting => { type => "HEATING", power => "ON", temperature => { celsius => $setting }, }, termination => { type => "MANUAL" }, }; $myjson = encode_json($myjson); } my $request = { url => $URL, header => { "Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}" }, method => $method, timeout => 3, callback => \&Tado_UpdateZoneOverlayCallback, hash => $hash, setting => $setting, zoneID => $i, data => $myjson }; HttpUtils_NonblockingGet($request); } # finaly update readings #TadoAPI_UpdateFn($hash); } } sub TadoAPI_UpdateFn(@){ my ($hash) = @_; my $name = $hash->{NAME}; TadoAPI_Status($hash); # zone specific updates if($apiStatus == 1){ my @zones = TadoAPI_ZoneRequest($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}; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ Log3 $name, 3, "TadoAPI $name" . ": " . "query-URL: $URL" if $debug; 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}; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ $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; } # todo remove static mobile id 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_SetGeoById(@){ my ($hash, $mobID, $geo) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my $URL=$QueryURL.qq{/$homeID/mobileDevices/$mobID/settings}; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ 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 => 3,); $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}; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ 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 => 3,); $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}; my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)){ 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 => 3,); $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_ZoneRequest(@) { # returns array with state of all zones my ($hash) = @_; my $name = $hash->{NAME}; my $homeID = $attr{$name}{homeID}; my @zones = (); my $TokenData = TadoAPI_LoadToken($hash); if(defined($TokenData)) { for (my $i=1; $i <= TadoAPI_GetZoneCount($hash); $i++) { my $URL=$QueryURL.qq{/$homeID/zones/$i/state}; 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, zoneID => $i }; Log3 $name, 3, 'Blocking GET: ' . Dumper($param) if $debug; my ($err, $data) = HttpUtils_BlockingGet($param); if($err ne "") { Log3 $name, 3, "TadoAPI $name" . ": " . "ZoneRequest: Error while requesting ".$param->{url}." - $err"; } elsif($data ne "") { Log3 $name, 3, "url ".$param->{url}." returned: $data" if $debug; # An dieser Stelle die Antwort parsen / verarbeiten mit $data my $decoded_data = eval { decode_json($data) }; if ($@){ Log3 $name, 3, "TadoAPI $name" . ": " . "RequestZones: Zone $i decode_json failed, invalid json. error:$@\n"; }else{ # pushes zone i to array push @zones, $decoded_data; } } } return @zones; } } sub TadoAPI_RequestMobileDevices(@) { 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 @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 => 3,); $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; my $TokenData = TadoAPI_LoadToken($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 => 3,); $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}; my $TokenData = TadoAPI_LoadToken($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 => 3,); $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; my $TokenData = TadoAPI_LoadToken($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 => 3,); $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 $TokenData = TadoAPI_LoadToken($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 => 3,); $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