From 5094dda70b74d043316ffb649c9d2f68ffa6154b Mon Sep 17 00:00:00 2001 From: Philipp Wo Date: Sat, 23 Nov 2019 11:49:06 +0100 Subject: [PATCH] changed encoding of pm file; commandref created --- 98_TadoAPI.pm | 2837 +++++++++++++++++++++++++------------------------ 1 file changed, 1438 insertions(+), 1399 deletions(-) diff --git a/98_TadoAPI.pm b/98_TadoAPI.pm index af586ef..dab0287 100644 --- a/98_TadoAPI.pm +++ b/98_TadoAPI.pm @@ -1,1399 +1,1438 @@ -#!/usr/bin/env perl -#=============================================================================== -# -# 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: 1.0 -# CREATED: 04/12/2019 07:55:44 PM -# REVISION: 11/21/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 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 $header = {}; -my $data = {}; -my $TokenData = {}; - -# helpers -my $apiStatus = 1; - -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 " . - $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_LoadToken($hash); - - # start the status update timer - RemoveInternalTimer($hash); - InternalTimer( gettimeofday() + 10, "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; - } - } - } - 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 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) - Setting: off=delete overlay; 0=heating power off; 1<=desired temperature (overlay)" if(@a < 4); - - if( $a[3] eq "off" ) { - TadoAPI_SetZoneOverlayById($hash, $value, "off"); - } elsif ($a[3] >= 0) { - TadoAPI_SetZoneOverlayById($hash, $value, $a[3]); - } - Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; - - } elsif( $cmd eq 'setAllOverlays' ) { - Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)"; - return "Need at least one parameter (Setting) - Setting: off=delete overlay; 0=heating power off; 1<=desired temperature (overlay)" if(@a < 3); - if( $value eq "off" ) { - TadoAPI_SetAllOverlays($hash, "off"); - } elsif ($value >= 0) { - TadoAPI_SetAllOverlays($hash, $value); - } - Log3 $name, 3, "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 'getTemperature' ) { - Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)"; - 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"; - - } 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); - } - - 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 ){ - print "\n"; - $message .= $item->{'name'} . ": " . $item->{'id'} . "\n"; - }; - Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; - last; - }; - - $cmd eq "getXTest" and do { - Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)"; - $message = "Name: " . TadoAPI_GetZoneNameById($hash, $value); - 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_OnlineStatus(@){ - 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 ''); - my $Token = undef; - - TadoAPI_OnlineStatus($hash); - - 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"; - Log3 $name, 5, "TadoAPI $name" . ": " . "Token is expired, requesting new one" if $tokenLifeTime < gettimeofday(); - $Token = TadoAPI_NewTokenRequest($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; - } -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); - 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); - 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"; - - # 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, 5, "TadoAPI $name" . ": SetOverlay for Zone $zoneID (Setting: " . $setting . ") - " . "query-URL: $URL"; - - if ($setting eq "off"){ - $method = "DELETE"; - Log3 $name, 3, "TadoAPI $name" . ": " . "Deleting Overlay for Zone $zoneID"; - }elsif($setting == 0){ - $method = "PUT"; - $myjson = { - setting => { - type => "HEATING", - power => "OFF", - }, - termination => { - type => "MANUAL" - }, - }; - $myjson = encode_json($myjson); - }elsif($setting > 0){ - $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 => 5, - callback => \&Tado_UpdateZoneOverlayCallback, - hash => $hash, - setting => $setting, - zoneID => $zoneID, - data => $myjson - }; - - #Log3 $name, 5, 'NonBlocking Request: ' . Dumper($request); - HttpUtils_NonblockingGet($request); - - } -} - - - -sub TadoAPI_SetAllOverlays(@){ - my ($hash, $setting) = @_; - my $name = $hash->{NAME}; - my $homeID = $attr{$name}{homeID}; - my $TokenData = TadoAPI_LoadToken($hash); - - if(defined($TokenData)){ - my $message = ""; - my $method = ""; - my $myjson =""; - - my @devArr = TadoAPI_GetTadoDevices($hash); - - for (my $i=0; $i < @devArr; $i++) { - my $zoneid = $devArr[$i]->{'id'}; - TadoAPI_SetZoneOverlayById($hash, $zoneid, $setting); - } - } -} - -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 - }; - - 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 => 5, - incrementalTimout => 1, - callback => \&TadoAPI_UpdateMobileReadingsCallback, - hash => $hash - }; - 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, 5, "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{ - foreach my $zone ( @$decoded_data ){ - my $zoneid = $zone->{'id'}; - Log3 $name, 5, "TadoAPI $name" . ": " . "Set Reading Update for Zone $zoneid "; - my ($temperature, $humidity, $desiredTemp, $currentHeatingPower, $overlay ) = TadoAPI_GetZoneReadingsById($hash, $zoneid); - my $zoneName = TadoAPI_GetZoneNameById($hash, $zoneid); - - if(defined($zoneName)){ - 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 ); - } - # iterate through all devices in zone - my $devices = $zone->{'devices'}; - foreach my $device ( @$devices ){ - readingsBeginUpdate($hash); - readingsBulkUpdate($hash, "Battery_" . $device->{'serialNo'}, $device->{'batteryState'}); - readingsEndUpdate( $hash, 1 ); - $hash->{LastRequest}="OK"; - } - - } - } - my $zonecount = TadoAPI_GetZoneCount($hash); - readingsBeginUpdate($hash); - readingsBulkUpdate($hash, "ActiveZones", $zonecount); - readingsEndUpdate( $hash, 1 ); - } -} - -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 - TadoAPI_GetZoneReadingsById($hash, $zoneID); - -} - -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_UpdateTadoDeviceInformationCallback($){ - my ($param, $err, $data) = @_; - my $hash = $param->{hash}; - my $name = $hash->{NAME}; - - if($err ne "") - { - Log3 $name, 3, "Error in UpdateTadoDeviceInformationCallback 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" . ": " . "UpdateTadoDeviceInformationCallback: decode_json failed, invalid json. error:$@\n" if $@; - Log3 $name, 3, "TadoAPI $name" . ": " . "UpdateTadoDeviceInformationCallback: Error in decoded data, Code: " . $decoded_data->{'errors'}->[0]->{'code'}; - $hash->{LastRequest}="error"; - }else{ - foreach my $zone ( @$decoded_data ){ - my $devices = $zone->{'devices'}; - foreach my $device ( @$devices ){ - readingsBeginUpdate($hash); - readingsBulkUpdate($hash, "Battery_" . $device->{'serialNo'}, $device->{'batteryState'}); - readingsEndUpdate( $hash, 1 ); - $hash->{LastRequest}="OK"; - } - } - } - } -} - -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 ){ - readingsBeginUpdate($hash); - readingsBulkUpdate($hash, "Geolocation_" . $item->{'id'}, $item->{'settings'}->{'geoTrackingEnabled'}); - readingsEndUpdate( $hash, 1 ); - $hash->{LastRequest}="OK"; - } - } - } -} - -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 - }; - - 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 - }; - - 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 - }; - - 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 - }; - - HttpUtils_NonblockingGet($request); - } -} - -####################################################################################################### -# API Requests -############### - - - - -###################################### -############ 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_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); - 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(@){ - 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)){ - 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); - my ($err, $data) = HttpUtils_BlockingGet($param); - - if($err ne "") - { - Log3 $name, 3, "TadoAPI $name" . ": " . "TadoAPI_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" . ": " . "TadoAPI_GetGeoById: decode_json failed, invalid json. error:$@\n"; - }else{ - my $setting = $decoded_data->{'geoTrackingEnabled'}; - readingsBeginUpdate($hash); - readingsBulkUpdate($hash, "Geolocation_" . $mobileID, $setting); - readingsEndUpdate( $hash, 1 ); - return $setting; - } - } - } - return undef; -} - -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); - 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'){ - 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); - 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 @devArr = TadoAPI_GetTadoDevices($hash); - my $zoneName = undef; - - foreach my $zone ( @devArr ){ - if ($zone->{'id'} == $zoneID){ - $zoneName = TadoAPI_ReplaceUmlaute($zone->{'name'}); - return $zoneName; - } - } - 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 => 5, - hash => $hash - }; - - #Log3 $name, 5, 'Blocking GET: ' . Dumper($param); - 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 = $decoded_data->{'sensorDataPoints'}->{'insideTemperature'}->{'celsius'}; - $humidity = $decoded_data->{'sensorDataPoints'}->{'humidity'}->{'percentage'}; - $desiredTemp = $decoded_data->{'setting'}->{'temperature'}->{'celsius'}; - $currentHeatingPower = $decoded_data->{'activityDataPoints'}->{'heatingPower'}->{'percentage'}; - $overlay = $decoded_data->{'overlayType'}; - if (!defined $overlay) {$overlay = "no overlay"}; - - my $zoneName = TadoAPI_GetZoneNameById($hash, $zoneID); - - if (defined($zoneName)){ - 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 ); - } - } - } - 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); - 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; - -# Beginn der Commandref - -=pod -=item [device] -=item summary integration of the Tado API -=item summary_DE Anbindung der Tado Heizungssteuerung über API - -=begin html - -

Tado

-
    -TadoAPI implements an interface to the Tado cloud. -

    - -Define -
      -define <name> TadoAPI <tado-mailadress> <homeID> -

      -Example: define myTado TadoAPI mail@example.com 12345 -

      -The username and password must match the username and password used on the Tado website. -After successful define, store PASSWORD with set myTado password <yout-tado-password> -Password is sored in encrypted FHEM store. All requests to the API are via oauth2 token. -
    -=end html - -=begin html_DE - - -=end html - -# Ende der Commandref -=cut \ No newline at end of file +#=============================================================================== +# +# 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: 1.0 +# CREATED: 04/12/2019 07:55:44 PM +# REVISION: 11/21/2019 05:17:22 PM +#=============================================================================== +package main; + +use strict; +use warnings; +use utf8; +use HTTP::Request::Common qw (POST GET PUT); +use HTTP::Headers; +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 $header = {}; +my $data = {}; +my $TokenData = {}; + +# helpers +my $apiStatus = 1; + +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 " . + $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_LoadToken($hash); + + # start the status update timer + RemoveInternalTimer($hash); + InternalTimer( gettimeofday() + 10, "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; + } + } + } + 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 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) - Setting: off=delete overlay; 0=heating power off; 1<=desired temperature (overlay)" if(@a < 4); + + if( $a[3] eq "off" ) { + TadoAPI_SetZoneOverlayById($hash, $value, "off"); + } elsif ($a[3] >= 0) { + TadoAPI_SetZoneOverlayById($hash, $value, $a[3]); + } + Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; + + } elsif( $cmd eq 'setAllOverlays' ) { + Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)"; + return "Need at least one parameter (Setting) - Setting: off=delete overlay; 0=heating power off; 1<=desired temperature (overlay)" if(@a < 3); + if( $value eq "off" ) { + TadoAPI_SetAllOverlays($hash, "off"); + } elsif ($value >= 0) { + TadoAPI_SetAllOverlays($hash, $value); + } + Log3 $name, 3, "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 'getTemperature' ) { + Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)"; + 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"; + + } 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); + } + + 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 ){ + print "\n"; + $message .= $item->{'name'} . ": " . $item->{'id'} . "\n"; + }; + Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n"; + last; + }; + + $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_OnlineStatus(@){ + 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 ''); + my $Token = undef; + + TadoAPI_OnlineStatus($hash); + + 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"; + Log3 $name, 5, "TadoAPI $name" . ": " . "Token is expired, requesting new one" if $tokenLifeTime < gettimeofday(); + $Token = TadoAPI_NewTokenRequest($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; + } +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); + 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); + 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"; + + # 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, 5, "TadoAPI $name" . ": SetOverlay for Zone $zoneID (Setting: " . $setting . ") - " . "query-URL: $URL"; + + if ($setting eq "off"){ + $method = "DELETE"; + Log3 $name, 3, "TadoAPI $name" . ": " . "Deleting Overlay for Zone $zoneID"; + }elsif($setting == 0){ + $method = "PUT"; + $myjson = { + setting => { + type => "HEATING", + power => "OFF", + }, + termination => { + type => "MANUAL" + }, + }; + $myjson = encode_json($myjson); + }elsif($setting > 0){ + $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 => 5, + callback => \&Tado_UpdateZoneOverlayCallback, + hash => $hash, + setting => $setting, + zoneID => $zoneID, + data => $myjson + }; + + #Log3 $name, 5, 'NonBlocking Request: ' . Dumper($request); + HttpUtils_NonblockingGet($request); + + } +} + + + +sub TadoAPI_SetAllOverlays(@){ + my ($hash, $setting) = @_; + my $name = $hash->{NAME}; + my $homeID = $attr{$name}{homeID}; + my $TokenData = TadoAPI_LoadToken($hash); + + if(defined($TokenData)){ + my $message = ""; + my $method = ""; + my $myjson =""; + + my @devArr = TadoAPI_GetTadoDevices($hash); + + for (my $i=0; $i < @devArr; $i++) { + my $zoneid = $devArr[$i]->{'id'}; + TadoAPI_SetZoneOverlayById($hash, $zoneid, $setting); + } + } +} + +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 + }; + + 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 => 10, + incrementalTimout => 1, + callback => \&TadoAPI_UpdateMobileReadingsCallback, + hash => $hash + }; + 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, 5, "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{ + foreach my $zone ( @$decoded_data ){ + my $zoneid = $zone->{'id'}; + Log3 $name, 5, "TadoAPI $name" . ": " . "Set Reading Update for Zone $zoneid "; + my ($temperature, $humidity, $desiredTemp, $currentHeatingPower, $overlay ) = TadoAPI_GetZoneReadingsById($hash, $zoneid); + my $zoneName = TadoAPI_GetZoneNameById($hash, $zoneid); + + if(defined($zoneName)){ + 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 ); + } + # iterate through all devices in zone + my $devices = $zone->{'devices'}; + foreach my $device ( @$devices ){ + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, "Battery_" . $device->{'serialNo'}, $device->{'batteryState'}); + readingsEndUpdate( $hash, 1 ); + $hash->{LastRequest}="OK"; + } + + } + } + my $zonecount = TadoAPI_GetZoneCount($hash); + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, "ActiveZones", $zonecount); + readingsEndUpdate( $hash, 1 ); + } +} + +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 + TadoAPI_GetZoneReadingsById($hash, $zoneID); + +} + +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_UpdateTadoDeviceInformationCallback($){ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + if($err ne "") + { + Log3 $name, 3, "Error in UpdateTadoDeviceInformationCallback 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" . ": " . "UpdateTadoDeviceInformationCallback: decode_json failed, invalid json. error:$@\n" if $@; + Log3 $name, 3, "TadoAPI $name" . ": " . "UpdateTadoDeviceInformationCallback: Error in decoded data, Code: " . $decoded_data->{'errors'}->[0]->{'code'}; + $hash->{LastRequest}="error"; + }else{ + foreach my $zone ( @$decoded_data ){ + my $devices = $zone->{'devices'}; + foreach my $device ( @$devices ){ + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, "Battery_" . $device->{'serialNo'}, $device->{'batteryState'}); + readingsEndUpdate( $hash, 1 ); + $hash->{LastRequest}="OK"; + } + } + } + } +} + +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 ){ + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, "Geolocation_" . $item->{'id'}, $item->{'settings'}->{'geoTrackingEnabled'}); + readingsEndUpdate( $hash, 1 ); + $hash->{LastRequest}="OK"; + } + } + } +} + +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 + }; + + 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 + }; + + 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 + }; + + 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 + }; + + HttpUtils_NonblockingGet($request); + } +} + +####################################################################################################### +# API Requests +############### + + + + +###################################### +############ 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_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); + 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(@){ + 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)){ + 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); + my ($err, $data) = HttpUtils_BlockingGet($param); + + if($err ne "") + { + Log3 $name, 3, "TadoAPI $name" . ": " . "TadoAPI_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" . ": " . "TadoAPI_GetGeoById: decode_json failed, invalid json. error:$@\n"; + }else{ + my $setting = $decoded_data->{'geoTrackingEnabled'}; + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, "Geolocation_" . $mobileID, $setting); + readingsEndUpdate( $hash, 1 ); + return $setting; + } + } + } + return undef; +} + +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); + 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'){ + 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); + 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 @devArr = TadoAPI_GetTadoDevices($hash); + my $zoneName = undef; + + foreach my $zone ( @devArr ){ + if ($zone->{'id'} == $zoneID){ + $zoneName = TadoAPI_ReplaceUmlaute($zone->{'name'}); + return $zoneName; + } + } + 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 => 5, + hash => $hash + }; + + #Log3 $name, 5, 'Blocking GET: ' . Dumper($param); + 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 = $decoded_data->{'sensorDataPoints'}->{'insideTemperature'}->{'celsius'}; + $humidity = $decoded_data->{'sensorDataPoints'}->{'humidity'}->{'percentage'}; + $desiredTemp = $decoded_data->{'setting'}->{'temperature'}->{'celsius'}; + $currentHeatingPower = $decoded_data->{'activityDataPoints'}->{'heatingPower'}->{'percentage'}; + $overlay = $decoded_data->{'overlayType'}; + if (!defined $overlay) {$overlay = "no overlay"}; + + my $zoneName = TadoAPI_GetZoneNameById($hash, $zoneID); + + if (defined($zoneName)){ + 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 ); + } + } + } + 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); + 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 helper +=item summary integration of the Tado API +=item summary_DE Anbindung der Tado Heizungssteuerung über API +=begin html + + +

    TadoAPI

    +
      + Module to control the integration of tado devices with FHEM.
      + Notes: +
        +
      • JSON has to be installed on the FHEM host.
      • +
      + + + 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> + Password is sored in encrypted FHEM store. All requests to the API are 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>
        + Setting: off 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: off 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. +
      • +
      +
      +
    + +=end html + +# Ende der Commandref +=cut