UNPKG

homebridge-broadlink-rm-pro

Version:

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

608 lines (501 loc) 23 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'); const FanAccessory = require('./fan'); class HumidifierDehumidifierAccessory extends FanAccessory { constructor (log, config = {}, serviceManagerType) { super(log, config, serviceManagerType); //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 = log; } this.humidityCallbackQueue = {}; this.monitorHumidity(); } setDefaults () { const { data, config, state } = this; super.setDefaults(); // Set config default values config.humidifierOnly = config.humidifierOnly || false; config.deHumidifierOnly = config.deHumidifierOnly || false; config.humidityUpdateFrequency = config.humidityUpdateFrequency || 10; config.humidityAdjustment = config.humidityAdjustment || 0; config.noHumidity = config.noHumidity || false; config.threshold = config.threshold || 5; data.fanOnly = data.fanOnly ? data.fanOnly : data.off; state.firstHumidityUpdate = true; } reset () { super.reset(); } async setSwitchState (hexData, previousValue){ this.previouslyOff = previousValue ? false : true; this.updateDeviceState (); super.setSwitchState (hexData, previousValue); super.checkAutoOnOff(); } async setCurrentState (hexData, previousValue) { const { data, config, log, logLevel, name, state, serviceManager } = this; if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} setCurrentState: requested update from ${previousValue} to ${state.currentState}`);} // Ignore if no change to the targetPosition if (state.currentState === previousValue || !state.switchState) {return;} switch(state.currentState){ case Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING: hexData = data.targetStateDehumidifier; break; case Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING: hexData = data.targetStateHumidifier; break; case Characteristic.CurrentHumidifierDehumidifierState.IDLE: hexData = data.fanOnly; break; } if (logLevel <=2) {log(`${name} setCurrentState: currently ${previousValue}, changing to ${state.currentState}`);} if(hexData) {await this.performSend(hexData);} serviceManager.updateCharacteristic(Characteristic.CurrentHumidifierDehumidifierState,state.currentState); this.previouslyOff = false; } async setHumidifierThreshold (hexData, previousValue) { const { config, name, log, state, logLevel } = this; if (state.HumidifierThreshold === previousValue && config.preventResendHex && !this.previouslyOff) {return;} const desiredState = this.getDesiredState (); const previousState = state.currentState; if (state.currentState === desiredState) {return;} if (logLevel <=2) {log(`${name} setHumidifierThreshold: currently ${previousValue} to ${state.DehumidifierThreshold}, changing to ${state.HumidifierThreshold} to ${state.DehumidifierThreshold}`);} state.currentState = desiredState; this.setCurrentState (hexData, previousState); } async setDehumidifierThreshold (hexData, previousValue) { const { config, name, log, state, logLevel } = this; if (state.DehumidifierThreshold === previousValue && config.preventResendHex && !this.previouslyOff) {return;} const desiredState = this.getDesiredState (); const previousState = state.currentState; if (state.currentState === desiredState) {return;} if (logLevel <=2) {log(`${name} setDeumidifierThreshold: currently ${state.HumidifierThreshold} to ${previousValue}, changing to ${state.HumidifierThreshold} to ${state.DehumidifierThreshold}`);} state.currentState = desiredState; this.setCurrentState (hexData, previousState); } getDesiredState () { const { config, log, name, state, serviceManager } = this; let desiredState = Characteristic.CurrentHumidifierDehumidifierState.IDLE; //Work out the ideal state if (state.targetState === Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER) { if ((state.currentHumidity > state.HumidifierThreshold) && (state.currentHumidity < state.DehumidifierThreshold)){ desiredState = Characteristic.CurrentHumidifierDehumidifierState.IDLE; } else if (state.currentHumidity < state.HumidifierThreshold) { desiredState = Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING; } else if (state.currentHumidity > state.DehumidifierThreshold) { desiredState = Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING; } } else if (state.targetState === Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER) { if(state.currentHumidity < state.HumidifierThreshold){ desiredState = Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING; } } else if (state.targetState === Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER) { if(state.currentHumidity > state.DehumidifierThreshold){ desiredState = Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING; } } if (config.humidifierOnly && desiredState === Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING) { desiredState = Characteristic.CurrentHumidifierDehumidifierState.IDLE; } if (config.deHumidifierOnly && desiredState === Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING) { desiredState = Characteristic.CurrentHumidifierDehumidifierState.IDLE; } return desiredState; } async updateDeviceState () { const { serviceManager, logLevel, config, name, log, state } = this; //Do nothing if turned off if (!state.switchState) { state.currentState = Characteristic.CurrentHumidifierDehumidifierState.INACTIVE; serviceManager.updateCharacteristic(Characteristic.CurrentHumidifierDehumidifierState,state.currentState); this.previouslyOff = true; return; } //Update "switchState to match device state if (state.targetState === Characteristic.TargetHumidifierDehumidifierState.OFF){ state.currentState = Characteristic.CurrentHumidifierDehumidifierState.INACTIVE; state.switchState = false; this.previouslyOff = true; return; } // Use hardcoded values if not using Humidity values if(config.noHumidity && state.targetState === Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER){ state.currentHumidity = 0 state.HumidifierThreshold = 100 } else if (config.noHumidity && state.targetState === Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER) { state.currentHumidity = 100 state.DehumidifierThreshold = 0 } const desiredState = this.getDesiredState (); if (state.currentState === desiredState && !this.previouslyOff) {return;} const previousState = state.currentState; if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} updateDeviceState: currently ${state.currentState}, changing to ${desiredState}`);} state.currentState = desiredState; this.setCurrentState (null, previousState); this.previouslyOff = false; } // Device Temperature Methods async monitorHumidity () { const { logLevel, config, host, log, name, state } = this; const device = getDevice({ host, log }); // Try again in a second if we don't have a device yet if (!device) { await delayForDuration(1); this.monitorHumidity(); return; } if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} monitorHumidity`);} //Broadlink module emits 'temperature for both sensors. device.on('temperature', this.onHumidity.bind(this)); device.checkHumidity(); this.updateHumidityUI(); if (!config.isUnitTest && !config.noHumidity) {setInterval(()=>{this.getCurrentHumidity(this.updateHumidityUI.bind(this))}, config.humidityUpdateFrequency * 1000)} } onHumidity (temperature,humidity) { const { config, host, logLevel, log, name, state } = this; const { humidityAdjustment } = config; // onHumidity is getting called twice. No known cause currently. // This helps prevent the same humidity from being processed twice if (Object.keys(this.humidityCallbackQueue).length === 0) {return;} humidity += humidityAdjustment; state.currentHumidity = humidity; if(logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} onHumidity (` + humidity + `)`);} //Fakegato history update //Ignore readings of exactly zero - the default no value value. if(config.noHistory !== true && this.state.currentHumidity != 0) { this.lastUpdatedAt = Date.now(); if(logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} Logging data to history: humidity: ${this.state.currentHumidity}`);} this.historyService.addEntry({ time: Math.round(new Date().valueOf() / 1000), humidity: this.state.currentHumidity }); } this.updateDeviceState(); this.processQueuedHumidityCallbacks(humidity); } addHumidityCallbackToQueue (callback) { const { config, host, logLevel, log, name, state } = this; // Clear the previous callback if (Object.keys(this.humidityCallbackQueue).length > 1) { if (state.currentHumidity) { if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} addHumidityCallbackToQueue (clearing previous callback, using existing humidity)`);} this.processQueuedHumidityCallbacks(state.currentHumidity); } } // Add a new callback const callbackIdentifier = uuid.v4(); this.humidityCallbackQueue[callbackIdentifier] = callback; // Read temperature from file if (config.humidityFilePath) { this.updateHumidityFromFile(); return; } // Read humidity from mqtt if (config.mqttURL) { const humidity = this.mqttValueForIdentifier('humidity'); this.onHumidity(null,humidity || 0); return; } //Check if we're actually reading from the device if(config.noHumidity) { let humidity = 50; if(state.targetState === Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER){ humidity = 100; } else if(state.targetState === Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER) { humidity = 0; } this.onHumidity(null,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} addHumidityCallbackToQueue (device no longer active, using existing humidity)`);} } this.processQueuedHumidityCallbacks(state.currentHumidity || 0); return; } device.checkHumidity(); if (logLevel <1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} addHumidityCallbackToQueue (requested humidity from device, waiting)`);} } updateHumidityFromFile () { const { config, logLevel, host, log, name, state } = this; const { humidityFilePath, batteryAlerts } = config; let humidity = null; let temperature = null; if (logLevel <=1) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} updateHumidityFromFile reading file: ${humidityFilePath}`);} fs.readFile(humidityFilePath, 'utf8', (err, data) => { if (err) { if (logLevel <=4) {log(`\x1b[31m[ERROR] \x1b[0m${name} updateHumidityFromFile\n\n${err.message}`);} } if (data === undefined || data.trim().length === 0) { if (logLevel <=3) {log(`\x1b[33m[WARNING]\x1b[0m ${name} updateHumidityFromFile error reading file: ${humidityFilePath}, using previous Temperature`);} humidity = (state.currentHumidity || 0); } const lines = data.split(/\r?\n/); if (/^[0-9]+\.*[0-9]*$/.test(lines[0])){ humidity = parseFloat(data); } else { lines.forEach((line) => { if(-1 < line.indexOf(':')){ const value = line.split(':'); if(value[0] == 'temperature') {temperature = parseFloat(value[1]);} if(value[0] == 'humidity') {humidity = parseFloat(value[1]);} if(value[0] == 'battery' && batteryAlerts) {state.batteryLevel = parseFloat(value[1]);} } }); } if (logLevel >= 4) {log(`\x1b[34m[DEBUG]\x1b[0m ${name} updateHumidityFromFile (parsed humidity: ${humidity})`);} this.onHumidity(temperature, humidity); }); } // MQTT onMQTTMessage (identifier, message) { const { state, logLevel, log, name } = this; if (identifier !== 'unknown' && identifier !== 'humidity' && identifier !== 'battery') { if (logLevel <=4) {log(`\x1b[31m[ERROR] \x1b[0m${name} onMQTTMessage (mqtt message received with unexpected identifier: ${identifier}, ${message.toString()})`);} return; } super.onMQTTMessage(identifier, message); let 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 humidityJSON = JSON.parse(value); if (typeof humidityJSON === 'object') { objectFound = true; let values = []; if ((identifier !== 'humidity')){ //Try to locate Battery fields if (values.length === 0) {values = findKey(humidityJSON, 'Batt');} if (values.length === 0) {values = findKey(humidityJSON, 'batt');} if (values.length === 0) {values = findKey(humidityJSON, 'Battery');} if (values.length === 0) {values = findKey(humidityJSON, 'battery');} if(values.length > 0) { batteryValue = values; values = []; } } if (identifier !== 'battery'){ //Try to locate Humidity fields if (values.length === 0) {values = findKey(humidityJSON, 'Hum');} if (values.length === 0) {values = findKey(humidityJSON, 'hum');} if (values.length === 0) {values = findKey(humidityJSON, 'Humidity');} if (values.length === 0) {values = findKey(humidityJSON, 'humidity');} if (values.length === 0) {values = findKey(humidityJSON, 'RelativeHumidity');} if (values.length === 0) {values = findKey(humidityJSON, 'relativehumidity');} if(values.length > 0) { humidityValue = values; values = []; } } if (values.length > 0) { value = values[0]; } else { value = undefined; } } } catch (err) {} //Couldn't parse as JSON if(objectFound) { if(humidityValue !== undefined && humidityValue.length > 0) { this.mqttValues.humidity = parseFloat(humidityValue[0]); } if(batteryValue !== undefined && batteryValue.length > 0) { state.batteryLevel = parseFloat(batteryValue[0]); this.mqttValues.battery = parseFloat(batteryValue[0]); } }else{ if (value === undefined || (typeof value === 'string' && value.trim().length === 0)) { if (logLevel <=4) {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.updateHumidityUI(); } processQueuedHumidityCallbacks (humidity) { if (Object.keys(this.humidityCallbackQueue).length === 0) {return;} Object.keys(this.humidityCallbackQueue).forEach((callbackIdentifier) => { const callback = this.humidityCallbackQueue[callbackIdentifier]; callback(null, humidity); delete this.humidityCallbackQueue[callbackIdentifier]; }) this.humidityCallbackQueue = {}; } updateHumidityUI () { const { config, serviceManager } = this; serviceManager.updateCharacteristic(Characteristic.CurrentRelativeHumidity, this.state.currentHumidity); } getCurrentHumidity (callback) { const { config, host, logLevel, log, name, state, serviceManager } = this; const { noHumidity } = config; this.addHumidityCallbackToQueue(callback); } setupServiceManager () { const { config, data, name, serviceManagerType } = this; const { on, off, targetStateHumidifier, targetStateDehumidifier, lockControls, unlockControls, swingToggle } = data || {}; // Defaults if (config.showLockPhysicalControls !== false) {config.showLockPhysicalControls = true} if (config.showSwingMode !== false && config.hideSwingMode !== true) {config.showSwingMode = true} if (config.showRotationDirection !== false && config.hideRotationDirection !== true) {config.showRotationDirection = true} this.serviceManager = new ServiceManagerTypes[serviceManagerType](name, Service.HumidifierDehumidifier, this.log); this.serviceManager.addToggleCharacteristic({ name: 'switchState', type: Characteristic.Active, getMethod: this.getCharacteristicValue, setMethod: this.setCharacteristicValue, bind: this, props: { onData: on, offData: off, setValuePromise: this.setSwitchState.bind(this) } }); this.serviceManager.addGetCharacteristic({ name: 'currentHumidity', type: Characteristic.CurrentRelativeHumidity, method: this.getCurrentHumidity, bind: this }); this.serviceManager.addToggleCharacteristic({ name: 'HumidifierThreshold', type: Characteristic.RelativeHumidityHumidifierThreshold, getMethod: this.getCharacteristicValue, setMethod: this.setCharacteristicValue, bind: this, props: { setValuePromise: this.setHumidifierThreshold.bind(this) } }); this.serviceManager.addToggleCharacteristic({ name: 'DehumidifierThreshold', type: Characteristic.RelativeHumidityDehumidifierThreshold, getMethod: this.getCharacteristicValue, setMethod: this.setCharacteristicValue, bind: this, props: { setValuePromise: this.setDehumidifierThreshold.bind(this) } }); this.serviceManager.addToggleCharacteristic({ name: 'currentState', type: Characteristic.CurrentHumidifierDehumidifierState, getMethod: this.getCharacteristicValue, setMethod: this.setCharacteristicValue, bind: this, props: { } }); this.serviceManager.addToggleCharacteristic({ name: 'targetState', type: Characteristic.TargetHumidifierDehumidifierState, getMethod: this.getCharacteristicValue, setMethod: this.setCharacteristicValue, bind: this, props: { setValuePromise: this.updateDeviceState.bind(this) } }); //Remove Auto mode if not getting Humidity readings if(config.noHumidity) { this.serviceManager .getCharacteristic(Characteristic.TargetHumidifierDehumidifierState) .setProps({ validValues: [0, 1, 2] }); } if (config.humidifierOnly) { this.serviceManager .getCharacteristic(Characteristic.TargetHumidifierDehumidifierState) .setProps({ validValues: [1] }); this.serviceManager .getCharacteristic(Characteristic.CurrentHumidifierDehumidifierState) .setProps({ validValues: [0, 2] }); this.serviceManager .getCharacteristic(Characteristic.RelativeHumidityDehumidifierThreshold) .setProps({ validValues: [100] }); } if (config.deHumidifierOnly) { this.serviceManager .getCharacteristic(Characteristic.TargetHumidifierDehumidifierState) .setProps({ validValues: [2] }); this.serviceManager .getCharacteristic(Characteristic.CurrentHumidifierDehumidifierState) .setProps({ validValues: [0, 3] }); this.serviceManager .getCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold) .setProps({ validValues: [0] }); } if (config.showLockPhysicalControls) { this.serviceManager.addToggleCharacteristic({ name: 'lockPhysicalControls', type: Characteristic.LockPhysicalControls, getMethod: this.getCharacteristicValue, setMethod: this.setCharacteristicValue, bind: this, props: { onData: lockControls, offData: unlockControls, setValuePromise: this.performSend.bind(this) } }); } if (config.showSwingMode) { this.serviceManager.addToggleCharacteristic({ name: 'swingMode', type: Characteristic.SwingMode, getMethod: this.getCharacteristicValue, setMethod: this.setCharacteristicValue, bind: this, props: { onData: swingToggle, offData: swingToggle, setValuePromise: this.performSend.bind(this) } }); } this.serviceManager.addToggleCharacteristic({ name: 'fanSpeed', type: Characteristic.RotationSpeed, getMethod: this.getCharacteristicValue, setMethod: this.setCharacteristicValue, bind: this, props: { setValuePromise: this.setFanSpeed.bind(this), minStep: config.stepSize, minValue: 0, maxVlue: 100 } }); } } module.exports = HumidifierDehumidifierAccessory;