########################################################################################
#
# MieleAtHome.pm
#
# FHEM module for Miele@home Devices
#
# Christian Hoenig
#
# $Id$
#
########################################################################################
#
# This programm is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# The GNU General Public License can be found at
# http://www.gnu.org/copyleft/gpl.html.
# A copy is found in the textfile GPL.txt and important notices to the license
# from the author is found in LICENSE.txt distributed with these scripts.
#
# This script is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
########################################################################################
package main;
use strict;
use warnings;
use utf8;
use Encode qw(encode_utf8);
use List::Util qw[min max];
use JSON;
my $version = "2.1.1";
my $MAH_hasMimeBase64 = 1;
# DONE:
use constant PROCESS_ACTIONS => {
0x01 => "start", # 1 START
0x02 => "stop", # 2 STOP
0x03 => "pause", # 3 PAUSE
0x04 => "startSuperFreezing", # 4 START SUPERFREEZING
0x05 => "stopSuperFreezing", # 5 STOP SUPERFREEZING
0x06 => "startSuperCooling", # 6 START SUPERCOOLING
0x07 => "stopSuperCooling", # 7 STOP SUPERCOOLING
};
# DONE:
use constant LIGHT_ACTIONS => {
0x01 => "enable", # 1 Enable
0x02 => "disable", # 2 Disable
};
# DONE:
use constant VENTILATION_STEPS => {
0x01 => "Step1", # 1 Step1
0x02 => "Step2", # 2 Step2
0x03 => "Step3", # 3 Step3
0x04 => "Step4", # 4 Step4
};
# DONE:
use constant MODE_ACTIONS => {
0x00 => "normalOperationMode", # 0 Normal operation mode
0x01 => "sabbathMode", # 1 Sabbath mode
};
# DONE:
# DONE:
# DONE:
# DONE:
# TODO:
# TODO:
# TODO:
use constant COUNTRIES => {
"Miele-Deutschland" => "de-DE",
"Miele-Eesti" => "et-EE",
"Miele-Norge" => "no-NO",
"Miele-Serbien" => "sr-RS", # "Miele-Србија" => "sr-RS",
"Miele-Belgie" => "nl-BE", # "Miele-België" => "nl-BE",
"Miele-Suomi" => "fi-FI",
"Miele-Hong-Kong" => "zh-HK",
"Miele-Russland" => "ru-RU", # "Miele-Россия" => "ru-RU",
"Miele-United-Arab-Emirates" => "en-AE",
"Miele-Portugual" => "pt-PT",
"Miele-Bulgarien" => "bg-BG", # "Miele-България" => "bg-BG",
"Miele-Schweiz" => "de-CH",
"Miele-India" => "en-IN",
"Miele-Semi-Pro" => "de-SX",
"Miele-Nihon" => "ja-JP",
"Miele-Danmark" => "da-DK",
"Miele-Hanguk" => "ko-KR",
"Miele-South-Africa" => "en-ZA",
"Miele-Lietuva" => "lt-LT",
"Miele-Chile" => "es-CL",
"Miele-Luxemburg" => "de-LU",
"Miele-Croatia" => "hr-HR",
"Miele-Latvija" => "lv-LV",
"Miele-China" => "zh-CN", # "Miele-Zhōngguó" => "zh-CN",
"Miele-Griechenland" => "el-GR", # "Miele-Ελλάδα" => "el-GR",
"Miele-Italia" => "it-IT",
"Miele-Mexico" => "es-MX", # "Miele-México" => "es-MX",
"Miele-France" => "fr-FR",
"Miele-Malaysia" => "en-MY",
"Miele-New-Zealand" => "en-NZ",
"Miele-Ukraine" => "ru-UA", # "Miele-Україна" => "ru-UA",
"Miele-Magyarorszag" => "hu-HU", # "Miele-Magyarország" => "hu-HU",
"Miele-Espana" => "es-ES", # "Miele-España" => "es-ES",
"Miele-Kasachstan" => "ru-KZ", # "Miele-Казахстан" => "ru-KZ",
"Miele-Sverige" => "sv-SE",
"Miele-Oesterreich" => "de-AT", # "Miele-Österreich" => "de-AT",
"Miele-Australia" => "en-AU",
"Miele-Singapore" => "en-SG",
"Miele-Thailand" => "en-TH",
"Miele-Kypros" => "el-CY",
"Miele-Slovenia" => "sl-SI",
"Miele-Weissrussland" => "ru-BY", # "Miele-Беларуси" => "ru-BY",
"Miele-Czechia" => "cs-CZ",
"Miele-Slovensko" => "sk-SK",
"Miele-UK" => "en-GB",
"Miele-Ireland" => "en-IE",
"Miele-Polska" => "pl-PL",
"Miele-Romania" => "ro-RO", # "Miele-România" => "ro-RO",
"Miele-Canada" => "en-CA",
"Miele-Nederland" => "nl-NL",
"Miele-Tuerkiye" => "tr-TR", # "Miele-Türkiye" => "tr-TR"
"Miele-USA" => "en-US",
};
#------------------------------------------------------------------------------------------------------
# Initialize
#------------------------------------------------------------------------------------------------------
sub MieleAtHome_Initialize($)
{
my ($hash) = @_;
MAH_Log(undef, 5, "called");
eval "use MIME::Base64";
$MAH_hasMimeBase64 = 0 if($@);
$hash->{DefFn} = "MAH_DefFn";
$hash->{UndefFn} = "MAH_UndefFn";
$hash->{DeleteFn} = "MAH_DeleteFn";
$hash->{AttrFn} = "MAH_AttrFn";
$hash->{SetFn} = "MAH_SetFn";
$hash->{GetFn} = "MAH_GetFn";
$hash->{RenameFn} = "MAH_RenameFn";
$hash->{AttrList} = "";
$hash->{AttrList} .= "api:poll,event ";
$hash->{AttrList} .= "clientId ";
$hash->{AttrList} .= "disable:1 ";
$hash->{AttrList} .= "login ";
$hash->{AttrList} .= "lang:de,en ";
$hash->{AttrList} .= "country:" . join(",", keys %{COUNTRIES()}) . " ";
$hash->{AttrList} .= $readingFnAttributes;
# maintenance
foreach my $d (sort keys %{$modules{MieleAtHome}{defptr}}) {
my $hash = $modules{MieleAtHome}{defptr}{$d};
# update version in devices
$hash->{VERSION} = $version;
# rename IODev -> IODevName (0.12.0)
if (defined($hash->{IODev})) {
$hash->{IODevName} = $hash->{IODev};
delete($hash->{IODev});
}
}
}
#------------------------------------------------------------------------------------------------------
# Define
#------------------------------------------------------------------------------------------------------
sub MAH_DefFn($$)
{
my ( $hash, $def ) = @_;
my @a = split( "[ \t]+", $def );
splice( @a, 1, 1 );
# check syntax
my $pCount = int(@a);
if ($pCount < 1 || $pCount > 3) {
return "Wrong syntax: use define MieleAtHome [deviceId] [interval]";
}
my $name = shift(@a);
my $deviceId = shift(@a);
my $interval = shift(@a);
MAH_Log($hash, 5, "called");
my $ioDevName = "";
if ($deviceId && $deviceId =~ /([0-9]+)+@(.+)/) {
$deviceId = $1;
$ioDevName = $2;
}
if ($deviceId) { $hash->{DEVICE_ID} = $deviceId; }
else { delete($hash->{DEVICE_ID}); }
if ($ioDevName) { $hash->{IODevName} = $ioDevName; }
else { delete($hash->{IODevName}); }
if ($interval) { $hash->{INTERVAL} = $interval; }
elsif ($deviceId) { $hash->{INTERVAL} = 120; } # default: 120
else { delete($hash->{INTERVAL}); }
$hash->{VERSION} = $version;
$hash->{HAS_MimeBase64} = $MAH_hasMimeBase64;
MAH_restoreOAuth2Credentials($hash);
$attr{$name}{room} = "MieleAtHome" if(!defined($attr{$name}{room}));
$attr{$name}{devStateIcon} = ".*:noIcon" if(!defined($attr{$name}{devStateIcon}));
$modules{MieleAtHome}{defptr}{"mah_".$name} = $hash;
if (defined($deviceId)) {
# check if $deviceId exists already
my $d = $modules{MieleAtHome}{defptr}{"deviceid_".$deviceId};
if (defined($d) && $d->{NAME} ne $name) {
$hash->{STATE} = 'Error';
readingsSingleUpdate($hash, "lastError", "MAH device with DeviceId $deviceId already defined as $d->{NAME}.", 1);
$hash->{DUPLICATE_INSTANCE} = "1";
return;
}
# remember our deviceId
$modules{MieleAtHome}{defptr}{"deviceid_".$deviceId} = $hash;
MAH_Log($hash, 4, "finished define with deviceId: $deviceId");
}
fhem("deletereading $name lastError");
if (MAH_isDisabled($hash)) {
readingsSingleUpdate( $hash, "state", "disabled", 1 );
return undef;
}
$hash->{STATE} = 'Initialized';
if (defined($deviceId)) {
# this will call MAH_refreshAccessToken itself if required
InternalTimer(gettimeofday()+($init_done ? 0 : 10), "MAH_start", $hash);
} else {
InternalTimer(gettimeofday()+($init_done ? 0 : 10), "MAH_refreshAccessToken", $hash);
}
# if MAH_getAccessToken returns "", it will request a new token on its own
# if (MAH_getAccessToken($hash) ne "") {
# InternalTimer(gettimeofday()+0, "MAH_startPoll", $hash);
# }
return undef;
}
#------------------------------------------------------------------------------------------------------
# Undefine
#------------------------------------------------------------------------------------------------------
sub MAH_UndefFn($$)
{
my ($hash, $name) = @_;
RemoveInternalTimer($hash);
MAH_closeEventSource($hash);
delete($modules{MieleAtHome}{defptr}{"mah_".$name});
my $deviceId = $hash->{DEVICE_ID};
if (defined($deviceId)) {
MAH_Log($hash, 4, "undefined with deviceId: $deviceId");
delete($modules{MieleAtHome}{defptr}{"deviceid_".$deviceId});
}
MAH_Log($hash, 4, "undefined");
return undef;
}
#------------------------------------------------------------------------------------------------------
# Delete
#------------------------------------------------------------------------------------------------------
sub MAH_DeleteFn($$)
{
my ($hash, $name) = @_;
MAH_deletePassword($hash);
MAH_deleteClientSecret($hash);
MAH_deleteOAuth2Credentials($hash);
MAH_Log($hash, 4, "deleted");
return undef;
}
#------------------------------------------------------------------------------------------------------
# AttrFn
#------------------------------------------------------------------------------------------------------
sub MAH_AttrFn(@)
{
my ($cmd, $name, $attrName, $attrVal) = @_;
my $hash = $defs{$name};
######################
#### disable #########
if ($attrName eq "disable") {
if ($cmd eq "set" && $attrVal eq "1") {
readingsSingleUpdate ( $hash, "state", "disabled", 1 );
MAH_Log($hash, 3, "disabled");
InternalTimer(gettimeofday()+0, "MAH_stopPoll", $hash);
InternalTimer(gettimeofday()+0, "MAH_closeEventSource", $hash);
}
elsif ($cmd eq "del") {
readingsSingleUpdate ( $hash, "state", "active", 1 );
MAH_Log($hash, 3, "enabled");
InternalTimer(gettimeofday()+0, "MAH_start", $hash);
}
}
###############
#### api ######
if ($attrName eq "api") {
if ($cmd eq "set") {
return "Invalid value for attribute $attrName" if ($attrVal ne "poll" && $attrVal ne "event");
}
InternalTimer(gettimeofday()+0, "MAH_start", $hash) if ($init_done);
}
#################
#### lang ######
if ($attrName eq "lang") {
if ($cmd eq "set") {
return "Invalid value for attribute $attrName" if ($attrVal ne "de" && $attrVal ne "en");
}
}
#################
#### login ######
if ($attrName eq "login") {
if ($cmd eq "set") {
return "Invalid value for attribute $attrName" if (!$attrVal);
#MAH_Log($hash, 1, "setting 'login' calls 'MAH_refreshAccessToken' ($init_done)");
InternalTimer(gettimeofday()+0, "MAH_refreshAccessToken", $hash) if ($init_done);
}
}
####################
#### clientId ######
if ($attrName eq "clientId") {
if ($cmd eq "set") {
return "Invalid value for attribute $attrName" if (!$attrVal);
#MAH_Log($hash, 1, "setting 'clientId' calls 'MAH_refreshAccessToken' ($init_done)");
InternalTimer(gettimeofday()+0, "MAH_refreshAccessToken", $hash) if ($init_done);
}
}
####################
#### country ######
if ($attrName eq "country") {
if ($cmd eq "set") {
if (!$attrVal ||
(!defined(COUNTRIES->{$attrVal}) &&
!grep { $_ eq $attrVal } values %{COUNTRIES()})) {
return "Invalid value for attribute $attrName"
}
#InternalTimer(gettimeofday()+0, "MAH_refreshAccessToken", $hash) if ($init_done);
}
}
return undef;
}
#------------------------------------------------------------------------------------------------------
# SetFn
#------------------------------------------------------------------------------------------------------
sub MAH_SetFn($$@)
{
my ($hash, $name, @aa) = @_;
my ($cmd, @args) = @aa;
# password and clientSecret are allowed even when 'disabled' (but only if we don't use an IODev)
my $list = "";
$list .= "password " if (!defined($hash->{IODevName}));
$list .= "clientSecret " if (!defined($hash->{IODevName}));
if ($cmd eq "?") {
return "Unknown argument $cmd, choose one of $list" if (MAH_isDisabled($hash));
}
if( $cmd eq 'clientSecret' ) {
return "usage: callback " if(@args != 1);
MAH_saveClientSecret($hash, $args[0]);
InternalTimer(gettimeofday()+0, "MAH_refreshAccessToken", $hash);
return undef;
}
elsif( $cmd eq 'password' ) {
return "usage: password " if(@args != 1);
MAH_savePassword($hash, $args[0]);
InternalTimer(gettimeofday()+0, "MAH_refreshAccessToken", $hash);
return undef;
}
elsif( $cmd eq 'autocreate' ) {
return "autocreate needs a valid ACCESS_TOKEN, please try again" if (MAH_getAccessToken($hash) eq "");
return "use $cmd without arguments" if(@args != 0);
InternalTimer(gettimeofday()+0, "MAH_autocreate", $hash);
return undef;
}
elsif( $cmd eq 'on' || $cmd eq 'off' ) {
return "use $cmd without arguments" if(@args != 0);
return MAH_setPower($hash, $cmd)
}
elsif( $cmd eq 'start' || $cmd eq 'stop' || $cmd eq 'pause' ||
$cmd eq 'startSuperFreezing' || $cmd eq 'stopSuperFreezing' ||
$cmd eq 'startSuperCooling' || $cmd eq 'stopSuperCooling' ) {
return "use $cmd without arguments" if(@args != 0);
return MAH_setProcessAction($hash, $cmd)
}
elsif( $cmd eq 'startTime') {
return "usage: startTime " if(@args != 1);
return MAH_setStartTime($hash, $args[0]);
}
elsif( $cmd eq 'update' ) {
return "use $cmd without arguments" if(@args != 0);
InternalTimer(gettimeofday()+0, "MAH_updateValues", $hash);
return undef;
}
elsif( $cmd eq 'ventilationStep') {
return "usage: ventilationStep " if(@args != 1);
return MAH_setVentilationStep($hash, $args[0]);
}
elsif( $cmd eq 'light') {
return "usage: light enable|disable" if(@args != 1);
return MAH_setLight($hash, $args[0]);
}
elsif( $cmd eq 'mode') {
return "usage: mode " if(@args != 1);
return MAH_setMode($hash, $args[0]);
}
elsif( $cmd =~ /targetTemperature_zone([0-9]+)/) {
return "usage: targetTemperature_zone${1} " if(@args != 1);
return MAH_setTargetTemperature($hash, $1, $args[0]);
}
else
{
$list .= "autocreate:noArg " if (!defined($hash->{IODevName}));
$list .= "update:noArg " if (defined($hash->{DEVICE_ID}) && !defined($hash->{EventSource}));
$list .= "on:noArg " if (defined($hash->{DEVICE_ID}) && ReadingsNum($name, "actions_powerOn", 0) == 1);
$list .= "off:noArg " if (defined($hash->{DEVICE_ID}) && ReadingsNum($name, "actions_powerOff", 0) == 1);
$list .= "startTime " if (defined($hash->{DEVICE_ID}) && ReadingsNum($name, "actions_startTime", 0) == 1);
# process actions
my @processActionIds = split(/,/, ReadingsVal($name, "actions_processAction", ""));
foreach my $processActionId (@processActionIds) {
if (defined PROCESS_ACTIONS->{$processActionId}) {
$list .= PROCESS_ACTIONS->{$processActionId} . ":noArg ";
}
}
# light
my $lightCmds = MAH_getAvailableCommands($hash, "actions_light", LIGHT_ACTIONS());
$list .= "light:${lightCmds} " if ($lightCmds ne "");
# ventilation steps
my $ventilationStepCmds = MAH_getAvailableCommands($hash, "actions_ventilationStep", VENTILATION_STEPS());
$list .= "ventilationStep:${ventilationStepCmds} " if ($ventilationStepCmds ne "");
# modes
my $modeCmds = MAH_getAvailableCommands($hash, "actions_modes", MODE_ACTIONS());
$list .= "mode:${modeCmds} " if ($modeCmds ne "");
# target temperatures
my @availableTargetTemperatures = split(/ /, ReadingsVal($name, "actions_targetTemperature", ""));
foreach my $targetTemperature (@availableTargetTemperatures) {
if ($targetTemperature =~ /^([0-9]+)\[(-?[0-9]+),(-?[0-9]+)\]$/) {
$list .= "targetTemperature_zone$1:slider,$2,1,$3 "
}
}
return "Unknown argument $cmd, choose one of $list";
}
}
#------------------------------------------------------------------------------------------------------
# returns the strings of the commands from const ACTIONS that are currently available (via `actions_...`)
#------------------------------------------------------------------------------------------------------
sub MAH_getAvailableCommands($$$)
{
my ($hash, $action_reading, $ACTIONS) = @_;
my $name = $hash->{NAME};
my $cmds = "";
my @ids = split(/,/, ReadingsVal($name, $action_reading, ""));
foreach my $id (@ids) {
if (defined $ACTIONS->{$id}) {
$cmds .= $ACTIONS->{$id} . ",";
}
}
chop($cmds); # remove trailing ','
return $cmds;
}
#------------------------------------------------------------------------------------------------------
# SetFn
#------------------------------------------------------------------------------------------------------
sub MAH_GetFn($$@)
{
my ($hash, $name, $opt, @args ) = @_;
my $list = "";
if ($opt eq "?") {
return "Unknown argument $opt, choose one of $list" if (MAH_isDisabled($hash));
}
if( $opt eq 'listDevices' ) {
return "listDevices needs a valid ACCESS_TOKEN, please try again" if (MAH_getAccessToken($hash) eq "");
my $devices = MAH_blockingGetAllDevicesRequest($hash);
if(ref($devices) ne 'ARRAY') {
readingsSingleUpdate($hash, "lastError", "listDevices failed: $devices", 1);
return;
}
my $retval;
for my $d (@{$devices}) {
$retval .= sprintf("%s (%s)", @{$d}[0], @{$d}[1]);
}
return $retval;
}
else
{
# these are only allowed when MAH is not 'disabled'
$list .= "listDevices:noArg " if (!defined($hash->{IODevName}) && !MAH_isDisabled($hash));
return "Unknown argument $opt, choose one of $list";
}
}
#------------------------------------------------------------------------------------------------------
# MAH_RenameFn
#------------------------------------------------------------------------------------------------------
sub MAH_RenameFn($$)
{
my ($newName, $oldName) = @_;
return unless (defined($defs{$newName}));
my $newHash = $defs{$newName};
# rename mah_-reference
if (defined($modules{MieleAtHome}{defptr}{"mah_".$oldName})) {
$modules{MieleAtHome}{defptr}{"mah_".$newName} = $newHash;
delete($modules{MieleAtHome}{defptr}{"mah_".$oldName});
}
MAH_renameClientSecret($newHash, $oldName, $newName);
MAH_renamePassword($newHash, $oldName, $newName);
MAH_renameOAuth2Credentials($newHash, $oldName, $newName);
}
#------------------------------------------------------------------------------------------------------
# start either poll- or event-based requests
#------------------------------------------------------------------------------------------------------
sub MAH_start($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
return undef if (MAH_isDisabled($hash));
return undef unless (defined($hash->{DEVICE_ID}));
return undef unless (MAH_hasLoginCredentials($hash));
my $api = AttrVal($name, "api", "poll");
MAH_Log($hash, 3, "starting $api");
if ($api eq "poll") {
MAH_closeEventSource($hash);
MAH_startPoll($hash);
} elsif ($api eq "event") {
MAH_stopPoll($hash);
MAH_openEventSource($hash);
}
}
#------------------------------------------------------------------------------------------------------
# stop polling the API
#------------------------------------------------------------------------------------------------------
sub MAH_stopPoll($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
RemoveInternalTimer($hash, "MAH_startPoll");
}
#------------------------------------------------------------------------------------------------------
# start polling the API
#------------------------------------------------------------------------------------------------------
sub MAH_startPoll($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
MAH_stopPoll($hash);
return undef if (MAH_isDisabled($hash));
return undef unless (defined($hash->{DEVICE_ID}));
return undef unless (MAH_hasLoginCredentials($hash));
my $interval = $hash->{INTERVAL};
# force interval of 60s while != Off
$interval = min($interval, 60) if ReadingsNum($name, "statusRaw", 1) != 1; # != Off
InternalTimer(gettimeofday()+$interval, "MAH_startPoll", $hash) if (defined($interval));
# MAH_getAccessToken will request a new one, if there is none
if (MAH_getAccessToken($hash) eq "") {
return;
}
MAH_updateValues($hash);
}
sub MAH_updateValues($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
MAH_sendGetDeviceIdentAndState($hash);
MAH_sendGetDeviceActionsRequest($hash);
}
#------------------------------------------------------------------------------------------------------
# MAH_closeEventSource
#------------------------------------------------------------------------------------------------------
sub MAH_closeEventSource($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
RemoveInternalTimer($hash, "MAH_eventSourceKeepAlive");
return if(!defined($hash->{helper}{HTTP_CONNECTION}));
HttpUtils_Close( $hash->{helper}{HTTP_CONNECTION} );
delete $hash->{helper}{HTTP_CONNECTION};
MAH_updateEventSourceReadings($hash, undef, undef, undef)
}
#------------------------------------------------------------------------------------------------------
# request values via event-api
#------------------------------------------------------------------------------------------------------
sub MAH_openEventSource($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
MAH_closeEventSource($hash);
return undef if (MAH_isDisabled($hash));
return undef unless (defined($hash->{DEVICE_ID}));
return undef unless (MAH_hasLoginCredentials($hash));
my $deviceId = $hash->{DEVICE_ID};
if ($deviceId eq "") {
return "Please set deviceId first";
}
# after a new token is requested, MAH_start() is called again
my $token = MAH_getAccessToken($hash);
if ($token eq "") {
return "Please authenticate first";
}
MAH_updateEventSourceReadings($hash, "connecting", TimeNow(), undef);
my $lang = AttrVal($name, "lang", "en");
my $params = {
# loglevel => 1,
url => "https://api.mcs3.miele.com/v1/devices/${deviceId}/events",
httpversion => '1.1',
method => 'GET',
timeout => 60, # ping interval from Miele seems to be 20s
incrementalTimeout => 1,
noshutdown => 1,
keepalive => 1,
header => { "Accept" => "text/event-stream",
"Accept-Language" => "${lang}",
"Authorization" => "Bearer " . $token },
hash => $hash,
callback => \&MAH_eventSourceDispatch,
};
map { $hash->{helper}{HTTP_CONNECTION}{$_} = $params->{$_} } keys %{$params};
HttpUtils_NonblockingGet( $hash->{helper}{HTTP_CONNECTION} );
# start keepalive
MAH_eventSourceKeepAlive($hash);
}
#------------------------------------------------------------------------------------------------------
sub MAH_eventSourceDispatch($$$)
{
my ($param, $err, $data) = @_;
my $hash = $param->{hash};
my $name = $hash->{NAME};
MAH_LogReply($hash, $param, $err, $data);
if ($err) {
MAH_updateEventSourceReadings($hash, "error: ${err}", undef, undef);
MAH_Log($hash, 1, "Error: $err");
# MAH_openEventSource() will close first
MAH_openEventSource($hash);
return undef;
}
MAH_updateEventSourceReadings($hash, "connected", undef, TimeNow());
# FORMAT: (https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format)
# event: ping
# data: ping
#while ($data ne "" && $data =~ m/event:\s*(.+)(?:\r?\n)?data:\s*(.+)(?:\r?\n)?(.*)/) {
my $event = "";
my $eventData = "";
while($data =~ m/([^:]*):\s*(.+)(?:\r?\n)?(.*)/) {
my $key = $1;
my $value = $2;
$data = $3;
$event = $2 if ($key eq "event");
$eventData = $2 if ($key eq "data");
next unless ($event ne "" && $eventData ne "");
MAH_Log($hash, 5, "$event >> $eventData");
# Attention
# currently the /all/-request replies with "devices" and "actions" (both with the indirection {"1234":})
# while the /1234/-request replys replies wwith "device[]" and "action[s]" ("device" without- and "actions" with the indirection {"1234":})
# but MAH_updateDeviceReadings() and MAH_updateActionReadings() remove the indirections if needed
if ($event eq "device" || $event eq "devices") {
MAH_updateDeviceReadings($hash, $eventData);
} elsif ($event eq "action" || $event eq "actions") {
MAH_updateActionReadings($hash, $eventData);
}
$event = "";
$eventData = "";
}
}
#------------------------------------------------------------------------------------------------------
# MAH_eventSourceKeepAlive
#------------------------------------------------------------------------------------------------------
sub MAH_eventSourceKeepAlive($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
my $api = AttrVal($name, "api", "poll");
return unless ($api eq "event");
InternalTimer(gettimeofday()+30, "MAH_eventSourceKeepAlive", $hash);
my $now = time();
my $timeout = 180;
# if we received an update within last 10s, all is fine ...
if (defined ($hash->{EventSourceLastUpdate})) {
my $lastUpdate = time_str2num($hash->{EventSourceLastUpdate});
if ($now - $lastUpdate <= $timeout) {
return;
}
}
MAH_Log($hash, 5, "EventSourceLastUpdate is > 10s ...");
# if we started withing the last 10s, all is fine, too ...
if (defined ($hash->{EventSourceLastConnect})) {
my $lastConnect = time_str2num($hash->{EventSourceLastConnect});
if ($now - $lastConnect <= $timeout) {
return;
}
}
MAH_Log($hash, 5, "EventSourceLastConnect is > $[timeout}s ...");
# ... otherwise ... timeout
MAH_Log($hash, 4, "timout in event stream, reconnecting...");
MAH_updateEventSourceReadings($hash, "timeout", undef, undef);
MAH_openEventSource($hash);
}
#------------------------------------------------------------------------------------------------------
# MAH_updateEventSourceReadings
#------------------------------------------------------------------------------------------------------
sub MAH_updateEventSourceReadings($$$$)
{
my ($hash, $status, $lastConnect, $lastUpdate) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
if (!$status && !$lastConnect && !$lastUpdate) {
delete($hash->{EventSource});
delete($hash->{EventSourceLastUpdate});
delete($hash->{EventSourceLastConnect});
fhem("deletereading $name _EventSource.*");
return;
}
readingsBeginUpdate($hash);
if ($status) {
$hash->{EventSource} = $status;
readingsBulkUpdate($hash, "_EventSource", $status);
}
if ($lastConnect) {
$hash->{EventSourceLastConnect} = $lastConnect;
readingsBulkUpdate($hash, "_EventSourceLastConnect", $lastConnect);
}
if ($lastUpdate) {
$hash->{EventSourceLastUpdate} = $lastUpdate;
readingsBulkUpdate($hash, "_EventSourceLastUpdate", $lastUpdate);
}
readingsEndUpdate($hash, 1);
}
#------------------------------------------------------------------------------------------------------
# MAH_refreshAccessToken
#------------------------------------------------------------------------------------------------------
sub MAH_refreshAccessToken($); # workaround for perl warning
sub MAH_refreshAccessToken($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
# let the IODev update the token
my $iohash = MAH_getIODevHash($hash);
return MAH_refreshAccessToken($iohash) if (defined($iohash));
# only refresh the token once
if (defined($hash->{TOKEN_REFRESH_IN_PROGRESS}) && $hash->{TOKEN_REFRESH_IN_PROGRESS} == 1) {
MAH_Log($hash, 4, "token refresh already in progress, skipping");
return;
}
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 1;
if (MAH_getAccessTokenPrivate($hash) ne "" && MAH_getRemainingTokenLifetime($hash) > 24 * 60 * 60) {
MAH_Log($hash, 4, "access-token still valid, skipping refresh. Call '{delete(\$defs{$name}{OAUTH2_ACCESS_TOKEN})}' in command bar to force refresh");
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
if (!MAH_hasLoginCredentials($hash)) {
readingsSingleUpdate($hash, "lastError", "please set login, password, clientId and clientSecret", 1);
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
} else {
fhem("deletereading $name lastError");
}
my $refreshToken = MAH_getRefreshTokenPrivate($hash);
if ($refreshToken ne "" && MAH_getRemainingTokenLifetime($hash) > 0) {
MAH_Log($hash, 4, "already have a refresh-token, using this for token-refresh");
MAH_doThirdpartyTokenRequest($hash, "", $refreshToken);
} else {
MAH_doThirdpartyLoginRequest($hash);
}
}
#------------------------------------------------------------------------------------------------------
sub MAH_doThirdpartyLoginRequest($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
my $clientId = MAH_getClientId($hash);
if (!defined($clientId)) {
readingsSingleUpdate($hash, "lastError", "clientId missing", 1);
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
# Step 1: Authorization
my $url = "https://api.mcs3.miele.com/thirdparty/login/"
. "?response_type=code"
. "&state=login"
. "&client_id=" . urlEncode($clientId)
. "&scope="
. "&redirect_uri=https%3A%2F%2Fapi.mcs3.miele.com%2Fthirdparty%2Flogin%2F";
my ($err, $reply) = HttpUtils_NonblockingGet({
url => $url,
timeout => 5,
hash => $hash,
method => "GET",
callback => \&MAH_onThirdpartyLoginReply,
});
}
sub MAH_onThirdpartyLoginReply($$$)
{
my ($param, $err, $data) = @_;
my $hash = $param->{hash};
my $name = $hash->{NAME};
MAH_LogReply($hash, $param, $err, $data);
if ($err) {
MAH_Log($hash, 3, "Error: $err");
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return $err;
}
MAH_doOauthLoginRequest($hash);
}
#------------------------------------------------------------------------------------------------------
sub MAH_doOauthLoginRequest($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
my $login = MAH_getLogin($hash);
if (!defined($login)) {
readingsSingleUpdate($hash, "lastError", "login missing", 1);
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
my $password = MAH_getPassword($hash);
if (!defined($password)) {
readingsSingleUpdate($hash, "lastError", "password missing", 1);
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
my $clientId = AttrVal($name, "clientId", "");
if (!defined($clientId)) {
readingsSingleUpdate($hash, "lastError", "clientId missing", 1);
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
my $country = COUNTRIES->{AttrVal($name, "country", "Miele-Deutschland")};
MAH_Log($hash, 5, "country for /oauth/auth is $country");
# Step 2: oauth
my $url = "https://api.mcs3.miele.com/oauth/auth";
my $data = "email=" . urlEncode($login)
. "&password=" . urlEncode($password)
. "&state=login"
. "&response_type=code"
. "&client_id=" . urlEncode($clientId)
. "&vgInformationSelector=$country"
. "&redirect_uri=https%3A%2F%2Fapi.mcs3.miele.com%2Fthirdparty%2Flogin%2F";
my ($err, $reply) = HttpUtils_NonblockingGet({
url => $url,
data => $data,
timeout => 5,
hash => $hash,
method => "POST",
ignoreredirects => 1,
callback => \&MAH_onOauthLoginReply,
});
}
sub MAH_onOauthLoginReply($$$)
{
my ($param, $err, $data) = @_;
my $hash = $param->{hash};
my $name = $hash->{NAME};
MAH_LogReply($hash, $param, $err, $data);
if ($err) {
MAH_Log($hash, 3, "Error: $err");
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
my $code = "";
my $headers = $param->{httpheader};
if ($headers =~ /(?s)code=([A-Z]{2}_[0-9a-f]+)/) {
MAH_Log($hash, 5, "Bearer found in headers");
$code = $1;
}
if ($code eq "") {
$code = MAH_scrapeGrantAccessPage($hash, $data);
if ($code ne "") {
MAH_Log($hash, 5, "Bearer found in HTML");
}
}
if ($code eq "") {
MAH_Log($hash, 2, "Error: Bearer code not found, giving up");
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
MAH_doThirdpartyTokenRequest($hash, $code, "");
}
sub MAH_scrapeGrantAccessPage($$)
{
my ($hash, $data) = (@_);
#
if ($data !~ /name="code" value="([^"]+)"/) {
MAH_Log($hash, 5, "code not found");
return "";
}
my $code = $1;
MAH_Log($hash, 2, "code found: $code");
# check if it looks like the right page (this could be removed!)
if (index($data, 'method="get" action="https://api.mcs3.miele.com/thirdparty/login/"') == -1) {
MAH_Log($hash, 2, "get-action not found");
return "";
}
return $code;
}
#------------------------------------------------------------------------------------------------------
# either use 2nd or 3rd parameter:
# 2nd: do authorization_code
# 3rd: do refresh_token
sub MAH_doThirdpartyTokenRequest($$$)
{
my ($hash, $bearerCode, $refreshToken) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
my $clientId = AttrVal($name, "clientId", "");
if ($clientId eq "") {
readingsSingleUpdate($hash, "lastError", "clientId missing", 1);
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
my $clientSecret = MAH_getClientSecret($hash);
if ($clientSecret eq "") {
readingsSingleUpdate($hash, "lastError", "clientSecret missing", 1);
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
# Step 3: token
my $url = "https://api.mcs3.miele.com/thirdparty/token/";
my $data = "client_id=" . urlEncode($clientId)
. "&client_secret=" . urlEncode($clientSecret);
if ($bearerCode ne "") {
$data .= "&grant_type=authorization_code"
. "&code=" . urlEncode($bearerCode)
. "&redirect_uri=https%3A%2F%2Fapi.mcs3.miele.com%2Fthirdparty%2Flogin%2F";
} elsif ($refreshToken ne "") {
$data .= "&grant_type=refresh_token"
. "&refresh_token=" . urlEncode($refreshToken);
} else {
MAH_Log($hash, 1, "ERROR: called with neither bearerCode nor refreshToken, this is a bug. plz report!");
return;
}
if ($bearerCode ne "") {
readingsSingleUpdate($hash, "tokenRefreshCount_withBearer", ReadingsNum($name, "tokenRefreshCount_withBearer", 0) + 1, 1);
} else {
readingsSingleUpdate($hash, "tokenRefreshCount_withRefreshToken", ReadingsNum($name, "tokenRefreshCount_withRefreshToken", 0) + 1, 1);
}
my ($err, $reply) = HttpUtils_NonblockingGet({
url => $url,
data => $data,
timeout => 5,
hash => $hash,
method => "POST",
ignoreredirects => 1,
callback => \&MAH_onThirdpartyTokenReply,
});
}
sub MAH_onThirdpartyTokenReply($$$)
{
my ($param, $err, $data) = @_;
my $hash = $param->{hash};
my $name = $hash->{NAME};
MAH_LogReply($hash, $param, $err, $data);
if ($err) {
MAH_Log($hash, 3, "Error: $err");
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
if ($param->{code} != 200) {
MAH_Log($hash, 3, "Error: code != 200: $param->{code}");
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
my $json = eval{decode_json($data)};
if ($@) {
MAH_Log($hash, 3, "JSON error while request: $@");
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
if (ref($json) ne "HASH") {
MAH_Log($hash, 3, "got wrong message for $name: $json");
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
return;
}
no strict "refs";
$hash->{OAUTH2_ACCESS_TOKEN} = $json->{access_token};
$hash->{OAUTH2_REFRESH_TOKEN} = $json->{refresh_token};
$hash->{OAUTH2_EXPIRES_IN} = $json->{expires_in};
$hash->{OAUTH2_EXPIRES_AT} = POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime(time + $json->{expires_in}));
use strict "refs";
# store in key/value so that they survive restart
MAH_saveOAuth2Credentials($hash);
# success
$hash->{TOKEN_REFRESH_IN_PROGRESS} = 0;
if (MAH_getAccessToken($hash) ne "") {
InternalTimer(gettimeofday()+0, "MAH_start", $hash);
}
}
#------------------------------------------------------------------------------------------------------
# MAH_blockingGetAllDevicesRequest
#------------------------------------------------------------------------------------------------------
sub MAH_blockingGetAllDevicesRequest($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $token = MAH_getAccessToken($hash);
if ($token eq "") {
return "Please authenticate first";
}
my $lang = AttrVal($name, "lang", "en");
my $url = "https://api.mcs3.miele.com/v1/devices/?language=${lang}";
my $header = { "accept" => "application/json; charset=utf-8",
"Authorization" => "Bearer " . $token };
my ($err, $data) = HttpUtils_BlockingGet({
url => $url,
header => $header,
timeout => 5,
hash => $hash,
method => "GET",
});
MAH_LogReply($hash, undef, $err, $data);
if ($err) {
MAH_Log($hash, 3, "Error: $err");
return $err;
}
my $decoded = eval{decode_json($data)};
if ($@) {
MAH_Log($hash, 3, "JSON error while request: $@");
return;
}
if (ref($decoded) ne "HASH") {
MAH_Log($hash, 3, "got wrong message for $name: $decoded");
return;
}
no strict "refs";
my @retval;
foreach my $id (keys %{$decoded}) {
push(@retval, [$id, $decoded->{$id}->{ident}->{type}->{value_localized}]);
}
use strict "refs";
return \@retval;
}
#------------------------------------------------------------------------------------------------------
# MAH_sendGetDeviceIdentAndState
#------------------------------------------------------------------------------------------------------
sub MAH_sendGetDeviceIdentAndState($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $deviceId = $hash->{DEVICE_ID};
if ($deviceId eq "") {
return "Please set deviceId first";
}
my $token = MAH_getAccessToken($hash);
if ($token eq "") {
return "Please authenticate first";
}
my $lang = AttrVal($name, "lang", "en");
my $url = "https://api.mcs3.miele.com/v1/devices/${deviceId}?language=${lang}";
my $header = { "accept" => "application/json; charset=utf-8",
"Authorization" => "Bearer " . $token };
my ($err, $data) = HttpUtils_NonblockingGet({
url => $url,
header => $header,
timeout => 5,
hash => $hash,
method => "GET",
callback => \&MAH_onGetDeviceIdentAndStateReply,
});
}
sub MAH_onGetDeviceIdentAndStateReply($$$)
{
my ($param, $err, $data) = @_;
my $hash = $param->{hash};
my $name = $hash->{NAME};
MAH_LogReply($hash, $param, $err, $data);
if ($err) {
MAH_Log($hash, 3, "Error: $err");
return $err;
}
if ($param->{code} != 200) {
MAH_Log($hash, 3, "Error: code != 200: $param->{code}");
return "invalid status code: " . $param->{code};
}
MAH_updateDeviceReadings($hash, $data);
}
sub MAH_updateDeviceReadings($$)
{
my ($hash, $data) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
my $json = eval{decode_json($data)};
if ($@) {
MAH_Log($hash, 3, "JSON error while request: $@");
return;
}
if (ref($json) ne "HASH") {
MAH_Log($hash, 3, "got wrong message for $name: $json");
return;
}
# {"code":500,"message":"There was an error processing your request. It has been logged (ID xx)."}
if (exists($json->{code})) {
MAH_Log($hash, 3, "got error code: $json");
return;
}
# the "devices"-reply (from event-sourcing) might be in the
# form {"12345":} instead of so get rid of this indirection
my $deviceId = $hash->{DEVICE_ID};
if (exists($json->{$deviceId})) {
MAH_Log($hash, 5, "dropping indirection for deviceId");
$json = $json->{$deviceId};
}
# decode_utf8() is required due do something like:
# dein json ist utf8 aber das problem scheint zu sein das decode_json bei zeichen
# <255 aus dem \u ein \x{..} macht. d.h. es erzeugt kein utf8 2-byte zeichen wie
# es richtig wäre sondern macht aus dem code point ein 1-byte zeichen das dann
# als latin-1 erscheint.
no strict "refs";
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "communicationModuleReleaseVersion", encode_utf8($json->{ident}->{xkmIdentLabel}->{releaseVersion}));
readingsBulkUpdate($hash, "communicationModuleTechType", encode_utf8($json->{ident}->{xkmIdentLabel}->{techType}));
readingsBulkUpdate($hash, "deviceHardwareFabIndex", encode_utf8($json->{ident}->{deviceIdentLabel}->{fabIndex}));
readingsBulkUpdate($hash, "deviceHardwareFabNumber", encode_utf8($json->{ident}->{deviceIdentLabel}->{fabNumber}));
readingsBulkUpdate($hash, "deviceHardwareMatNumber", encode_utf8($json->{ident}->{deviceIdentLabel}->{matNumber}));
readingsBulkUpdate($hash, "deviceHardwareTechType", encode_utf8($json->{ident}->{deviceIdentLabel}->{techType}));
readingsBulkUpdate($hash, "deviceName", encode_utf8($json->{ident}->{deviceName}));
readingsBulkUpdate($hash, "deviceType", encode_utf8($json->{ident}->{type}->{value_localized}));
readingsBulkUpdate($hash, "elapsedTime", MAH_formatTime(@{$json->{state}->{elapsedTime}}));
readingsBulkUpdate($hash, "remainingTime", MAH_formatTime(@{$json->{state}->{remainingTime}}));
readingsBulkUpdate($hash, "startTime", MAH_formatTime(@{$json->{state}->{startTime}}));
readingsBulkUpdate($hash, "dryingStep", encode_utf8($json->{state}->{dryingStep}->{value_localized}));
readingsBulkUpdate($hash, "light", encode_utf8($json->{state}->{light}));
readingsBulkUpdate($hash, "batteryLevel", encode_utf8($json->{state}->{batteryLevel}));
readingsBulkUpdate($hash, "programID", encode_utf8($json->{state}->{ProgramID}->{value_localized}));
readingsBulkUpdate($hash, "programPhase", encode_utf8($json->{state}->{programPhase}->{value_localized}));
readingsBulkUpdate($hash, "programType", encode_utf8($json->{state}->{programType}->{value_localized}));
readingsBulkUpdate($hash, "spinningSpeed", encode_utf8($json->{state}->{spinningSpeed}->{value_localized}));
readingsBulkUpdate($hash, "status", encode_utf8($json->{state}->{status}->{value_localized}));
readingsBulkUpdate($hash, "statusRaw", $json->{state}->{status}->{value_raw});
readingsBulkUpdate($hash, "ventilationStep", encode_utf8($json->{state}->{ventilationStep}->{value_localized}));
# not documented yet
#readingsBulkUpdate($hash, "plateStep", @{$json->{state}->{plateStep}});
# not documented yet
readingsBulkUpdate($hash, "ecoFeedbackCurrentWaterConsumption", encode_utf8($json->{state}->{ecoFeedback}->{currentWaterConsumption}->{value}));
readingsBulkUpdate($hash, "ecoFeedbackCurrentEnergyConsumption", encode_utf8($json->{state}->{ecoFeedback}->{currentEnergyConsumption}->{value}));
readingsBulkUpdate($hash, "ecoFeedbackWaterForecast", encode_utf8($json->{state}->{ecoFeedback}->{waterForecast}));
readingsBulkUpdate($hash, "ecoFeedbackEnergyForecast", encode_utf8($json->{state}->{ecoFeedback}->{energyForecast}));
readingsBulkUpdate($hash, "remoteEnableFullRC", $json->{state}->{remoteEnable}->{fullRemoteControl});
readingsBulkUpdate($hash, "remoteEnableSmartGrid", $json->{state}->{remoteEnable}->{smartGrid});
readingsBulkUpdate($hash, "remoteEnableMobileStart", $json->{state}->{remoteEnable}->{mobileStart});
readingsBulkUpdate($hash, "signalDoor", $json->{state}->{signalDoor});
readingsBulkUpdate($hash, "signalFailure", $json->{state}->{signalFailure});
readingsBulkUpdate($hash, "signalInfo", $json->{state}->{signalInfo});
# target temperature
my @targetTemperatures = MAH_decodeTemperature($hash, @{$json->{state}->{targetTemperature}});
if (scalar(@targetTemperatures) == 1) {
readingsBulkUpdate($hash, "targetTemperature", ${targetTemperatures[0]});
} else {
for (my $i = 0; $i < scalar(@targetTemperatures); $i++) {
readingsBulkUpdate($hash, "targetTemperature_zone".($i+1), ${targetTemperatures[$i]});
}
}
# temperature
my @temperatures = MAH_decodeTemperature($hash, @{$json->{state}->{temperature}});
if (scalar(@temperatures) == 1) {
readingsBulkUpdate($hash, "temperature", ${temperatures[0]});
} else {
for (my $i = 0; $i < scalar(@temperatures); $i++) {
readingsBulkUpdate($hash, "temperature_zone".($i+1), ${temperatures[$i]});
}
}
#eta & state
my ($eta, $etaHR) = MAH_calculateETA($json->{state}->{remainingTime},
$json->{state}->{startTime},
$json->{state}->{status}->{value_raw});
readingsBulkUpdate($hash, "eta", $eta);
readingsBulkUpdate($hash, "etaHR", $etaHR);
readingsBulkUpdate($hash, "state", sprintf("%s (%s)",
encode_utf8($json->{state}->{status}->{value_localized}), $eta));
readingsEndUpdate($hash, 1 );
use strict "refs";
return undef;
}
#------------------------------------------------------------------------------------------------------
# MAH_sendGetDeviceActionsRequest
#------------------------------------------------------------------------------------------------------
sub MAH_sendGetDeviceActionsRequest($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $deviceId = $hash->{DEVICE_ID};
if ($deviceId eq "") {
return "Please set deviceId first";
}
my $token = MAH_getAccessToken($hash);
if ($token eq "") {
return "Please authenticate first";
}
my $lang = AttrVal($name, "lang", "en");
my $url = "https://api.mcs3.miele.com/v1/devices/${deviceId}/actions?language=${lang}";
my $header = { "accept" => "application/json; charset=utf-8",
"Authorization" => "Bearer " . $token };
my ($err, $data) = HttpUtils_NonblockingGet({
url => $url,
header => $header,
timeout => 5,
hash => $hash,
method => "GET",
callback => \&MAH_onGetDeviceActionsReply,
});
}
sub MAH_onGetDeviceActionsReply($$$)
{
my ($param, $err, $data) = @_;
my $hash = $param->{hash};
my $name = $hash->{NAME};
MAH_LogReply($hash, $param, $err, $data);
if ($err) {
MAH_Log($hash, 3, "Error: $err");
return $err;
}
if ($param->{code} != 200) {
MAH_Log($hash, 3, "Error: code != 200: $param->{code}");
return;
}
MAH_updateActionReadings($hash, $data)
}
sub MAH_updateActionReadings($$)
{
my ($hash, $data) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called");
my $json = eval{decode_json($data)};
if ($@) {
MAH_Log($hash, 3, "JSON error while request: $@");
return;
}
if (ref($json) ne "HASH") {
MAH_Log($hash, 3, "got wrong message for $name: $json");
return;
}
# {"code":500,"message":"There was an error processing your request. It has been logged (ID xx)."}
if (exists($json->{code})) {
MAH_Log($hash, 3, "got error code: $json");
return;
}
# the "actions"-reply (from event-sourcing) might be in the
# form {"12345":} instead of so get rid of this indirection
my $deviceId = $hash->{DEVICE_ID};
if (exists($json->{$deviceId})) {
MAH_Log($hash, 5, "dropping indirection for deviceId");
$json = $json->{$deviceId};
}
no strict "refs";
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "actions_processAction", join(",", @{$json->{processAction}}));
readingsBulkUpdate($hash, "actions_light", join(",", @{$json->{light}}));
readingsBulkUpdate($hash, "actions_startTime", join(",", MAH_parseActionsStartTime($json->{startTime})));
readingsBulkUpdate($hash, "actions_ventilationStep", join(",", @{$json->{ventilationStep}}));
readingsBulkUpdate($hash, "actions_programId", join(",", @{$json->{programId}}));
readingsBulkUpdate($hash, "actions_targetTemperature", MAH_parseActionsTargetTemperature($hash,$json->{targetTemperature}));
readingsBulkUpdate($hash, "actions_deviceName", $json->{deviceName});
readingsBulkUpdate($hash, "actions_powerOff", defined($json->{powerOff}) ? $json->{powerOff} : "0");
readingsBulkUpdate($hash, "actions_powerOn", defined($json->{powerOn}) ? $json->{powerOn} : "0");
# readingsBulkUpdate($hash, "actions_colors", );
readingsBulkUpdate($hash, "actions_modes", join(",", @{$json->{modes}}));
readingsEndUpdate($hash, 1 );
use strict "refs";
return undef;
}
#------------------------------------------------------------------------------------------------------
# format time from array
# "targetTemperature":[
# {"value_raw":600,"value_localized":6.0,"unit":"Celsius"},
# {"value_raw":-1800,"value_localized":-18.0,"unit":"Celsius"},
# {"value_raw":-32768,"value_localized":null,"unit":"Celsius"}],
# "temperature":[
# {"value_raw":593,"value_localized":5.93,"unit":"Celsius"},
# {"value_raw":-1800,"value_localized":-18.0,"unit":"Celsius"},
# {"value_raw":-32768,"value_localized":null,"unit":"Celsius"}],
#------------------------------------------------------------------------------------------------------
sub MAH_decodeTemperature($@)
{
my ($hash, @temps) = @_;
my $name = $hash->{NAME};
my @retval;
foreach my $t (@temps) {
if ($t->{value_raw} != -32768) {
push(@retval, $t->{value_localized});
}
}
return @retval;
}
#------------------------------------------------------------------------------------------------------
# parse the startTime from actions which is either [] or [[0,0],[23,59]]
#------------------------------------------------------------------------------------------------------
sub MAH_parseActionsStartTime($)
{
my ($startTime) = @_;
my @startTimeArray = @{$startTime};
if (scalar(@startTimeArray) == 0) {
return "";
}
if (scalar(@startTimeArray) == 2) {
return MAH_formatTime(@{$startTimeArray[0]}) . "-" . MAH_formatTime(@{$startTimeArray[1]});
}
return "[?]";
}
#------------------------------------------------------------------------------------------------------
# parse the target temperature from actions
# [
# {
# "zone": 1,
# "min": 1,
# "max": 9
# },
# {
# "zone": 2,
# "min": -26,
# "max": -16
# }
# ]
#------------------------------------------------------------------------------------------------------
sub MAH_parseActionsTargetTemperature($$)
{
my ($hash,$json) = @_;
no strict "refs";
my $retval = "";
my @zones = @{$json};
for (my $i = 0; $i < scalar(@zones); $i++) {
my $zone = $zones[$i];
$retval .= $zone->{zone} . "[" . $zone->{min} . "," . $zone->{max} . "] ";
}
use strict "refs";
chop($retval); # remove trailing space
return $retval;
}
#------------------------------------------------------------------------------------------------------
# calculate the estimated time of arrival (as HH:MM and as human readable version)
#------------------------------------------------------------------------------------------------------
sub MAH_calculateETA($$$)
{
my ($remaining, $start, $statusRaw) = @_;
# 1 = OFF
# 2 = ON
# 3 = PROGRAMMED
# 4 = PROGRAMMED WAITING TO START
# 5 = RUNNING
# 6 = PAUSE
# 7 = END PROGRAMMED
# 8 = FAILURE
# 9 = PROGRAMME INTERRUPTED
# 10 = IDLE
# 11 = RINSE HOLD
# 12 = SERVICE
# 13 = SUPERFREEZING
# 14 = SUPERCOOLING
# 15 = SUPERHEATING
# 146 = SUPERCOOLING_SUPERFREEZING
# 255 = NOT_CONNECTED
my ($remainingHour, $remainingMinute) = @{$remaining};
my ($startHour, $startMinute) = @{$start};
return ("-:-", "-:-") if ($statusRaw == 1 || $statusRaw == 255); # Off
my $startOffsetSecs = $startHour * 3600 + $startMinute * 60;
my $remainingSecs = $remainingHour * 3600 + $remainingMinute * 60;
if ($statusRaw == 4) { # delay active
my $eta = POSIX::strftime("%H:%M", localtime(time + $startOffsetSecs + $remainingSecs));
my $etaHR = $eta;
return ($eta, $etaHR);
}
if ($statusRaw == 2 || $statusRaw == 7) { # On (but not running) or End
my $eta = POSIX::strftime("%H:%M", localtime(time + $remainingSecs)); # ignore startOffsetSecs here as this is very strange
my $etaHR = "+" . MAH_formatTime($remainingHour, $remainingMinute);
$etaHR = "Ende" if ($remainingHour == 0 && $remainingMinute == 0);
return ($eta, $etaHR);
}
# if ($statusRaw == 5) { # In Betrieb
my $eta = POSIX::strftime("%H:%M", localtime(time + $remainingSecs)); # ignore startOffsetSecs here as this is very strange
my $etaHR = $eta;
# write remaining minutes in the last 15 minutes instead of
$etaHR = "+" . MAH_formatTime($remainingHour, $remainingMinute) if ($remainingSecs <= 15 * 60);
return ($eta, $etaHR);
# }
# return POSIX::strftime("%Y-%m-%d %H:%M", localtime(time + $offset));
}
#------------------------------------------------------------------------------------------------------
# format time from array
#------------------------------------------------------------------------------------------------------
sub MAH_formatTime(@)
{
my ($hour, $minute) = @_;
return sprintf("%d:%02d", defined($hour) ? $hour : 0,
defined($minute) ? $minute : 0);
}
#------------------------------------------------------------------------------------------------------
# MAH_autocreate
#------------------------------------------------------------------------------------------------------
sub MAH_autocreate($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $devices = MAH_blockingGetAllDevicesRequest($hash);
if(ref($devices) ne 'ARRAY') {
readingsSingleUpdate($hash, "lastError", "autocreate failed: $devices", 1);
return;
}
for my $d (@{$devices}) {
my $deviceId = @{$d}[0];
if (defined($modules{MieleAtHome}{defptr}{"deviceid_".$deviceId})) {
MAH_Log($hash, 3, "autocreate - device with deviceId $deviceId already exists");
} else {
my $nameOfDevice = "Miele_${deviceId}";
if (IsDevice($nameOfDevice)) {
MAH_Log($hash, 3, "not autocreating device, as device with proposed name already exists (${nameOfDevice})");
} else {
fhem("define $nameOfDevice MieleAtHome ${deviceId}\@${name}");
if (IsDevice($nameOfDevice)) {
# return "Can't create, device $nameOfDevice already existing."
# unless (IsDevice($nameOfDevice, "IPCAM"));
fhem("attr ".$nameOfDevice." comment Auto-created by $name")
unless (defined($attr{$nameOfDevice}{comment}));
MAH_Log($hash, 3, "created device ${nameOfDevice}, with deviceId ${deviceId}");
}
}
}
}
}
#------------------------------------------------------------------------------------------------------
# MAH_setPower
#------------------------------------------------------------------------------------------------------
sub MAH_setPower($$)
{
my ($hash, $onOrOff) = @_;
my $name = $hash->{NAME};
if ($onOrOff eq "on") {
return "power 'on' is currently not available" if (ReadingsNum($name, "actions_powerOn", 0) != 1);
return MAH_setAction($hash, "powerOn", "true");
} elsif ($onOrOff eq "off") {
return "power 'off' is currently not available" if (ReadingsNum($name, "actions_powerOff", 0) != 1);
return MAH_setAction($hash, "powerOff", "true");
} else {
return "use either 'on' or 'off'";
}
}
#------------------------------------------------------------------------------------------------------
# MAH_setProcessAction
#------------------------------------------------------------------------------------------------------
sub MAH_setProcessAction($$)
{
my ($hash, $processActionName) = @_;
my $name = $hash->{NAME};
my ($processActionId) = grep{ PROCESS_ACTIONS->{$_} eq $processActionName } keys %{PROCESS_ACTIONS()};
if (!defined $processActionId) {
return "invalid processAction: '${processActionName}'";
}
my @availableProcessActions = split(/,/, ReadingsVal($name, "actions_processAction", ""));
if (! grep {$_ eq $processActionId} @availableProcessActions) {
return "'${processActionName}' is currently not available";
}
return MAH_setAction($hash, "processAction", "${processActionId}");
}
#------------------------------------------------------------------------------------------------------
# MAH_setLight
#------------------------------------------------------------------------------------------------------
sub MAH_setLight($$)
{
my ($hash, $lightActionName) = @_;
my $name = $hash->{NAME};
my ($lightActionId) = grep{ LIGHT_ACTIONS->{$_} eq $lightActionName } keys %{LIGHT_ACTIONS()};
if (!defined $lightActionId) {
return "invalid light action: '${lightActionName}'";
}
my @availableLightActions = split(/,/, ReadingsVal($name, "actions_light", ""));
if (! grep {$_ eq $lightActionId} @availableLightActions) {
return "'${lightActionName}' is currently not available";
}
return MAH_setAction($hash, "light", "${lightActionId}");
}
#------------------------------------------------------------------------------------------------------
# MAH_setMode
#------------------------------------------------------------------------------------------------------
sub MAH_setMode($$)
{
my ($hash, $modeActionName) = @_;
my $name = $hash->{NAME};
my ($modeActionId) = grep{ MODE_ACTIONS->{$_} eq $modeActionName } keys %{MODE_ACTIONS()};
if (!defined $modeActionId) {
return "invalid mode action: '${modeActionName}'";
}
my @availableModeActions = split(/,/, ReadingsVal($name, "actions_modes", ""));
if (! grep {$_ eq $modeActionId} @availableModeActions) {
return "'${modeActionName}' is currently not available";
}
return MAH_setAction($hash, "modes", "${modeActionId}");
}
#------------------------------------------------------------------------------------------------------
# MAH_setTargetTemperature
#------------------------------------------------------------------------------------------------------
sub MAH_setTargetTemperature($$$)
{
my ($hash, $zone, $temp) = @_;
my $name = $hash->{NAME};
my @availableTargetTemperatures = split(/ /, ReadingsVal($name, "actions_targetTemperature", ""));
foreach my $targetTemperature (@availableTargetTemperatures) {
if ($targetTemperature =~ /^([0-9]+)\[(-?[0-9]+),(-?[0-9]+)\]$/) {
if ($1 eq $zone) {
if ($2 <= int($temp) && int($temp) <= $3) {
return MAH_setAction($hash, "targetTemperature", "[{\"zone\": $zone, \"value\": $temp}]");
} else {
return "temperature for zone ${zone} out of range, must be between ${2} and ${3}";
}
}
}
}
return "zone ${zone} not setable";
}
#------------------------------------------------------------------------------------------------------
# MAH_setVentilationStep
#------------------------------------------------------------------------------------------------------
sub MAH_setVentilationStep($$)
{
my ($hash, $ventilationStepName) = @_;
my $name = $hash->{NAME};
my ($ventilationStepId) = grep{ VENTILATION_STEPS->{$_} eq $ventilationStepName } keys %{VENTILATION_STEPS()};
if (!defined $ventilationStepId) {
return "invalid ventilation step: '${ventilationStepName}'";
}
my @availableVentilationStepIds = split(/,/, ReadingsVal($name, "actions_ventilationStep", ""));
if (! grep {$_ eq $ventilationStepId} @availableVentilationStepIds) {
return "'${ventilationStepName}' is currently not available";
}
return MAH_setAction($hash, "ventilationStep", "${ventilationStepId}");
}
#------------------------------------------------------------------------------------------------------
# MAH_setStartTime
#------------------------------------------------------------------------------------------------------
sub MAH_setStartTime($$)
{
my ($hash, $startTimeString) = @_;
my $name = $hash->{NAME};
if ($startTimeString =~ m/$[0-9]+:[0-9]+]^/) {
return "invalid startTime format: '${startTimeString}', offset must be [h]h:mm";
}
if (ReadingsNum($name, "actions_startTime", 0) != 1) {
return "'startTime' is currently not setable";
}
$startTimeString =~ s/:/,/;
return MAH_setAction($hash, "startTime", "[${startTimeString}]");
}
#------------------------------------------------------------------------------------------------------
# MAH_setAction
#------------------------------------------------------------------------------------------------------
sub MAH_setAction($$$)
{
my ($hash, $action, $value) = @_;
my $actionJson = "{\"$action\":$value}";
return MAH_sendSetActionRequest($hash, $actionJson);
}
#------------------------------------------------------------------------------------------------------
# MAH_sendSetActionRequest, $action needs to be the json-encoded action like »{"powerOn":true}«
#------------------------------------------------------------------------------------------------------
sub MAH_sendSetActionRequest($$)
{
my ($hash, $action) = @_;
my $name = $hash->{NAME};
MAH_Log($hash, 5, "called with action $action");
my $deviceId = $hash->{DEVICE_ID};
if ($deviceId eq "") {
return "Please set deviceId first";
}
my $token = MAH_getAccessToken($hash);
if ($token eq "") {
return "Please authenticate first";
}
my $url = "https://api.mcs3.miele.com/v1/devices/${deviceId}/actions";
my $header = { "accept" => "*/*",
"Content-Type" => "application/json",
"Authorization" => "Bearer " . $token };
my ($err, $reply) = HttpUtils_NonblockingGet({
url => $url,
header => $header,
timeout => 30, # this somethimes takes soooome time
hash => $hash,
method => "PUT",
data => $action,
callback => \&MAH_onSetActionReply,
});
return undef;
}
#------------------------------------------------------------------------------------------------------
sub MAH_onSetActionReply($$$)
{
my ($param, $err, $data) = @_;
my $hash = $param->{hash};
my $name = $hash->{NAME};
MAH_LogReply($hash, $param, $err, $data);
if ($err) {
MAH_Log($hash, 3, "Error: $err");
return $err;
}
# it generally takes some time for the API to react to changes
InternalTimer(gettimeofday()+5, "MAH_sendGetDeviceActionsRequest", $hash);
}
#------------------------------------------------------------------------------------------------------
# * if it is a duplicate instance -> bah
# * if disabled -> ...
#------------------------------------------------------------------------------------------------------
sub MAH_isDisabled($)
{
my ($hash) = @_;
return $hash->{DUPLICATE_INSTANCE} ||
AttrVal($hash->{NAME}, "disable", "") ||
IsDisabled($hash->{NAME});
}
#------------------------------------------------------------------------------------------------------
# MAH_hasLoginCredentials
#------------------------------------------------------------------------------------------------------
sub MAH_hasLoginCredentials($)
{
my ($hash) = @_;
return 0 unless (defined(MAH_getClientId($hash)));
return 0 unless (defined(MAH_getLogin($hash)));
return 0 unless (defined(MAH_getPassword($hash)));
return 0 unless (defined(MAH_getClientSecret($hash)));
return 1;
}
#------------------------------------------------------------------------------------------------------
# MAH_getClientId(), MAH_getLogin(), MAH_getPassword(), MAH_getClientSecret()
#------------------------------------------------------------------------------------------------------
sub MAH_getClientId($); # workaround for perl warning
sub MAH_getClientId($)
{
my ($hash) = @_;
my $retval = AttrVal($hash->{NAME}, "clientId", "");
return $retval if ($retval ne "");
my $iohash = MAH_getIODevHash($hash);
return MAH_getClientId($iohash) if (defined($iohash));
return undef;
}
sub MAH_getLogin($); # workaround for perl warning
sub MAH_getLogin($)
{
my ($hash) = @_;
my $retval = AttrVal($hash->{NAME}, "login", "");
return $retval if ($retval ne "");
my $iohash = MAH_getIODevHash($hash);
return MAH_getLogin($iohash) if (defined($iohash));
return undef;
}
sub MAH_getPassword($); # workaround for perl warning
sub MAH_getPassword($)
{
my ($hash) = @_;
my $retval = MAH_loadPassword($hash);
return $retval if (defined($retval));
my $iohash = MAH_getIODevHash($hash);
return MAH_getPassword($iohash) if (defined($iohash));
return undef;
}
sub MAH_getClientSecret($); # workaround for perl warning
sub MAH_getClientSecret($)
{
my ($hash) = @_;
my $retval = MAH_loadClientSecret($hash);
return $retval if (defined($retval));
my $iohash = MAH_getIODevHash($hash);
return MAH_getClientSecret($iohash) if (defined($iohash));
return undef;
}
sub MAH_getAccessToken($); # workaround for perl warning
sub MAH_getAccessToken($)
{
my ($hash) = @_;
# try to find token
my $accessToken = MAH_getAccessTokenPrivate($hash);
if ($accessToken ne "") {
my $secs = MAH_getRemainingTokenLifetime($hash);
MAH_Log($hash, 4, "found local token with remaining lifetime of ${secs} seconds");
MAH_refreshAccessToken($hash) if ($secs < 24 * 60 * 60);
return $accessToken if ($secs > 0);
}
# if we could not find a token, refrehs it async
MAH_refreshAccessToken($hash);
return "";
}
sub MAH_getAccessTokenPrivate($); # workaround for perl warning
sub MAH_getAccessTokenPrivate($)
{
my ($hash) = @_;
# try to find local token
if (defined($hash->{OAUTH2_ACCESS_TOKEN})) {
return $hash->{OAUTH2_ACCESS_TOKEN};
}
# try to find token in IODev
my $iohash = MAH_getIODevHash($hash);
return MAH_getAccessTokenPrivate($iohash) if (defined($iohash));
return "";
}
sub MAH_getRefreshTokenPrivate($); # workaround for perl warning
sub MAH_getRefreshTokenPrivate($)
{
my ($hash) = @_;
# try to find local token
if (defined($hash->{OAUTH2_REFRESH_TOKEN})) {
return $hash->{OAUTH2_REFRESH_TOKEN};
}
# try to find token in IODev
my $iohash = MAH_getIODevHash($hash);
return MAH_getRefreshTokenPrivate($iohash) if (defined($iohash));
return "";
}
sub MAH_getRemainingTokenLifetime($); # workaround for perl warning
sub MAH_getRemainingTokenLifetime($)
{
my ($hash) = @_;
if (defined($hash->{OAUTH2_EXPIRES_AT})) {
my $secs = time_str2num($hash->{OAUTH2_EXPIRES_AT}) - time;
return $secs;
}
# try to find token in IODev
my $iohash = MAH_getIODevHash($hash);
return MAH_getRemainingTokenLifetime($iohash) if (defined($iohash));
return 0;
}
#------------------------------------------------------------------------------------------------------
# MAH_getIODevHash
#------------------------------------------------------------------------------------------------------
sub MAH_getIODevHash($)
{
my ($hash) = @_;
return undef unless (defined($hash->{IODevName}));
return undef unless (defined($defs{$hash->{IODevName}}));
return $defs{$hash->{IODevName}};
}
#------------------------------------------------------------------------------------------------------
# Util: clientSecret
#------------------------------------------------------------------------------------------------------
sub MAH_saveClientSecret($$)
{
my ($hash,$clientSecret) = @_;
return MAH_setKeyValue($hash, "clientSecret", $clientSecret);
}
sub MAH_loadClientSecret($)
{
my ($hash) = @_;
return MAH_getKeyValue($hash, "clientSecret");
}
sub MAH_renameClientSecret($$$)
{
my ($newHash,$oldName,$newName) = @_;
MAH_renameKeyValue($newHash, $oldName, $newName, "clientSecret");
}
sub MAH_deleteClientSecret($)
{
my ($hash) = @_;
return MAH_deleteKeyValue($hash, "clientSecret");
}
#------------------------------------------------------------------------------------------------------
# Util: password
#------------------------------------------------------------------------------------------------------
sub MAH_savePassword($$)
{
my ($hash,$password) = @_;
return MAH_setKeyValue($hash, "passwd", $password);
}
sub MAH_loadPassword($)
{
my ($hash) = @_;
return MAH_getKeyValue($hash, "passwd");
}
sub MAH_renamePassword($$$)
{
my ($newHash,$oldName,$newName) = @_;
MAH_renameKeyValue($newHash, $oldName, $newName, "passwd");
}
sub MAH_deletePassword($)
{
my ($hash) = @_;
return MAH_deleteKeyValue($hash, "passwd");
}
#------------------------------------------------------------------------------------------------------
# Util: oauth2 credentials
#------------------------------------------------------------------------------------------------------
sub MAH_saveOAuth2Credentials($)
{
my ($hash) = @_;
MAH_setKeyValue($hash, "OAUTH2_ACCESS_TOKEN", $hash->{OAUTH2_ACCESS_TOKEN});
MAH_setKeyValue($hash, "OAUTH2_REFRESH_TOKEN", $hash->{OAUTH2_REFRESH_TOKEN});
MAH_setKeyValue($hash, "OAUTH2_EXPIRES_IN", $hash->{OAUTH2_EXPIRES_IN});
MAH_setKeyValue($hash, "OAUTH2_EXPIRES_AT", $hash->{OAUTH2_EXPIRES_AT});
}
sub MAH_restoreOAuth2Credentials($)
{
my ($hash) = @_;
my $v = MAH_getKeyValue($hash, "OAUTH2_ACCESS_TOKEN");
if (defined($v) && !defined($hash->{OAUTH2_ACCESS_TOKEN})) {
$hash->{OAUTH2_ACCESS_TOKEN} = $v;
}
$v = MAH_getKeyValue($hash, "OAUTH2_REFRESH_TOKEN");
if (defined($v) && !defined($hash->{OAUTH2_REFRESH_TOKEN})) {
$hash->{OAUTH2_REFRESH_TOKEN} = $v;
}
$v = MAH_getKeyValue($hash, "OAUTH2_EXPIRES_IN");
if (defined($v) && !defined($hash->{OAUTH2_EXPIRES_IN})) {
$hash->{OAUTH2_EXPIRES_IN} = $v;
}
$v = MAH_getKeyValue($hash, "OAUTH2_EXPIRES_AT");
if (defined($v) && !defined($hash->{OAUTH2_EXPIRES_AT})) {
$hash->{OAUTH2_EXPIRES_AT} = $v;
}
}
sub MAH_renameOAuth2Credentials($$$)
{
my ($newHash,$oldName,$newName) = @_;
MAH_renameKeyValue($newHash, $oldName, $newName, "OAUTH2_ACCESS_TOKEN");
MAH_renameKeyValue($newHash, $oldName, $newName, "OAUTH2_REFRESH_TOKEN");
MAH_renameKeyValue($newHash, $oldName, $newName, "OAUTH2_EXPIRES_IN");
MAH_renameKeyValue($newHash, $oldName, $newName, "OAUTH2_EXPIRES_AT");
}
sub MAH_deleteOAuth2Credentials($)
{
my ($hash) = @_;
MAH_deleteKeyValue($hash, "OAUTH2_ACCESS_TOKEN");
MAH_deleteKeyValue($hash, "OAUTH2_REFRESH_TOKEN");
MAH_deleteKeyValue($hash, "OAUTH2_EXPIRES_IN");
MAH_deleteKeyValue($hash, "OAUTH2_EXPIRES_AT");
}
#------------------------------------------------------------------------------------------------------
# Util: MAH_setKeyValue
#------------------------------------------------------------------------------------------------------
sub MAH_setKeyValue($$$)
{
my ($hash,$subkey,$value) = @_;
my $type = $hash->{TYPE};
my $name = $hash->{NAME};
my $key = "${type}_${name}_${subkey}";
# always prepend passwords with '=' to allow to upgrade from having no
# base64 encoding to using base64. decode_base64() ignores everything
# after the '=' so if we try to decode a not decoded password (starting
# with '='), this will result in an empty value which can be detected.
$value = "=" . $value;
# base64 encode if possible
$value = encode_base64($value,'') if ($MAH_hasMimeBase64);
my $err = setKeyValue($key, $value);
MAH_Log($hash, 3, "Error when setting $key: $err") if ($err);
return $err;
}
#------------------------------------------------------------------------------------------------------
# Util: MAH_getKeyValue
#------------------------------------------------------------------------------------------------------
sub MAH_getKeyValue($$)
{
my ($hash,$subkey) = @_;
my $type = $hash->{TYPE};
my $name = $hash->{NAME};
my $key = "${type}_${name}_${subkey}";
my ($err, $value) = getKeyValue($key);
# error
if ($err) {
MAH_Log($hash, 3, "Error when fetching $key: $err");
return undef;
}
# no value found
return undef unless (defined($value));
my $retval = $value;
if ($MAH_hasMimeBase64) {
# try to base64-decode the retval.
$retval = decode_base64($value);
# if it is empty, it was not encoded (as decode_base64() ignores everything
# after our initial '=')
$retval = $value if ($retval eq "");
}
# our retval is always stored with a leading '='
if ($retval !~ /^=.*/) {
MAH_Log($hash, 3, "failed to fetch retval: $retval");
return undef;
}
# remove the leading '=' which was added in MAH_setBasicAuth()
return substr($retval, 1);
}
#------------------------------------------------------------------------------------------------------
# Util: MAH_renameKeyValue
#------------------------------------------------------------------------------------------------------
sub MAH_renameKeyValue($$$$)
{
my ($newHash,$oldName,$newName,$subkey) = @_;
my $type = $newHash->{TYPE};
my $oldKey = "${type}_${oldName}_${subkey}";
my $newKey = "${type}_${newName}_${subkey}";
my ($err, $data) = getKeyValue($oldKey);
return undef unless(defined($data));
setKeyValue($newKey, $data);
setKeyValue($oldKey, undef);
}
#------------------------------------------------------------------------------------------------------
# Util: MAH_deleteKeyValue
#------------------------------------------------------------------------------------------------------
sub MAH_deleteKeyValue($$)
{
my ($hash,$subkey) = @_;
my $type = $hash->{TYPE};
my $name = $hash->{NAME};
my $key = "${type}_${name}_${subkey}";
my $err = setKeyValue($key, undef);
}
#------------------------------------------------------------------------------------------------------
# Util: Log replys of network requests
#------------------------------------------------------------------------------------------------------
sub MAH_LogReply($$$$)
{
my ($hash, $param, $err, $data) = @_;
my $logLevel = 5;
# prevent warnings with uninitialized variables
my $logMessage = "received reply";
$logMessage .= ", err:$err" if ($err);
$logMessage .= ", code:$param->{code}" if ($param && defined($param->{code}));
$logMessage .= ", headers:$param->{httpheader}" if ($param && defined($param->{httpheader}));
$logMessage .= ", data:$data" if ($data);
my $line = ( caller(0) )[2];
my $modAndSub = ( caller(1) )[3];
my $subroutine = ( split(':', $modAndSub) )[2];
MAH_LogPrivate($hash, $logLevel, $logMessage, $line, $subroutine);
}
#------------------------------------------------------------------------------------------------------
# Util: Log
#------------------------------------------------------------------------------------------------------
sub MAH_Log($$$)
{
my ($hash, $logLevel, $logMessage) = @_;
my $line = ( caller(0) )[2];
my $modAndSub = ( caller(1) )[3];
my $subroutine = ( split(':', $modAndSub) )[2];
MAH_LogPrivate($hash, $logLevel, $logMessage, $line, $subroutine);
}
#------------------------------------------------------------------------------------------------------
# Util: LogPrivates
#------------------------------------------------------------------------------------------------------
sub MAH_LogPrivate($$$$$)
{
my ($hash, $logLevel, $logMessage, $line, $subroutine) = @_;
my $name = ( ref($hash) eq "HASH" ) ? $hash->{NAME} : "MieleAtHome";
# replace non-printable characters by ""
$logMessage =~ s/([\x{00}-\x{1f}\x{7f}-\x{7fffffff}])/'<'.unpack('(H2)',$1).'>'/ge;
Log3($hash, $logLevel, "${name} (MieleAtHome::${subroutine}:${line}) " . $logMessage);
#Log3($hash, $logLevel, "${name} (MieleAtHome::${subroutine}:${line}) Stack was: " . MAH_getStacktrace());
}
#------------------------------------------------------------------------------------------------------
# Util: returns a stacktrace as a string (for debbugging)
#------------------------------------------------------------------------------------------------------
sub MAH_getStacktrace()
{
my ($package, $filename, $line, $subroutine, $hasargs, $wantarray, $evaltext, $is_require, $hints, $bitmask, $hinthash);
my $i = 2; # skip MAH_getStacktrace() and MAH_Log()
my @r;
my $retval = "";
while (@r = caller($i)) {
($package, $filename, $line, $subroutine, $hasargs, $wantarray, $evaltext, $is_require, $hints, $bitmask, $hinthash) = @r;
$subroutine = ( split( ':', $subroutine ) )[2];
$retval = "->${line}:${subroutine}${retval}";
$i++;
}
return $retval;
}
# must be last
1;
=pod
=item device
=item summary Module to control Miele@home-devices via their 3rd party API
=item summary_DE Modul zur Steuerung von Miele@home-Geräten mittels 3rd Party API
=begin html
MieleAtHome
MieleAtHome - Controls Miele@home Devices
About
The MieleAtHome module uses the Miele 3rd Party Cloud API. You need a Miele Developer Account to use it! See below for details.
To use the MieleAtHome module you first have to define a device which will act als shared provider for your credentials. When this one is set up, you can use the autocreate-feature to create devices for your appliances.
Miele Developer Account:
To use this module you need to register as a developer at https://www.miele.com/f/com/en/register_api.aspx. After you successfully registered, you will receive a clientId and a clientSecret which you'll need to configure in your <gateway>-device.
This instance (MieleConnection) will be used to share the credentials. You have to set the attributes login and clientId. Then you have to set password and clientSecret via set MieleConnection-command.
(2a) Autocreate devices: set MieleConnection autocreate
This will create a device called Miele_xxxxxxxxxxxxx. You can rename autocreated devices afterwards.
This statement creates the instance of your specific Miele@home appliance with the name Waschmaschine and the DeviceId 000123456789 and a refresh-interval of 120 seconds. The interval is optional, its default is 120 seconds.
Remote starting appliences
Appliences like wasching mashines can be started remotely via FHEM, but this is rather unintuitive:
Therefore the user loads the applience, selects a start or end time within the next 24 hours and presses the start button. The applience then waits for a "remote start" and can be started by FHEM before the start or end time has elapsed. If this is not received within the specified start or end time, it starts itself to be ready in time.
Set
autocreate
autocreate fhem-devices for each Miele@home appliance found in your account. Needs login, clientId, password and clientSecret to be configured properly. Only available for the gateway device.
clientSecret <secret>
sets the clientSecret of your Miele@home-developer Account and stores it in a file (base64-encoded if you have MIME::Base64 installed).
light [enable|disable]
enable/disable the light of your device. only available depending on the type and state of your appliance.
mode <mode>
set the mode of your applience. can be either sabbathMode or normalOperationMode.
on
power up your device. only available depending on the type and state of your appliance.
off
power off your device. only available depending on the type and state of your appliance.
password <pass>
set the password of your Miele@home Account and stores it in a file (base64-encoded if you have MIME::Base64 installed).
pause
pause your device. only available depending on the type and state of your appliance.
start
start your device. only available depending on the type and state of your appliance.
startTime <[H]H:MM>
modify the start time of your device relative from current time. only available depending on the type and state of your appliance.
stop
stop your device. only available depending on the type and state of your appliance.
startSuperFreezing
start super freezing your device. only available depending on the type and state of your appliance.
stopSuperFreezing
stop super freezing your device. only available depending on the type and state of your appliance.
startSuperCooling
start super cooling your device. only available depending on the type and state of your appliance.
stopSuperCooling
stop super cooling your device. only available depending on the type and state of your appliance.
targetTemperature_zoneN <temp>
set the targetTemperature of zone N of your applience. The available temperature-range is determined by the Miele-API.
update
instantly update all readings.
ventilationStep [Step1|Step2|Step3|Step4]
set the ventilation step of your device. only available depending on the type and state of your appliance.
Get
listDevices
lists the devices associated with your Miele@home-account. Needs login, clientId, password and clientSecret to be configured properly.
Attributes
api [poll|event]
set to poll if you want FHEM to repeatedly poll the Miele server for updates. Set to event if you want to have a keepalive-connection to the Miele server. With event, updates are received as soon as they are updated on the Miele server. With poll updates are only received in the configured interval. poll is the current default but the default will be switched to event after a testing period.
clientId
set the clientId of your Miele@home-developer account.
country
set the country where you registered your Miele@home account.
login
set the login of your Miele@home account.
disable
disables this MieleAtHome-instance.
lang [de|en]
request the readings in either german or english. en is default.