iobroker.tado
Version:
Tado cloud connector to control Tado devices
1,025 lines (964 loc) • 109 kB
JavaScript
'use strict';
const utils = require('@iobroker/adapter-core');
const TadoApi = require('./lib/tadoApi.js');
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 axios = require('axios');
const { version } = require('./package.json');
const debounce = require('lodash.debounce');
const TOKEN_EXPIRATION_WINDOW = 10;
const TOKEN_API_TIMEOUT = 10000; //10s
const TOKEN_BASE_URL = `https://login.tado.com/oauth2`;
const TADO_X_URL = `https://hops.tado.com`;
const CLIENT_ID = `1bb50063-6b0c-4d11-bd99-387f4a91cc46`;
const DEBOUNCE_TIME = 750; //750ms debouncing (waiting if further calls come in and just execute the last one)
const DELAY_AFTER_CALL = 300; //300ms pause between api calls
// @ts-expect-error create axios instance
const axiosInstanceToken = axios.create({
timeout: TOKEN_API_TIMEOUT,
baseURL: TOKEN_BASE_URL,
});
let polling; // Polling timer
let pooltimer = [];
let outdated = {
//${homeId}.Mobile_Devices
manageMobileDevices: {
isOutdated: false,
lastUpdate: 0,
intervall: 60,
},
//${homeId}.Weather
manageWeather: {
isOutdated: false,
lastUpdate: 0,
intervall: 10,
},
//${homeId}.Rooms
manageZones: {
isOutdated: false,
lastUpdate: 0,
intervall: 60,
},
//${homeId}.Rooms.${zoneId}.devices.${deviceId}.offset
manageTemperatureOffset: {
isOutdated: false,
lastUpdate: 0,
intervall: 60 * 24,
},
//${homeId}.Rooms.${zoneId}.timeTables
manageTimeTables: {
isOutdated: false,
lastUpdate: 0,
intervall: 60 * 24,
},
//${homeId}.Rooms.${zoneId}.awayConfig
manageAwayConfiguration: {
isOutdated: false,
lastUpdate: 0,
intervall: 60 * 24,
},
//${homeId}.Home.state
manageHomeState: {
isOutdated: false,
lastUpdate: 60,
},
};
for (let key in outdated) {
outdated[key].intervall = outdated[key].intervall * 60 * 1000;
}
class Tado extends utils.Adapter {
/**
* @param {Partial<ioBroker.AdapterOptions>} [options]
*/
constructor(options) {
// @ts-expect-error Call super constructor
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.lastupdate = 0;
this.retryCount = 0;
this.apiCallinExecution = false;
this.intervall_time = 60 * 1000;
this.oldStatesVal = [];
this.isTadoX = false;
this.numberOfCalls = { day: new Date().getDate(), calls: 0 };
//data objects
this.getMeData = null;
this.homeData = null;
this.zonesData = {};
this.timeTablesData = {};
this.awayConfigurationData = {};
this.roomCapabilities = {};
//token
this.device_code = '';
this.uri4token = '';
this.accessToken = {};
this.refreshTokenInProgress = false;
this.shouldRefreshToken = false;
this.api = new TadoApi(this, DELAY_AFTER_CALL); //pause between calls
this.debouncedSetZoneOverlay = debounce(
(homeId, zoneId, config, resolve, reject) => {
this._setZoneOverlay(homeId, zoneId, config).then(resolve).catch(reject);
},
DEBOUNCE_TIME, // debouncing (waiting if further calls come in and just execute the last one)
);
this.debouncedSetManualControlTadoX = debounce(
(homeId, roomId, power, temperature, terminationMode, boostMode, durationInSeconds, resolve, reject) => {
this._setManualControlTadoX(homeId, roomId, power, temperature, terminationMode, boostMode, durationInSeconds)
.then(resolve)
.catch(reject);
},
DEBOUNCE_TIME,
);
}
/**
* 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}`);
//read config
this.intervall_time = Math.max(30, this.config.intervall) * 1000;
this.logCalls = this.config.logCalls || false;
this.logCallsDetail = this.config.logCallsDetail || false;
this.intervall_time = Math.max(60, this.config.standard || 60) * 1000;
outdated.manageMobileDevices.intervall = (this.config.mobildevices || 0) * 60 * 1000;
outdated.manageWeather.intervall = (this.config.weather || 0) * 60 * 1000;
outdated.manageZones.intervall = (this.config.zones || 0) * 60 * 1000;
outdated.manageTemperatureOffset.intervall = (this.config.temperatureOffset || 0) * 60 * 1000;
outdated.manageHomeState.intervall = (this.config.homeState || 0) * 60 * 1000;
outdated.manageAwayConfiguration.intervall = (this.config.awayConfiguration || 0) * 60 * 1000;
outdated.manageTimeTables.intervall = (this.config.timeTables || 0) * 60 * 1000;
for (let key in outdated) {
if (outdated[key].intervall === 0) {
outdated[key].intervall = 999999999999999;
}
this.debugLog(`${key}: ${outdated[key].intervall}`);
}
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.connect();
}
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`);
axiosInstanceToken
.post(`/device_authorize?client_id=${CLIENT_ID}&scope=offline_access`, {})
.then(responseRaw => {
let response = responseRaw.data;
this.log.debug(`Response t_o_k_e_n Step 1 is ${JSON.stringify(response)}`);
this.device_code = response.device_code;
this.uri4token = response.verification_uri_complete;
msg.callback &&
this.sendTo(
msg.from,
msg.command,
{ error: `Copy address in your browser and proceed ${this.uri4token}` },
msg.callback,
);
this.log.info(`Copy address in your browser and proceed ${this.uri4token}`);
this.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`);
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 responseRaw => {
this.debugLog(`Response t_o_k_e_n Step 2 is ${JSON.stringify(responseRaw.data)}`);
await this.manageNewToken(responseRaw.data);
msg.callback && this.sendTo(msg.from, msg.command, { error: `Done! Adapter starts now...` }, msg.callback);
this.log.info(`Token process done! Adapter starts now...`);
this.debugLog('t_o_k_e_n Step 2 done');
await this.connect();
})
.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 '${this.uri4token}' in your browser and follow described steps on webpage`,
);
return;
}
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) {
try {
this.debugLog(`${id} changed`);
const [temperature, mode, power, remainingTimeInSeconds, nextTimeBlockStart, boostMode] = await Promise.all([
this.getStateAsync(`${homeId}.Rooms.${roomId}.setting.temperature.value`),
this.getStateAsync(`${homeId}.Rooms.${roomId}.manualControlTermination.controlType`),
this.getStateAsync(`${homeId}.Rooms.${roomId}.setting.power`),
this.getStateAsync(`${homeId}.Rooms.${roomId}.manualControlTermination.remainingTimeInSeconds`),
this.getStateAsync(`${homeId}.Rooms.${roomId}.nextTimeBlock.start`),
this.getStateAsync(`${homeId}.Rooms.${roomId}.boostMode`),
]);
const set_boostMode = getStateValue(boostMode, false, toBoolean);
const set_remainingTimeInSeconds = getStateValue(remainingTimeInSeconds, 1800, val => parseInt(val.toString()));
const set_temp = getStateValue(temperature, 20, val => parseFloat(val.toString()));
const set_NextTimeBlockStartExists = getStateValue(nextTimeBlockStart, false, () => true); //return always true if exists
let set_power = getStateValue(power, 'OFF', val => val.toString().toUpperCase());
let set_terminationMode = getStateValue(mode, 'NO_OVERLAY', val => val.toString().toUpperCase());
let offSet, deviceID;
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;
case 'temperatureOffset':
if (state.val == null) {
this.log.warn('No valid value for offset found, ignored!');
break;
}
offSet = parseFloat(String(state.val));
deviceID = beforeStatename;
this.debugLog(`Offset new is ${offSet}`);
this.debugLog(`DeviceId is ${deviceID}`);
this.setOffSetTadoX(homeId, deviceID, offSet);
break;
}
} catch (error) {
this.log.error(`Issue at state change (TadoX): ${error}`);
console.error(`Issue at state change (TadoX): ${error}`);
if (error instanceof Error) {
this.errorHandling(error);
}
}
}
/**
* 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') {
let set_offset = getStateValue(state, 0, parseFloat);
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') {
let set_childLockEnabled = getStateValue(state, false, toBoolean);
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') {
let set_tt_id = getStateValue(state, 0, parseInt);
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') {
let set_presence = getStateValue(state, 'HOME', val => val.toString().toUpperCase());
this.debugLog(`Presence changed in home '${homeId}' to value '${set_presence}'`);
this.setPresenceLock(homeId, set_presence);
} else if (statename == 'masterswitch') {
let set_masterswitch = getStateValue(state, 'unknown', val => 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 = getStateValue(openWindowDetectionEnabled, false, toBoolean);
let set_openWindowDetectionTimeoutInSeconds = getStateValue(openWindowDetectionTimeoutInSeconds, 900, Number);
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, temperature, mode, power, durationInSeconds, nextTimeBlockStart] = await Promise.all([
this.getStateAsync(`${homeId}.Rooms.${zoneId}.setting.type`),
this.getStateAsync(`${homeId}.Rooms.${zoneId}.setting.temperature.celsius`),
this.getStateAsync(`${homeId}.Rooms.${zoneId}.overlay.termination.typeSkillBasedApp`),
this.getStateAsync(`${homeId}.Rooms.${zoneId}.setting.power`),
this.getStateAsync(`${homeId}.Rooms.${zoneId}.overlay.termination.durationInSeconds`),
this.getStateAsync(`${homeId}.Rooms.${zoneId}.nextTimeBlock.start`),
]);
let acMode, fanLevel, horizontalSwing, verticalSwing, fanSpeed, swing, light;
const set_type = getStateValue(type, 'HEATING', val => val.toString().toUpperCase());
const set_durationInSeconds = getStateValue(durationInSeconds, 1800, parseInt);
let set_temp = getStateValue(temperature, 20, val => parseFloat(val.toString()));
let set_power = getStateValue(power, 'OFF', val => val.toString().toUpperCase());
let set_mode = getStateValue(mode, 'NO_OVERLAY', val => val.toString().toUpperCase());
const set_NextTimeBlockStartExists = getStateValue(nextTimeBlockStart, false, () => true); //return always true if exists
if (set_type == 'AIR_CONDITIONING') {
[acMode, fanSpeed, fanLevel, horizontalSwing, verticalSwing, swing, light] = await Promise.all([
this.getStateAsync(`${homeId}.Rooms.${zoneId}.setting.mode`),
this.getStateAsync(`${homeId}.Rooms.${zoneId}.setting.fanSpeed`),
this.getStateAsync(`${homeId}.Rooms.${zoneId}.setting.fanLevel`),
this.getStateAsync(`${homeId}.Rooms.${zoneId}.setting.horizontalSwing`),
this.getStateAsync(`${homeId}.Rooms.${zoneId}.setting.verticalSwing`),
this.getStateAsync(`${homeId}.Rooms.${zoneId}.setting.swing`),
this.getStateAsync(`${homeId}.Rooms.${zoneId}.setting.light`),
]);
}
const set_acMode =
acMode === undefined ? 'NOT_AVAILABLE' : getStateValue(acMode, 'COOL', val => val.toString().toUpperCase());
const set_fanSpeed =
fanSpeed === undefined ? 'NOT_AVAILABLE' : getStateValue(fanSpeed, 'AUTO', val => val.toString().toUpperCase());
const set_fanLevel =
fanLevel === undefined ? 'NOT_AVAILABLE' : getStateValue(fanLevel, 'AUTO', val => val.toString().toUpperCase());
const set_horizontalSwing =
horizontalSwing === undefined
? 'NOT_AVAILABLE'
: getStateValue(horizontalSwing, 'OFF', val => val.toString().toUpperCase());
const set_verticalSwing =
verticalSwing === undefined ? 'NOT_AVAILABLE' : getStateValue(verticalSwing, 'OFF', val => val.toString().toUpperCase());
const set_swing = swing === undefined ? 'NOT_AVAILABLE' : getStateValue(swing, 'OFF', val => val.toString().toUpperCase());
const set_light = light === undefined ? 'NOT_AVAILABLE' : getStateValue(light, 'OFF', val => 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}`);
let shouldSetOverlay = false;
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}'`);
shouldSetOverlay = true;
break;
case 'durationInSeconds':
set_mode = 'TIMER';
await this.setState(`${homeId}.Rooms.${zoneId}.overlay.termination.typeSkillBasedApp`, set_mode, true);
// fallthrough
case 'fanSpeed':
case 'mode':
case 'fanLevel':
case 'swing':
case 'light':
case 'horizontalSwing':
case 'verticalSwing':
shouldSetOverlay = true;
break;
case 'typeSkillBasedApp':
if (set_mode == 'NO_OVERLAY') {
break;
}
this.debugLog(`TypeSkillBasedApp changed for room '${zoneId}' in home '${homeId}' to '${set_mode}'`);
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);
}
shouldSetOverlay = 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}'`,
);
shouldSetOverlay = true;
}
} else {
this.debugLog(
`Power changed for room '${zoneId}' in home '${homeId}' to '${state.val}' and temperature '${set_temp}' and mode '${set_mode}'`,
);
shouldSetOverlay = true;
}
break;
default:
}
if (shouldSetOverlay) {
this.debugLog(`Calling setZoneOverlay for room '${zoneId}' in home '${homeId}' due to change in '${statename}'`);
await this.setZoneOverlay(homeId, zoneId, {
power: set_power,
temperature: set_temp,
typeSkillBasedApp: set_mode,
durationInSeconds: set_durationInSeconds,
type: set_type,
acMode: set_acMode,
fanLevel: set_fanLevel,
horizontalSwing: set_horizontalSwing,
verticalSwing: set_verticalSwing,
fanSpeed: set_fanSpeed,
swing: set_swing,
light: set_light,
});
}
}
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;
}
} else {
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) {
return new Promise((resolve, reject) => {
this.debouncedSetManualControlTadoX(homeId, roomId, power, temperature, terminationMode, boostMode, durationInSeconds, resolve, reject);
});
}
/**
* @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) {
try {
//{`"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.api.apiCall(`${TADO_X_URL}/homes/${homeId}/rooms/${roomId}/manualControl`, 'post', payload);
this.debugLog(`setManualControlTadoX() response is ${JSON.stringify(apiResponse)}`);
await this.manageRoomStatesTadoX(homeId, roomId);
} catch (error) {
this.log.error(`Issue at setManualControlTadoX(): '${error}'`);
console.error(`Issue at setManualControlTadoX(): '${error}'`);
if (error instanceof Error) {
this.errorHandling(error);
}
}
}
/**
* @param {string} homeId
* @param {string} deviceID
* @param {number} offSet offset of the device
*/
async setOffSetTadoX(homeId, deviceID, offSet) {
try {
offSet = Math.round(offSet * 10) / 10;
if (offSet < -10 || offSet > 10) {
this.log.warn('Temperature offset should be between -10.0 and 10.0');
await this.sleep(3000);
await this.manageRoomsTadoX(homeId);
return;
}
const payload = { temperatureOffset: offSet };
let apiResponse = await this.api.apiCall(`${TADO_X_URL}/homes/${homeId}/roomsAndDevices/devices/${deviceID}`, 'patch', payload);
this.debugLog(`setOffSetTadoX() response is ${JSON.stringify(apiResponse)}`);
await this.sleep(5000);
await this.manageRoomsTadoX(homeId);
} catch (error) {
this.log.error(`Issue at setOffSetTadoX(): '${error}'`);
console.error(`Issue at setOffSetTadoX(): '${error}'`);
if (error instanceof Error) {
this.errorHandling(error);
}
}
}
/**
* @param {string} homeId
* @param {string} roomId
*/
async setResumeRoomScheduleTadoX(homeId, roomId) {
try {
let apiResponse = await this.api.apiCall(`${TADO_X_URL}/homes/${homeId}/rooms/${roomId}/resumeSchedule`, 'post');
this.debugLog(`setResumeRoomScheduleTadoX() response is ${JSON.stringify(apiResponse)}`);
await this.manageRoomStatesTadoX(homeId, roomId);
} catch (error) {
this.log.error(`Issue at setResumeRoomScheduleTadoX(): '${error}'`);
console.error(`Issue at setResumeRoomScheduleTadoX(): '${error}'`);
if (error instanceof Error) {
this.errorHandling(error);
}
}
}
/**
* @param {string} homeId
*/
async setResumeHomeScheduleTadoX(homeId) {
try {
let apiResponse = await this.api.apiCall(`${TADO_X_URL}/homes/${homeId}/quickActions/resumeSchedule`, 'post');
this.debugLog(`setResumeHomeScheduleTadoX() response is ${JSON.stringify(apiResponse)}`);
await this.manageRoomsTadoX(homeId);
} catch (error) {
this.log.error(`Issue at setResumeHomeScheduleTadoX(): '${error}'`);
console.error(`Issue at setResumeHomeScheduleTadoX(): '${error}'`);
if (error instanceof Error) {
this.errorHandling(error);
}
}
}
/**
* @param {string} homeId
*/
async setBoostTadoX(homeId) {
try {
let apiResponse = await this.api.apiCall(`${TADO_X_URL}/homes/${homeId}/quickActions/boost`, 'post');
this.debugLog(`setBoostTadoX() response is ${JSON.stringify(apiResponse)}`);
await this.manageRoomsTadoX(homeId);
} catch (error) {
this.log.error(`Issue at setBoostTadoX(): '${error}'`);
console.error(`Issue at setBoostTadoX(): '${error}'`);
if (error instanceof Error) {
this.errorHandling(error);
}
}
}
/**
* @param {string} homeId
*/
async setAllOffTadoX(homeId) {
try {
let apiResponse = await this.api.apiCall(`${TADO_X_URL}/homes/${homeId}/quickActions/allOff`, 'post');
this.debugLog(`setAllOffTadoX() response is ${JSON.stringify(apiResponse)}`);
await this.manageRoomsTadoX(homeId);
} catch (error) {
this.log.error(`Issue at setAllOffTadoX(): '${error}'`);
console.error(`Issue at setAllOffTadoX(): '${error}'`);
if (error instanceof Error) {
this.errorHandling(error);
}
}
}
/**
* @param {string} homeId
* @param {string} zoneId
*/
async setClearZoneOverlay(homeId, zoneId) {
try {
const url = `/api/v2/homes/${homeId}/zones/${zoneId}/overlay`;
if ((await isOnline()) == false) {
throw new Error('No internet connection detected!');
}
await this.api.apiCall(url, 'delete');
this.debugLog(`Called 'DELETE ${url}'`);
await jsonExplorer.setLastStartTime();
await this.manageZoneStates(homeId, zoneId);
this.debugLog('CheckExpire() at clearZoneOverlay() started');
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.api.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.manageTemperatureOffset(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.api.apiCall(`/api/v2/homes/${homeId}/zones/${zoneId}/schedule/activeTimetable`, 'put', timeTable);
if (apiResponse) {
await this.manageTimeTables(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.api.apiCall(`/api/v2/homes/${homeId}/presenceLock`, 'delete');
} else {
apiResponse = await this.api.apiCall(`/api/v2/homes/${homeId}/presenceLock`, 'put', homeState);
}
await this.manageHomeState(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);
}
}
}
/**
* Sets the zone overlay with the given options.
*
* @param {string} homeId
*