UNPKG

iobroker.tado

Version:

Tado cloud connector to control Tado devices

834 lines (767 loc) 102 kB
'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"