homebridge-th16ermostat
Version:
Use Sonoff TH16 device as a simple thermostat.
275 lines • 13.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const axios_1 = __importDefault(require("axios"));
let hap;
class TH16ermostatPlugin {
// ctor
constructor(log, config) {
// state
this.currTemp = '';
this.targetTemp = 0;
this.currRelativeHumidity = '';
this.currentHeatingState = hap.Characteristic.CurrentHeatingCoolingState.OFF; // [0, 1] only
this.targetHeatingState = hap.Characteristic.TargetHeatingCoolingState.OFF; // [0, 1, 3] only
this.isOffline = false;
this.enableHumidity = false;
this.minTemp = -25;
this.maxTemp = 25;
this.deltaTemp = 0.2;
this.stepTemp = 0.5;
this.tempUnits = 'C';
this.deviceIPAddress = '';
this.deviceStatStatus = '/cm?cmnd=status%208';
this.deviceStatPower = '/cm?cmnd=power';
this.deviceCmndOn = '/cm?cmnd=power%20on';
this.deviceCmndOff = '/cm?cmnd=power%20off';
this.pollingInterval = 60;
log.debug('TH16ermostat constructing!');
this.log = log;
this.name = config.name;
// Config values
this.sensorName = config.sensorName;
this.deviceIPAddress = config.deviceIPAddress;
this.enableHumidity = config.enableHumidity || this.enableHumidity;
this.deviceStatStatus = config.deviceStatStatus || this.deviceStatStatus;
this.deviceStatPower = config.deviceStatPower || this.deviceStatPower;
this.deviceCmndOn = config.deviceCmndOn || this.deviceCmndOn;
this.deviceCmndOff = config.deviceCmndOff || this.deviceCmndOff;
this.pollingInterval = config.pollingInterval || this.pollingInterval;
this.minTemp = config.minTemp || this.minTemp;
this.maxTemp = config.maxTemp || this.maxTemp;
this.deltaTemp = config.deltaTemp || this.deltaTemp;
this.stepTemp = config.StepTemp || this.stepTemp;
this.tempUnits = config.tempUnits || this.tempUnits;
// Fix config values if necessary
//
if (this.deltaTemp < 0.1) {
// prevent zero or negavite (minimum of 0.1)
this.deltaTemp = 0.1;
}
// init state values
this.targetTemp = this.minTemp;
// create services
this.thermostatService = new hap.Service.Thermostat(this.name);
this.informationService = new hap.Service.AccessoryInformation();
this.humidityService = new hap.Service.HumiditySensor(this.name);
this.servicesArray = [];
}
/*
* This method is optional to implement. It is called when HomeKit ask to identify the accessory.
* Typical this only ever happens at the pairing process.
*/
identify() {
this.log('Sonoff TH16 thermostat');
}
/*
* This method is called directly after creation of this instance.
* It should return all services which should be added to the accessory.
*/
getServices() {
this.log.debug('TH16ermostat initializing!');
// init Humidity Sensor service
if (this.enableHumidity) {
this.humidityService.getCharacteristic(hap.Characteristic.CurrentRelativeHumidity)
.on("get" /* GET */, (callback) => {
this.log.info('Get CURRENT relative humidity: ' + this.currRelativeHumidity);
callback(undefined, this.currRelativeHumidity);
});
}
// init Thermostat service
this.thermostatService.getCharacteristic(hap.Characteristic.CurrentHeatingCoolingState)
.on("get" /* GET */, (callback) => {
this.log.info('Get CURRENT heating state: ' + this.heatingStateToStr(this.currentHeatingState));
callback(undefined, this.isOffline ? hap.Characteristic.CurrentHeatingCoolingState.OFF : this.currentHeatingState);
})
.on("set" /* SET */, (value, callback) => {
this.currentHeatingState = value;
this.log.info('Set CURRENT heating state to: ' + this.heatingStateToStr(this.currentHeatingState));
this.pollDeviceStatus();
callback();
});
this.thermostatService.getCharacteristic(hap.Characteristic.TargetHeatingCoolingState)
.on("get" /* GET */, (callback) => {
this.log.info('Get TARGET heating state: ' + this.heatingStateToStr(this.targetHeatingState));
callback(undefined, this.isOffline ? hap.Characteristic.TargetHeatingCoolingState.OFF : this.targetHeatingState);
})
.on("set" /* SET */, (value, callback) => {
this.targetHeatingState = value;
this.log.info('Set TARGET heating state to: ' + this.heatingStateToStr(this.targetHeatingState));
if (hap.Characteristic.TargetHeatingCoolingState.COOL === value) {
this.log.info('COOL state not supported! Setting as OFF');
this.targetHeatingState = hap.Characteristic.TargetHeatingCoolingState.OFF;
}
this.pollDeviceStatus();
callback();
})
.setProps({
validValues: [0, 1, 3],
});
this.thermostatService.getCharacteristic(hap.Characteristic.CurrentTemperature)
.on("get" /* GET */, (callback) => {
this.log.info('Get CURRENT temperature: ' + this.currTemp);
callback(undefined, this.currTemp);
});
this.thermostatService.getCharacteristic(hap.Characteristic.TargetTemperature)
.on("get" /* GET */, (callback) => {
this.log.info('Get TARGET temperature: ' + this.targetTemp);
callback(undefined, this.targetTemp);
})
.on("set" /* SET */, (value, callback) => {
this.targetTemp = value;
this.log.info('Set TARGET temperature to: ' + this.targetTemp);
this.pollDeviceStatus();
callback();
})
.setProps({
minValue: this.minTemp,
maxValue: this.maxTemp,
minStep: this.stepTemp,
});
this.thermostatService.getCharacteristic(hap.Characteristic.TemperatureDisplayUnits)
.on("get" /* GET */, (callback) => {
callback(undefined, this.tempUnits);
})
.on("set" /* SET */, (value, callback) => {
this.log.info('[Ignoring] Set Temperature Units to: ' + this.tempUnits);
callback();
});
// init Information service
this.informationService
.setCharacteristic(hap.Characteristic.Manufacturer, 'Sonoff')
.setCharacteristic(hap.Characteristic.Model, 'TH16');
this.log.debug('TH16ermostat finished initializing!');
// Polling service
this.log.debug('Polling each ' + this.pollingInterval + ' seconds.');
this.pollingTimer = setInterval(this.pollDeviceStatus.bind(this), this.pollingInterval * 1000);
// Get initial state
this.pollDeviceStatus();
this.servicesArray = [
this.informationService,
this.thermostatService,
];
if (this.enableHumidity) {
this.servicesArray.push(this.humidityService);
}
return this.servicesArray;
}
setDevicePower(value) {
this.log.info('TH16ermostat: Power ' + this.heatingStateToStr(value) +
'(Temp: ' + this.currTemp + ' -> ' + this.targetTemp + ')');
const url = 'http://' + this.deviceIPAddress;
axios_1.default.get(url + (value === hap.Characteristic.CurrentHeatingCoolingState.HEAT ? this.deviceCmndOn : this.deviceCmndOff))
.then((response) => {
this.currentHeatingState =
(response.data.POWER === 'ON') ?
hap.Characteristic.CurrentHeatingCoolingState.HEAT :
hap.Characteristic.CurrentHeatingCoolingState.OFF;
}).catch((err) => {
this.log.error('Failed to set relay state: cmd=' + this.deviceStatStatus + ' [' + err + ']');
});
}
pollDeviceStatus() {
const deviceStatus = async () => {
let pwr, tmp, hum;
const url = 'http://' + this.deviceIPAddress;
await axios_1.default.get(url + this.deviceStatStatus, { timeout: 3000 })
.then((response) => {
tmp = parseFloat(response.data.StatusSNS[this.sensorName].Temperature);
}).catch((err) => {
throw new Error('Failed to get status: cmd=' + this.deviceStatStatus + ' [' + err + ']');
});
await axios_1.default.get(url + this.deviceStatStatus, { timeout: 3000 })
.then((response) => {
hum = parseFloat(response.data.StatusSNS[this.sensorName].Humidity);
}).catch((err) => {
throw new Error('Failed to get status: cmd=' + this.deviceStatStatus + ' [' + err + ']');
});
await axios_1.default.get(url + this.deviceStatPower, { timeout: 3000 })
.then((response) => {
pwr = (response.data.POWER === 'ON') ?
hap.Characteristic.CurrentHeatingCoolingState.HEAT :
hap.Characteristic.CurrentHeatingCoolingState.OFF;
}).catch((err) => {
throw new Error('Failed to get power status: cmd=' + this.deviceStatPower + ' [' + err + ']');
});
return { 'TMP_STAT': (await tmp), 'HUM_STAT': (await hum), 'PWR_STAT': (await pwr) };
};
deviceStatus()
.then((response) => {
// device online
this.isOffline = false;
// state values
this.currTemp = response['TMP_STAT'];
this.currRelativeHumidity = response['HUM_STAT'];
this.currentHeatingState = response['PWR_STAT'];
this.thermostatService.setCharacteristic(hap.Characteristic.CurrentTemperature, this.currTemp);
if (this.enableHumidity) {
this.humidityService.setCharacteristic(hap.Characteristic.CurrentRelativeHumidity, this.currRelativeHumidity);
}
// init target state from current state
let targetRelayOn = (this.currentHeatingState === hap.Characteristic.CurrentHeatingCoolingState.HEAT);
switch (this.targetHeatingState) {
case hap.Characteristic.TargetHeatingCoolingState.AUTO:
{
// AUTO mode: Compare temperatures
if (parseFloat(this.currTemp) >= (this.targetTemp + this.deltaTemp)) {
targetRelayOn = false;
}
else if (parseFloat(this.currTemp) <= (this.targetTemp - this.deltaTemp)) {
targetRelayOn = true;
}
}
break;
case hap.Characteristic.CurrentHeatingCoolingState.HEAT:
targetRelayOn = true;
break;
case hap.Characteristic.CurrentHeatingCoolingState.OFF:
targetRelayOn = false;
break;
}
// Change status if needed (this.currentHeatingState as boolean here)
if (targetRelayOn && !this.currentHeatingState) {
this.setDevicePower(hap.Characteristic.CurrentHeatingCoolingState.HEAT);
}
else if (!targetRelayOn && this.currentHeatingState) {
this.setDevicePower(hap.Characteristic.CurrentHeatingCoolingState.OFF);
}
})
.catch((err) => {
this.currTemp = '--';
this.thermostatService.setCharacteristic(hap.Characteristic.CurrentTemperature, this.currTemp);
this.currRelativeHumidity = '--';
if (this.enableHumidity) {
this.humidityService.setCharacteristic(hap.Characteristic.CurrentRelativeHumidity, this.currRelativeHumidity);
}
// output error only once, do not spam the log on each poll
if (!this.isOffline) {
this.log.debug('Device offline? ' + err);
this.isOffline = true;
}
});
}
// helpers
//
heatingStateToStr(state) {
// assuming we have no 'COOL'!
// assiming current and target to have the first 2 values identical!
switch (state) {
case hap.Characteristic.TargetHeatingCoolingState.OFF:
return 'OFF';
case hap.Characteristic.TargetHeatingCoolingState.HEAT:
return 'HEAT';
case hap.Characteristic.TargetHeatingCoolingState.AUTO:
return 'AUTO';
default:
return 'UNKNOWN';
}
}
}
module.exports = (api) => {
hap = api.hap;
api.registerAccessory('homebridge-th16ermostat', 'TH16ermostat', TH16ermostatPlugin);
};
//# sourceMappingURL=index.js.map