UNPKG

iobroker.tado

Version:

Tado cloud connector to control Tado devices

1,025 lines (964 loc) 109 kB
'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 *