From c897913005bf3a0f19ae77c72d3918c346c262cd Mon Sep 17 00:00:00 2001 From: Kraig McConaghy Date: Sat, 17 Oct 2015 20:51:57 -0500 Subject: [PATCH 1/3] Initial work. Still in progress --- platforms/Nest.js | 488 ++++++++++++++++++++-------------------------- 1 file changed, 211 insertions(+), 277 deletions(-) diff --git a/platforms/Nest.js b/platforms/Nest.js index c1ef3bd..8803b22 100644 --- a/platforms/Nest.js +++ b/platforms/Nest.js @@ -1,5 +1,11 @@ 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){ @@ -29,8 +35,9 @@ 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); foundAccessories.push(accessory); } } @@ -42,7 +49,7 @@ NestPlatform.prototype = { } } -function NestThermostatAccessory(log, name, device, deviceId) { +function NestThermostatAccessory(log, name, device, deviceId, initialData) { // device info if (name) { this.name = name; @@ -53,124 +60,226 @@ function NestThermostatAccessory(log, name, device, deviceId) { this.serial = device.serial_number; this.deviceId = deviceId; this.log = log; + Accessory.call(this, name, uuid.generate(deviceId)); + + this.getService(Service.AccessoryInformation) + .setCharacteristic(Characteristic.Manufacturer, "Nest") + .setCharacteristic(Characteristic.Model, this.model) + .setCharacteristic(Characteristic.SerialNumber, this.serial); + + this.addService(Service.Thermostat, name); + + this.getService(Service.Thermostat) + .setCharacteristic(Characteristic.TemperatureDisplayUnits, this.extractTemperatureUnits(device)) + .on('get', this.getTemperatureUnits); + + this.getService(Service.Thermostat) + .setCharacteristic(Characteristic.CurrentTemperature, this.extractCurrentTemperature(device)) + //.getCharacteristic(Characteristic.CurrentTemperature) + .on('get', this.getCurrentTemperature); + + this.getService(Service.Thermostat) + .setCharacteristic(Characteristic.TargetTemperature, this.extractTargetTemperature(device)) + .on('get', this.getTargetTemperature) + .on('set', this.setTargetTemperature); + + this.getService(Service.Thermostat) + .setCharacteristic(Characteristic.CurrentHeatingCoolingState, this.extractCurrentHeatingCooling(device)) + .on('get', this.getCurrentHeatingCooling); + + this.getService(Service.Thermostat) + .setCharacteristic(Characteristic.TargetHeatingCoolingState, this.extractTargetHeatingCooling(device)) + .on('get', this.getTargetHeatingCoooling) + .on('set', this.setTargetHeatingCooling); + + //this.getService(Service.Thermostat) + // .getCharacteristic(Characteristic.CurrentRelativeHumidity) + // .on('get', function(callback) { + // that.getCurrentRelativeHumidity(function(currentRelativeHumidity){ + // callback(currentRelativeHumidity); + // }); + // }); + + + } - -NestThermostatAccessory.prototype = { - getCurrentHeatingCooling: function(callback){ - - 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; +inherits(NestThermostatAccessory, Accessory); +//NestThermostatAccessory.prototype.parent = Accessory.prototype; +Service.prototype.getCharacteristic = function(name) { + // returns a characteristic object from the service + // If Service.prototype.getCharacteristic(Characteristic.Type) does not find the characteristic, + // but the type is in optionalCharacteristics, it adds the characteristic.type to the service and returns it. + var index, characteristic; + for (index in this.characteristics) { + characteristic = this.characteristics[index]; + if (typeof name === 'string' && characteristic.displayName === name) { + return characteristic; + } + else if (typeof name === 'function' && characteristic instanceof name) { + return characteristic; + } + } + if (typeof name === 'function') { + for (index in this.optionalCharacteristics) { + characteristic = this.optionalCharacteristics[index]; + if (characteristic instanceof name) { + return this.addCharacteristic(name); } - that.log("Current heating for " + this.name + "is: " + currentHeatingCooling); - callback(currentHeatingCooling); - }); + } + } +}; +NestThermostatAccessory.prototype.getServices = function() { + return this.services; +}; - }, - - getTargetHeatingCoooling: function(callback){ - +NestThermostatAccessory.prototype.extractCurrentHeatingCooling = function(device){ + var currentHeatingCooling = 0; + switch(device.current_schedule_mode) { + case "OFF": + currentHeatingCooling = 0; + break; + case "HEAT": + currentHeatingCooling = 1; + break; + case "COOL": + currentHeatingCooling = 2; + break; + case "RANGE": + currentHeatingCooling = 3; + break; + default: + currentHeatingCooling = 0; + } + this.log("Current heating for " + this.name + "is: " + currentHeatingCooling); + return currentHeatingCooling; +}; +NestThermostatAccessory.prototype.getCurrentHeatingCooling = function(callback){ + var that = this; + this.log("Checking current heating cooling for: " + this.name); + nest.fetchStatus(function (data) { + var device = data.device[that.deviceId]; + var currentHeatingCooling = that.extractCurrentHeatingCooling(device); + callback(currentHeatingCooling); + }); +}; +NestThermostatAccessory.prototype.extractTargetHeatingCooling = function(device){ + 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; + } + this.log("Current target heating for " + this.name + " is: " + targetHeatingCooling); + return targetHeatingCooling; +}; +NestThermostatAccessory.prototype.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); + var targetHeatingCooling = that.extractTargetHeatingCooling(device); callback(targetHeatingCooling); }); - }, + }; - getCurrentTemperature: function(callback){ +NestThermostatAccessory.prototype.extractCurrentTemperature = function(device){ + var curTemp = this.extractAsDisplayUnit(device.current_temperature, device); + this.log("Current temperature for " + this.name + " is: " + curTemp); + return curTemp; +}; +NestThermostatAccessory.prototype.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); + var curTemp = this.extractCurrentTemperature(device); + callback(curTemp); }); + }; +NestThermostatAccessory.prototype.extractTargetTemperature = function(device){ + var targetTemp; + if (device.target_temperature != undefined) { + targetTemp = device.target_temperature; + } else if (device.temperature_lock_high_temp != undefined) { + targetTemp = device.temperature_lock_high_temp; + } else { + return null; + } - }, - - getTargetTemperature: function(callback){ - + targetTemp = this.extractAsDisplayUnit(targetTemp, device); + this.log("Target temperature for " + this.name + " is: " + targetTemp); + return targetTemp; +}; +NestThermostatAccessory.prototype.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); + var targetTemp = this.extractTargetTemperature(device); + callback(targetTemp); }); + }; +NestThermostatAccessory.prototype.extractTemperatureUnits = function(device) { + var temperatureUnits = 0; + switch(device.temperature_scale) { + case "F": + this.log("Tempature unit for " + this.name + " is: " + "Fahrenheit"); + temperatureUnits = 1; + break; + case "C": + this.log("Tempature unit for " + this.name + " is: " + "Celsius"); + temperatureUnits = 0; + break; + default: + temperatureUnits = 0; + } + return temperatureUnits; +}; - }, +NestThermostatAccessory.prototype.isFahrenheitUnit = function(unit) { + return unit == 1; +}; - getTemperatureUnits: function(callback){ +NestThermostatAccessory.prototype.convertToDisplayUnit = function(value, displayUnit) { + return this.isFahrenheitUnit(displayUnit) ? nest.ctof(value) : value; +}; +NestThermostatAccessory.prototype.convertToValueUnit = function(value, displayUnit) { + return this.isFahrenheitUnit(displayUnit) ? nest.ftoc(value) : value; +}; + +NestThermostatAccessory.prototype.extractAsDisplayUnit = function(value, device) { + var tempUnit = this.extractTemperatureUnits(device); + return this.convertToDisplayUnit(value, tempUnit); +}; + +NestThermostatAccessory.prototype.extractAsValueUnit = function(value, device) { + return this.convertToValueUnit(value, this.extractTemperatureUnits(device)); +}; + +NestThermostatAccessory.prototype.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; - } - + var temperatureUnits = that.extractTemperatureUnits(device); callback(temperatureUnits); }); + }; - - }, - - getCurrentRelativeHumidity: function(callback){ +NestThermostatAccessory.prototype.getCurrentRelativeHumidity = function(callback){ var that = this; @@ -181,12 +290,9 @@ NestThermostatAccessory.prototype = { }) - }, - - setTargetHeatingCooling: function(targetHeatingCooling){ - - var that = this; + }; +NestThermostatAccessory.prototype.setTargetHeatingCooling = function(targetHeatingCooling, callback){ var targetTemperatureType = 'off'; switch(targetHeatingCooling) { case 0: @@ -208,190 +314,18 @@ NestThermostatAccessory.prototype = { 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", - }] - }]; + if (callback) { + callback(); } -} +}; + +NestThermostatAccessory.prototype.setTargetTemperature = function(targetTemperature, callback){ + this.log("Setting target temperature for " + this.name + " to: " + targetTemperature); + nest.setTemperature(this.deviceId, targetTemperature); + if (callback) { + callback(); + } +}; module.exports.accessory = NestThermostatAccessory; module.exports.platform = NestPlatform; From b5210f424f62371f8528d83d33b0d5f7f8cbf77d Mon Sep 17 00:00:00 2001 From: Kraig McConaghy Date: Mon, 19 Oct 2015 23:16:31 -0500 Subject: [PATCH 2/3] More fixes --- platforms/Nest.js | 82 +++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/platforms/Nest.js b/platforms/Nest.js index 8803b22..682fdea 100644 --- a/platforms/Nest.js +++ b/platforms/Nest.js @@ -14,6 +14,7 @@ function NestPlatform(log, config){ this.password = config["password"]; this.log = log; + this.accessoryLookup = { }; } NestPlatform.prototype = { @@ -28,6 +29,8 @@ NestPlatform.prototype = { that.log("There was a problem authenticating with Nest."); } else { + + nest.fetchStatus(function (data) { for (var deviceId in data.device) { if (data.device.hasOwnProperty(deviceId)) { @@ -36,12 +39,27 @@ NestPlatform.prototype = { if (data.shared[deviceId].hasOwnProperty('current_temperature')) { var initialData = data.shared[deviceId]; - var name = initialData.name + 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) }); } @@ -74,34 +92,16 @@ function NestThermostatAccessory(log, name, device, deviceId, initialData) { .on('get', this.getTemperatureUnits); this.getService(Service.Thermostat) - .setCharacteristic(Characteristic.CurrentTemperature, this.extractCurrentTemperature(device)) - //.getCharacteristic(Characteristic.CurrentTemperature) - .on('get', this.getCurrentTemperature); - - this.getService(Service.Thermostat) - .setCharacteristic(Characteristic.TargetTemperature, this.extractTargetTemperature(device)) + .setCharacteristic(Characteristic.TargetTemperature, this.extractTargetTemperature(initialData)) .on('get', this.getTargetTemperature) .on('set', this.setTargetTemperature); this.getService(Service.Thermostat) - .setCharacteristic(Characteristic.CurrentHeatingCoolingState, this.extractCurrentHeatingCooling(device)) - .on('get', this.getCurrentHeatingCooling); - - this.getService(Service.Thermostat) - .setCharacteristic(Characteristic.TargetHeatingCoolingState, this.extractTargetHeatingCooling(device)) - .on('get', this.getTargetHeatingCoooling) + .setCharacteristic(Characteristic.TargetHeatingCoolingState, this.extractTargetHeatingCooling(initialData)) + .on('get', this.getTargetHeatingCooling) .on('set', this.setTargetHeatingCooling); - //this.getService(Service.Thermostat) - // .getCharacteristic(Characteristic.CurrentRelativeHumidity) - // .on('get', function(callback) { - // that.getCurrentRelativeHumidity(function(currentRelativeHumidity){ - // callback(currentRelativeHumidity); - // }); - // }); - - - + this.updateData(initialData); } inherits(NestThermostatAccessory, Accessory); //NestThermostatAccessory.prototype.parent = Accessory.prototype; @@ -133,9 +133,16 @@ NestThermostatAccessory.prototype.getServices = function() { return this.services; }; +NestThermostatAccessory.prototype.updateData = function(data) { + var thermostat = this.getService(Service.Thermostat); + thermostat.setCharacteristic(Characteristic.CurrentTemperature, this.extractCurrentTemperature(data)); + thermostat.setCharacteristic(Characteristic.CurrentHeatingCoolingState, this.extractCurrentHeatingCooling(data)); + thermostat.setCharacteristic(Characteristic.CurrentRelativeHumidity, this.extractCurrentRelativeHumidity(data)); +}; + NestThermostatAccessory.prototype.extractCurrentHeatingCooling = function(device){ var currentHeatingCooling = 0; - switch(device.current_schedule_mode) { + switch(device.target_temperature_type) { case "OFF": currentHeatingCooling = 0; break; @@ -184,7 +191,7 @@ NestThermostatAccessory.prototype.extractTargetHeatingCooling = function(device) this.log("Current target heating for " + this.name + " is: " + targetHeatingCooling); return targetHeatingCooling; }; -NestThermostatAccessory.prototype.getTargetHeatingCoooling = function(callback){ +NestThermostatAccessory.prototype.getTargetHeatingCooling = function(callback){ var that = this; this.log("Checking target heating cooling for: " + this.name); nest.fetchStatus(function (data) { @@ -200,14 +207,6 @@ NestThermostatAccessory.prototype.extractCurrentTemperature = function(device){ this.log("Current temperature for " + this.name + " is: " + curTemp); return curTemp; }; -NestThermostatAccessory.prototype.getCurrentTemperature = function(callback){ - var that = this; - nest.fetchStatus(function (data) { - var device = data.shared[that.deviceId]; - var curTemp = this.extractCurrentTemperature(device); - callback(curTemp); - }); - }; NestThermostatAccessory.prototype.extractTargetTemperature = function(device){ var targetTemp; @@ -279,18 +278,11 @@ NestThermostatAccessory.prototype.getTemperatureUnits = function(callback){ }); }; -NestThermostatAccessory.prototype.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); - }) - - - }; +NestThermostatAccessory.prototype.extractCurrentRelativeHumidity = function(device) { + var humidity = device.current_humidity; + this.log("Humidity for " + this.name + " is: " + humidity); + return humidity; +}; NestThermostatAccessory.prototype.setTargetHeatingCooling = function(targetHeatingCooling, callback){ var targetTemperatureType = 'off'; From 9740a5052038a38da1faec37b6a29fff3f1957eb Mon Sep 17 00:00:00 2001 From: Kraig McConaghy Date: Tue, 27 Oct 2015 23:07:34 -0500 Subject: [PATCH 3/3] Several fixes. Full simulated temperature range support. --- package.json | 4 +- platforms/Nest.js | 360 +++++++++++++++++++--------------------------- 2 files changed, 153 insertions(+), 211 deletions(-) diff --git a/package.json b/package.json index ff5563b..ee1027d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "color": "0.10.x", "eibd": "^0.3.1", "elkington": "kevinohara80/elkington", - "hap-nodejs": "^0.0.2", + "hap-nodejs": "^0.0.3", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "lifx-api": "^1.0.1", @@ -32,7 +32,7 @@ "request": "2.49.x", "sonos": "0.8.x", "teslams": "1.0.1", - "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 682fdea..b4dd77f 100644 --- a/platforms/Nest.js +++ b/platforms/Nest.js @@ -6,14 +6,12 @@ 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 = { }; } @@ -27,10 +25,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)) { @@ -68,255 +63,202 @@ NestPlatform.prototype = { } function NestThermostatAccessory(log, name, device, deviceId, initialData) { - // device info - if (name) { - this.name = name; - } else { - this.name = "Nest"; - } - this.model = device.model_version; - this.serial = device.serial_number; - this.deviceId = deviceId; - this.log = log; - Accessory.call(this, name, uuid.generate(deviceId)); + // 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, this.model) - .setCharacteristic(Characteristic.SerialNumber, this.serial); + .setCharacteristic(Characteristic.Model, device.model_version) + .setCharacteristic(Characteristic.SerialNumber, device.serial_number); this.addService(Service.Thermostat, name); this.getService(Service.Thermostat) - .setCharacteristic(Characteristic.TemperatureDisplayUnits, this.extractTemperatureUnits(device)) - .on('get', this.getTemperatureUnits); + .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) - .setCharacteristic(Characteristic.TargetTemperature, this.extractTargetTemperature(initialData)) - .on('get', this.getTargetTemperature) - .on('set', this.setTargetTemperature); + .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) - .setCharacteristic(Characteristic.TargetHeatingCoolingState, this.extractTargetHeatingCooling(initialData)) - .on('get', this.getTargetHeatingCooling) - .on('set', this.setTargetHeatingCooling); + .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; -Service.prototype.getCharacteristic = function(name) { - // returns a characteristic object from the service - // If Service.prototype.getCharacteristic(Characteristic.Type) does not find the characteristic, - // but the type is in optionalCharacteristics, it adds the characteristic.type to the service and returns it. - var index, characteristic; - for (index in this.characteristics) { - characteristic = this.characteristics[index]; - if (typeof name === 'string' && characteristic.displayName === name) { - return characteristic; - } - else if (typeof name === 'function' && characteristic instanceof name) { - return characteristic; - } - } - if (typeof name === 'function') { - for (index in this.optionalCharacteristics) { - characteristic = this.optionalCharacteristics[index]; - if (characteristic instanceof name) { - return this.addCharacteristic(name); - } - } - } -}; +NestThermostatAccessory.prototype.parent = Accessory.prototype; NestThermostatAccessory.prototype.getServices = function() { return this.services; }; NestThermostatAccessory.prototype.updateData = function(data) { + if (data != undefined) { + this.currentData = data; + } var thermostat = this.getService(Service.Thermostat); - thermostat.setCharacteristic(Characteristic.CurrentTemperature, this.extractCurrentTemperature(data)); - thermostat.setCharacteristic(Characteristic.CurrentHeatingCoolingState, this.extractCurrentHeatingCooling(data)); - thermostat.setCharacteristic(Characteristic.CurrentRelativeHumidity, this.extractCurrentRelativeHumidity(data)); + 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.extractCurrentHeatingCooling = function(device){ - var currentHeatingCooling = 0; - switch(device.target_temperature_type) { - case "OFF": - currentHeatingCooling = 0; - break; - case "HEAT": - currentHeatingCooling = 1; - break; - case "COOL": - currentHeatingCooling = 2; - break; - case "RANGE": - currentHeatingCooling = 3; - break; - default: - currentHeatingCooling = 0; +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; } - this.log("Current heating for " + this.name + "is: " + currentHeatingCooling); - return currentHeatingCooling; + if ((state & Characteristic.CurrentHeatingCoolingState.HEAT) && this.currentData.can_heat && low > current) { + return Characteristic.CurrentHeatingCoolingState.HEAT; + } + return Characteristic.CurrentHeatingCoolingState.OFF; }; -NestThermostatAccessory.prototype.getCurrentHeatingCooling = function(callback){ - var that = this; - this.log("Checking current heating cooling for: " + this.name); - nest.fetchStatus(function (data) { - var device = data.device[that.deviceId]; - var currentHeatingCooling = that.extractCurrentHeatingCooling(device); - callback(currentHeatingCooling); - }); -}; -NestThermostatAccessory.prototype.extractTargetHeatingCooling = function(device){ - var targetHeatingCooling = 0; - switch(device.target_temperature_type) { + +NestThermostatAccessory.prototype.getTargetHeatingCooling = function(){ + switch(this.currentData.target_temperature_type) { case "off": - targetHeatingCooling = 0; - break; + return Characteristic.CurrentHeatingCoolingState.OFF; case "heat": - targetHeatingCooling = 1; - break; + return Characteristic.CurrentHeatingCoolingState.HEAT; case "cool": - targetHeatingCooling = 2; - break; + return Characteristic.CurrentHeatingCoolingState.COOL; case "range": - targetHeatingCooling = 3; - break; + return Characteristic.CurrentHeatingCoolingState.HEAT | Characteristic.CurrentHeatingCoolingState.COOL; default: - targetHeatingCooling = 0; + return Characteristic.CurrentHeatingCoolingState.OFF; } - this.log("Current target heating for " + this.name + " is: " + targetHeatingCooling); - return targetHeatingCooling; -}; -NestThermostatAccessory.prototype.getTargetHeatingCooling = 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 = that.extractTargetHeatingCooling(device); - callback(targetHeatingCooling); - }); - }; - - -NestThermostatAccessory.prototype.extractCurrentTemperature = function(device){ - var curTemp = this.extractAsDisplayUnit(device.current_temperature, device); - this.log("Current temperature for " + this.name + " is: " + curTemp); - return curTemp; }; -NestThermostatAccessory.prototype.extractTargetTemperature = function(device){ - var targetTemp; - if (device.target_temperature != undefined) { - targetTemp = device.target_temperature; - } else if (device.temperature_lock_high_temp != undefined) { - targetTemp = device.temperature_lock_high_temp; - } else { - return null; +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; } - - targetTemp = this.extractAsDisplayUnit(targetTemp, device); - this.log("Target temperature for " + this.name + " is: " + targetTemp); - return targetTemp; }; -NestThermostatAccessory.prototype.getTargetTemperature = function(callback){ - var that = this; - nest.fetchStatus(function (data) { - var device = data.shared[that.deviceId]; - var targetTemp = this.extractTargetTemperature(device); - callback(targetTemp); - }); - }; -NestThermostatAccessory.prototype.extractTemperatureUnits = function(device) { - var temperatureUnits = 0; - switch(device.temperature_scale) { +NestThermostatAccessory.prototype.getTemperatureUnits = function() { + switch(this.device.temperature_scale) { case "F": - this.log("Tempature unit for " + this.name + " is: " + "Fahrenheit"); - temperatureUnits = 1; - break; + return Characteristic.TemperatureDisplayUnits.FAHRENHEIT; case "C": - this.log("Tempature unit for " + this.name + " is: " + "Celsius"); - temperatureUnits = 0; - break; + return Characteristic.TemperatureDisplayUnits.CELSIUS; default: - temperatureUnits = 0; + return Characteristic.TemperatureDisplayUnits.CELSIUS; } - return temperatureUnits; -}; - -NestThermostatAccessory.prototype.isFahrenheitUnit = function(unit) { - return unit == 1; -}; - -NestThermostatAccessory.prototype.convertToDisplayUnit = function(value, displayUnit) { - return this.isFahrenheitUnit(displayUnit) ? nest.ctof(value) : value; -}; - -NestThermostatAccessory.prototype.convertToValueUnit = function(value, displayUnit) { - return this.isFahrenheitUnit(displayUnit) ? nest.ftoc(value) : value; -}; - -NestThermostatAccessory.prototype.extractAsDisplayUnit = function(value, device) { - var tempUnit = this.extractTemperatureUnits(device); - return this.convertToDisplayUnit(value, tempUnit); -}; - -NestThermostatAccessory.prototype.extractAsValueUnit = function(value, device) { - return this.convertToValueUnit(value, this.extractTemperatureUnits(device)); -}; - -NestThermostatAccessory.prototype.getTemperatureUnits = function(callback){ - var that = this; - nest.fetchStatus(function (data) { - var device = data.device[that.deviceId]; - var temperatureUnits = that.extractTemperatureUnits(device); - callback(temperatureUnits); - }); - }; - -NestThermostatAccessory.prototype.extractCurrentRelativeHumidity = function(device) { - var humidity = device.current_humidity; - this.log("Humidity for " + this.name + " is: " + humidity); - return humidity; }; NestThermostatAccessory.prototype.setTargetHeatingCooling = function(targetHeatingCooling, callback){ - 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'; - } + var targetTemperatureType = null; - this.log("Setting target heating cooling for " + this.name + " to: " + targetTemperatureType); - nest.setTargetTemperatureType(this.deviceId, targetTemperatureType); - - if (callback) { - callback(); + 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){ - this.log("Setting target temperature for " + this.name + " to: " + targetTemperature); - nest.setTemperature(this.deviceId, targetTemperature); - if (callback) { - 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;