├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── bearer-token.txt ├── dist ├── lifx.js └── utils.js ├── package.json ├── source ├── lifx.js └── utils.js └── test └── test-lifx.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "sourceRoot": "/source", 4 | "presets": [ "es2015" ] 5 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = false 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [package.json] 16 | indent_style = space 17 | indent_size = 2 18 | end_of_line = lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # Idea project directory (e.g. PhpStorm, WebStorm ...) 36 | .idea 37 | 38 | # Ignore test file 39 | /bearer-token.txt 40 | 41 | # Ignore coverage report 42 | coverage 43 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # Idea project directory (e.g. PhpStorm, WebStorm ...) 36 | .idea 37 | 38 | # Ignore test files 39 | bearer-token.txt 40 | 41 | # Don't publish the actual source 42 | source 43 | 44 | # Ignore coverage report 45 | coverage 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "4" 5 | - "5" 6 | after_success: 7 | - npm run report-coverage -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 klarstil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lifx HTTP Api Node.js Wrapper 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/lifx-http-api.svg)](https://www.npmjs.com/package/lifx-http-api) ![Dependency Status](https://david-dm.org/klarstil/lifx-http-api.svg) [![Build Status](https://travis-ci.org/klarstil/lifx-http-api.svg?branch=master)](https://travis-ci.org/klarstil/lifx-http-api) [![License MIT](https://img.shields.io/badge/license-mit-brightgreen.svg)](https://github.com/klarstil/lifx-http-api/blob/master/LICENSE) [![codecov.io](https://codecov.io/github/klarstil/lifx-http-api/coverage.svg?branch=master)](https://codecov.io/github/klarstil/lifx-http-api?branch=master) 4 | 5 | A thin Node.js API wrapper of the [Lifx HTTP protocol](http://api.developer.lifx.com/). 6 | 7 | This library is not, in any way, affiliated or related to Lifi Labs, Inc.. Use at your own risk. 8 | 9 | ## Installation 10 | 11 | ```sh 12 | $ npm install lifx-http-api --save 13 | ``` 14 | 15 | ## Compatibility 16 | 17 | Node.js 4.2.6+ is tested and supported on Mac, Linux and Windows. 18 | 19 | ## Bearer Token 20 | 21 | A bearer token is mandatory to use the API. A new token can be obtain in the [Lifx Cloud Settings](https://cloud.lifx.com/). 22 | 23 | ## Usage 24 | 25 | The thin API wrapper uses a client for network communication. This client handles all requests against the Lifx API. 26 | 27 | ```js 28 | var lifx = require('lifx-http-api'), 29 | client; 30 | 31 | client = new lifx({ 32 | bearerToken: '' 33 | }); 34 | ``` 35 | 36 | The `Client` object provides promises by the great [Q library](https://github.com/kriskowal/q). You can either use callbacks or promises. 37 | 38 | ```js 39 | // Using callbacks 40 | client.listLights('all', function(err, data) { 41 | if(err) { 42 | console.error(err); 43 | return; 44 | } 45 | 46 | console.log(data) 47 | }); 48 | 49 | // Using promises 50 | client.listLights('all').then(console.log, console.error); 51 | ``` 52 | 53 | ### Getting lights and scenes 54 | 55 | #### `client.listLights(selector, [cb])` 56 | Gets lights belonging to the authenticated account. Filter the lights using selectors. 57 | 58 | Option | Type | Default | Description 59 | ------ | ---- | ------- | ----------- 60 | `selector` | string | | Selector for the light bulb you want to get. See the [Selector section](http://api.developer.lifx.com/docs/selectors) to get more information. 61 | `cb` | function | null | `function(err, data) {}` Callback function which will be called when the HTTP request to the API was processed. 62 | 63 | **Usage example:** 64 | ```js 65 | // Using callbacks 66 | client.listLights('all', function(err, data) { 67 | if(err) { 68 | console.error(err); 69 | return; 70 | } 71 | 72 | console.log(data) 73 | }); 74 | 75 | // Using promises 76 | client.listLights('all').then(console.log, console.error); 77 | ``` 78 | 79 | ### Modifying light state 80 | 81 | #### `client.setState(selector, settings, [cb])` 82 | Sets the state of the lights within the selector. 83 | 84 | Option | Type | Default | Description 85 | ------ | ---- | ------- | ----------- 86 | `selector` | string | | Selector for the light bulb you want to get. See the [Selector section](http://api.developer.lifx.com/docs/selectors) to get more information. 87 | `settings` | object | `{}` | State configuration object. See the [official documentation ](http://api.developer.lifx.com/docs/set-state) for further information. 88 | `cb` | function | null | `function(err, data) {}` Callback function which will be called when the HTTP request to the API was processed. 89 | 90 | **Usage example:** 91 | ```js 92 | // Using callbacks 93 | client.setState('all', { 94 | power: 'on', 95 | color: 'blue saturation:0.5', 96 | brightness: 0.5, 97 | duration: 5 98 | }, function(err, data) { 99 | if(err) { 100 | console.error(err); 101 | return; 102 | } 103 | 104 | console.log(data) 105 | }); 106 | 107 | // Using promises 108 | client.setState('all', { 109 | power: 'on', 110 | color: 'blue saturation:0.5', 111 | brightness: 0.5, 112 | duration: 5 113 | }).then(console.log, console.error); 114 | ``` 115 | 116 | #### `client.setStates(settings, [cb])` 117 | This endpoint allows you to set different states on multiple selectors in a single request. 118 | 119 | Option | Type | Default | Description 120 | ------ | ---- | ------- | ----------- 121 | `settings` | Mixed | `{}` | Multiple State configuration object. See the [official documentation ](http://api.developer.lifx.com/docs/set-state) for further information. 122 | `cb` | function | null | `function(err, data) {}` Callback function which will be called when the HTTP request to the API was processed. 123 | 124 | **Usage example:** 125 | ```js 126 | // Using callbacks 127 | client.setState('all', { 128 | "states": [ { 129 | "selector": "all", 130 | "power": "on" 131 | }, { 132 | "selector": "group:test", 133 | "brightness": 0.5 134 | } ], 135 | "defaults": { 136 | "duration": 5.0 137 | } 138 | }, function (err, data) { 139 | if (err) { 140 | console.error(err); 141 | return; 142 | } 143 | 144 | console.log(data) 145 | }); 146 | ``` 147 | 148 | #### `client.togglePower(selector, [duration], [cb])` 149 | Turn off lights if they are on, or turn them on if they are off. Physically powered off lights are ignored. 150 | 151 | Option | Type | Default | Description 152 | ------ | ---- | ------- | ----------- 153 | `selector` | string | `all`| Selector for the light bulb you want to get. See the [Selector section](http://api.developer.lifx.com/docs/selectors) to get more information. 154 | `duration` | int | 0 | Turning on or off will be faded over the time (in seconds). 155 | `cb` | function | null | `function(err, data) {}` Callback function which will be called when the HTTP request to the API was processed. 156 | 157 | **Usage example:** 158 | ```js 159 | // Using callbacks 160 | client.togglePower('all', 1.5, function (err, data) { 161 | if (err) { 162 | console.error(err); 163 | return; 164 | } 165 | 166 | console.log(data) 167 | }); 168 | ``` 169 | 170 | #### `client.breathe(selector, [settings], [cb])` 171 | Performs a breathe effect by slowly fading between the given colors. Use the parameters to tweak the effect. 172 | 173 | Option | Type | Default | Description 174 | ------ | ---- | ------- | ----------- 175 | `selector` | string | `all`| Selector for the light bulb you want to get. See the [Selector section](http://api.developer.lifx.com/docs/selectors) to get more information. 176 | `settings` | Object | `{}` | Breathe effect object, see the [official documentation](http://api.developer.lifx.com/docs/breathe-effect) for all available parameter. 177 | `cb` | function | null | `function(err, data) {}` Callback function which will be called when the HTTP request to the API was processed. 178 | 179 | **Usage example:** 180 | ```js 181 | // Using callbacks 182 | client.breathe('all', { 183 | color: '#006633', 184 | from_color: '#00AF33', 185 | period: 1, 186 | cycles: 10, 187 | persist: true, 188 | power_on: true, 189 | peak: 0.8 190 | }, function (err, data) { 191 | if (err) { 192 | console.error(err); 193 | return; 194 | } 195 | 196 | console.log(data) 197 | }); 198 | ``` 199 | 200 | #### `client.pulse(selector, [settings], [cb])` 201 | Performs a pulse effect by quickly flashing between the given colors. Use the parameters to tweak the effect. 202 | 203 | Option | Type | Default | Description 204 | ------ | ---- | ------- | ----------- 205 | `selector` | string | `all`| Selector for the light bulb you want to get. See the [Selector section](http://api.developer.lifx.com/docs/selectors) to get more information. 206 | `settings` | Object | `{}` | Pulse effect object, see the [official documentation](http://api.developer.lifx.com/docs/pulse-effect) for all available parameter. 207 | `cb` | function | null | `function(err, data) {}` Callback function which will be called when the HTTP request to the API was processed. 208 | 209 | **Usage example:** 210 | ```js 211 | // Using callbacks 212 | client.pulse('all', { 213 | color: '#006633', 214 | from_color: '#00AF33', 215 | period: 1, 216 | cycles: 10, 217 | persist: true, 218 | power_on: true, 219 | peak: 0.8 220 | }, function (err, data) { 221 | if (err) { 222 | console.error(err); 223 | return; 224 | } 225 | 226 | console.log(data) 227 | }); 228 | ``` 229 | 230 | #### `client.cycle(selector, [settings], [cb])` 231 | Make the light(s) cycle to the next or previous state in a list of states. 232 | 233 | Option | Type | Default | Description 234 | ------ | ---- | ------- | ----------- 235 | `selector` | string | `all`| Selector for the light bulb you want to get. See the [Selector section](http://api.developer.lifx.com/docs/selectors) to get more information. 236 | `settings` | Object | `{}` | Cycle states object, see the [official documentation](http://api.developer.lifx.com/docs/cycle) for all available parameter. 237 | `cb` | function | null | `function(err, data) {}` Callback function which will be called when the HTTP request to the API was processed. 238 | 239 | **Usage example:** 240 | ```js 241 | // Using callbacks 242 | client.cycle('all', { 243 | "states": [{ 244 | "brightness": 1.0 245 | }, { 246 | "brightness": 0.5 247 | }, { 248 | "brightness": 0.1 249 | }, { 250 | "power": "off" 251 | }], 252 | "defaults": { 253 | "power": "on", // all states default to on 254 | "saturation": 0, // every state is white 255 | "duration": 2.0 // all transitions will be applied over 2 seconds 256 | } 257 | }, function (err, data) { 258 | if (err) { 259 | console.error(err); 260 | return; 261 | } 262 | 263 | console.log(data) 264 | }); 265 | ``` 266 | 267 | ### Working with scenes 268 | 269 | #### `client.listScenes([cb])` 270 | Lists all the scenes available in the users account. 271 | 272 | Option | Type | Default | Description 273 | ------ | ---- | ------- | ----------- 274 | `cb` | function | null | `function(err, data) {}` Callback function which will be called when the HTTP request to the API was processed. 275 | 276 | **Usage example:** 277 | ```js 278 | // Using callbacks 279 | client.listScene(function(err, data) { 280 | if(err) { 281 | console.error(err); 282 | return; 283 | } 284 | 285 | console.log(data) 286 | }); 287 | 288 | // Using promises 289 | client.listScenes().then(console.log, console.error); 290 | ``` 291 | 292 | #### `client.activateScene(selector, [duration], [cb])` 293 | Activates a scene from the users account. 294 | 295 | Option | Type | Default | Description 296 | ------ | ---- | ------- | ----------- 297 | `selector` | string | `all`| Scene selector. See the [Selector section](http://api.developer.lifx.com/docs/selectors) to get more information. 298 | `duration` | int | 0 | Fades to the scene (in seconds). 299 | `cb` | function | null | `function(err, data) {}` Callback function which will be called when the HTTP request to the API was processed. 300 | 301 | **Usage example:** 302 | ```js 303 | // Using callbacks 304 | client.activateScene('scene_id:d073d501cf2c', 1.2, function (err, data) { 305 | if (err) { 306 | console.error(err); 307 | return; 308 | } 309 | 310 | console.log(data) 311 | }); 312 | ``` 313 | 314 | ### Utility methods 315 | 316 | #### `client.validateColor(color, [cb])` 317 | This method lets you validate a user's color string and return the hue, saturation, brightness and kelvin values that the API will interpret as. 318 | 319 | Option | Type | Default | Description 320 | ------ | ---- | ------- | ----------- 321 | `color` | string | | Color string you'd like to validate. See the [Color section](http://api.developer.lifx.com/docs/colors) to get more information. 322 | `cb` | function | null | `function(err, data) {}` Callback function which will be called when the HTTP request to the API was processed. 323 | 324 | **Usage example:** 325 | ```js 326 | // Using callbacks 327 | client.validateColor('#0198E1', function (err, data) { 328 | if (err) { 329 | console.error(err); 330 | return; 331 | } 332 | 333 | console.log(data) 334 | }); 335 | ``` 336 | 337 | ### Client API 338 | 339 | #### `client.getVersion()` 340 | Returns the api version. 341 | 342 | **Usage example:** 343 | ```js 344 | client.getVersion(); // outputs "v1" 345 | ``` 346 | 347 | #### `client.setVersion(version)` 348 | Sets the api version. Returns `true` if the version was set sucessfully, otherwise `false`. 349 | 350 | Option | Type | Default | Description 351 | ------ | ---- | ------- | ----------- 352 | `version` | string | | API version which will be used by the `Client` object. 353 | 354 | **Usage example:** 355 | ```js 356 | client.setVersion('v2beta'); 357 | ``` 358 | 359 | #### `client.getUrl()` 360 | Returns the api url. 361 | 362 | **Usage example:** 363 | ```js 364 | client.getUrl(); // outputs "https://lifx.com/api/" 365 | ``` 366 | 367 | #### `client.setUrl(url)` 368 | Sets the api url. Returns `true` if the url was set sucessfully, otherwise `false`. 369 | 370 | Option | Type | Default | Description 371 | ------ | ---- | ------- | ----------- 372 | `url` | string | | API url which will be used by the `Client` object. 373 | 374 | **Usage example:** 375 | ```js 376 | client.setUrl('https://my-lifx-api-url.com'); 377 | ``` 378 | 379 | #### `client.getApiUrl()` 380 | Returns the full Lifx api endpoint 381 | 382 | 383 | **Usage example:** 384 | ```js 385 | client.getApiUrl(); // outputs "https://lifx.com/api/v1" 386 | ``` 387 | 388 | #### `client.getBearerToken()` 389 | Returns the bearer authentication token. 390 | 391 | **Usage example:** 392 | ```js 393 | client.getBearerToken(); // outputs "" 394 | ``` 395 | 396 | #### `client.setBearerToken(token)` 397 | Sets the bearer authentication token. Returns `true` if the token was set sucessfully, otherwise `false`. 398 | 399 | Option | Type | Default | Description 400 | ------ | ---- | ------- | ----------- 401 | `token` | string | | Bearer authentication token which will be used by the `Client` object. 402 | 403 | **Usage example:** 404 | ```js 405 | client.setBearerToken(''); 406 | ``` 407 | 408 | #### `client.send(settings, cb)` 409 | Sends a request to the Lifx API. 410 | 411 | Option | Type | Default | Description 412 | ------ | ---- | ------- | ----------- 413 | `settings` | Object | `{}` | `request` configuration settings. See the [offical documentation](https://github.com/request/request) for further information. 414 | `cb` | function | null | `function(err, data) {}` Callback function which will be called when the HTTP request to the API was processed. 415 | 416 | **Usage example:** 417 | ```js 418 | client.send({ 419 | url: 'lights/all/state', 420 | body: { 421 | power: 'on', 422 | color: 'blue saturation:0.5', 423 | brightness: 0.5, 424 | duration: 5 425 | }, 426 | method: 'PUT' 427 | }, function(err, data) { 428 | if (err) console.error(err); 429 | else console.log(data); 430 | }) 431 | ``` 432 | 433 | ### Client settings 434 | 435 | The `Client` object can be configured at initialization: 436 | 437 | ```js 438 | var lifx = require('lifx-http-api'), 439 | client; 440 | 441 | client = new lifx({ 442 | bearerToken: '', // Authentication token 443 | version: 'v2beta', // API version 444 | url: 'https://api.lifx.com' // API endpoint 445 | }); 446 | ``` -------------------------------------------------------------------------------- /bearer-token.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/lifx.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('request'), 4 | utils = require('./utils'), 5 | _ = require('lodash'), 6 | Q = require('q'); 7 | 8 | /** 9 | * 10 | * Sets up the lifx http api client. 11 | * 12 | * @param settings 13 | * @returns {Client} 14 | * @constructor 15 | */ 16 | function Client(settings) { 17 | var defaults = { 18 | version: 'v1', 19 | url: 'https://api.lifx.com/', 20 | bearerToken: null 21 | }; 22 | settings = settings || {}; 23 | 24 | if (!settings.hasOwnProperty('bearerToken') || !settings.bearerToken.length) { 25 | throw new Error('Authentication token is required to use the API.'); 26 | } 27 | 28 | this.settings = _.merge(defaults, settings); 29 | 30 | return this; 31 | } 32 | 33 | /** 34 | * Returns the api version. 35 | * 36 | * @returns {string} 37 | */ 38 | Client.prototype.getVersion = function () { 39 | return this.settings.version; 40 | }; 41 | 42 | /** 43 | * Sets the api version. 44 | * 45 | * @param {string} version 46 | * @returns {boolean} 47 | */ 48 | Client.prototype.setVersion = function (version) { 49 | if (!version || !version.length) { 50 | return false; 51 | } 52 | 53 | this.settings.version = version; 54 | 55 | return true; 56 | }; 57 | 58 | /** 59 | * Returns the api url. 60 | * 61 | * @returns {string} 62 | */ 63 | Client.prototype.getUrl = function () { 64 | return this.settings.url; 65 | }; 66 | 67 | /** 68 | * Sets the api url. 69 | * 70 | * @param {string} url 71 | * @returns {boolean} 72 | */ 73 | Client.prototype.setUrl = function (url) { 74 | if (!url || !url.length) { 75 | return false; 76 | } 77 | this.settings.url = url; 78 | 79 | return true; 80 | }; 81 | 82 | /** 83 | * Returns the full api url 84 | * @returns {string} 85 | */ 86 | Client.prototype.getApiUrl = function () { 87 | return '' + this.settings.url + this.settings.version; 88 | }; 89 | 90 | /** 91 | * Returns the bearer token which is used to authenticate against the api. 92 | * @returns {null|string} 93 | */ 94 | Client.prototype.getBearerToken = function () { 95 | return this.settings.bearerToken; 96 | }; 97 | 98 | /** 99 | * Sets the bearer token 100 | * 101 | * @param {string} token 102 | * @returns {boolean} 103 | */ 104 | Client.prototype.setBearerToken = function (token) { 105 | if (!token || !token.length) { 106 | return false; 107 | } 108 | this.settings.bearerToken = token; 109 | 110 | return true; 111 | }; 112 | 113 | /** 114 | * Returns the client configuration 115 | * 116 | * @returns {object} 117 | */ 118 | Client.prototype.getSettings = function () { 119 | return this.settings; 120 | }; 121 | 122 | /** 123 | * Sends the actual request to the lifx api end point. 124 | * 125 | * @param {object} settings 126 | * @param {function} cb 127 | */ 128 | Client.prototype.send = function (settings, cb) { 129 | var defaults = { 130 | qs: {}, 131 | json: true, 132 | method: 'GET', 133 | baseUrl: this.getApiUrl(), 134 | headers: { 135 | Authorization: 'Bearer ' + this.getBearerToken() 136 | } 137 | }; 138 | 139 | settings = settings || {}; 140 | settings = _.merge(defaults, settings); 141 | 142 | request(settings, function (err, response, body) { 143 | var errors = utils.checkForErrors(response, body); 144 | if (err || errors.length) { 145 | cb(err || errors, null); 146 | return; 147 | } 148 | cb(null, body); 149 | }); 150 | }; 151 | 152 | /** 153 | * Lists all lights or specific lights depending on the selector. 154 | * 155 | * @param {string} selector 156 | * @param {function} cb 157 | * @returns {*} either the promise or nothing 158 | */ 159 | Client.prototype.listLights = function (selector, cb) { 160 | var deferred = Q.defer(); 161 | 162 | selector = selector || 'all'; 163 | if (!utils.verifySelector(selector)) { 164 | throw new Error('Selector is not valid.'); 165 | } 166 | 167 | this.send({ url: 'lights/' + selector }, function (err, data) { 168 | if (err) deferred.reject(err);else deferred.resolve(data); 169 | }); 170 | 171 | return deferred.promise.nodeify(cb); 172 | }; 173 | 174 | /** 175 | * Lists all available scenes 176 | * 177 | * @param {function} cb 178 | * @returns {*} either the promise or nothing 179 | */ 180 | Client.prototype.listScenes = function (cb) { 181 | var deferred = Q.defer(); 182 | 183 | this.send({ url: 'scenes' }, function (err, data) { 184 | if (err) deferred.reject(err);else deferred.resolve(data); 185 | }); 186 | 187 | return deferred.promise.nodeify(cb); 188 | }; 189 | 190 | /** 191 | * Sets a sets to all or a specific light. 192 | * 193 | * @param {string} selector 194 | * @param {object} settings 195 | * @param {function} cb 196 | * @returns {*} either the promise or nothing 197 | */ 198 | Client.prototype.setState = function (selector, settings, cb) { 199 | var deferred = Q.defer(); 200 | 201 | selector = selector || 'all'; 202 | if (!utils.verifySelector(selector)) { 203 | throw new Error('Selector is not valid.'); 204 | } 205 | 206 | settings = settings || {}; 207 | this.send({ 208 | url: 'lights/' + selector + '/state', 209 | body: settings, 210 | method: 'PUT' 211 | }, function (err, data) { 212 | if (err) deferred.reject(err);else deferred.resolve(data); 213 | }); 214 | 215 | return deferred.promise.nodeify(cb); 216 | }; 217 | 218 | /** 219 | * Sets multiple states at once. 220 | * 221 | * @param {object} settings 222 | * @param {function} cb 223 | * @returns {*} either the promise or nothing 224 | */ 225 | Client.prototype.setStates = function (settings, cb) { 226 | var deferred = Q.defer(); 227 | 228 | settings = settings || {}; 229 | if (!settings.hasOwnProperty('states')) { 230 | throw new Error('"states" is required.'); 231 | } 232 | 233 | this.send({ 234 | url: 'lights/states', 235 | body: settings, 236 | method: 'PUT' 237 | }, function (err, data) { 238 | if (err) deferred.reject(err);else deferred.resolve(data); 239 | }); 240 | 241 | return deferred.promise.nodeify(cb); 242 | }; 243 | 244 | /** 245 | * Toggles the power status of the light. 246 | * 247 | * @param {string} selector 248 | * @param {string} duration 249 | * @param {function} cb 250 | * @returns {*} either the promise or nothing 251 | */ 252 | Client.prototype.togglePower = function (selector, duration, cb) { 253 | var deferred = Q.defer(); 254 | 255 | selector = selector || 'all'; 256 | if (!utils.verifySelector(selector)) { 257 | throw new Error('Selector is not valid.'); 258 | } 259 | 260 | duration = duration || 0; 261 | this.send({ 262 | url: 'lights/' + selector + '/toggle', 263 | body: { duration: duration }, 264 | method: 'POST' 265 | }, function (err, data) { 266 | if (err) deferred.reject(err);else deferred.resolve(data); 267 | }); 268 | 269 | return deferred.promise.nodeify(cb); 270 | }; 271 | 272 | /** 273 | * Creates a breathe effect with the matching light bulbs. 274 | * 275 | * @param {string} selector 276 | * @param {object} settings 277 | * @param {function} cb 278 | * @returns {*} either the promise or nothing 279 | */ 280 | Client.prototype.breathe = function (selector, settings, cb) { 281 | var deferred = Q.defer(); 282 | 283 | selector = selector || 'all'; 284 | if (!utils.verifySelector(selector)) { 285 | throw new Error('Selector is not valid.'); 286 | } 287 | 288 | settings = settings || {}; 289 | if (!settings.hasOwnProperty('color')) { 290 | throw new Error('"color" is required.'); 291 | } 292 | 293 | this.send({ 294 | url: 'lights/' + selector + '/effects/breathe', 295 | body: settings, 296 | method: 'POST' 297 | }, function (err, data) { 298 | if (err) deferred.reject(err);else deferred.resolve(data); 299 | }); 300 | 301 | return deferred.promise.nodeify(cb); 302 | }; 303 | 304 | /** 305 | * Creates a pulse effect with the matching light bulbs. 306 | * 307 | * @param {string} selector 308 | * @param {object} settings 309 | * @param {function} cb 310 | * @returns {*} either the promise or nothing 311 | */ 312 | Client.prototype.pulse = function (selector, settings, cb) { 313 | var deferred = Q.defer(); 314 | 315 | selector = selector || 'all'; 316 | if (!utils.verifySelector(selector)) { 317 | throw new Error('Selector is not valid.'); 318 | } 319 | 320 | settings = settings || {}; 321 | if (!settings.hasOwnProperty('color')) { 322 | throw new Error('"color" is required.'); 323 | } 324 | 325 | this.send({ 326 | url: 'lights/' + selector + '/effects/pulse', 327 | body: settings, 328 | method: 'POST' 329 | }, function (err, data) { 330 | if (err) deferred.reject(err);else deferred.resolve(data); 331 | }); 332 | 333 | return deferred.promise.nodeify(cb); 334 | }; 335 | 336 | /** 337 | * Cycles through different defined states. 338 | * 339 | * @param {string} selector 340 | * @param {object} settings 341 | * @param {function} cb 342 | * @returns {*} either the promise or nothing 343 | */ 344 | Client.prototype.cycle = function (selector, settings, cb) { 345 | var deferred = Q.defer(); 346 | 347 | selector = selector || 'all'; 348 | if (!utils.verifySelector(selector)) { 349 | throw new Error('Selector is not valid.'); 350 | } 351 | 352 | settings = settings || {}; 353 | if (!settings.hasOwnProperty('states')) { 354 | throw new Error('"states" is required.'); 355 | } 356 | 357 | this.send({ 358 | url: 'lights/' + selector + '/cycle', 359 | body: settings, 360 | method: 'POST' 361 | }, function (err, data) { 362 | if (err) deferred.reject(err);else deferred.resolve(data); 363 | }); 364 | 365 | return deferred.promise.nodeify(cb); 366 | }; 367 | 368 | /** 369 | * Activates the specific scene to the specific light bulbs. 370 | * 371 | * @param {string} selector 372 | * @param {string} duration 373 | * @param {function} cb 374 | * @returns {*} either the promise or nothing 375 | */ 376 | Client.prototype.activateScene = function (selector, duration, cb) { 377 | var deferred = Q.defer(); 378 | 379 | selector = selector || 'all'; 380 | if (!utils.verifySelector(selector)) { 381 | throw new Error('Selector is not valid.'); 382 | } 383 | 384 | duration = duration || 0; 385 | this.send({ 386 | url: 'scenes/' + selector + '/activate', 387 | body: { duration: duration }, 388 | method: 'PUT' 389 | }, function (err, data) { 390 | if (err) deferred.reject(err);else deferred.resolve(data); 391 | }); 392 | 393 | return deferred.promise.nodeify(cb); 394 | }; 395 | 396 | /** 397 | * Validates any given color string against the api. The api returns the converted color as a response 398 | * 399 | * @param {string} color 400 | * @param {function} cb 401 | * @returns {*} either the promise or nothing 402 | */ 403 | Client.prototype.validateColor = function (color, cb) { 404 | var deferred = Q.defer(); 405 | 406 | color = color || ''; 407 | if (!color.length) { 408 | throw new Error('"color" is required.'); 409 | } 410 | 411 | this.send({ 412 | url: 'color', 413 | qs: { string: color }, 414 | method: 'GET' 415 | }, function (err, data) { 416 | if (err) deferred.reject(err);else deferred.resolve(data); 417 | }); 418 | 419 | return deferred.promise.nodeify(cb); 420 | }; 421 | 422 | module.exports = Client; 423 | -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | module.exports = { 6 | 7 | /** 8 | * Checks the HTTP response code and returns the according error message. 9 | * 10 | * See: 11 | * 12 | * @param {object} response 13 | * @param {string} body 14 | * @returns {string} 15 | */ 16 | checkForErrors: function checkForErrors(response, body) { 17 | var responseCodes = [{ codes: 400, message: 'Request was invalid.' }, { codes: 401, message: 'Bad access token.' }, { codes: 403, message: 'Bad OAuth scope.' }, { codes: 404, message: 'Selector did not match any lights.' }, { codes: 422, message: 'Missing or malformed parameters' }, { codes: 426, message: 'HTTP was used to make the request instead of HTTPS. Repeat the request using HTTPS instead.' }, { codes: 429, message: 'The request exceeded a rate limit.' }, { codes: [500, 502, 503, 523], message: 'Something went wrong on Lif\'s end.' }]; 18 | 19 | var error = ''; 20 | responseCodes.every(function (responseCode) { 21 | if (_.isArray(responseCode.codes)) { 22 | responseCode.codes.forEach(function (code) { 23 | if (code === response.statusCode) { 24 | error = responseCode.message; 25 | return false; 26 | } 27 | 28 | return true; 29 | }); 30 | } 31 | }); 32 | 33 | // Special case HTTP code 422 Unprocessable Entity 34 | if (response && response.statusCode === 422) { 35 | error = body.error; 36 | } 37 | 38 | return error; 39 | }, 40 | 41 | /** 42 | * Checks the light selector if it's valid. 43 | * 44 | * See: 45 | * 46 | * @param {string} selector 47 | * @returns {boolean} 48 | */ 49 | verifySelector: function verifySelector(selector) { 50 | var validSelectors = ['all', 'label:', 'id:', 'group_id:', 'group:', 'location_id:', 'location:', 'scene_id:'], 51 | isValid = false; 52 | 53 | if (!selector || !selector.length) { 54 | return false; 55 | } 56 | 57 | validSelectors.every(function (sel) { 58 | if (selector.startsWith(sel)) { 59 | isValid = true; 60 | 61 | return false; 62 | } 63 | 64 | return true; 65 | }); 66 | 67 | return isValid; 68 | } 69 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lifx-http-api", 3 | "version": "1.0.3", 4 | "description": "Thin wrapper around the Lifx HTTP API", 5 | "main": "dist/lifx.js", 6 | "scripts": { 7 | "prepublish": "npm run build", 8 | "build": "node_modules/.bin/babel source --presets babel-preset-es2015 --out-dir dist", 9 | "test": "npm run build && node_modules/.bin/istanbul cover -root dist/ node_modules/mocha/bin/_mocha --recursive test", 10 | "report-coverage": "node_modules/.bin/codecov" 11 | }, 12 | "keywords": [ 13 | "lifx", 14 | "http", 15 | "api", 16 | "promises", 17 | "light", 18 | "bulb" 19 | ], 20 | "author": { 21 | "name": "klarstil", 22 | "email": "klarstil@googlemail.com" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/klarstil/lifx-http-api" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/klarstil/lifx-http-api/issues" 30 | }, 31 | "license": "MIT", 32 | "dependencies": { 33 | "lodash": "^4.2.1", 34 | "q": "^1.4.1", 35 | "request": "^2.69.0" 36 | }, 37 | "devDependencies": { 38 | "babel-cli": "^6.4.5", 39 | "babel-preset-es2015": "^6.3.13", 40 | "chai": "^3.5.0", 41 | "chai-http": "^2.0.1", 42 | "codecov": "^1.0.1", 43 | "istanbul": "^0.4.2", 44 | "mocha": "^2.4.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/lifx.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | utils = require('./utils'), 3 | _ = require('lodash'), 4 | Q = require('q'); 5 | 6 | /** 7 | * 8 | * Sets up the lifx http api client. 9 | * 10 | * @param settings 11 | * @returns {Client} 12 | * @constructor 13 | */ 14 | function Client(settings) { 15 | var defaults = { 16 | version: 'v1', 17 | url: 'https://api.lifx.com/', 18 | bearerToken: null 19 | }; 20 | settings = settings || {}; 21 | 22 | if(!settings.hasOwnProperty('bearerToken') || !settings.bearerToken.length) { 23 | throw new Error('Authentication token is required to use the API.'); 24 | } 25 | 26 | this.settings = _.merge(defaults, settings); 27 | 28 | return this; 29 | } 30 | 31 | /** 32 | * Returns the api version. 33 | * 34 | * @returns {string} 35 | */ 36 | Client.prototype.getVersion = function() { 37 | return this.settings.version; 38 | }; 39 | 40 | /** 41 | * Sets the api version. 42 | * 43 | * @param {string} version 44 | * @returns {boolean} 45 | */ 46 | Client.prototype.setVersion = function(version) { 47 | if(!version || !version.length) { 48 | return false; 49 | } 50 | 51 | this.settings.version = version; 52 | 53 | return true; 54 | }; 55 | 56 | /** 57 | * Returns the api url. 58 | * 59 | * @returns {string} 60 | */ 61 | Client.prototype.getUrl = function() { 62 | return this.settings.url; 63 | }; 64 | 65 | /** 66 | * Sets the api url. 67 | * 68 | * @param {string} url 69 | * @returns {boolean} 70 | */ 71 | Client.prototype.setUrl = function(url) { 72 | if(!url || !url.length) { 73 | return false; 74 | } 75 | this.settings.url = url; 76 | 77 | return true; 78 | }; 79 | 80 | /** 81 | * Returns the full api url 82 | * @returns {string} 83 | */ 84 | Client.prototype.getApiUrl = function() { 85 | return `${this.settings.url}${this.settings.version}`; 86 | }; 87 | 88 | /** 89 | * Returns the bearer token which is used to authenticate against the api. 90 | * @returns {null|string} 91 | */ 92 | Client.prototype.getBearerToken = function() { 93 | return this.settings.bearerToken; 94 | }; 95 | 96 | /** 97 | * Sets the bearer token 98 | * 99 | * @param {string} token 100 | * @returns {boolean} 101 | */ 102 | Client.prototype.setBearerToken = function(token) { 103 | if(!token || !token.length) { 104 | return false; 105 | } 106 | this.settings.bearerToken = token; 107 | 108 | return true; 109 | }; 110 | 111 | /** 112 | * Returns the client configuration 113 | * 114 | * @returns {object} 115 | */ 116 | Client.prototype.getSettings = function() { 117 | return this.settings; 118 | }; 119 | 120 | /** 121 | * Sends the actual request to the lifx api end point. 122 | * 123 | * @param {object} settings 124 | * @param {function} cb 125 | */ 126 | Client.prototype.send = function(settings, cb) { 127 | var defaults = { 128 | qs: {}, 129 | json: true, 130 | method: 'GET', 131 | baseUrl: this.getApiUrl(), 132 | headers: { 133 | Authorization: `Bearer ${this.getBearerToken()}` 134 | } 135 | }; 136 | 137 | settings = settings || {}; 138 | settings = _.merge(defaults, settings); 139 | 140 | request(settings, function(err, response, body) { 141 | var errors = utils.checkForErrors(response, body); 142 | if(err || errors.length) { 143 | cb(err || errors, null); 144 | return; 145 | } 146 | cb(null, body); 147 | }); 148 | }; 149 | 150 | /** 151 | * Lists all lights or specific lights depending on the selector. 152 | * 153 | * @param {string} selector 154 | * @param {function} cb 155 | * @returns {*} either the promise or nothing 156 | */ 157 | Client.prototype.listLights = function(selector, cb) { 158 | var deferred = Q.defer(); 159 | 160 | selector = selector || 'all'; 161 | if(!utils.verifySelector(selector)) { 162 | throw new Error('Selector is not valid.'); 163 | } 164 | 165 | this.send({ url: 'lights/' + selector }, function(err, data) { 166 | if (err) deferred.reject(err); 167 | else deferred.resolve(data); 168 | }); 169 | 170 | return deferred.promise.nodeify(cb); 171 | }; 172 | 173 | /** 174 | * Lists all available scenes 175 | * 176 | * @param {function} cb 177 | * @returns {*} either the promise or nothing 178 | */ 179 | Client.prototype.listScenes = function(cb) { 180 | var deferred = Q.defer(); 181 | 182 | this.send({ url: 'scenes' }, function(err, data) { 183 | if (err) deferred.reject(err); 184 | else deferred.resolve(data); 185 | }); 186 | 187 | return deferred.promise.nodeify(cb); 188 | }; 189 | 190 | /** 191 | * Sets a sets to all or a specific light. 192 | * 193 | * @param {string} selector 194 | * @param {object} settings 195 | * @param {function} cb 196 | * @returns {*} either the promise or nothing 197 | */ 198 | Client.prototype.setState = function(selector, settings, cb) { 199 | var deferred = Q.defer(); 200 | 201 | selector = selector || 'all'; 202 | if(!utils.verifySelector(selector)) { 203 | throw new Error('Selector is not valid.'); 204 | } 205 | 206 | settings = settings || {}; 207 | this.send({ 208 | url: 'lights/' + selector + '/state', 209 | body: settings, 210 | method: 'PUT' 211 | }, function(err, data) { 212 | if (err) deferred.reject(err); 213 | else deferred.resolve(data); 214 | }); 215 | 216 | return deferred.promise.nodeify(cb); 217 | }; 218 | 219 | /** 220 | * Sets multiple states at once. 221 | * 222 | * @param {object} settings 223 | * @param {function} cb 224 | * @returns {*} either the promise or nothing 225 | */ 226 | Client.prototype.setStates = function(settings, cb) { 227 | var deferred = Q.defer(); 228 | 229 | settings = settings || {}; 230 | if(!settings.hasOwnProperty('states')) { 231 | throw new Error('"states" is required.') 232 | } 233 | 234 | this.send({ 235 | url: 'lights/states', 236 | body: settings, 237 | method: 'PUT' 238 | }, function(err, data) { 239 | if (err) deferred.reject(err); 240 | else deferred.resolve(data); 241 | }); 242 | 243 | return deferred.promise.nodeify(cb); 244 | }; 245 | 246 | /** 247 | * Toggles the power status of the light. 248 | * 249 | * @param {string} selector 250 | * @param {string} duration 251 | * @param {function} cb 252 | * @returns {*} either the promise or nothing 253 | */ 254 | Client.prototype.togglePower = function(selector, duration, cb) { 255 | var deferred = Q.defer(); 256 | 257 | selector = selector || 'all'; 258 | if(!utils.verifySelector(selector)) { 259 | throw new Error('Selector is not valid.'); 260 | } 261 | 262 | duration = duration || 0; 263 | this.send({ 264 | url: 'lights/' + selector + '/toggle', 265 | body: { duration: duration }, 266 | method: 'POST' 267 | }, function(err, data) { 268 | if (err) deferred.reject(err); 269 | else deferred.resolve(data); 270 | }); 271 | 272 | return deferred.promise.nodeify(cb); 273 | }; 274 | 275 | /** 276 | * Creates a breathe effect with the matching light bulbs. 277 | * 278 | * @param {string} selector 279 | * @param {object} settings 280 | * @param {function} cb 281 | * @returns {*} either the promise or nothing 282 | */ 283 | Client.prototype.breathe = function(selector, settings, cb) { 284 | var deferred = Q.defer(); 285 | 286 | selector = selector || 'all'; 287 | if(!utils.verifySelector(selector)) { 288 | throw new Error('Selector is not valid.'); 289 | } 290 | 291 | settings = settings || {}; 292 | if(!settings.hasOwnProperty('color')) { 293 | throw new Error('"color" is required.'); 294 | } 295 | 296 | this.send({ 297 | url: 'lights/' + selector + '/effects/breathe', 298 | body: settings, 299 | method: 'POST' 300 | }, function(err, data) { 301 | if (err) deferred.reject(err); 302 | else deferred.resolve(data); 303 | }); 304 | 305 | return deferred.promise.nodeify(cb); 306 | }; 307 | 308 | /** 309 | * Creates a pulse effect with the matching light bulbs. 310 | * 311 | * @param {string} selector 312 | * @param {object} settings 313 | * @param {function} cb 314 | * @returns {*} either the promise or nothing 315 | */ 316 | Client.prototype.pulse = function(selector, settings, cb) { 317 | var deferred = Q.defer(); 318 | 319 | selector = selector || 'all'; 320 | if(!utils.verifySelector(selector)) { 321 | throw new Error('Selector is not valid.'); 322 | } 323 | 324 | settings = settings || {}; 325 | if(!settings.hasOwnProperty('color')) { 326 | throw new Error('"color" is required.'); 327 | } 328 | 329 | this.send({ 330 | url: 'lights/' + selector + '/effects/pulse', 331 | body: settings, 332 | method: 'POST' 333 | }, function(err, data) { 334 | if (err) deferred.reject(err); 335 | else deferred.resolve(data); 336 | }); 337 | 338 | return deferred.promise.nodeify(cb); 339 | }; 340 | 341 | /** 342 | * Cycles through different defined states. 343 | * 344 | * @param {string} selector 345 | * @param {object} settings 346 | * @param {function} cb 347 | * @returns {*} either the promise or nothing 348 | */ 349 | Client.prototype.cycle = function(selector, settings, cb) { 350 | var deferred = Q.defer(); 351 | 352 | selector = selector || 'all'; 353 | if(!utils.verifySelector(selector)) { 354 | throw new Error('Selector is not valid.'); 355 | } 356 | 357 | settings = settings || {}; 358 | if(!settings.hasOwnProperty('states')) { 359 | throw new Error('"states" is required.'); 360 | } 361 | 362 | this.send({ 363 | url: 'lights/' + selector + '/cycle', 364 | body: settings, 365 | method: 'POST' 366 | }, function(err, data) { 367 | if (err) deferred.reject(err); 368 | else deferred.resolve(data); 369 | }); 370 | 371 | return deferred.promise.nodeify(cb); 372 | }; 373 | 374 | /** 375 | * Activates the specific scene to the specific light bulbs. 376 | * 377 | * @param {string} selector 378 | * @param {string} duration 379 | * @param {function} cb 380 | * @returns {*} either the promise or nothing 381 | */ 382 | Client.prototype.activateScene = function(selector, duration, cb) { 383 | var deferred = Q.defer(); 384 | 385 | selector = selector || 'all'; 386 | if(!utils.verifySelector(selector)) { 387 | throw new Error('Selector is not valid.'); 388 | } 389 | 390 | duration = duration || 0; 391 | this.send({ 392 | url: 'lights/' + selector + '/cycle', 393 | body: { duration: duration }, 394 | method: 'PUT' 395 | }, function(err, data) { 396 | if (err) deferred.reject(err); 397 | else deferred.resolve(data); 398 | }); 399 | 400 | return deferred.promise.nodeify(cb); 401 | }; 402 | 403 | /** 404 | * Validates any given color string against the api. The api returns the converted color as a response 405 | * 406 | * @param {string} color 407 | * @param {function} cb 408 | * @returns {*} either the promise or nothing 409 | */ 410 | Client.prototype.validateColor = function(color, cb) { 411 | var deferred = Q.defer(); 412 | 413 | color = color || ''; 414 | if(!color.length) { 415 | throw new Error('"color" is required.'); 416 | } 417 | 418 | this.send({ 419 | url: 'color', 420 | qs: { color: color }, 421 | method: 'PUT' 422 | }, function(err, data) { 423 | if (err) deferred.reject(err); 424 | else deferred.resolve(data); 425 | }); 426 | 427 | return deferred.promise.nodeify(cb); 428 | }; 429 | 430 | module.exports = Client; 431 | -------------------------------------------------------------------------------- /source/utils.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = { 4 | 5 | /** 6 | * Checks the HTTP response code and returns the according error message. 7 | * 8 | * See: 9 | * 10 | * @param {object} response 11 | * @param {string} body 12 | * @returns {string} 13 | */ 14 | checkForErrors: function(response, body) { 15 | var responseCodes = [ 16 | { codes: 400, message: 'Request was invalid.' }, 17 | { codes: 401, message: 'Bad access token.' }, 18 | { codes: 403, message: 'Bad OAuth scope.' }, 19 | { codes: 404, message: 'Selector did not match any lights.' }, 20 | { codes: 422, message: 'Missing or malformed parameters' }, 21 | { codes: 426, message: 'HTTP was used to make the request instead of HTTPS. Repeat the request using HTTPS instead.' }, 22 | { codes: 429, message: 'The request exceeded a rate limit.' }, 23 | { codes: [ 500, 502, 503, 523 ], message: 'Something went wrong on Lif\'s end.' } 24 | ]; 25 | 26 | var error = ''; 27 | responseCodes.every(function(responseCode) { 28 | if(_.isArray(responseCode.codes)) { 29 | responseCode.codes.forEach(function(code) { 30 | if (code === response.statusCode) { 31 | error = responseCode.message; 32 | return false; 33 | } 34 | 35 | return true; 36 | }); 37 | } 38 | }); 39 | 40 | // Special case HTTP code 422 Unprocessable Entity 41 | if(response && response.statusCode === 422) { 42 | error = body.error; 43 | } 44 | 45 | return error; 46 | }, 47 | 48 | /** 49 | * Checks the light selector if it's valid. 50 | * 51 | * See: 52 | * 53 | * @param {string} selector 54 | * @returns {boolean} 55 | */ 56 | verifySelector: function(selector) { 57 | var validSelectors = [ 58 | 'all', 59 | 'label:', 60 | 'id:', 61 | 'group_id:', 62 | 'group:', 63 | 'location_id:', 64 | 'location:', 65 | 'scene_id:' 66 | ], isValid = false; 67 | 68 | if(!selector || !selector.length) { 69 | return false; 70 | } 71 | 72 | validSelectors.every(function(sel) { 73 | if(selector.startsWith(sel)) { 74 | isValid = true; 75 | 76 | return false; 77 | } 78 | 79 | return true; 80 | }); 81 | 82 | return isValid; 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /test/test-lifx.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'), 4 | fs = require('fs'), 5 | chaiHttp = require('chai-http'), 6 | lifx = require('../dist/lifx'), 7 | utils = require('../dist/utils'), 8 | expect = chai.expect, 9 | client, token, 10 | process = require('process'); 11 | 12 | chai.use(chaiHttp); 13 | 14 | if (process.env.BEARER_TOKEN) { 15 | token = process.env.BEARER_TOKEN; 16 | } else { 17 | token = fs.readFileSync(__dirname + '/../bearer-token.txt', 'utf8'); 18 | } 19 | 20 | describe('Lifx HTTP API wrapper', function() { 21 | describe('Client initialization', function() { 22 | it('should create a client with a token', function() { 23 | client = new lifx({ 24 | bearerToken: token 25 | }); 26 | 27 | expect(client).to.be.an.instanceof(lifx); 28 | }); 29 | 30 | it('should not create a client without a token', function() { 31 | expect(function() { 32 | client = new lifx(); 33 | }).to.throw(Error); 34 | }); 35 | 36 | it('should create a client with a custom config', function() { 37 | var opts = { 38 | bearerToken: token, 39 | version: 'v1beta', 40 | url: 'http://google' 41 | }; 42 | client = new lifx(opts); 43 | 44 | expect(client.settings.version).to.equal(opts.version); 45 | expect(client.settings.url).to.equal(opts.url); 46 | expect(client.settings.bearerToken).to.equal(opts.bearerToken); 47 | }); 48 | 49 | it('should be possible to set & get a version', function() { 50 | var data = 'someVersion'; 51 | 52 | client = new lifx({ 53 | bearerToken: token 54 | }); 55 | 56 | expect(client.setVersion(data)).to.be.true; 57 | expect(client.setVersion()).to.be.false; 58 | expect(client.getVersion()).to.equal(data); 59 | }); 60 | 61 | it('should be possible to set a url', function() { 62 | var data = 'someUrl'; 63 | 64 | client = new lifx({ 65 | bearerToken: token 66 | }); 67 | 68 | expect(client.setUrl(data)).to.be.true; 69 | expect(client.setUrl()).to.be.false; 70 | expect(client.getUrl()).to.equal(data); 71 | }); 72 | 73 | it('should be possible to set & get a bearer token', function() { 74 | var data = 'ashiodhsaio3122asad3123d'; 75 | 76 | client = new lifx({ 77 | bearerToken: token 78 | }); 79 | 80 | expect(client.setBearerToken(data)).to.be.true; 81 | expect(client.setBearerToken()).to.be.false; 82 | expect(client.getBearerToken()).to.equal(data); 83 | }); 84 | 85 | it('should be possible to get the api url', function() { 86 | client = new lifx({ 87 | bearerToken: token 88 | }); 89 | 90 | expect(client.getApiUrl()).to.equal('https://api.lifx.com/v1'); 91 | }); 92 | 93 | it('should be possible to get the client settings', function() { 94 | var opts = { 95 | bearerToken: 'token', 96 | version: 'v1beta', 97 | url: 'http://google' 98 | }; 99 | 100 | client = new lifx(opts); 101 | 102 | expect(client.getSettings()).to.have.keys([ 'bearerToken', 'version', 'url' ]); 103 | }); 104 | }); 105 | 106 | describe('Utils tests', function() { 107 | it('should validate selectors', function() { 108 | 109 | expect(utils.verifySelector('all')).to.be.true; 110 | expect(utils.verifySelector('label:test')).to.be.true; 111 | expect(utils.verifySelector('label:test test')).to.be.true; 112 | expect(utils.verifySelector('group_id:asd3212easdx')).to.be.true; 113 | expect(utils.verifySelector('group:test')).to.be.true; 114 | expect(utils.verifySelector('group:test test')).to.be.true; 115 | expect(utils.verifySelector('location_id:asd3212easdx')).to.be.true; 116 | expect(utils.verifySelector('location:test')).to.be.true; 117 | expect(utils.verifySelector('location:test test')).to.be.true; 118 | expect(utils.verifySelector('scene_id:asd3212easdx')).to.be.true; 119 | 120 | expect(utils.verifySelector('test')).to.be.false; 121 | expect(utils.verifySelector('test:label')).to.be.false; 122 | }) 123 | }); 124 | 125 | describe('Client integration', function() { 126 | it('should get all available lights', function() { 127 | client = new lifx({ 128 | bearerToken: token 129 | }); 130 | 131 | expect(function() { 132 | client.listLights('testing'); 133 | }).to.throw(Error); 134 | 135 | client.listLights('all', function(data) { 136 | var light = data[0]; 137 | 138 | expect(light).to.have.keys([ 139 | 'id', 'uuid', 'label', 'connected', 'power', 'color', 'brightness', 'group', 'location', 140 | 'last_seen', 'seconds_since_seen', 'product' 141 | ]); 142 | }); 143 | }); 144 | 145 | it('should get all available scenes', function() { 146 | client = new lifx({ 147 | bearerToken: token 148 | }); 149 | 150 | client.listScenes(function(data) { 151 | var scene = data[0]; 152 | 153 | expect(scene).to.have.keys([ 154 | 'uuid', 'name', 'account', 'states', 'created_at', 'updated_at' 155 | ]); 156 | }); 157 | }); 158 | 159 | it('should validate a color', function() { 160 | client = new lifx({ 161 | bearerToken: token 162 | }); 163 | 164 | expect(function() { 165 | client.validateColor(); 166 | }).to.throw(Error); 167 | 168 | client.validateColor('#ff000f', function(data) { 169 | expect(data).to.have.keys([ 170 | 'hue', 'saturation', 'brightness', 'kelvin' 171 | ]); 172 | }); 173 | }); 174 | 175 | it('should toggle the power state', function() { 176 | client = new lifx({ 177 | bearerToken: token 178 | }); 179 | 180 | expect(function() { 181 | client.setState('testing'); 182 | }).to.throw(Error); 183 | 184 | client.togglePower('all', function(data) { 185 | expect(data.result).to.be.instanceof(Array); 186 | 187 | var result = data.result[0]; 188 | 189 | expect(result).to.have.keys([ 190 | 'id', 'label', 'status' 191 | ]); 192 | }); 193 | }); 194 | 195 | it('should set the state', function() { 196 | client = new lifx({ 197 | bearerToken: token 198 | }); 199 | 200 | expect(function() { 201 | client.setState('testing'); 202 | }).to.throw(Error); 203 | 204 | client.setState('all', { 205 | power: 'on' 206 | }, function(data) { 207 | expect(data.result).to.be.instanceof(Array); 208 | 209 | var result = data.result[0]; 210 | 211 | expect(result).to.have.keys([ 212 | 'id', 'label', 'status' 213 | ]); 214 | }); 215 | }); 216 | }); 217 | }); 218 | --------------------------------------------------------------------------------