From 23eec416a8b9200ee5cd587d53bafce302dcf6af Mon Sep 17 00:00:00 2001 From: chria Date: Sun, 26 Dec 2021 00:51:57 +0000 Subject: [PATCH 1/2] Added TRV support --- index.js | 8 ++ lib/trv_accessory.js | 196 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 lib/trv_accessory.js diff --git a/index.js b/index.js index 12b2b96d..dbd1691c 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,7 @@ const Fanv2Accessory = require('./lib/fanv2_accessory'); const HeaterAccessory = require('./lib/heater_accessory'); const GarageDoorAccessory = require('./lib/garagedoor_accessory'); const AirPurifierAccessory = require('./lib/air_purifier_accessory') +const TRVAccessory = require('./lib/trv_accessory') const WindowCoveringAccessory = require('./lib/window_covering_accessory') const ContactSensorAccessory = require('./lib/contactsensor_accessory'); const LeakSensorAccessory = require('./lib/leak_sensor_accessory') @@ -183,7 +184,13 @@ class TuyaPlatform { this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); this.deviceAccessories.set(uuid, deviceAccessory); break; + case 'wk': + deviceAccessory = new TRVAccessory(this, homebridgeAccessory, device); + this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); + this.deviceAccessories.set(uuid, deviceAccessory); + break; default: + this.log.log(deviceType+" was not a recognised deviceType") break; } @@ -211,6 +218,7 @@ class TuyaPlatform { async refreshDeviceStates(message) { const uuid = this.api.hap.uuid.generate(message.devId); const deviceAccessorie = this.deviceAccessories.get(uuid); + this.log.log("Update Device-------------------") if (deviceAccessorie) { deviceAccessorie.updateState(message); } diff --git a/lib/trv_accessory.js b/lib/trv_accessory.js new file mode 100644 index 00000000..cadbf33b --- /dev/null +++ b/lib/trv_accessory.js @@ -0,0 +1,196 @@ +const BaseAccessory = require('./base_accessory') + +let Accessory; +let Service; +let Characteristic; +let UUIDGen; + +class TRVAccessory extends BaseAccessory { + constructor(platform, homebridgeAccessory, deviceConfig) { + + ({ Accessory, Characteristic, Service } = platform.api.hap); + super( + platform, + homebridgeAccessory, + deviceConfig, + Accessory.Categories.THERMOSTAT, + Service.Thermostat + ); + this.statusArr = deviceConfig.status; + this.functionArr = deviceConfig.functions ? deviceConfig.functions : []; + + + this.refreshAccessoryServiceIfNeed(this.statusArr, false); + } + + //init Or refresh AccessoryService + refreshAccessoryServiceIfNeed(statusArr, isRefresh) { + this.isRefresh = isRefresh; + + for (var statusMap of statusArr) { + this.log.error('[TUYA - TRV] '+statusMap.code+' '+statusMap.value) + if (statusMap.code === 'temp_current' || statusMap.code === 'temp_current_f') { + this.temperatureMap = statusMap + this.normalAsync(Characteristic.CurrentTemperature, this.temperatureMap.value/10, { + minValue: 2, + maxValue: 28, + minStep: 1 + }) + + const hbUnits = this.tuyaParamToHomeBridge(Characteristic.TemperatureDisplayUnits, this.temperatureMap); + this.normalAsync(Characteristic.TemperatureDisplayUnits, hbUnits, { + minValue: hbUnits, + maxValue: hbUnits, + validValues: [hbUnits] + }) + } + else if (statusMap.code === 'temp_set' || statusMap.code === 'temp_set_f') { + this.temperatureMap = statusMap + this.normalAsync(Characteristic.TargetTemperature, this.temperatureMap.value/10, { + minValue: 2, + maxValue: 28, + minStep: 1 + }) + } + else if (statusMap.code === 'mode' ) { + this.modeMap = statusMap + if (this.modeMap.value == "auto" || this.modeMap.value == "temp_auto") + { + this.normalAsync(Characteristic.TargetHeatingCoolingState, 3) + } + else if (this.modeMap.value == "manual" ) { + this.normalAsync(Characteristic.TargetHeatingCoolingState, 1) + } + else if (this.modeMap.value == "holiday" ) { + this.normalAsync(Characteristic.TargetHeatingCoolingState, 0) + } + else { + this.normalAsync(Characteristic.TargetHeatingCoolingState, 3) + } + } + } + } + + + normalAsync(name, hbValue, props) { + this.setCachedState(name, hbValue); + if (this.isRefresh) { + this.service + .getCharacteristic(name) + .updateValue(hbValue); + } else { + this.getAccessoryCharacteristic(name, props); + } + } + + getAccessoryCharacteristic(name, props) { + //set Accessory service Characteristic + // this.log.log("[TUYA - TRV] - "+name,props) + this.service.getCharacteristic(name) + .setProps(props || {}) + .on('get', callback => { + if (this.hasValidCache()) { + callback(null, this.getCachedState(name)); + } + }) + .on('set', (value, callback) => { + if (name == Characteristic.TemperatureDisplayUnits) { + callback(); + return; + } + var param = this.getSendParam(name, value) + this.platform.tuyaOpenApi.sendCommand(this.deviceId, param).then(() => { + this.setCachedState(name, value); + + callback(); + }).catch((error) => { + this.log.error('[SET][%s] Characteristic.Brightness Error: %s', this.homebridgeAccessory.displayName, error); + this.invalidateCache(); + callback(error); + }); + }); + + } + + //get Command SendData + getSendParam(name, value) { + var code; + var value; + this.log.error('[TUYA - TRV] - getSendParam'+name+' \n\t value:'+value) + switch (name) { + case Characteristic.TargetHeatingCoolingState: + if ( value > 1 ) { + value = "auto" + this.setCachedState(Characteristic.TargetTemperature, 8); + this.service.getCharacteristic(Characteristic.TargetTemperature).updateValue(8); + + } + else if ( value == 1 ) { + value = "manual" + } + else { + value = "holiday" + } + code = "mode"; + return { + "commands": [ + { + "code": code, + "value": value + } + ] + }; + case Characteristic.TargetTemperature : + const tempset = value*10; + code = "temp_set"; + value = tempset; + + + this.setCachedState(Characteristic.TargetHeatingCoolingState, 1); + this.service.getCharacteristic(Characteristic.TargetHeatingCoolingState).updateValue(1); + + + return { + "commands": [ + { + "code": code, + "value": value + }, + { + "code": "mode", + "value": "manual" + } + ] + }; + default: + this.log.error("[TUYA - TRV] - Unknown Charcteristic",name) + return { + "commands": [] + } + } + + } + + + tuyaParamToHomeBridge(name, param) { + switch (name) { + case Characteristic.TemperatureDisplayUnits: + let units + if (param.code === 'temp_current') { + units = 0 + } else { + units = 1 + } + return units + } + } + + + //update device status + updateState(data) { + this.log.error('[TUYA - TRV] - Refreshing') + this.refreshAccessoryServiceIfNeed(data.status, true); + } +} + +module.exports = TRVAccessory; From 382089e9a6c9a41e4b78217857a58e10464d5e84 Mon Sep 17 00:00:00 2001 From: chria Date: Sat, 15 Jan 2022 17:10:36 +0000 Subject: [PATCH 2/2] trv working --- index.js | 15 ++- lib/base_accessory.js | 16 ++- lib/thermostat_accessory.js | 240 ++++++++++++++++++++++++++++++++++++ lib/trv_accessory.js | 23 ++-- lib/tuyamqttapi.js | 2 +- 5 files changed, 273 insertions(+), 23 deletions(-) create mode 100644 lib/thermostat_accessory.js diff --git a/index.js b/index.js index dbd1691c..2c16aca1 100644 --- a/index.js +++ b/index.js @@ -7,9 +7,9 @@ const SwitchAccessory = require('./lib/switch_accessory'); const SmokeSensorAccessory = require('./lib/smokesensor_accessory'); const Fanv2Accessory = require('./lib/fanv2_accessory'); const HeaterAccessory = require('./lib/heater_accessory'); +const ThermostatAccessory = require('./lib/thermostat_accessory'); const GarageDoorAccessory = require('./lib/garagedoor_accessory'); const AirPurifierAccessory = require('./lib/air_purifier_accessory') -const TRVAccessory = require('./lib/trv_accessory') const WindowCoveringAccessory = require('./lib/window_covering_accessory') const ContactSensorAccessory = require('./lib/contactsensor_accessory'); const LeakSensorAccessory = require('./lib/leak_sensor_accessory') @@ -163,6 +163,12 @@ class TuyaPlatform { this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); this.deviceAccessories.set(uuid, deviceAccessory); break; + case 'wk': + case 'wkf': + deviceAccessory = new ThermostatAccessory(this, homebridgeAccessory, device); + this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); + this.deviceAccessories.set(uuid, deviceAccessory); + break; case 'ckmkzq': //garage_door_opener deviceAccessory = new GarageDoorAccessory(this, homebridgeAccessory, device); this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); @@ -184,13 +190,7 @@ class TuyaPlatform { this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); this.deviceAccessories.set(uuid, deviceAccessory); break; - case 'wk': - deviceAccessory = new TRVAccessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - this.deviceAccessories.set(uuid, deviceAccessory); - break; default: - this.log.log(deviceType+" was not a recognised deviceType") break; } @@ -218,7 +218,6 @@ class TuyaPlatform { async refreshDeviceStates(message) { const uuid = this.api.hap.uuid.generate(message.devId); const deviceAccessorie = this.deviceAccessories.get(uuid); - this.log.log("Update Device-------------------") if (deviceAccessorie) { deviceAccessorie.updateState(message); } diff --git a/lib/base_accessory.js b/lib/base_accessory.js index d5dbd274..802e4fa7 100644 --- a/lib/base_accessory.js +++ b/lib/base_accessory.js @@ -6,7 +6,7 @@ let UUIDGen; //Base class of Accessory class BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig, categoryType, serviceType, subServices = []) { + constructor(platform, homebridgeAccessory, deviceConfig, categoryType, serviceType, subServices = [], hasBatteryService = false) { this.platform = platform; this.deviceId = deviceConfig.id; this.categoryType = categoryType; @@ -51,7 +51,7 @@ class BaseAccessory { } // Service - if (this.subServices.length == 0 || this.subServices.length == 1) { + if (this.subServices.length == 0) { // Service this.service = this.homebridgeAccessory.getService(this.serviceType); if (this.service) { @@ -74,6 +74,16 @@ class BaseAccessory { } } + if (hasBatteryService) { + this.battery = this.homebridgeAccessory.getService(Service.Battery); + if (this.battery) { + this.battery.setCharacteristic(Characteristic.Name, this.deviceConfig.name + ' Battery'); + } + else { + this.battery = this.homebridgeAccessory.addService(Service.Battery, this.deviceConfig.name + ' Battery'); + } + } + this.homebridgeAccessory.on('identify', (paired, callback) => { callback(); }); @@ -118,4 +128,4 @@ class BaseAccessory { } } -module.exports = BaseAccessory; \ No newline at end of file +module.exports = BaseAccessory; diff --git a/lib/thermostat_accessory.js b/lib/thermostat_accessory.js new file mode 100644 index 00000000..5e37c390 --- /dev/null +++ b/lib/thermostat_accessory.js @@ -0,0 +1,240 @@ +const BaseAccessory = require('./base_accessory') + +let Accessory; +let Service; +let Characteristic; + +class ThermostatAccessory extends BaseAccessory { + constructor(platform, homebridgeAccessory, deviceConfig) { + + ({ Accessory, Characteristic, Service } = platform.api.hap); + super( + platform, + homebridgeAccessory, + deviceConfig, + Accessory.Categories.THERMOSTAT, + Service.Thermostat, + [], + false + ); + this.statusArr = deviceConfig.status; + this.functionArr = deviceConfig.functions ? deviceConfig.functions : []; + this.temp_multiplier = this.getTempMultiplier(this.statusArr); + this.temp_set_range = this.getTempSetDPRange(this.statusArr); + this.refreshAccessoryServiceIfNeed(this.statusArr, false); + } + + refreshAccessoryServiceIfNeed(statusArr, isRefresh) { + this.isRefresh = isRefresh; + for (var statusMap of statusArr) { + if (statusMap.code === 'temp_current' || statusMap.code === 'temp_current_f') { + this.temperatureMap = statusMap; + this.normalAsync(Characteristic.CurrentTemperature, this.parseTuyaTemp(this.temperatureMap.value), { + minValue: -20, + maxValue: statusMap.code == 'temp_current' ? 50 : 122, + minStep: 0.1 + }) + + const hbUnits = this.tuyaParamToHomeBridge(Characteristic.TemperatureDisplayUnits, this.temperatureMap); + this.normalAsync(Characteristic.TemperatureDisplayUnits, hbUnits, { + minValue: hbUnits, + maxValue: hbUnits, + validValues: [hbUnits] + }) + } + if (statusMap.code === 'temp_set' || statusMap.code === 'temp_set_f') { + this.tempsetMap = statusMap + this.normalAsync(Characteristic.TargetTemperature, this.parseTuyaTemp(this.tempsetMap.value), { + minValue: this.temp_set_range.min, + maxValue: this.temp_set_range.max, + minStep: 0.5 + }) + } + if (statusMap.code === 'mode') { + this.heaterMode = statusMap; + const currentMode = this.parseTuyaMode(this.heaterMode.value); + this.normalAsync(Characteristic.CurrentHeatingCoolingState, currentMode > 2 ? 2 : currentMode, { + validValues:[0, 1, 2] + }) + this.normalAsync(Characteristic.TargetHeatingCoolingState, currentMode, { + validValues:[0, 1, 2, 3] + }) + } + } + } + + normalAsync(name, hbValue, props) { + this.setCachedState(name, hbValue); + if (this.isRefresh) { + this.service + .getCharacteristic(name) + .updateValue(hbValue); + } else { + this.getAccessoryCharacteristic(name, props); + } + } + + normalAsyncBattery(name, hbValue, props) { + this.setCachedState(name, hbValue); + if (this.isRefresh) { + this.battery + .getCharacteristic(name) + .updateValue(hbValue); + } else { + this.battery.getCharacteristic(name) + .setProps(props || {}) + .on('get', callback => { + if (this.hasValidCache()) { + callback(null, this.getCachedState(name)); + } + }); + } + } + + getAccessoryCharacteristic(name, props) { + this.service.getCharacteristic(name) + .setProps(props || {}) + .on('get', callback => { + if (this.hasValidCache()) { + callback(null, this.getCachedState(name)); + } + }) + .on('set', (value, callback) => { + var param = this.getSendParam(name, value) + if(!param) { + callback(); + return; + } + this.platform.tuyaOpenApi.sendCommand(this.deviceId, param).then(() => { + this.setCachedState(name, value); + if(name == Characteristic.TargetHeatingCoolingState) { + this.setCachedState(Characteristic.CurrentHeatingCoolingState, value > 2 ? 2 : value); + } + callback(); + }).catch((error) => { + this.log.error('[SET][%s] Characteristic.TargetTemperature Error: %s', this.homebridgeAccessory.displayName, error); + this.invalidateCache(); + callback(error); + }); + }); + } + + getSendParam(name, value) { + var code; + var value; + switch (name) { + case Characteristic.TargetTemperature: + const tempset = value; + code = this.tempsetMap.code; + value = this.convertToTuya(tempset); + break; + case Characteristic.TargetHeatingCoolingState: + const modeset = value; + code = this.heaterMode.code; + value = this.convertToTuyaMode(modeset); + break; + default: + return undefined; + } + return { + "commands": [ + { + "code": code, + "value": value + } + ] + }; + } + + tuyaParamToHomeBridge(name, param) { + switch (name) { + case Characteristic.TemperatureDisplayUnits: + let units + if (param.code === 'temp_current') { + units = 0 + } else { + units = 1 + } + return units + case Characteristic.StatusLowBattery: + let value + if (param >= 20) { + value = Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + } else { + value = Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + } + return value + } + } + + getTempSetDPRange(statusAttr) { + let tempSetRange + if (this.functionArr.length > 0) { + for (const funcDic of this.functionArr) { + let valueRange = JSON.parse(funcDic.values) + let isnull = (JSON.stringify(valueRange) == "{}") + switch (funcDic.code) { + case 'temp_set': + tempSetRange = isnull + ? { 'min': 0, 'max': 50 } + : { 'min': this.parseTuyaTemp(parseInt(valueRange.min)), 'max': this.parseTuyaTemp(parseInt(valueRange.max)) } + break; + case 'temp_set_f': + tempSetRange = isnull + ? { 'min': 32, 'max': 104 } + : { 'min': this.parseTuyaTemp(parseInt(valueRange.min)), 'max': this.parseTuyaTemp(parseInt(valueRange.max)) } + break; + default: + break; + } + } + } + // fallback from status map + if (!tempSetRange) { + return statusAttr.some((e) => e.code == 'temp_current_f') + ? { 'min': 32, 'max': 104 } + : {'min': 0, 'max': 50 }; + } + return tempSetRange + } + + convertToTuya(homebridgeTemp) { + return homebridgeTemp * this.temp_multiplier; + } + + parseTuyaTemp(tuyaTemp) { + return tuyaTemp / this.temp_multiplier; + } + + convertToTuyaMode(homebridgeMode) { + if(homebridgeMode === 1) { // heat + return 'manual'; + } + if(homebridgeMode === 2) { // cool + return 'holiday'; + } + return 'auto'; + } + + parseTuyaMode(tuyaMode) { + if(tuyaMode === 'manual') { + return 1; // heat + } + if(tuyaMode === 'holiday' || tuyaMode == 'holidayready') { + return 2; // cool + } + return 3; // auto + } + + getTempMultiplier(statusAttr) { + var temp = statusAttr.find((e) => e.code == 'temp_current'); + var tempf = statusAttr.find((e) => e.code == 'temp_current_f'); + return temp.value > 100 || tempf.value > 200 ? 10 : 1; + } + + updateState(data) { + this.refreshAccessoryServiceIfNeed(data.status, true); + } +} + +module.exports = ThermostatAccessory; diff --git a/lib/trv_accessory.js b/lib/trv_accessory.js index cadbf33b..b389ab1c 100644 --- a/lib/trv_accessory.js +++ b/lib/trv_accessory.js @@ -3,7 +3,7 @@ const BaseAccessory = require('./base_accessory') let Accessory; let Service; let Characteristic; -let UUIDGen; + class TRVAccessory extends BaseAccessory { constructor(platform, homebridgeAccessory, deviceConfig) { @@ -14,12 +14,11 @@ class TRVAccessory extends BaseAccessory { homebridgeAccessory, deviceConfig, Accessory.Categories.THERMOSTAT, - Service.Thermostat + Service.Thermostat, + [Service.TemperatureSensor] ); this.statusArr = deviceConfig.status; this.functionArr = deviceConfig.functions ? deviceConfig.functions : []; - - this.refreshAccessoryServiceIfNeed(this.statusArr, false); } @@ -36,13 +35,18 @@ class TRVAccessory extends BaseAccessory { maxValue: 28, minStep: 1 }) - + this.normalAsync(Characteristic.HeatingThresholdTemperature, this.temperatureMap.value/10, { + minValue: 2, + maxValue: 28, + minStep: 1 + }) const hbUnits = this.tuyaParamToHomeBridge(Characteristic.TemperatureDisplayUnits, this.temperatureMap); this.normalAsync(Characteristic.TemperatureDisplayUnits, hbUnits, { minValue: hbUnits, maxValue: hbUnits, validValues: [hbUnits] }) + this,subServices } else if (statusMap.code === 'temp_set' || statusMap.code === 'temp_set_f') { this.temperatureMap = statusMap @@ -56,16 +60,13 @@ class TRVAccessory extends BaseAccessory { this.modeMap = statusMap if (this.modeMap.value == "auto" || this.modeMap.value == "temp_auto") { - this.normalAsync(Characteristic.TargetHeatingCoolingState, 3) + this.normalAsync(Characteristic.TargetHeatingCoolingState, 3,{validValues: [Characteristic.TargetHeatingCoolingState.HEAT,3]}) } else if (this.modeMap.value == "manual" ) { - this.normalAsync(Characteristic.TargetHeatingCoolingState, 1) - } - else if (this.modeMap.value == "holiday" ) { - this.normalAsync(Characteristic.TargetHeatingCoolingState, 0) + this.normalAsync(Characteristic.TargetHeatingCoolingState, 1,{validValues: [Characteristic.TargetHeatingCoolingState.HEAT,3]}) } else { - this.normalAsync(Characteristic.TargetHeatingCoolingState, 3) + this.normalAsync(Characteristic.TargetHeatingCoolingState, 3,{validValues: [Characteristic.TargetHeatingCoolingState.HEAT,3]}) } } } diff --git a/lib/tuyamqttapi.js b/lib/tuyamqttapi.js index a2d94b1f..7cde6064 100644 --- a/lib/tuyamqttapi.js +++ b/lib/tuyamqttapi.js @@ -127,7 +127,7 @@ class TuyaOpenMQ { const buf = Buffer.allocUnsafe(6); buf.writeUIntBE(t, 0, 6); cipher.setAAD(buf); - + var msg = cipher.update(data_buffer); return msg.toString('utf8'); }