diff --git a/config.schema.json b/config.schema.json index ee892f68..85f405cf 100644 --- a/config.schema.json +++ b/config.schema.json @@ -84,6 +84,12 @@ "SimpleHeater" ] }, + { + "title": "Simple Thermostat", + "enum": [ + "SimpleThermostat" + ] + }, { "title": "Garage Door", "enum": [ @@ -321,42 +327,99 @@ "type": "integer", "placeholder": "15", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['AirConditioner','Convector', 'SimpleHeater'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['AirConditioner','Convector', 'SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" } }, "maxTemperature": { "type": "integer", "placeholder": "40", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['AirConditioner','Convector', 'SimpleHeater'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['AirConditioner','Convector', 'SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" } }, "minTemperatureSteps": { "type": "integer", "placeholder": "1", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['AirConditioner'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['AirConditioner', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" } }, "dpActive": { "type": "integer", - "placeholder": "7", + "placeholder": "118", + "description": "DP that shows whether the thermostat is requesting heating or idle.", + "condition": { + "functionBody": "return model.devices && model.devices[arrayIndices] && ['Convector', 'SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" + } + }, + "dpActiveNotBoolean": { + "type": "boolean", + "placeholder": false, + "description": "Check this if dpActive uses a string value to determine the operating mode (heating/idle) instead of a boolean.", + "condition": { + "functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleThermostat'].includes(model.devices[arrayIndices].type);" + } + }, + "heatingIndicatorOff": { + "type": "string", + "placeholder": "warming", + "description": "Value of dpActive that shows its idling", + "condition": { + "functionBody": "return model.devices && model.devices[arrayIndices].dpActiveNotBoolean === true && model.devices[arrayIndices] && ['SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" + } + }, + "heatingIndicatorOn": { + "type": "string", + "placeholder": "heating", + "description": "Value of dpActive that shows its requesting heat", + "condition": { + "functionBody": "return model.devices && model.devices[arrayIndices].dpActiveNotBoolean === true && model.devices[arrayIndices] && ['SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" + } + }, + "dpProgramState": { + "type": "integer", + "placeholder": "103", + "description": "DP that shows if the thermostat is off or on. It can also be based on the currently active scheduled program. E.g. if the thermostat is in FROST or HOLIDAY (no heating according to schedule) it is Off, whereas if the program is AUTO, is it 'On').", + "condition": { + "functionBody": "return model.devices && model.devices[arrayIndices] && ['Convector', 'SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" + } + }, + "dpProgramStateNotBoolean": { + "type": "boolean", + "placeholder": false, + "description": "Check this if dpProgramState uses a string value to determine whether the thermostat is off or on (FROST/AUTO/MANUAL) instead of a boolean.", + "condition": { + "functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleThermostat'].includes(model.devices[arrayIndices].type);" + } + }, + "scheduledHeatingOff": { + "type": "string", + "placeholder": "FROST", + "description": "Value of dpProgramState that shows that the schedule has been turned off", + "condition": { + "functionBody": "return model.devices && model.devices[arrayIndices].dpProgramStateNotBoolean === true && model.devices[arrayIndices] && ['SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" + } + }, + "scheduledHeatingOn": { + "type": "string", + "placeholder": "AUTO", + "description": "Value of dpProgramState that shows that the schedule has been turned on", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['Convector', 'SimpleHeater'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices].dpProgramStateNotBoolean === true && model.devices[arrayIndices] && ['SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" } }, "dpDesiredTemperature": { "type": "integer", "placeholder": "2", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['Convector', 'SimpleHeater'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['Convector', 'SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" } }, "dpCurrentTemperature": { "type": "integer", "placeholder": "3", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['Convector', 'SimpleHeater'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['Convector', 'SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" } }, "dpRotationSpeed": { @@ -410,21 +473,21 @@ "type": "integer", "placeholder": "1", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleHeater'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" } }, "thresholdTemperatureDivisor": { "type": "integer", "placeholder": "1", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleHeater'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" } }, "targetTemperatureDivisor": { "type": "integer", "placeholder": "1", "condition": { - "functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleHeater'].includes(model.devices[arrayIndices].type);" + "functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleHeater', 'SimpleThermostat'].includes(model.devices[arrayIndices].type);" } }, "dpAction": { diff --git a/index.js b/index.js index e1892a55..df0a0d52 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,7 @@ const SimpleDimmer2Accessory = require('./lib/SimpleDimmer2Accessory'); const SimpleBlindsAccessory = require('./lib/SimpleBlindsAccessory'); const SimpleBlinds2Accessory = require('./lib/SimpleBlinds2Accessory'); const SimpleHeaterAccessory = require('./lib/SimpleHeaterAccessory'); +const SimpleThermostatAccessory = require('./lib/SimpleThermostatAccessory'); const SimpleFanAccessory = require('./lib/SimpleFanAccessory'); const SimpleFanLightAccessory = require('./lib/SimpleFanLightAccessory'); const SwitchAccessory = require('./lib/SwitchAccessory'); @@ -45,6 +46,7 @@ const CLASS_DEF = { simpleblinds: SimpleBlindsAccessory, simpleblinds2: SimpleBlinds2Accessory, simpleheater: SimpleHeaterAccessory, + simplethermostat: SimpleThermostatAccessory, switch: SwitchAccessory, fan: SimpleFanAccessory, fanlight: SimpleFanLightAccessory, diff --git a/lib/SimpleThermostatAccessory.js b/lib/SimpleThermostatAccessory.js new file mode 100644 index 00000000..d608a996 --- /dev/null +++ b/lib/SimpleThermostatAccessory.js @@ -0,0 +1,170 @@ +const BaseAccessory = require('./BaseAccessory'); + +class SimpleThermostatAccessory extends BaseAccessory { + static getCategory(Categories) { + return Categories.AIR_HEATER; + } + + constructor(...props) { + super(...props); + } + + _registerPlatformAccessory() { + const { Service } = this.hap; + + this.accessory.addService(Service.HeaterCooler, this.device.context.name); + + super._registerPlatformAccessory(); + } + + _registerCharacteristics(dps) { + const { Service, Characteristic } = this.hap; + const service = this.accessory.getService(Service.HeaterCooler); + this._checkServiceName(service, this.device.context.name); + + this.deviceName = this.device.context.name || 'Thermostat'; + this.dpActive = this._getCustomDP(this.device.context.dpActive) || '1'; + this.dpProgramState = this._getCustomDP(this.device.context.dpProgramState) || '1'; + this.dpDesiredTemperature = this._getCustomDP(this.device.context.dpDesiredTemperature) || '2'; + this.dpCurrentTemperature = this._getCustomDP(this.device.context.dpCurrentTemperature) || '3'; + this.temperatureDivisor = parseInt(this.device.context.temperatureDivisor) || 1; + this.thresholdTemperatureDivisor = parseInt(this.device.context.thresholdTemperatureDivisor) || 1; + this.targetTemperatureDivisor = parseInt(this.device.context.targetTemperatureDivisor) || 1; + this.heatingIndicatorOff = this.device.context.heatingIndicatorOff || 'warming'; + this.heatingIndicatorOn = this.device.context.heatingIndicatorOn || 'heating'; + this.scheduledHeatingOff = this.device.context.scheduledHeatingOff || 'FROST'; + this.scheduledHeatingOn = this.device.context.scheduledHeatingOn || 'AUTO'; + this.activeStateNotBool = this.device.context.dpActiveNotBoolean || false; + this.programStateNotBool = this.device.context.dpProgramStateNotBoolean || false; + + const characteristicActive = service.getCharacteristic(Characteristic.Active) + .updateValue(this._getActive(dps[this.dpProgramState])) + .on('get', this.getActive.bind(this)) + .on('set', this.setActive.bind(this)); + + const characteristicCurrentHeaterCoolerState = service.getCharacteristic(Characteristic.CurrentHeaterCoolerState) + .updateValue(this._getCurrentHeaterCoolerState(dps)) + .on('get', this.getCurrentHeaterCoolerState.bind(this)); + + service.getCharacteristic(Characteristic.TargetHeaterCoolerState) + .setProps({ + minValue: 1, + maxValue: 1, + validValues: [Characteristic.TargetHeaterCoolerState.HEAT] + }) + .updateValue(this._getTargetHeaterCoolerState()) + .on('get', this.getTargetHeaterCoolerState.bind(this)); + + const characteristicCurrentTemperature = service.getCharacteristic(Characteristic.CurrentTemperature) + .updateValue(this._getDividedState(dps[this.dpCurrentTemperature], this.temperatureDivisor)) + .on('get', this.getDividedState.bind(this, this.dpCurrentTemperature, this.temperatureDivisor)); + + + const characteristicHeatingThresholdTemperature = service.getCharacteristic(Characteristic.HeatingThresholdTemperature) + .setProps({ + minValue: this.device.context.minTemperature || 15, + maxValue: this.device.context.maxTemperature || 35, + minStep: this.device.context.minTemperatureSteps || 0.5 + }) + .updateValue(this._getDividedState(dps[this.dpDesiredTemperature], this.thresholdTemperatureDivisor)) + .on('get', this.getDividedState.bind(this, this.dpDesiredTemperature, this.thresholdTemperatureDivisor)) + .on('set', this.setTargetThresholdTemperature.bind(this)); + + this.characteristicHeatingThresholdTemperature = characteristicHeatingThresholdTemperature; + + this.device.on('change', (changes, state) => { + + if (changes.hasOwnProperty(this.dpProgramState)) { + const newActive = this._getActive(changes[this.dpProgramState]); + if (characteristicActive.value !== newActive) { + characteristicActive.updateValue(newActive); + } + } + + if (changes.hasOwnProperty(this.dpDesiredTemperature)) { + if (characteristicHeatingThresholdTemperature.value !== changes[this.dpDesiredTemperature]) + characteristicHeatingThresholdTemperature.updateValue(changes[this.dpDesiredTemperature] / this.targetTemperatureDivisor); + } + + if (changes.hasOwnProperty(this.dpActive)) { + const newCurrentHeaterCoolerState = this._getCurrentHeaterCoolerState(changes[this.dpActive]); + if (characteristicCurrentHeaterCoolerState.value !== newCurrentHeaterCoolerState) { + characteristicCurrentHeaterCoolerState.updateValue(newCurrentHeaterCoolerState); + } + } + + if (changes.hasOwnProperty(this.dpCurrentTemperature) && characteristicCurrentTemperature.value !== changes[this.dpCurrentTemperature]) characteristicCurrentTemperature.updateValue(this._getDividedState(changes[this.dpCurrentTemperature], this.temperatureDivisor)); + + console.log('[Tuya] ' + this.deviceName + ' changed: ' + JSON.stringify(state)); + }); + } + + getActive(callback) { + this.getState(this.dpProgramState, (err, dp) => { + if (err) return callback(err); + + callback(null, this._getActive(dp)); + }); + } + + _getActive(dp) { + const { Characteristic } = this.hap; + if (this.programStateNotBool == true) { + return dp == this.scheduledHeatingOff ? Characteristic.Active.INACTIVE : Characteristic.Active.ACTIVE; + } else { return dp ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE; } + } + + setActive(value, callback) { + const { Characteristic } = this.hap; + + switch (value) { + case Characteristic.Active.ACTIVE: + if (this.programStateNotBool == true) { return this.setState(this.dpProgramState, this.scheduledHeatingOn, callback); } + else { return this.setState(this.dpProgramState, true, callback); } + + case Characteristic.Active.INACTIVE: + if (this.programStateNotBool == true) { return this.setState(this.dpProgramState, this.scheduledHeatingOff, callback); } + else { return this.setState(this.dpProgramState, false, callback); } + } + + callback(); + } + + getCurrentHeaterCoolerState(callback) { + this.getState([this.dpActive], (err, dps) => { + if (err) return callback(err); + + callback(null, this._getCurrentHeaterCoolerState(dps)); + }); + } + + _getCurrentHeaterCoolerState(dps) { + const { Characteristic } = this.hap; + if (this.activeStateNotBool == true) { + return dps == this.heatingIndicatorOn ? Characteristic.CurrentHeaterCoolerState.HEATING : Characteristic.CurrentHeaterCoolerState.IDLE; + } else { return dps ? Characteristic.CurrentHeaterCoolerState.HEATING : Characteristic.CurrentHeaterCoolerState.IDLE; } + } + + getTargetHeaterCoolerState(callback) { + callback(null, this._getTargetHeaterCoolerState()); + } + + _getTargetHeaterCoolerState() { + const { Characteristic } = this.hap; + return Characteristic.TargetHeaterCoolerState.HEAT; + } + + setTargetThresholdTemperature(value, callback) { + this.setState(this.dpDesiredTemperature, value * this.thresholdTemperatureDivisor, err => { + if (err) return callback(err); + + if (this.characteristicHeatingThresholdTemperature) { + this.characteristicHeatingThresholdTemperature.updateValue(value); + } + + callback(); + }); + } +} + +module.exports = SimpleThermostatAccessory;