UNPKG

homebridge-tcc

Version:

Honeywell Total Connect Comfort support for Homebridge: https://github.com/nfarina/homebridge

668 lines (589 loc) 26.7 kB
/*jslint node: true */ 'use strict'; var debug = require('debug')('tcc'); const moment = require('moment'); var homebridgeLib = require('homebridge-lib'); const FirmwareRevision = require('../package.json').version var Accessory, Service, Characteristic, UUIDGen, FakeGatoHistoryService, CustomCharacteristics; var os = require("os"); var hostname = os.hostname(); var Tcc = require('./lib/tcc.js').tcc; var myAccessories = []; var thermostats; var outsideSensorsCreated = false; const PLUGIN_NAME = "homebridge-tcc"; const PLATFORM_NAME = "tcc"; module.exports = function (homebridge) { Accessory = homebridge.platformAccessory; Service = homebridge.hap.Service; Characteristic = homebridge.hap.Characteristic; UUIDGen = homebridge.hap.uuid; CustomCharacteristics = new homebridgeLib.EveHomeKitTypes(homebridge).Characteristics; FakeGatoHistoryService = require('fakegato-history')(homebridge); homebridge.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, tccPlatform); }; class tccPlatform { constructor(log, config, api) { this.api = api; this.username = config['username']; this.password = config['password']; this.refresh = config['refresh'] || 600; // Lower than 10 minutes triggers request rate limiter on Honeywell site. this.usePermanentHolds = config['usePermanentHolds'] || false; this.log = log; this.sensors = config['sensors']; this.storage = config['storage'] || "fs"; // Enable config based DEBUG logging enable this.debug = config['debug'] || false; if (this.debug) { debug.enabled = true; } api.on('didFinishLaunching', this.didFinishLaunching); } didFinishLaunching = () => { this.log("didFinishLaunching"); thermostats = new Tcc(this); thermostats.pollThermostat().then((devices) => { for (var zone in devices.hb) { debug("Creating accessory for", devices.hb[zone].Name + "(" + devices.hb[zone].ThermostatID + ")"); //debug("tccPlatform.prototype.didFinishLaunching()",this.devices) var thermostatAccessory = new TccAccessory(this, devices.hb[zone], this.sensors); updateStatus(thermostatAccessory, devices.hb[zone]); const createOutsideSensors = (this.sensors == "all" || this.sensors == "outside"); // does user want outside sensors created? if so, only create 1 set if (createOutsideSensors && !outsideSensorsCreated) { const outsideAccessory = new TccSensorsAccessory(this, devices.hb[zone], this.sensors); updateStatus(outsideAccessory, devices.hb[zone]); outsideSensorsCreated = true; } else if (!createOutsideSensors) { const outsideAccessory = getAccessoryByName("Outside Sensors"); if (outsideAccessory) { const outsideTempSensor = outsideAccessory.getService("Outside Temperature"); if (outsideTempSensor) { outsideAccessory.removeService(outsideTempSensor); } const outsideHumiditySensor = outsideAccessory.getService("Outside Humidity"); if (outsideHumiditySensor) { outsideAccessory.removeService(outsideHumiditySensor); } } } } }).catch((err) => { this.log("Critical Error - No devices created, please restart."); this.log.error(err); }); setInterval(pollDevices.bind(this), this.refresh * 1000); // Poll every minute } configureAccessory(accessory) { this.log("configureAccessory %s", accessory.displayName); if (accessory.getService(Service.Thermostat)) { accessory.log = this.log; accessory .getService(Service.Thermostat) .getCharacteristic(Characteristic.TargetHeatingCoolingState) .on('set', setTargetHeatingCooling.bind(accessory)); accessory .getService(Service.Thermostat) .getCharacteristic(Characteristic.CoolingThresholdTemperature) .on('set', setCoolingThresholdTemperature.bind(accessory)); // this.addOptionalCharacteristic(Characteristic.HeatingThresholdTemperature); accessory .getService(Service.Thermostat) .getCharacteristic(Characteristic.HeatingThresholdTemperature) .on('set', setHeatingThresholdTemperature.bind(accessory)); accessory .getService(Service.Thermostat) .getCharacteristic(Characteristic.TargetTemperature) .on('set', setTargetTemperature.bind(accessory)); // } debug("FakeGatoHistoryService", this.storage, this.refresh); accessory.context.logEventCounter = 9; // Update fakegato on startup accessory.loggingService = new FakeGatoHistoryService("thermo", accessory, { storage: this.storage, minutes: this.refresh * 10 / 60 }); // only attach this to the actual thermostat accessories, not the sensors accessory accessory.context.ChangeThermostat = new ChangeThermostat(accessory); debug("configureAccessory", accessory.context.ChangeThermostat); } // add fakegato logging for if (accessory.displayName == "Outside Sensors") { debug(accessory); debug("FakeGatoHistoryService", this.storage, this.refresh); accessory.context.logEventCounter = 9; // Update fakegato on startup accessory.loggingService = new FakeGatoHistoryService("weather", accessory, { storage: this.storage, minutes: this.refresh * 10 / 60 }); } myAccessories.push(accessory); //debug(accessory.context) } } function pollDevices() { thermostats.pollThermostat().then((devices) => { myAccessories.forEach(function (accessory) { debug("pollDevices - updateStatus", accessory.displayName); if (devices.hb[accessory.context.ThermostatID]) { updateStatus(accessory, devices.hb[accessory.context.ThermostatID]); } else { this.log("ERROR: no data for", accessory.displayName); // debug("accessory", accessory); if (accessory.getService(Service.Thermostat)) { accessory.getService(Service.Thermostat).getCharacteristic(Characteristic.TargetTemperature) .updateValue(new Error("Status missing for thermostat")); } } }.bind(this)); }).catch((err) => { if (err.message === 'Error: GetLocations InvalidSessionID') { // [Thermostat] ERROR: pollDevices Error: GetLocations InvalidSessionID // this.log("ERROR: pollDevices", err.message); } else if (err.message) { // [Thermostat] ERROR: pollDevices Error: GetLocations InvalidSessionID this.log("pollDevices", err.message); } else { this.log("ERROR: pollDevices", err); } myAccessories.forEach(function (accessory) { if (accessory.getService(Service.Thermostat)) { accessory.getService(Service.Thermostat).getCharacteristic(Characteristic.TargetTemperature) .updateValue(new Error("Status missing for thermostat")); } }); }); } function updateStatus(accessory, device) { accessory.getService(Service.AccessoryInformation).getCharacteristic(Characteristic.Name) .updateValue(device.Name); accessory.getService(Service.AccessoryInformation).getCharacteristic(Characteristic.Model) .updateValue(device.Model); // check if user wants separate temperature and humidity sensors if (accessory.getService(device.Name + " Temperature")) { //debug("updateStatus() " + device.Name + " InsideTemperature = true"); var InsideTemperature = accessory.getService(device.Name + " Temperature"); InsideTemperature.getCharacteristic(Characteristic.CurrentTemperature) .updateValue(device.CurrentTemperature); } if (accessory.getService("Outside Temperature")) { //debug("updateStatus() " + device.Name + " outsideTemperature = true"); var OutsideTemperature = accessory.getService("Outside Temperature"); OutsideTemperature.getCharacteristic(Characteristic.CurrentTemperature) .updateValue(device.OutsideTemperature); } if (accessory.getService(device.Name + " Humidity")) { //debug("updateStatus() " + device.Name + " insideHumidity = true"); var InsideHumidity = accessory.getService(device.Name + " Humidity"); InsideHumidity.getCharacteristic(Characteristic.CurrentRelativeHumidity) .updateValue(device.InsideHumidity); } if (accessory.getService("Outside Humidity")) { //debug("updateStatus() " + device.Name + " outsideHumidity = true"); var OutsideHumidity = accessory.getService("Outside Humidity"); OutsideHumidity.getCharacteristic(Characteristic.CurrentRelativeHumidity) .updateValue(device.OutsideHumidity); } // fakegato for outside sensor if (accessory.displayName == "Outside Sensors") { accessory.context.logEventCounter++; if (!(accessory.context.logEventCounter % 10)) { accessory.loggingService.addEntry({ time: moment().unix(), humidity: device.OutsideHumidity, temp: device.OutsideTemperature, pressure: 0 }); accessory.context.logEventCounter = 0; } } // update thermostat if (accessory.getService(device.Name)) { var service = accessory.getService(Service.Thermostat); service.getCharacteristic(Characteristic.Name) .updateValue(device.Name); service.getCharacteristic(Characteristic.TargetTemperature) .updateValue(device.TargetTemperature); service.getCharacteristic(Characteristic.CurrentTemperature) .updateValue(device.CurrentTemperature); service.getCharacteristic(Characteristic.CurrentHeatingCoolingState) .updateValue(device.CurrentHeatingCoolingState); service.getCharacteristic(Characteristic.TargetHeatingCoolingState) .updateValue(device.TargetHeatingCoolingState); if (device.device.UI.CanSetSwitchAuto) { service.getCharacteristic(Characteristic.CoolingThresholdTemperature) .updateValue(device.CoolingThresholdTemperature); service.getCharacteristic(Characteristic.HeatingThresholdTemperature) .updateValue(device.HeatingThresholdTemperature); } // Fakegato Support accessory.context.logEventCounter++; if (!(accessory.context.logEventCounter % 10)) { accessory.loggingService.addEntry({ time: moment().unix(), currentTemp: device.CurrentTemperature, setTemp: device.TargetTemperature, valvePosition: device.CurrentHeatingCoolingState }); accessory.context.logEventCounter = 0; } } } class TccAccessory { constructor(that, device, sensors) { this.log = that.log; //this.log("Adding TCC Device", device.Name); this.name = device.Name; this.ThermostatID = device.ThermostatID; this.device = device; this.usePermanentHolds = that.usePermanentHolds; this.storage = that.storage; this.refresh = that.refresh; // debug("TccAccessory()", device); var uuid = UUIDGen.generate(this.name + " - TCC"); var createInsideHumiditySensors = false; var createInsideTemperatureSensors = false; // need to get config for this thermostat id switch (sensors) { case "none": createInsideHumiditySensors = false; createInsideTemperatureSensors = false; break; case "all": createInsideHumiditySensors = true; createInsideTemperatureSensors = true; break; case "inside": createInsideHumiditySensors = true; createInsideTemperatureSensors = true; break; case "insideHumidity": createInsideHumiditySensors = true; createInsideTemperatureSensors = false; break; case "outside": createInsideHumiditySensors = false; createInsideTemperatureSensors = false; break; } // Check for invalid humidity value if (device.InsideHumidity == 128) { debug("Invalid inside humidity value for", device.Name + "(" + device.ThermostatID + ")"); createInsideHumiditySensors = false; } if (!getAccessoryByName(this.name)) { this.log("Adding TCC Device (deviceID=" + this.ThermostatID + ")", this.name); this.accessory = new Accessory(this.name, uuid, 10); this.accessory.log = that.log; this.accessory.context.ThermostatID = device.ThermostatID; this.accessory.context.name = this.name; this.accessory.context.logEventCounter = 9; // Update fakegato on startup this.accessory.getService(Service.AccessoryInformation) .setCharacteristic(Characteristic.Manufacturer, "TCC") .setCharacteristic(Characteristic.Model, device.Model) .setCharacteristic(Characteristic.SerialNumber, hostname + "-" + this.name) .setCharacteristic(Characteristic.FirmwareRevision, FirmwareRevision); this.accessory.addService(Service.Thermostat, this.name); // check if user wants separate temperature and humidity sensors by zone/thermostat debug("createInsideHumiditySensors: ", createInsideHumiditySensors); debug("createInsideTemperatureSensors: ", createInsideTemperatureSensors); if (createInsideTemperatureSensors) { // debug("TccAccessory() " + this.name + " InsideTemperature = true, existing sensor"); this.InsideTemperatureService = this.accessory.addService(Service.TemperatureSensor, this.name + " Temperature", "Inside"); this.InsideTemperatureService .getCharacteristic(Characteristic.CurrentTemperature) .setProps({ minValue: -100, // If you need this, you have major problems!!!!! maxValue: 100 }); } if (createInsideHumiditySensors) { debug("TccAccessory() " + this.name + " insideHumidity = true, existing sensor"); this.InsideHumidityService = this.accessory.addService(Service.HumiditySensor, this.name + " Humidity", "Inside"); this.InsideHumidityService .getCharacteristic(Characteristic.CurrentRelativeHumidity); } // .setProps({validValues: hbValues.TargetHeatingCoolingStateValidValues}) this.accessory .getService(Service.Thermostat) .getCharacteristic(Characteristic.TargetHeatingCoolingState) .setProps({ validValues: device.TargetHeatingCoolingStateValidValues }) .on('set', setTargetHeatingCooling.bind(this.accessory)); this.accessory .getService(Service.Thermostat) .getCharacteristic(Characteristic.CurrentTemperature) .setProps({ minValue: -100, // If you need this, you have major problems!!!!! maxValue: 100 }); this.accessory .getService(Service.Thermostat) .getCharacteristic(Characteristic.TargetTemperature) .setProps({ minValue: parseFloat(device.TargetTemperatureHeatMinValue), maxValue: parseFloat(device.TargetTemperatureCoolMaxValue) }) .on('set', setTargetTemperature.bind(this.accessory)); if (device.device.UI.CanSetSwitchAuto) { // Only available on models with an Auto Mode this.accessory .getService(Service.Thermostat) .getCharacteristic(Characteristic.CoolingThresholdTemperature) .setProps({ minValue: parseFloat(device.TargetTemperatureCoolMinValue), maxValue: parseFloat(device.TargetTemperatureCoolMaxValue) }) .on('set', setCoolingThresholdTemperature.bind(this.accessory)); // this.addOptionalCharacteristic(Characteristic.HeatingThresholdTemperature); this.accessory .getService(Service.Thermostat) .getCharacteristic(Characteristic.HeatingThresholdTemperature) .setProps({ minValue: parseFloat(device.TargetTemperatureHeatMinValue), maxValue: parseFloat(device.TargetTemperatureHeatMaxValue) }) .on('set', setHeatingThresholdTemperature.bind(this.accessory)); } this.accessory .getService(Service.Thermostat).log = this.log; this.accessory.loggingService = new FakeGatoHistoryService("thermo", this.accessory, { storage: this.storage, minutes: this.refresh * 10 / 60 }); this.accessory .getService(Service.Thermostat).addCharacteristic(CustomCharacteristics.ValvePosition); this.accessory.context.ChangeThermostat = new ChangeThermostat(this.accessory); that.api.registerPlatformAccessories("homebridge-tcc", "tcc", [this.accessory]); myAccessories.push(this.accessory); return this.accessory; } else { this.log("Existing TCC accessory (deviceID=" + this.ThermostatID + ")", this.name); // need to check if accessory/zone/thermostat already exists, but user added temp/humidity sensors then must declare this.accessory = getAccessoryByName(this.name); debug("Heating Threshold", this.accessory.getService(Service.Thermostat).getCharacteristic(Characteristic.HeatingThresholdTemperature).props); debug("Cooling Threshold", this.accessory.getService(Service.Thermostat).getCharacteristic(Characteristic.CoolingThresholdTemperature).props); if (createInsideTemperatureSensors && !this.accessory.getService(this.name + " Temperature")) { debug("TccAccessory() " + this.name + " InsideTemperature = true, adding sensor"); this.InsideTemperatureService = this.accessory.addService(Service.TemperatureSensor, this.name + " Temperature", "Inside"); this.InsideTemperatureService .getCharacteristic(Characteristic.CurrentTemperature) .setProps({ minValue: -100, // If you need this, you have major problems!!!!! maxValue: 100 }); } else if (!createInsideTemperatureSensors && this.accessory.getService(this.name + " Temperature")) { this.accessory.removeService(this.accessory.getService(this.name + " Temperature")); } if (createInsideHumiditySensors && !this.accessory.getService(this.name + " Humidity")) { debug("TccAccessory() " + this.name + " InsideHumidity = true, adding sensor"); this.InsideHumidityService = this.accessory.addService(Service.HumiditySensor, this.name + " Humidity", "Inside"); this.InsideHumidityService .getCharacteristic(Characteristic.CurrentRelativeHumidity); } else if (!createInsideHumiditySensors && this.accessory.getService(this.name + " Humidity")) { this.accessory.removeService(this.accessory.getService(this.name + " Humidity")); } return this.accessory; } } } class TccSensorsAccessory { constructor(that, device, sensors) { this.log = that.log; //this.log("Adding TCC Sensors Device"); this.name = "Outside Sensors"; this.ThermostatID = device.ThermostatID; this.device = device; this.storage = that.storage; this.refresh = that.refresh; //debug("TccSensorsAccessory()",device); var uuid = UUIDGen.generate(this.name + " - TCC"); if (!getAccessoryByName(this.name)) { this.log("Adding TCC Outside Sensors (deviceID=" + this.ThermostatID + ")", this.name); this.accessory = new Accessory(this.name, uuid, 10); this.accessory.log = that.log; this.accessory.context.ThermostatID = device.ThermostatID; this.accessory.context.name = this.name; this.accessory.context.logEventCounter = 9; // Update fakegato on startup this.accessory.getService(Service.AccessoryInformation) .setCharacteristic(Characteristic.Manufacturer, "TCC") .setCharacteristic(Characteristic.Model, device.Model) .setCharacteristic(Characteristic.SerialNumber, hostname + "-" + this.name) .setCharacteristic(Characteristic.FirmwareRevision, FirmwareRevision); // create outside temp sensor debug("TccSensorsAccessory() " + this.name + " outsideTemperature = true, existing sensor"); this.OutsideTemperatureService = this.accessory.addService(Service.TemperatureSensor, "Outside Temperature", "Outside"); this.OutsideTemperatureService .getCharacteristic(Characteristic.CurrentTemperature) .setProps({ minValue: -100, // If you need this, you have major problems!!!!! maxValue: 100 }); // Check for invalid humidity value if (this.device.OutsideHumidity == 128) { debug("Invalid outside humidity value for", this.device.Name + "(" + this.device.ThermostatID + ")"); } else { // create outside humidity sensor debug("TccSensorsAccessory() " + this.name + " outsideHumidity = true, existing sensor"); this.OutsideHumidityService = this.accessory.addService(Service.HumiditySensor, "Outside Humidity", "Outside"); this.OutsideHumidityService .getCharacteristic(Characteristic.CurrentRelativeHumidity); this.accessory.loggingService = new FakeGatoHistoryService("weather", this.accessory, { storage: this.storage, minutes: this.refresh * 10 / 60 }); } that.api.registerPlatformAccessories("homebridge-tcc", "tcc", [this.accessory]); myAccessories.push(this.accessory); return this.accessory; } else { this.log("Existing TCC outside sensors accessory (deviceID=" + this.ThermostatID + ")", this.name); // need to check if accessory/zone/thermostat already exists, but user added temp/humidity sensors then must declare this.accessory = getAccessoryByName(this.name); if (!this.accessory.getService("Outside Temperature")) { debug("TccSensorsAccessory() " + this.name + " OutsideTemperature = true, adding sensor"); this.OutsideTemperatureService = this.accessory.addService(Service.TemperatureSensor, "Outside Temperature", "Outside"); this.OutsideTemperatureService .getCharacteristic(Characteristic.CurrentTemperature) .setProps({ minValue: -100, // If you need this, you have major problems!!!!! maxValue: 100 }); } // Check for invalid humidity value if (this.device.OutsideHumidity == 128) { debug("Invalid outside humidity value for", this.device.Name + "(" + this.device.ThermostatID + ")"); if (this.accessory.getService("Outside Humidity")) { this.accessory.removeService(this.accessory.getService("Outside Humidity")); } } else { if (!this.accessory.getService("Outside Humidity")) { debug("TccSensorsAccessory() " + this.name + " outsideHumidity = true, adding sensor"); this.OutsideHumidityService = this.accessory.addService(Service.HumiditySensor, "Outside Humidity", "Outside"); this.OutsideHumidityService .getCharacteristic(Characteristic.CurrentRelativeHumidity); } } return this.accessory; } } } function setTargetTemperature(value, callback) { this.log("Setting target temperature for", this.displayName, "to", value + "°"); this.context.logEventCounter = 9; // debug("this", this); this.context.ChangeThermostat.put({ TargetTemperature: value }).then((thermostat) => { // debug("setTargetTemperature", this, thermostat); updateStatus(this, thermostat); callback(null); }).catch((error) => { callback(error); }); } function setTargetHeatingCooling(value, callback) { this.log("Setting switch for", this.displayName, "to", value); this.context.logEventCounter = 9; this.context.ChangeThermostat.put({ TargetHeatingCooling: value }).then((thermostat) => { // debug("setTargetHeatingCooling", this, thermostat); updateStatus(this, thermostat); callback(null); }).catch((error) => { callback(error); }); } function setHeatingThresholdTemperature(value, callback) { this.log("Setting HeatingThresholdTemperature for", this.displayName, "to", value); this.context.ChangeThermostat.put({ HeatingThresholdTemperature: value }).then((thermostat) => { // debug("setTargetHeatingCooling", this, thermostat); updateStatus(this, thermostat); callback(null); }).catch((error) => { callback(error); }); } function setCoolingThresholdTemperature(value, callback) { this.log("Setting CoolingThresholdTemperature for", this.displayName, "to", value); this.context.ChangeThermostat.put({ CoolingThresholdTemperature: value }).then((thermostat) => { // debug("setTargetHeatingCooling", this, thermostat); updateStatus(this, thermostat); callback(null); }).catch((error) => { callback(error); }); } function getAccessoryByName(accessoryName) { var value; myAccessories.forEach(function (accessory) { // debug("getAccessoryByName zone", accessory.name, name); if (accessory.context.name === accessoryName) { value = accessory; } }); return value; } function getAccessoryByThermostatID(ThermostatID) { var value; myAccessories.forEach(function (accessory) { // debug("getAccessoryByName zone", accessory.name, name); if (accessory.context.ThermostatID === ThermostatID) { value = accessory; } }); return value; } // Consolidate change requests received over 100ms into a single request class ChangeThermostat { constructor(accessory) { // debug("ChangeThermostat", accessory); this.desiredState = {}; this.deferrals = []; this.ThermostatID = accessory.context.ThermostatID; this.waitTimeUpdate = 100; // wait 100ms before processing change } put(state) { debug("put %s ->", this.ThermostatID, state); return new Promise((resolve, reject) => { this.desiredState.ThermostatID = this.ThermostatID; for (const key in state) { // console.log("ChangeThermostat", accessory); this.desiredState[key] = state[key]; } const d = { resolve: resolve, reject: reject }; this.deferrals.push(d); // debug("setTimeout", this.timeout); if (!this.timeout) { this.timeout = setTimeout(() => { // debug("put start"); thermostats.ChangeThermostat(this.desiredState).then((thermostat) => { for (const d of this.deferrals) { d.resolve(thermostat); } this.desiredState = {}; this.deferrals = []; this.timeout = null; // debug("put complete", thermostat); }).catch((error) => { for (const d of this.deferrals) { d.reject(error); } this.desiredState = {}; this.deferrals = []; this.timeout = null; // debug("put error", error); }); }, this.waitTimeUpdate); } }); } }