Dateien hochladen nach „“

This commit is contained in:
Philipp Wo 2019-10-17 10:09:00 +00:00
parent 7679d7d2a2
commit 4bb6218aa8
1 changed files with 704 additions and 0 deletions

704
98_TadoAPI.pm Normal file
View File

@ -0,0 +1,704 @@
#!/usr/bin/env perl
#===============================================================================
#
# FILE: 98_TadoAPI.pm
#
# USAGE: Module for FHEM
# Info: Turn $debug on for debugging
#
# REQUIREMENTS: Below modules should be pre-installed.
# HTTP::Request::Common
# HTTP::Headers
# LWP::UserAgent::Paranoid
# Data::Dumper;
# JSON
# BUGS: ---
# NOTES: ---
# AUTHOR: Philipp Wolfmajer
# ORGANIZATION:
# VERSION: 1.0
# CREATED: 04/12/2019 07:55:44 PM
# REVISION: 10/16/2019 02:17:22 PM
#===============================================================================
package main;
use strict;
use warnings;
use utf8;
use FHEM::Meta;
use HTTP::Request::Common qw (POST GET PUT);
use HTTP::Headers;
use LWP::UserAgent::Paranoid;
use JSON;
####DEFAULTS############
my $client_id='public-api-preview';
my $client_secret='4HJGRffVR8xb3XdEUQpjgZ1VplJi6Xgw';
my $scope='home.user';
my $AuthURL = qq{https://auth.tado.com/oauth/token};
my $DataURL = qq{https://my.tado.com/api/v2/me};
my $QueryURL = qq{https://my.tado.com/api/v2/homes};
my $tokenFile = "./temp/TadoAPItoken_module";
my $debug = 0;
my $header = {};
my $data = {};
my $TokenData = {};
my $apiStatus = 1;
#####################################
#Request data for HTTP Request & Response
my $Request=undef;
my $Response=undef;
#Empty variable for LWP User Agent for sending HTTP Request
my $UserAgent = undef;
my %sets = (
"getTemperature" => "noArg",
"refreshToken" => "noArg",
"setGeo" => "on,off"
);
my %gets = (
"getHomeMode" => "noArg",
"getGeo" => "noArg"
);
sub
TadoAPI_Initialize($)
{
my ($hash) = @_;
$hash->{DefFn} = "TadoAPI_Define";
$hash->{InitFn} = "TadoAPI_Init";
$hash->{SetFn} = "TadoAPI_Set";
$hash->{GetFn} = "TadoAPI_Get";
$hash->{AttrList} =
"homeID " .
"mobileID " .
"debug:1,0 " .
$main::readingFnAttributes;
}
sub TadoAPI_Init($$)
{
my ($hash,$args) = @_;
my $u = "wrong syntax: define <name> TadoAPI <username> <password> [<homeID>] [<mobileID>]";
return $u if(int(@$args) < 2);
#TadoAPI_Connect($hash) if($init_done);
return undef;
}
sub TadoAPI_Define($$)
{
my ($hash, $def) = @_;
my @a = split( "[ \t]+", $def );
my $name = shift @a;
my $type = shift @a;
return "Invalid number of arguments: "
. "define <name> TadoAPI <username> <password> [<homeID>] [<mobileID>]"
if ( int(@a) < 2 );
my ( $user, $passwd, $homeID, $mobileID ) = @a;
Log3 $name, 3, "TadoAPI_Define $name: called ";
$hash->{STATE}="defined";
# Initialize the device
return $@ unless ( FHEM::Meta::SetInternals($hash) );
$hash->{TADO_USER} = $user;
$hash->{USER_PASSWORD} = $passwd;
if ( defined($homeID) && $homeID ne "" ) {
$attr{$name}{"homeID"} = $homeID;
}
if ( defined($mobileID) && $mobileID ne "" ) {
$attr{$name}{"mobileID"} = $mobileID;
}
$main::attr{$a[0]}{"homeID"}= $homeID;
$main::attr{$a[0]}{"mobileID"}= $mobileID;
my @args = ($homeID, $mobileID);
if ($main::init_done) {
eval {
# start timer
TadoAPI_Update($hash);
};
return TadoAPI_Catch($@) if $@;
}
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};
#debug
$debug = $attr{$name}{debug};
if(!defined($sets{$cmd})) {
my @cmds = ();
foreach my $key (sort keys %sets) {
push @cmds, $sets{$key} ? $key.":".join(",",$sets{$key}) : $key;
}
return "Unknown argument $a[1], choose one of " . join(" ", @cmds);
}
if( $cmd eq 'setGeo' ) {
if( $value eq "on" ) {
Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)\n";
TadoAPI_SetGeo($hash, 1);
} else {
Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)\n";
TadoAPI_SetGeo($hash, 0);
}
TadoAPI_GetGeo($hash);
return undef;
} elsif( $cmd eq 'refreshToken' ) {
Log3 $name, 3, "TadoAPI: set $name: processing ($cmd)\n";
TadoAPI_Connect($hash);
return undef;
} elsif( $cmd eq 'getTemperature' ) {
Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)" if $debug;
TadoAPI_GetTemperature($hash);
Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n";
return undef;
}
return TadoAPI_Catch($@) if $@;
}
sub TadoAPI_Get(@) {
my ($hash, @a) = @_;
return "Need at least one parameters" if(@a < 2);
my $cmd = $a[1];
my $value = $a[2];
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $mobileID = $attr{$name}{mobileID};
#debug
$debug = $attr{$name}{debug};
if(!defined($gets{$cmd})) {
my @cmds = ();
foreach my $key (sort keys %gets) {
push @cmds, $gets{$key} ? $key.":".join(",",$gets{$key}) : $key;
}
return "Unknown argument $a[1], choose one of " . join(" ", @cmds);
}
if($cmd =~ /\Qget\E/) {
eval {
COMMAND_HANDLER: {
$cmd eq "getGeo" and do {
Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)" if $debug;
TadoAPI_GetGeo($hash);
Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n";
last;
};
$cmd eq "getHomeMode" and do {
Log3 $name, 3, "TadoAPI $name" . ": " . "processing ($cmd)" if $debug;
TadoAPI_GetHomeMode($hash);
Log3 $name, 3, "TadoAPI $name" . ": " . "$cmd finished\n";
last;
};
}
};
return TadoAPI_Catch($@) if $@;
return undef;
}
}
sub TadoAPI_Catch($) {
my $exception = shift;
if ($exception) {
$exception =~ /^(.*)( at.*FHEM.*)$/;
return $1;
}
return undef;
}
sub TadoAPI_Undefine($$) {
my ( $hash, $name ) = @_;
RemoveInternalTimer($hash);
#todo remove tokenfile
return undef;
}
sub TadoAPI_callback($$$){
my ($param, $err, $data) = @_;
my $hash = $param->{hash};
my $name = $hash->{NAME};
if($param->{code} == 401 || $param->{code} == 400){
$apiStatus = 1;
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "API-Status", "online");
readingsEndUpdate( $hash, 1 );
Log3 $name, 3, "TadoAPI $name" . ": " . "API is online. Callback Status: " . $param->{code} if $debug;
}else{
$apiStatus = 0;
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "API-Status", "offline");
readingsEndUpdate( $hash, 1 );
Log3 $name, 3, "TadoAPI $name" . ": " . "API error: apiStatus $apiStatus ($err)" if $debug;
}
return undef;
}
sub TadoAPI_Status(@){
my ($hash) = @_;
my $name = $hash->{NAME};
# test api status
my $param = {
url => $AuthURL,
timeout => 5,
hash => $hash,
method => "GET",
header => "",
callback => \&TadoAPI_callback
};
#test if api is reachable
HttpUtils_NonblockingGet($param);
return undef;
}
sub TadoAPI_Connect(@) {
my ($hash) = @_;
my $name = $hash->{NAME};
my $tokenFileName = $tokenFile."_".$name;
my $tokenLifeTime = $hash->{TOKEN_LIFETIME};
$tokenLifeTime = localtime($tokenLifeTime);
#debug
$debug = $attr{$name}{debug};
#load existing token, or try to refresh old one
Log3 $name, 3, "TadoAPI $name" . ": " . "Loading Token Data from file: $tokenFileName." if $debug;
TadoAPI_Status($hash);
if($apiStatus){
eval {
open(TOKENFILE, '<', $tokenFileName) or die("ERROR: $!");
$TokenData = decode_json(<TOKENFILE>)};
if($@ || $hash->{TOKEN_LIFETIME} < gettimeofday()){
Log3 $name, 3, "TadoAPI $name" . ": " . "Error while loading: $@" if $debug && $@;
Log3 $name, 3, "TadoAPI $name" . ": " . "Token expired, requesting new one" if $debug && $hash->{TOKEN_LIFETIME} < gettimeofday();
TadoAPI_requestNewToken($hash);
}else{
Log3 $name, 3, "TadoAPI $name" . ": " . "Token expires at " . $tokenLifeTime if $debug;
# if token is about to expire, refresh him
if (($hash->{TOKEN_LIFETIME}-60) < gettimeofday()){
Log3 $name, 3, "TadoAPI $name" . ": " . "Token will expire soon, refreshing" if $debug;
TadoAPI_refreshToken($hash);
}
}
close(TOKENFILE);
}
return undef;
}
sub TadoAPI_requestNewToken(@) {
my ($hash) = @_;
my $name = $hash->{NAME};
my $username = $hash->{TADO_USER};
my $password = $hash->{USER_PASSWORD};
my $tokenFileName = $tokenFile."_".$name;
Log3 $name, 3, "TadoAPI $name" . ": " . "Requesting new Token (TadoAPI_requestNewToken)" if $debug;
$data = {
client_id => $client_id,
client_secret => $client_secret,
username => $username,
password => $password,
scope => $scope,
grant_type=>'password'
};
$Request = POST($AuthURL,$data);
$UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,);
$Response = $UserAgent->request($Request);
if($Response->is_success){
$TokenData = decode_json($Response->content);
#write token data in file
open(TOKENFILE,">$tokenFileName") or die("ERROR: $!");
print TOKENFILE $Response->content."\n";
close(TOKENFILE);
# token time management
$hash->{TOKEN_LIFETIME} = gettimeofday() + $TokenData->{'expires_in'};
Log3 $name, 3, "TadoAPI $name" . ": " . "Retrived new authentication token successfully. Valid until " . localtime($hash->{TOKEN_LIFETIME}) if $debug;
#return to apistatus
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "API-Status", "online");
readingsEndUpdate( $hash, 1 );
return 1;
}else{
Log3 $name, 3, "TadoAPI $name" . ": " . "Error in token retrival [Authentication Error]";
print $Response->status_line."\n" if $debug;
#apiStatus down
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "API-Status", "error");
readingsEndUpdate( $hash, 1 );
return 0;
}
return undef;
}
sub TadoAPI_refreshToken(@) {
my ($hash) = @_;
my $name = $hash->{NAME};
my $tokenFileName = $tokenFile."_".$name;
Log3 $name, 3, "TadoAPI $name" . ": " . "calling TadoAPI_refreshToken()" if $debug;
$data = {
client_id => $client_id,
client_secret => $client_secret,
scope => $scope,
grant_type=>'refresh_token',
refresh_token => $TokenData->{'refresh_token'}
};
$Request = POST($AuthURL,$data);
$UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 3,);
$Response = $UserAgent->request($Request);
$header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8");
if($Response->is_success){
Log3 $name, 3, "TadoAPI $name" . ": " . "refreshed authentication token successfully." if $debug;
$TokenData = decode_json($Response->content);
Log3 $name, 3, "TadoAPI $name" . ": " . "writting refreshed Token Data to file $tokenFileName" if $debug;
open(TOKENFILE,">$tokenFileName") or die("ERROR: $!");
print TOKENFILE $Response->content."\n";
close(TOKENFILE);
#Log3 $name, 3, "TadoAPI $name" . ": " . "refreshed Token successful written" if $debug;
return 1;
}else{
Log3 $name, 3, "TadoAPI $name" . ": " . "token expired, requesting new token";
Log3 $name, 3, "TadoAPI $name" . ": " . $Response->status_line if $debug;
Log3 $name, 3, "TadoAPI $name" . ": " . "response code: " . $Response->code if $debug;
#if token is expired and connection up, request new one
if($Response->code == 500){
Log3 $name, 3, "TadoAPI $name" . ": " . "error: not connected - response: 500" if $debug;
$apiStatus = 0;
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "API-Status", "error");
readingsEndUpdate( $hash, 1 );
}
else{
TadoAPI_requestNewToken($hash);
}
return 0;
}
return undef;
}
sub TadoAPI_Update(@){
my ($hash) = @_;
my $name = $hash->{NAME};
Log3 $name, 3, "TadoAPI $name" . ": " . "TadoAPI_Update called" if $debug;
# timer loop
#
my $nextTimer = "none";
# if api online, try again in 5 minutes
if ( $apiStatus ) {
$nextTimer = gettimeofday() + 300;
}
RemoveInternalTimer($hash);
InternalTimer( $nextTimer, "TadoAPI_Update", $hash, 0 );
# update subs
TadoAPI_GetUpdate($hash);
return undef;
}
######################## tado methods ########################
##############################################################
sub TadoAPI_GetUpdate(@){
my ($hash) = @_;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $mobileID = $attr{$name}{mobileID};
my $URL=$QueryURL.qq{/$homeID/zones/1/state};
my $deviceURL=$QueryURL.qq{/$homeID/mobileDevices/$mobileID/settings};
my $setting = 0;
my $temperature = 0;
my $tadoMode = 0;
my $success = 0;
TadoAPI_Connect($hash);
# zone specific updates
if($apiStatus == 1){
$header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}");
$UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,);
$UserAgent->default_headers($header);
$Request = GET($URL);
$Response = $UserAgent->request($Request);
if($Response->is_success){
my $ResponseData = decode_json($Response->content);
$temperature = $ResponseData->{'sensorDataPoints'}->{'insideTemperature'}->{'celsius'};
$tadoMode = $ResponseData->{'tadoMode'};
Log3 $name, 3, "TadoAPI $name" . ": " . "UpdateFn: Actual temperature is: $temperature Celsius and home mode $tadoMode" if $debug;
$success = 1;
}else{
Log3 $name, 3, "TadoAPI $name" . ": " . "UpdateFn: [Authentication Error]". $Response->status_line;
$success = 0;
}
# device specific updates
$Request = GET($deviceURL);
$Response = $UserAgent->request($Request);
if($Response->is_success && $success){
my $ResponseData = decode_json($Response->content);
$setting = $ResponseData->{'geoTrackingEnabled'};
Log3 $name, 3, "TadoAPI $name" . ": " . "UpdateFn: Actual geo setting for $mobileID is: $setting" if $debug;
$success = 1;
}else{
Log3 $name, 3, "TadoAPI $name" . ": " . "UpdateFn: [Authentication Error]";
$success = 0;
}
if ($success){
# readings update
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "Temperatur", $temperature);
readingsBulkUpdate($hash, "Geolocation", $setting);
readingsBulkUpdate($hash, "HomeMode", $tadoMode);
readingsEndUpdate( $hash, 1 );
}
$success = 0;
}
return undef;
}
sub TadoAPI_GetTemperature(@){
my ($hash) = @_;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $URL=$QueryURL.qq{/$homeID/zones/1/state};
my $temperature = 0;
Log3 $name, 3, "TadoAPI $name" . ": " . "homeID: $homeID" if $debug;
Log3 $name, 3, "TadoAPI $name" . ": " . "query-URL: $URL" if $debug;
if($apiStatus == 1){
TadoAPI_Connect($hash);
$header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}");
$UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,);
$UserAgent->default_headers($header);
$Request = GET($URL);
$Response = $UserAgent->request($Request);
if($Response->is_success){
my $ResponseData = decode_json($Response->content);
$temperature = $ResponseData->{'sensorDataPoints'}->{'insideTemperature'}->{'celsius'};
Log3 $name, 3, "TadoAPI $name" . ": " . "Retriving zone 1 state:\n" . $Response->content if $debug;
Log3 $name, 3, "TadoAPI $name" . ": " . "Actual temperature is: $temperature Celsius" if $debug;
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "Temperatur", $temperature);
readingsEndUpdate( $hash, 1 );
return $temperature;
}else{
Log3 $name, 3, "TadoAPI $name" . ": " . "[Authentication Error]". $Response->status_line;
}
}
return undef;
}
sub TadoAPI_GetGeo(@){
my ($hash) = @_;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $mobileID = $attr{$name}{mobileID};
my $URL=$QueryURL.qq{/$homeID/mobileDevices/$mobileID/settings};
my $setting = 0;
Log3 $name, 3, "TadoAPI $name" . ": " . "homeID: $homeID" if $debug;
Log3 $name, 3, "TadoAPI $name" . ": " . "mobID: $mobileID" if $debug;
Log3 $name, 3, "TadoAPI $name" . ": " . "query-URL: $URL" if $debug;
if($apiStatus == 1){
TadoAPI_Connect($hash);
$header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}");
$UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,);
$UserAgent->default_headers($header);
$Request = GET($URL);
$Response = $UserAgent->request($Request);
if($Response->is_success){
my $ResponseData = decode_json($Response->content);
$setting = $ResponseData->{'geoTrackingEnabled'};
#print "\n Retriving State:\n" if $debug;
#print $Response->content."\n\n" if $debug;
Log3 $name, 3, "TadoAPI $name" . ": " . "Actual geo setting for $mobileID is: $setting" if $debug;
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "Geolocation", $setting);
readingsEndUpdate( $hash, 1 );
return $setting;
}else{
Log3 $name, 3, "TadoAPI $name" . ": " . "[Authentication Error in TadoAPI_GetGeo()]";
Log3 $name, 3, "TadoAPI $name" . ": " . "Error: ". $Response->status_line if $debug;
}
}
}
sub TadoAPI_GetHomeMode(@) {
my ($hash) = @_;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $URL=$QueryURL.qq{/$homeID/zones/1/state};
my $tadoMode = 0;
if($apiStatus == 1){
TadoAPI_Connect($hash);
$header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}");
$UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,);
$UserAgent->default_headers($header);
$Request = GET($URL);
$Response = $UserAgent->request($Request);
if($Response->is_success){
my $ResponseData = decode_json($Response->content);
$tadoMode = $ResponseData->{'tadoMode'};
#debug
#{Log 3, ("Retriving Zone 1 State:") if $verbose};
#print $Response->content."\n\n" if $verbose;
Log3 $name, 3, "TadoAPI $name" . ": " . "Actual tado mode is: $tadoMode" if $debug;
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "HomeMode", $tadoMode);
readingsEndUpdate( $hash, 1 );
}else{
Log3 $name, 3, "TadoAPI $name" . ": " . "[Authentication Error]: " . $Response->status_line if $debug;
}
}
return 0;
}
sub TadoAPI_SetGeo(@){
my ($hash, $geo) = @_;
my $name = $hash->{NAME};
my $homeID = $attr{$name}{homeID};
my $mobileID = $attr{$name}{mobileID};
my $URL=$QueryURL.qq{/$homeID/mobileDevices/$mobileID/settings};
Log3 $name, 3, "TadoAPI $name" . ": " . "homeID: $homeID" if $debug;
Log3 $name, 3, "TadoAPI $name" . ": " . "geo: $geo" if $debug;
Log3 $name, 3, "TadoAPI $name" . ": " . "query-URL: $URL" if $debug;
if($apiStatus == 1){
TadoAPI_Connect($hash);
$header = HTTP::Headers->new("Content-Type"=>"application/json;charset=UTF-8","Authorization" => "$TokenData->{'token_type'} $TokenData->{'access_token'}");
$UserAgent = LWP::UserAgent::Paranoid->new(ssl_opts => { verify_hostname => 1 },protocols_allowed => ['https','http'],request_timeout => 5,);
$UserAgent->default_headers($header);
$Request = HTTP::Request->new('PUT',$URL);
$Request->content_type('application/json');
if($geo){
$Request->content('{"geoTrackingEnabled":"true"}');
}else{
$Request->content('{"geoTrackingEnabled":"false"}');
}
$Response = $UserAgent->request($Request);
if($Response->is_success){
print "\n Retriving State:\n" if $debug;
Log3 $name, 3, "TadoAPI $name" . ": " . "Retriving state:\n" . $Response->content if $debug;
Log3 $name, 3, "TadoAPI $name" . ": " . "Set geo setting for $mobileID to: $geo";
}else{
Log3 $name, 3, "TadoAPI $name" . ": " . "Error in setGeo()";
Log3 $name, 3, "TadoAPI $name" . ": " . "Status: " . $Response->status_line if $debug;
}
}
}
1;
# Beginn der Commandref
=pod
=item [device]
=item summary integration of the Tado API
=item summary_DE Anbindung der Tado Heizungssteuerung über API
=begin html
-
=end html
=begin html_DE
-
=end html
# Ende der Commandref
=cut