diff --git a/package.json b/package.json index bc8bf99..eb9e811 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "telldus-live": "^0.2.1", "teslams": "1.0.1", "tough-cookie": "^2.0.0", - "unofficial-nest-api": "git+https://github.com/hachidorii/unofficial_nodejs_nest.git#d8d48edc952b049ff6320ef99afa7b2f04cdee98", + "unofficial-nest-api": "git+https://github.com/kraigm/unofficial_nodejs_nest.git#3cbd337adc32fab3b481659b38d86f9fcd6a9c02", "wemo": "0.2.x", "wink-js": "0.0.5", "xml2js": "0.4.x", diff --git a/platforms/Nest.js b/platforms/Nest.js index 7853a67..5fc45d3 100644 --- a/platforms/Nest.js +++ b/platforms/Nest.js @@ -1,13 +1,17 @@ -var types = require("hap-nodejs/accessories/types.js"); var nest = require('unofficial-nest-api'); +var Service = require("hap-nodejs").Service; +var Characteristic = require("hap-nodejs").Characteristic; +var Accessory = require("hap-nodejs").Accessory; +var uuid = require("hap-nodejs").uuid; +var inherits = require('util').inherits; function NestPlatform(log, config){ + // auth info + this.username = config["username"]; + this.password = config["password"]; - // auth info - this.username = config["username"]; - this.password = config["password"]; - - this.log = log; + this.log = log; + this.accessoryLookup = { }; } NestPlatform.prototype = { @@ -20,8 +24,7 @@ NestPlatform.prototype = { nest.login(this.username, this.password, function (err, data) { if (err) { that.log("There was a problem authenticating with Nest."); - } - else { + } else { nest.fetchStatus(function (data) { for (var deviceId in data.device) { if (data.device.hasOwnProperty(deviceId)) { @@ -29,12 +32,28 @@ NestPlatform.prototype = { // it's a thermostat, adjust this to detect other accessories if (data.shared[deviceId].hasOwnProperty('current_temperature')) { - var name = data.shared[deviceId].name - var accessory = new NestThermostatAccessory(that.log, name, device, deviceId); + var initialData = data.shared[deviceId]; + var name = initialData.name; + var accessory = new NestThermostatAccessory(that.log, name, device, deviceId, initialData); + that.accessoryLookup[deviceId] = accessory; foundAccessories.push(accessory); } } } + function subscribe() { + nest.subscribe(subscribeDone, ['shared']); + } + + function subscribeDone(deviceId, data, type) { + // data if set, is also stored here: nest.lastStatus.shared[thermostatID] + if (deviceId && that.accessoryLookup[deviceId]) { + that.log('Update to Device: ' + deviceId + " type: " + type); + that.accessoryLookup[deviceId].updateData(data); + } + setTimeout(subscribe, 2000); + } + + subscribe(); callback(foundAccessories) }); } @@ -42,356 +61,204 @@ NestPlatform.prototype = { } } -function NestThermostatAccessory(log, name, device, deviceId) { - // device info - if (name) { - this.name = name; - } else { - this.name = "Nest" + device.serial_number; - } - this.model = device.model_version; - this.serial = device.serial_number; - this.deviceId = deviceId; - this.log = log; +function NestThermostatAccessory(log, name, device, deviceId, initialData) { + // device info + this.name = name || ("Nest" + device.serial_number); + this.deviceId = deviceId; + this.log = log; + this.device = device; + + var id = uuid.generate('nest.thermostat.' + deviceId); + Accessory.call(this, name, id); + this.uuid_base = id; + + this.currentData = initialData; + + this.getService(Service.AccessoryInformation) + .setCharacteristic(Characteristic.Manufacturer, "Nest") + .setCharacteristic(Characteristic.Model, device.model_version) + .setCharacteristic(Characteristic.SerialNumber, device.serial_number); + + this.addService(Service.Thermostat, name); + + this.getService(Service.Thermostat) + .getCharacteristic(Characteristic.TemperatureDisplayUnits) + .on('get', function(callback) { + var units = this.getTemperatureUnits(); + var unitsName = units == Characteristic.TemperatureDisplayUnits.FAHRENHEIT ? "Fahrenheit" : "Celsius"; + this.log("Tempature unit for " + this.name + " is: " + unitsName); + if (callback) callback(null, units); + }.bind(this)); + + this.getService(Service.Thermostat) + .getCharacteristic(Characteristic.CurrentTemperature) + .on('get', function(callback) { + var curTemp = this.getCurrentTemperature(); + this.log("Current temperature for " + this.name + " is: " + curTemp); + if (callback) callback(null, curTemp); + }.bind(this)); + + this.getService(Service.Thermostat) + .getCharacteristic(Characteristic.CurrentHeatingCoolingState) + .on('get', function(callback) { + var curHeatingCooling = this.getCurrentHeatingCooling(); + this.log("Current heating for " + this.name + " is: " + curHeatingCooling); + if (callback) callback(null, curHeatingCooling); + }.bind(this)); + + this.getService(Service.Thermostat) + .getCharacteristic(Characteristic.TargetTemperature) + .on('get', function(callback) { + var targetTemp = this.getTargetTemperature(); + this.log("Target temperature for " + this.name + " is: " + targetTemp); + if (callback) callback(null, targetTemp); + }.bind(this)) + .on('set', this.setTargetTemperature.bind(this)); + + this.getService(Service.Thermostat) + .getCharacteristic(Characteristic.TargetHeatingCoolingState) + .on('get', function(callback) { + var targetHeatingCooling = this.getTargetHeatingCooling(); + this.log("Target heating for " + this.name + " is: " + targetHeatingCooling); + if (callback) callback(null, targetHeatingCooling); + }.bind(this)) + .on('set', this.setTargetHeatingCooling.bind(this)); + + this.updateData(initialData); } +inherits(NestThermostatAccessory, Accessory); +NestThermostatAccessory.prototype.parent = Accessory.prototype; -NestThermostatAccessory.prototype = { - getCurrentHeatingCooling: function(callback){ +NestThermostatAccessory.prototype.getServices = function() { + return this.services; +}; - var that = this; - - this.log("Checking current heating cooling for: " + this.name); - nest.fetchStatus(function (data) { - var device = data.device[that.deviceId]; - - var currentHeatingCooling = 0; - switch(device.current_schedule_mode) { - case "OFF": - targetHeatingCooling = 0; - break; - case "HEAT": - currentHeatingCooling = 1; - break; - case "COOL": - currentHeatingCooling = 2; - break; - case "RANGE": - currentHeatingCooling = 3; - break; - default: - currentHeatingCooling = 0; - } - that.log("Current heating for " + this.name + "is: " + currentHeatingCooling); - callback(currentHeatingCooling); - }); - - - }, - - getTargetHeatingCoooling: function(callback){ - - var that = this; - - this.log("Checking target heating cooling for: " + this.name); - nest.fetchStatus(function (data) { - var device = data.device[that.deviceId]; - - var targetHeatingCooling = 0; - switch(device.target_temperature_type) { - case "off": - targetHeatingCooling = 0; - break; - case "heat": - targetHeatingCooling = 1; - break; - case "cool": - targetHeatingCooling = 2; - break; - case "range": - targetHeatingCooling = 3; - break; - default: - targetHeatingCooling = 0; - } - that.log("Current target heating for " + this.name + " is: " + targetHeatingCooling); - callback(targetHeatingCooling); - }); - }, - - getCurrentTemperature: function(callback){ - - var that = this; - - nest.fetchStatus(function (data) { - var device = data.shared[that.deviceId]; - that.log("Current temperature for " + this.name + " is: " + device.current_temperature); - callback(device.current_temperature); - }); - - - }, - - getTargetTemperature: function(callback){ - - var that = this; - - nest.fetchStatus(function (data) { - var device = data.shared[that.deviceId]; - that.log("Target temperature for " + this.name + " is: " + device.target_temperature); - callback(device.target_temperature); - }); - - - }, - - getTemperatureUnits: function(callback){ - - var that = this; - - nest.fetchStatus(function (data) { - var device = data.device[that.deviceId]; - var temperatureUnits = 0; - switch(device.temperature_scale) { - case "F": - that.log("Tempature unit for " + this.name + " is: " + "Fahrenheit"); - temperatureUnits = 1; - break; - case "C": - that.log("Tempature unit for " + this.name + " is: " + "Celsius"); - temperatureUnits = 0; - break; - default: - temperatureUnits = 0; - } - - callback(temperatureUnits); - }); - - - }, - - getCurrentRelativeHumidity: function(callback){ - - var that = this; - - nest.fetchStatus(function (data) { - var device = data.device[that.deviceId]; - that.log("Humidity for " + this.name + " is: " + device.current_humidity); - callback(device.current_humidity); - }) - - - }, - - setTargetHeatingCooling: function(targetHeatingCooling){ - - var that = this; - - var targetTemperatureType = 'off'; - switch(targetHeatingCooling) { - case 0: - targetTemperatureType = 'off'; - break; - case 1: - targetTemperatureType = 'heat'; - break; - case 2: - targetTemperatureType = 'cool'; - break; - case 3: - targetTemperatureType = 'range'; - break; - default: - targetTemperatureType = 'off'; - } - - this.log("Setting target heating cooling for " + this.name + " to: " + targetTemperatureType); - nest.setTargetTemperatureType(this.deviceId, targetTemperatureType); - - - }, - - setTargetTemperature: function(targetTemperature){ - - var that = this; - - this.log("Setting target temperature for " + this.name + " to: " + targetTemperature); - nest.setTemperature(this.deviceId, targetTemperature); - - - }, - - getServices: function() { - var that = this; - return [{ - sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of the accessory", - designedMaxLength: 255 - },{ - cType: types.MANUFACTURER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Nest", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.model, - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.serial, - supportEvents: false, - supportBonjour: false, - manfDescription: "SN", - designedMaxLength: 255 - },{ - cType: types.IDENTIFY_CTYPE, - onUpdate: null, - perms: ["pw"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Identify Accessory", - designedMaxLength: 1 - }] - },{ - sType: types.THERMOSTAT_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of thermostat", - designedMaxLength: 255 - },{ - cType: types.CURRENTHEATINGCOOLING_CTYPE, - onUpdate: null, - onRead: function(callback) { - that.getCurrentHeatingCooling(function(currentHeatingCooling){ - callback(currentHeatingCooling); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Current Mode", - designedMaxLength: 1, - designedMinValue: 0, - designedMaxValue: 2, - designedMinStep: 1, - },{ - cType: types.TARGETHEATINGCOOLING_CTYPE, - onUpdate: function(value) { - that.setTargetHeatingCooling(value); - }, - onRead: function(callback) { - that.getTargetHeatingCoooling(function(targetHeatingCooling){ - callback(targetHeatingCooling); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Mode", - designedMinValue: 0, - designedMaxValue: 3, - designedMinStep: 1, - },{ - cType: types.CURRENT_TEMPERATURE_CTYPE, - onUpdate: null, - onRead: function(callback) { - that.getCurrentTemperature(function(currentTemperature){ - callback(currentTemperature); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: 20, - supportEvents: false, - supportBonjour: false, - manfDescription: "Current Temperature", - unit: "celsius" - },{ - cType: types.TARGET_TEMPERATURE_CTYPE, - onUpdate: function(value) { - that.setTargetTemperature(value); - }, - onRead: function(callback) { - that.getTargetTemperature(function(targetTemperature){ - callback(targetTemperature); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 20, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Temperature", - designedMinValue: 16, - designedMaxValue: 38, - designedMinStep: 1, - unit: "celsius" - },{ - cType: types.TEMPERATURE_UNITS_CTYPE, - onUpdate: null, - onRead: function(callback) { - that.getTemperatureUnits(function(temperatureUnits){ - callback(temperatureUnits); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Unit", - },{ - cType: types.CURRENT_RELATIVE_HUMIDITY_CTYPE, - onUpdate: null, - onRead: function(callback) { - that.getCurrentRelativeHumidity(function(currentRelativeHumidity){ - callback(currentRelativeHumidity); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Humidity", - }] - }]; +NestThermostatAccessory.prototype.updateData = function(data) { + if (data != undefined) { + this.currentData = data; } -} + var thermostat = this.getService(Service.Thermostat); + thermostat.getCharacteristic(Characteristic.TemperatureDisplayUnits).getValue(); + thermostat.getCharacteristic(Characteristic.CurrentTemperature).getValue(); + thermostat.getCharacteristic(Characteristic.CurrentHeatingCoolingState).getValue(); + thermostat.getCharacteristic(Characteristic.TargetHeatingCoolingState).getValue(); + thermostat.getCharacteristic(Characteristic.TargetTemperature).getValue(); +}; + +NestThermostatAccessory.prototype.getCurrentHeatingCooling = function(){ + var current = this.getCurrentTemperature(); + var state = this.getTargetHeatingCooling(); + + var isRange = state == (Characteristic.CurrentHeatingCoolingState.HEAT | Characteristic.CurrentHeatingCoolingState.COOL); + var high = isRange ? this.currentData.target_temperature_high : this.currentData.target_temperature; + var low = isRange ? this.currentData.target_temperature_low : this.currentData.target_temperature; + + // Add threshold + var threshold = .2; + high += threshold; + low -= threshold; + + if ((state & Characteristic.CurrentHeatingCoolingState.COOL) && this.currentData.can_cool && high < current) { + return Characteristic.CurrentHeatingCoolingState.COOL; + } + if ((state & Characteristic.CurrentHeatingCoolingState.HEAT) && this.currentData.can_heat && low > current) { + return Characteristic.CurrentHeatingCoolingState.HEAT; + } + return Characteristic.CurrentHeatingCoolingState.OFF; +}; + +NestThermostatAccessory.prototype.getTargetHeatingCooling = function(){ + switch(this.currentData.target_temperature_type) { + case "off": + return Characteristic.CurrentHeatingCoolingState.OFF; + case "heat": + return Characteristic.CurrentHeatingCoolingState.HEAT; + case "cool": + return Characteristic.CurrentHeatingCoolingState.COOL; + case "range": + return Characteristic.CurrentHeatingCoolingState.HEAT | Characteristic.CurrentHeatingCoolingState.COOL; + default: + return Characteristic.CurrentHeatingCoolingState.OFF; + } +}; + +NestThermostatAccessory.prototype.getCurrentTemperature = function(){ + return this.currentData.current_temperature; +}; + +NestThermostatAccessory.prototype.getTargetTemperature = function() { + switch (this.getTargetHeatingCooling()) { + case Characteristic.CurrentHeatingCoolingState.HEAT | Characteristic.CurrentHeatingCoolingState.COOL: + // Choose closest target as single target + var high = this.currentData.target_temperature_high; + var low = this.currentData.target_temperature_low; + var cur = this.currentData.current_temperature; + return Math.abs(high - cur) < Math.abs(cur - low) ? high : low; + default: + return this.currentData.target_temperature; + } +}; + +NestThermostatAccessory.prototype.getTemperatureUnits = function() { + switch(this.device.temperature_scale) { + case "F": + return Characteristic.TemperatureDisplayUnits.FAHRENHEIT; + case "C": + return Characteristic.TemperatureDisplayUnits.CELSIUS; + default: + return Characteristic.TemperatureDisplayUnits.CELSIUS; + } +}; + +NestThermostatAccessory.prototype.setTargetHeatingCooling = function(targetHeatingCooling, callback){ + var targetTemperatureType = null; + + switch(targetHeatingCooling) { + case Characteristic.CurrentHeatingCoolingState.HEAT: + targetTemperatureType = 'heat'; + break; + case Characteristic.CurrentHeatingCoolingState.COOL: + targetTemperatureType = 'cool'; + break; + case Characteristic.CurrentHeatingCoolingState.HEAT | Characteristic.CurrentHeatingCoolingState.COOL: + targetTemperatureType = 'range'; + break; + default: + targetTemperatureType = 'off'; + break; + } + + this.log("Setting target heating cooling for " + this.name + " to: " + targetTemperatureType); + nest.setTargetTemperatureType(this.deviceId, targetTemperatureType); + + if (callback) callback(null, targetTemperatureType); +}; + +NestThermostatAccessory.prototype.setTargetTemperature = function(targetTemperature, callback){ + + switch (this.getTargetHeatingCooling()) { + case Characteristic.CurrentHeatingCoolingState.HEAT | Characteristic.CurrentHeatingCoolingState.COOL: + // Choose closest target as single target + var high = this.currentData.target_temperature_high; + var low = this.currentData.target_temperature_low; + var cur = this.currentData.current_temperature; + var isHighTemp = Math.abs(high - cur) < Math.abs(cur - low); + if (isHighTemp) { + high = targetTemperature; + } else { + low = targetTemperature; + } + this.log("Setting " + (isHighTemp ? "high" : "low") + " target temperature for " + this.name + " to: " + targetTemperature); + nest.setTemperatureRange(this.deviceId, low, high); + break; + default: + this.log("Setting target temperature for " + this.name + " to: " + targetTemperature); + nest.setTemperature(this.deviceId, targetTemperature); + break; + } + + if (callback) callback(null, targetTemperature); +}; module.exports.accessory = NestThermostatAccessory; module.exports.platform = NestPlatform;