diff --git a/.gitignore b/.gitignore index 8ba684b..81a1589 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # Node node_modules/ npm-debug.log +.node-version # Intellij .idea/ diff --git a/README.md b/README.md index cf74caa..4a4e98e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Since Siri supports devices added through HomeKit, this means that with HomeBrid * _Siri, turn off the Speakers._ ([Sonos](http://www.sonos.com)) * _Siri, turn on the Dehumidifier._ ([WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)) * _Siri, turn on Away Mode._ ([Xfinity Home](http://www.comcast.com/home-security.html)) - * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad)) + * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com)) If you would like to support any other devices, please write a shim and create a pull request and I'd be happy to add it to this official list. diff --git a/config-sample.json b/config-sample.json index de32480..e97c5de 100644 --- a/config-sample.json +++ b/config-sample.json @@ -22,6 +22,12 @@ "server": "127.0.0.1", "port": "8005" }, + { + "platform": "PhilipsHue", + "name": "Phillips Hue", + "ip_address": "127.0.0.1", + "username": "252deadbeef0bf3f34c7ecb810e832f" + }, { "platform": "ISY", "name": "ISY", diff --git a/package.json b/package.json index 92d1d2b..c12ab9f 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "request": "2.49.x", "node-persist": "0.0.x", "xmldoc": "0.1.x", + "node-hue-api": "^1.0.5", "xml2js": "0.4.x", - "carwingsjs": "0.0.x", "sonos": "0.8.x", "wemo": "0.2.x", diff --git a/platforms/PhilipsHue.js b/platforms/PhilipsHue.js new file mode 100644 index 0000000..fcd5624 --- /dev/null +++ b/platforms/PhilipsHue.js @@ -0,0 +1,337 @@ +// Philips Hue Platform Shim for HomeBridge +// +// Remember to add platform to config.json. Example: +// "platforms": [ +// { +// "platform": "PhilipsHue", +// "name": "Philips Hue", +// "ip_address": "127.0.0.1", +// "username": "252deadbeef0bf3f34c7ecb810e832f" +// } +// ], +// +// When you attempt to add a device, it will ask for a "PIN code". +// The default code for all HomeBridge accessories is 031-45-154. +// + +/* jslint node: true */ +/* globals require: false */ +/* globals config: false */ + +"use strict"; + +var hue = require("node-hue-api"), + HueApi = hue.HueApi, + lightState = hue.lightState; + +var types = require("../lib/HAP-NodeJS/accessories/types.js"); + +function PhilipsHuePlatform(log, config) { + this.log = log; + this.ip_address = config["ip_address"]; + this.username = config["username"]; +} + +function PhilipsHueAccessory(log, device, api) { + this.id = device.id; + this.name = device.name; + this.model = device.modelid; + this.device = device; + this.api = api; + this.log = log; +} + +// Get the ip address of the first available bridge with meethue.com or a network scan. +var locateBridge = function (callback) { + var that = this; + + // Report the results of the scan to the user + var getIp = function (err, bridges) { + if (!bridges || bridges.length === 0) { + that.log("No Philips Hue bridges found."); + callback(err || new Error("No bridges found")); + return; + } + + if (bridges.length > 1) { + that.log("Warning: Multiple Philips Hue bridges detected. The first bridge will be used automatically. To use a different bridge, set the `ip_address` manually in the configuration."); + } + + that.log( + "Philips Hue bridges found:\n" + + (bridges.map(function (bridge) { + // Bridge name is only returned from meethue.com so use id instead if it isn't there + return "\t" + bridge.ipaddress + ' - ' + (bridge.name || bridge.id); + })).join("\n") + ); + + callback(null, bridges[0].ipaddress); + }; + + // Try to discover the bridge ip using meethue.com + that.log("Attempting to discover Philips Hue bridge with meethue.com..."); + hue.nupnpSearch(function (locateError, bridges) { + if (locateError) { + that.log("Philips Hue bridge discovery with meethue.com failed. Register your bridge with the meethue.com for more reliable discovery."); + + that.log("Attempting to discover Philips Hue bridge with network scan..."); + + // Timeout after one minute + hue.upnpSearch(60000) + .then(function (bridges) { + that.log("Scan complete"); + getIp(null, bridges); + }) + .fail(function (scanError) { + that.log("Philips Hue bridge discovery with network scan failed. Check your network connection or set ip_address manually in configuration."); + getIp(new Error("Scan failed: " + scanError.message)); + }).done(); + } else { + getIp(null, bridges); + } + }); +}; + +PhilipsHuePlatform.prototype = { + accessories: function(callback) { + this.log("Fetching Philips Hue lights..."); + var that = this; + var getLights = function () { + var api = new HueApi(that.ip_address, that.username); + // Connect to the API and loop through lights + api.lights(function(err, response) { + if (err) throw err; + response.lights.map(function(light) { + var foundAccessories = []; + // Get the state of each individual light and add to platform + api.lightStatus(light.id, function(err, device) { + if (err) throw err; + device.id = light.id; + var accessory = new PhilipsHueAccessory(that.log, device, api); + foundAccessories.push(accessory); + callback(foundAccessories); + }); + }); + }); + }; + + // Discover the bridge if needed + if (!this.ip_address) { + locateBridge.call(this, function (err, ip_address) { + if (err) throw err; + + // TODO: Find a way to persist this + that.ip_address = ip_address; + that.log("Save the Philips Hue bridge ip address "+ ip_address +" to your config to skip discovery."); + getLights(); + }); + } else { + getLights(); + } + } +}; + +PhilipsHueAccessory.prototype = { + // Convert 0-65535 to 0-360 + hueToArcDegrees: function(value) { + value = value/65535; + value = value*100; + value = Math.round(value); + return value; + }, + // Convert 0-360 to 0-65535 + arcDegreesToHue: function(value) { + value = value/360; + value = value*65535; + value = Math.round(value); + return value; + }, + // Convert 0-255 to 0-100 + bitsToPercentage: function(value) { + value = value/255; + value = value*100; + value = Math.round(value); + return value; + }, + // Create and set a light state + executeChange: function(api, device, characteristic, value) { + var that = this; + var state = lightState.create(); + switch(characteristic.toLowerCase()) { + case 'identify': + state.alert('select'); + break; + case 'power': + if (value) { + state.on(); + } + else { + state.off(); + } + break; + case 'hue': + state.hue(this.arcDegreesToHue(value)); + break; + case 'brightness': + state.brightness(value); + break; + case 'saturation': + state.saturation(value); + break; + } + api.setLightState(device.id, state, function(err, lights) { + if (!err) { + that.log(device.name + ", characteristic: " + characteristic + ", value: " + value + "."); + } + else { + that.log(err); + } + }); + }, + // Get Services + getServices: function() { + var that = this; + var bulb_characteristics = [ + { + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: false, + supportBonjour: false, + manfDescription: "Name of service", + designedMaxLength: 255 + },{ + cType: types.POWER_STATE_CTYPE, + onUpdate: function(value) { + that.executeChange(that.api, that.device, "power", value); + }, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: that.device.state.on, + supportEvents: false, + supportBonjour: false, + manfDescription: "Turn On the Light", + designedMaxLength: 1 + },{ + cType: types.BRIGHTNESS_CTYPE, + onUpdate: function(value) { + that.executeChange(that.api, that.device, "brightness", value); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: that.bitsToPercentage(that.device.state.bri), + supportEvents: false, + supportBonjour: false, + manfDescription: "Adjust Brightness of Light", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + } + ]; + // Handle the Hue/Hue Lux divergence + if (that.device.state.hasOwnProperty('hue') && that.device.state.hasOwnProperty('sat')) { + bulb_characteristics.push({ + cType: types.HUE_CTYPE, + onUpdate: function(value) { + that.executeChange(that.api, that.device, "hue", value); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: that.hueToArcDegrees(that.device.state.hue), + supportEvents: false, + supportBonjour: false, + manfDescription: "Adjust Hue of Light", + designedMinValue: 0, + designedMaxValue: 360, + designedMinStep: 1, + unit: "arcdegrees" + }); + bulb_characteristics.push({ + cType: types.SATURATION_CTYPE, + onUpdate: function(value) { + that.executeChange(that.api, that.device, "saturation", value); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: that.bitsToPercentage(that.device.state.sat), + supportEvents: false, + supportBonjour: false, + manfDescription: "Adjust Saturation of Light", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }); + } + var accessory_data = [ + { + 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: "Philips", + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + },{ + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: that.model, + supportEvents: false, + supportBonjour: false, + manfDescription: "Model", + designedMaxLength: 255 + },{ + cType: types.SERIAL_NUMBER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: that.device.uniqueid, + supportEvents: false, + supportBonjour: false, + manfDescription: "SN", + designedMaxLength: 255 + },{ + cType: types.IDENTIFY_CTYPE, + onUpdate: function(value) { + that.executeChange(that.api, that.device, "identify", value); + }, + perms: ["pw"], + format: "bool", + initialValue: false, + supportEvents: false, + supportBonjour: false, + manfDescription: "Identify Accessory", + designedMaxLength: 1 + } + ] + },{ + sType: types.LIGHTBULB_STYPE, + // `bulb_characteristics` defined based on bulb type + characteristics: bulb_characteristics + } + ]; + return accessory_data; + } +}; + +module.exports.platform = PhilipsHuePlatform;