From e85bcfcf32bd097a641c1b598a9df7ea33bbe199 Mon Sep 17 00:00:00 2001 From: Kiwi Cam <32912464+kiwi-cam@users.noreply.github.com> Date: Sat, 17 Jun 2023 16:11:30 +1200 Subject: [PATCH] Trying banboobees changes --- accessories/accessory.js | 12 +- accessories/aircon.js | 364 ++++++++++++++++++++++++++++++++++----- accessories/light.js | 79 ++++++++- accessories/switch.js | 94 +++++++++- accessories/tv.js | 48 +++++- base/accessory.js | 30 +++- helpers/getDevice.js | 17 +- platform.js | 2 +- 8 files changed, 567 insertions(+), 79 deletions(-) diff --git a/accessories/accessory.js b/accessories/accessory.js index 07f34892..d254839e 100644 --- a/accessories/accessory.js +++ b/accessories/accessory.js @@ -88,7 +88,7 @@ class BroadlinkRMAccessory extends HomebridgeAccessory { const device = getDevice({ host, log }); if (!host || !device) { // Error reporting - sendData({ host, hexData: data, log, name, logLevel }); + await sendData({ host, hexData: data, log, name, logLevel }); return; } @@ -107,9 +107,8 @@ class BroadlinkRMAccessory extends HomebridgeAccessory { await this.performRepeatSend(data[index], actionCallback); if (pause) { - // this.pauseTimeoutPromise = delayForDuration(pause); - // await this.pauseTimeoutPromise; await new Promise(resolve => setTimeout(resolve, pause * 1000)); + log(`${name} pause (${device.host.address}; ${device.host.macAddress}) ${pause * 1000} ms`); } } }); @@ -126,10 +125,9 @@ class BroadlinkRMAccessory extends HomebridgeAccessory { for (let index = 0; data && index < sendCount; index++) { await sendData({ host, hexData: data, log, name, logLevel }); - if (interval && index < sendCount - 1) { - // this.intervalTimeoutPromise = delayForDuration(interval); - // await this.intervalTimeoutPromise; - await new Promise(resolve => setTimeout(resolve, interval * 1000)); + if (interval && index < sendCount) { + await new Promise(resolve => setTimeout(resolve, interval * 1000)); + log(`${name} interval (${host}) ${interval * 1000} ms`); } } } diff --git a/accessories/aircon.js b/accessories/aircon.js index c82c5394..dd61f1a0 100644 --- a/accessories/aircon.js +++ b/accessories/aircon.js @@ -1,3 +1,4 @@ +// -*- mode: js; js-indent-level : 2 -*- const { assert } = require('chai'); const uuid = require('uuid'); const fs = require('fs'); @@ -34,20 +35,61 @@ class AirConAccessory extends BroadlinkRMAccessory { // 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.services = this.getServices(); + //this.displayName = config.name; + //this.lastUpdatedAt = undefined; + this.historyService = new HistoryService( + config.enableModeHistory ? 'custom' : 'room', + {displayName: config.name, services: this.getServices(), log: log}, + {storage: 'fs', filename: 'RMPro_' + config.name.replace(' ','-') + '_persist.json'}); + // this.historyService.log = this.log; + + if (config.enableModeHistory) { + this.valveInterval = 1; + let state2 = this.state; + //console.log(state2) + this.state = new Proxy(state2, { + set: async function(target, key, value) { + if (target[key] != value) { + Reflect.set(target, key, value); + if (this.historyService) { + if (key == 'targetTemperature') { + //this.log(`adding history of targetTemperature.`, value) + this.historyService.addEntry( + {time: Math.round(new Date().valueOf()/1000), + setTemp: value || 30}) + await this.mqttpublish('targetTemperature', value) + } else if (key == 'targetHeatingCoolingState') { + // this.log(`adding history of targetHeatingCoolingState.`, value * 25) + // this.historyService.addEntry( + // {time: Math.round(new Date().valueOf()/1000), + // valvePosition: value ? Math.round((this.state.currentTemperature - this.state.targetTemperature)/this.state.targetTemperature*100 + 50) : 0 + // //value * 25 + // }) + this.valveInterval = 1; + clearTimeout(this.valveTimer); + this.thermoHistory(); + } else if (key == 'currentHeatingCoolingState') { + await this.mqttpublish('mode', this.HeatingCoolingConfigKeys[value]) + await this.mqttpublish('targetTemperature', this.state.targetTemperature) + } + } + } + return true + }.bind(this) + }) + } } this.temperatureCallbackQueue = {}; this.monitorTemperature(); + this.thermoHistory(); } correctReloadedState (state) { - if (state.currentHeatingCoolingState === Characteristic.CurrentHeatingCoolingState.OFF) { - state.targetTemperature = state.currentTemperature; - } + //if (state.currentHeatingCoolingState === Characteristic.CurrentHeatingCoolingState.OFF) { + // state.targetTemperature = state.currentTemperature; + //} state.targetHeatingCoolingState = state.currentHeatingCoolingState; @@ -98,6 +140,7 @@ class AirConAccessory extends BroadlinkRMAccessory { // Set state default values // state.targetTemperature = state.targetTemperature || config.minTemperature; + state.targetTemperature = state.targetTemperature || config.maxTemperature || config.minTemperature; state.currentHeatingCoolingState = state.currentHeatingCoolingState || Characteristic.CurrentHeatingCoolingState.OFF; state.targetHeatingCoolingState = state.targetHeatingCoolingState || Characteristic.TargetHeatingCoolingState.OFF; state.firstTemperatureUpdate = true; @@ -138,22 +181,52 @@ class AirConAccessory extends BroadlinkRMAccessory { } } - updateServiceTargetHeatingCoolingState (value) { + async updateServiceTargetHeatingCoolingState (value) { const { serviceManager, state } = this; - delayForDuration(0.2).then(() => { + await delayForDuration(0.2).then(() => { serviceManager.setCharacteristic(Characteristic.TargetHeatingCoolingState, value); }); } - updateServiceCurrentHeatingCoolingState (value) { - const { serviceManager, state } = this; + async updateServiceCurrentHeatingCoolingState (value) { + const { serviceManager, name, state, log, logLevel } = this; + const keys = this.HeatingCoolingConfigKeys; + let update = value; + + if (value === Characteristic.TargetHeatingCoolingState.AUTO) { + if (state.currentTemperature <= state.targetTemperature) { + update = Characteristic.TargetHeatingCoolingState.COOL; + } else { + update = Characteristic.TargetHeatingCoolingState.HEAT; + } + log(`${name} updateServiceCurrentHeatingCoolingState target:${keys[value]} update:${keys[update]}`); + } - delayForDuration(0.25).then(() => { - serviceManager.setCharacteristic(Characteristic.CurrentHeatingCoolingState, value); + await delayForDuration(0.25).then(() => { + serviceManager.setCharacteristic(Characteristic.CurrentHeatingCoolingState, update); }); } + async getCurrentHeatingCoolingState (current) { + const { serviceManager, name, state, log, logLevel } = this; + const keys = this.HeatingCoolingConfigKeys; + let target = state.targetHeatingCoolingState; + let update = current; + + if (current !== Characteristic.TargetHeatingCoolingState.OFF && + target === Characteristic.TargetHeatingCoolingState.AUTO) { + if (state.currentTemperature <= state.targetTemperature) { + update = Characteristic.TargetHeatingCoolingState.COOL; + } else { + update = Characteristic.TargetHeatingCoolingState.HEAT; + } + if (logLevel <=1) log(`${name} getCurrentHeatingCoolingState current:${keys[current]} update:${keys[update]}`); + } + + return update; + } + // Allows this accessory to know about switch accessories that can determine whether // auto-on/off should be permitted. @@ -176,8 +249,8 @@ class AirConAccessory extends BroadlinkRMAccessory { return (!this.autoSwitchAccessory || (this.autoSwitchAccessory && this.autoSwitchAccessory.state && this.autoSwitchAccessory.state.switchState)); } - setTargetTemperature (previousValue) { - const { config, log, logLevel, name, serviceManager, state } = this; + async setTargetTemperature (HexData,previousValue) { + const { HeatingCoolingConfigKeys, data, config, log, logLevel, name, serviceManager, state } = this; const { preventResendHex, minTemperature, maxTemperature } = config; if (state.targetTemperature === previousValue && preventResendHex && !this.previouslyOff) {return;} @@ -187,6 +260,20 @@ class AirConAccessory extends BroadlinkRMAccessory { 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})`);} + const mode = HeatingCoolingConfigKeys[state.targetHeatingCoolingState]; + const r = new RegExp(`${mode}`); + const k = Object.keys(data).sort().filter(x => x.match(r)); + const modemin = parseInt(k[0].match(/\d+/)[0]); + const modemax = parseInt(k[k.length - 1].match(/\d+/)[0]); + const temperature = state.targetTemperature; + if (temperature < modemin) { + state.targetTemperature = previousValue; + throw new Error(`${name} Target temperature (${temperature}) is below minimal ${mode} temperature (${modemin})`); + } else if (temperature > modemax) { + state.targetTemperature = previousValue; + throw new Error(`${name} Target temperature (${temperature}) is above maxmum ${mode} temperature (${modemax})`); + } + // Used within correctReloadedState() so that when re-launching the accessory it uses // this temperature rather than one automatically set. state.userSpecifiedTargetTemperature = state.targetTemperature; @@ -211,7 +298,7 @@ class AirConAccessory extends BroadlinkRMAccessory { if (state.targetHeatingCoolingState === state.currentHeatingCoolingState && preventResendHex) {return;} if (targetHeatingCoolingState === 'off') { - this.updateServiceCurrentHeatingCoolingState(HeatingCoolingStates.off); + await this.updateServiceCurrentHeatingCoolingState(HeatingCoolingStates.off); if (currentHeatingCoolingState === 'cool' && data.offDryMode !== undefined) { // Dry off mode when previously cooling @@ -237,24 +324,40 @@ class AirConAccessory extends BroadlinkRMAccessory { // 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]); + await this.updateServiceTargetHeatingCoolingState(HeatingCoolingStates[replaceAutoMode]); return; } let temperature = state.targetTemperature; - let mode = HeatingCoolingConfigKeys[state.targetHeatingCoolingState]; - + const mode = HeatingCoolingConfigKeys[state.targetHeatingCoolingState]; + const r = new RegExp(`${mode}`); + const k = Object.keys(data).sort().filter(x => x.match(r)); + const modemin = parseInt(k[0].match(/\d+/)[0]); + const modemax = parseInt(k[k.length - 1].match(/\d+/)[0]); + this.log(`${name} setTargetHeatingCoolingState mode(${mode}) range[${modemin}, ${modemax}]`); + // serviceManager.getCharacteristic(Characteristic.TargetTemperature).setProps({ + // minValue: modemin, + // maxValue: modemax, + // minstep: 1 + // }); + // this.updateServiceCurrentHeatingCoolingState(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; + if (temperature < modemin || temperature > modemax) { + + // Selecting a heating/cooling state allows a default temperature to be used for the given state. + if (state.targetHeatingCoolingState === Characteristic.TargetHeatingCoolingState.AUTO) { + temperature = temperature - modemax > 0 ? modemax : mododemin; + } else if (state.targetHeatingCoolingState === Characteristic.TargetHeatingCoolingState.HEAT) { + temperature = modemin; + } else if (state.targetHeatingCoolingState === Characteristic.TargetHeatingCoolingState.COOL) { + temperature = modemax; + } } //Set the mode, and send the mode hex - this.updateServiceCurrentHeatingCoolingState(state.targetHeatingCoolingState); + await this.updateServiceCurrentHeatingCoolingState(state.targetHeatingCoolingState); if (data.heat && mode === 'heat'){ await this.performSend(data.heat); } else if (data.cool && mode === 'cool'){ @@ -269,8 +372,9 @@ class AirConAccessory extends BroadlinkRMAccessory { if (logLevel <=1) {this.log(`${name} sentMode (${mode})`);} //Force Temperature send - delayForDuration(0.25).then(() => { - this.sendTemperature(temperature, state.currentTemperature); + await delayForDuration(0.25).then(() => { + //this.sendTemperature(temperature, state.currentTemperature); // what a bad. + this.sendTemperature(temperature, previousValue); serviceManager.refreshCharacteristicUI(Characteristic.TargetTemperature); }); } @@ -296,12 +400,12 @@ class AirConAccessory extends BroadlinkRMAccessory { 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); + } + this.autoOffTimeoutPromise = delayForDuration(onDuration); + await this.autoOffTimeoutPromise; + await this.performSend(data.off); + await this.updateServiceTargetHeatingCoolingState(this.HeatingCoolingStates.off); + await this.updateServiceCurrentHeatingCoolingState(this.HeatingCoolingStates.off); } }); } @@ -327,7 +431,7 @@ class AirConAccessory extends BroadlinkRMAccessory { 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]); + await this.updateServiceCurrentHeatingCoolingState(HeatingCoolingStates[mode]); } if((previousTemperature !== finalTemperature) || (state.firstTemperatureUpdate && !preventResendHex)){ @@ -422,7 +526,7 @@ class AirConAccessory extends BroadlinkRMAccessory { if (!config.isUnitTest) {setInterval(this.updateTemperatureUI.bind(this), config.temperatureUpdateFrequency * 1000)} } - onTemperature (temperature,humidity) { + async onTemperature (temperature,humidity) { const { config, host, logLevel, log, name, state } = this; const { minTemperature, maxTemperature, temperatureAdjustment, humidityAdjustment, noHumidity, tempSourceUnits } = config; @@ -448,18 +552,42 @@ class AirConAccessory extends BroadlinkRMAccessory { //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(); + //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){ + if(noHumidity && config.enableModeHistory === false){ this.historyService.addEntry({ time: Math.round(new Date().valueOf() / 1000), temp: this.state.currentTemperature }); + await this.mqttpublish('temperature', `{"temperature":${this.state.currentTemperature}}`); }else{ this.historyService.addEntry({ time: Math.round(new Date().valueOf() / 1000), temp: this.state.currentTemperature, humidity: this.state.currentHumidity }); + await this.mqttpublish('temperature', `{"temperature":${this.state.currentTemperature}, "humidity":${this.state.currentHumidity}}`); } } this.processQueuedTemperatureCallbacks(temperature); } + async thermoHistory() { + const {config} = this; + if (config.noHistory !== true && config.enableModeHistory) { + const valve = this.state.targetHeatingCoolingState ? + (this.state.currentTemperature - this.state.targetTemperature)/this.state.targetTemperature*100*2 + 50 : 0; + if (valve >= 0 && valve <= 100) { + this.historyService.addEntry({ + time: Math.round(new Date().valueOf() / 1000), + setTemp: this.state.targetTemperature, + valvePosition: valve + }); + } else { + this.valveInterval = 0.7; + } + + this.valveInterval = Math.min(this.valveInterval * 1/0.7, 10); + this.valveTimer = setTimeout(() => { + this.thermoHistory(); + }, Math.round(this.valveInterval * 60 * 1000)); + } + } + addTemperatureCallbackToQueue (callback) { const { config, host, logLevel, log, name, state } = this; const { mqttURL, temperatureFilePath, w1DeviceID, noHumidity } = config; @@ -600,7 +728,10 @@ class AirConAccessory extends BroadlinkRMAccessory { Object.keys(this.temperatureCallbackQueue).forEach((callbackIdentifier) => { const callback = this.temperatureCallbackQueue[callbackIdentifier]; - callback(null, temperature); + //callback(null, temperature); + + this.serviceManager.getCharacteristic(Characteristic.CurrentTemperature).updateValue(temperature); + delete this.temperatureCallbackQueue[callbackIdentifier]; }) @@ -627,6 +758,8 @@ class AirConAccessory extends BroadlinkRMAccessory { return callback(null, pseudoDeviceTemperature); } + callback(null, this.state.currentTemperature); + this.addTemperatureCallbackToQueue(callback); } @@ -693,8 +826,57 @@ class AirConAccessory extends BroadlinkRMAccessory { } // MQTT - onMQTTMessage (identifier, message) { - const { state, logLevel, log, name } = this; + async onMQTTMessage (identifier, message) { + const { state, logLevel, log, name, config } = this; + const mqttStateOnly = config.mqttStateOnly === false ? false : true; + + super.onMQTTMessage(identifier, message); + + if (identifier === 'mode' || + identifier.toLowerCase() === 'currentheatingcoolingstate' || + identifier.toLowerCase() === 'currentheatercoolerstate') { + let mode = this.mqttValuesTemp[identifier].toLowerCase(); + switch (mode) { + case 'off': + case 'heat': + case 'cool': + case 'auto': + let state = this.HeatingCoolingStates[mode]; + //log(`${name} onMQTTMessage (set HeatingCoolingState to ${mode}).`); + this.reset(); + if (mqttStateOnly) { + this.state.currentHeatingCoolingState = state; + this.serviceManager.refreshCharacteristicUI(Characteristic.CurrentHeatingCoolingState); + this.state.targetHeatingCoolingState = state; + this.serviceManager.refreshCharacteristicUI(Characteristic.TargetHeatingCoolingState); + } else { + await this.updateServiceTargetHeatingCoolingState(state); + } + log(`${name} onMQTTMessage (set currentHeatingCoolingState to ${this.state.currentHeatingCoolingState}).`); + break; + default: + log(`\x1b[31m[ERROR] \x1b[0m${name} onMQTTMessage (unexpected HeatingCoolingState: ${this.mqttValuesTemp[identifier]})`); + } + return; + } + + if (identifier.toLowerCase() === 'targettemperature' || + identifier.toLowerCase() === 'coolingthresholdtemperature' || + identifier.toLowerCase() === 'heatingthresholdtemperature') { + let target = parseInt(this.mqttValuesTemp[identifier].match(/^([0-9]+)$/g)); + if (target > 0 && target >= config.minTemperature && target <= config.maxTemperature) { + if (mqttStateOnly) { + this.state.targetTemperature = target; + this.serviceManager.refreshCharacteristicUI(Characteristic.TargetTemperature); + } else { + this.serviceManager.setCharacteristic(Characteristic.TargetTemperature, target); + } + log(`${name} onMQTTMessage (set targetTemperature to ${target}).`); + } else { + log(`\x1b[31m[ERROR] \x1b[0m${name} onMQTTMessage (unexpected targetTemperature: ${this.mqttValuesTemp[identifier]})`); + } + return; + } 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()})`); @@ -702,8 +884,6 @@ class AirConAccessory extends BroadlinkRMAccessory { return; } - super.onMQTTMessage(identifier, message); - let temperatureValue, humidityValue, batteryValue; let objectFound = false; let value = this.mqttValuesTemp[identifier]; @@ -788,6 +968,42 @@ class AirConAccessory extends BroadlinkRMAccessory { this.onTemperature(this.mqttValues.temperature,this.mqttValues.humidity); } + getValvePosition(callback) { + let valve = this.state.targetHeatingCoolingState ? + (this.state.currentTemperature - this.state.targetTemperature)/this.state.targetTemperature*100*2 + 50 : 0; + valve = valve < 0 ? 0 : (valve > 100 ? 100 : valve); + //callback(null, this.state.targetHeatingCoolingState * 25); + //console.log('getValvePosition() is requested.', this.displayName, valve); + callback(null, valve); + } + + setProgramCommand(value, callback) { + // not implemented + //console.log('setProgramCommand() is requested. %s', value, this.displayName); + callback(); + } + + getProgramData(callback) { + // not implemented + // var data = "12f1130014c717040af6010700fc140c170c11fa24366684ffffffff24366684ffffffff24366684ffffffff24366684ffffffff24366684ffffffff24366684ffffffff24366684fffffffff42422222af3381900001a24366684ffffffff"; + var data = "ff04f6"; + var buffer = new Buffer.from(('' + data).replace(/[^0-9A-F]/ig, ''), 'hex').toString('base64'); + //console.log('getProgramData() is requested. (%s)', buffer, this.displayName); + callback(null, buffer); + } + + localCharacteristic(key, uuid, props) { + let characteristic = class extends Characteristic { + constructor() { + super(key, uuid); + this.setProps(props); + } + } + characteristic.UUID = uuid; + + return characteristic; + } + // Service Manager Setup setupServiceManager () { @@ -795,6 +1011,68 @@ class AirConAccessory extends BroadlinkRMAccessory { this.serviceManager = new ServiceManagerTypes[serviceManagerType](name, Service.Thermostat, this.log); + config.enableTargetTemperatureHistory = config.enableTargetTemperatureHistory === true || false; + config.enableModeHistory = config.enableModeHistory === true || config.enableTargetTemperatureHistory === true || false; + if (config.noHistory !== true) { + if (config.enableTargetTemperatureHistory === true) { + this.log(`${this.name} Accessory is configured to record HeatingCoolingState and targetTemperature histories.`); + } else if (config.enableModeHistory === true) { + this.log(`${this.name} Accessory is configured to record HeatingCoolingState history.`); + } + } + + if(config.noHistory !== true && config.enableModeHistory) { + const ValvePositionCharacteristic = this.localCharacteristic( + 'ValvePosition', 'E863F12E-079E-48FF-8F27-9C2605A29F52', + {format: Characteristic.Formats.UINT8, + unit: Characteristic.Units.PERCENTAGE, + perms: [ + Characteristic.Perms.READ, + Characteristic.Perms.NOTIFY + ]}); + + this.serviceManager.addGetCharacteristic({ + name: 'currentValvePosition', + //type: eve.Characteristics.ValvePosition, + type: ValvePositionCharacteristic, + method: this.getValvePosition, + bind: this + }); + + if (config.enableTargetTemperatureHistory) { + const ProgramDataCharacteristic = this.localCharacteristic( + 'ProgramData', 'E863F12F-079E-48FF-8F27-9C2605A29F52', + {format: Characteristic.Formats.DATA, + perms: [ + Characteristic.Perms.READ, + Characteristic.Perms.NOTIFY + ]}); + + const ProgramCommandCharacteristic = this.localCharacteristic( + 'ProgramCommand', 'E863F12C-079E-48FF-8F27-9C2605A29F52', + {format: Characteristic.Formats.DATA, + perms: [ + Characteristic.Perms.WRITE + ]}); + + this.serviceManager.addGetCharacteristic({ + name: 'setProgramData', + //type: eve.Characteristics.ProgramData, + type: ProgramDataCharacteristic, + method: this.getProgramData, + bind: this, + }); + + this.serviceManager.addSetCharacteristic({ + name: 'setProgramCommand', + //type: eve.Characteristics.ProgramCommand, + type: ProgramCommandCharacteristic, + method: this.setProgramCommand, + bind: this, + }); + } + } + this.serviceManager.addToggleCharacteristic({ name: 'currentHeatingCoolingState', type: Characteristic.CurrentHeatingCoolingState, @@ -802,7 +1080,7 @@ class AirConAccessory extends BroadlinkRMAccessory { setMethod: this.setCharacteristicValue, bind: this, props: { - + getValuePromise: this.getCurrentHeatingCoolingState.bind(this) } }); diff --git a/accessories/light.js b/accessories/light.js index f46f95fc..e898a1dd 100644 --- a/accessories/light.js +++ b/accessories/light.js @@ -1,3 +1,4 @@ +// -*- js-indent-level : 2 -*- const { assert } = require('chai'); const ServiceManagerTypes = require('../helpers/serviceManagerTypes'); const delayForDuration = require('../helpers/delayForDuration'); @@ -6,15 +7,15 @@ const catchDelayCancelError = require('../helpers/catchDelayCancelError') const SwitchAccessory = require('./switch'); class LightAccessory extends SwitchAccessory { - + setDefaults () { super.setDefaults(); - + const { config } = this; config.onDelay = config.onDelay || 0.1; config.defaultBrightness = config.defaultBrightness || 100; - config.defaultColorTemperature = config.defaultColorTemperature || 140; + config.defaultColorTemperature = config.defaultColorTemperature || 500; } reset () { @@ -82,6 +83,21 @@ class LightAccessory extends SwitchAccessory { } } + async setExclusivesOFF () { + const { log, name, logLevel } = this; + if (this.exclusives) { + this.exclusives.forEach(x => { + if (x.state.switchState) { + this.log(`${name} setSwitchState: (${x.name} is configured to be turned off)`); + x.reset(); + x.state.switchState = false; + x.lastBrightness = undefined; + x.serviceManager.refreshCharacteristicUI(Characteristic.On); + } + }); + } + } + async setSwitchState (hexData, previousValue) { const { config, data, host, log, name, state, logLevel, serviceManager } = this; let { defaultBrightness, useLastKnownBrightness } = config; @@ -93,18 +109,21 @@ class LightAccessory extends SwitchAccessory { this.setExclusivesOFF(); const brightness = (useLastKnownBrightness && state.brightness > 0) ? state.brightness : defaultBrightness; const colorTemperature = useLastKnownColorTemperature ? state.colorTemperature : defaultColorTemperature; - if (brightness !== state.brightness || previousValue !== state.switchState) { + if (brightness !== state.brightness || previousValue !== state.switchState || colorTemperature !== state.colorTemperature) { log(`${name} setSwitchState: (brightness: ${brightness})`); state.switchState = false; state.brightness = brightness; serviceManager.setCharacteristic(Characteristic.Brightness, brightness); + serviceManager.refreshCharacteristicUI(Characteristic.Brightness); if (this.dataKeys('colorTemperature').length > 0) { state.colorTemperature = colorTemperature; serviceManager.setCharacteristic(Characteristic.ColorTemperature, colorTemperature); + serviceManager.refreshCharacteristicUI(Characteristic.ColorTemperature); } } else { if (hexData) {await this.performSend(hexData);} + await this.mqttpublish('On', 'true'); this.checkAutoOnOff(); } @@ -112,6 +131,7 @@ class LightAccessory extends SwitchAccessory { this.lastBrightness = undefined; if (hexData) {await this.performSend(hexData);} + await this.mqttpublish('On', 'false'); this.checkAutoOnOff(); } @@ -142,6 +162,7 @@ class LightAccessory extends SwitchAccessory { this.onDelayTimeoutPromise = delayForDuration(onDelay); await this.onDelayTimeoutPromise; } + await this.mqttpublish('On', 'true'); } // Find hue closest to the one requested @@ -170,6 +191,7 @@ class LightAccessory extends SwitchAccessory { if (state.brightness > 0) { state.switchState = true; + // await this.mqttpublish('On', 'true'); } await this.checkAutoOnOff(); @@ -191,10 +213,10 @@ class LightAccessory extends SwitchAccessory { log(`${name} setBrightness: (turn on, wait ${onDelay}s)`); await this.performSend(on); - log(`${name} setHue: (wait ${onDelay}s then send data)`); this.onDelayTimeoutPromise = delayForDuration(onDelay); await this.onDelayTimeoutPromise; } + await this.mqttpublish('On', 'true'); } if (data['brightness+'] || data['brightness-'] || data['availableBrightnessSteps']) { @@ -231,6 +253,7 @@ class LightAccessory extends SwitchAccessory { } else { log(`${name} setBrightness: (off)`); await this.performSend(off); + await this.mqttpublish('On', 'false'); } await this.checkAutoOnOff(); @@ -256,6 +279,7 @@ class LightAccessory extends SwitchAccessory { this.onDelayTimeoutPromise = delayForDuration(onDelay); await this.onDelayTimeoutPromise; } + await this.mqttpublish('On', 'true'); } if (data['colorTemperature+'] || data['colorTemperature-'] || data['availableColorTemperatureSteps']) { assert(data['colorTemperature+'] && data['colorTemperature-'] && data['availableColorTemperatureSteps'], `\x1b[31m[CONFIG ERROR] \x1b[33mcolorTemperature+, colorTemperature- and availableColorTemperatureSteps\x1b[0m need to be set.`); @@ -313,12 +337,51 @@ class LightAccessory extends SwitchAccessory { return foundValues } + async getLastActivation(callback) { + const lastActivation = this.state.lastActivation ? + Math.max(0, this.state.lastActivation - this.historyService.getInitialTime()) : 0; + + callback(null, lastActivation); + } + + localCharacteristic(key, uuid, props) { + let characteristic = class extends Characteristic { + constructor() { + super(key, uuid); + this.setProps(props); + } + } + characteristic.UUID = uuid; + + return characteristic; + } + setupServiceManager () { const { data, name, config, serviceManagerType } = this; const { on, off } = data || { }; + const history = config.history === true || config.noHistory === false; - this.serviceManager = new ServiceManagerTypes[serviceManagerType](name, Service.Lightbulb, this.log); - + //this.serviceManager = new ServiceManagerTypes[serviceManagerType](name, Service.Lightbulb, this.log); + this.serviceManager = new ServiceManagerTypes[serviceManagerType](name, history ? Service.Switch : Service.Lightbulb, this.log); + + if (history) { + const LastActivationCharacteristic = this.localCharacteristic( + 'LastActivation', 'E863F11A-079E-48FF-8F27-9C2605A29F52', + {format: Characteristic.Formats.UINT32, + unit: Characteristic.Units.SECONDS, + perms: [ + Characteristic.Perms.READ, + Characteristic.Perms.NOTIFY + ]}); + + this.serviceManager.addGetCharacteristic({ + name: 'LastActivation', + type: LastActivationCharacteristic, + method: this.getLastActivation, + bind: this + }); + } + this.serviceManager.addToggleCharacteristic({ name: 'switchState', type: Characteristic.On, @@ -353,7 +416,7 @@ class LightAccessory extends SwitchAccessory { bind: this, props: { setValuePromise: this.setColorTemperature.bind(this), - ignorePreviousValue: true + ignorePreviousValue: true // TODO: Check what this does and test it } }); } diff --git a/accessories/switch.js b/accessories/switch.js index c311c4c6..4a45dac2 100644 --- a/accessories/switch.js +++ b/accessories/switch.js @@ -1,3 +1,4 @@ +// -*- js-indent-level : 2 -*- const ServiceManagerTypes = require('../helpers/serviceManagerTypes'); const delayForDuration = require('../helpers/delayForDuration'); const catchDelayCancelError = require('../helpers/catchDelayCancelError'); @@ -10,8 +11,37 @@ class SwitchAccessory extends BroadlinkRMAccessory { constructor (log, config = {}, serviceManagerType) { super(log, config, serviceManagerType); - if (!config.isUnitTest) {this.checkPing(ping)} - + // Fakegato setup + if (config.history === true || config.noHistory === false) { + this.historyService = new HistoryService('switch', { displayName: config.name, log: log }, { storage: 'fs', filename: 'RMPro_' + config.name.replace(' ','-') + '_persist.json'}); + this.historyService.addEntry( + {time: Math.round(new Date().valueOf()/1000), + status: this.state.switchState ? 1 : 0}) + + let state2 = this.state; + this.state = new Proxy(state2, { + set: async function(target, key, value) { + if (target[key] != value) { + Reflect.set(target, key, value); + if (this.historyService) { + if (key == `switchState`) { + //this.log(`adding history of switchState.`, value); + const time = Math.round(new Date().valueOf()/1000); + //if (value) { + this.state.lastActivation = time; + //} + this.historyService.addEntry( + {time: time, status: value ? 1 : 0}) + // await this.mqttpublish('On', value ? 'true' : 'false') + } + } + } + return true + }.bind(this) + }) + + if (!config.isUnitTest) {this.checkPing(ping)} + } } setDefaults () { @@ -113,10 +143,12 @@ class SwitchAccessory extends BroadlinkRMAccessory { this.reset(); if (hexData) {await this.performSend(hexData);} + await this.mqttpublish('On', state.switchState ? 'true' : 'false') if (config.stateless === true) { state.switchState = false; serviceManager.refreshCharacteristicUI(Characteristic.On); + await this.mqttpublish('On', 'false') } else { this.checkAutoOnOff(); } @@ -169,12 +201,70 @@ class SwitchAccessory extends BroadlinkRMAccessory { }); } + async getLastActivation(callback) { + const lastActivation = this.state.lastActivation ? + Math.max(0, this.state.lastActivation - this.historyService.getInitialTime()) : 0; + + callback(null, lastActivation); + } + + localCharacteristic(key, uuid, props) { + let characteristic = class extends Characteristic { + constructor() { + super(key, uuid); + this.setProps(props); + } + } + characteristic.UUID = uuid; + + return characteristic; + } + + // MQTT + onMQTTMessage (identifier, message) { + const { state, logLevel, log, name, config } = this; + const mqttStateOnly = config.mqttStateOnly === false ? false : true; + + super.onMQTTMessage(identifier, message); + + if (identifier.toLowerCase() === 'on') { + const on = this.mqttValuesTemp[identifier] === 'true' ? true : false; + this.reset(); + if (mqttStateOnly) { + this.state.switchState = on; + this.serviceManager.refreshCharacteristicUI(Characteristic.On); + } else { + this.serviceManager.setCharacteristic(Characteristic.On, on) + } + log(`${name} onMQTTMessage (set switchState to ${this.state.switchState}).`); + } + } + setupServiceManager () { const { data, name, config, serviceManagerType } = this; const { on, off } = data || { }; + const history = config.history === true || config.noHistory === false; this.serviceManager = new ServiceManagerTypes[serviceManagerType](name, Service.Switch, this.log); + if (history) { + const LastActivationCharacteristic = this.localCharacteristic( + 'LastActivation', 'E863F11A-079E-48FF-8F27-9C2605A29F52', + {format: Characteristic.Formats.UINT32, + unit: Characteristic.Units.SECONDS, + perms: [ + Characteristic.Perms.READ, + Characteristic.Perms.NOTIFY + ]}); + + this.serviceManager.addGetCharacteristic({ + name: 'LastActivation', + type: LastActivationCharacteristic, + method: this.getLastActivation, + bind: this + }); + } + this.serviceManager.addToggleCharacteristic({ name: 'switchState', type: Characteristic.On, diff --git a/accessories/tv.js b/accessories/tv.js index 1812336a..63f62ef6 100644 --- a/accessories/tv.js +++ b/accessories/tv.js @@ -92,26 +92,28 @@ class TVAccessory extends BroadlinkRMAccessory { } async pingCallback(active) { - const { config, state, serviceManager } = this; + const { name, config, state, serviceManager, log } = this; if (this.stateChangeInProgress){ return; } if (this.lastPingResponse !== undefined && this.lastPingResponse !== active) { + // console.log(`[${new Date().toLocaleString()}] ${name} Ping: Turned ${active ? 'on' : 'off'}.`); if (config.syncInputSourceWhenOn && active && this.state.currentInput !== undefined) { + log(`${name} received ping response. Sync input source.`); await this.setInputSource(); // sync if asynchronously turned on } } this.lastPingResponse = active; if (config.pingIPAddressStateOnly) { - state.switchState = active ? true : false; + state.switchState = active ? 1 : 0; serviceManager.refreshCharacteristicUI(Characteristic.Active); return; } - const value = active ? true : false; + const value = active ? 1 : 0; serviceManager.setCharacteristic(Characteristic.Active, value); } @@ -124,6 +126,7 @@ class TVAccessory extends BroadlinkRMAccessory { if (hexData) {await this.performSend(hexData);} this.checkAutoOnOff(); + // console.log(`[${new Date().toLocaleString()}] ${name} Active: set to ${this.state.switchState ? 'ON' : 'OFF'}.`); } async checkPingGrace () { @@ -202,6 +205,8 @@ class TVAccessory extends BroadlinkRMAccessory { } await this.performSend(data.inputs[newValue].data); + // this.serviceManager.setCharacteristic(Characteristic.ActiveIdentifier, newValue); + // log(`${name} select input source to ${data.inputs[newValue].name}(${newValue}).`); } setupServiceManager() { @@ -247,6 +252,34 @@ class TVAccessory extends BroadlinkRMAccessory { } }); + // this.serviceManager.setCharacteristic(Characteristic.ActiveIdentifier, 1); + + // this.serviceManager + // .getCharacteristic(Characteristic.ActiveIdentifier) + // .on('get', (callback) => { + // //console.log(`${name} Input: get ${this.state.input}.`); + // callback(null, this.state.input || 0); + // }) + // .on('set', (newValue, callback) => { + // if ( + // !data || + // !data.inputs || + // !data.inputs[newValue] || + // !data.inputs[newValue].data + // ) { + // log(`${name} Input: No input data found. Ignoring request.`); + // callback(null); + // return; + // } + + // this.state.input = newValue; + // //this.serviceManager.setCharacteristic(Characteristic.ActiveIdentifier, newValue); + // this.performSend(data.inputs[newValue].data); + + // callback(null); + // console.log(`${name} Input: set to ${newValue}.`); + // }); + this.serviceManager .getCharacteristic(Characteristic.RemoteKey) .on('set', async (newValue, callback) => { @@ -350,7 +383,8 @@ class TVAccessory extends BroadlinkRMAccessory { callback(null); }); - const speakerService = new Service.TelevisionSpeaker('Speaker', 'Speaker'); + // const speakerService = new Service.TelevisionSpeaker('Speaker', 'Speaker'); + const speakerService = new Service.TelevisionSpeaker(`${name} Speaker`, '${name} Speaker'); speakerService.setCharacteristic( Characteristic.Active, @@ -393,11 +427,12 @@ class TVAccessory extends BroadlinkRMAccessory { await this.performSend(hexData); callback(null); }); - + speakerService.setCharacteristic(Characteristic.Mute, false); speakerService .getCharacteristic(Characteristic.Mute) .on('get', (callback) => { + // console.log(`${name} Mute: get ${this.state.Mute}.`); callback(null, this.state.Mute || false); }) .on('set', async (newValue, callback) => { @@ -428,7 +463,8 @@ class TVAccessory extends BroadlinkRMAccessory { if (data.inputs && data.inputs instanceof Array) { for (let i = 0; i < data.inputs.length; i++) { const input = data.inputs[i]; - const inputService = new Service.InputSource(`input${i}`, `input${i}`); + // const inputService = new Service.InputSource(`input${i}`, `input${i}`); + const inputService = new Service.InputSource(`${name} input${i}`, `${name} input${i}`); inputService .setCharacteristic(Characteristic.Identifier, i) diff --git a/base/accessory.js b/base/accessory.js index c9b8c522..cd6534a2 100644 --- a/base/accessory.js +++ b/base/accessory.js @@ -142,9 +142,9 @@ class HomebridgeAccessory { const data = value ? onData : offData; if (setValuePromise) { - setValuePromise(data, previousValue); + await setValuePromise(data, previousValue); } else if (data) { - this.performSetValueAction({ host, data, log, name }); + await this.performSetValueAction({ host, data, log, name }); } callback(null); } catch (err) { @@ -154,7 +154,7 @@ class HomebridgeAccessory { } async getCharacteristicValue(props, callback) { - const { propertyName } = props; + const { propertyName, getValuePromise } = props; const { log, name, logLevel } = this; let value; @@ -171,6 +171,11 @@ class HomebridgeAccessory { value = this.state[propertyName]; } + if (getValuePromise) { + value = await getValuePromise(value); + this.state[propertyName] = value; + } + if (this.logLevel <= 1) {log(`${name} get${capitalizedPropertyName}: ${value}`);} callback(null, value); } @@ -262,13 +267,13 @@ class HomebridgeAccessory { const { config, log, logLevel, name } = this; let { mqttTopic, mqttURL, mqttUsername, mqttPassword } = config; - if (!mqttTopic || !mqttURL) {return;} + if (!mqttURL) {return;} this.mqttValues = {}; this.mqttValuesTemp = {}; // Perform some validation of the mqttTopic option in the config. - if (typeof mqttTopic !== 'string' && !Array.isArray(mqttTopic)) { + if (mqttTopic && typeof mqttTopic !== 'string' && !Array.isArray(mqttTopic)) { if (this.logLevel <= 4) {log(`\x1b[31m[CONFIG ERROR]\x1b[0m ${name} \x1b[33mmqttTopic\x1b[0m value is incorrect. Please check out the documentation for more details.`)} return; @@ -304,7 +309,7 @@ class HomebridgeAccessory { // Create an easily referenced instance variable const mqttTopicIdentifiersByTopic = {}; - mqttTopic.forEach(({ identifier, topic }) => { + mqttTopic && mqttTopic.forEach(({ identifier, topic }) => { mqttTopicIdentifiersByTopic[topic] = identifier; }) @@ -346,7 +351,7 @@ class HomebridgeAccessory { if (this.logLevel <= 2) {log(`\x1b[35m[INFO]\x1b[0m ${name} MQTT client connected.`)} - mqttTopic.forEach(({ topic }) => { + mqttTopic && mqttTopic.forEach(({ topic }) => { mqttClient.subscribe(topic) }) }) @@ -362,6 +367,17 @@ class HomebridgeAccessory { }) } + async mqttpublish (topic, message) { + if (this.mqttClient) { + try { + await this.mqttClient.publish(`homebridge-broadlink-rm/${this.config.type}/${this.name}/${topic}`, `${message}`) + // this.log(`${this.name}: MQTT publish(topic: ${topic}, message: ${message})`) + } catch (e) { + this.log(`${this.name}: Failed to publish MQTT message. ${e}`) + } + } + } + onMQTTMessage(identifier, message) { this.mqttValuesTemp[identifier] = message.toString(); } diff --git a/helpers/getDevice.js b/helpers/getDevice.js index 0e99e87f..1c8ed219 100644 --- a/helpers/getDevice.js +++ b/helpers/getDevice.js @@ -4,7 +4,7 @@ const delayForDuration = require('./delayForDuration'); const dgram = require('dgram'); const Mutex = require('await-semaphore').Mutex; -const pingFrequency = 5000; +const pingFrequency = 20000; const keepAliveFrequency = 90000; const pingTimeout = 5; @@ -35,16 +35,22 @@ const startPing = (device, log) => { } if (!active && device.state === 'active' && device.retryCount === 2) { - log(`Broadlink RM device at ${device.host.address} (${device.host.macAddress || ''}) is no longer reachable after three attempts.`); + if (!broadlink.accessories || broadlink.accessories.find((x) => x.host === undefined || x.host === device.host.address || x.host === device.host.macAddress)) { + log(`Broadlink RM device at ${device.host.address} (${device.host.macAddress || ''}) is no longer reachable after three attempts.`); + } device.state = 'inactive'; device.retryCount = 0; } else if (!active && device.state === 'active') { - if(broadlink.debug) {log(`Broadlink RM device at ${device.host.address} (${device.host.macAddress || ''}) is no longer reachable. (attempt ${device.retryCount})`);} + if (!broadlink.accessories || broadlink.accessories.find((x) => x.host === undefined || x.host === device.host.address || x.host === device.host.macAddress)) { + if(broadlink.debug) {log(`Broadlink RM device at ${device.host.address} (${device.host.macAddress || ''}) is no longer reachable. (attempt ${device.retryCount})`);} + } device.retryCount += 1; } else if (active && device.state !== 'active') { - if (device.state === 'inactive') {log(`Broadlink RM device at ${device.host.address} (${device.host.macAddress || ''}) has been re-discovered.`);} + if (!broadlink.accessories || broadlink.accessories.find((x) => x.host === undefined || x.host === device.host.address || x.host === device.host.macAddress)) { + if (device.state === 'inactive') {log(`Broadlink RM device at ${device.host.address} (${device.host.macAddress || ''}) has been re-discovered.`);} + } device.state = 'active'; device.retryCount = 0; @@ -63,9 +69,10 @@ const discoveredDevices = {}; const manualDevices = {}; let discoverDevicesInterval; -const discoverDevices = (automatic = true, log, logLevel, deviceDiscoveryTimeout = 60) => { +const discoverDevices = (automatic = true, log, logLevel, deviceDiscoveryTimeout = 60, accessories = null) => { broadlink.log = log; broadlink.debug = logLevel <=1; + broadlink.accessories = accessories; //broadlink.logLevel = logLevel; if (automatic) { diff --git a/platform.js b/platform.js index af83be47..972050c3 100644 --- a/platform.js +++ b/platform.js @@ -110,7 +110,7 @@ const BroadlinkRMPlatform = class extends HomebridgePlatform { if (!hosts) { if (logLevel <=2) {log(`\x1b[35m[INFO]\x1b[0m Automatically discovering Broadlink RM devices.`)} - discoverDevices(true, log, logLevel, config.deviceDiscoveryTimeout); + discoverDevices(true, log, logLevel, config.deviceDiscoveryTimeout, config.accessories); return; }