├── .gitignore ├── README.md ├── example.js ├── lib ├── colors.js ├── group.js ├── http.js ├── hue.js ├── light.js ├── paths.js └── utilities.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.sw[a-z] 3 | *.orig 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Hue Module 2 | 3 | This is a node.js client library for the [Philips Hue](http://www.meethue.com). 4 | 5 | 6 | ## Installation 7 | 8 | npm install hue-module 9 | 10 | ## Usage 11 | 12 | In order to use the hue.js library, you will need to know the IP address of the 13 | hue base station. You can find this through the 14 | [meethue dashboard](http://www.meethue.com/) or using arp. To use arp, note the 15 | MAC address of the device (written on the bottom of the base station) and then 16 | issue the `arp -a` command from your terminal. 17 | 18 | var hue = require('hue-module'); 19 | 20 | hue.load({ 21 | "host" : "IP Address", 22 | "key" : "Username/Key", 23 | "port" : 80 24 | }); 25 | 26 | hue.lights(function(lights) { 27 | for (i in lights) { 28 | if (lights.hasOwnProperty(i)) { 29 | hue.change(lights[i].set({"on": true, "rgb":[0,255,255]})); 30 | } 31 | } 32 | }); 33 | 34 | At the moment there is no way to discover a base station or register with it. This is coming soon. 35 | 36 | ## API 37 | 38 | Below you will find an outline of the available methods, their purpose, and the 39 | corresponding usage example. 40 | 41 | ### Find your basestation 42 | 43 | If you do not already know the IP address of the base station you can search for it. 44 | 45 | hue.nupnpDiscover(callback) 46 | 47 | ### Register a username 48 | 49 | To be able to send requests you need to register for a username. Do so by calling the following command after loading. 50 | 51 | hue.getUsername(callback) 52 | 53 | An IP address is returned in the callback that can then be used to load the module. 54 | 55 | ### Get a list of lights 56 | 57 | hue.lights(callback) 58 | 59 | In the callback a list of lights are returned. Each light can be set however one chooses. 60 | 61 | ### Get a particular light 62 | 63 | hue.light(lightID, callback) 64 | 65 | ### Set settings of a particular light 66 | 67 | light.set(attributes) 68 | 69 | Usage example: 70 | 71 | hue.light(1, function(light) { 72 | light.set({ "on": false }); 73 | }); 74 | 75 | ### Render changes to bulb 76 | 77 | hue.change(light) 78 | 79 | Usage example: 80 | 81 | hue.light(1, function(light) { 82 | hue.change(light.set({ "on": false })); 83 | }); 84 | ### Get a list of light groups 85 | 86 | hue.groups(callback) 87 | 88 | An array of groups is returned in the callback. 89 | 90 | ### Get a particular group 91 | 92 | hue.group(groupID, callback) 93 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var hue = require('./lib/hue'); 2 | 3 | var loadBridge = function(host) { 4 | hue.load({ 5 | "host" : host 6 | }); 7 | 8 | hue.getUsername(function(err, result) { 9 | if (err) { 10 | console.log(err); 11 | return; 12 | } 13 | 14 | turnOnLights(host, result.username); 15 | }); 16 | }; 17 | 18 | var turnOnLights = function(host, username) { 19 | hue.load({ 20 | "host" : host, 21 | "key" : username 22 | }); 23 | 24 | hue.lights(function(lights) { 25 | for (var i in lights) { 26 | if (lights.hasOwnProperty(i)) { 27 | hue.change(lights[i].set({ 28 | "on" : true, 29 | "rgb" : [ 30 | Math.random() * 256 >>> 0, 31 | Math.random() * 256 >>> 0, 32 | Math.random() * 256 >>> 0 33 | ] 34 | })); 35 | } 36 | } 37 | }); 38 | }; 39 | 40 | hue.nupnpDiscover(function(error, hosts) { 41 | 42 | if (error) { 43 | console.error(error); 44 | return; 45 | } 46 | 47 | for (var i in hosts) { 48 | if (hosts.hasOwnProperty(i)) { 49 | loadBridge(hosts[i].internalipaddress); 50 | } 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /lib/colors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Color utility functions. 3 | * No external dependencies. 4 | * Rewritten for use as a node.js module. 5 | * Special thanks for the RGB to CIE conversion code goes out to the Q42 team 6 | * for their Q42.HueApi work. Dank u! 7 | * More info: https://github.com/Q42/Q42.HueApi. 8 | * 9 | * https://github.com/bjohnso5/hue-hacking 10 | * Copyright (c) 2013 Bryan Johnson; Licensed MIT */ 11 | 12 | var XYPoint = function(x, y) { 13 | this.x = x; 14 | this.y = y; 15 | }, 16 | Red = new XYPoint(0.675, 0.322), 17 | Lime = new XYPoint(0.4091, 0.518), 18 | Blue = new XYPoint(0.167, 0.04); 19 | 20 | exports.rgbToCIE1931 = function(red, green, blue) { 21 | var point = _getXYPointFromRGB(red, green, blue); 22 | return [point.x, point.y]; 23 | }; 24 | 25 | 26 | function _randomFromInterval(from /* Number */ , to /* Number */ ) { 27 | return Math.floor(Math.random() * (to - from + 1) + from); 28 | } 29 | 30 | function _randomRGBValue() { 31 | return _randomFromInterval(0, 255); 32 | } 33 | 34 | function _crossProduct(p1, p2) { 35 | return (p1.x * p2.y - p1.y * p2.x); 36 | } 37 | 38 | function _checkPointInLampsReach(p) { 39 | var v1 = new XYPoint(Lime.x - Red.x, Lime.y - Red.y), 40 | v2 = new XYPoint(Blue.x - Red.x, Blue.y - Red.y), 41 | q = new XYPoint(p.x - Red.x, p.y - Red.y), 42 | s = _crossProduct(q, v2) / _crossProduct(v1, v2), 43 | t = _crossProduct(v1, q) / _crossProduct(v1, v2); 44 | return (s >= 0.0) && (t >= 0.0) && (s + t <= 1.0); 45 | } 46 | 47 | function _getClosestPointToPoint(A, B, P) { 48 | var AP = new XYPoint(P.x - A.x, P.y - A.y), 49 | AB = new XYPoint(B.x - A.x, B.y - A.y), 50 | ab2 = AB.x * AB.x + AB.y * AB.y, 51 | ap_ab = AP.x * AB.x + AP.y * AB.y, 52 | t = ap_ab / ab2; 53 | if (t < 0.0) t = 0.0; 54 | else if (t > 1.0) t = 1.0; 55 | return new XYPoint(A.x + AB.x * t, A.y + AB.y * t); 56 | } 57 | 58 | function _getDistanceBetweenTwoPoints(one, two) { 59 | var dx = one.x - two.x, 60 | // horizontal difference 61 | dy = one.y - two.y; // vertical difference 62 | return Math.sqrt(dx * dx + dy * dy); 63 | } 64 | 65 | function _getXYPointFromRGB(red, green, blue) { 66 | var r = (red > 0.04045) ? Math.pow((red + 0.055) / (1.0 + 0.055), 2.4) : (red / 12.92), 67 | g = (green > 0.04045) ? Math.pow((green + 0.055) / (1.0 + 0.055), 2.4) : (green / 12.92), 68 | b = (blue > 0.04045) ? Math.pow((blue + 0.055) / (1.0 + 0.055), 2.4) : (blue / 12.92), 69 | X = r * 0.4360747 + g * 0.3850649 + b * 0.0930804, 70 | Y = r * 0.2225045 + g * 0.7168786 + b * 0.0406169, 71 | Z = r * 0.0139322 + g * 0.0971045 + b * 0.7141733, 72 | cx = X / (X + Y + Z), 73 | cy = Y / (X + Y + Z); 74 | cx = isNaN(cx) ? 0.0 : cx; 75 | cy = isNaN(cy) ? 0.0 : cy; 76 | //Check if the given XY value is within the colourreach of our lamps. 77 | var xyPoint = new XYPoint(cx, cy), 78 | inReachOfLamps = _checkPointInLampsReach(xyPoint); 79 | if (!inReachOfLamps) { 80 | //Color is unreproducible, find the closest point on each line in the CIE 1931 'triangle'. 81 | var pAB = _getClosestPointToPoint(Red, Lime, xyPoint), 82 | pAC = _getClosestPointToPoint(Blue, Red, xyPoint), 83 | pBC = _getClosestPointToPoint(Lime, Blue, xyPoint), 84 | // Get the distances per point and see which point is closer to our Point. 85 | dAB = _getDistanceBetweenTwoPoints(xyPoint, pAB), 86 | dAC = _getDistanceBetweenTwoPoints(xyPoint, pAC), 87 | dBC = _getDistanceBetweenTwoPoints(xyPoint, pBC), 88 | lowest = dAB, 89 | closestPoint = pAB; 90 | if (dAC < lowest) { 91 | lowest = dAC; 92 | closestPoint = pAC; 93 | } 94 | if (dBC < lowest) { 95 | lowest = dBC; 96 | closestPoint = pBC; 97 | } 98 | // Change the xy value to a value which is within the reach of the lamp. 99 | cx = closestPoint.x; 100 | cy = closestPoint.y; 101 | } 102 | 103 | return new XYPoint(cx, cy); 104 | } 105 | -------------------------------------------------------------------------------- /lib/group.js: -------------------------------------------------------------------------------- 1 | var color = require('./colors'), 2 | utilities = require('./utilities'); 3 | 4 | var Group = function () { 5 | this.type = "group"; 6 | }; 7 | 8 | module.exports.create = function () { 9 | return new Group(); 10 | }; 11 | 12 | Group.prototype.set = function(value) { 13 | if (value.rgb) { 14 | utilities.combine(this, { "xy": color.rgbToCIE1931(value.rgb[0], value.rgb[1], value.rgb[2]) }); 15 | delete value.rgb; 16 | } 17 | 18 | utilities.combine(this, value); 19 | 20 | return this; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/http.js: -------------------------------------------------------------------------------- 1 | var http = require('follow-redirects').http, 2 | util = require("util"); 3 | 4 | module.exports = { 5 | httpPost : doPost, 6 | httpPut : doPut, 7 | httpGet : doGet, 8 | jsonGet : doJsonGet 9 | }; 10 | 11 | function doGet(host, port, path, callback) { 12 | return _doRequest( 13 | { 14 | "host": host, 15 | "port": port, 16 | "path": path 17 | }, callback); 18 | } 19 | 20 | function doJsonGet(host, port, path, callback) { 21 | return _doJsonRequest( 22 | { 23 | "host": host, 24 | "port": port, 25 | "path": path 26 | }, callback); 27 | } 28 | 29 | function doPost(host, port, path, values, callback) { 30 | return _doJsonRequest( 31 | { 32 | "host" : host, 33 | "port" : port, 34 | "path" : path, 35 | "method": "POST", 36 | "values": values 37 | }, callback); 38 | } 39 | 40 | function doPut(host, port, path, values, callback) { 41 | return _doJsonRequest( 42 | { 43 | "host" : host, 44 | "port" : port, 45 | "path" : path, 46 | "method": "PUT", 47 | "values": values 48 | }, callback); 49 | } 50 | 51 | function _doJsonRequest(parameters, callback) { 52 | return _doRequest(parameters, callback); 53 | } 54 | 55 | function _buildOptions(parameters) { 56 | var options = {}; 57 | 58 | for (var i in parameters.values) { 59 | if ((parameters.values).hasOwnProperty(i)) { 60 | if (i == 'name' || i == 'id') { 61 | delete parameters.values[i]; 62 | } 63 | } 64 | } 65 | 66 | if (parameters.host) 67 | options.host = parameters.host; 68 | else 69 | throw new Error("A host name must be provided in the parameters"); 70 | 71 | options.method = parameters.method || "GET"; 72 | 73 | if (parameters.path) 74 | options.path = parameters.path; 75 | 76 | if (parameters.values) 77 | options.body = JSON.stringify(parameters.values); 78 | 79 | options.headers = { 80 | accept: '*/*' 81 | }; 82 | options.maxRedirects = 3; 83 | 84 | if (parameters.port) 85 | options.port = parameters.port; 86 | 87 | return options; 88 | } 89 | 90 | function _doRequest(parameters, callback) { 91 | if (!callback) callback = function() {}; 92 | 93 | parameters = _buildOptions(parameters); 94 | var content = ''; 95 | var request = http.request(parameters, function(response) { 96 | response.setEncoding('utf8'); 97 | response.on("data", function(chunk) { 98 | content += chunk; 99 | }); 100 | response.on('end', function() { 101 | callback(null, _parseJsonResult(content)); 102 | }); 103 | }); 104 | 105 | request.on('error', function(e) { 106 | callback(e, null); 107 | }); 108 | 109 | if (parameters.method == "POST" || parameters.method == "PUT") 110 | request.write(parameters.body); 111 | 112 | request.end(); 113 | } 114 | 115 | function _parseJsonResult(result) { 116 | return JSON.parse(result.toString()); 117 | } 118 | -------------------------------------------------------------------------------- /lib/hue.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | util = require('util'), 3 | path = require('./paths'), 4 | http = require('./http'), 5 | light = require('./light'), 6 | group = require('./group'), 7 | exports = module.exports = {}; 8 | 9 | var authenticated = false; 10 | 11 | exports.discover = function(timeout, callback) { 12 | 13 | if (typeof(callback) == "undefined") { 14 | callback = timeout; 15 | timeout = 5000; 16 | } 17 | 18 | var os = require('os'); 19 | var dgram = require("dgram"); 20 | 21 | /* get a list of our local IPv4 addresses */ 22 | 23 | var interfaces = os.networkInterfaces(); 24 | var addresses = []; 25 | for (var dev in interfaces) { 26 | for (var i = 0; i < interfaces[dev].length; i++) { 27 | if (interfaces[dev][i].family != 'IPv4') continue; 28 | if (interfaces[dev][i].internal) continue; 29 | addresses.push(interfaces[dev][i].address); 30 | } 31 | } 32 | 33 | /* this code adapted from https://github.com/Burgestrand/ruhue/blob/master/lib/ruhue.rb#L23 */ 34 | 35 | var socket = dgram.createSocket("udp4"); 36 | socket.bind(function() { 37 | socket.setBroadcast(true); 38 | socket.setMulticastTTL(128); 39 | addresses.forEach(function(address) { 40 | socket.addMembership("239.255.255.250", address); 41 | }); 42 | 43 | var payload = new Buffer([ 44 | "M-SEARCH * HTTP/1.1", 45 | "HOST: 239.255.255.250:1900", 46 | "MAN: ssdp:discover", 47 | "MX: 10", 48 | "ST: ssdp:all" 49 | ].join("\n")); 50 | 51 | socket.on("error", console.error); 52 | 53 | var timer = null; 54 | socket.on("message", function(msg, rinfo) { 55 | if (msg.toString('utf8').match(/IpBridge/)) { // check to see if it's a HUE responding 56 | socket.close(); 57 | if (timer) clearTimeout(timer); 58 | 59 | callback(null, rinfo.address); 60 | } 61 | }); 62 | 63 | socket.send(payload, 0, payload.length, 1900, "239.255.255.250", function() { 64 | timer = setTimeout(function () { 65 | socket.close(); 66 | callback("Discovery timeout expired."); 67 | }, timeout); 68 | }); 69 | }); 70 | }; 71 | 72 | exports.nupnpDiscover = function(callback) { 73 | 74 | http.httpGet("www.meethue.com", 80, path.nupnp(), callback); 75 | }; 76 | 77 | exports.getUsername = function(callback) { 78 | 79 | var parameters = { 80 | "devicetype" : "hue-module" 81 | }; 82 | 83 | var host = this.host, 84 | port = this.port; 85 | 86 | http.httpPost(host, port, path.api(), parameters, function(err, response) { 87 | if (err) { 88 | return callback(err, null); 89 | } 90 | if (response) { 91 | if (response[0].success) { 92 | callback(null, response[0].success); 93 | } 94 | else { 95 | callback(response[0].error); 96 | } 97 | } 98 | }); 99 | }; 100 | 101 | exports.load = function(parameters) { 102 | 103 | this.host = parameters.host; 104 | this.port = parameters.port || 80; 105 | this.key = parameters.key; 106 | 107 | authenticated = false; 108 | }; 109 | 110 | exports.lights = function(callback) { 111 | 112 | if (!callback) callback = function() {}; 113 | 114 | function buildResults(result) { 115 | var lights = [], 116 | id; 117 | 118 | for (id in result) { 119 | if (result.hasOwnProperty(id)) { 120 | lights.push(light.create().set({ "id": id, "name": result[id].name })); 121 | } 122 | } 123 | return lights; 124 | } 125 | 126 | function process(err, result) { 127 | callback(buildResults(result)); 128 | } 129 | 130 | var host = this.host, 131 | key = this.key, 132 | port = this.port; 133 | 134 | if (authenticated) { 135 | 136 | http.jsonGet(host, port, path.lights(key), process); 137 | } 138 | else { 139 | connect(host, port, key, function() { 140 | http.jsonGet(host, port, path.lights(key), process); 141 | }); 142 | } 143 | }; 144 | 145 | exports.light = function(id, callback) { 146 | 147 | if (!callback) callback = function() {}; 148 | 149 | function process(err, result) { 150 | callback(light.create().set(result.state).set({ "name": result.name, "id": id })); 151 | } 152 | var host = this.host, 153 | key = this.key, 154 | port = this.port; 155 | 156 | if (authenticated) { 157 | 158 | http.jsonGet(host, port, path.lights(key, id), process); 159 | } 160 | else { 161 | connect(host, port, key, function() { 162 | http.jsonGet(host, port, path.lights(key, id), process); 163 | }); 164 | } 165 | }; 166 | 167 | exports.groups = function(callback) { 168 | 169 | if (!callback) callback = function() {}; 170 | 171 | function buildResults(result) { 172 | 173 | var groups = [], 174 | id; 175 | 176 | for (id in result) { 177 | if (result.hasOwnProperty(id)) { 178 | groups.push(group.create().set({ "id": id, "name": result[id].name })); 179 | } 180 | } 181 | return groups; 182 | } 183 | 184 | function process(err, result) { 185 | callback(buildResults(result)); 186 | } 187 | 188 | var host = this.host, 189 | key = this.key, 190 | port = this.port; 191 | 192 | if (authenticated) { 193 | 194 | http.jsonGet(host, port, path.groups(key), process); 195 | } 196 | else { 197 | connect(host, port, key, function(){ 198 | http.jsonGet(host, port, path.groups(key), process); 199 | }); 200 | } 201 | }; 202 | 203 | exports.group = function(id, callback) { 204 | 205 | if (!callback) callback = function() {}; 206 | 207 | function process(err, result) { 208 | 209 | callback(group.create().set(result.action).set({ "name": result.name, "id": id })); 210 | } 211 | var host = this.host, 212 | key = this.key, 213 | port = this.port; 214 | 215 | if (authenticated) { 216 | 217 | http.jsonGet(host, port, path.groups(key, id), process); 218 | } 219 | else { 220 | connect(host, port, key, function() { 221 | http.jsonGet(host, port, path.groups(key, id), process); 222 | }); 223 | } 224 | }; 225 | 226 | exports.createGroup = function(name, lights, callback) { 227 | 228 | if (!callback) callback = function() {}; 229 | 230 | var host = this.host, 231 | key = this.key, 232 | port = this.port, 233 | values = { 234 | "name" : name, 235 | "lights": _intArrayToStringArray(lights) 236 | }; 237 | 238 | if (authenticated) { 239 | http.httpPost(host, port, path.groups(key, null), values); 240 | } 241 | else { 242 | connect(host, port, key, function() { 243 | http.httpPost(host, port, path.groups(key, null), values); 244 | }); 245 | } 246 | }; 247 | 248 | exports.change = function(object){ 249 | 250 | var host = this.host, 251 | key = this.key, 252 | port = this.port, 253 | location; 254 | 255 | if (object.type == 'group') { 256 | location = path.groupState(key, object.id); 257 | } 258 | else { 259 | location = path.lightState(key, object.id); 260 | } 261 | 262 | if (authenticated) { 263 | 264 | http.httpPut(host, port, location, object); 265 | } 266 | else { 267 | connect(host, port, key, function(){ 268 | http.httpPut(host, port, location, object); 269 | }); 270 | } 271 | }; 272 | 273 | 274 | function connect(host, port, key, callback) { 275 | 276 | if (!host || !key) 277 | throw new Error('And IP address and application username are required parameters.'); 278 | 279 | if (!callback) callback = function() {}; 280 | http.jsonGet(host, port, path.api(key), function(err, result) { 281 | if (err) 282 | throw new Error('There is no Hue Station at the given address.'); 283 | 284 | authenticated = true; 285 | callback(result); 286 | }); 287 | } 288 | 289 | function _intArrayToStringArray(array){ 290 | 291 | retArr = []; 292 | for (var entry in array) 293 | retArr.push(array[entry]+""); 294 | return retArr; 295 | } 296 | -------------------------------------------------------------------------------- /lib/light.js: -------------------------------------------------------------------------------- 1 | var color = require('./colors'), 2 | utilities = require('./utilities'); 3 | 4 | var Light = function () { 5 | 6 | this.type = "light"; 7 | }; 8 | 9 | module.exports.create = function () { 10 | 11 | return new Light(); 12 | }; 13 | 14 | Light.prototype.set = function(value) { 15 | 16 | if (value.rgb !== null) { 17 | utilities.combine(this, { "xy": color.rgbToCIE1931(value.rgb[0], value.rgb[1], value.rgb[2]) }); 18 | delete value.rgb; 19 | } 20 | 21 | utilities.combine(this, value); 22 | 23 | return this; 24 | }; 25 | -------------------------------------------------------------------------------- /lib/paths.js: -------------------------------------------------------------------------------- 1 | function nupnpPath() { 2 | return "/api/nupnp"; 3 | } 4 | 5 | function getApiPath(username) { 6 | var url = "/api"; 7 | if (username) 8 | url += "/" + username; 9 | return url; 10 | } 11 | 12 | function getApiLightsPath(username, lightID) { 13 | var url = getApiPath(username) + "/lights"; 14 | if (lightID) 15 | url += "/" + lightID; 16 | return url; 17 | } 18 | 19 | function getApiGroupPath(username, groupID) { 20 | var url = getApiPath(username) + "/groups"; 21 | if (groupID) 22 | url += "/" + groupID; 23 | return url; 24 | } 25 | 26 | function getApiGroupPathState(username, groupID) { 27 | return getApiGroupPath(username, groupID) + "/action"; 28 | } 29 | function getApiLightStatePath(username, lightId) { 30 | return getApiLightsPath(username, lightId) + "/state"; 31 | } 32 | 33 | module.exports = { 34 | nupnp: nupnpPath, 35 | api: getApiPath, 36 | lights: getApiLightsPath, 37 | lightState: getApiLightStatePath, 38 | groups: getApiGroupPath, 39 | groupState: getApiGroupPathState 40 | }; -------------------------------------------------------------------------------- /lib/utilities.js: -------------------------------------------------------------------------------- 1 | exports.combine = function(obj, values) { 2 | var argIdx = 1, 3 | state, 4 | property; 5 | 6 | while (argIdx < arguments.length) { 7 | state = arguments[argIdx]; 8 | for (property in state) { 9 | obj[property] = state[property]; 10 | } 11 | argIdx++; 12 | } 13 | return obj; 14 | }; 15 | 16 | exports.getBounded = function(value, min, max) { 17 | if (isNaN(value)) 18 | value = min; 19 | 20 | if (value < min) 21 | return min; 22 | else if (value > max) 23 | return max; 24 | else 25 | return value; 26 | }; 27 | 28 | exports.brightnessToHue = function(value) { 29 | return Math.floor(exports.getBounded(value, 0, 100) * (255 / 100)); 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hue-module", 3 | "version": "0.1.3", 4 | "description": "A client library for interacting with the Philips Hue lighting products", 5 | "main": "lib/hue.js", 6 | "dependencies": { 7 | "follow-redirects": "0.0.6" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/pushchris/hue-module.git" 16 | }, 17 | "keywords": [ 18 | "Hue", 19 | "Philips", 20 | "Lighting" 21 | ], 22 | "readmeFilename": "README.md", 23 | "author": "Chris Anderson", 24 | "license": "Beerware" 25 | } 26 | --------------------------------------------------------------------------------