iobroker.tado
Version:
Tado cloud connector to control Tado devices
834 lines (767 loc) • 102 kB
JavaScript
'use strict';
const utils = require('@iobroker/adapter-core');
const EXPIRATION_WINDOW_IN_SECONDS = 10;
const tado_url = 'https://my.tado.com';
const tado_app_url = `https://app.tado.com/`;
const tadoX_url = `https://hops.tado.com`;
const client_id = `1bb50063-6b0c-4d11-bd99-387f4a91cc46`;
const jsonExplorer = require('iobroker-jsonexplorer');
const state_attr = require(`${__dirname}/lib/state_attr.js`); // Load attribute library
const isOnline = require('@esm2cjs/is-online').default;
const https = require('https');
const axios = require('axios');
const { version } = require('./package.json');
// @ts-ignore
let axiosInstance = axios.create({
timeout: 20000, //20000
baseURL: `${tado_url}/`,
httpsAgent: new https.Agent({ keepAlive: true }),
referer: tado_app_url,
origin: tado_app_url
});
// @ts-ignore
const axiosInstanceToken = axios.create({
baseURL: `https://login.tado.com/oauth2`
});
const ONEHOUR = 60 * 60 * 1000;
let polling; // Polling timer
let pooltimer = [];
class Tado extends utils.Adapter {
/**
* @param {Partial<ioBroker.AdapterOptions>} [options={}]
*/
constructor(options) {
// @ts-ignore
super({
...options,
name: 'tado',
});
this.on('ready', this.onReady.bind(this));
this.on('stateChange', this.onStateChange.bind(this));
this.on('unload', this.onUnload.bind(this));
this.on('message', this.onMessage.bind(this));
jsonExplorer.init(this, state_attr);
this.getMe_data = null;
this.home_data = null;
this.lastupdate = 0;
this.apiCallinExecution = false;
this.intervall_time = 60 * 1000;
this.roomCapabilities = {};
this.oldStatesVal = [];
this.isTadoX = false;
this.device_code = '';
this.uri4token = '';
this.retryCount = 0;
this.refreshTokenInProgress = false;
}
/**
* Is called when databases are connected and adapter received configuration.
*/
async onReady() {
jsonExplorer.sendVersionInfo(version);
this.log.info('Started with JSON-Explorer version ' + jsonExplorer.version);
this.intervall_time = Math.max(30, this.config.intervall) * 1000;
const tokenObject = await this.getObjectAsync('_config');
this.debugLog('T-Object from config is' + JSON.stringify(tokenObject));
this.accessToken = tokenObject && tokenObject.native && tokenObject.native.tokenSet ? tokenObject.native.tokenSet : null;
this.debugLog('accessT is ' + JSON.stringify(this.accessToken));
if (this.accessToken == null) {
this.accessToken = {};
this.accessToken.token = {};
this.accessToken.token.refresh_token = '';
}
await jsonExplorer.stateSetCreate('info.connection', 'connection', false);
await this.DoConnect();
}
async onMessage(msg) {
try {
if (typeof msg === 'object' && msg.command) {
switch (msg.command) {
case 'auth1': {
this.debugLog(`Received t_o_k_e_n creation Step 1 message`);
let that = this;
axiosInstanceToken.post(`/device_authorize?client_id=${client_id}&scope=offline_access`, {})
.then(function (responseRaw) {
let response = responseRaw.data;
that.log.debug('Response t_o_k_e_n Step 1 is ' + JSON.stringify(response));
that.device_code = response.device_code;
that.uri4token = response.verification_uri_complete;
msg.callback && that.sendTo(msg.from, msg.command, { error: `Copy address in your browser and proceed ${that.uri4token}` }, msg.callback);
that.debugLog('t_o_k_e_n Step 1 done');
})
.catch(error => {
this.log.error('Error at token creation Step 1 ' + error);
console.error('Error at t_o_k_e_n creation Step 1 ' + error);
if (error?.response?.data) {
console.error(error + ' with response ' + JSON.stringify(error.response.data));
this.log.error(error + ' with response ' + JSON.stringify(error.response.data));
}
if (error.message) error.message = `CreateT Step1 failed: ${error.message}`;
this.errorHandling(error);
});
break;
}
case 'auth2': {
this.debugLog(`Received t_o_k_e_n step 2 message`);
let that = this;
if (!this.device_code) {
this.log.error('Step 1 was not executed, but step 2 startet! Please start/restart with Step 1.');
break;
}
const uri = `/token?client_id=${client_id}&device_code=${this.device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code`;
this.debugLog('t_o_k_e_n Step 2 Url is ' + uri);
axiosInstanceToken.post(uri, {})
.then(async function (responseRaw) {
that.debugLog('Response t_o_k_e_n Step 2 is ' + JSON.stringify(responseRaw.data));
await that.manageNewToken(responseRaw.data);
msg.callback && that.sendTo(msg.from, msg.command, { error: `Done! Adapter starts now...` }, msg.callback);
that.debugLog('t_o_k_e_n Step 2 done');
await that.DoConnect();
})
.catch(error => {
this.log.error('Error at token creation Step 2 ' + error);
console.error('Error at t_o_k_e_n creation Step 2 ' + error);
if (error?.response?.data) {
let message = JSON.stringify(error.response.data);
if (message.includes('authorization_pending')) {
this.log.error(`Step 1 not completed. Open link '${that.uri4token}' in your browser and follow described steps on webpage`);
return;
} else {
console.error(error + ' with response ' + JSON.stringify(error.response.data));
this.log.error(error + ' with response ' + JSON.stringify(error.response.data));
}
}
if (error.message) error.message = `CreateT Step2 failed: ${error.message}`;
this.errorHandling(error);
});
break;
}
}
}
} catch (error) {
this.log.error(`Issue at token process: ${error}`);
if (error instanceof Error) this.errorHandling(error);
}
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
* @param {() => void} callback
*/
onUnload(callback) {
try {
//this.resetTimer();
this.log.info('cleaned everything up...');
callback();
} catch {
callback();
}
}
//////////////////////////////////////////////////////////////////////
/* ON STATE CHANGE */
//////////////////////////////////////////////////////////////////////
/**
* @param {string} id
* @param {ioBroker.State} state
* @param {string} homeId
* @param {string} roomId
* @param {string} deviceId
* @param {string} statename
* @param {string} beforeStatename
*/
async onStateChangeTadoX(id, state, homeId, roomId, deviceId, statename, beforeStatename) {
this.debugLog(id + ' changed');
const temperature = await this.getStateAsync(homeId + '.Rooms.' + roomId + '.setting.temperature.value');
const mode = await this.getStateAsync(homeId + '.Rooms.' + roomId + '.manualControlTermination.controlType');
const power = await this.getStateAsync(homeId + '.Rooms.' + roomId + '.setting.power');
const remainingTimeInSeconds = await this.getStateAsync(homeId + '.Rooms.' + roomId + '.manualControlTermination.remainingTimeInSeconds');
const nextTimeBlockStart = await this.getStateAsync(homeId + '.Rooms.' + roomId + '.nextTimeBlock.start');
const boostMode = await this.getStateAsync(homeId + '.Rooms.' + roomId + '.boostMode');
const set_boostMode = (boostMode == null || boostMode == undefined || boostMode.val == null || boostMode.val == '') ? false : toBoolean(boostMode.val);
const set_remainingTimeInSeconds = (remainingTimeInSeconds == null || remainingTimeInSeconds == undefined || remainingTimeInSeconds.val == null) ? 1800 : parseInt(remainingTimeInSeconds.val.toString());
const set_temp = (temperature == null || temperature == undefined || temperature.val == null || temperature.val == '') ? 20 : parseFloat(temperature.val.toString());
const set_NextTimeBlockStartExists = (nextTimeBlockStart == null || nextTimeBlockStart == undefined || nextTimeBlockStart.val == null || nextTimeBlockStart.val == '') ? false : true;
let set_power = (power == null || power == undefined || power.val == null || power.val == '') ? 'OFF' : power.val.toString().toUpperCase();
let set_terminationMode = (mode == null || mode == undefined || mode.val == null || mode.val == '') ? 'NO_OVERLAY' : mode.val.toString().toUpperCase();
this.debugLog('boostMode is: ' + set_boostMode);
this.debugLog('Power is: ' + set_power);
this.debugLog(`Temperature is: ${set_temp}`);
this.debugLog('Termination mode is: ' + set_terminationMode);
this.debugLog('RemainingTimeInSeconds is: ' + set_remainingTimeInSeconds);
this.debugLog('NextTimeBlockStart exists: ' + set_NextTimeBlockStartExists);
this.debugLog('DevicId is: ' + deviceId);
switch (statename) {
case ('power'):
if (set_terminationMode == 'NO_OVERLAY') {
if (set_power == 'ON') {
this.debugLog(`Overlay cleared for room '${roomId}' in home '${homeId}'`);
await this.setResumeRoomScheduleTadoX(homeId, roomId);
break;
}
else {
set_terminationMode = 'MANUAL';
}
}
await this.setManualControlTadoX(homeId, roomId, set_power, set_temp, set_terminationMode, set_boostMode, set_remainingTimeInSeconds);
if (set_power == 'OFF') jsonExplorer.stateSetCreate(homeId + '.Rooms.' + roomId + '.setting.temperature.value', 'value', null);
break;
case ('value'):
if (beforeStatename != 'temperature') {
this.log.warn('Change of ' + id + ' ignored'); break;
}
if (set_terminationMode == 'NO_OVERLAY') {
if (set_NextTimeBlockStartExists) set_terminationMode = 'NEXT_TIME_BLOCK';
else set_terminationMode = 'MANUAL';
}
set_power = 'ON';
await this.setManualControlTadoX(homeId, roomId, set_power, set_temp, set_terminationMode, set_boostMode, set_remainingTimeInSeconds);
break;
case ('boost'):
if (state.val == true) {
await this.setBoostTadoX(homeId);
await jsonExplorer.sleep(1000);
this.create_state(id, 'boost', false);
}
break;
case ('resumeScheduleHome'):
if (state.val == true) {
await this.setResumeHomeScheduleTadoX(homeId);
await jsonExplorer.sleep(1000);
this.create_state(id, 'resumeScheduleHome', false);
}
break;
case ('resumeScheduleRoom'):
if (state.val == true) {
await this.setResumeRoomScheduleTadoX(homeId, roomId);
await jsonExplorer.sleep(1000);
this.create_state(id, 'resumeScheduleRoom', false);
}
break;
case ('allOff'):
if (state.val == true) {
await this.setAllOffTadoX(homeId);
await jsonExplorer.sleep(1000);
this.create_state(id, 'allOff', false);
}
break;
case ('remainingTimeInSeconds'):
set_terminationMode = 'TIMER';
await this.setManualControlTadoX(homeId, roomId, set_power, set_temp, set_terminationMode, set_boostMode, set_remainingTimeInSeconds);
break;
case ('controlType'):
if (beforeStatename != 'manualControlTermination') {
this.log.warn('Change of ' + id + ' ignored'); break;
}
await this.setManualControlTadoX(homeId, roomId, set_power, set_temp, set_terminationMode, set_boostMode, set_remainingTimeInSeconds);
break;
}
}
/**
* Is called if a subscribed state changes
* @param {string} id
* @param {ioBroker.State | null | undefined} state
*/
async onStateChange(id, state) {
if (state) {
// The state was changed
if (state.ack === false) {
if (this.oldStatesVal[id] === state.val) {
this.debugLog(`State ${id} did not change, value is ${state.val}. No further actions!`);
return;
}
try {
this.debugLog('GETS INTERESSTING!!!');
const idSplitted = id.split('.');
const homeId = idSplitted[2];
const zoneId = idSplitted[4];
const deviceId = idSplitted[6];
const statename = idSplitted[idSplitted.length - 1];
const beforeStatename = idSplitted[idSplitted.length - 2];
this.debugLog(`Attribute '${id}' changed. '${statename}' will be checked.`);
if (statename != 'meterReadings' && statename != 'presence') {
if (this.isTadoX) {
await this.onStateChangeTadoX(id, state, homeId, zoneId, deviceId, statename, beforeStatename);
return;
}
}
if (statename == 'meterReadings') {
let meterReadings = {};
try {
meterReadings = JSON.parse(String(state.val));
} catch (error) {
this.log.error(`'${state.val}' is not a valide JSON for meterReadings - ${error}`);
return;
}
if (meterReadings.date && meterReadings.reading) {
let date = String(meterReadings.date);
if (typeof meterReadings.reading != 'number') {
this.log.error('meterReadings.reading is not a number!');
return;
}
let regEx = /^\d{4}-\d{2}-\d{2}$/;
if (!date.match(regEx)) {
this.log.error('meterReadings.date has other format than YYYY-MM-DD');
return;
}
await this.setReading(homeId, meterReadings);
} else {
this.log.error('meterReadings does not contain date and reading');
return;
}
}
else if (statename == 'offsetCelsius') {
const offset = state;
let set_offset = (offset == null || offset == undefined || offset.val == null) ? 0 : parseFloat(offset.val.toString());
this.debugLog(`Offset changed for device '${deviceId}' in home '${homeId}' to value '${set_offset}'`);
this.setTemperatureOffset(homeId, zoneId, deviceId, set_offset);
} else if (statename == 'childLockEnabled') {
const childLockEnabled = state;
let set_childLockEnabled = (childLockEnabled == null || childLockEnabled == undefined || childLockEnabled.val == null || childLockEnabled.val == '') ? false : toBoolean(childLockEnabled.val);
this.debugLog(`ChildLockEnabled changed for device '${deviceId}' in home '${homeId}' to value '${set_childLockEnabled}'`);
this.setChildLock(homeId, zoneId, deviceId, set_childLockEnabled);
} else if (statename == 'tt_id') {
const tt_id = state;
let set_tt_id = (tt_id == null || tt_id == undefined || tt_id.val == null || tt_id.val == '') ? 0 : parseInt(tt_id.val.toString());
this.debugLog(`TimeTable changed for room '${zoneId}' in home '${homeId}' to value '${set_tt_id}'`);
this.setActiveTimeTable(homeId, zoneId, set_tt_id);
} else if (statename == 'presence') {
const presence = state;
let set_presence = (presence == null || presence == undefined || presence.val == null || presence.val == '') ? 'HOME' : presence.val.toString().toUpperCase();
this.debugLog(`Presence changed in home '${homeId}' to value '${set_presence}'`);
this.setPresenceLock(homeId, set_presence);
} else if (statename == 'masterswitch') {
const masterswitch = state;
let set_masterswitch = (masterswitch == null || masterswitch == undefined || masterswitch.val == null || masterswitch.val == '') ? 'unknown' : masterswitch.val.toString().toUpperCase();
this.debugLog(`Masterswitch changed in home '${homeId}' to value '${set_masterswitch}'`);
await this.setMasterSwitch(set_masterswitch);
await this.sleep(1000);
await this.setState(`${homeId}.Home.masterswitch`, '', true);
} else if (statename == 'activateOpenWindow') {
this.debugLog(`Activate Open Window for room '${zoneId}' in home '${homeId}'`);
await this.setActivateOpenWindow(homeId, zoneId);
} else if (idSplitted[idSplitted.length - 2] === 'openWindowDetection' && (statename == 'openWindowDetectionEnabled' || statename == 'timeoutInSeconds')) {
const openWindowDetectionEnabled = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.openWindowDetection.openWindowDetectionEnabled');
const openWindowDetectionTimeoutInSeconds = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.openWindowDetection.timeoutInSeconds');
let set_openWindowDetectionEnabled = (openWindowDetectionEnabled == null || openWindowDetectionEnabled == undefined || openWindowDetectionEnabled.val == null || openWindowDetectionEnabled.val == '') ? false : toBoolean(openWindowDetectionEnabled.val);
let set_openWindowDetectionTimeoutInSeconds = (openWindowDetectionTimeoutInSeconds == null || openWindowDetectionTimeoutInSeconds == undefined || openWindowDetectionTimeoutInSeconds.val == null || openWindowDetectionTimeoutInSeconds.val == '') ? 900 : Number(openWindowDetectionTimeoutInSeconds.val);
this.debugLog('Open Window Detection enabled: ' + set_openWindowDetectionEnabled);
this.debugLog('Open Window Detection Timeout is: ' + set_openWindowDetectionTimeoutInSeconds);
this.debugLog(`Changing open window detection for '${zoneId}' in home '${homeId}'`);
await this.setOpenWindowDetectionSettings(homeId, zoneId, {
enabled: set_openWindowDetectionEnabled,
timeoutInSeconds: set_openWindowDetectionTimeoutInSeconds
});
} else {
const type = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.setting.type');
const temperature = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.setting.temperature.celsius');
const mode = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.overlay.termination.typeSkillBasedApp');
const power = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.setting.power');
const durationInSeconds = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.overlay.termination.durationInSeconds');
const nextTimeBlockStart = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.nextTimeBlock.start');
let acMode, fanLevel, horizontalSwing, verticalSwing, fanSpeed, swing, light;
let set_type = (type == null || type == undefined || type.val == null || type.val == '') ? 'HEATING' : type.val.toString().toUpperCase();
let set_durationInSeconds = (durationInSeconds == null || durationInSeconds == undefined || durationInSeconds.val == null) ? 1800 : parseInt(durationInSeconds.val.toString());
let set_temp = (temperature == null || temperature == undefined || temperature.val == null || temperature.val == '') ? 20 : parseFloat(temperature.val.toString());
let set_power = (power == null || power == undefined || power.val == null || power.val == '') ? 'OFF' : power.val.toString().toUpperCase();
let set_mode = (mode == null || mode == undefined || mode.val == null || mode.val == '') ? 'NO_OVERLAY' : mode.val.toString().toUpperCase();
let set_NextTimeBlockStartExists = (nextTimeBlockStart == null || nextTimeBlockStart == undefined || nextTimeBlockStart.val == null || nextTimeBlockStart.val == '') ? false : true;
if (set_type == 'AIR_CONDITIONING') {
acMode = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.setting.mode');
fanSpeed = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.setting.fanSpeed');
fanLevel = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.setting.fanLevel');
horizontalSwing = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.setting.horizontalSwing');
verticalSwing = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.setting.verticalSwing');
swing = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.setting.swing');
light = await this.getStateAsync(homeId + '.Rooms.' + zoneId + '.setting.light');
}
let set_light = '', set_swing = '', set_horizontalSwing = '', set_verticalSwing = '', set_fanLevel = '', set_fanSpeed = '', set_acMode = '';
if (acMode == undefined) set_acMode = 'NOT_AVAILABLE';
else {
set_acMode = (acMode == null || acMode.val == null || acMode.val == '') ? 'COOL' : acMode.val.toString().toUpperCase();
}
if (fanSpeed == undefined) set_fanSpeed = 'NOT_AVAILABLE';
else {
set_fanSpeed = (fanSpeed == null || fanSpeed.val == null || fanSpeed.val == '') ? 'AUTO' : fanSpeed.val.toString().toUpperCase();
}
if (fanLevel == undefined) set_fanLevel = 'NOT_AVAILABLE';
else {
set_fanLevel = (fanLevel == null || fanLevel.val == null || fanLevel.val == '') ? 'AUTO' : fanLevel.val.toString().toUpperCase();
}
if (horizontalSwing == undefined) set_horizontalSwing = 'NOT_AVAILABLE';
else {
set_horizontalSwing = (horizontalSwing == null || horizontalSwing.val == null || horizontalSwing.val == '') ? 'OFF' : horizontalSwing.val.toString().toUpperCase();
}
if (verticalSwing == undefined) set_verticalSwing = 'NOT_AVAILABLE';
else {
set_verticalSwing = (verticalSwing == null || verticalSwing.val == null || verticalSwing.val == '') ? 'OFF' : verticalSwing.val.toString().toUpperCase();
}
if (swing == undefined) set_swing = 'NOT_AVAILABLE';
else {
set_swing = (swing == null || swing.val == null || swing.val == '') ? 'OFF' : swing.val.toString().toUpperCase();
}
if (light == undefined) set_light = 'NOT_AVAILABLE';
else {
set_light = (light == null || light.val == null || light.val == '') ? 'OFF' : light.val.toString().toUpperCase();
}
this.debugLog('Type is: ' + set_type);
this.debugLog('Power is: ' + set_power);
this.debugLog(`Temperature is: ${set_temp}`);
this.debugLog('Execution mode (typeSkillBasedApp) is: ' + set_mode);
this.debugLog('DurationInSeconds is: ' + set_durationInSeconds);
this.debugLog('NextTimeBlockStart exists: ' + set_NextTimeBlockStartExists);
this.debugLog('Mode is: ' + set_acMode);
this.debugLog('FanSpeed is: ' + set_fanSpeed);
this.debugLog('FanLevel is: ' + set_fanLevel);
this.debugLog('HorizontalSwing is: ' + set_horizontalSwing);
this.debugLog('VerticalSwing is: ' + set_verticalSwing);
this.debugLog('Swing is: ' + set_swing);
this.debugLog('Light is: ' + set_light);
switch (statename) {
case ('overlayClearZone'):
this.debugLog(`Overlay cleared for room '${zoneId}' in home '${homeId}'`);
await this.setClearZoneOverlay(homeId, zoneId);
break;
case ('fahrenheit'): //do the same as with celsius but just convert to celsius
case ('celsius'):
if (statename == 'fahrenheit') {
set_temp = Math.round((5 / 9) * (Number(state.val) - 32) * 10) / 10;
}
if (set_mode == 'NO_OVERLAY') {
if (set_NextTimeBlockStartExists) set_mode = 'NEXT_TIME_BLOCK';
else set_mode = 'MANUAL';
}
set_power = 'ON';
this.debugLog(`Temperature changed for room '${zoneId}' in home '${homeId}' to '${set_temp}'`);
await this.setZoneOverlay(homeId, zoneId, set_power, set_temp, set_mode, set_durationInSeconds, set_type, set_acMode, set_fanLevel, set_horizontalSwing, set_verticalSwing, set_fanSpeed, set_swing, set_light);
break;
case ('durationInSeconds'):
set_mode = 'TIMER';
this.debugLog(`DurationInSecond changed for room '${zoneId}' in home '${homeId}' to '${set_durationInSeconds}'`);
await this.setState(`${homeId}.Rooms.${zoneId}.overlay.termination.typeSkillBasedApp`, set_mode, true);
await this.setZoneOverlay(homeId, zoneId, set_power, set_temp, set_mode, set_durationInSeconds, set_type, set_acMode, set_fanLevel, set_horizontalSwing, set_verticalSwing, set_fanSpeed, set_swing, set_light);
break;
case ('fanSpeed'):
this.debugLog(`FanSpeed changed for room '${zoneId}' in home '${homeId}' to '${set_fanSpeed}'`);
await this.setZoneOverlay(homeId, zoneId, set_power, set_temp, set_mode, set_durationInSeconds, set_type, set_acMode, set_fanLevel, set_horizontalSwing, set_verticalSwing, set_fanSpeed, set_swing, set_light);
break;
case ('mode'):
this.debugLog(`Mode changed for room '${zoneId}' in home '${homeId}' to '${set_acMode}'`);
await this.setZoneOverlay(homeId, zoneId, set_power, set_temp, set_mode, set_durationInSeconds, set_type, set_acMode, set_fanLevel, set_horizontalSwing, set_verticalSwing, set_fanSpeed, set_swing, set_light);
break;
case ('fanLevel'):
this.debugLog(`fanLevel changed for room '${zoneId}' in home '${homeId}' to '${set_fanLevel}'`);
await this.setZoneOverlay(homeId, zoneId, set_power, set_temp, set_mode, set_durationInSeconds, set_type, set_acMode, set_fanLevel, set_horizontalSwing, set_verticalSwing, set_fanSpeed, set_swing, set_light);
break;
case ('swing'):
this.debugLog(`swing changed for room '${zoneId}' in home '${homeId}' to '${set_swing}'`);
await this.setZoneOverlay(homeId, zoneId, set_power, set_temp, set_mode, set_durationInSeconds, set_type, set_acMode, set_fanLevel, set_horizontalSwing, set_verticalSwing, set_fanSpeed, set_swing, set_light);
break;
case ('light'):
this.debugLog(`light changed for room '${zoneId}' in home '${homeId}' to '${set_light}'`);
await this.setZoneOverlay(homeId, zoneId, set_power, set_temp, set_mode, set_durationInSeconds, set_type, set_acMode, set_fanLevel, set_horizontalSwing, set_verticalSwing, set_fanSpeed, set_swing, set_light);
break;
case ('horizontalSwing'):
this.debugLog(`horizontalSwing changed for room '${zoneId}' in home '${homeId}' to '${set_horizontalSwing}'`);
await this.setZoneOverlay(homeId, zoneId, set_power, set_temp, set_mode, set_durationInSeconds, set_type, set_acMode, set_fanLevel, set_horizontalSwing, set_verticalSwing, set_fanSpeed, set_swing, set_light);
break;
case ('verticalSwing'):
this.debugLog(`verticalSwing changed for room '${zoneId}' in home '${homeId}' to '${set_verticalSwing}'`);
await this.setZoneOverlay(homeId, zoneId, set_power, set_temp, set_mode, set_durationInSeconds, set_type, set_acMode, set_fanLevel, set_horizontalSwing, set_verticalSwing, set_fanSpeed, set_swing, set_light);
break;
case ('typeSkillBasedApp'):
if (set_mode == 'NO_OVERLAY') { break; }
this.debugLog(`TypeSkillBasedApp changed for room '${zoneId}' in home '${homeId}' to '${set_mode}'`);
await this.setZoneOverlay(homeId, zoneId, set_power, set_temp, set_mode, set_durationInSeconds, set_type, set_acMode, set_fanLevel, set_horizontalSwing, set_verticalSwing, set_fanSpeed, set_swing, set_light);
if (set_mode == 'MANUAL') {
await this.setState(`${homeId}.Rooms.${zoneId}.overlay.termination.expiry`, null, true);
await this.setState(`${homeId}.Rooms.${zoneId}.overlay.termination.durationInSeconds`, null, true);
await this.setState(`${homeId}.Rooms.${zoneId}.overlay.termination.remainingTimeInSeconds`, null, true);
}
break;
case ('power'):
if (set_mode == 'NO_OVERLAY') {
if (set_power == 'ON') {
this.debugLog(`Overlay cleared for room '${zoneId}' in home '${homeId}'`);
await this.setClearZoneOverlay(homeId, zoneId);
}
else {
set_mode = 'MANUAL';
this.debugLog(`Power changed for room '${zoneId}' in home '${homeId}' to '${state.val}' and temperature '${set_temp}' and mode '${set_mode}'`);
await this.setZoneOverlay(homeId, zoneId, set_power, set_temp, set_mode, set_durationInSeconds, set_type, set_acMode, set_fanLevel, set_horizontalSwing, set_verticalSwing, set_fanSpeed, set_swing, set_light);
}
} else {
this.debugLog(`Power changed for room '${zoneId}' in home '${homeId}' to '${state.val}' and temperature '${set_temp}' and mode '${set_mode}'`);
await this.setZoneOverlay(homeId, zoneId, set_power, set_temp, set_mode, set_durationInSeconds, set_type, set_acMode, set_fanLevel, set_horizontalSwing, set_verticalSwing, set_fanSpeed, set_swing, set_light);
}
break;
default:
}
}
this.debugLog('State change detected from different source than adapter');
this.debugLog(`state ${id} changed: ${state.val} (ack = ${state.ack})`);
} catch (error) {
this.log.error(`Issue at state change: ${error}`);
console.error(`Issue at state change: ${error}`);
if (error instanceof Error) this.errorHandling(error);
}
} else {
this.oldStatesVal[id] = state.val;
//this.debugLog(`Changed value ${state.val} for ID ${id} stored`);
//this.debugLog(`state ${id} changed: ${state.val} (ack = ${state.ack})`);
}
} else {
// The state was deleted
this.debugLog(`state ${id} deleted`);
}
}
//////////////////////////////////////////////////////////////////////
/* SET API CALLS */
//////////////////////////////////////////////////////////////////////
/**
* @param {string} homeId
* @param {string} roomId
* @param {string} power
* @param {number} temperature
* @param {string} terminationMode
* @param {boolean} boostMode
* @param {number} durationInSeconds
*/
async setManualControlTadoX(homeId, roomId, power, temperature, terminationMode, boostMode, durationInSeconds) {
//{`"setting`":{`"power`":`"ON`",`"isBoost`":false,`"temperature`":{`"value`":18.5,`"valueRaw`":18.52,`"precision`":0.1}},`"termination`":{`"type`":`"NEXT_TIME_BLOCK`"}}
if (power != 'ON' && power != 'OFF') throw new Error(`Power has value ${power} but should have the value 'ON' or 'OFF'.`);
if (terminationMode != 'NEXT_TIME_BLOCK' && terminationMode != 'MANUAL' && terminationMode != 'TIMER') throw new Error(`TerminationMode has value ${terminationMode} but should have 'NEXT_TIMEBLOCK' or 'MANUAL' or 'TIMER'.`);
temperature = Math.round(temperature * 10) / 10;
let payload = {};
payload.termination = {};
payload.termination.type = terminationMode;
payload.setting = {};
payload.setting.power = power;
payload.setting.isBoost = toBoolean(boostMode);
if (power == 'OFF') payload.setting.temperature = null;
else {
payload.setting.temperature = {};
payload.setting.temperature.value = temperature;
}
if (terminationMode == 'TIMER') payload.termination.durationInSeconds = durationInSeconds;
this.debugLog('setManualControlTadoX() payload is ' + JSON.stringify(payload));
let apiResponse = await this.apiCall(`${tadoX_url}/homes/${homeId}/rooms/${roomId}/manualControl`, 'post', payload);
this.debugLog('setManualControlTadoX() response is ' + JSON.stringify(apiResponse));
await this.DoRoomsStateTadoX(homeId, roomId);
}
/**
* @param {string} homeId
* @param {string} roomId
*/
async setResumeRoomScheduleTadoX(homeId, roomId) {
let apiResponse = await this.apiCall(`${tadoX_url}/homes/${homeId}/rooms/${roomId}/resumeSchedule`, 'post');
this.debugLog('setResumeRoomScheduleTadoX() response is ' + JSON.stringify(apiResponse));
await this.DoRoomsStateTadoX(homeId, roomId);
}
/**
* @param {string} homeId
*/
async setResumeHomeScheduleTadoX(homeId) {
let apiResponse = await this.apiCall(`${tadoX_url}/homes/${homeId}/quickActions/resumeSchedule`, 'post');
this.debugLog('setResumeHomeScheduleTadoX() response is ' + JSON.stringify(apiResponse));
await this.DoRoomsTadoX(homeId);
}
/**
* @param {string} homeId
*/
async setBoostTadoX(homeId) {
let apiResponse = await this.apiCall(`${tadoX_url}/homes/${homeId}/quickActions/boost`, 'post');
this.debugLog('setBoostTadoX() response is ' + JSON.stringify(apiResponse));
await this.DoRoomsTadoX(homeId);
}
/**
* @param {string} homeId
*/
async setAllOffTadoX(homeId) {
let apiResponse = await this.apiCall(`${tadoX_url}/homes/${homeId}/quickActions/allOff`, 'post');
this.debugLog('setAllOffTadoX() response is ' + JSON.stringify(apiResponse));
await this.DoRoomsTadoX(homeId);
}
/**
* @param {string} homeId
* @param {string} zoneId
*/
async setClearZoneOverlay(homeId, zoneId) {
try {
let url = `/api/v2/homes/${homeId}/zones/${zoneId}/overlay`;
if (await isOnline() == false) {
throw new Error('No internet connection detected!');
}
await this.apiCall(url, 'delete');
this.debugLog(`Called 'DELETE ${url}'`);
await jsonExplorer.setLastStartTime();
await this.DoZoneStates(homeId, zoneId);
this.debugLog('CheckExpire() at clearZoneOverlay() started');
await jsonExplorer.checkExpire(homeId + '.Rooms.' + zoneId + '.overlay.*');
}
catch (error) {
this.log.error(`Issue at clearZoneOverlay(): '${error}'`);
console.error(`Issue at clearZoneOverlay(): '${error}'`);
if (error instanceof Error) this.errorHandling(error);
}
}
/**
* @param {string} homeId
* @param {string} zoneId
* @param {string} deviceId
* @param {number} set_offset
*/
async setTemperatureOffset(homeId, zoneId, deviceId, set_offset) {
if (!set_offset) set_offset = 0;
if (set_offset <= -10 || set_offset > 10) this.log.warn('Offset out of range +/-10°');
set_offset = Math.round(set_offset * 100) / 100;
const offset = {
celsius: Math.min(10, Math.max(-9.99, set_offset))
};
try {
if (await isOnline() == false) {
throw new Error('No internet connection detected!');
}
let apiResponse = await this.apiCall(`/api/v2/devices/${deviceId}/temperatureOffset`, 'put', offset);
this.debugLog(`API 'temperatureOffset' for home '${homeId}' and deviceID '${deviceId}' with body ${JSON.stringify(offset)} called.`);
this.debugLog(`Response from 'temperatureOffset' is ${JSON.stringify(apiResponse)}`);
if (apiResponse) await this.DoTemperatureOffset(homeId, zoneId, deviceId, apiResponse);
}
catch (error) {
let eMsg = `Issue at setTemperatureOffset: '${error}'. Based on body ${JSON.stringify(offset)}`;
this.log.error(eMsg);
console.error(eMsg);
if (error instanceof Error) this.errorHandling(error);
}
}
/**
* @param {string} homeId
* @param {string} zoneId
* @param {number} timetableId
*/
async setActiveTimeTable(homeId, zoneId, timetableId) {
if (!timetableId) timetableId = 0;
if (!(timetableId == 0 || timetableId == 1 || timetableId == 2)) {
this.log.error(`Invalid value '${timetableId}' for state 'timetable_id'. Allowed values are '0', '1' and '2'.`);
return;
}
const timeTable = {
id: timetableId
};
let apiResponse;
this.debugLog('setActiveTimeTable JSON ' + JSON.stringify(timeTable));
//this.log.info(`Call API 'activeTimetable' for home '${homeId}' and zone '${zoneId}' with body ${JSON.stringify(timeTable)}`);
try {
if (await isOnline() == false) {
throw new Error('No internet connection detected!');
}
apiResponse = await this.apiCall(`/api/v2/homes/${homeId}/zones/${zoneId}/schedule/activeTimetable`, 'put', timeTable);
if (apiResponse) await this.DoTimeTables(homeId, zoneId, apiResponse);
this.debugLog(`API 'activeTimetable' for home '${homeId}' and zone '${zoneId}' with body ${JSON.stringify(timeTable)} called.`);
this.debugLog(`Response from 'setActiveTimeTable' is ${JSON.stringify(apiResponse)}`);
}
catch (error) {
let eMsg = `Issue at setActiveTimeTable: '${error}'. Based on body ${JSON.stringify(timeTable)}`;
this.log.error(eMsg);
console.error(eMsg);
if (error instanceof Error) this.errorHandling(error);
}
}
/**
* @param {string} homeId
* @param {string} homePresence
*/
async setPresenceLock(homeId, homePresence) {
if (!homePresence) homePresence = 'HOME';
if (homePresence !== 'HOME' && homePresence !== 'AWAY' && homePresence !== 'AUTO') {
this.log.error(`Invalid value '${homePresence}' for state 'homePresence'. Allowed values are HOME, AWAY and AUTO.`);
return;
}
const homeState = {
homePresence: homePresence.toUpperCase()
};
let apiResponse;
this.debugLog('homePresence JSON ' + JSON.stringify(homeState));
//this.log.info(`Call API 'activeTimetable' for home '${homeId}' and zone '${zoneId}' with body ${JSON.stringify(timeTable)}`);
try {
if (await isOnline() == false) {
throw new Error('No internet connection detected!');
}
if (homePresence === 'AUTO') {
apiResponse = await this.apiCall(`/api/v2/homes/${homeId}/presenceLock`, 'delete');
} else {
apiResponse = await this.apiCall(`/api/v2/homes/${homeId}/presenceLock`, 'put', homeState);
}
await this.DoHomeState(homeId);
this.debugLog(`API 'state' for home '${homeId}' with body ${JSON.stringify(homeState)} called.`);
this.debugLog(`Response from 'presenceLock' is ${JSON.stringify(apiResponse)}`);
}
catch (error) {
let eMsg = `Issue at setPresenceLock: '${error}'. Based on body ${JSON.stringify(homeState)}`;
this.log.error(eMsg);
console.error(eMsg);
if (error instanceof Error) this.errorHandling(error);
}
}
/**
* @param {string} homeId
* @param {string} zoneId
* @param {string} power
* @param {number} temperature
* @param {string} typeSkillBasedApp
* @param {number} durationInSeconds
* @param {string} type
* @param {string} acMode
* @param {string} fanLevel
* @param {string} horizontalSwing
* @param {string} verticalSwing
* @param {string} fanSpeed
* @param {string} swing
* @param {string} light
*/
async setZoneOverlay(homeId, zoneId, power, temperature, typeSkillBasedApp, durationInSeconds, type, acMode, fanLevel, horizontalSwing, verticalSwing, fanSpeed, swing, light) {
power = power.toUpperCase();
typeSkillBasedApp = typeSkillBasedApp.toUpperCase();
durationInSeconds = Math.max(10, durationInSeconds);
type = type.toUpperCase();
fanSpeed = fanSpeed.toUpperCase();
acMode = acMode.toUpperCase();
fanLevel = fanLevel.toUpperCase();
horizontalSwing = horizontalSwing.toUpperCase();
verticalSwing = verticalSwing.toUpperCase();
swing = swing.toUpperCase();
light = light.toUpperCase();
if (!temperature) temperature = 21;
temperature = Math.round(temperature * 100) / 100;
let config = {
setting: {
type: type,
}
};
try {
config.setting.power = power;
if (typeSkillBasedApp != 'NO_OVERLAY') {
config.termination = {};
config.termination.typeSkillBasedApp = typeSkillBasedApp;
if (typeSkillBasedApp != 'TIMER') {
config.termination.durationInSeconds = null;
}
else {
config.termination.durationInSeconds = durationInSeconds;
}
}
if (type != 'HEATING' && type != 'AIR_CONDITIONING' && type != 'HOT_WATER') {
this.log.error(`Invalid value '${type}' for state 'type'. Supported values are HOT_WATER, AIR_CONDITIONING and HEATING`);
return;
}
if (power != 'ON' && power != 'OFF') {
this.log.error(`Invalid value '${power}' for state 'power'. Supported values are ON and OFF`);
return;
}
if (typeSkillBasedApp != 'TIMER' && typeSkillBasedApp != 'MANUAL' && typeSkillBasedApp != 'NEXT_TIME_BLOCK' && typeSkillBasedApp != 'NO_OVERLAY' && typeSkillBasedApp != 'TADO_MODE') {
this.log.error(`Invalid value '${typeSkillBasedApp}' for state 'typeSkillBasedApp'. Allowed values are TIMER, MANUAL and NEXT_TIME_BLOCK`);
return;
}
/* Capability Management
{"1":{"type":"HEATING","temperatures":{"celsius":{"min":5,"max":25,"step":0.1},"fahrenheit":{"min":41,"max":77,"step":0.1}}}}
{"0":{"type":"HOT_WATER","canSetTemperature":true,"temperatures":{"celsius":{"min":30,"max":65,"step":1},"fahrenheit":{"min":86,"max":149,"step":1}}}}
{"0":{"type":"HOT_WATER","canSetTemperature":false}}
{"1":{"type":"AIR_CONDITIONING","COOL":{"temperatures":{"celsius":{"min":16,"max":30,"step":1},"fahrenheit":{"min":61,"max":86,"step":1}},"fanSpeeds":["AUTO","HIGH","MIDDLE","LOW"],"swings":["OFF","ON"]},"DRY":{"fanSpeeds":["MIDDLE","LOW"],"swings":["OFF","ON"]},"FAN":{"fanSpeeds":["HIGH","MIDDLE","LOW"],"swings":["OFF","ON"]},"HEAT":{"temperatures":{"celsius":{"min":16,"max":30,"step":1},"fahrenheit":{"min":61,"max":86,"step":1}},"fanSpeeds":["AUTO","HIGH","MIDDLE","LOW"],"swings":["OFF","ON"]},"initialStates":{"mode":"COOL","modes":{"COOL":{"temperature":{"celsius":23,"fahrenheit":74},"fanSpeed":"LOW","swing":"OFF"},"DRY":{"fanSpeed":"LOW","swing":"OFF"},"FAN":{"fanSpeed":"LOW","swing":"OFF"},"HEAT":{"temperature":{"celsius":23,"fahrenheit":74},"fanSpeed":"LOW","swing":"OFF"}}}},"3":{"type":"AIR_CONDITIONING","COOL":{"temperatures":{"celsius":{"min":16,"max":30,"step":1},"fahrenheit":{"min":61,"max":86,"step":1}},"fanSpeeds":["AUTO","HIGH","MIDDLE","LOW"],"swings":["OFF","ON"]},"DRY":{"fanSpeeds":["MIDDLE","LOW"],"swings":["OFF","ON"]},"FAN":{"fanSpeeds":["HIGH","MIDDLE","LOW"],"swings":["OFF","ON"]},"HEAT":{"temperatures":{"celsius":{"min":16,"max":30,"step":1},"fahrenheit":{"min":61,"max":86,"step":1}},"fanSpeeds":["AUTO","HIGH","MIDDLE","LOW"],"swings":["OFF","ON"]},"initialStates":{"mode":"COOL","modes":{"COOL":{"temperature":{"celsius":23,"fahrenheit":74},"fanSpeed":"LOW","swing":"OFF"},"DRY":{"fanSpeed":"LOW","swing":"OFF"},"FAN":{"fanSpeed":"LOW","swing":"OFF"},"HEAT":{"temperature"