diff --git a/Changelog.md b/Changelog.md index fc29a3b..131fd6b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,13 @@ # Change Log +## 0.2.7 +- Added functions `nupnpSearch` and `upnpSearch` for bridge discovery and deprecated old search function references +- Updated the Groups API and documentation to support latest Hue Bridge software version +- `LightGroup 0` name now provided from the bridge, rather than called `All Lights` +- Provided separate functions for the different types of groups that are now possible in Bridge API version 1.4+ +- Added advanced option to specify the port number for the bridge +- Added convenience `getVersion` function to obtain software and API versions of the bridge + ## 0.2.6 - Fixes a bug introduced in 0.2.5 that would remove the rgb state value from a LightState object thereby making different to what was originally set if using it in multiple `setLightState()` calls diff --git a/README.md b/README.md index 9d1b4df..33237ec 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,6 @@ swallowed silently. For a list of changes, please refer to the change log; [Changes](Changelog.md) -Please note that a number of breaking changes have occurred in moving from version 0.1.x to 0.2.x, but these were necessary to -provide more consistency in the API and to ensure that moving forward the library will be able to better adjust to changes -in the firmware of the Phillips Hue Bridge. - ## Work In Progress There is still some work to be done around completing the ability to define schedules in a better way that properly @@ -59,6 +55,7 @@ The offical Hue documentation recommends an approach to finding bridges by using to find your bridges on the network. This API library provided you with both options, but leaves it to the developer to decide on the approach to be used, i.e. fallback, parallel, or just one type. + #### nupnpSearch() or locateBridges() This API function makes use of the official API endpoint that reveals the bridges on a network. It is a call through to ``http://meethue.com/api/nupnp`` which may not work in all circumstances (your bridge must have signed into the methue portal), @@ -118,7 +115,7 @@ Hue Bridges Found: [{"id":"001788096103","ipaddress":"192.168.2.129"}] ### Registering a new Device/User with the Bridge -Once you have discovered the IP Address for your bridge (either from the locate/search function, or looking it up on the +Once you have discovered the IP Address for your bridge (either from the UPnP/N-UPnP function, or looking it up on the Philips Hue website), then you will need to register your application with the Hue Bridge. Registration requires you to issue a request to the Bridge after pressing the Link Button on the Bridge (although you can @@ -159,35 +156,51 @@ This will provide results detailing the configuration of the bridge (IP Address, ``` { "name": "Philips hue", - "mac": "00:x:xx:xx:xx:xx", - "dhcp": true, - "ipaddress": "192.168.2.129", - "netmask": "255.255.255.0", - "gateway": "192.168.2.1", - "proxyaddress": "none", - "proxyport": 0, - "UTC": "2013-06-15T13:20:08", - "whitelist": { - "51780342fd7746f2fb4e65c30b91d7": { - "last use date": "2013-05-29T20:29:51", - "create date": "2013-05-29T20:29:51", - "name": "Node.js API" + "zigbeechannel": 11, + "mac": "xx:xx:xx:xx:xx:xx", + "dhcp": false, + "ipaddress": "192.168.2.245", + "netmask": "255.255.255.0", + "gateway": "192.168.2.254", + "proxyaddress": "none", + "proxyport": 0, + "UTC": "2015-01-10T13:18:51", + "localtime": "2015-01-10T13:18:51", + "timezone": "Europe/London", + "whitelist": { + "fG2EZIaS2pZuSeKH": { + "last use date": "2015-01-09T22:54:21", + "create date": "2014-05-18T17:11:10", + "name": "philips.lighting.hue#iPad" + }, + "0f607264fc6318a92b9e13c65db7cd3c": { + "last use date": "2014-12-23T17:25:16", + "create date": "2014-12-23T17:14:30", + "name": "iPad" + } }, - "08a902b95915cdd9b75547cb50892dc4": { - "last use date": "1987-01-06T22:53:37", - "create date": "2013-04-02T13:39:18", - "name": "Node Hue Api Tests User" + "swversion": "01018228", + "apiversion": "1.5.0", + "swupdate": { + "updatestate": 0, + "checkforupdate": false, + "devicetypes": { + "bridge": false, + "lights": [] + }, + "url": "", + "text": "", + "notify": false + }, + "linkbutton": false, + "portalservices": true, + "portalconnection": "connected", + "portalstate": { + "signedon": true, + "incoming": true, + "outgoing": true, + "communication": "connected" } - }, - "swversion": "01005825", - "swupdate": { - "updatestate": 0, - "url": "", - "text": "", - "notify": false - }, - "linkbutton": false, - "portalservices": true } ``` @@ -204,6 +217,46 @@ look for a field that is not present in the above result, like the ``mac``, ``ip properties to check. +### Software and API Version +The version of the software and API for the bridge is available from the `config` function, but out of convenience there +is also a `getVersion` function which filters the `config` return data to just give you the version details. + +```js +var HueApi = require("node-hue-api").HueApi; + +var displayResult = function(result) { + console.log(JSON.stringify(result, null, 2)); +}; + +var hostname = "192.168.2.129", + username = "08a902b95915cdd9b75547cb50892dc4", + api; + +api = new HueApi(hostname, username); + +// -------------------------- +// Using a promise +api.getVersion().then(displayResult).done(); + +// -------------------------- +// Using a callback +api.getVersion(function(err, config) { + if (err) throw err; + displayResult(config); +}); +``` + +This will result in data output as follows; +``` +{ + "name": "Philips hue", + "version": { + "api": "1.5.0", + "software": "01018228" + } +} +``` + ### Registering without an existing Device/User ID A user can be registered on the Bridge using ``registerUser()`` or ``createUser()`` functions. This is useful when you have not got an existing user account on the Bridge to use to access its protected functions. @@ -856,11 +909,10 @@ If the call is successful, then ``true`` will be returned by the function call, ## Working with Groups -The Groups API for the Phillips Hue Bridge is not complete at this time, with some of API endpoints not officially -supported yet. This API does attempt to provide functions to invoke these end points, but in testing, some of them have -been identified as being problematic, in that they report success, but nothing on the actual Bridge changes. In most of -these cases, restarting the Bridge (pulling the power cable) resulted in the calls working again for a short period of -time. Your mileage may vary if you are creating and modifying these newly created groups... +The Hue Bridge can support groups of lights so that you can do things like setting a colour and status to a group +of lights instead of just a single light. + +There is a special "All Lights" Group with an id of `0` that is defined in the bridge that a user cannot modify. ### Obtaining all Groups from the Bridge To obtain all the groups defined in the bridge use the __groups()__ function; @@ -897,22 +949,39 @@ This will produce an array of values detailing the id and names of the groups; [ { "id": "0", - "name": "All Lights" + "name": "Lightset 0", + "type": "LightGroup" }, { "id": "1", - "name": "VRC 1" + "name": "VRC 1", + "type": "LightGroup", + "lights": [1, 2, 3, 4, 5] } ] ``` -The "All Lights" Group is a special instance and will always exist and have the id of "0" as specified in the Hue Api -documentation. +The "Lightset 0" Group is a special instance and will always exist and have the id of "0" as specified in the Hue Api +documentation. Due to this internal group being maintained by the bridge internally, it will not return an array of light +ids like any of the other types of Groups. + +The `groups` function will return all types of Groups in the bridge, these include new types of groups that support the +new [Hue Beyond|http://www2.meethue.com/en-us/the-range/hue-beyond]. + +To support the addition of these new types of groups, and the fact that most users will only want a subset of the types +there are now three new functions that will filter the types of groups for you; +* `luminaries` Will obtain only the *Luminarie* groups (i.e. a collection of lights that make up a single device). These are not user modifiable. +* `lightSources` Will obtain the *Lightsource* groups (i.e. a subset of the lights in a Luminarie). These are not user modifiable. +* `lightGroups` Will obtain the defined groups in the bridge ### Obtaining the Details of a Group Definition -To obtain the details of the lights that make up a group (and some extra information like the last action that was performed) +To get the specific details of the lights that make up a group (and some extra information like the last action that was performed) use the __getGroup(id)__ function. +In Hue Bridge API version 1.4+ the full data for the group will be returned when obtaining all groups via the `groups` +or `lightGroups` functions. The only exception to this is the special All Lights Group, id 0, which requires the use of +a specific lookup to obtain the full details. + ```js var HueApi = require("node-hue-api").HueApi; @@ -942,7 +1011,7 @@ Which will return produce a result like; ``` { "id": "0", - "name": "All Lights", + "name": "Lightset 0", "lights": [ "1", "2", @@ -950,6 +1019,7 @@ Which will return produce a result like; "4", "5" ], + "type": "LightGroup", "lastAction": { "on": true, "bri": 128, @@ -971,23 +1041,10 @@ A function ``setGroupLightState()`` exists for interacting with a group of light particular state. This function is identical to that of the ``setLightState()`` function above, except that it works on groups instead of a single light. -In the early versions of this library the group and individual lights were controlled via a single ``setLightState()`` -function, but this has been removed from version _0.2.x_ as it was not clear that a single boolean changed the target for the -function invocation which felt wrong. - - -### Updating a Group -It is possible to update the associated lights and the name of a group after it has been created on the bridge. The function -``updateGroup()`` allows you to do this. -You can set the name, the lightIds or both with this function, just omit what you do not want to set, it will work out which -parameter was passed based on type, a String for the name and an array for the light ids. - -When invoking this function ``true`` will be returned if the Bridge accepts the requested change, but under some circumstances -if the group has just been created, then Bridge reports success, but does not actually change the configuration details. In these -cases, a restart of the Bridge might resolve the issue. +### Create a New Group +To create a new group use the __createGroup(name, lightIds)__ function; -Changing the name of an existing group; ```js var HueApi = require("node-hue-api").HueApi; @@ -999,23 +1056,42 @@ var host = "192.168.2.129", username = "08a902b95915cdd9b75547cb50892dc4", api = new HueApi(host, username); -// Update the name of the group +// Create a new Group on the bridge // -------------------------- // Using a promise -api.updateGroup(1, "new group name") +api.createGroup("a new group", [4, 5]) .then(displayResults) .done(); // -------------------------- // Using a callback -api.updateGroup(1, "new group name", function(err, result){ +api.createGroup("group name", [1, 4, 5], function(err, result){ if (err) throw err; displayResults(result); }); ``` -Changing the lights associated with an existing group; +The function will return a promise with a result that contains the id of the newly created group; +``` +{ + "id": "2" +} +``` + + +### Updating a Group +It is possible to update the associated lights and the name of a group after it has been created on the bridge. The function +``updateGroup()`` allows you to do this. + +You can set the name, the lightIds or both with this function, just omit what you do not want to set, it will work out which +parameter was passed based on type, a String for the name and an array for the light ids. + +When invoking this function ``true`` will be returned if the Bridge accepts the requested change. +It can take take a short period of time before the bridge will actually reflect the change requested, in experience 1.5 +seconds has always covered the necessary time to effect the change, but it could be quicker than that. + +Changing the name of an existing group; ```js var HueApi = require("node-hue-api").HueApi; @@ -1027,23 +1103,23 @@ var host = "192.168.2.129", username = "08a902b95915cdd9b75547cb50892dc4", api = new HueApi(host, username); -// Update the lights in the group to ids 1, 2, and 3. +// Update the name of the group // -------------------------- // Using a promise -api.updateGroup(1, [1, 2, 3]) +api.updateGroup(1, "new group name") .then(displayResults) .done(); // -------------------------- // Using a callback -api.updateGroup(1, [1, 2, 3], function(err, result){ +api.updateGroup(1, "new group name", function(err, result){ if (err) throw err; displayResults(result); }); ``` -Changing both the name and the lights for an existing group; +Changing the lights associated with an existing group; ```js var HueApi = require("node-hue-api").HueApi; @@ -1055,29 +1131,23 @@ var host = "192.168.2.129", username = "08a902b95915cdd9b75547cb50892dc4", api = new HueApi(host, username); -// Update both the name and the lights in the group to ids 4, 5. +// Update the lights in the group to ids 1, 2, and 3. // -------------------------- // Using a promise -api.updateGroup(1, "group name", [4, 5]) +api.updateGroup(1, [1, 2, 3]) .then(displayResults) .done(); // -------------------------- // Using a callback -api.updateGroup(1, "group name", [4, 5], function(err, result){ +api.updateGroup(1, [1, 2, 3], function(err, result){ if (err) throw err; displayResults(result); }); ``` - -### Create a New Group -The creation of groups is not officially supported in the released Hue API from Phillips (version 1.0). This has been -tested on a Hue Bridge, but use at your own risk *(you may have to reset the bridge to factory defaults if something goes wrong)*. - -To create a new group use the __createGroup(name, lightIds)__ function; - +Changing both the name and the lights for an existing group; ```js var HueApi = require("node-hue-api").HueApi; @@ -1089,29 +1159,22 @@ var host = "192.168.2.129", username = "08a902b95915cdd9b75547cb50892dc4", api = new HueApi(host, username); -// Create a new Group on the bridge +// Update both the name and the lights in the group to ids 4, 5. // -------------------------- // Using a promise -api.createGroup("a new group", [4, 5]) +api.updateGroup(1, "group name", [4, 5]) .then(displayResults) .done(); // -------------------------- // Using a callback -api.createGroup("group name", [1, 4, 5], function(err, result){ +api.updateGroup(1, "group name", [4, 5], function(err, result){ if (err) throw err; displayResults(result); }); ``` -The function will return a promise with a result that contains the id of the newly created group; -``` -{ - "id": "2" -} -``` - ### Deleting a Group The deletion of groups is not officially supported in the released Hue API from Phillips (version 1.0), but it is still @@ -1467,6 +1530,8 @@ as in the case if the schedule does not exist. ## Advanced Options +### Timeouts + If there are issues with the Bridge not responding in time for a result of error to be delivered, then you may need to tweak the timeout settings for the API. When this happens you will get an `ETIMEOUT` error. @@ -1485,10 +1550,31 @@ var host = "192.168.2.129", api = new HueApi(host, password, timeout); ``` -The default timeout, when onw is not specified will be 10000ms (10 seconds). This is usually enough time for the bridge +The default timeout, when not specified will be 10000ms (10 seconds). This is usually enough time for the bridge to respond unless you are returning a very large result (like the complete state for the bridge in a large installation) +### Bridge Port Number + +If you are running your bridge over a router or using some kind of NAT, it may be possible that the Hue Bridge is not +running on the default port. If this is the case, then you can set the port number as an advanced configuration option +when creating the API connection to the bridge. + +*Please note that for normal usage, you should never set the port value.* + +```js +var hue = require("node-hue-api"), + HueApi = hue.HueApi; + +var host = "192.168.2.129", + username = "08a902b95915cdd9b75547cb50892dc4", + timeout = 20000 // timeout in milliseconds, + port = 8080 // not the default port for the bridge, + api; + +api = new HueApi(host, password, timeout, port); +``` + ## License Copyright 2013. All Rights Reserved. diff --git a/hue-api/commands/groups-api.js b/hue-api/commands/groups-api.js index a41a35e..397418c 100644 --- a/hue-api/commands/groups-api.js +++ b/hue-api/commands/groups-api.js @@ -1,7 +1,7 @@ "use strict"; // -// The Documented Phillips Hue Bridge API for groups http://developers.meethue.com/2_groupssapi.html +// The Documented Phillips Hue Bridge API for groups http://www.developers.meethue.com/documentation/groups-api // // This module wraps up all the functionality for the definition and basic processing of the parameters for the API // so that it can be called from the httpPromise module. @@ -10,35 +10,36 @@ // Hue API documentation, than having it scatter piece meal through various other classes and functions. // -var Trait = require("traits").Trait, - tApiMethod = require("./traits/tApiMethod"), - tDescription = require("./traits/tDescription"), - tBodyArguments = require("./traits/tBodyArguments"), - tLightStateBody = require("./traits/tLightStateBody"), - tPostProcessing = require("./traits/tPostProcessing"), - ApiError = require("../errors").ApiError, - utils = require("../utils"), - ALL_LIGHTS_NAME = "All Lights", - apiTraits = {}; +var Trait = require("traits").Trait + , tApiMethod = require("./traits/tApiMethod") + , tDescription = require("./traits/tDescription") + , tBodyArguments = require("./traits/tBodyArguments") + , tLightStateBody = require("./traits/tLightStateBody") + , tPostProcessing = require("./traits/tPostProcessing") + , tErrorHandling = require("./traits/tErrorHandling") + , ApiError = require("../errors").ApiError + , utils = require("../utils") + ; + +var ALL_LIGHTS_NAME = "Lightset 0" + , apiTraits = {} + ; apiTraits.getAllGroups = Trait.compose( tApiMethod("/api//groups", - "GET", - "1.0", - "Whitelist" + "GET", + "1.0", + "Whitelist" ), - tDescription("Gets a list of all groups that have been added to the bridge. A group is a list of lights that " + - "can be created, modified and deleted by a user. The maximum numbers of groups is 16. N.B. For " + - "the first bridge firmware release, bridge software version 01003542 only, a limited number of " + - "these APIs are supported in the firmware so only control of groups/0 is supported."), - tPostProcessing(_processAllGroups) + tDescription("Gets a list of all groups that have been added to the bridge. A group is a list of lights that can be created, modified and deleted by a user. The maximum numbers of groups is 16. N.B. For the first bridge firmware release, bridge software version 01003542 only, a limited number of these APIs are supported in the firmware so only control of groups/0 is supported."), + tPostProcessing(processAllGroups) ); apiTraits.getGroupAttributes = Trait.compose( tApiMethod("/api//groups/", - "GET", - "1.0", - "Whitelist" + "GET", + "1.0", + "Whitelist" ), tDescription("Gets the name, light membership and last command for a given group.") // tPostProcessing(_processGroupResult) // Cannot use this as we need to inject the id we were called with @@ -46,9 +47,9 @@ apiTraits.getGroupAttributes = Trait.compose( apiTraits.setGroupAttributes = Trait.compose( tApiMethod("/api//groups/", - "PUT", - "1.0", - "Whitelist" + "PUT", + "1.0", + "Whitelist" ), tDescription("Allows the user to modify the name and light membership of a group."), tBodyArguments( @@ -58,29 +59,29 @@ apiTraits.setGroupAttributes = Trait.compose( {"name": "lights", "type": "list int", "optional": true} ] ), - tPostProcessing(_ensureSuccessful) + tPostProcessing(ensureSuccessful) ); apiTraits.setGroupState = Trait.compose( tApiMethod("/api//groups//action", - "PUT", - "1.0", - "Whitelist" + "PUT", + "1.0", + "Whitelist" ), tDescription("Modifies the state of all lights in a group"), tLightStateBody(), - tPostProcessing(_processSetLightStateResult) + tPostProcessing(processSetLightStateResult) ); -// In version 1.0 of the Phillips Hue API this is not officially supported and has been reverse engineered from -// tinkering with the api end points... apiTraits.createGroup = Trait.compose( tApiMethod("/api//groups", - "POST", - "0.0", - "Whitelist" + "POST", + "1.0", + "Whitelist" ), - tDescription("Creates a new Group. This endPoint has been reverse engineered and is not officially supported by Phillips Hue."), + tDescription("Creates a new group containing the lights specified and optional name. A new group " + + "is created in the bridge with the next available id. Note: For bridges < 1.4 lights must be on, otherwise " + + "they won't be saved in the group. For 1.4 there is no such restriction."), tBodyArguments( "application/json", [ @@ -88,28 +89,28 @@ apiTraits.createGroup = Trait.compose( {"name": "lights", "type": "list int", "optional": false} ] ), - tPostProcessing(_processCreateGroup) + tPostProcessing(processCreateGroup) ); apiTraits.deleteGroup = Trait.compose( tApiMethod("/api//groups/", - "DELETE", - "0.0", - "Whitelist" + "DELETE", + "1.0", + "Whitelist" ), - tDescription("Deletes a Group. This endPoint has been reverse engineered and is not officially supported by Phillips Hue."), - tPostProcessing(_ensureSuccessful) + tDescription("Deletes the specified group from the bridge."), + tPostProcessing(ensureSuccessful), + tErrorHandling({305: "It is not allowed to update or delete group of this type"}) ); module.exports = { - "getAllGroups" : Trait.create(Object.prototype, apiTraits.getAllGroups), + "getAllGroups": Trait.create(Object.prototype, apiTraits.getAllGroups), "getGroupAttributes": Trait.create(Object.prototype, apiTraits.getGroupAttributes), "setGroupAttributes": Trait.create(Object.prototype, apiTraits.setGroupAttributes), - "setGroupState" : Trait.create(Object.prototype, apiTraits.setGroupState), - "createGroup" : Trait.create(Object.prototype, apiTraits.createGroup), - "deleteGroup" : Trait.create(Object.prototype, apiTraits.deleteGroup), - "NAME_ALL_LIGHTS" : ALL_LIGHTS_NAME + "setGroupState": Trait.create(Object.prototype, apiTraits.setGroupState), + "createGroup": Trait.create(Object.prototype, apiTraits.createGroup), + "deleteGroup": Trait.create(Object.prototype, apiTraits.deleteGroup), }; /** @@ -118,25 +119,29 @@ module.exports = { * @returns {Array} An array of groups {"id": {*}, "name": {*}} known to the bridge. * @private */ -function _processAllGroups(result) { +function processAllGroups(result) { var groupArray = []; // There is an implicit all lights group that is not returned in the results of the lookup, so explicitly add it - groupArray.push({"id": "0", "name": ALL_LIGHTS_NAME}); + groupArray.push({"id": "0", "name": ALL_LIGHTS_NAME, type: "LightGroup"}); Object.keys(result).forEach(function (value) { + var group = result[value]; + groupArray.push({ - "id" : value, - "name": result[value].name - }); + "id": value, + "name": group.name, + "type": group.type, + "lights": group.lights + }); }); return groupArray; } -function _processCreateGroup (result) { +function processCreateGroup(result) { var idString; - _ensureSuccessful(result); + ensureSuccessful(result); idString = result[0].success.id; idString = idString.substr(idString.lastIndexOf("/") + 1); @@ -144,31 +149,16 @@ function _processCreateGroup (result) { return {"id": idString}; } -function _ensureSuccessful(result) { -// console.log(JSON.stringify(result)); +function ensureSuccessful(result) { if (!utils.wasSuccessful(result)) { throw new ApiError(utils.parseErrors(result).join(", ")); } return true; } -function _processSetLightStateResult(result) { +function processSetLightStateResult(result) { if (!utils.wasSuccessful(result)) { throw new ApiError(utils.parseErrors(result).join(", ")); } return true; -} - -//function _processGroupResult(group) { -// //TODO the group id is injected via the call of the promise -// var result = { -// "id" : String(id), -// // Inject our "known name" for the all lights group if necessary -// "name" : id === 0 ? ALL_LIGHTS_NAME : group.name, -// "lights" : group.lights, -// "lastAction": group.action -// }; -// -// //TODO Api has a placeholder for scenes which are currently not used -// return result; -//} \ No newline at end of file +} \ No newline at end of file diff --git a/hue-api/commands/traits/tErrorHandling.js b/hue-api/commands/traits/tErrorHandling.js new file mode 100644 index 0000000..a30169f --- /dev/null +++ b/hue-api/commands/traits/tErrorHandling.js @@ -0,0 +1,16 @@ +"use strict"; + +var util = require("util") + , Trait = require("traits").Trait + , ApiError = require("../../errors").ApiError + ; + +module.exports = function (codeMap) { + if (!codeMap) { + throw new ApiError("A status code to error messages object must be provided"); + } + + return Trait({ + "statusCodeMap": codeMap + }); +}; diff --git a/hue-api/commands/traits/tPostProcessing.js b/hue-api/commands/traits/tPostProcessing.js index 412deea..8903b58 100644 --- a/hue-api/commands/traits/tPostProcessing.js +++ b/hue-api/commands/traits/tPostProcessing.js @@ -1,20 +1,37 @@ "use strict"; -var Trait = require("traits").Trait, - ApiError = require("../../errors").ApiError; +var util = require("util") + , Trait = require("traits").Trait + , ApiError = require("../../errors").ApiError + ; module.exports = function (fn) { - if (!fn) { - throw new ApiError("A post processing function must be provided"); - } + var processingFunctions; - if (typeof fn !== "function") { - throw new ApiError("The post processing function must be a function; " + typeof fn ); + if (arguments.length === 0) { + throw new ApiError("At least one post processing functions must be provided"); } - return Trait( - { - "postProcessingFn" : fn - } - ); + processingFunctions = validateFunction(Array.prototype.slice.call(arguments)); + + return Trait({ + "postProcessing": processingFunctions + }); }; + +function validateFunction(fn) { + var result = []; + + if (util.isArray(fn)) { + fn.forEach(function (actualFn) { + result = result.concat(validateFunction(actualFn)); + }); + } else { + if (typeof fn !== "function") { + throw new ApiError("The post processing function must be a function; " + typeof fn); + } + result.push(fn); + } + + return result; +} diff --git a/hue-api/httpPromise.js b/hue-api/httpPromise.js index bea09b6..66a677a 100644 --- a/hue-api/httpPromise.js +++ b/hue-api/httpPromise.js @@ -15,21 +15,29 @@ function _invoke(command, parameters) { , promise ; - promise = requestUtil.request(options) + promise = requestUtil.request(options); + + if (command.statusCodeMap) { + promise = promise.then(generateErrorsIfMatched(command.statusCodeMap)); + } + + promise = promise .then(_requireStatusCode200) .then(function (requestResult) { - var result; - - if (options.json) { - result = _checkForError(requestResult); - } else { - result = requestResult.data; - } - return result; - }); - - if (command.postProcessingFn) { - promise = promise.then(command.postProcessingFn); + var result; + + if (options.json) { + result = _checkForError(requestResult); + } else { + result = requestResult.data; + } + return result; + }); + + if (command.postProcessing) { + command.postProcessing.forEach(function (fn) { + promise = promise.then(fn); + }); } return promise; @@ -43,13 +51,9 @@ function _buildOptions(command, parameters) { hostname: parameters.host }; -// if (parameters.host) { -// hostAndPort = parameters.host.split(":"); -// urlObj.host = hostAndPort[0]; -// urlObj.port = hostAndPort[1] || "80"; -// } else { -// throw new Error("A host name must be provided in the parameters"); -// } + if (parameters.port) { + urlObj.port = parameters.port; + } options.timeout = parameters.timeout || 10000; options.method = command.method || "GET"; @@ -105,9 +109,9 @@ function _getError(jsonObject) { } } else if (jsonObject.error) { return { - "type" : jsonObject.error.type, + "type": jsonObject.error.type, "description": jsonObject.error.description, - "address" : jsonObject.error.address + "address": jsonObject.error.address }; } } @@ -132,10 +136,23 @@ function _requireStatusCode200(result) { if (result.statusCode !== 200) { throw new errors.ApiError( { - type : "Response Error", - description: "Unexpected response status; " + result.statusCode + type: "Response Error", + description: "Unexpected response status; " + result.statusCode, + statusCode: result.statusCode } ); } return result; +} + +function generateErrorsIfMatched(map) { + return function(result) { + if (map && map[result.statusCode]) { + throw new errors.ApiError({ + type: result.statusCode, + message: map[result.statusCode] + }); + } + return result; + }; } \ No newline at end of file diff --git a/hue-api/index.js b/hue-api/index.js index bff5138..a4761af 100644 --- a/hue-api/index.js +++ b/hue-api/index.js @@ -14,13 +14,40 @@ var Q = require("q") ; -function HueApi(host, username, timeout) { +function HueApi(host, username, timeout, port) { this.host = host; this.username = username; this.timeout = timeout || 10000; + + // By default users should not specify this and just leave undefined + if (port) { + this.port = port; + } } module.exports = HueApi; +/** + * Gets the version data for the Philips Hue Bridge. + * + * @param cb An optional callback function if you don't want to be informed via a promise. + * @returns {Q.promise} A promise will be provided that will resolve to the version data for the bridge, or {null} if a + * callback was provided. + */ +HueApi.prototype.getVersion = function (cb) { + var promise = this.config() + .then(function (data) { + return { + name: data.name, + version: { + api: data.apiversion, + software: data.swversion + } + }; + }); + + return utils.promiseOrCallback(promise, cb); +} + /** * Loads the description for the Philips Hue. * @@ -60,7 +87,7 @@ HueApi.prototype.connect = HueApi.prototype.config; * @param cb An optional callback function if you don't want to be informed via a promise. * @returns {Q.promise} A promise with the result, or {null} if a callback function was provided */ -HueApi.prototype.getFullState = function(cb) { +HueApi.prototype.getFullState = function (cb) { var options = _defaultOptions(this), promise = http.invoke(configurationApi.getFullState, options); @@ -178,9 +205,9 @@ HueApi.prototype.registeredUsers = function (cb) { device = list[key]; devices.push( { - "name" : device.name, + "name": device.name, "username": key, - "created" : device["create date"], + "created": device["create date"], "accessed": device["last use date"] } ); @@ -308,11 +335,11 @@ HueApi.prototype.setLightState = function (id, stateValues, cb) { if (stateValues.rgb) { promise = this.lightStatus(id) - .then(function(lightDetails) { + .then(function (lightDetails) { options.values.xy = rgb.convertRGBtoXY(stateValues.rgb, lightDetails); delete options.values.rgb; }) - .then(function() { + .then(function () { return http.invoke(lightsApi.setLightState, options); }) ; @@ -353,7 +380,7 @@ HueApi.prototype.setGroupLightState = function (id, stateValues, cb) { * Obtains all the groups from the Hue Bridge as an Array of {id: {*}, name: {*}} objects. * * @param cb An optional callback function to use if you do not want to use a promise for the results. - * @return A promise that will set the specified state on the light, or {null} if a callback was provided. + * @return A promise that will obtain the groups, or {null} if a callback was provided. */ HueApi.prototype.groups = function (cb) { var options = _defaultOptions(this), @@ -363,6 +390,42 @@ HueApi.prototype.groups = function (cb) { }; +/** + * Obtains all the Luminaries from the Hue Bridge as an Array of {id: {*}, name: {*}} objects. + * + * @param cb An optional callback function to use if you do not want to use a promise for the results. + * @return A promise that will obtain the luminaries, or {null} if a callback was provided. + */ +HueApi.prototype.luminaires = function (cb) { + var promise = this._filterGroups("Luminaire"); + return utils.promiseOrCallback(promise, cb); +}; + + +/** + * Obtains all the LightSources from the Hue Bridge as an Array of {id: {*}, name: {*}} objects. + * + * @param cb An optional callback function to use if you do not want to use a promise for the results. + * @return A promise that will obtain the lightsources, or {null} if a callback was provided. + */ +HueApi.prototype.lightSources = function (cb) { + var promise = this._filterGroups("Lightsource"); + return utils.promiseOrCallback(promise, cb); +}; + + +/** + * Obtains all the LightGroups from the Hue Bridge as an Array of {id: {*}, name: {*}} objects. + * + * @param cb An optional callback function to use if you do not want to use a promise for the results. + * @return A promise that will obtain the LightGroups, or {null} if a callback was provided. + */ +HueApi.prototype.lightGroups = function (cb) { + var promise = this._filterGroups("LightGroup"); + return utils.promiseOrCallback(promise, cb); +}; + + /** * Obtains the details for a specified group in a format of {id: {*}, name: {*}, lights: [], lastAction: {*}}. * @@ -376,17 +439,19 @@ HueApi.prototype.getGroup = function (id, cb) { //TODO find a way to make this a normal post processing action in the groups-api, the id from the call needs to be injected... function processGroupResult(group) { - return { - "id" : String(id), - - // Inject our "known name" for the all lights group if necessary - "name": id === 0 ? groupsApi.NAME_ALL_LIGHTS : group.name, - - "lights" : group.lights, + var result = { + "id": String(id), + "name": group.name, + "type": group.type, + "lights": group.lights, "lastAction": group.action - - // Hue Api has a placeholder for scenes which are currently not used. }; + + if (group.type === "Luminaire" && group.modelid) { + result.modelid = group.modelid; + } + + return result; } promise = _setGroupIdOption(options, id); @@ -423,6 +488,8 @@ HueApi.prototype.updateGroup = function (id, name, lightIds, cb) { cb = param; } else if (Array.isArray(param)) { options.values.lights = utils.createStringValueArray(param); + } else if (param === undefined || param === null) { + // Ignore it } else { options.values.name = param; } @@ -457,7 +524,7 @@ HueApi.prototype.createGroup = function (name, lightIds, cb) { promise; options.values = { - name : name, + name: name, lights: utils.createStringValueArray(lightIds) }; @@ -474,9 +541,6 @@ HueApi.prototype.createGroup = function (name, lightIds, cb) { * @return {*} A promise that will return if the deletion was successful, or null if a callback was provided. */ HueApi.prototype.deleteGroup = function (id, cb) { - // In version 1.0 of the Phillips Hue API this is not officially supported and has been reverse engineered from - // tinkering with the api end points... - var options = _defaultOptions(this), promise = _setGroupIdOptionForModification(options, id); @@ -599,6 +663,25 @@ HueApi.prototype.updateSchedule = function (id, schedule, cb) { // PRIVATE METHODS //////////////////////////////////////////////////////////////////////////////////////////////// +HueApi.prototype._filterGroups = function (type) { + var self = this; + + return self.groups() + .then(function (groups) { + var results = []; + + if (groups) { + groups.forEach(function (group) { + if (group.type === type) { + results.push(group); + } + }) + } + + return results; + }); +}; + /** * Creates a new schedule in the Hue Bridge. * @@ -640,7 +723,7 @@ function _setCreateUserOptions(options, username, deviceType) { } options.values = { - "username" : validatedUsername, + "username": validatedUsername, "devicetype": deviceType || "Node.js API" }; @@ -865,11 +948,17 @@ function _errorPromise(message) { * @private */ function _defaultOptions(api) { - return { - "host" : api.host, + var result = { + "host": api.host, "username": api.username, "timeout": api.timeout }; + + if (api.port) { + result.port = api.port; + } + + return result; } /** diff --git a/package.json b/package.json index 9f3c803..f90a56e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-hue-api", - "version": "0.2.6", + "version": "0.2.7", "author": "Peter Murray ", "contributors": [ { diff --git a/test/getConfiguration-test.js b/test/getConfiguration-test.js index 7498f46..2db2752 100644 --- a/test/getConfiguration-test.js +++ b/test/getConfiguration-test.js @@ -9,9 +9,9 @@ var HueApi = require("../hue-api"), describe("Hue API", function () { - describe("#config", function () { + var hue = new HueApi(testValues.host, testValues.username); - var hue = new HueApi(testValues.host, testValues.username); + describe("#config", function () { function validateConfigResults(results) { expect(results).to.be.an.instanceOf(Object); @@ -21,22 +21,40 @@ describe("Hue API", function () { expect(results).to.have.property("dhcp"); expect(results).to.have.property("ipaddress").to.equal(testValues.host); expect(results).to.have.property("gateway"); + expect(results).to.have.property("proxyaddress"); expect(results).to.have.property("proxyport"); + expect(results).to.have.property("UTC"); + expect(results).to.have.property("localtime"); + expect(results).to.have.property("timezone"); + expect(results).to.have.property("whitelist"); + expect(results).to.have.property("swversion"); + expect(results).to.have.property("apiversion"); + expect(results).to.have.property("swupdate"); + expect(results.swupdate).to.have.property("updatestate"); + expect(results.swupdate).to.have.property("checkforupdate"); + expect(results).to.have.property("linkbutton"); + expect(results).to.have.property("portalservices"); + expect(results).to.have.property("portalconnection"); + + expect(results).to.have.property("portalstate"); + expect(results.portalstate).to.have.property("signedon"); + expect(results.portalstate).to.have.property("incoming"); + expect(results.portalstate).to.have.property("outgoing"); } it("using #promise", function (finished) { hue.config() .then(function (results) { - validateConfigResults(results); - finished(); - }) + validateConfigResults(results); + finished(); + }) .done(); }); @@ -49,4 +67,34 @@ describe("Hue API", function () { }); }); }); + + describe("#getVersion", function () { + + function validateVersionResults(results) { + expect(results).to.be.an.instanceOf(Object); + + expect(results).to.have.property("name"); + expect(results).to.have.property("version"); + expect(results.version).to.have.property("api", testValues.version.api); + expect(results.version).to.have.property("software", testValues.version.software); + } + + it("using #promise", function (done) { + hue.getVersion() + .then(function (results) { + validateVersionResults(results); + done(); + }) + .done(); + }); + + it("using #callback", function (done) { + hue.getVersion(function (err, results) { + expect(err).to.be.null; + + validateVersionResults(results); + done(); + }); + }); + }); }); \ No newline at end of file diff --git a/test/groups-test.js b/test/groups-test.js index c6efabb..0c7c4ba 100644 --- a/test/groups-test.js +++ b/test/groups-test.js @@ -1,11 +1,12 @@ "use strict"; -var HueApi = require("../hue-api"), - ApiError = require("../hue-api/errors").ApiError, - testValues = require("./support/testValues.js"), - lightState = require("../").lightState, - expect = require("chai").expect, - Q = require("q"); +var HueApi = require("../hue-api") + , ApiError = require("../hue-api/errors").ApiError + , testValues = require("./support/testValues.js") + , lightState = require("../").lightState + , expect = require("chai").expect + , Q = require("q") + ; describe("Hue API", function () { @@ -24,8 +25,9 @@ describe("Hue API", function () { expect(results).to.have.length.greaterThan(0); // The first result should always be that of the all lights group - expect(results[0].id).to.equal("0"); - expect(results[0].name).to.equal("All Lights"); + expect(results[0]).to.have.property("id", "0"); + expect(results[0]).to.have.property("name", "Lightset 0"); + expect(results[0]).to.have.property("type", "LightGroup"); } it("using #promise should retrieve all groups", function (finished) { @@ -46,12 +48,85 @@ describe("Hue API", function () { }); }); + describe("#luminaries", function () { + + it("using #promise", function (done) { + hue.luminaires() + .then(function (data) { + expect(data).to.be.an.instanceOf(Array); + expect(data).to.be.empty; + done(); + }) + .done(); + }); + + it("using #callback", function (done) { + hue.luminaires(function (err, data) { + expect(err).to.be.null; + expect(data).to.be.an.instanceOf(Array); + expect(data).to.be.empty; + done(); + }); + }); + }); + + describe("#lightSources", function () { + + it("using #promise", function (done) { + hue.lightSources() + .then(function (data) { + expect(data).to.be.an.instanceOf(Array); + expect(data).to.be.empty; + done(); + }) + .done(); + }); + + it("using #callback", function (done) { + hue.lightSources(function (err, data) { + expect(err).to.be.null; + expect(data).to.be.an.instanceOf(Array); + expect(data).to.be.empty; + done(); + }); + }); + }); + + describe("#lightGroups", function () { + + it("using #promise", function (done) { + hue.lightGroups() + .then(function (data) { + expect(data).to.be.an.instanceOf(Array); + expect(data.length).to.be.at.least(1); + + expect(data[0]).to.have.property("id", "0"); + expect(data[0]).to.have.property("name", "Lightset 0"); + done(); + }) + .done(); + }); + + it("using #callback", function (done) { + hue.lightGroups(function (err, data) { + expect(err).to.be.null; + + expect(data).to.be.an.instanceOf(Array); + expect(data.length).to.be.at.least(1); + + expect(data[0]).to.have.property("id", "0"); + expect(data[0]).to.have.property("name", "Lightset 0"); + done(); + }); + }); + }); + describe("#getGroup", function () { function validateAllLightsResult(groupDetails) { - expect(groupDetails).to.have.property("id").to.equal("0"); - expect(groupDetails).to.have.property("name").to.equal("All Lights"); + expect(groupDetails).to.have.property("id", "0"); + expect(groupDetails).to.have.property("name", "Lightset 0"); expect(groupDetails).to.have.property("lastAction"); expect(groupDetails).to.have.property("lights").to.be.instanceOf(Array); @@ -59,7 +134,7 @@ describe("Hue API", function () { } function failTest() { - expect.fail("Should not be called"); + throw new Error("Should not be called"); } describe("using #promise", function () { @@ -162,8 +237,7 @@ describe("Hue API", function () { expect(group).to.have.property("name").to.have.string(groupName); expect(group).to.have.property("lights").to.be.instanceOf(Array); - //TODO these do not always populate correctly due to bug int he bridge -// expect(group.lights).to.contain("1", "2", "3"); + expect(group.lights).to.contain("1", "2", "3"); } function deleteGroup() { @@ -173,11 +247,11 @@ describe("Hue API", function () { function validateDeletion() { return hue.getGroup(createdGroupId) .then(function () { - expect.fail("Should not be called"); - }) + throw new Error("Should not be called"); + }) .fail(function (err) { - validateMissingGroup(err); - }); + validateMissingGroup(err); + }); } function complete() { @@ -223,119 +297,140 @@ describe("Hue API", function () { }); - //TODO these have been disabled due to quirks in the bridge in the way that groups are created and updated there after... describe("#updateGroup", function () { -// var origName = "UpdateTests", -// origLights = ["1", "2"], -// groupId; -// -// // Create a group to test on -// beforeEach(function (finished) { -// -// hue.createGroup(origName, origLights) -// .then(function (result) { -// groupId = result.id; -// finished(); -// }) -// .done(); -// }); -// -// // Remove the created group after each test -// afterEach(function (finished) { -// -// hue.deleteGroup(groupId) -// .then(function () { -// finished(); -// }) -// .done(); -// }); -// -// function getGroup() { -// return hue.getGroup(groupId); -// } -// -// -// describe("using #promise", function () { -// -// it("should update only the name of a group", function (finished) { -// function validateRename(details) { -// // Name changed -// expect(details.name).to.have.string("promiseRename"); -// // Lights Unchanged -// expect(details.lights).to.have.length(2); -// expect(details.lights).to.contain("1", "2"); -// } -// -// hue.updateGroup(groupId, "promiseRename") -// .then(_waitForBridge) -// .then(getGroup) -// .then(validateRename) -// .then(finished) -// .done(); -// }); -// -// it("should update only the lights in a group", function (finished) { -// function validateLightsChange(details) { -// // Name unchanged -// expect(details.name).to.have.string(origName); -// // Lights changed -// expect(details.lights).to.have.length(2); -// expect(details.lights).to.contain("1", "4"); -// } -// -// hue.updateGroup(groupId, ["1", "4"]) -// .then(_waitForBridge) -// .then(getGroup) -// .then(validateLightsChange) -// .then(finished) -// .done(); -// }); -// -// it("should update name and lights in a group", function (finished) { -// function validateUpdate(details) { -// expect(details.name).to.have.string("pSecondRename"); -// -// expect(details.lights).to.have.length(2); -// expect(details.lights).to.contain("4", "5"); -// } -// -// hue.updateGroup(groupId, "pSecondRename", ["4", "5"]) -// .then(_waitForBridge) -// .then(getGroup) -// .then(validateUpdate) -// .then(finished) -// .done(); -// }); -// }); -// -// -// describe("using #callback", function () { -// //TODO duplicate above tests -// -// it("should update only the name of a group", function (finished) { -// function validateRename(details) { -// // Name changed -// expect(details.name).to.have.string("promiseRename"); -// // Lights Unchanged -// expect(details.lights).to.have.length(2); -// expect(details.lights).to.contain("1", "2"); -// } -// -// hue.updateGroup(groupId, "promiseRename", function (err, result) { -// expect(err).to.be.null; -// -// setTimeout(function () { -// hue.getGroup(groupId, function (err, group) { -// expect(err).to.be.null; -// console.log(JSON.stringify(group, null, 2)); -// validateRename(group); -// finished(); -// }); -// }, 2000); -// }); -// }); -// }); + var origName = "UpdateTests" + , origLights = ["1", "2"] + , updateName = "renamedGroupName" + , updateLights = ["3", "4", "5"] + , groupId + ; + + // Create a group to test on + beforeEach(function (finished) { + + hue.createGroup(origName, origLights) + .then(function (result) { + groupId = result.id; + finished(); + }) + .done(); + }); + + // Remove the created group after each test + afterEach(function (finished) { + + hue.deleteGroup(groupId) + .then(function () { + finished(); + }) + .done(); + }); + + function getGroup() { + return hue.getGroup(groupId); + } + + function validateGroup(expectedName, expectedLights) { + return function (details) { + // Name + expect(details).to.have.property("name", expectedName); + + // Lights + expect(details.lights).to.have.length(expectedLights.length); + expect(details.lights).to.have.members(expectedLights); + }; + } + + describe("using #promise", function () { + + it("should update only the name of a group", function (finished) { + hue.updateGroup(groupId, updateName) + .then(_waitForBridge) + .then(getGroup) + .then(validateGroup(updateName, origLights)) + .then(finished) + .done(); + }); + + it("should update only the lights in a group", function (finished) { + hue.updateGroup(groupId, updateLights) + .then(_waitForBridge) + .then(getGroup) + .then(validateGroup(origName, updateLights)) + .then(finished) + .done(); + }); + + it("should update name and lights in a group", function (finished) { + hue.updateGroup(groupId, updateName, updateLights) + .then(_waitForBridge) + .then(getGroup) + .then(validateGroup(updateName, updateLights)) + .then(finished) + .done(); + }); + }); + + + describe("using #callback", function () { + + it("should update only the name of a group", function (finished) { + hue.updateGroup(groupId, updateName, + function (err, result) { + expect(err).to.be.null; + expect(result).to.be.true; + + _waitForBridge() + .then(getGroup) + .then(validateGroup(updateName, origLights)) + .then(finished) + .done(); + }); + }); + + it("should update only the lights in a group", function (finished) { + hue.updateGroup(groupId, updateLights, + function (err, result) { + expect(err).to.be.null; + expect(result).to.be.true; + + _waitForBridge() + .then(getGroup) + .then(validateGroup(origName, updateLights)) + .then(finished) + .done(); + }); + }); + + it("should update name and lights in a group", function (finished) { + hue.updateGroup(groupId, updateName, updateLights, + function (err, result) { + expect(err).to.be.null; + expect(result).to.be.true; + + _waitForBridge() + .then(getGroup) + .then(validateGroup(updateName, updateLights)) + .then(finished) + .done(); + }); + }); + + it("should update the name if the lights are null", function(finished) { + hue.updateGroup(groupId, updateName, null, + function (err, result) { + expect(err).to.be.null; + expect(result).to.be.true; + + _waitForBridge() + .then(getGroup) + .then(validateGroup(updateName, origLights)) + .then(finished) + .done(); + }); + }); + }); }); describe("#setGroupLightState", function () { @@ -343,14 +438,14 @@ describe("Hue API", function () { it("using #promise", function (finished) { hue.setGroupLightState(0, lightState.create().off()) .then(function (result) { - expect(result).to.be.true; - finished(); - }) + expect(result).to.be.true; + finished(); + }) .done(); }); it("using #callback", function (finished) { - hue.setGroupLightState(0, lightState.create().off(), function(err, result) { + hue.setGroupLightState(0, lightState.create().off(), function (err, result) { expect(err).to.be.null; expect(result).to.be.true; finished(); @@ -358,31 +453,26 @@ describe("Hue API", function () { }); }); -// TODO include these tests -// -// describe("#updateGroup", function () { -// -// it("should fail on invalid group", function (finished) { -// var failIfCalled = function () { -// expect.fail("The function call should have produced an error for invalid group id"); -// finished(); -// }, -// -// checkError = function (err) { -// expect(err.type).to.equal(3); -// expect(err.message).to.contain("resource,"); -// expect(err.message).to.contain("not available"); -// finished(); -// }; -// -// hue.updateGroup(99, "a name") -// .then(_waitForBridge) -// .then(failIfCalled) -// .fail(checkError) -// .done(); -// }); -// }); + describe("#updateGroup", function () { + + it("should fail on invalid group id", function (finished) { + var failIfCalled = function () { + throw new Error("The function call should have produced an error for invalid group id"); + } + , checkError = function (err) { + console.error(JSON.stringify(err)); + expect(err).to.have.property("type", 0); + expect(err.message).to.contain("cannot be modified"); + finished(); + }; + hue.updateGroup(99, "a name") + .then(_waitForBridge) + .then(failIfCalled) + .fail(checkError) + .done(); + }); + }); }); }); @@ -391,9 +481,9 @@ function _waitForBridge(id) { var deferred = Q.defer(); setTimeout(function () { - deferred.resolve(id); - }, - 1500); + deferred.resolve(id); + }, + 1500); return deferred.promise; } diff --git a/test/scheduledEvent-tests.js b/test/scheduledEvent-tests.js index 9924820..82be02b 100644 --- a/test/scheduledEvent-tests.js +++ b/test/scheduledEvent-tests.js @@ -52,7 +52,7 @@ describe("ScheduleEvent", function () { it("should not accept invalid date strings", function () { try { scheduledEvent.on("1995-00-00T00:00:00"); - expect.fail("should have got a parsing error"); + throw new Error("should have got a parsing error"); } catch (error) { if (error instanceof ApiError) { diff --git a/test/support/testValues.js b/test/support/testValues.js index ea4f767..de0478a 100644 --- a/test/support/testValues.js +++ b/test/support/testValues.js @@ -1,11 +1,19 @@ module.exports = { host : "192.168.2.245", username : "08a902b95915cdd9b75547cb50892dc4", + lightsCount : 16, + locateTimeout: 7000, maxScheduleNameLength: 32, + testLightId: 6, hueLightId: 6, luxLightId: 15, - livingColorLightId: 1 + livingColorLightId: 1, + + version: { + api: "1.5.0", + software: "01018228" + } }; \ No newline at end of file