UNPKG

homebridge-broadlink-rm-pro

Version:

Broadlink RM plugin (including the mini and pro) for homebridge with AC Pro and TV features

896 lines (718 loc) 37.4 kB
const { assert } = require('chai'); const uuid = require('uuid'); const fs = require('fs'); const findKey = require('find-key'); const delayForDuration = require('../helpers/delayForDuration'); const ServiceManagerTypes = require('../helpers/serviceManagerTypes'); const catchDelayCancelError = require('../helpers/catchDelayCancelError'); const { getDevice } = require('../helpers/getDevice'); const BroadlinkRMAccessory = require('./accessory'); class AirConAccessory extends BroadlinkRMAccessory { constructor (log, config = {}, serviceManagerType) { super(log, config, serviceManagerType); // Characteristic isn't defined until runtime so we set these the instance scope const HeatingCoolingStates = { off: Characteristic.TargetHeatingCoolingState.OFF, cool: Characteristic.TargetHeatingCoolingState.COOL, heat: Characteristic.TargetHeatingCoolingState.HEAT, auto: Characteristic.TargetHeatingCoolingState.AUTO }; this.HeatingCoolingStates = HeatingCoolingStates; config.heatOnly = config.heatOnly || false; config.coolOnly = config.coolOnly || false; const HeatingCoolingConfigKeys = {}; HeatingCoolingConfigKeys[Characteristic.TargetHeatingCoolingState.OFF] = 'off'; HeatingCoolingConfigKeys[Characteristic.TargetHeatingCoolingState.COOL] = 'cool'; HeatingCoolingConfigKeys[Characteristic.TargetHeatingCoolingState.HEAT] = 'heat'; HeatingCoolingConfigKeys[Characteristic.TargetHeatingCoolingState.AUTO] = 'auto'; this.HeatingCoolingConfigKeys = HeatingCoolingConfigKeys; // Fakegato setup if(config.noHistory !== true) { this.displayName = config.name; this.lastUpdatedAt = undefined; this.historyService = new HistoryService("room", this, { storage: 'fs', filename: 'RMPro_' + config.name.replace(' ','-') + '_persist.json'}); this.historyService.log = this.log; } this.temperatureCallbackQueue = {}; this.monitorTemperature(); } correctReloadedState (state) { if (state.currentHeatingCoolingState === Characteristic.CurrentHeatingCoolingState.OFF) { state.targetTemperature = state.currentTemperature; } state.targetHeatingCoolingState = state.currentHeatingCoolingState; if (state.userSpecifiedTargetTemperature) {state.targetTemperature = state.userSpecifiedTargetTemperature} } setDefaults () { const { config, state } = this; // Set config default values if (config.turnOnWhenOff === undefined) {config.turnOnWhenOff = config.sendOnWhenOff || false;} // Backwards compatible with `sendOnWhenOff` if (config.minimumAutoOnOffDuration === undefined) {config.minimumAutoOnOffDuration = config.autoMinimumDuration || 120;} // Backwards compatible with `autoMinimumDuration` config.minTemperature = config.minTemperature || -15; config.maxTemperature = config.maxTemperature || 50; config.tempStepSize = config.tempStepSize || 1; if(config.mqttURL) { //MQTT updates when published so frequent refreshes aren't required ( 10 minute default as a fallback ) config.temperatureUpdateFrequency = config.temperatureUpdateFrequency || 600; } else { config.temperatureUpdateFrequency = config.temperatureUpdateFrequency || 10; } config.units = config.units ? config.units.toLowerCase() : 'c'; config.temperatureAdjustment = config.temperatureAdjustment || 0; config.humidityAdjustment = config.humidityAdjustment || 0; config.autoSwitchName = config.autoSwitch || config.autoSwitchName; if (config.preventResendHex === undefined && config.allowResend === undefined) { config.preventResendHex = false; } else if (config.allowResend !== undefined) { config.preventResendHex = !config.allowResend; } // When a temperature hex doesn't exist we try to use the hex set for these // default temperatures config.defaultCoolTemperature = config.defaultCoolTemperature || 16; config.defaultHeatTemperature = config.defaultHeatTemperature || 30; // ignore Humidity if set to not use it, or using Temperature source that doesn't support it if(config.noHumidity || config.w1Device || config.pseudoDeviceTemperature){ state.currentHumidity = null; config.noHumidity = true; } else { config.noHumidity = false; } // Used to determine when we should use the defaultHeatTemperature or the // defaultHeatTemperature config.heatTemperature = config.heatTemperature || 22; // Set state default values // state.targetTemperature = state.targetTemperature || config.minTemperature; state.currentHeatingCoolingState = state.currentHeatingCoolingState || Characteristic.CurrentHeatingCoolingState.OFF; state.targetHeatingCoolingState = state.targetHeatingCoolingState || Characteristic.TargetHeatingCoolingState.OFF; state.firstTemperatureUpdate = true; // Check required properties if (config.pseudoDeviceTemperature) { assert.isBelow(config.pseudoDeviceTemperature, config.maxTemperature + 1, `\x1b[31m[CONFIG ERROR] \x1b[33mpseudoDeviceTemperature\x1b[0m (${config.pseudoDeviceTemperature}) must be less than the maxTemperature (${config.maxTemperature})`) assert.isAbove(config.pseudoDeviceTemperature, config.minTemperature - 1, `\x1b[31m[CONFIG ERROR] \x1b[33mpseudoDeviceTemperature\x1b[0m (${config.pseudoDeviceTemperature}) must be more than the minTemperature (${config.minTemperature})`) } // minTemperature can't be more than 10 or HomeKit throws a fit - This limitation has been removed //assert.isBelow(config.minTemperature, 11, `\x1b[31m[CONFIG ERROR] \x1b[33mminTemperature\x1b[0m (${config.minTemperature}) must be <= 10`) // maxTemperature > minTemperature assert.isBelow(config.minTemperature, config.maxTemperature, `\x1b[31m[CONFIG ERROR] \x1b[33mmaxTemperature\x1b[0m (${config.minTemperature}) must be more than minTemperature (${config.minTemperature})`) } reset () { super.reset(); this.state.isRunningAutomatically = false; if (this.shouldIgnoreAutoOnOffPromise) { this.shouldIgnoreAutoOnOffPromise.cancel(); this.shouldIgnoreAutoOnOffPromise = undefined; this.shouldIgnoreAutoOnOff = false; } if (this.turnOnWhenOffDelayPromise) { this.turnOnWhenOffDelayPromise.cancel(); this.turnOnWhenOffDelayPromise = undefined; } if (this.autoOffTimeoutPromise) { this.autoOffTimeoutPromise.cancel(); this.autoOffTimeoutPromise = null; } } updateServiceTargetHeatingCoolingState (value) { const { serviceManager, state } = this; delayForDuration(0.2).then(() => { serviceManager.setCharacteristic(Characteristic.TargetHeatingCoolingState, value); }); } updateServiceCurrentHeatingCoolingState (value) { const { serviceManager, state } = this; delayForDuration(0.25).then(() => { serviceManager.setCharacteristic(Characteristic.CurrentHeatingCoolingState, value); }); } // Allows this accessory to know about switch accessories that can determine whether // auto-on/off should be permitted. updateAccessories (accessories) { const { config, name, log, logLevel } = this; const { autoSwitchName } = config; if (!autoSwitchName) {return;} if (logLevel <=2) {log(`${name} Linking autoSwitch "${autoSwitchName}"`)} const autoSwitchAccessories = accessories.filter(accessory => accessory.name === autoSwitchName); if (autoSwitchAccessories.length === 0) {return log(`${name} No accessory could be found with the name "${autoSwitchName}". Please update the "autoSwitchName" value or add a matching switch accessory.`);} this.autoSwitchAccessory = autoSwitchAccessories[0]; } isAutoSwitchOn () { return (!this.autoSwitchAccessory || (this.autoSwitchAccessory && this.autoSwitchAccessory.state && this.autoSwitchAccessory.state.switchState)); } setTargetTemperature (previousValue) { const { config, log, logLevel, name, serviceManager, state } = this; const { preventResendHex, minTemperature, maxTemperature } = config; if (state.targetTemperature === previousValue && preventResendHex && !this.previouslyOff) {return;} this.previouslyOff = false; if (state.targetTemperature < minTemperature) {return log(`The target temperature (${this.targetTemperature}) must be more than the minTemperature (${minTemperature})`);} if (state.targetTemperature > maxTemperature) {return log(`The target temperature (${this.targetTemperature}) must be less than the maxTemperature (${maxTemperature})`);} // Used within correctReloadedState() so that when re-launching the accessory it uses // this temperature rather than one automatically set. state.userSpecifiedTargetTemperature = state.targetTemperature; // Do the actual sending of the temperature this.sendTemperature(state.targetTemperature, previousValue); serviceManager.updateCharacteristic(Characteristic.TargetTemperature, state.targetTemperature); } async setTargetHeatingCoolingState (hexData, previousValue) { const { HeatingCoolingConfigKeys, HeatingCoolingStates, config, data, host, log, logLevel, name, serviceManager, state, debug } = this; const { preventResendHex, defaultCoolTemperature, defaultHeatTemperature, replaceAutoMode } = config; const targetHeatingCoolingState = HeatingCoolingConfigKeys[state.targetHeatingCoolingState]; const lastUsedHeatingCoolingState = HeatingCoolingConfigKeys[state.lastUsedHeatingCoolingState]; const currentHeatingCoolingState = HeatingCoolingConfigKeys[state.currentHeatingCoolingState]; // Some calls are made to this without a value for some unknown reason if (state.targetHeatingCoolingState === undefined) {return;} // Check to see if it's changed if (state.targetHeatingCoolingState === state.currentHeatingCoolingState && preventResendHex) {return;} if (targetHeatingCoolingState === 'off') { this.updateServiceCurrentHeatingCoolingState(HeatingCoolingStates.off); if (currentHeatingCoolingState === 'cool' && data.offDryMode !== undefined) { // Dry off mode when previously cooling if (logLevel <=2) {log(`${name} Previous state ${currentHeatingCoolingState}, setting off with dry mode`);} await this.performSend(data.offDryMode); } else { await this.performSend(data.off); } this.reset(); return; } if (previousValue === Characteristic.TargetHeatingCoolingState.OFF) {this.previouslyOff = true;} // If the air-conditioner is turned off then turn it on first and try this again if (this.checkTurnOnWhenOff()) { this.turnOnWhenOffDelayPromise = delayForDuration(.3); await this.turnOnWhenOffDelayPromise } // Perform the auto -> cool/heat conversion if `replaceAutoMode` is specified if (replaceAutoMode && targetHeatingCoolingState === 'auto') { if (logLevel <=2) {log(`${name} setTargetHeatingCoolingState (converting from auto to ${replaceAutoMode})`);} this.updateServiceTargetHeatingCoolingState(HeatingCoolingStates[replaceAutoMode]); return; } let temperature = state.targetTemperature; let mode = HeatingCoolingConfigKeys[state.targetHeatingCoolingState]; if (state.currentHeatingCoolingState !== state.targetHeatingCoolingState){ // Selecting a heating/cooling state allows a default temperature to be used for the given state. if (state.targetHeatingCoolingState === Characteristic.TargetHeatingCoolingState.HEAT) { temperature = defaultHeatTemperature; } else if (state.targetHeatingCoolingState === Characteristic.TargetHeatingCoolingState.COOL) { temperature = defaultCoolTemperature; } //Set the mode, and send the mode hex this.updateServiceCurrentHeatingCoolingState(state.targetHeatingCoolingState); if (data.heat && mode === 'heat'){ await this.performSend(data.heat); } else if (data.cool && mode === 'cool'){ await this.performSend(data.cool); } else if (data.auto && mode === 'auto'){ await this.performSend(data.auto); } else if (hexData) { //Just send the provided temperature hex if no mode codes are set await this.performSend(hexData); } if (logLevel <=1) {this.log(`${name} sentMode (${mode})`);} //Force Temperature send delayForDuration(0.25).then(() => { this.sendTemperature(temperature, state.currentTemperature); serviceManager.updateCharacteristic(Characteristic.TargetTemperature,state.targetTemperature); }); } serviceManager.updateCharacteristic(Characteristic.CurrentHeatingCoolingState,state.currentHeatingCoolingState); serviceManager.updateCharacteristic(Characteristic.TargetHeatingCoolingState,state.targetHeatingCoolingState); this.checkAutoOff(); } async checkAutoOff () { await catchDelayCancelError(async () => { const {config, name, data, log} = this; let {enableAutoOff, onDuration, enableAutoOn, offDuration} = config; onDuration = onDuration|| 60; offDuration = offDuration|| 60; if (enableAutoOn) { log(`${name} enableAutoOn is not supported.`); } if (enableAutoOff && parseInt(onDuration) > 0) { log(`${name} setTargetHeatingCoolingState: (automatically turn off in ${onDuration} seconds)`); if (this.autoOffTimeoutPromise) { this.autoOffTimeoutPromise.cancel(); this.autoOffTimeoutPromise = null; } this.autoOffTimeoutPromise = delayForDuration(onDuration); await this.autoOffTimeoutPromise; await this.performSend(data.off); this.updateServiceTargetHeatingCoolingState(this.HeatingCoolingStates.off); this.updateServiceCurrentHeatingCoolingState(this.HeatingCoolingStates.off); } }); } // Thermostat async sendTemperature (temperature, previousTemperature) { const { HeatingCoolingConfigKeys, HeatingCoolingStates, config, data, host, log, name, state, logLevel } = this; const { preventResendHex, defaultCoolTemperature, heatTemperature, ignoreTemperatureWhenOff, sendTemperatureOnlyWhenOff } = config; if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} Potential sendTemperature (${temperature})`);} // Ignore Temperature if off, staying off - and set to ignore. OR temperature not provided if ((!state.targetHeatingCoolingState && ignoreTemperatureWhenOff) || !temperature) { if (logLevel <=2) {log(`${name} Ignoring sendTemperature due to "ignoreTemperatureWhenOff": true or no temperature set.`);} return; } let mode = HeatingCoolingConfigKeys[state.targetHeatingCoolingState]; const { hexData, finalTemperature } = this.getTemperatureHexData(mode, temperature); state.targetTemperature = finalTemperature; // Update the heating/cooling mode based on the pseudo-mode - if pressent. if (hexData['pseudo-mode']){ mode = hexData['pseudo-mode']; if (mode) {assert.oneOf(mode, [ 'heat', 'cool', 'auto' ], `\x1b[31m[CONFIG ERROR] \x1b[33mpseudo-mode\x1b[0m should be one of "heat", "cool" or "auto"`)} this.updateServiceCurrentHeatingCoolingState(HeatingCoolingStates[mode]); } if((previousTemperature !== finalTemperature) || (state.firstTemperatureUpdate && !preventResendHex)){ //Set the temperature await this.performSend(hexData.data); if (logLevel <=2) {this.log(`${name} sentTemperature (${state.targetTemperature})`);} state.firstTemperatureUpdate = false; } } getTemperatureHexData (mode, temperature) { const { config, data, name, state, logLevel } = this; const { defaultHeatTemperature, defaultCoolTemperature, heatTemperature } = config; let finalTemperature = temperature; if (mode === 'off') { let hexData = data.off; return { finalTemperature, hexData }; } let hexData = data[`${mode}${temperature}`]; if (!hexData) { // Mode based code not found, try mode-less if (logLevel <=3) {this.log(`${name} No ${mode} HEX code found for ${temperature}`);} hexData = data[`temperature${temperature}`]; } else { if (hexData['pseudo-mode']) { if (logLevel <=2) {this.log(`\x1b[36m[INFO] \x1b[0m${name} Configuration found for ${mode}${temperature} with pseudo-mode. Pseudo-mode will replace the configured mode.`);} } } // You may not want to set the hex data for every single mode... if (!hexData) { const defaultTemperature = (temperature >= heatTemperature) ? defaultHeatTemperature : defaultCoolTemperature; hexData = data[`temperature${defaultTemperature}`]; assert(hexData, `\x1b[31m[CONFIG ERROR] \x1b[0m You need to provide a hex code for the following temperature: \x1b[33m{ "temperature${temperature}": { "data": "HEXCODE", "pseudo-mode" : "heat/cool" } }\x1b[0m or provide the default temperature: \x1b[33m { "temperature${defaultTemperature}": { "data": "HEXCODE", "pseudo-mode" : "heat/cool" } }\x1b[0m`); if (logLevel <=2) {this.log(`${name} Update to default temperature (${defaultTemperature})`);} finalTemperature = defaultTemperature; } return { finalTemperature, hexData } } async checkTurnOnWhenOff () { const { config, data, host, log, logLevel, name, state } = this; const { on } = data; if (state.currentHeatingCoolingState === Characteristic.TargetHeatingCoolingState.OFF && config.turnOnWhenOff) { if (logLevel <=2) {log(`${name} sending "on" hex before sending temperature`);} if (on) { await this.performSend(on); } else { if (logLevel <=4) {log(`\x1b[31m[CONFIG ERROR] \x1b[0m ${name} No On Hex configured, but turnOnWhenOff enabled`);} } return true; } return false; } // Device Temperature Methods async monitorTemperature () { const { config, host, log, logLevel, name, state } = this; const { temperatureFilePath, pseudoDeviceTemperature, w1DeviceID } = config; if (pseudoDeviceTemperature !== undefined) {return;} //Force w1 and file devices to a minimum 1 minute refresh if (w1DeviceID || temperatureFilePath) {config.temperatureUpdateFrequency = Math.max(config.temperatureUpdateFrequency,60);} const device = getDevice({ host, log }); // Try again in a second if we don't have a device yet if (!device) { await delayForDuration(1); this.monitorTemperature(); return; } if (logLevel <=1) {log(`${name} monitorTemperature`);} device.on('temperature', this.onTemperature.bind(this)); device.checkTemperature(); this.updateTemperatureUI(); if (!config.isUnitTest) {setInterval(()=>{this.getCurrentTemperature(this.updateTemperatureUI.bind(this))}, config.temperatureUpdateFrequency * 1000)} } onTemperature (temperature,humidity) { const { config, host, logLevel, log, name, state } = this; const { minTemperature, maxTemperature, temperatureAdjustment, humidityAdjustment, noHumidity, tempSourceUnits } = config; // onTemperature is getting called twice. No known cause currently. // This helps prevent the same temperature from being processed twice if (Object.keys(this.temperatureCallbackQueue).length === 0) {return;} temperature += temperatureAdjustment; if (tempSourceUnits == 'F') {temperature = (temperature - 32) * 5/9;} state.currentTemperature = temperature; if(logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} onTemperature (${temperature})`);} if(humidity) { if(noHumidity){ state.currentHumidity = null; }else{ humidity += humidityAdjustment; state.currentHumidity = humidity; if(logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} onHumidity (` + humidity + `)`);} } } //Process Fakegato history //Ignore readings of exactly zero - the default no value value. if(config.noHistory !== true && this.state.currentTemperature != 0.00) { this.lastUpdatedAt = Date.now(); if(logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} Logging data to history: temp: ${this.state.currentTemperature}, humidity: ${this.state.currentHumidity}`);} if(noHumidity){ this.historyService.addEntry({ time: Math.round(new Date().valueOf() / 1000), temp: this.state.currentTemperature }); }else{ this.historyService.addEntry({ time: Math.round(new Date().valueOf() / 1000), temp: this.state.currentTemperature, humidity: this.state.currentHumidity }); } } this.processQueuedTemperatureCallbacks(temperature); } addTemperatureCallbackToQueue (callback) { const { config, host, logLevel, log, name, state } = this; const { mqttURL, temperatureFilePath, w1DeviceID, noHumidity } = config; // Clear the previous callback if (Object.keys(this.temperatureCallbackQueue).length > 1) { if (state.currentTemperature) { if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} addTemperatureCallbackToQueue (clearing previous callback, using existing temperature)`);} this.processQueuedTemperatureCallbacks(state.currentTemperature); } } // Add a new callback const callbackIdentifier = uuid.v4(); this.temperatureCallbackQueue[callbackIdentifier] = callback; // Read temperature from file if (temperatureFilePath) { this.updateTemperatureFromFile(); return; } // Read temperature from W1 Device if (w1DeviceID) { this.updateTemperatureFromW1(); return; } // Read temperature from mqtt if (mqttURL) { const temperature = this.mqttValueForIdentifier('temperature'); const humidity = noHumidity ? null : this.mqttValueForIdentifier('humidity'); this.onTemperature(temperature || 0,humidity); return; } // Read temperature from Broadlink RM device // If the device is no longer available, use previous tempeature const device = getDevice({ host, log }); if (!device || device.state === 'inactive') { if (device && device.state === 'inactive') { if (logLevel <=3) {log(`${name} addTemperatureCallbackToQueue (device no longer active, using existing temperature)`);} } this.processQueuedTemperatureCallbacks(state.currentTemperature || 0); return; } device.checkTemperature(); if (logLevel <1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} addTemperatureCallbackToQueue (requested temperature from device, waiting)`);} } updateTemperatureFromFile () { const { config, logLevel, host, log, name, state } = this; const { temperatureFilePath, noHumidity, batteryAlerts } = config; let humidity = null; let temperature = null; let battery = null; if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} updateTemperatureFromFile reading file: ${temperatureFilePath}`);} fs.readFile(temperatureFilePath, 'utf8', (err, data) => { if (err) { if (logLevel <=4) {log(`\x1b[31m[ERROR] \x1b[0m${name} updateTemperatureFromFile\n\n${err.message}`);} } if (data === undefined || data.trim().length === 0) { if (logLevel <=3) {log(`\x1b[33m[WARNING]\x1b[0m ${name} updateTemperatureFromFile error reading file: ${temperatureFilePath}, using previous Temperature`);} if (!noHumidity) {humidity = (state.currentHumidity || 0);} temperature = (state.currentTemperature || 0); } const lines = data.split(/\r?\n/); if (/^[0-9]+\.*[0-9]*$/.test(lines[0])){ temperature = parseFloat(data); } else { lines.forEach((line) => { if(-1 < line.indexOf(':')){ let value = line.split(':'); if(value[0] == 'temperature') {temperature = parseFloat(value[1]);} if(value[0] == 'humidity' && !noHumidity) {humidity = parseFloat(value[1]);} if(value[0] == 'battery' && batteryAlerts) {battery = parseFloat(value[1]);} } }); } //Default battery level if none returned if (battery) { state.batteryLevel = battery; }else{ state.batteryLevel = 100; } if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} updateTemperatureFromFile (parsed temperature: ${temperature} humidity: ${humidity})`);} this.onTemperature(temperature, humidity); }); } updateTemperatureFromW1 () { const { config, logLevel, host, log, name, state } = this; const { w1DeviceID } = config; var W1PATH = "/sys/bus/w1/devices"; var fName = W1PATH + "/" + w1DeviceID + "/w1_slave"; var temperature; if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} updateTemperatureFromW1 reading file: ${fName}`);} fs.readFile(fName, 'utf8', (err, data) => { if (err) { log(`\x1b[31m[ERROR] \x1b[0m${name} updateTemperatureFromW1\n\n${err.message}`); } if(data.includes("t=")){ var matches = data.match(/t=([0-9]+)/); temperature = parseInt(matches[1]) / 1000; }else{ if (logLevel <=3) {log(`\x1b[33m[WARNING]\x1b[0m ${name} updateTemperatureFromW1 error reading file: ${fName}, using previous Temperature`);} temperature = (state.currentTemperature || 0); } //Default battery level state.batteryLevel = 100; if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} updateTemperatureFromW1 (parsed temperature: ${temperature})`);} this.onTemperature(temperature); }); } processQueuedTemperatureCallbacks (temperature) { if (Object.keys(this.temperatureCallbackQueue).length === 0) {return;} Object.keys(this.temperatureCallbackQueue).forEach((callbackIdentifier) => { const callback = this.temperatureCallbackQueue[callbackIdentifier]; callback(null, temperature); delete this.temperatureCallbackQueue[callbackIdentifier]; }) this.temperatureCallbackQueue = {}; this.checkTemperatureForAutoOnOff(temperature); } updateTemperatureUI () { const { config, serviceManager } = this; const { noHumidity } = config; serviceManager.updateCharacteristic(Characteristic.CurrentTemperature, this.state.currentTemperature); if(!noHumidity){serviceManager.updateCharacteristic(Characteristic.CurrentRelativeHumidity, this.state.currentHumidity);} } getCurrentTemperature (callback) { const { config, host, logLevel, log, name, state } = this; const { pseudoDeviceTemperature } = config; // Some devices don't include a thermometer and so we can use `pseudoDeviceTemperature` instead if (pseudoDeviceTemperature !== undefined) { if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} getCurrentTemperature (using pseudoDeviceTemperature ${pseudoDeviceTemperature} from config)`);} return callback(null, pseudoDeviceTemperature); } this.addTemperatureCallbackToQueue(callback); } getCurrentHumidity (callback) { const { config, host, logLevel, log, name, state } = this; const { pseudoDeviceTemperature } = config; return callback(null, state.currentHumidity); } async checkTemperatureForAutoOnOff (temperature) { const { config, host, log, logLevel, name, serviceManager, state } = this; let { autoHeatTemperature, autoCoolTemperature, minimumAutoOnOffDuration } = config; if (this.shouldIgnoreAutoOnOff) { if (logLevel <=2) {log(`${name} checkTemperatureForAutoOn (ignore within ${minimumAutoOnOffDuration}s of previous auto-on/off due to "minimumAutoOnOffDuration")`);} return; } if (!autoHeatTemperature && !autoCoolTemperature) {return;} if (!this.isAutoSwitchOn()) { if (logLevel <=2) {log(`${name} checkTemperatureForAutoOnOff (autoSwitch is off)`);} return; } if (logLevel <=1) {log(`${name} checkTemperatureForAutoOnOff`);} if (autoHeatTemperature && temperature < autoHeatTemperature) { this.state.isRunningAutomatically = true; if (logLevel <=2) {log(`${name} checkTemperatureForAutoOnOff (${temperature} < ${autoHeatTemperature}: auto heat)`);} serviceManager.setCharacteristic(Characteristic.TargetHeatingCoolingState, Characteristic.TargetHeatingCoolingState.HEAT); } else if (autoCoolTemperature && temperature > autoCoolTemperature) { this.state.isRunningAutomatically = true; if (logLevel <=2) {log(`${name} checkTemperatureForAutoOnOff (${temperature} > ${autoCoolTemperature}: auto cool)`);} serviceManager.setCharacteristic(Characteristic.TargetHeatingCoolingState, Characteristic.TargetHeatingCoolingState.COOL); } else { if (logLevel <=1) {log(`${name} checkTemperatureForAutoOnOff (temperature is ok)`);} if (this.state.isRunningAutomatically) { this.isAutomatedOff = true; if (logLevel <=2) {log(`${name} checkTemperatureForAutoOnOff (auto off)`);} serviceManager.setCharacteristic(Characteristic.TargetHeatingCoolingState, Characteristic.TargetHeatingCoolingState.OFF); } else { return; } } this.shouldIgnoreAutoOnOff = true; this.shouldIgnoreAutoOnOffPromise = delayForDuration(minimumAutoOnOffDuration); await this.shouldIgnoreAutoOnOffPromise; this.shouldIgnoreAutoOnOff = false; } getTemperatureDisplayUnits (callback) { const { config } = this; const temperatureDisplayUnits = (config.units.toLowerCase() === 'f') ? Characteristic.TemperatureDisplayUnits.FAHRENHEIT : Characteristic.TemperatureDisplayUnits.CELSIUS; callback(null, temperatureDisplayUnits); } // MQTT onMQTTMessage (identifier, message) { const { state, logLevel, log, name } = this; if (identifier !== 'unknown' && identifier !== 'temperature' && identifier !== 'humidity' && identifier !== 'battery' && identifier !== 'combined') { log(`\x1b[31m[ERROR] \x1b[0m${name} onMQTTMessage (mqtt message received with unexpected identifier: ${identifier}, ${message.toString()})`); return; } super.onMQTTMessage(identifier, message); let temperatureValue, humidityValue, batteryValue; let objectFound = false; let value = this.mqttValuesTemp[identifier]; if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} onMQTTMessage (raw value: ${value})`);} try { //Attempt to parse JSON - if result is JSON const temperatureJSON = JSON.parse(value); if (typeof temperatureJSON === 'object') { objectFound = true; let values = []; if (identifier !== 'temperature' && identifier !== 'battery'){ //Try to locate other Humidity fields if (values.length === 0) {values = findKey(temperatureJSON, 'Hum');} if (values.length === 0) {values = findKey(temperatureJSON, 'hum');} if (values.length === 0) {values = findKey(temperatureJSON, 'Humidity');} if (values.length === 0) {values = findKey(temperatureJSON, 'humidity');} if (values.length === 0) {values = findKey(temperatureJSON, 'RelativeHumidity');} if (values.length === 0) {values = findKey(temperatureJSON, 'relativehumidity');} if(values.length > 0) { humidityValue = values; values = []; } } if (identifier !== 'temperature' && identifier !== 'humidity'){ //Try to locate other Battery fields if (values.length === 0) {values = findKey(temperatureJSON, 'Batt');} if (values.length === 0) {values = findKey(temperatureJSON, 'batt');} if (values.length === 0) {values = findKey(temperatureJSON, 'Battery');} if (values.length === 0) {values = findKey(temperatureJSON, 'battery');} if(values.length > 0) { batteryValue = values; values = []; } } if(identifier !== 'battery' && identifier !== 'humidity'){ //Try to locate other Temperature fields if (values.length === 0) {values = findKey(temperatureJSON, 'temp');} if (values.length === 0) {values = findKey(temperatureJSON, 'Temp');} if (values.length === 0) {values = findKey(temperatureJSON, 'temperature');} if (values.length === 0) {values = findKey(temperatureJSON, 'Temperature');} if (values.length === 0) {values = findKey(temperatureJSON, 'local_temperature');} if(values.length > 0) { temperatureValue = values; } } if (values.length > 0) { value = values[0]; } else { value = undefined; } } } catch (err) {} //Result couldn't be parsed as JSON if(objectFound) { if(temperatureValue !== undefined && temperatureValue.length > 0) { this.mqttValues.temperature = parseFloat(temperatureValue[0]); } if(batteryValue !== undefined && batteryValue.length > 0) { state.batteryLevel = parseFloat(batteryValue[0]); this.mqttValues.battery = parseFloat(batteryValue[0]); } if(humidityValue !== undefined && humidityValue.length > 0) { this.mqttValues.humidity = parseFloat(humidityValue[0]); } }else{ if (value === undefined || (typeof value === 'string' && value.trim().length === 0)) { log(`\x1b[31m[ERROR] \x1b[0m${name} onMQTTMessage (mqtt value not found)`); return; } if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} onMQTTMessage (parsed value: ${value})`);} value = parseFloat(value); if (identifier == 'battery'){ state.batteryLevel = value; return; } this.mqttValues[identifier] = value; } this.onTemperature(this.mqttValues.temperature,this.mqttValues.humidity); } // Service Manager Setup setupServiceManager () { const { config, name, serviceManagerType } = this; this.serviceManager = new ServiceManagerTypes[serviceManagerType](name, Service.Thermostat, this.log); this.serviceManager.addToggleCharacteristic({ name: 'currentHeatingCoolingState', type: Characteristic.CurrentHeatingCoolingState, getMethod: this.getCharacteristicValue, setMethod: this.setCharacteristicValue, bind: this, props: { } }); this.serviceManager.addToggleCharacteristic({ name: 'targetTemperature', type: Characteristic.TargetTemperature, getMethod: this.getCharacteristicValue, setMethod: this.setCharacteristicValue, bind: this, props: { setValuePromise: this.setTargetTemperature.bind(this), ignorePreviousValue: true } }); this.serviceManager.addToggleCharacteristic({ name: 'targetHeatingCoolingState', type: Characteristic.TargetHeatingCoolingState, getMethod: this.getCharacteristicValue, setMethod: this.setCharacteristicValue, bind: this, props: { setValuePromise: this.setTargetHeatingCoolingState.bind(this), ignorePreviousValue: true } }); if (config.heatOnly) { this.serviceManager .getCharacteristic(Characteristic.TargetHeatingCoolingState) .setProps({ minValue: 0, maxValue: 1, validValues: [0,1] }); } if (config.coolOnly) { this.serviceManager .getCharacteristic(Characteristic.TargetHeatingCoolingState) .setProps({ minValue: 0, maxValue: 2, validValues: [0,2] }); } this.serviceManager.addGetCharacteristic({ name: 'currentTemperature', type: Characteristic.CurrentTemperature, method: this.getCurrentTemperature, bind: this }); if (!config.noHumidity){ this.serviceManager.addGetCharacteristic({ name: 'currentHumidity', type: Characteristic.CurrentRelativeHumidity, method: this.getCurrentHumidity, bind: this }) } this.serviceManager.addGetCharacteristic({ name: 'temperatureDisplayUnits', type: Characteristic.TemperatureDisplayUnits, method: this.getTemperatureDisplayUnits, bind: this }) this.serviceManager .getCharacteristic(Characteristic.TargetTemperature) .setProps({ minValue: config.minTemperature, maxValue: config.maxTemperature, minStep: config.tempStepSize }); this.serviceManager .getCharacteristic(Characteristic.CurrentTemperature) .setProps({ minStep: 0.1 }); } } module.exports = AirConAccessory