Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding thermostat feature #358

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
83 changes: 73 additions & 10 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@
"SimpleHeater"
]
},
{
"title": "Simple Thermostat",
"enum": [
"SimpleThermostat"
]
},
{
"title": "Garage Door",
"enum": [
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -45,6 +46,7 @@ const CLASS_DEF = {
simpleblinds: SimpleBlindsAccessory,
simpleblinds2: SimpleBlinds2Accessory,
simpleheater: SimpleHeaterAccessory,
simplethermostat: SimpleThermostatAccessory,
switch: SwitchAccessory,
fan: SimpleFanAccessory,
fanlight: SimpleFanLightAccessory,
Expand Down
170 changes: 170 additions & 0 deletions lib/SimpleThermostatAccessory.js
Original file line number Diff line number Diff line change
@@ -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;