FHEM-Tado/98_TadoAPI.pm

1919 lines
61 KiB
Perl

#===============================================================================
# $Id: 98_TadoAPI.pm 106 2022-02-13 15:34:28Z psycho160 $
#
# FILE: 98_TadoAPI.pm
#
# USAGE: Module for FHEM
# Info: Turn verbose on for debugging
#
# REQUIREMENTS: Below modules should be pre-installed.
# HTTP::Request::Common
# HTTP::Headers
# Data::Dumper;
# JSON
#
# BUGS: ---
# NOTES: https://git.wolfmajer.at
# AUTHOR: Philipp Wolfmajer
# ORGANIZATION:
# VERSION: 1.0
# CREATED: 04/12/2019
# REVISION: 02/13/2023
#===============================================================================
package main;
use strict;
use warnings;
use utf8;
use HTTP::Request::Common qw (POST GET PUT);
use HTTP::Headers;
use JSON;
use POSIX qw(strftime);
####DEFAULTS############
my $client_id = 'public-api-preview';
my $client_secret = '4HJGRffVR8xb3XdEUQpjgZ1VplJi6Xgw';
my $scope = 'home.user';
my $AuthURL = qq{https://auth.tado.com/oauth/token};
my $DataURL = qq{https://my.tado.com/api/v2/me};
my $QueryURL = qq{https://my.tado.com/api/v2/homes};
my $tokenFile = "./FHEM/FhemUtils/TadoAPI_token";
my $header = {};
my $reqDebug = 5;
# helpers
my $apiStatus = 1;
my %sets = (
"zoneUpdate" => "",
"refreshToken" => "noArg",
"password" => "",
"update" => "noArg",
"setGeo" => "",
"setZoneOverlay" => "",
"timedZoneOverlay" => "",
"updateAllOverlays" => "noArg",
"setAllOverlays" => ""
);
my %gets = (
"getZoneDevices" => "noArg",
"getZoneInfo" => "noArg",
"getGeo" => "",
#"getXTest" => "",
"getMobileDevices" => "noArg"
);
sub TadoAPI_Initialize {
my $hash = shift;
my $TYPE = "TadoAPI";
$hash->{DefFn} = \&TadoAPI_Define;
$hash->{InitFn} = \&TadoAPI_Init;
$hash->{SetFn} = \&TadoAPI_Set;
$hash->{GetFn} = \&TadoAPI_Get;
$hash->{AttrList} = ""
. "homeID "
. "mobileID "
. "showPosData:0,1 "
. "updateIntervall "
. $readingFnAttributes;
return;
}
sub TadoAPI_Init {
my $hash = shift;
my $def = shift;
my @args = split( "[ \t][ \t]*", $def );
my $u =
"wrong syntax: define <name> TadoAPI <username>";
return $u if ( int(@args) < 2 );
return;
}
sub TadoAPI_Define {
my $hash = shift;
my $def = shift;
my @a = split( "[ \t]+", $def );
my $name = shift @a;
my $type = shift @a;
my $tokenFileName = $tokenFile . "_" . $name;
return "Invalid number of arguments: "
. "define <name> TadoAPI <username>"
if ( int(@a) < 1 );
my ( $user, $homeID ) = @a;
Log3 $name, 3, "TadoAPI_Define $name: called ";
$hash->{STATE} = "defined";
# Initialize the device
return $@ unless ( FHEM::Meta::SetInternals($hash) );
$hash->{TADO_USER} = $user;
$hash->{TOKEN_FILE} = $tokenFileName;
my @args = ($homeID);
if ($main::init_done) {
# do something?
return TadoAPI_Catch($@) if $@;
}
my $password = TadoAPI_readPassword($name);
if ( defined($password) ) {
TadoAPI_CheckStatus($hash);
TadoAPI_LoadToken($hash);
# start the status update timer
RemoveInternalTimer($hash);
InternalTimer( gettimeofday() + 15, "TadoAPI_Update", $hash, 0 );
if ( defined($homeID) && $homeID ne "" ) {
$attr{$name}{homeID} = $homeID;
}
else {
my $id = TadoAPI_GetHomeId($hash);
if ( defined($id) && $id ne "" ) {
$attr{$name}{homeID} = $id;
}
}
}
else {
$hash->{STATE} = "no password set";
}
return;
}
sub TadoAPI_Set {
my $hash = shift;
my $name = shift;
my $cmd = shift // return qq{set $name needs at least one argument};
my $value = shift;
my $value2 = shift;
my $value3 = shift;
my $message = undef;
if ( !defined( $sets{$cmd} ) ) {
my @cmds = ();
for my $key ( sort keys %sets ) {
push @cmds,
$sets{$key} ? $key . ":" . join( ",", $sets{$key} ) : $key;
}
return "Unknown argument $cmd, choose one of " . join( " ", @cmds );
}
if ( ( $cmd ne "password" ) ) {
my $pwd = TadoAPI_readPassword($name);
unless ( defined $pwd ) {
$message =
"Error: no tado password set. Please define it with 'set $name password Your_tado_Password'";
Log3 $name, 2, "$name, $message";
$hash->{STATE} = "no password set";
return $message;
}
}
if ( $cmd eq 'setGeo' ) {
return "Need at least two parameters (mobileID, Setting)" if ( !defined($value) );
if ( $value2 eq "on" ) {
Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)";
TadoAPI_SetGeoById( $hash, $value, 1 );
}
else {
Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)";
TadoAPI_SetGeoById( $hash, $value, 0 );
}
TadoAPI_GetGeoById( $hash, $value );
}
elsif ( $cmd eq 'setZoneOverlay' ) {
Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)";
return
"Need at least two parameters [ZoneID] [Setting] (duration in sec); Setting Info: remove=delete overlay; 0=heating power off; 1<=desired temperature (overlay)"
if ( !defined($value2) );
if ( $value2 eq "remove" ) {
TadoAPI_SetZoneOverlayById( $hash, $value, "remove" );
}
elsif ( defined( $value3 ) ) {
TadoAPI_SetZoneOverlayById( $hash, $value, $value2, $value3 );
}
elsif ( $value2 >= 0 ) {
TadoAPI_SetZoneOverlayById( $hash, $value, $value2 );
}
Log3 $name, 4, "TadoAPI $name" . ": " . "$cmd finished";
}
elsif ( $cmd eq 'timedZoneOverlay' ) {
Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)";
return
"Need at least three parameters [ZoneID] [Duration (sec)] [Setting]"
if (!defined($value3) );
if ( defined( $value3 ) ) {
TadoAPI_SetTimedZoneOverlay( $hash, $value, $value2, $value3 );
}
Log3 $name, 4, "TadoAPI $name" . ": " . "$cmd finished";
}
elsif ( $cmd eq 'setAllOverlays' ) {
Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)";
return "Need at least one parameter (Setting) - Setting: remove=delete overlay; 0=heating power off; 1<=desired temperature (overlay)" if ( !defined($value) );
if ( $value eq "remove" ) {
TadoAPI_SetAllOverlays( $hash, "remove" );
}
elsif ( $value >= 0 ) {
TadoAPI_SetAllOverlays( $hash, $value );
}
Log3 $name, 4, "TadoAPI $name" . ": " . "$cmd finished\n";
}
elsif ( $cmd eq 'updateAllOverlays' ) {
Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)";
TadoAPI_GetAllZoneOverlays($hash);
Log3 $name, 4, "TadoAPI $name" . ": " . "$cmd finished\n";
}
elsif ( $cmd eq 'refreshToken' ) {
Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)";
RemoveInternalTimer($hash);
InternalTimer( gettimeofday() + 10, "TadoAPI_Update", $hash, 0 );
TadoAPI_LoadToken($hash);
Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n";
}
elsif ( $cmd eq 'update' ) {
Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)";
TadoAPI_UpdateFn($hash);
Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n";
}
elsif ( $cmd eq 'zoneUpdate' ) {
Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)";
return "ZoneID as parameter needed" if ( !$value );
if ( $value >= 1 ) {
my ( $temperature, $humidity, $desiredTemp, $currentHeatingPower,
$overlay )
= TadoAPI_GetZoneReadingsById( $hash, $value );
my $zoneName = TadoAPI_GetZoneNameById( $hash, $value );
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 );
$message = "OK";
}
}
else {
return "Wrong ZoneID";
}
Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n";
}
elsif ( $cmd eq 'password' ) {
Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)";
# den Rest der das passwort enthält, als ein String
if (defined($value)){
$message = TadoAPI_storePassword( $name, $value );
}
else{
$message = "no password given";
}
# 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 $@;
return;
}
sub TadoAPI_Get {
my $hash = shift;
my $name = shift;
my $cmd = shift // return qq{get $name needs at least one argument};
my $value = shift;
my $homeID = $attr{$name}{homeID};
my $message = undef;
if ( !defined( $gets{$cmd} ) ) {
my @cmds = ();
for my $key ( sort keys %gets ) {
push @cmds,
$gets{$key} ? $key . ":" . join( ",", $gets{$key} ) : $key;
}
return "Unknown argument $cmd, choose one of " . join( " ", @cmds );
}
my $pwd = TadoAPI_readPassword($name);
unless ( defined $pwd ) {
$message =
"Error: no tado password set. Please define it with 'set $name password Your_tado_Password'";
Log3 $name, 2, "$name, $message";
$hash->{STATE} = "no password set";
return $message;
}
if ( $cmd =~ /\Qget\E/ ) {
COMMAND_HANDLER: {
$cmd eq "getGeo" and do {
return "Need at least one parameter (mobileID)" if ( !defined($value) );
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";
for my $item (@data) {
$message .= $item->{'name'} . ": " . $item->{'id'} . "\n";
}
Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished";
last;
};
# only for testing
$cmd eq "getXTest" and do {
Log3 $name, 5, "TadoAPI $name" . ": " . "processing ($cmd)";
my $zoneName = TadoAPI_GetZoneNameById( $hash, $value );
$zoneName = "wrong Zone ID" unless $zoneName;
$message = "Name: " . $zoneName;
Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n";
last;
};
$cmd eq "getZoneDevices" and do {
Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)";
my @devArr = TadoAPI_GetTadoDevices($hash);
my $devicecount = 0;
$message = "Tado-Device(s):\n";
for ( my $i = 0 ; $i < @devArr ; $i++ ) {
my $tadodevices = $devArr[$i]->{'devices'};
$message .= "ZoneID: " . ( $devArr[$i]->{'id'} );
my $spacer = 0;
for 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;
}
}
sub TadoAPI_Catch {
my $exception = shift;
if ($exception) {
$exception =~ /^(.*)( at.*FHEM.*)$/;
return $1;
}
return;
}
sub TadoAPI_Undefine {
my $hash = shift;
my $name = shift;
RemoveInternalTimer($hash);
#todo remove tokenfile
return;
}
sub TadoAPI_CheckStatus {
my $hash = shift;
my $name = $hash->{NAME};
# test api status
my $param = {
url => $AuthURL,
timeout => 5,
hash => $hash,
method => "GET",
header => "",
callback => \&TadoAPI_callback
};
#test if api is reachable
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $AuthURL";
HttpUtils_NonblockingGet($param);
return;
}
sub TadoAPI_LoadToken {
my $hash = shift;
my $name = $hash->{NAME};
my $tokenFileName = $tokenFile . "_" . $name;
my $tokenLifeTime = $hash->{TOKEN_LIFETIME};
$tokenLifeTime = 0 if ( !defined $tokenLifeTime || $tokenLifeTime eq '' );
my $Token = undef;
if ($apiStatus) {
my $TOKENFILE;
eval{
open( $TOKENFILE, q{<}, $tokenFileName ) or do {
$apiStatus = 0;
$hash->{STATE} = "Token error";
return;
};
$Token = decode_json(<$TOKENFILE>) };
close($TOKENFILE);
if ( $@ || $tokenLifeTime < gettimeofday() ) {
Log3 $name, 5,
"TadoAPI $name" . ": "
. "Error while loading: $@ ,requesting new one"
if $@;
Log3 $name, 5,
"TadoAPI $name" . ": " . "Token is expired, requesting new one"
if $tokenLifeTime < gettimeofday();
$Token = TadoAPI_NewTokenRequest($hash);
TadoAPI_CheckStatus($hash);
}
else {
Log3 $name, 5,
"TadoAPI $name" . ": "
. "Token expires at "
. localtime($tokenLifeTime);
# if token is about to expire, refresh him
if ( ( $tokenLifeTime - 45 ) < gettimeofday() ) {
Log3 $name, 5,
"TadoAPI $name" . ": " . "Token will expire soon, refreshing";
$Token = TadoAPI_TokenRefresh($hash);
}
}
return $Token if $Token;
}
TadoAPI_CheckStatus($hash);
return;
}
sub TadoAPI_NewTokenRequest {
my $hash = shift;
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()";
my $data = {
client_id => $client_id,
client_secret => $client_secret,
username => $username,
password => $password,
scope => $scope,
grant_type => 'password'
};
my $param = {
url => $AuthURL,
method => 'POST',
timeout => 5,
hash => $hash,
data => $data
};
#Log3 $name, 5, 'Blocking GET: ' . Dumper($param);
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $AuthURL";
my ( $err, $returnData ) = HttpUtils_BlockingGet($param);
if ( $err ne "" ) {
Log3 $name, 3,
"TadoAPI $name" . ": "
. "NewTokenRequest: Error while requesting "
. $param->{url}
. " - $err";
}
elsif ( $returnData ne "" ) {
Log3 $name, 5, "url " . $param->{url} . " returned: $returnData";
my $decoded_data = eval { decode_json($returnData) };
if ($@) {
Log3 $name, 3, "TadoAPI $name" . ": "
. "NewTokenRequest: decode_json failed, invalid json. error: $@ ";
}
else {
#write token data in file
open( my $TOKENFILE, q{>}, $tokenFileName ) or do {
$apiStatus = 0;
$hash->{STATE} = "Token error";
return;
};
print $TOKENFILE $returnData . "\n";
close($TOKENFILE);
# token lifetime management
if ( defined($decoded_data->{'expires_in'}) ){
$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;
}
}
return;
}
sub TadoAPI_TokenRefresh {
my $hash = shift;
my $name = $hash->{NAME};
my $tokenFileName = $tokenFile . "_" . $name;
my $Token = undef;
# load token
my $TOKENFILE;
eval {
open( $TOKENFILE, q{<}, $tokenFileName ) or do {
$apiStatus = 0;
$hash->{STATE} = "Token error";
return;
};
$Token = decode_json(<$TOKENFILE>) };
close($TOKENFILE);
my $data = {
client_id => $client_id,
client_secret => $client_secret,
scope => $scope,
grant_type => 'refresh_token',
refresh_token => $Token->{'refresh_token'}
};
my $param = {
url => $AuthURL,
method => 'POST',
timeout => 5,
hash => $hash,
data => $data
};
#Log3 $name, 5, 'Blocking GET TokenRefresh: ' . Dumper($param);
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $AuthURL";
my ( $err, $returnData ) = 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 ( $returnData ne "" ) {
Log3 $name, 5, "url " . $param->{url} . " returned: $returnData";
my $decoded_data = eval { decode_json($returnData); };
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( my $TOKENFILE, q{>}, $tokenFileName ) or do {
$apiStatus = 0;
$hash->{STATE} = "Token error";
return;
};
print $TOKENFILE $returnData . "\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;
}
}
return;
}
sub TadoAPI_Update {
my $hash = shift;
my $name = $hash->{NAME};
Log3 $name, 5, "TadoAPI $name" . ": " . "TadoAPI_Update called";
my $nextTimer = "none";
my $intervall = 300;
$intervall = $attr{$name}{updateIntervall}
if ( defined( $attr{$name}{updateIntervall} )
&& $attr{$name}{updateIntervall} =~ m/^-?\d+$/ );
$nextTimer = gettimeofday() + $intervall;
$hash->{NEXT_UPDATE} = localtime($nextTimer);
Log3 $name, 5, "TadoAPI $name" . ": " . "Next Timer = $nextTimer";
RemoveInternalTimer($hash);
InternalTimer( $nextTimer, "TadoAPI_Update", $hash, 0 );
# update subs
TadoAPI_UpdateFn($hash);
return;
}
######################## tado methods ########################
##############################################################
sub TadoAPI_SetZoneOverlayById {
my $hash = shift;
my $zoneID = shift;
my $setting = shift;
my $duration = shift;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $URL = $QueryURL . qq{/$homeID/zones/$zoneID/overlay};
my $CurrentTokenData = TadoAPI_LoadToken($hash);
if ( defined($CurrentTokenData) ) {
my $method = "";
my $myjson = undef;
Log3 $name, 5,
"TadoAPI $name"
. ": SetOverlay for Zone $zoneID (Setting: "
. $setting . ") - "
. "query-URL: $URL";
my $dt = time();
$dt += $duration if defined($duration);
# remove overlay & lock
if ( $setting eq "remove" ) {
$method = "DELETE";
Log3 $name, 3,
"TadoAPI $name" . ": " . "Deleting Overlay for Zone $zoneID";
delete( $hash->{helper}{LockedZones}{$zoneID} );
}
# turn heating of
elsif ( $setting == 0 ) {
# turn off for timer
if ( defined($duration) && $duration > 0 ) {
$method = "PUT";
$myjson = {
type => "MANUAL",
setting => {
type => "HEATING",
power => "OFF"
},
termination => {
type => "TIMER",
durationInSeconds => $duration,
expiry => strftime( '%Y-%m-%dT%H:%M:%SZ', gmtime($dt) )
}
};
Log3 $name, 3, "TadoAPI $name" . ": "
. "Timer Overlay for Zone $zoneID . Power off for: $duration seconds";
}
# infinite off
else {
$method = "PUT";
$myjson = {
setting => {
type => "HEATING",
power => "OFF",
},
termination => {
type => "MANUAL"
},
};
}
}
elsif ( $setting > 0 ) {
# set timed overlay
if ( defined($duration) && $duration > 0 ) {
$method = "PUT";
$myjson = {
type => "MANUAL",
setting => {
type => "HEATING",
power => "ON",
temperature => {
celsius => $setting
}
},
termination => {
type => "TIMER",
durationInSeconds => $duration,
expiry => strftime( '%Y-%m-%dT%H:%M:%SZ', gmtime($dt) ),
}
};
Log3 $name, 3, "TadoAPI $name" . ": "
. "Set Timer Overlay for Zone $zoneID with $duration seconds expire.";
# set lock for this zone
$hash->{helper}{LockedZones}{$zoneID} = "locked";
}
else {
# infinite setting
$method = "PUT";
$myjson = {
setting => {
type => "HEATING",
power => "ON",
temperature => {
celsius => $setting
},
},
termination => {
type => "MANUAL"
},
};
}
}
$myjson = encode_json($myjson) if ( defined($myjson) );
my $request = {
url => $URL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" =>
"$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => $method,
timeout => 5,
callback => \&Tado_UpdateZoneOverlayCallback,
hash => $hash,
setting => $setting,
zoneID => $zoneID,
data => $myjson
};
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL";
HttpUtils_NonblockingGet($request);
}
return;
}
sub TadoAPI_SetAllOverlays {
my $hash = shift;
my $setting = shift;
my $duration = shift;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my @zones = TadoAPI_GetTadoDevices($hash);
for ( my $i = 0 ; $i < @zones ; $i++ ) {
my $zoneID = $zones[$i]->{'id'};
if ( defined($duration) && $duration > 0 ) {
TadoAPI_SetZoneOverlayById( $hash, $zoneID, $setting, $duration );
}
else {
TadoAPI_SetZoneOverlayById( $hash, $zoneID, $setting );
}
}
return;
}
sub TadoAPI_GetAllZoneOverlays {
my $hash = shift;
my $name = $hash->{NAME};
my @zones = TadoAPI_GetTadoDevices($hash);
for my $zone (@zones) {
my $zoneID = $zone->{'id'};
my $zoneName = TadoAPI_ReplaceUmlaute( $zone->{'name'} );
my ( $temperature, $humidity, $desiredTemp, $currentHeatingPower,
$overlay )
= TadoAPI_GetZoneReadingsById( $hash, $zoneID );
readingsSingleUpdate( $hash, "OverlayType_" . $zoneName, $overlay, 1 );
}
return;
}
sub TadoAPI_UpdateFn {
my $hash = shift;
my $name = $hash->{NAME};
my $CurrentTokenData = TadoAPI_LoadToken($hash);
my $homeID = $attr{$name}{homeID};
if ( $apiStatus == 1 && defined($CurrentTokenData) ) {
# zone specific updates
my $URL = $QueryURL . qq{/$homeID/zones};
my $request = {
url => $URL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" =>
"$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => 'GET',
timeout => 25,
incrementalTimout => 1,
hash => $hash,
callback => \&TadoAPI_UpdateAllZoneReadingsCallback
};
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "UpdFN: Request $URL";
HttpUtils_NonblockingGet($request);
# mobile devices
$URL = $QueryURL . qq{/$homeID/mobileDevices};
$request = {
url => $URL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" =>
"$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => 'GET',
timeout => 7,
incrementalTimout => 1,
callback => \&TadoAPI_UpdateMobileReadingsCallback,
hash => $hash
};
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL";
HttpUtils_NonblockingGet($request);
}
return;
}
########################################################################################################################################################################
# Callback Subs
########################################################################################################################################################################
sub TadoAPI_callback {
my $param = shift;
my $err = shift;
my $data = shift;
my $hash = $param->{hash};
my $name = $hash->{NAME};
$param->{code} = 0 unless defined $param->{code};
if ( $param->{code} == 401 || $param->{code} == 400 ) {
$apiStatus = 1;
$hash->{STATE} = "reachable";
Log3 $name, 5,
"TadoAPI $name" . ": "
. "API is reachable. Callback Status: "
. $param->{code};
}
else {
$apiStatus = 0;
$hash->{STATE} = "error";
Log3 $name, 3,
"TadoAPI $name" . ": " . "API error: apiStatus $apiStatus ($err)";
}
return;
}
sub TadoAPI_UpdateAllZoneReadingsCallback {
my $param = shift;
my $err = shift;
my $data = shift;
my $hash = $param->{hash};
my $name = $hash->{NAME};
if ( $err ne "" ) {
Log3 $name, 3,
"Error in TadoAPI_UpdateZoneCallback 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" . ": "
. "UpdateAllZonesCallback: decode_json failed, invalid json. error:$@\n"
if $@;
Log3 $name, 3,
"TadoAPI $name" . ": "
. "UpdateAllZonesCallback: Error in decoded data, Code: "
. $decoded_data->{'errors'}->[0]->{'code'}
if ( exists( $decoded_data->{'errors'}->[0]->{'code'} ) );
$hash->{LastRequest} = "error";
}
else {
readingsBeginUpdate($hash);
for my $zone (@$decoded_data) {
my $zoneID = $zone->{'id'};
my $zoneName = TadoAPI_ReplaceUmlaute( $zone->{'name'} );
Log3 $name, 5, "TadoAPI $name" . ": "
. "Set Reading Update for Zone $zoneID ";
my ( $temperature, $humidity, $desiredTemp,
$currentHeatingPower, $overlay )
= TadoAPI_GetZoneReadingsById( $hash, $zoneID );
# updates zone readings
readingsBulkUpdate( $hash, "ZoneID_" . $zoneName, $zoneID );
readingsBulkUpdate( $hash, "Temperatur_" . $zoneName, $temperature );
readingsBulkUpdate( $hash, "Luftfeuchtigkeit_" . $zoneName, $humidity );
readingsBulkUpdate( $hash, "Heizleistung_" . $zoneName, $currentHeatingPower );
readingsBulkUpdate( $hash, "OverlayType_" . $zoneName, $overlay );
readingsBulkUpdate( $hash, "DesiredTemp_" . $zoneName, $desiredTemp );
# iterate through all devices in zone
my $devices = $zone->{'devices'};
for my $device (@$devices) {
readingsBulkUpdate(
$hash,
"Battery_" . $device->{'serialNo'},
$device->{'batteryState'}
);
readingsBulkUpdate(
$hash,
"ChildLockEnabled_" . $device->{'serialNo'},
$device->{'childLockEnabled'}
);
}
}
readingsEndUpdate( $hash, 1 );
}
my $zonecount = TadoAPI_GetZoneCount($hash);
readingsBeginUpdate($hash);
readingsBulkUpdate( $hash, "ActiveZones", $zonecount );
readingsEndUpdate( $hash, 0 );
$hash->{LastRequest} = "OK";
}
return;
}
sub Tado_UpdateZoneOverlayCallback {
my $param = shift;
my $err = shift;
my $data = shift;
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";
}
elsif ( $data ne "" ) {
Log3 $name, 5, "url " . $param->{url} . " returned: $data";
Log3 $name, 3, "TadoAPI $name" . ": "
. "set (async) Overlay for Zone $zoneID to: $setting";
}
# finaly update readings
my ( $temperature, $humidity, $desiredTemp, $currentHeatingPower, $overlay )
= TadoAPI_GetZoneReadingsById( $hash, $zoneID );
my $zoneName = TadoAPI_GetZoneNameById( $hash, $zoneID );
if ( defined($zoneName) ) {
# updates zone readings
readingsBeginUpdate($hash);
# readingsBulkUpdate($hash, "Temperatur_" . $zoneName, $temperature);
# readingsBulkUpdate($hash, "Luftfeuchtigkeit_" . $zoneName, $humidity);
# readingsBulkUpdate($hash, "Heizleistung_" . $zoneName, $currentHeatingPower);
readingsBulkUpdate( $hash, "OverlayType_" . $zoneName, $overlay );
readingsBulkUpdate( $hash, "DesiredTemp_" . $zoneName, $desiredTemp );
# lock zone if timed overlay
if ( exists( $hash->{helper}->{LockedZones}{$zoneID} ) ) {
readingsBulkUpdate( $hash, "Zone" . $zoneID . "Lock", "timer" );
readingsEndUpdate( $hash, 1 );
}
else {
readingsEndUpdate( $hash, 1 );
readingsDelete( $hash, "Zone" . $zoneID . "Lock" );
}
}
return;
}
sub TadoAPI_LogInfoCallback {
my $param = shift;
my $err = shift;
my $data = shift;
my $hash = $param->{hash};
my $name = $hash->{NAME};
if ( $err ne "" ) {
Log3 $name, 3,
"Error in LogInfoCallback while requesting "
. $param->{url}
. " - $err";
}
elsif ( $data ne "" ) {
Log3 $name, 3,
"TadoAPI $name" . ": " . $param->{infotext} . ":\n" . $data . "\n";
}
return;
}
sub TadoAPI_SetGeoByIdCallback {
my $param = shift;
my $err = shift;
my $data = shift;
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, "SetGeoById URL: " . $param->{url} . " returned: $data";
}
return;
}
sub TadoAPI_UpdateMobileReadingsCallback {
my $param = shift;
my $err = shift;
my $data = shift;
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 {
for my $item (@$decoded_data) {
TadoAPI_GetGeoById( $hash, $item->{'id'}, $item );
}
}
}
return;
}
sub TadoAPI_GetZoneInfo {
my $hash = shift;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $CurrentTokenData = TadoAPI_LoadToken($hash);
if ( defined($CurrentTokenData) ) {
# HomeInfo
my $URL = qq{https://my.tado.com/api/v2/me};
my $request = {
url => $URL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" => "$CurrentTokenData->{'token_type'} $CurrentTokenData->{'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" => "$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => 'GET',
timeout => 3,
infotext => "Tado Devices Info",
hash => $hash,
callback => \&TadoAPI_LogInfoCallback
};
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL";
HttpUtils_NonblockingGet($request);
# Mobileinfo
$URL = $QueryURL . qq{/$homeID/mobileDevices};
$request = {
url => $URL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" => "$CurrentTokenData->{'token_type'} $CurrentTokenData->{'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" => "$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => 'GET',
timeout => 3,
infotext => "Mobile Device $mobileID",
hash => $hash,
callback => \&TadoAPI_LogInfoCallback
};
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL";
HttpUtils_NonblockingGet($request);
}
# zones
my @devArr = TadoAPI_GetTadoDevices($hash);
for ( my $i = 0 ; $i < @devArr ; $i++ ) {
my $zoneID = $devArr[$i]->{'id'};
$URL = $QueryURL . qq{/$homeID/zones/$zoneID/state};
my $infotext =
"ZoneID $zoneID ("
. TadoAPI_GetZoneNameById( $hash, $zoneID )
. ") Status";
$request = {
url => $URL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" => "$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => 'GET',
timeout => 3,
infotext => $infotext,
hash => $hash,
callback => \&TadoAPI_LogInfoCallback
};
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL";
HttpUtils_NonblockingGet($request);
}
}
return;
}
sub TadoAPI_SetGeoById {
my $hash = shift;
my $mobileID = shift;
my $geo = shift;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $URL = $QueryURL . qq{/$homeID/mobileDevices/$mobileID/settings};
my $CurrentTokenData = TadoAPI_LoadToken($hash);
my $data = {};
if ( defined($CurrentTokenData) ) {
if ($geo) {
$data = { geoTrackingEnabled => "true" };
}
else {
$data = { geoTrackingEnabled => "false" };
}
$data = encode_json($data);
my $request = {
url => $URL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" => "$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => 'PUT',
timeout => 3,
mobileID => $mobileID,
data => $data,
hash => $hash,
callback => \&TadoAPI_SetGeoByIdCallback
};
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL";
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "PUT setting $data";
HttpUtils_NonblockingGet($request);
}
return;
}
######################################
############ Helpers #################
######################################
sub TadoAPI_ReplaceUmlaute {
my $string = shift;
my %umlaute = (
"ä" => "ae",
"Ä" => "Ae",
"ü" => "ue",
"Ü" => "Ue",
"ö" => "oe",
"Ö" => "Oe",
"ß" => "ss"
);
my $umlautkeys = join( "|", keys(%umlaute) );
$string =~ s/($umlautkeys)/$umlaute{$1}/g;
return $string;
}
# helper sub for fhem tablet-ui thermostat widget: set timedZoneOverlay <zoneID> <duration> <setting>
sub TadoAPI_SetTimedZoneOverlay {
my $hash = shift;
my $zoneID = shift;
my $duration = shift;
my $setting = shift;
my $name = $hash->{NAME};
TadoAPI_SetZoneOverlayById( $hash, $zoneID, $setting, $duration );
return;
}
sub TadoAPI_GetHomeId {
# returns first home id only
my $hash = shift;
my $name = $hash->{NAME};
my $CurrentTokenData = TadoAPI_LoadToken($hash);
if ( defined($CurrentTokenData) ) {
my $param = {
url => $DataURL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" => "$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => 'GET',
timeout => 2,
hash => $hash,
};
#Log3 $name, 5, 'Blocking GET: ' . Dumper($param);
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $DataURL";
my ( $err, $data ) = HttpUtils_BlockingGet($param);
if ( $err ne "" ) {
Log3 $name, 3,
"TadoAPI $name" . ": "
. "GetHomeId: Error while requesting "
. $param->{url}
. " - $err";
}
elsif ( $data ne "" ) {
Log3 $name, 5, "URL " . $param->{url} . " returned: $data";
my $decoded_data = eval { decode_json($data) };
if ($@) {
Log3 $name, 3,
"TadoAPI $name" . ": "
. "GetHomeId: Decode_json failed, invalid json. error:$@"
if $@;
$hash->{LastRequest} = "error";
}
else {
$hash->{LastRequest} = "OK";
return $decoded_data->{'homes'}->[0]->{'id'}
if ( exists( $decoded_data->{'homes'} ) );
}
}
}
return;
}
sub TadoAPI_GetGeoById {
# returns geo setting and distance from home; takes an item object or querys itself
my $hash = shift;
my $mobileID = shift;
my $item = shift;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $URL = $QueryURL . qq{/$homeID/mobileDevices};
if ( !defined($item) ) {
my $CurrentTokenData = TadoAPI_LoadToken($hash);
if ( defined($CurrentTokenData) ) {
my $param = {
url => $URL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" => "$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => 'GET',
timeout => 4,
hash => $hash,
};
#Log3 $name, 5, 'Blocking GET: ' . Dumper($param);
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL";
my ( $err, $data ) = HttpUtils_BlockingGet($param);
if ( $err ne "" ) {
Log3 $name, 3,
"TadoAPI $name" . ": "
. "GetGeoById: Error while requesting "
. $param->{url}
. " - $err";
}
elsif ( $data ne "" ) {
Log3 $name, 5,
"GetGeoById URL: " . $param->{url} . " returned: $data";
my $decoded_data = eval { decode_json($data) };
if ($@) {
Log3 $name, 3,
"TadoAPI $name" . ": "
. "GetGeoById: Decode_json failed, invalid json. error:$@\n"
if $@;
Log3 $name, 3,
"TadoAPI $name" . ": "
. "GetGeoById: Error in UpdateMobileReadingsCallback, Code: "
. $decoded_data->{'errors'}->[0]->{'code'};
$hash->{LastRequest} = "error";
}
else {
for my $item (@$decoded_data) {
if ( $item->{'id'} eq $mobileID ) {
return my ( $setting, $distance ) =
TadoAPI_ParseMobileItem( $hash, $item );
}
}
}
}
}
}
elsif ( defined($item) ) {
Log3 $name, 5,
"TadoAPI $name" . ": " . "GetGeoById: parsing passed item";
return my ( $setting, $distance ) =
TadoAPI_ParseMobileItem( $hash, $item );
}
return;
}
sub TadoAPI_ParseMobileItem {
my $hash = shift;
my $item = shift;
my $name = $hash->{NAME};
my $setting = 0;
$setting = 1 if $item->{'settings'}->{'geoTrackingEnabled'};
my $distance = "-";
$distance = $item->{'location'}->{'relativeDistanceFromHomeFence'}
if $setting;
readingsBeginUpdate($hash);
readingsBulkUpdate(
$hash,
"GeoTracking_" . $item->{'id'},
$item->{'settings'}->{'geoTrackingEnabled'}
);
if ( defined( $item->{'location'}->{'atHome'} )
&& $item->{'location'}->{'atHome'} )
{
# present
readingsBulkUpdate( $hash, "GeoLocation_" . $item->{'id'}, "present" );
}
elsif ( defined( $item->{'location'}->{'atHome'} ) ) {
# away
readingsBulkUpdate( $hash, "GeoLocation_" . $item->{'id'}, "away" );
}
else {
# no state
readingsDelete( $hash, "GeoLocation_" . $item->{'id'} );
}
readingsBulkUpdate( $hash, "GeoDistance_" . $item->{'id'}, $distance )
if $setting && $attr{$name}{showPosData};
readingsDelete( $hash, "GeoDistance_" . $item->{'id'} )
if !defined( $attr{$name}{showPosData} )
|| $attr{$name}{showPosData} == 0
|| !$setting;
readingsEndUpdate( $hash, 1 );
$hash->{LastRequest} = "OK";
return ( $setting, $distance );
}
sub TadoAPI_GetMobileDevices {
my $hash = shift;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $URL = $QueryURL . qq{/$homeID/mobileDevices};
my $CurrentTokenData = TadoAPI_LoadToken($hash);
if ( defined($CurrentTokenData) ) {
my $param = {
url => $URL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" => "$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => 'GET',
timeout => 2,
hash => $hash
};
#Log3 $name, 5, 'Blocking GET: ' . Dumper($param);
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL";
my ( $err, $data ) = HttpUtils_BlockingGet($param);
if ( $err ne "" ) {
Log3 $name, 3,
"TadoAPI $name" . ": "
. "GetMobileDevices: Error while requesting "
. $param->{url}
. " - $err";
}
elsif ( $data ne "" ) {
my @devices = ();
Log3 $name, 5, "url " . $param->{url} . " returned: $data";
my $decoded_data = eval { decode_json($data) };
if ($@) {
Log3 $name, 3, "TadoAPI $name" . ": "
. "GetMobileDevices: decode_json failed, invalid json. error:$@\n";
}
else {
if ( ref($decoded_data) eq 'ARRAY' ) {
for my $item (@$decoded_data) {
push @devices, $item;
}
# default case
return @devices;
}
elsif ( ref($decoded_data) eq 'HASH' ) {
# error, api response is a hash in case of error
Log3 $name, 3,
"TadoAPI $name" . ": "
. "GetMobileDevices: "
. $decoded_data->{'errors'}->[0]->{'code'}
if ( exists( $decoded_data->{'errors'} ) );
}
}
}
}
return;
}
sub TadoAPI_GetZoneCount {
my $hash = shift;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $URL = $QueryURL . qq{/$homeID/zones};
my $zonecount = 0;
my $CurrentTokenData = TadoAPI_LoadToken($hash);
if ( defined($CurrentTokenData) ) {
my $param = {
url => $URL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" =>
"$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => 'GET',
timeout => 2,
hash => $hash
};
#Log3 $name, 5, 'Blocking GET: ' . Dumper($param);
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL";
my ( $err, $data ) = HttpUtils_BlockingGet($param);
if ( $err ne "" ) {
Log3 $name, 3,
"TadoAPI $name" . ": "
. "GetZoneCount: Error while requesting "
. $param->{url}
. " - $err";
}
elsif ( $data ne "" ) {
my @devices = ();
Log3 $name, 5, "url " . $param->{url} . " returned: $data";
my $decoded_data = eval { decode_json($data) };
if ($@) {
Log3 $name, 3, "TadoAPI $name" . ": "
. "GetZoneCount: decode_json failed, invalid json. error:$@\n";
}
else {
if ( ref($decoded_data) eq 'ARRAY' ) {
for 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;
}
sub TadoAPI_GetZoneNameById {
my $hash = shift;
my $zoneID = shift;
my $name = $hash->{NAME};
my $zoneName = undef;
my @zones = TadoAPI_GetTadoDevices($hash);
for my $zone (@zones) {
if ( $zone->{'id'} == $zoneID ) {
$zoneName = TadoAPI_ReplaceUmlaute( $zone->{'name'} );
return $zoneName;
}
}
Log3 $name, 3,
"TadoAPI $name" . ": " . "Error GetZoneNameById: Wrong zone ID ($zoneID)";
return;
}
sub TadoAPI_GetZoneReadingsById {
my $hash = shift;
my $zoneID = shift;
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 $CurrentTokenData = TadoAPI_LoadToken($hash);
if ( defined($CurrentTokenData) ) {
my $param = {
url => $URL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" => "$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => 'GET',
timeout => 4,
hash => $hash
};
#Log3 $name, 5, 'Blocking GET: ' . Dumper($param);
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL";
my ( $err, $data ) = HttpUtils_BlockingGet($param);
if ( $err ne "" ) {
Log3 $name, 3,
"TadoAPI $name" . ": "
. "GetZoneReadingsById: Error while requesting "
. $param->{url}
. " - $err";
}
elsif ( $data ne "" ) {
Log3 $name, 5, "url " . $param->{url} . " returned: $data";
my $decoded_data = eval { decode_json($data) };
if ($@) {
Log3 $name, 3, "TadoAPI $name" . ": "
. "GetZoneReadingsById: Zone $zoneID decode_json failed, invalid json. error:$@\n";
}
else {
$temperature = sprintf( "%.1f",
$decoded_data->{'sensorDataPoints'}->{'insideTemperature'}
->{'celsius'} );
$humidity = $decoded_data->{'sensorDataPoints'}->{'humidity'}
->{'percentage'};
if ( $decoded_data->{'setting'}->{'power'} eq "OFF" ) {
$desiredTemp = "OFF";
}
else {
$desiredTemp =
$decoded_data->{'setting'}->{'temperature'}->{'celsius'};
}
$currentHeatingPower =
$decoded_data->{'activityDataPoints'}->{'heatingPower'}
->{'percentage'};
$overlay = $decoded_data->{'overlayType'};
if ( !defined $overlay ) { $overlay = "no overlay" }
return ( $temperature, $humidity, $desiredTemp,
$currentHeatingPower, $overlay );
}
}
}
return;
}
sub TadoAPI_GetTadoDevices {
# returns array with zonenames and zone devices
my $hash = shift;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $URL = $QueryURL . qq{/$homeID/zones};
my $CurrentTokenData = TadoAPI_LoadToken($hash);
if ( defined($CurrentTokenData) ) {
my $param = {
url => $URL,
header => {
"Content-Type" => "application/json;charset=UTF-8",
"Authorization" =>
"$CurrentTokenData->{'token_type'} $CurrentTokenData->{'access_token'}"
},
method => 'GET',
timeout => 5,
hash => $hash
};
#Log3 $name, 5, 'Blocking GET: ' . Dumper($param);
Log3 $name, $reqDebug, "TadoAPI $name" . ": " . "Request $URL";
my ( $err, $data ) = HttpUtils_BlockingGet($param);
if ( $err ne "" ) {
Log3 $name, 3,
"TadoAPI $name" . ": "
. "RequestTadoDevices: Error while requesting "
. $param->{url}
. " - $err";
}
elsif ( $data ne "" ) {
Log3 $name, 5, "url " . $param->{url} . " returned: $data";
my $decoded_data = eval { decode_json($data) };
if ($@) {
Log3 $name, 3, "TadoAPI $name" . ": "
. "RequestTadoDevices: decode_json failed, invalid json. error:$@\n";
}
else {
if ( ref($decoded_data) eq 'ARRAY' ) {
my @devices = ();
for my $dev (@$decoded_data) {
push @devices, $dev;
}
return @devices;
}
}
}
}
return;
}
######################################################
# storePW & readPW Code geklaut aus 96_SIP.pm :)
######################################################
sub TadoAPI_storePassword {
my $name = shift;
my $password = shift;
my $index = "TadoAPI_" . $name . "_passwd";
my $key = getUniqueId() . $index;
my $e_pwd = "";
if ( eval { require 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 = shift;
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";
}
if ( defined($password) ) {
if ( eval { require 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;
}
}
1;
=pod
=item device
=item summary integration of the Tado API
=item summary_DE Anbindung der Tado Heizungssteuerung &uuml;ber API
=begin html
<a name="TadoAPI"></a>
<h3>TadoAPI</h3>
<ul>
The TadoAPI module connects your tado devices to FHEM. Most zone readings are shown and desired temperature for a zone can be set.<br>
TadoAPI makes use of the (unofficial) tado api and does NOT rely on any addition local client installed.<br>
Notes:
<ul>
<li>JSON has to be installed on the FHEM host.<br>
Please install the module (e.g. with <code>sudo apt-get install libjson-perl</code>) or the correct method for the underlying platform/system.</li>
</ul>
<a name="TadoAPIdefine"></a>
<b>Define</b>
<ul>
The username and password must match the username and password used on the Tado website.<br>
After successful define, store PASSWORD with <code>set &lt;name&gt; password &lt;your-tado-password&gt;</code>.<br>
Note: Password is encrypted and saved in FHEM uniqueID file. All requests to the API are handeld via oauth2 token.<br>
Examples:
<ul><code>
define &lt;name&gt; TadoAPI mail@example.com [&lt;homeID&gt;]<br>
</code></ul>
<br>
</ul>
<a name="TadoAPIset"></a>
<b>Set</b>
<ul>
<li>
<code>set &lt;name&gt; &lt;tado password&gt;</code><br>
Stores <code>password</code> from tado account encrypted in FHEM.<br>
Without stored password all functions are blocked !<br>
IMPORTANT : if you rename the fhem Device you must set the password again!
</li>
<li>
<code>set &lt;name&gt; update</code><br>
Reloads all information from the tado installation (devices, battery state, geolocation,...).
</li>
<li>
<code>set &lt;name&gt; setZoneOverlay &lt;zoneID&gt; &lt;setting&gt; [&lt;duration&gt;]</code><br>
Setting: remove = delete overlay; 0 = heating power off; &gt; 1 sets desired temperature to given value (overlay)
</li>
<li>
<code>set &lt;name&gt; setAllOverlays &lt;setting&gt;</code><br>
Same as above, but for all zones<br>
Setting: remove = delete overlay; 0 = heating power off; &gt; 1 sets desired temperature to given value (overlay)</li>
<br>
</ul>
<br>
<a name="TadoAPIattr"></a>
<b>Attributes</b>
<ul>
<li>homeID<br>
Home ID that will be used for <b>API </b> querys.
</li>
<li>updateIntervall<br>
Intervall (in seconds) that is used for polling the <b>tado API</b> to update readings.
</li>
<li>showPosData<br>
If set to <b>1</b> readings with relative distance to tado home are shown.
</li>
</ul>
<br>
</ul>
=end html
# Ende der Commandref
=cut