From 1d05905667f454dd74c26d069613c9201f27747f Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 18 Nov 2019 15:54:39 +0000 Subject: [PATCH 01/35] Preparing of 4.x release - Refactoring of Type/Parameter system so that it can be reused for all bridge Objects - Refactoring of Bridge Model classes to use type system and flatten strucutre. - Removal of ability to directly access the Model objects and instead drive all creation and population via the v3.model module. - Scenes tidied up - Consistent create + get implementation when creating new objects on the Bridge - Added ResourceLink API support - Documentation and Example updates --- .gitignore | 5 +- Changelog.md | 50 +++ README.md | 8 +- docs/resourceLink.md | 146 +++++++ docs/resourcelinks.md | 141 +++++++ docs/ruleAction.md | 4 +- docs/ruleCondition.md | 14 +- docs/rules.md | 7 +- docs/scene.md | 147 ++++--- docs/scenes.md | 10 +- docs/sensor.md | 124 +++--- docs/sensors.md | 22 +- .../createAndDeleteResourceLink.js | 42 ++ .../v3/resourceLinks/getAllResourceLinks.js | 29 ++ examples/v3/resourceLinks/getResourceLink.js | 30 ++ .../v3/resourceLinks/updateResourceLink.js | 51 +++ examples/v3/rules/createRule.js | 14 +- examples/v3/scenes/createScene.js | 19 +- examples/v3/sensors/createNewSensor.js | 23 +- examples/v3/sensors/creatingClipSensors.js | 137 +++--- examples/v3/sensors/updateSensorState.js | 5 +- hue-api/LightStateShim.js | 2 +- hue-api/SceneBuilder.js | 20 +- hue-api/ScheduledEventBuilder.js | 4 +- hue-api/index.js | 2 +- lib/HueError.js | 26 +- lib/api/Api.js | 2 + lib/api/Configuration.test.js | 2 + lib/api/Groups.js | 32 +- lib/api/Groups.test.js | 23 +- lib/api/Lights.test.js | 43 +- lib/api/ResourceLinks.js | 45 ++ lib/api/ResourceLinks.test.js | 170 ++++++++ lib/api/Rules.js | 8 +- lib/api/Rules.test.js | 61 +-- lib/api/Scenes.js | 9 +- lib/api/Scenes.test.js | 40 +- lib/api/Schedules.js | 1 + lib/api/Sensors.js | 9 +- lib/api/Sensors.test.js | 50 +-- lib/api/Users.test.js | 2 +- lib/api/http/endpoints/groups.js | 139 ++----- lib/api/http/endpoints/lights.js | 30 +- lib/api/http/endpoints/resourcelinks.js | 131 ++++++ lib/api/http/endpoints/rules.js | 96 ++--- lib/api/http/endpoints/scenes.js | 54 ++- lib/api/http/endpoints/schedules.js | 6 +- lib/api/http/endpoints/sensors.js | 40 +- .../http/placeholders/GroupIdPlaceholder.js | 6 +- .../http/placeholders/LightIdPlaceholder.js | 7 +- .../http/placeholders/NumberPlaceholder.js | 28 -- lib/api/http/placeholders/PlaceHolder.js | 30 -- lib/api/http/placeholders/Placeholder.js | 43 ++ .../placeholders/ResourceLinkPlaceholder.js | 14 + .../http/placeholders/RuleIdPlaceholder.js | 6 +- .../http/placeholders/SceneIdPlaceholder.js | 6 +- .../placeholders/ScheduleIdPlaceholder.js | 6 +- .../http/placeholders/SensorIdPlaceholder.js | 6 +- .../http/placeholders/StringPlaceholder.js | 22 - .../http/placeholders/UsernamePlaceholder.js | 14 +- lib/api/http/util.js | 18 + lib/api/index.js | 7 - lib/api/index.test.js | 9 - lib/api/stateCache.js | 5 +- lib/bridge-model/BridgeObject.js | 67 --- lib/bridge-model/BridgeObjectWithNumberId.js | 10 - lib/bridge-model/Group.js | 63 --- lib/bridge-model/Scene.js | 214 ---------- lib/bridge-model/devices/Device.js | 26 -- lib/bridge-model/devices/lights/ColorLight.js | 12 - .../devices/lights/ColorTemperatureLight.js | 12 - .../devices/lights/DimmableLight.js | 13 - .../devices/lights/ExtendedColorLight.js | 12 - lib/bridge-model/devices/lights/Light.js | 110 ----- lib/bridge-model/devices/lights/OnOffLight.js | 12 - lib/bridge-model/devices/lights/index.js | 44 -- .../devices/sensors/CLIPCommon.js | 46 --- .../devices/sensors/CLIPGenericFlag.js | 22 - .../devices/sensors/CLIPGenericStatus.js | 22 - .../devices/sensors/CLIPHumidity.js | 21 - .../devices/sensors/CLIPLightlevel.js | 64 --- .../devices/sensors/CLIPOpenClose.js | 20 - .../devices/sensors/CLIPPresence.js | 20 - .../devices/sensors/CLIPSensor.js | 17 - .../devices/sensors/CLIPSwitch.js | 20 - .../devices/sensors/CLIPTemperature.js | 20 - lib/bridge-model/devices/sensors/Daylight.js | 75 ---- lib/bridge-model/devices/sensors/Sensor.js | 78 ---- .../devices/sensors/ZLLLightLevel.js | 63 --- .../devices/sensors/ZLLPresence.js | 73 ---- lib/bridge-model/devices/sensors/ZLLSwitch.js | 53 --- .../devices/sensors/ZLLTemperature.js | 14 - lib/bridge-model/devices/sensors/index.js | 50 --- lib/bridge-model/index.js | 15 - lib/bridge-model/rules/index.js | 54 --- lib/model/BridgeObject.js | 146 +++++++ lib/model/Group.js | 144 +++++++ lib/model/Light.js | 179 ++++++++ lib/model/ResourceLink.js | 209 ++++++++++ lib/model/ResourceLink.test.js | 101 +++++ lib/{bridge-model => model}/Schedule.js | 47 ++- lib/model/Schedule.test.js | 15 + .../color-gamuts.js => model/colorGamuts.js} | 0 .../datetime/AbsoluteTime.js | 0 .../datetime/AbsoluteTime.test.js | 0 .../datetime/BridgeTime.js | 0 .../datetime/DateTimeUtil.js | 0 .../datetime/HueDate.js | 0 .../datetime/HueDate.test.js | 0 .../datetime/HueTime.js | 0 .../datetime/HueTime.test.js | 0 .../datetime/RandomizedTime.js | 0 .../datetime/RandomizedTimer.js | 0 .../datetime/RecurringRandomizedTime.js | 0 .../datetime/RecurringRandomizedTimer.js | 0 .../datetime/RecurringTime.js | 0 .../datetime/RecurringTimer.js | 0 .../datetime/TimeInterval.js | 0 lib/{bridge-model => model}/datetime/Timer.js | 0 .../datetime/Timer.test.js | 0 lib/{bridge-model => model}/datetime/index.js | 0 lib/model/index.js | 238 +++++++++++ .../lightstate/BaseStates.js | 0 .../lightstate/CommonStates.js | 0 .../lightstate/CommonStates.test.js | 122 +++--- .../lightstate/GroupState.js | 0 .../lightstate/LightState.js | 0 .../lightstate/SceneLightState.js | 0 .../lightstate/States.js | 6 +- .../lightstate/index.js | 0 .../lightstate/stateParameters.js | 50 +-- lib/{bridge-model => model}/rules/Rule.js | 72 +++- .../rules/actions/GroupStateAction.js | 0 .../rules/actions/LightStateAction.js | 0 .../rules/actions/RuleAction.js | 2 +- .../rules/actions/SceneAction.js | 0 .../rules/actions/ScheduleStateAction.js | 0 .../rules/actions/SensorStateAction.js | 0 .../rules/actions/index.js | 0 .../rules/conditions/GroupCondition.js | 0 .../rules/conditions/GroupCondition.test.js | 0 .../rules/conditions/RuleCondition.js | 0 .../rules/conditions/SensorCondition.js | 4 +- .../rules/conditions/SensorCondition.test.js | 8 +- .../rules/conditions/index.js | 0 .../rules/conditions/operators/Ddx.js | 0 .../rules/conditions/operators/Dx.js | 0 .../rules/conditions/operators/Equals.js | 0 .../rules/conditions/operators/Equals.test.js | 0 .../rules/conditions/operators/GreaterThan.js | 0 .../rules/conditions/operators/In.js | 0 .../rules/conditions/operators/LessThan.js | 0 .../rules/conditions/operators/NotIn.js | 0 .../rules/conditions/operators/NotStable.js | 0 .../operators/RuleConditionOperator.js | 0 .../rules/conditions/operators/Stable.js | 0 .../rules/conditions/operators/index.js | 0 lib/model/scenes/GroupScene.js | 35 ++ lib/model/scenes/LightScene.js | 34 ++ lib/model/scenes/Scene.js | 93 +++++ .../scenes}/Scene.test.js | 12 +- lib/model/sensors/CLIPGenericFlag.js | 26 ++ lib/model/sensors/CLIPGenericStatus.js | 26 ++ lib/model/sensors/CLIPHumidity.js | 27 ++ lib/model/sensors/CLIPLightlevel.js | 71 ++++ lib/model/sensors/CLIPOpenClose.js | 26 ++ lib/model/sensors/CLIPPresence.js | 26 ++ .../sensors/CLIPPresence.test.js | 5 +- lib/model/sensors/CLIPSensor.js | 58 +++ lib/model/sensors/CLIPSwitch.js | 27 ++ lib/model/sensors/CLIPSwitch.test.js | 44 ++ lib/model/sensors/CLIPTemperature.js | 26 ++ lib/model/sensors/Daylight.js | 67 +++ lib/model/sensors/Daylight.test.js | 83 ++++ lib/model/sensors/Sensor.js | 199 +++++++++ .../devices => model}/sensors/ZGPSwitch.js | 25 +- lib/model/sensors/ZGPSwitch.test.js | 112 +++++ lib/model/sensors/ZLLLightlevel.js | 74 ++++ lib/model/sensors/ZLLPresence.js | 69 ++++ lib/model/sensors/ZLLSwitch.js | 56 +++ lib/model/sensors/ZLLTemperature.js | 23 ++ lib/parameters/BooleanType.js | 15 - lib/parameters/ChoiceType.js | 31 -- lib/parameters/Int16Type.js | 10 - lib/parameters/Int8Type.js | 10 - lib/parameters/IntegerType.js | 11 - lib/parameters/ListType.js | 53 --- lib/parameters/ParameterType.js | 39 -- lib/parameters/RangedNumberType.js | 60 --- lib/parameters/StringType.js | 15 - lib/parameters/UInt16Type.js | 10 - lib/parameters/UInt8Type.js | 9 - lib/rgb.test.js | 2 +- lib/types/Boolean.test.js | 84 ++++ lib/types/BooleanType.js | 26 ++ lib/types/ChoiceType.js | 30 ++ lib/types/ChoiceType.test.js | 175 ++++++++ lib/{parameters => types}/FloatType.js | 3 +- lib/types/FloatType.test.js | 221 ++++++++++ lib/types/Int16Type.js | 14 + lib/types/Int16Type.test.js | 234 +++++++++++ lib/types/Int8Type.js | 14 + lib/types/Int8Type.test.js | 237 +++++++++++ lib/types/ListType.js | 77 ++++ lib/types/ListType.test.js | 312 ++++++++++++++ lib/types/ObjectType.js | 96 +++++ lib/types/ObjectType.test.js | 261 ++++++++++++ lib/types/RangedNumberType.js | 65 +++ lib/types/StringType.js | 66 +++ lib/types/StringType.test.js | 350 ++++++++++++++++ lib/types/Type.js | 67 +++ lib/types/Type.test.js | 203 +++++++++ lib/types/UInt16Type.js | 14 + lib/types/UInt16Type.test.js | 222 ++++++++++ lib/types/UInt8Type.js | 14 + lib/types/UInt8Type.test.js | 222 ++++++++++ lib/{parameters => types}/index.js | 5 + lib/v3.js | 74 +++- package-lock.json | 389 ++++++------------ package.json | 9 +- test/scene-tests.js | 2 +- test/schedule-tests.js | 2 +- 222 files changed, 7201 insertions(+), 2783 deletions(-) create mode 100644 docs/resourceLink.md create mode 100644 docs/resourcelinks.md create mode 100644 examples/v3/resourceLinks/createAndDeleteResourceLink.js create mode 100644 examples/v3/resourceLinks/getAllResourceLinks.js create mode 100644 examples/v3/resourceLinks/getResourceLink.js create mode 100644 examples/v3/resourceLinks/updateResourceLink.js create mode 100644 lib/api/ResourceLinks.js create mode 100644 lib/api/ResourceLinks.test.js create mode 100644 lib/api/http/endpoints/resourcelinks.js delete mode 100644 lib/api/http/placeholders/NumberPlaceholder.js delete mode 100644 lib/api/http/placeholders/PlaceHolder.js create mode 100644 lib/api/http/placeholders/Placeholder.js create mode 100644 lib/api/http/placeholders/ResourceLinkPlaceholder.js delete mode 100644 lib/api/http/placeholders/StringPlaceholder.js delete mode 100644 lib/bridge-model/BridgeObject.js delete mode 100644 lib/bridge-model/BridgeObjectWithNumberId.js delete mode 100644 lib/bridge-model/Group.js delete mode 100644 lib/bridge-model/Scene.js delete mode 100644 lib/bridge-model/devices/Device.js delete mode 100644 lib/bridge-model/devices/lights/ColorLight.js delete mode 100644 lib/bridge-model/devices/lights/ColorTemperatureLight.js delete mode 100644 lib/bridge-model/devices/lights/DimmableLight.js delete mode 100644 lib/bridge-model/devices/lights/ExtendedColorLight.js delete mode 100644 lib/bridge-model/devices/lights/Light.js delete mode 100644 lib/bridge-model/devices/lights/OnOffLight.js delete mode 100644 lib/bridge-model/devices/lights/index.js delete mode 100644 lib/bridge-model/devices/sensors/CLIPCommon.js delete mode 100644 lib/bridge-model/devices/sensors/CLIPGenericFlag.js delete mode 100644 lib/bridge-model/devices/sensors/CLIPGenericStatus.js delete mode 100644 lib/bridge-model/devices/sensors/CLIPHumidity.js delete mode 100644 lib/bridge-model/devices/sensors/CLIPLightlevel.js delete mode 100644 lib/bridge-model/devices/sensors/CLIPOpenClose.js delete mode 100644 lib/bridge-model/devices/sensors/CLIPPresence.js delete mode 100644 lib/bridge-model/devices/sensors/CLIPSensor.js delete mode 100644 lib/bridge-model/devices/sensors/CLIPSwitch.js delete mode 100644 lib/bridge-model/devices/sensors/CLIPTemperature.js delete mode 100644 lib/bridge-model/devices/sensors/Daylight.js delete mode 100644 lib/bridge-model/devices/sensors/Sensor.js delete mode 100644 lib/bridge-model/devices/sensors/ZLLLightLevel.js delete mode 100644 lib/bridge-model/devices/sensors/ZLLPresence.js delete mode 100644 lib/bridge-model/devices/sensors/ZLLSwitch.js delete mode 100644 lib/bridge-model/devices/sensors/ZLLTemperature.js delete mode 100644 lib/bridge-model/devices/sensors/index.js delete mode 100644 lib/bridge-model/index.js delete mode 100644 lib/bridge-model/rules/index.js create mode 100644 lib/model/BridgeObject.js create mode 100644 lib/model/Group.js create mode 100644 lib/model/Light.js create mode 100644 lib/model/ResourceLink.js create mode 100644 lib/model/ResourceLink.test.js rename lib/{bridge-model => model}/Schedule.js (52%) create mode 100644 lib/model/Schedule.test.js rename lib/{bridge-model/devices/lights/color-gamuts.js => model/colorGamuts.js} (100%) rename lib/{bridge-model => model}/datetime/AbsoluteTime.js (100%) rename lib/{bridge-model => model}/datetime/AbsoluteTime.test.js (100%) rename lib/{bridge-model => model}/datetime/BridgeTime.js (100%) rename lib/{bridge-model => model}/datetime/DateTimeUtil.js (100%) rename lib/{bridge-model => model}/datetime/HueDate.js (100%) rename lib/{bridge-model => model}/datetime/HueDate.test.js (100%) rename lib/{bridge-model => model}/datetime/HueTime.js (100%) rename lib/{bridge-model => model}/datetime/HueTime.test.js (100%) rename lib/{bridge-model => model}/datetime/RandomizedTime.js (100%) rename lib/{bridge-model => model}/datetime/RandomizedTimer.js (100%) rename lib/{bridge-model => model}/datetime/RecurringRandomizedTime.js (100%) rename lib/{bridge-model => model}/datetime/RecurringRandomizedTimer.js (100%) rename lib/{bridge-model => model}/datetime/RecurringTime.js (100%) rename lib/{bridge-model => model}/datetime/RecurringTimer.js (100%) rename lib/{bridge-model => model}/datetime/TimeInterval.js (100%) rename lib/{bridge-model => model}/datetime/Timer.js (100%) rename lib/{bridge-model => model}/datetime/Timer.test.js (100%) rename lib/{bridge-model => model}/datetime/index.js (100%) create mode 100644 lib/model/index.js rename lib/{bridge-model => model}/lightstate/BaseStates.js (100%) rename lib/{bridge-model => model}/lightstate/CommonStates.js (100%) rename lib/{bridge-model => model}/lightstate/CommonStates.test.js (89%) rename lib/{bridge-model => model}/lightstate/GroupState.js (100%) rename lib/{bridge-model => model}/lightstate/LightState.js (100%) rename lib/{bridge-model => model}/lightstate/SceneLightState.js (100%) rename lib/{bridge-model => model}/lightstate/States.js (94%) rename lib/{bridge-model => model}/lightstate/index.js (100%) rename lib/{bridge-model => model}/lightstate/stateParameters.js (74%) rename lib/{bridge-model => model}/rules/Rule.js (58%) rename lib/{bridge-model => model}/rules/actions/GroupStateAction.js (100%) rename lib/{bridge-model => model}/rules/actions/LightStateAction.js (100%) rename lib/{bridge-model => model}/rules/actions/RuleAction.js (94%) rename lib/{bridge-model => model}/rules/actions/SceneAction.js (100%) rename lib/{bridge-model => model}/rules/actions/ScheduleStateAction.js (100%) rename lib/{bridge-model => model}/rules/actions/SensorStateAction.js (100%) rename lib/{bridge-model => model}/rules/actions/index.js (100%) rename lib/{bridge-model => model}/rules/conditions/GroupCondition.js (100%) rename lib/{bridge-model => model}/rules/conditions/GroupCondition.test.js (100%) rename lib/{bridge-model => model}/rules/conditions/RuleCondition.js (100%) rename lib/{bridge-model => model}/rules/conditions/SensorCondition.js (97%) rename lib/{bridge-model => model}/rules/conditions/SensorCondition.test.js (90%) rename lib/{bridge-model => model}/rules/conditions/index.js (100%) rename lib/{bridge-model => model}/rules/conditions/operators/Ddx.js (100%) rename lib/{bridge-model => model}/rules/conditions/operators/Dx.js (100%) rename lib/{bridge-model => model}/rules/conditions/operators/Equals.js (100%) rename lib/{bridge-model => model}/rules/conditions/operators/Equals.test.js (100%) rename lib/{bridge-model => model}/rules/conditions/operators/GreaterThan.js (100%) rename lib/{bridge-model => model}/rules/conditions/operators/In.js (100%) rename lib/{bridge-model => model}/rules/conditions/operators/LessThan.js (100%) rename lib/{bridge-model => model}/rules/conditions/operators/NotIn.js (100%) rename lib/{bridge-model => model}/rules/conditions/operators/NotStable.js (100%) rename lib/{bridge-model => model}/rules/conditions/operators/RuleConditionOperator.js (100%) rename lib/{bridge-model => model}/rules/conditions/operators/Stable.js (100%) rename lib/{bridge-model => model}/rules/conditions/operators/index.js (100%) create mode 100644 lib/model/scenes/GroupScene.js create mode 100644 lib/model/scenes/LightScene.js create mode 100644 lib/model/scenes/Scene.js rename lib/{bridge-model => model/scenes}/Scene.test.js (77%) create mode 100644 lib/model/sensors/CLIPGenericFlag.js create mode 100644 lib/model/sensors/CLIPGenericStatus.js create mode 100644 lib/model/sensors/CLIPHumidity.js create mode 100644 lib/model/sensors/CLIPLightlevel.js create mode 100644 lib/model/sensors/CLIPOpenClose.js create mode 100644 lib/model/sensors/CLIPPresence.js rename lib/{bridge-model/devices => model}/sensors/CLIPPresence.test.js (93%) create mode 100644 lib/model/sensors/CLIPSensor.js create mode 100644 lib/model/sensors/CLIPSwitch.js create mode 100644 lib/model/sensors/CLIPSwitch.test.js create mode 100644 lib/model/sensors/CLIPTemperature.js create mode 100644 lib/model/sensors/Daylight.js create mode 100644 lib/model/sensors/Daylight.test.js create mode 100644 lib/model/sensors/Sensor.js rename lib/{bridge-model/devices => model}/sensors/ZGPSwitch.js (62%) create mode 100644 lib/model/sensors/ZGPSwitch.test.js create mode 100644 lib/model/sensors/ZLLLightlevel.js create mode 100644 lib/model/sensors/ZLLPresence.js create mode 100644 lib/model/sensors/ZLLSwitch.js create mode 100644 lib/model/sensors/ZLLTemperature.js delete mode 100644 lib/parameters/BooleanType.js delete mode 100644 lib/parameters/ChoiceType.js delete mode 100644 lib/parameters/Int16Type.js delete mode 100644 lib/parameters/Int8Type.js delete mode 100644 lib/parameters/IntegerType.js delete mode 100644 lib/parameters/ListType.js delete mode 100644 lib/parameters/ParameterType.js delete mode 100644 lib/parameters/RangedNumberType.js delete mode 100644 lib/parameters/StringType.js delete mode 100644 lib/parameters/UInt16Type.js delete mode 100644 lib/parameters/UInt8Type.js create mode 100644 lib/types/Boolean.test.js create mode 100644 lib/types/BooleanType.js create mode 100644 lib/types/ChoiceType.js create mode 100644 lib/types/ChoiceType.test.js rename lib/{parameters => types}/FloatType.js (65%) create mode 100644 lib/types/FloatType.test.js create mode 100644 lib/types/Int16Type.js create mode 100644 lib/types/Int16Type.test.js create mode 100644 lib/types/Int8Type.js create mode 100644 lib/types/Int8Type.test.js create mode 100644 lib/types/ListType.js create mode 100644 lib/types/ListType.test.js create mode 100644 lib/types/ObjectType.js create mode 100644 lib/types/ObjectType.test.js create mode 100644 lib/types/RangedNumberType.js create mode 100644 lib/types/StringType.js create mode 100644 lib/types/StringType.test.js create mode 100644 lib/types/Type.js create mode 100644 lib/types/Type.test.js create mode 100644 lib/types/UInt16Type.js create mode 100644 lib/types/UInt16Type.test.js create mode 100644 lib/types/UInt8Type.js create mode 100644 lib/types/UInt8Type.test.js rename lib/{parameters => types}/index.js (90%) diff --git a/.gitignore b/.gitignore index 523751f..d51a224 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,7 @@ node_modules # Scratch Files scratch.js -.DS_Store \ No newline at end of file +.DS_Store + +# TypeScript Definition Files +*.d.ts \ No newline at end of file diff --git a/Changelog.md b/Changelog.md index 7df3af0..bb383bd 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,55 @@ # Change Log +## 4.0.0 +- `v3.api` removed the `create` function as it was deprecated, use `createRemote()` fro the remote API, `createLocal()` + for the local API or `createInsecureLocal()` for non-hue bridges that do not support https connections + +- `v3.Scene` has been removed, use the following functions to create a new Scene instance: + * `v3.model.createLightScene()` + * `v3.model.createGroupScene()` + This change has also allowed for the separation of the attributes and getter/setters locked down properly based on + the type of Scene, i.e. Cannot change the lights in a GroupScene (as they are controlled by the Group). + +- `v3.sensors` has been removed, use `v3.model.createCLIPxxx()` functions instead + +- `v3.rules` has been removed to `v3.model` + * To create a `Rule` use `v3.model.createRule()` + * To create a `RuleCondition` use `v3.model.ruleConditions.[group|sensor]` + * To create a `RuleAction` use `v3.model.ruleActions.[light|group|sensor|scene]` + +- `v3.model` added to support exposing the underlying model objects that represent bridge objects. This module will allow + you to create all of the necessary objects, e.g. `createGroupScene()` + +- All creation function calls to the bridge will now return the created model object. This change makes it consistent as + some calls would return the object, others would return the id but no other data. + + This changes return object from the promise on the following calls: + * `api.rules.createRule()` + * `api.scenes.createScene()` + * `api.sensors.createSensor()` + +- Added support for `ResourceLinks` in the API + +- Type system from the `LightState` definitions is now used in all Bridge Object Models to define the attributes/properties + obtained from the Bridge. + + This provides a consistent validation mechanism to all bridge related attributes data. As part of this being used in + the models, some validation is performed at the time of setting a value instead of waiting on when sending it to the + hue bridge (some things still have to wait be sent to the bridge) so the validation is closer to the point of call. + +- Added ability to serialize a model object into JSON and then restore it to a corresponding object from the JSON + payload. This was requested to aid in server/client side code situations, as the creation of the model objects are + not directly exposed in the library by design. Related to issue #132 + +- Adding more in-depth tests to greatly increase coverage around types and models + +- Creating Sensors (CLIP variety) has changed as the classes for the sensor objects are no longer directly accessible. + All `CLIPxxx` sensors need to be built from the `v3.model.createCLIP[xxx]Sensor()` function for the desired type, + e.g. `v3.model.createCLIPGenericStatusSensor()` for a `CLIPGenericStatus` sensor. + + The function call to instantiate the sensors also no longer take an object to set various attributes of the sensor, + you need to call the approriate setter on the class now to se the attribute, e.g. `sensor.manufacturername = 'node-hue-api-sensor';` + ## 3.4.1 - Fixing issue with the lookup for the Hue motion sensor, issue #146 diff --git a/README.md b/README.md index 3704b27..0539950 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,14 @@ The library fully supports `local network` and `remote internet` access to the H - [Rule Object](docs/rule.md) - [RuleCondition Object](docs/ruleCondition.md) - [RuleAction Object](docs/ruleAction.md) + - [ResourceLinks](/docs/resourcelinks.md) + - [ResourceLink Object](docs/resourceLink.md) - [Configuration](docs/configuration.md) - [Remote](docs/remote.md) - [Examples](#examples) - [Discover and connect to the Hue Bridge for the first time](#discover-and-connect-to-the-hue-bridge-for-the-first-time) - [Set a LightState on a Light](#set-a-light-state-on-a-light) - - [Using Hue Remote API](#useing-hue) + - [Using Hue Remote API](#using-hue-remote-api) - [Philips Hue Resources](#philips-hue-resources) - [License](#license) @@ -138,6 +140,8 @@ _Note that there are a number of runnable code samples in the [examples/v3](exam - [Rule Object](docs/rule.md) - [RuleCondition Object](docs/ruleCondition.md) - [RuleAction Object](docs/ruleAction.md) +- [ResourceLinks](/docs/resourcelinks.md) + - [ResourceLink Object](docs/resourceLink.md) - [Configuration](docs/configuration.md) - [Remote](docs/remote.md) @@ -150,6 +154,7 @@ the documentation links [above](#v3-api). Alternatively take a look at the [examples directory](examples/v3) in this repository for complete self contained runnable example code. +--- ### Discover and connect to the Hue Bridge for the first time @@ -225,6 +230,7 @@ For more details on discovery of Hue Bridges, check out the [discovery API](./do along with the [users API](./docs/users.md). +--- ### Set a Light State on a Light Once you have created your user account and know the IP Address of the Hue Bridge you can interact with things on it. diff --git a/docs/resourceLink.md b/docs/resourceLink.md new file mode 100644 index 0000000..524da7e --- /dev/null +++ b/docs/resourceLink.md @@ -0,0 +1,146 @@ +# ResourceLink + +A `ResourceLink` is a grouping construct for various Hue Bridge resources that are linked to provide some level of +interconnected functionality. This is used primarily for the Hue Formulas, but can be leveraged by API developers as an +advanced mechanism for building advanced functionality. + + +* [Create a ResourceLink](#creating-a-resourcelink) +* [ResourceLink Properties and Functions](#resourcelink-properties-and-functions) + + +## Creating a ResourceLink + +You can create a `ResourceLink` by using the `v3.model.createResourceLink()` function. + +```js +const Scene = require('node-hue-api').v3.model; +const myResourceLink = model.createResourceLink(); +``` + + +## ResourceLink Properties and Functions + +* [id](#id) +* [name](#name) +* [description](#description) +* [type](#type) +* [classid](#classid) +* [owner](#owner) +* [recycle](#recycle) +* [links](#links) + * [resetLinks()](#resetlinks) + * [addLink()](#addlink) + * [removeLink()](#removelinks) +* [toString](#tostring) +* [toStringDetailed](#tostringdetailed) + + + + +### id +Get the `id` for the ResourceLink. +* `get` + + +### name +Get/Set a name for the ResourceLink. +* `get` +* `set` + + +### description +Get/Set a description for the ResourceLink. +* `get` +* `set` + + +### type +Get the type of the ResourceLink, which is always `Link` at the current time. +* `get` + + +### classid +Get/Set a classid for the ResourceLink. This is specific to the application and can be used to identify the purpose of +the ResourceLink. + +The Hue API documentation gives the following example use case: + + The resourcelink class can be used to identify resourcelink with the same purpose, like classid 1 for wake-up, 2 for going to sleep, etc. (best practice use range 1 – 10000) + +* `get` +* `set` + + +### owner +Gets the owner of the ResourceLink, which is only populated on ResourceLinks obtained from the Hue Bridge. +* `get` + + +### recycle +Get/Set the `recyle` attribute of the ResourceLink. This is used to flag scenes that can be automatically deleted by +the bridge. + +If the `recycle` state is set to `false` the Hue bridge will keep the ResourceLink until an application removes it. +* `get` +* `set` + + +### links +There is a property on the ResourceLink `links` that will return a copy of the existing links object defined in the +ResourceLink. + +* `get` + +The object returned will have a key value of the name of the type of link (e.g. `groups`) and an Array of the ids for the +linked items of that type. Any types of links that have no items, will not be present in the links object. + +For example if we had links for lights with ids `1`, `2` and `3` and group `0` the `links` object would look like: + +```json +{ + "lights": [1, 2, 3], + "groups": [0] +} +``` + + +#### resetLinks() +A function `resetLinks()` will clear out any existing links on the ResourceLink. + + +#### addLink() +The function `addLink(type, id)` allows for the adding of a link to the ResourceLink. + +* `type`: One of the supported types: + * `lights` + * `sensors` + * `groups` + * `scenes` + * `rules` + * `schedules` + * `resourcelinks` +* `id`: The id of the type of object that you are adding as a link, e.g. a group id if the the `type` was a group + + +#### removeLink() +The function `removeLink(type, id)` allows for the removal of a specific link from the ResourceLink. + +* `type`: One of the supported types: + * `lights` + * `sensors` + * `groups` + * `scenes` + * `rules` + * `schedules` + * `resourcelinks` +* `id`: The id of the type of object that you are removing as a link, e.g. a group id if the the `type` was a group + + + +### toString() +The `toString()` function will obtain a simple `String` representation of the Scene. + + +### toStringDetailed() +The `toStringDetailed()` function will obtain a more detailed representation of the Scene object. \ No newline at end of file diff --git a/docs/resourcelinks.md b/docs/resourcelinks.md new file mode 100644 index 0000000..f7fdeb8 --- /dev/null +++ b/docs/resourcelinks.md @@ -0,0 +1,141 @@ +# ResourceLinks API + +The `resourceLinks` API provides a means of interacting with the `ResourceLinks` in Hue Bridge. + +A `ResourceLink` is a collection/grouping mechanism for linking various bridge resources that are interconnected. The +Hue Formulas that you add to your bridge are examples of these. + +See [`ResourceLink`s](./resourceLink.md) for more details on the `ResourceLink` objects + + +* [getAll()](#getall) +* [get(id)](#get) +* [createResourceLink()](#createresourcelink) +* [updateResouceLink()](#updateresourcelink) +* [deleteResourceLink()](#deleteresourcelink) + + +## getAll() +The `getAll()` function allows you to get all the `ResourceLinks` that the Hue Bridge has registered with it. + +```js +api.resourceLinks.getAll() + .then(allResourceLinks => { + // Display the ResourceLinks from the bridge + allResourceLinks.forEach(resourceLink => { + console.log(resourceLink.toStringDetailed()); + }); + }); +``` + +This function call will resolve to an `Array` of `ResourceLink` objects. + +A complete code sample for this function is available [here](../examples/v3/resourceLinks/getAllResourceLinks.js). + + + +## get() +The `get(id)` function allows a specific `ResourceLink` to be retrieved from the Hue Bridge. + +* `id`: The `String` id of the `ResourceLink` to retrieve. + + +```js +api.resourceLinks.get(62738) + .then(resourceLink => { + console.log(resourceLink.toStringDetailed()); + }) +; +``` + +This function call will resolve to a `ResourceLink` object for the specified `id`. + +If the `ResourceLink` cannot be found an `ApiError` will be returned with a `getHueErrorType()` value of `3`. + +A complete code sample for this function is available [here](../examples/v3/resourceLinks/getResourceLink.js). + + + + +## createResourceLink() +The `createResourceLink(ResourceLink)` function allows for the creation of new `ResourceLink`s in the Hue Bridge. + +* `resourceLink`: A `ResourceLink` object that has been configured with the desired settings that you want to store. + +```js +const resourceLink = v3.model.createResourceLink(); +resourceLink.name = 'My Resource Link'; +resourceLink.description = 'A test resource link for node-hue-api'; +resourceLink.recycle = true; +resourceLink.classid = 100; +resourceLink.addLink('groups', 0); + +api.resourceLinks.createResourceLink(resourceLink) + .then(resourceLink => { + console.log(`Successfully created ResourceLink\n${resourceLink.toStringDetailed()}`); + }) +; +``` + +The function will resolve with a corresponding `ResourceLink` object that was created. + +A complete code sample for this function is available [here](../examples/v3/resourceLinks/createAndDeleteResourceLink.js). + + + +## updateResourceLink() +The `updateResourceLink(resourceLink)` function allows you to update an existing `ResourceLink` in the Hue Bridge. + +* `resourceLink`: A `ResourceLink` object that was obtained from the API and then updated with the appropriate data changes to apply. + +```js +// A resourceLink needs to be obtained from the bridge first, assume one has called "resourceLink" +resourceLink.name = 'Updated ResourceLink Name'; + +api.resourceLink.updateResourceLink(resourceLink); + .then(updated => { + console.log(`Updated ResourceLink properties: ${JSON.stringify(updated)}`); + }) +; +``` + +The function will resolve to an object that contains the attribute names of the `ResourceLink` that were updated set +to the success status of the change to the attribute. + +_Note currently no checks are performed against the existing attributes, so all updatable attributes are sent to the bridge +when invoking this function._ + +For example, the result from the above example would resolve to: + +```js +{ + "name": true, + "description": true, + "classid": true, + "links": true +} +``` + +A complete code sample for this function is available [here](../examples/v3/resourceLinks/updateResourceLink.js). + + + + +## deleteResourceLink() +The `deleteResourceLink(id)` function will delete the specified `ResourceLink` identified by the `id` from the Hue Bridge. + +* `id`: The `id` of the `ResourceLink` to delete from the Hue Bridge, or a `ResourceLink` object that was obtained from + the Hue Bridge + +```js +api.resourceLinks.deleteResourceLink('abc170f') + .then(result => { + console.log(`Deleted ResourceLink? ${result}`); + }) +; +``` + +The call will resolve to a `Boolean` indicating the success status of the deletion. + +A complete code sample for this function is available [here](../examples/v3/resourceLinks/createAndDeleteResourceLink.js). + diff --git a/docs/ruleAction.md b/docs/ruleAction.md index 5c35653..59fbe66 100644 --- a/docs/ruleAction.md +++ b/docs/ruleAction.md @@ -15,8 +15,8 @@ fluent interface for building up the various `RuleActions`s for `Rule`s. ## Instantiating a RuleAction -A `RuleAction` can be built using the `v3.rules.actions` Object, currently this allows for the creation of actions via -the functions: +A `RuleAction` can be built using the `v3.model.ruleActions` Object, currently this allows for the creation of actions +via the functions: * `light`: Creates a [`LightStateAction`](#lightstateaction) that will set a `LightState` on a specific `Light` * `group`: Creates a [`GroupStateAction`](#groupstateaction) that will set a state `GroupLightState` on a `Group` diff --git a/docs/ruleCondition.md b/docs/ruleCondition.md index abf502f..7076b4b 100644 --- a/docs/ruleCondition.md +++ b/docs/ruleCondition.md @@ -22,13 +22,13 @@ fluent interface for building up the various `RuleCondition`s for `Rule`s. ## Condition Builders -A `RuleCondition` can be built using the `v3.rules.conditions` Object, currently this allows for the creation of +A `RuleCondition` can be built using the `v3.model.ruleCconditions` Object, currently this allows for the creation of conditions for `Sensors` and `Groups`. ## SensorCondition Builder -A `SensorCondition` builder can be created using the `v3.rules.conditions.sensor(sensor)` function. +A `SensorCondition` builder can be created using the `v3.model.ruleConditions.sensor(sensor)` function. * `sensor`: The `Sensor` obtained from bridge via the API that you wish to use in a condition. @@ -37,7 +37,7 @@ instance of the one you desire). ```js const v3 = require('node-hue-api').v3 - , conditions = v3.rules.conditions; + , conditions = v3.model.ruleConditions; const mySensor = await v3.sensors.get(sesnorId); const mySensorCondition = conditions.sensor(mySensor); @@ -86,7 +86,7 @@ The following are code examples of setting up various SensorConditions. Create a `RuleCondition` that will trigger on a `flag` attribute change on a CLIPGenericFlag Sensor (i.e. trigger on every change): ```js const v3 = require('node-hue-api').v3 - , conditions = v3.rules.conditions + , conditions = v3.model.ruleConditions ; // Create a SensorCondition that will trigger on a flag attribute change for the CLIPGenericFlag Sensor: @@ -97,7 +97,7 @@ const ruleCondition = sensorCondition.getRuleCondition(); Create a RuleCondition that will trigger when the `flag` attribute changes to `true` for a CLIPGenericFlag Sensor: ```js const v3 = require('node-hue-api').v3 - , conditions = v3.rules.conditions + , conditions = v3.model.ruleConditions ; // Create a SensorCondition that will trigger on the flag attribute being true CLIPGenericFlag Sensor: @@ -108,7 +108,7 @@ const ruleCondition = sensorCondition.getRuleCondition(); ## GroupCondition Builder -A `GroupCondition` builder can be created using the `v3.rules.conditions.group(id)` function. +A `GroupCondition` builder can be created using the `v3.model.ruleConditions.group(id)` function. * `id`: The id for the group (or the Group instance) that you wish to build the condition on @@ -153,7 +153,7 @@ The following are code examples of setting up various GroupConditions. Create a `GroupCondition` that will trigger when any light is on in a group: ```js const v3 = require('node-hue-api').v3 - , conditions = v3.rules.conditions + , conditions = v3.model.ruleConditions ; // Create a SensorCondition that will trigger on a flag attribute change for the CLIPGenericFlag Sensor: diff --git a/docs/rules.md b/docs/rules.md index 183940f..e8b317e 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -67,15 +67,15 @@ The `createRule(rule)` function will create a new `Rule` in the Hue Bridge. // You need to have created the myRule instance using code before invoking this api.rules.createRule(myRule) .then(result => { - console.log(`Created Rule: ${result.id}`); + // Will get an instance of a Rule object + console.log(`Created Rule: ${result.toStringDetailed()}`); }) .catch(err => { console.error(`Rule was not valid: ${err.message}`); }); ``` -The function will return an Object with an `id` property for the new Rule is the Rule was valid, otherwise will throw -an `ApiError`. +The function will return the created `Rule` object, otherwise will throw an `ApiError`. _Note: It is not possible to completely validate all the possible combinations of attributes in a `Rule` as to whether or not it is valid before trying to create it in the Hue Bridge. @@ -83,7 +83,6 @@ The library will perform a number of checks around eliminating common and obviou the ultimate check is made by the bridge, but I have seen some very generic error messages in testing when there are issues in the Rule definition._ - A complete code sample for this function is available [here](../examples/v3/rules/createRule.js). diff --git a/docs/scene.md b/docs/scene.md index 7bf631f..f7def78 100644 --- a/docs/scene.md +++ b/docs/scene.md @@ -5,84 +5,44 @@ The Hue Bridge can support two variations of a Scene, `LightScene` and `GroupSc A Scene is represented as an `id` a `name` and a list of `Lights` stored inside the Hue Bridge. These are separate from what a scene is in the iOS and Android applications. +* [Common Scene Properties](#common-scene-properties-and-functions) * [LightScene](#lightscene) + * [Creating a LightScene](#creating-a-lightscene) + * [Properties](#lightscene-properties) + * [lights](#lights) + * [lightstates](#lightstates) * [GroupScene](#groupscene) -* [LightStateScene](#lightstatescene) + * [Creating a GroupScene](#creating-a-groupscene) + * [Properties](#groupscene-properties) + * [group](#group-1) + * [lights](#lights-1) + * [lightstates](#lightstates-1) -* [Create a Scene](#creating-a-scene) -* [Scene Properties](#scene-properties) - -## LightScene -This is the `default` type of `Scene` in the Hue Bridge. It maintains a list of lights that the are associated with the -Scene, which can be updated. - - -## GroupScene -A `GroupScene` is a `Scene` with a `type` of `GroupScene`. These scenes are linked to an specified Group in the Hue Bridge. - -The associated lights for the `GroupScene` is controlled via the `Group`. When the `Group` becomes empty or is removed -from the Bridge, the associated `GroupScene`s are also removed. - -The lights for a GroupScene are not modifiable. - - -## LightStateScene -A `LightStateScene` is a Scene that explictly sets the light states for each of the lights that makes up the Scene. - - -## Creating a Scene - -You can create a `Scene` by using the `new` operator. - -```js -const Scene = require('node-hue-api').v3.Scene; -const myScene = new Scene(); -``` - - -## Scene Properties +## Common Scene Properties and Functions Depending upon the properties that you chose to set on the `Scene` the `type` attribute will be implicitly set for you by the API. This `type` dictates what attributes are modifiable in the Hue Bridge. -* [name](#name) * [id](#id) -* [group](#group) -* [lights](#lights) -* [lightstates](#lightstates) +* [name](#name) * [type](#type) * [owner](#owner) * [recycle](#recycle) -* [lockec](#locked) +* [locked](#locked) * [appdata](#appdata) * [picture](#picture) * [lastupdated](#lastupdated) * [version](#version) -* [payload](#payload) +* [toString()](#tostring) +* [toStringDetailed()](#tostringdetailed) -### name -Get/Set a name for the Scene. -* `get` -* `set` ### id Get the `id` for the Scene. * `get` -### group -The group ID for the Scene if associated with a group. If you set this, the `type` will be set to `GroupScene` implicitly. -* `get` -* `set` - -### lights -The associated light ids for the Scene. If this is a `GroupScene` these are popualte by the Hue Bridge and are read only. -If you invoke `set` on this property then the `type` will be implicitly set to `LightScene`. -* `get` -* `set` - -### lightstates -Gets/Sets the desired LightState for the lights in the scene. This is primiarily used to provide some backwards -compatibility in the API with v2, not for normal user usage. +### name +Get/Set a name for the Scene. * `get` * `set` @@ -126,7 +86,6 @@ For example we could store the `location` and `application_name` in the `appdata data: 'my-custom-app-data' } ``` - ### picture Get/Set the picture data for the scene. The Hue Bridge does not support setting this via a PUT call which means it is @@ -142,14 +101,76 @@ Gets the last updated time for the Scene. Gets the version of the Scene * `get` -### payload -Obtains the JSON payload that can be used to create/update the Scene on the Hue Bridge. -* `get` - ### toString() The `toString()` function will obtain a simple `String` representation of the Scene. ### toStringDetailed() -The `toStringDetailed()` function will obtain a more detailed representation of the Scene object. \ No newline at end of file +The `toStringDetailed()` function will obtain a more detailed representation of the Scene object. + + + +# LightScene +This is the `default` type of `Scene` in the Hue Bridge. It maintains a list of lights that the are associated with the +Scene, which can be updated. + +## Creating a LightScene +You can create a new LightScene object using the `v3.model.createLightScene()` function. + + +## LightScene Properties +The following are the LightScene specific properties above those already defined in the [Common Scene Properties and functions](#common-scene-properties-and-functions). + +### lights +The associated light ids for the `LightScene`. + +* `get`: Obtains the Array of light ids +* `set`: Set the Array of light ids associated with the LightScene + +### lightstates +Gets/Sets the desired LightState for the lights in the scene. This is primarily used to provide some backwards +compatibility in the API with v2, not for normal user usage. +* `get` +* `set` + +_Note: lightStates are only present on scenes that have explicitly been retrieved from the Hue Bridge, that is, scenes +that you have obtained from the `v3.api.scenes.get(id)` API call._ + + + + +# GroupScene +A `GroupScene` is a `Scene` with a `type` of `GroupScene`. These scenes are linked to an specified Group in the Hue Bridge. + +The associated lights for the `GroupScene` is controlled via the `Group`. When the `Group` becomes empty or is removed +from the Bridge, the associated `GroupScene`s are also removed. + +The lights for a GroupScene are not modifiable as they belong to the `Group` object. + + +## Creating a GroupScene +You can create a new GroupScene object using the `v3.model.createGroupScene()` function. + + +## GroupScene Properties +The following are the LightScene specific properties above those already defined in the [Common Scene Properties and functions](#common-scene-properties-and-functions). + +### group +The group ID for the GroupScene if associated with a group. +* `get` +* `set` + +### lights +The associated light ids for the `GroupScene`. This is controlled via the membership of the lights in the `Group` that +the GroupScene is associated with. + +* `get`: Obtains the Array of light ids in the target Group + +### lightstates +Gets the LightStates for the lights in the GroupScene. This is primarily used to provide some backwards +compatibility in the API with v2, not for normal user usage. +* `get` + +_Note: lightStates are only present on scenes that have explicitly been retrieved from the Hue Bridge, that is, scenes +that you have obtained from the `v3.api.scenes.get(id)` API call._ diff --git a/docs/scenes.md b/docs/scenes.md index e8557bd..a6ae5d3 100644 --- a/docs/scenes.md +++ b/docs/scenes.md @@ -24,7 +24,7 @@ Bridge will be 102. ## getAll() -The `getAll()` function allows you to get all the lights that the Hue Bridge has registered with it. +The `getAll()` function allows you to get all the scenes that the Hue Bridge has registered with it. ```js api.scenes.getAll() @@ -90,18 +90,18 @@ The `createScene(scene)` function allows for the creation of new `Scene`s in the * `scene`: A `Scene` object that has been configured with the desired settings for rhe scene being created. ```js -const scene = new Scene(); +const scene = v3.model.createLightScene(); scene.name = 'My Scene'; scene.lights = [1, 2, 3]; api.scenes.createScene(scene) - .then(result => { - console.log(`Successfully created scene with id: ${result.id}`); + .then(scene => { + console.log(`Successfully created scene\n${scene.toStringDetailed()}`); }) ; ``` -The function will resolve with a `Object` with a value of `id` that is the newly created scene's id. +The function will resolve with a corresponding `GroupScene` or `LightScene` object, depending upon what was passed in. _Note: Whilst the Hue API itself will allow a scene to be updated via creation call, this library will prevent such a thing, by removing any `id` value from the `Scene` object to prevent overwriting an existing `Scene`. diff --git a/docs/sensor.md b/docs/sensor.md index 3b35dce..d88cc66 100644 --- a/docs/sensor.md +++ b/docs/sensor.md @@ -8,7 +8,7 @@ Some of these Sensors are Hardware sensors whilst others are Software constructs The sensors that can be built and controlled via software are the `CLIP` variety. The API provides access to the Sensor objects via the [`v3.api.sensors` API](sensors.md), but you can also create new -CLIP Sensor objects using the various CLIP sensor classes from the `v3.sensors.clip` objects. +CLIP Sensor objects using the various CLIP sensor classes by using the `v3.model.createCLIP[xxx]Sensor()` functions. - [CLIP Sensors](#clipsensors) @@ -19,7 +19,7 @@ CLIP Sensor objects using the various CLIP sensor classes from the `v3.sensors.c - [CLIP OpenClose](#clipopenclose) - [CLIP Presence](#clippresence) - [CLIP Switch](#clipswitch) - - [CLIP Temperatue](#cliptemperature) + - [CLIP Temperature](#cliptemperature) @@ -67,16 +67,13 @@ The unique properties for the GenericFlag Sensor are: Creating a `CLIPGenericFlag` sensor can be done as shown below: ```js -const CLIPGenericFlag = require('node-hue-api').v3.sensors.clip.GenericFlag; +const model = require('node-hue-api').v3.model; -const sensorConfig = { - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api' -}; - -const mySensor = new CLIPGenericFlagSensor(sensorConfig); +const mySensor = model.createCLIPGenericFlagSensor(); +mySensor.modelid = 'software'; +mySensor.swversion = '1.0'; +mySensor.uniqueid = '00:00:00:01'; +mySensor.manufacturername = 'node-hue-api'; // Set the name of the sensor mySensor.name = 'My awesome clip generic flag sensor'; @@ -103,16 +100,13 @@ The unique properties for the `GenericFlag` Sensor are: Creating a `CLIPGenericStatus` sensor can be done as shown below: ```js -const CLIPGenericStatus = require('node-hue-api').v3.sensors.clip.GenericStatus; - -const sensorConfig = { - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api' -}; +const model = require('node-hue-api').v3.model; -const mySensor = new CLIPGenericStatusSensor(sensorConfig); +const mySensor = model.createCLIPGenericStatusSensor(); +mySensor.modelid = 'software'; +mySensor.swversion = '1.0'; +mySensor.uniqueid = '00:00:00:01'; +mySensor.manufacturername = 'node-hue-api'; // Set the name of the sensor mySensor.name = 'My awesome clip generic status sensor'; @@ -134,16 +128,13 @@ The unique properties for the `Humidity` Sensor are: Creating a `CLIPHumidity` sensor can be done as shown below: ```js -const CLIPHumiditySensor = require('node-hue-api').v3.sensors.clip.Humidity; +const model = require('node-hue-api').v3.model; -const sensorConfig = { - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api' -}; - -const mySensor = new CLIPHumiditySensor(sensorConfig); +const mySensor = model.createCLIPHumiditySensor(); +mySensor.modelid = 'software'; +mySensor.swversion = '1.0'; +mySensor.uniqueid = '00:00:00:01'; +mySensor.manufacturername = 'node-hue-api'; // Set the name of the sensor mySensor.name = 'Lounge Humidity'; @@ -169,16 +160,13 @@ The unique properties for the `Lighlevel` Sensor are: Creating a `CLIPLightLevel` sensor can be done as shown below: ```js -const CLIPLightLevel = require('node-hue-api').v3.sensors.clip.LightLevel; - -const sensorConfig = { - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api' -}; +const model = require('node-hue-api').v3.model; -const mySensor = new CLIPLightLevel(sensorConfig); +const mySensor = model.createCLIPLightlevelSensor(); +mySensor.modelid = 'software'; +mySensor.swversion = '1.0'; +mySensor.uniqueid = '00:00:00:01'; +mySensor.manufacturername = 'node-hue-api'; // Set the name of the sensor mySensor.name = 'Lounge LightLevel'; @@ -200,16 +188,13 @@ The unique properties for the `OpenClose` Sensor are: Creating a `CLIPOpenClose` sensor can be done as shown below: ```js -const CLIPOpenClose = require('node-hue-api').v3.sensors.clip.OpenClose; +const model = require('node-hue-api').v3.model; -const sensorConfig = { - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api' -}; - -const mySensor = new CLIPOpenClose(sensorConfig); +const mySensor = model.createCLIPOpenCloseSensor(); +mySensor.modelid = 'software'; +mySensor.swversion = '1.0'; +mySensor.uniqueid = '00:00:00:01'; +mySensor.manufacturername = 'node-hue-api'; // Set the name of the sensor mySensor.name = 'Lounge Door'; @@ -231,16 +216,13 @@ The unique properties for the `Presense` Senor are: Creating a `CLIPPresence` sensor can be done as shown below: ```js -const CLIPresence = require('node-hue-api').v3.sensors.clip.Presence; - -const sensorConfig = { - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api' -}; +const model = require('node-hue-api').v3.model; -const mySensor = new CLIPresence(sensorConfig); +const mySensor = model.createCLIPPresenceSensor(); +mySensor.modelid = 'software'; +mySensor.swversion = '1.0'; +mySensor.uniqueid = '00:00:00:01'; +mySensor.manufacturername = 'node-hue-api'; // Set the name of the sensor mySensor.name = 'Lounge Presence'; @@ -264,16 +246,13 @@ the last button pressed/released an whether it was a short or long press. Creating a `CLIPSwitch` sensor can be done as shown below: ```js -const CLIPSwitch = require('node-hue-api').v3.sensors.clip.Switch; +const model = require('node-hue-api').v3.model; -const sensorConfig = { - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api' -}; - -const mySensor = new CLIPSwitch(sensorConfig); +const mySensor = model.createCLIPSwitchSensor(); +mySensor.modelid = 'software'; +mySensor.swversion = '1.0'; +mySensor.uniqueid = '00:00:00:01'; +mySensor.manufacturername = 'node-hue-api'; // Set the name of the sensor mySensor.name = 'Lounge Wall Switch'; @@ -295,16 +274,13 @@ The unique properties for the `Temperature` Sensor are: Creating a `CLIPTemperature` sensor can be done as shown below: ```js -const CLIPTemperature = require('node-hue-api').v3.sensors.clip.Temperature; - -const sensorConfig = { - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api' -}; +const model = require('node-hue-api').v3.model; -const mySensor = new CLIPSwitch(sensorConfig); +const mySensor = model.createCLIPTemperatureSensor(); +mySensor.modelid = 'software'; +mySensor.swversion = '1.0'; +mySensor.uniqueid = '00:00:00:01'; +mySensor.manufacturername = 'node-hue-api'; // Set the name of the sensor mySensor.name = 'Lounge Temperature'; diff --git a/docs/sensors.md b/docs/sensors.md index 549df06..2c20bdd 100644 --- a/docs/sensors.md +++ b/docs/sensors.md @@ -129,17 +129,13 @@ documentation or the [example code](../examples/v3/sensors/creatingClipSensors.j ```js api.sensors.createSensor(sensor) - .then(result => { - console.log(`Created sensor with id: ${result.id}`) + .then(sensor => { + console.log(`Created sensor\n${sensor.toStringDetailed()}`) }) ``` -The returned `Object` will contain the new `Sensor` id under the `id` attribute, for example: -```js -{ - "id": 1 -} -``` +The promise will resolve to an instance of a `Sensor` that will be an instance of the type of sensor data that you +passed in. e.g. a `CLIPOpenClose` sensor. A complete code sample is available [here](../examples/v3/sensors/createNewSensor.js). @@ -205,7 +201,15 @@ api.sensors.updateSensorState(mySensor) }); ``` -The function will resolve to a `Boolean` indicating the success status of the update. +The function will resolve to a `Object` with the keys being the state values that were attempted to be updated and the +value set to a `Boolean` indicating if the bridge updated the value. + +For example for an OpenClose `Sensor` it would return the following object (as it only has a state of `open`): +```json +{ + "open": true +} +``` _Note: This will only work for CLIP `Sensor` types as other sensor types are usually hardware devices. Each type of sensor has different state attributes that can be modified. Consult the [`Sensor` documentation](./sensor.md) for the diff --git a/examples/v3/resourceLinks/createAndDeleteResourceLink.js b/examples/v3/resourceLinks/createAndDeleteResourceLink.js new file mode 100644 index 0000000..512ba57 --- /dev/null +++ b/examples/v3/resourceLinks/createAndDeleteResourceLink.js @@ -0,0 +1,42 @@ +'use strict'; + +const v3 = require('../../../index').v3; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3; + +const model = v3.model; + +// Replace this with your username for accessing the bridge +const USERNAME = require('../../../test/support/testValues').username; + +// +// This code will create a new ResourceLink on the bridge associated with the group 0 (all lights group) + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + const resourceLink = model.createResourceLink(); + resourceLink.name = 'API Created ResourceLink'; + resourceLink.description = 'A test resource link for node-hue-api'; + resourceLink.recycle = true; + resourceLink.classid = 100; + resourceLink.addLink('groups', 0); + + return api.resourceLinks.createResourceLink(resourceLink) + .then(createdResourceLink => { + console.log(`${createdResourceLink.toStringDetailed()}`); + + // Delete the created resource link + return api.resourceLinks.deleteResourceLink(createdResourceLink); + }); + }) + .then(deleteResult => { + console.log(`\ndeleted created ResourceLink? ${deleteResult}`); + }) + .catch(err => { + console.error(`Unexpected Error: ${err.message}`); + }) +; diff --git a/examples/v3/resourceLinks/getAllResourceLinks.js b/examples/v3/resourceLinks/getAllResourceLinks.js new file mode 100644 index 0000000..ca6f699 --- /dev/null +++ b/examples/v3/resourceLinks/getAllResourceLinks.js @@ -0,0 +1,29 @@ +'use strict'; + +const v3 = require('../../../index').v3; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3; + +// Replace this with your username for accessing the bridge +const USERNAME = require('../../../test/support/testValues').username; + +// +// This code will obtain all the ResourceLinks from the bridge and display them on the console + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + return api.resourceLinks.getAll(); + }) + .then(resourceLinks => { + resourceLinks.forEach(resourceLink => { + console.log(`${resourceLink.toStringDetailed()}`); + }) + }) + .catch(err => { + console.error(`Unexpected Error: ${err.message}`); + }) +; diff --git a/examples/v3/resourceLinks/getResourceLink.js b/examples/v3/resourceLinks/getResourceLink.js new file mode 100644 index 0000000..1bb3895 --- /dev/null +++ b/examples/v3/resourceLinks/getResourceLink.js @@ -0,0 +1,30 @@ +'use strict'; + +const v3 = require('../../../index').v3; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3; + +// Replace this with your username for accessing the bridge +const USERNAME = require('../../../test/support/testValues').username; + +// Replace this with your desired ID for the ResourceLink you want to retrieve +const RESOURCE_LINK_ID = 62738; + +// +// This code will obtain the specified ResourceLink identified by the RESOURCE_LINK_ID above and display it on the console + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + return api.resourceLinks.get(RESOURCE_LINK_ID); + }) + .then(resourceLink => { + console.log(`${resourceLink.toStringDetailed()}`); + }) + .catch(err => { + console.error(`Unexpected Error: ${err.message}`); + }) +; diff --git a/examples/v3/resourceLinks/updateResourceLink.js b/examples/v3/resourceLinks/updateResourceLink.js new file mode 100644 index 0000000..4b4f294 --- /dev/null +++ b/examples/v3/resourceLinks/updateResourceLink.js @@ -0,0 +1,51 @@ +'use strict'; + +const v3 = require('../../../index').v3; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3; + +const model = v3.model; + +// Replace this with your username for accessing the bridge +const USERNAME = require('../../../test/support/testValues').username; + +// +// This code will create a new ResourceLink on the bridge associated with the group 0 (all lights group) and then +// modify it before finally removing it from the bridge. +// + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + const resourceLink = model.createResourceLink(); + resourceLink.name = 'API Created ResourceLink'; + resourceLink.description = 'A test resource link for node-hue-api'; + resourceLink.recycle = true; + resourceLink.classid = 100; + resourceLink.addLink('groups', 0); + + return api.resourceLinks.createResourceLink(resourceLink) + .then(createdResourceLink => { + // Show details of the created ResourceLink + console.log(`Created ResourceLink:\n${createdResourceLink.toStringDetailed()}`); + + // Modify the name and description on the ResourceLink + createdResourceLink.name = 'Updated name'; + createdResourceLink.description = 'This ResourceLink has been updated after creation'; + + return api.resourceLinks.updateResourceLink(createdResourceLink) + .then(updateResults => { + console.log(`\nUpdated ResourceLink id: ${createdResourceLink.id}\nFields updated:\n${JSON.stringify(updateResults, null, 2)}`); + + // Delete the created/updated resource link + return api.resourceLinks.deleteResourceLink(createdResourceLink); + }); + }); + }) + .catch(err => { + console.error(`Unexpected Error: ${err.message}`); + }) +; diff --git a/examples/v3/rules/createRule.js b/examples/v3/rules/createRule.js index 5c5d184..029fb83 100644 --- a/examples/v3/rules/createRule.js +++ b/examples/v3/rules/createRule.js @@ -4,7 +4,7 @@ const v3 = require('../../../index').v3; // If using this code outside of this library the above should be replaced with // const v3 = require('node-hue-api').v3; -const LightState = v3.lightStates.LightState +const LightState = v3.lightStates.LightState; // Set this to your username for the bridge const USERNAME = require('../../../test/support/testValues').username; @@ -29,24 +29,20 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - const rule = new v3.rules.Rule(); + const rule = new v3.model.createRule(); rule.name = 'node-hue-api test rule'; rule.recycle = true; // All lights group has any light turn on - rule.addCondition(v3.rules.conditions.group(0).when().anyOn().equals(true)); + rule.addCondition(v3.model.ruleConditions.group(0).when().anyOn().equals(true)); // The light with id LIGHT_ID - rule.addAction(v3.rules.actions.light(LIGHT_ID).withState(new LightState().alertShort())); + rule.addAction(v3.model.ruleActions.light(LIGHT_ID).withState(new LightState().alertShort())); return api.rules.createRule(rule) - .then(result => { - console.log(`Created Rule ${result.id}`); - return api.rules.get(result.id); - }) .then(rule => { // Display the details for the rule we created - console.log(rule.toStringDetailed()); + console.log(`Created a Rule\n ${rule.toStringDetailed()}`); // Now remove it, disable this line if you want the rule to remain after running this code return api.rules.deleteRule(rule.id); diff --git a/examples/v3/scenes/createScene.js b/examples/v3/scenes/createScene.js index e7cf9fa..94d5f69 100644 --- a/examples/v3/scenes/createScene.js +++ b/examples/v3/scenes/createScene.js @@ -1,11 +1,11 @@ 'use strict'; const v3 = require('../../../index').v3 - , Scene = v3.Scene + , model = v3.model ; // If using this code outside of this library the above should be replaced with // const v3 = require('node-hue-api').v3 -// , Scene = v3.Scene +// , model = v3.model // ; // Replace this with your username for accessing the bridge @@ -18,12 +18,12 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - const scene = new Scene(); + const scene = model.createLightScene(); // Set the name for the Scene scene.name = 'my-cool-scene'; // Set the target lights for the scene, effectively making this LightScene - scene.lights = [1, 2]; + scene.lights = [2, 3]; // Some custom application data for the scene (only relevant to our application) scene.appdata = { @@ -31,10 +31,13 @@ v3.discovery.nupnpSearch() data: 'my-custom-data' }; - return api.scenes.createScene(scene); - }) - .then(scene => { - console.log(`Created LightScene with id: ${scene.id}`); + return api.scenes.createScene(scene) + .then(scene => { + console.log(`Created LightScene\n${scene.toStringDetailed()}`); + + // Now remove the scene we just created + return api.scenes.deleteScene(scene.id); + }); }) .catch(err => { console.error(`Unexpected Error: ${err.message}`); diff --git a/examples/v3/sensors/createNewSensor.js b/examples/v3/sensors/createNewSensor.js index 47187ae..288425c 100644 --- a/examples/v3/sensors/createNewSensor.js +++ b/examples/v3/sensors/createNewSensor.js @@ -4,7 +4,7 @@ const v3 = require('../../../index').v3; // If using this code outside of this library the above should be replaced with // const v3 = require('node-hue-api').v3; -const CLIPOpenCloseSensor = v3.sensors.clip.OpenClose; +const model = v3.model; // Replace this with your username for accessing the bridge const USERNAME = require('../../../test/support/testValues').username; @@ -20,27 +20,20 @@ v3.discovery.nupnpSearch() }) .then(api => { // Build a new sensor object to save to the bridge - const myOpenCloseSensor = new CLIPOpenCloseSensor({ - modelid: 'node-hue-api software sensor', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api' - }); - // Set a name + const myOpenCloseSensor = model.createCLIPOpenCloseSensor(); + myOpenCloseSensor.modelid = 'node-hue-api software sensor'; + myOpenCloseSensor.swversion = '1.0'; + myOpenCloseSensor.uniqueid = '00:00:00:01'; + myOpenCloseSensor.manufacturername = 'node-hue-api'; + myOpenCloseSensor.name = 'Test Open/Close Sensor'; // Set an initial open state of false myOpenCloseSensor.open = false; // Create the new sensor on the bridge return api.sensors.createSensor(myOpenCloseSensor) - .then(result => { - console.log(`Created new sensor with id: ${result.id}`); - - // Get the new sensor from the bridge - return api.sensors.get(result.id); - }) .then(sensor => { - console.log(sensor.toStringDetailed()); + console.log(`Created a Sensor\n${sensor.toStringDetailed()}`); // Delete the sensor from the bridge return api.sensors.deleteSensor(sensor); diff --git a/examples/v3/sensors/creatingClipSensors.js b/examples/v3/sensors/creatingClipSensors.js index fcb2fdf..efe232c 100644 --- a/examples/v3/sensors/creatingClipSensors.js +++ b/examples/v3/sensors/creatingClipSensors.js @@ -4,167 +4,152 @@ const v3 = require('../../../index').v3; // If using this code outside of this library the above should be replaced with // const v3 = require('node-hue-api').v3; -const clipSensors = v3.sensors.clip; +const model = v3.model; //********************************************************************************************************************** // Create a CLIPGenericFlag Sensor // -const genericFlagSensor = new clipSensors.GenericFlag({ - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api', - name: 'my-generic-flag-sensor' -}); - +const genericFlagSensor = model.createCLIPGenericFlagSensor(); +genericFlagSensor.modelid = 'software'; +genericFlagSensor.swversion = '1.0'; +genericFlagSensor.uniqueid = '00:00:00:01'; +genericFlagSensor.manufacturername = 'node-hue-api'; +genericFlagSensor.name = 'my-generic-flag-sensor'; + +// Set the state attribute, flag genericFlagSensor.flag = false; -// Display the payload of the sensor object that can be stored in the Hue Bridge -console.log(JSON.stringify(genericFlagSensor.payload, null, 2)); +// Display the details of the sensor +console.log(JSON.stringify(genericFlagSensor.getHuePayload(), null, 2)); //********************************************************************************************************************** - //********************************************************************************************************************** // Create a CLIPGenericStatus Sensor // -const genericStatusSensor = new clipSensors.GenericStatus({ - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api', - name: 'my-generic-status-sensor' -}); +const genericStatusSensor = model.createCLIPGenericStatusSensor(); +genericStatusSensor.modelid = 'software'; +genericStatusSensor.swversion = '1.0'; +genericStatusSensor.uniqueid = '00:00:00:01'; +genericStatusSensor.manufacturername = 'node-hue-api'; +genericStatusSensor.name = 'my-generic-status-sensor'; genericStatusSensor.status = 100; // Display the payload of the sensor object that can be stored in the Hue Bridge -console.log(JSON.stringify(genericStatusSensor.payload, null, 2)); +console.log(JSON.stringify(genericStatusSensor.getHuePayload(), null, 2)); //********************************************************************************************************************** - //********************************************************************************************************************** // Create a CLIP Humidity Sensor // -const humiditySensor = new clipSensors.Humidity({ - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api', - name: 'My Humidity Sensor' -}); +const humiditySensor = model.createCLIPHumiditySensor(); +humiditySensor.modelid = 'software'; +humiditySensor.swversion = '1.0'; +humiditySensor.uniqueid = '00:00:00:01'; +humiditySensor.manufacturername = 'node-hue-api'; +humiditySensor.name = 'My Humidity Sensor'; humiditySensor.humidity = 2000; // This is 20% as it stores values in 0.01% steps // Display the payload of the sensor object that can be stored in the Hue Bridge -console.log(JSON.stringify(humiditySensor.payload, null, 2)); +console.log(JSON.stringify(humiditySensor.getHuePayload(), null, 2)); //********************************************************************************************************************** - //********************************************************************************************************************** // Create a CLIP Light Level Sensor // -const lightLevelSensor = new clipSensors.Lightlevel({ - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api', - name: 'Lounge Light Level' -}); +const lightLevelSensor = model.createCLIPLightlevelSensor(); +lightLevelSensor.modelid = 'software'; +lightLevelSensor.swversion = '1.0'; +lightLevelSensor.uniqueid = '00:00:00:01'; +lightLevelSensor.manufacturername = 'node-hue-api'; +lightLevelSensor.name = 'Lounge Light Level'; lightLevelSensor.lightlevel = 0; lightLevelSensor.dark = true; lightLevelSensor.daylight = false; // Display the payload of the sensor object that can be stored in the Hue Bridge -console.log(JSON.stringify(lightLevelSensor.payload, null, 2)); +console.log(JSON.stringify(lightLevelSensor.getHuePayload(), null, 2)); //********************************************************************************************************************** - //********************************************************************************************************************** // Create a CLIP Open Close Sensor // -const openCloseSensor = new clipSensors.OpenClose({ - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api', - name: 'Lounge Door' -}); +const openCloseSensor = model.createCLIPOpenCloseSensor(); +openCloseSensor.modelid = 'software'; +openCloseSensor.swversion = '1.0'; +openCloseSensor.uniqueid = '00:00:00:01'; +openCloseSensor.manufacturername = 'node-hue-api'; +openCloseSensor.name = 'Lounge Door'; openCloseSensor.open = false; // Display the payload of the sensor object that can be stored in the Hue Bridge -console.log(JSON.stringify(openCloseSensor.payload, null, 2)); +console.log(JSON.stringify(openCloseSensor.getHuePayload(), null, 2)); //********************************************************************************************************************** - //********************************************************************************************************************** // Create a CLIP Presence Sensor // -const presenceSensor = new clipSensors.Presence({ - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api', - name: 'Lounge Presence' -}); +const presenceSensor = model.createCLIPPresenceSensor(); +presenceSensor.modelid = 'software'; +presenceSensor.swversion = '1.0'; +presenceSensor.uniqueid = '00:00:00:01'; +presenceSensor.manufacturername = 'node-hue-api'; +presenceSensor.name = 'Lounge Presence'; presenceSensor.presence = true; // Display the payload of the sensor object that can be stored in the Hue Bridge -console.log(JSON.stringify(presenceSensor.payload, null, 2)); +console.log(JSON.stringify(presenceSensor.getHuePayload(), null, 2)); //********************************************************************************************************************** - //********************************************************************************************************************** // Create a CLIP Switch Sensor // -const switchSensor = new clipSensors.Switch({ - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api', - name: 'Lounge Wall Switch' -}); +const switchSensor = model.createCLIPSwitchSensor(); +switchSensor.modelid = 'software'; +switchSensor.swversion = '1.0'; +switchSensor.uniqueid = '00:00:00:01'; +switchSensor.manufacturername = 'node-hue-api'; +switchSensor.name = 'Lounge Wall Switch'; switchSensor.buttonevent = 2000; // Display the payload of the sensor object that can be stored in the Hue Bridge -console.log(JSON.stringify(switchSensor.payload, null, 2)); +console.log(JSON.stringify(switchSensor.getHuePayload(), null, 2)); //********************************************************************************************************************** - //********************************************************************************************************************** // Create a CLIP Temperature Sensor // -const tempSensor = new clipSensors.Temperature({ - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'node-hue-api', - name: 'Lounge Temperature' -}); +const tempSensor = model.createCLIPTemperatureSensor(); +tempSensor.modelid = 'software'; +tempSensor.swversion = '1.0'; +tempSensor.uniqueid = '00:00:00:01'; +tempSensor.manufacturername = 'node-hue-api'; +tempSensor.name = 'Lounge Temperature'; // Set temperature to 38.5 degrees tempSensor.temperature = 3850; // Display the payload of the sensor object that can be stored in the Hue Bridge -console.log(JSON.stringify(tempSensor.payload, null, 2)); - -//********************************************************************************************************************** +console.log(JSON.stringify(tempSensor.getHuePayload(), null, 2)); +//********************************************************************************************************************** \ No newline at end of file diff --git a/examples/v3/sensors/updateSensorState.js b/examples/v3/sensors/updateSensorState.js index 61aa6ff..fda0bdc 100644 --- a/examples/v3/sensors/updateSensorState.js +++ b/examples/v3/sensors/updateSensorState.js @@ -9,9 +9,6 @@ const CLIPOpenCloseSensor = v3.sensors.clip.OpenClose; // Replace this with your username for accessing the bridge const USERNAME = require('../../../test/support/testValues').username; -// Replace with the desired sensor ID that you want to rename -const SENSOR_ID = 1000; - // // This code will create a CLIP Sensor that we can interact with, setting it's state attributes and will then clean up // after itself and remove the Sensor it created. @@ -60,7 +57,7 @@ v3.discovery.nupnpSearch() return api.sensors.updateSensorState(sensor); }) .then(result => { - console.log(`Updated the Sensor, ${sensorId}, state successfully? ${result}`); + console.log(`Updated the Sensor, ${sensorId}, state values updated: ${JSON.stringify(result)}`); // Get the updated sensor object from the Bridge again, it should have a lastupdated attribute for the change now return api.sensors.get(sensorId); diff --git a/hue-api/LightStateShim.js b/hue-api/LightStateShim.js index 01a3aaf..f0a9cfc 100644 --- a/hue-api/LightStateShim.js +++ b/hue-api/LightStateShim.js @@ -1,6 +1,6 @@ 'use strict'; -const LightState = require('../lib/bridge-model/lightstate/LightState') +const LightState = require('../lib/model/lightstate/LightState') ; module.exports = class LightStateShim { diff --git a/hue-api/SceneBuilder.js b/hue-api/SceneBuilder.js index 1e86fd4..502332c 100644 --- a/hue-api/SceneBuilder.js +++ b/hue-api/SceneBuilder.js @@ -1,23 +1,23 @@ 'use strict'; -const Scene = require('../lib/bridge-model/Scene'); +const Scene = require('../lib/model/scenes/Scene'); -const Builder = function() { +const SceneBuilder = function() { this._scene = new Scene(); }; -module.exports = Builder; +module.exports = SceneBuilder; -Builder.prototype.getScene = function() { +SceneBuilder.prototype.getScene = function() { return this._scene; }; -Builder.prototype.withName = function (name) { +SceneBuilder.prototype.withName = function (name) { this._scene.name = name; return this; }; -Builder.prototype.withLights = function (lightIds) { +SceneBuilder.prototype.withLights = function (lightIds) { let ids; if (Array.isArray(lightIds)) { @@ -30,12 +30,12 @@ Builder.prototype.withLights = function (lightIds) { return this; }; -Builder.prototype.withTransitionTime = function (milliseconds) { +SceneBuilder.prototype.withTransitionTime = function (milliseconds) { this._scene.transitiontime = milliseconds; return this; }; -Builder.prototype.withAppData = function (data) { +SceneBuilder.prototype.withAppData = function (data) { let appData; if (data.version) { @@ -50,12 +50,12 @@ Builder.prototype.withAppData = function (data) { return this; }; -Builder.prototype.withPicture = function (picture) { +SceneBuilder.prototype.withPicture = function (picture) { this._scene.picture = picture; return this; }; -Builder.prototype.withRecycle = function (recycle) { +SceneBuilder.prototype.withRecycle = function (recycle) { this._scene.recycle = recycle; return this; }; \ No newline at end of file diff --git a/hue-api/ScheduledEventBuilder.js b/hue-api/ScheduledEventBuilder.js index 1b485f6..ff959d1 100644 --- a/hue-api/ScheduledEventBuilder.js +++ b/hue-api/ScheduledEventBuilder.js @@ -3,8 +3,8 @@ //TODO this is a bit of a mess now and has invalid references, probably document to use new objects... const ApiError = require('../lib/ApiError') - , Schedule = require('../lib/bridge-model/Schedule') - , dateTime = require('../lib/bridge-model/datetime/index') + , Schedule = require('../lib/model/Schedule') + , dateTime = require('../lib/model/datetime/index') ; diff --git a/hue-api/index.js b/hue-api/index.js index ea5a267..9f376fd 100644 --- a/hue-api/index.js +++ b/hue-api/index.js @@ -9,7 +9,7 @@ const utils = require('./utils') , newApi = require('../lib/api/index') , discovery = require('../lib/api/discovery') , LightState = require('./LightStateShim') - , SceneLightState = require('../lib/bridge-model/lightstate/SceneLightState') + , SceneLightState = require('../lib/model/lightstate/SceneLightState') ; function HueApi(config) { diff --git a/lib/HueError.js b/lib/HueError.js index 5575b3d..c66ed07 100644 --- a/lib/HueError.js +++ b/lib/HueError.js @@ -1,5 +1,20 @@ 'use strict'; +//TODO create wrapper types +const ERROR_TYPES = { + 1: 'unauthorized user', + 2: 'body contains invalid JSON', + 3: 'resource not found', + 4: 'method not available for resource', + 5: 'missing paramters in body', + 6: 'parameter not available', + 7: 'invalid value for parameter', + 8: 'parameter not modifiable', + 11: 'too many items in list', + 12: 'portal connection is required', + 901: 'bridge internal error', +} + class HueError { constructor(payload) { @@ -19,7 +34,16 @@ class HueError { } get message() { - return this.payload.message; + let str = this.payload.message + , type = this.type + ; + + if (type === 5 || type === 6) { + // The address makes the error more meaningful + str = `${str}: ${this.address}`; + } + + return str; } get rawError() { diff --git a/lib/api/Api.js b/lib/api/Api.js index b16e310..1c5a33d 100644 --- a/lib/api/Api.js +++ b/lib/api/Api.js @@ -10,6 +10,7 @@ const Capabilities = require('./Capabilities') , Scenes = require('./Scenes') , Remote = require('./Remote') , Rules = require('./Rules') + , ResourceLinks = require('./ResourceLinks') , StateCache = require('./stateCache') // , EntertainmentApi = require('./entertainment/EntertainmentApi') , HueApiConfig = require('./HueApiConfig') @@ -31,6 +32,7 @@ module.exports = class Api { self.scenes = new Scenes(self); self.users = new Users(self); self.rules = new Rules(self); + self.resourceLinks = new ResourceLinks(self); // Add the remote API if this is a remote instance of the API if (self._config.isRemote) { diff --git a/lib/api/Configuration.test.js b/lib/api/Configuration.test.js index ec74412..92ae0c6 100644 --- a/lib/api/Configuration.test.js +++ b/lib/api/Configuration.test.js @@ -7,6 +7,8 @@ const expect = require('chai').expect , testValues = require('../../test/support/testValues.js') //TODO move these ; +//TODO these need updating + describe('Hue API #configuration', () => { let hue; diff --git a/lib/api/Groups.js b/lib/api/Groups.js index 5f03648..7969900 100644 --- a/lib/api/Groups.js +++ b/lib/api/Groups.js @@ -2,6 +2,7 @@ const groupsApi = require('./http/endpoints/groups') , ApiDefinition = require('./http/ApiDefinition.js') + , Group = require('../model/Group') ; @@ -39,24 +40,42 @@ module.exports = class Groups extends ApiDefinition { createGroup(name, lights) { - return this._create({name: name, lights: lights, type: 'LightGroup'}); + const group = new Group(); + group.name = name; + group.lights = lights; + group.type = 'LightGroup'; + + return this._create(group); } createRoom(name, lights, roomClass) { - return this._create({name: name, lights: lights, type: 'Room', room: roomClass}); + const group = new Group(); + group.name = name; + group.lights = lights; + group.type = 'Room'; + group.class = roomClass; + + return this._create(group); } createZone(name, lights, roomClass) { - return this._create({name: name, lights: lights, type: 'Zone', room: roomClass}); - } + const group = new Group(); + group.name = name; + group.lights = lights; + group.type = 'Zone'; + group.class = roomClass; + return this._create(group); + } //TODO support the creation of other groups, Entertainment updateAttributes(id, data) { + //TODO use a group object here? return this.execute(groupsApi.setGroupAttributes, {id: id, groupAttributes: data}); } deleteGroup(id) { + //TODO support a group object? return this.execute(groupsApi.deleteGroup, {id: id}); } @@ -68,7 +87,6 @@ module.exports = class Groups extends ApiDefinition { return this.execute(groupsApi.setGroupState, {id: id, state: state}); } - getLightGroups() { return this._getByType('LightGroup'); } @@ -108,10 +126,10 @@ module.exports = class Groups extends ApiDefinition { }); } - _create(payload) { + _create(group) { const self = this; - return this.execute(groupsApi.createGroup, payload) + return this.execute(groupsApi.createGroup, {group: group}) .then(result => { return self.get(result.id); }); diff --git a/lib/api/Groups.test.js b/lib/api/Groups.test.js index 0e18db6..4f048f1 100644 --- a/lib/api/Groups.test.js +++ b/lib/api/Groups.test.js @@ -4,7 +4,7 @@ const expect = require('chai').expect , v3Api = require('../v3').api , discovery = require('../v3').discovery , testValues = require('../../test/support/testValues.js') //TODO move these - , GroupState = require('../bridge-model/lightstate/GroupState') + , GroupState = require('../model/lightstate/GroupState') ; const TEST_GROUP_NAME = 'd4b3af3ab4df72d10666726d'; @@ -82,7 +82,7 @@ describe('Hue API #groups', () => { it('should fail to resolve a group for an invalid id number', async () => { try { - const result = await hue.groups.get(99999); + const result = await hue.groups.get(65534); expect.fail('should not get here'); } catch (err) { expect(err.message).to.contain('not available'); @@ -95,7 +95,7 @@ describe('Hue API #groups', () => { await hue.groups.get('ab62c6'); expect.fail('should not get here'); } catch (err) { - expect(err.message).to.contain('is not an integer'); + expect(err.message).to.contain('not a parsable number value'); } }); }); @@ -104,10 +104,10 @@ describe('Hue API #groups', () => { describe('#getByName()', () => { it('should get an existing group', async () => { - const allGroups = await hue.groups.getAll(); - const targetGroupName = allGroups[1].name; - - const groups = await hue.groups.getByName(targetGroupName); + const allGroups = await hue.groups.getAll() + , targetGroupName = allGroups[1].name + , groups = await hue.groups.getByName(targetGroupName) + ; expect(groups).to.be.instanceof(Array); expect(groups).to.have.length.greaterThan(0); @@ -224,6 +224,8 @@ describe('Hue API #groups', () => { describe('#updateAttributes()', () => { + //TODO need to deal with tests for updates for room class + describe('groups', () => { const initialGroupName = 'updateGroupTest' @@ -235,6 +237,8 @@ describe('Hue API #groups', () => { beforeEach('createGroup group for update', async () => { const result = await hue.groups.createGroup(initialGroupName, initialGroupLights); groupId = result.id; + + console.log(JSON.stringify(result.getHuePayload(), null, 2)); }); afterEach('delete test group for update', async () => { @@ -280,10 +284,12 @@ describe('Hue API #groups', () => { expect(group).to.have.property('name').to.equal(newName); expect(group).to.have.property('lights').to.have.members(newLights); }); + + //TODO validate that we cannot change the class attribute }); - describe('rooms', () => { + describe.skip('rooms', () => { //TODO need to complete this, need to createGroup a room that does not exist and then update it, including updating the class }); }); @@ -327,4 +333,5 @@ describe('Hue API #groups', () => { expect(groupStatus.action).to.have.property('on').to.be.false; }); }); + }); \ No newline at end of file diff --git a/lib/api/Lights.test.js b/lib/api/Lights.test.js index d71e6bb..17dd359 100644 --- a/lib/api/Lights.test.js +++ b/lib/api/Lights.test.js @@ -30,8 +30,9 @@ describe('Hue API #lights', () => { let light = lights[0]; expect(light).to.have.property('id').to.be.greaterThan(0); - expect(light.bridgeData).to.have - .keys('name', 'modelid', 'type', 'swversion', 'swupdate', 'uniqueid', 'manufacturername', + //TODO this is a pointless test now that we use objects to model the data + expect(light.getHuePayload()).to.have + .keys('id', 'name', 'modelid', 'type', 'swversion', 'swupdate', 'uniqueid', 'manufacturername', 'state', 'capabilities', 'config', 'productname'); }); }); @@ -69,7 +70,7 @@ describe('Hue API #lights', () => { describe('#searchForNew()', () => { - it('should peform a search', async () => { + it('should perform a search', async () => { const result = await hue.lights.searchForNew(); expect(result).to.be.true; }); @@ -142,17 +143,43 @@ describe('Hue API #lights', () => { describe('#rename()', () => { + const renameLightId = 2; + + let originalName; + + beforeEach(async () => { + const light = await hue.lights.getLightById(renameLightId); + originalName = light.name; + }); + + afterEach(async() => { + if (originalName) { + await hue.lights.rename(renameLightId, originalName); + } + }); + it('should rename a light', async () => { - const id = 2 - , newName = 'Lounge Living Color' - , result = await hue.lights.rename(id, newName) - , actual = await hue.lights.getLightAttributesAndState(id); + const newName = 'Lounge Living Color' + , result = await hue.lights.rename(renameLightId, newName) + , actual = await hue.lights.getLightAttributesAndState(renameLightId); expect(result).to.be.true; - expect(actual).to.have.property('id', id); + expect(actual).to.have.property('id', renameLightId); expect(actual).to.have.property('name', newName); }); + + + it('should error is name is too long', async () => { + const newName = `Renamed Light ${Date.now()} ${Date.now()}`; + + try { + await hue.lights.rename(renameLightId, newName); + expect.fail('Should have failed to rename light'); + } catch (err) { + expect(err.message).to.contain('does not meet maximum length requirement'); + } + }); }); diff --git a/lib/api/ResourceLinks.js b/lib/api/ResourceLinks.js new file mode 100644 index 0000000..10f47fb --- /dev/null +++ b/lib/api/ResourceLinks.js @@ -0,0 +1,45 @@ +'use strict'; + +const resourceLinksApi = require('./http/endpoints/resourcelinks') + , ResourceLink = require('../model/ResourceLink') + , ApiDefinition = require('./http/ApiDefinition.js') +; + + +module.exports = class ResourceLinks extends ApiDefinition { + + constructor(hueApi) { + super(hueApi); + } + + getAll() { + return this.execute(resourceLinksApi.getAll); + } + + get(id) { + return this.execute(resourceLinksApi.getResourceLink, {id: id}); + } + + createResourceLink(resourceLink) { + const self = this; + + return self.execute(resourceLinksApi.createResourceLink, {resourceLink: resourceLink}) + .then(result => { + return self.get(result.id); + }); + } + + deleteResourceLink(id) { + let resourceLinkId = id; + if (id instanceof ResourceLink) { + resourceLinkId = id.id; + } + return this.execute(resourceLinksApi.deleteResourceLink, {id: resourceLinkId}); + } + + updateResourceLink(resourceLink) { + return this.execute(resourceLinksApi.updateResourceLink, {id: resourceLink.id, resourceLink: resourceLink}); + } + + //TODO consider adding getByName() +}; \ No newline at end of file diff --git a/lib/api/ResourceLinks.test.js b/lib/api/ResourceLinks.test.js new file mode 100644 index 0000000..707166c --- /dev/null +++ b/lib/api/ResourceLinks.test.js @@ -0,0 +1,170 @@ +'use strict'; + +const expect = require('chai').expect + , v3Api = require('../v3').api + , discovery = require('../v3').discovery + , testValues = require('../../test/support/testValues.js') + , model = require('../model') +; + + +describe('Hue API #resourceLinks', () => { + + let hue; + + before(() => { + return discovery.nupnpSearch() + .then(searchResults => { + const localApi = v3Api.createLocal(searchResults[0].ipaddress); + return localApi.connect(testValues.username) + .then(api => { + hue = api; + }); + }); + }); + + + describe('#getAll()', () => { + + it('should get all resource links', async () => { + const resourceLinks = await hue.resourceLinks.getAll(); + + expect(resourceLinks).to.be.instanceOf(Array); + const resourceLink = resourceLinks[0]; + // console.log(resourceLink.toStringDetailed()); + expect(model.isResourceLinkInstance(resourceLink)).to.be.true; + }); + }); + + + describe('#get()', () => { + + it('should get a resource link that exists', async () => { + const allResourceLinks = await hue.resourceLinks.getAll() + , targetResourceLink = allResourceLinks[allResourceLinks.length - 1] + , resourceLinkId = targetResourceLink.id + ; + + const resourceLink = await hue.resourceLinks.get(resourceLinkId); + expect(model.isResourceLinkInstance(resourceLink)).to.be.true; + expect(resourceLink).to.have.property('id').to.equal(resourceLinkId); + + //TODO need to do a deep equals on contents against targetResourceLink + expect(resourceLink).to.have.property('name').to.equal(targetResourceLink.name); + }); + + //TODO test get failure + }); + + + describe('#createResourceLink()', () => { + + let createdResourceLinkId; + + beforeEach(() => { + createdResourceLinkId = null; + }); + + afterEach(async () => { + if (createdResourceLinkId) { + await hue.resourceLinks.deleteResourceLink(createdResourceLinkId); + } + }); + + it('should create a resource link', async () => { + const resourceLink = model.createResourceLink(); + resourceLink.name = 'Test ResourceLink'; + resourceLink.description = 'A test resource link for node-hue-api'; + resourceLink.recycle = true; + resourceLink.classid = 100; + resourceLink.addLink('groups', 0); + + const result = await hue.resourceLinks.createResourceLink(resourceLink); + expect(model.isResourceLinkInstance(result)).to.be.true; + createdResourceLinkId = result.id; + + expect(result).to.have.property('name').to.equal(resourceLink.name); + expect(result).to.have.property('description').to.equal(resourceLink.description); + expect(result).to.have.property('recycle').to.equal(resourceLink.recycle); + expect(result).to.have.property('classid').to.equal(resourceLink.classid); + + expect(result).to.have.property('links').to.have.property('groups').to.have.members(['0']); + + // Owner should be set on resultant ResourceLinks + expect(result).to.have.property('owner').to.equal(testValues.username); + }); + + + it('should fail to create on an incomplete resource link', async () => { + const resourceLink = model.createResourceLink(); + resourceLink.name = 'Test Resource Link'; + resourceLink.description = 'A test resource link for node-hue-api'; + resourceLink.recycle = true; + + try { + await hue.resourceLinks.createResourceLink(resourceLink); + expect.fail('Should have thrown exception above'); + } catch (err) { + expect(err.message).to.contain('some links defined'); + } + }); + }); + + + describe('#deleteResourceLink()', () => { + + it('should delete an existing resource link', async () => { + const resourceLink = model.createResourceLink(); + resourceLink.name = 'Test ResourceLink to be deleted'; + resourceLink.description = 'A test resource link for node-hue-api'; + resourceLink.recycle = true; + resourceLink.classid = 1; + resourceLink.addLink('groups', 0); + + const createdResourceLink = await hue.resourceLinks.createResourceLink(resourceLink); + expect(createdResourceLink).to.have.property('id'); + + const result = await hue.resourceLinks.deleteResourceLink(createdResourceLink.id); + expect(result).to.be.true; + }); + }); + + + describe('#updateResourceLink()', () => { + + let existingResourceLink; + + beforeEach(async () => { + const resourceLink = model.createResourceLink(); + resourceLink.name = 'Update Resource Link Tests'; + resourceLink.description = 'A test resource link for node-hue-api'; + resourceLink.recycle = true; + resourceLink.classid = 1; + resourceLink.addLink('groups', 0); + + existingResourceLink = await hue.resourceLinks.createResourceLink(resourceLink); + }); + + afterEach(async () => { + if (existingResourceLink) { + await hue.resourceLinks.deleteResourceLink(existingResourceLink); + } + }); + + + it('should update the name of a resource link', async () => { + const newName = `RL ${Date.now()}`; + + existingResourceLink.name = newName; + + const updated = await hue.resourceLinks.updateResourceLink(existingResourceLink); + expect(updated).to.have.property('name').to.be.true; + expect(updated).to.have.property('description').to.be.true; + expect(updated).to.have.property('classid').to.be.true; + expect(updated).to.have.property('links').to.be.true; + + const resourceLink = await hue.resourceLinks.get(existingResourceLink.id); + expect(resourceLink).to.have.property('name').to.equal(newName); + }); + }); +}); \ No newline at end of file diff --git a/lib/api/Rules.js b/lib/api/Rules.js index 276de2c..ebde8c2 100644 --- a/lib/api/Rules.js +++ b/lib/api/Rules.js @@ -1,7 +1,7 @@ 'use strict'; const rulesApi = require('./http/endpoints/rules') - , Rule = require('../bridge-model/rules/Rule') + , Rule = require('../model/rules/Rule') , ApiDefinition = require('./http/ApiDefinition.js') ; @@ -28,7 +28,11 @@ module.exports = class Sensors extends ApiDefinition { // } createRule(rule) { - return this.execute(rulesApi.createRule, {rule: rule}); + const self = this; + return self.execute(rulesApi.createRule, {rule: rule}) + .then(data => { + return self.get(data.id); + }); } deleteRule(id) { diff --git a/lib/api/Rules.test.js b/lib/api/Rules.test.js index 27be1ab..697345c 100644 --- a/lib/api/Rules.test.js +++ b/lib/api/Rules.test.js @@ -3,14 +3,14 @@ const expect = require('chai').expect , v3Api = require('../v3').api , discovery = require('../v3').discovery + , model = require('../v3').model - , CLIPOpenCloseSensor = require('../v3').sensors.clip.OpenClose - - , Rule = require('../bridge-model/rules/Rule') + // , Rule = require('../model/rules/Rule') , LightState = require('../v3').lightStates.LightState - , conditionOperators = require('../v3').rules.conditions.operators + // , conditionOperators = require('../v3').rules.conditions.operators + , conditionOperators = model.ruleConditionOperators - , rules = require('../v3').rules + // , rules = require('../v3').rules , testValues = require('../../test/support/testValues.js') ; @@ -29,16 +29,15 @@ describe('Hue API #rules', () => { hue = api; }) .then(() => { - const sensor = new CLIPOpenCloseSensor({ - name: 'Test Open/Close Sensor', - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'software', - config: { - url: 'http://developers.meethue.com' - } - }); + const sensor = model.createCLIPOpenCloseSensor(); + sensor.name = 'Test Open/Close Sensor'; + sensor.modelid = 'software'; + sensor.swversion = '1.0'; + sensor.uniqueid = '00:00:00:01'; + sensor.manufacturername = 'software'; + sensor.config = { + url: 'http://developers.meethue.com' + }; return hue.sensors.createSensor(sensor); }) .then(result => { @@ -56,7 +55,7 @@ describe('Hue API #rules', () => { try { await hue.sensors.deleteSensor(testOpenCloseSensor.id); } catch (err) { - console.log(`Failed to remove CLIP Sensor, ${testOpenCloseSensor.toString()}, error: ${err}`); + console.error(`Failed to remove CLIP Sensor, ${testOpenCloseSensor.toString()}, error: ${err}`); } } }); @@ -70,9 +69,11 @@ describe('Hue API #rules', () => { expect(results).to.be.instanceOf(Array); expect(results).to.have.length.greaterThan(0); - expect(results[0]).to.be.instanceOf(Rule); - expect(results[0]).to.have.property('name'); - expect(results[0]).to.have.property('owner'); + const rule = results[0]; + + expect(model.isRuleInstance(rule)).to.be.true; + expect(rule).to.have.property('name'); + expect(rule).to.have.property('owner'); //TODO more fields }); @@ -109,12 +110,12 @@ describe('Hue API #rules', () => { }); it('should create a new rule', async () => { - const rule = new Rule(); + const rule = model.createRule(); rule.name = 'Simple Test Rule'; rule.recycle = true; - rule.addCondition(rules.conditions.sensor(testOpenCloseSensor).when('open').changed()); - rule.addAction(new rules.actions.light(testValues.hueLightId).withState(new LightState().on())); + rule.addCondition(model.ruleConditions.sensor(testOpenCloseSensor).when('open').changed()); + rule.addAction(model.ruleActions.light(testValues.hueLightId).withState(new LightState().on())); const result = await hue.rules.createRule(rule); expect(result).to.have.property('id'); @@ -128,11 +129,11 @@ describe('Hue API #rules', () => { let ruleId = null; beforeEach(async () => { - const rule = new Rule(); + const rule = model.createRule(); rule.name = 'Test Rule to be deleted'; rule.recycle = true; - rule.addCondition(rules.conditions.sensor(testOpenCloseSensor).when('open').equals(true)); - rule.addAction(new rules.actions.light(testValues.hueLightId).withState(new LightState().on())); + rule.addCondition(model.ruleConditions.sensor(testOpenCloseSensor).when('open').equals(true)); + rule.addAction(model.ruleActions.light(testValues.hueLightId).withState(new LightState().on())); const result = await hue.rules.createRule(rule); ruleId = result.id; @@ -187,11 +188,11 @@ describe('Hue API #rules', () => { ; beforeEach(async () => { - const newRule = new Rule(); + const newRule = model.createRule(); newRule.name = 'Test Rule to be deleted'; newRule.recycle = true; - newRule.addCondition(rules.conditions.sensor(testOpenCloseSensor).when('open').equals(true)); - newRule.addAction(new rules.actions.light(testValues.hueLightId).withState(new LightState().on())); + newRule.addCondition(model.ruleConditions.sensor(testOpenCloseSensor).when('open').equals(true)); + newRule.addAction(model.ruleActions.light(testValues.hueLightId).withState(new LightState().on())); const result = await hue.rules.createRule(newRule); ruleId = result.id; @@ -226,7 +227,7 @@ describe('Hue API #rules', () => { it('should update conditions', async () => { rule.resetConditions(); - rule.addCondition(rules.conditions.sensor(testOpenCloseSensor).when('open').equals(false)); + rule.addCondition(model.ruleConditions.sensor(testOpenCloseSensor).when('open').equals(false)); const result = await hue.rules.updateRule(rule); expect(result).to.have.property('name').to.be.true; @@ -244,7 +245,7 @@ describe('Hue API #rules', () => { it('should add an action', async () => { rule.resetActions(); - rule.addAction(rules.actions.light(0).withState(new LightState().on(false))); + rule.addAction(model.ruleActions.light(0).withState(new LightState().on(false))); const result = await hue.rules.updateRule(rule); expect(result).to.have.property('name').to.be.true; diff --git a/lib/api/Scenes.js b/lib/api/Scenes.js index 5948657..e2d8cca 100644 --- a/lib/api/Scenes.js +++ b/lib/api/Scenes.js @@ -2,7 +2,7 @@ const scenesApi = require('./http/endpoints/scenes') , ApiDefinition = require('./http/ApiDefinition') - , GroupLightState = require('../bridge-model/lightstate/GroupState') + , GroupLightState = require('../model/lightstate/GroupState') ; module.exports = class Scenes extends ApiDefinition { @@ -22,7 +22,12 @@ module.exports = class Scenes extends ApiDefinition { } createScene(scene) { - return this.execute(scenesApi.createScene, {scene: scene}); + const self = this; + + return this.execute(scenesApi.createScene, {scene: scene}) + .then(data => { + return self.get(data.id); + }); } get(id) { diff --git a/lib/api/Scenes.test.js b/lib/api/Scenes.test.js index 5bbbff7..6a1d0b9 100644 --- a/lib/api/Scenes.test.js +++ b/lib/api/Scenes.test.js @@ -3,8 +3,8 @@ const expect = require('chai').expect , v3Api = require('../v3').api , discovery = require('../v3').discovery - , Scene = require('../bridge-model/Scene') - , SceneLightState = require('../bridge-model/lightstate/SceneLightState') + , model = require('../model') + , SceneLightState = require('../model/lightstate/SceneLightState') , testValues = require('../../test/support/testValues.js') , ApiError = require('../ApiError') ; @@ -33,30 +33,27 @@ describe('Hue API #scenes', () => { expect(results).to.be.instanceOf(Array); expect(results).to.have.length.greaterThan(10); - expect(results[0]).to.be.instanceOf(Scene); - expect(results[0]).to.have.property('id'); - expect(results[0]).to.have.property('locked'); - expect(results[0]).to.have.property('name'); - expect(results[0]).to.have.property('owner'); - expect(results[0]).to.have.property('appdata'); + const scene = results[0]; + expect(model.isSceneInstance(scene)).to.be.true; }); }); describe('#get()', () => { - let validSceneId; + let targetScene; before(async () => { const scenes = await hue.scenes.getAll(); - validSceneId = scenes[0].id; + targetScene = scenes[0]; }); it('should get a specific scene', async () => { - const scene = await hue.scenes.get(validSceneId); + const scene = await hue.scenes.get(targetScene.id); - expect(scene).to.be.instanceof(Scene); - expect(scene).to.have.property('id').to.equal(validSceneId); + expect(model.isSceneInstance(scene)).to.be.true; + expect(scene).to.have.property('id').to.equal(targetScene.id); + expect(scene).to.have.property('type').to.equal(targetScene.type); }); it('should fail for invalid scene id', async () => { @@ -84,7 +81,7 @@ describe('Hue API #scenes', () => { const results = await hue.scenes.getByName(validSceneName); expect(results).to.be.instanceof(Array); - expect(results).to.have.length(1) + expect(results).to.have.length(1); expect(results[0]).to.have.property('name').to.equal(validSceneName); }); @@ -129,7 +126,7 @@ describe('Hue API #scenes', () => { let id; beforeEach(async () => { - const scene = new Scene(); + const scene = model.createLightScene(); scene.name = 'test-delete'; scene.lights = [2]; @@ -151,12 +148,11 @@ describe('Hue API #scenes', () => { let scene; beforeEach(async () => { - const newScene = new Scene(); + const newScene = model.createLightScene(); newScene.name = 'testing-update'; newScene.lights = SCENE_LIGHT_IDS; scene = await hue.scenes.createScene(newScene); - }); afterEach(async () => { @@ -169,12 +165,10 @@ describe('Hue API #scenes', () => { describe('#updateScene()', () => { it('should update an existing scene name', async () => { - const updatedName = 'testing-update-name' - , update = new Scene() - ; - update.name = updatedName; + const updatedName = 'testing-update-name'; + scene.name = updatedName; - const result = await hue.scenes.update(scene.id, update); + const result = await hue.scenes.update(scene.id, scene); expect(result).to.have.property('name').to.be.true; const updatedScene = await hue.scenes.get(scene.id); @@ -203,7 +197,7 @@ describe('Hue API #scenes', () => { let scene; beforeEach(async () => { - const newScene = new Scene(); + const newScene = model.createLightScene(); newScene.name = 'testing-update'; newScene.lights = SCENE_LIGHT_IDS; diff --git a/lib/api/Schedules.js b/lib/api/Schedules.js index 7eb598a..a74f56d 100644 --- a/lib/api/Schedules.js +++ b/lib/api/Schedules.js @@ -17,6 +17,7 @@ module.exports = class Schedules extends ApiDefinition { createSchedule(schedule) { //TODO convert to schedule if possible here return this.execute(schedulesApi.createSchedule, {schedule: schedule}); + //TODO return the actual Schedule model object using a get } get(id) { diff --git a/lib/api/Sensors.js b/lib/api/Sensors.js index f95b7d6..f26f74c 100644 --- a/lib/api/Sensors.js +++ b/lib/api/Sensors.js @@ -1,7 +1,7 @@ 'use strict'; const sensorsApi = require('./http/endpoints/sensors') - , Sensor = require('../bridge-model/devices/sensors/Sensor.js') + , Sensor = require('../model/sensors/Sensor.js') , ApiDefinition = require('./http/ApiDefinition.js') ; @@ -33,7 +33,12 @@ module.exports = class Sensors extends ApiDefinition { } createSensor(sensor) { - return this.execute(sensorsApi.createSensor, {sensor: sensor}); + const self = this; + + return self.execute(sensorsApi.createSensor, {sensor: sensor}) + .then(data => { + return self.get(data.id); + }); } deleteSensor(id) { diff --git a/lib/api/Sensors.test.js b/lib/api/Sensors.test.js index 7887d47..2caea9d 100644 --- a/lib/api/Sensors.test.js +++ b/lib/api/Sensors.test.js @@ -4,8 +4,8 @@ const expect = require('chai').expect , v3Api = require('../v3').api , discovery = require('../v3').discovery , testValues = require('../../test/support/testValues.js') //TODO move these - , Sensor = require('../bridge-model/devices/sensors/Sensor') - , CLIPOpenClose = require('../bridge-model/devices/sensors/CLIPOpenClose') + , Sensor = require('../model/sensors/Sensor') + , CLIPOpenClose = require('../model/sensors/CLIPOpenClose') ; @@ -25,17 +25,18 @@ describe('Hue API #sensors', () => { }); - function getSensorData(name) { - return { - name: name, - modelid: 'software', - swversion: '1.0', - uniqueid: '00:00:00:01', - manufacturername: 'software', - config: { - url: 'http://developers.meethue.com' - } + function createClipOpenCloseSensor(name) { + const sensor = new CLIPOpenClose(); + sensor.name = name; + sensor.modelid = 'software'; + sensor.swversion = '1.0'; + sensor.uniqueid = '00:00:00:01'; + sensor.manufacturername = 'software'; + sensor.config = { + url: 'http://developers.meethue.com' }; + + return sensor; } describe('#getAll()', () => { @@ -102,7 +103,7 @@ describe('Hue API #sensors', () => { }); it('should create a new sensor', async () => { - const sensor = new CLIPOpenClose(getSensorData('testSoftwareSensor')) + const sensor = createClipOpenCloseSensor('testSoftwareSensor') , result = await hue.sensors.createSensor(sensor) , createdSensor = await hue.sensors.get(result.id) ; @@ -124,7 +125,7 @@ describe('Hue API #sensors', () => { let sensorId; beforeEach('', async () => { - const sensor = new CLIPOpenClose(getSensorData('updateNameTest')) + const sensor = createClipOpenCloseSensor('updateNameTest') , result = await hue.sensors.createSensor(sensor) ; sensorId = result.id; @@ -153,7 +154,7 @@ describe('Hue API #sensors', () => { let sensorId; beforeEach('', async () => { - const sensor = new CLIPOpenClose(getSensorData('testSensorConfig')) + const sensor = createClipOpenCloseSensor('testSensorConfig') , result = await hue.sensors.createSensor(sensor) ; sensorId = result.id; @@ -167,17 +168,18 @@ describe('Hue API #sensors', () => { it('should update the config', async () => { const sensor = await hue.sensors.get(sensorId); - expect(sensor).to.have.property('on', true); + expect(sensor).to.have.property('on'); + const initalOnState = sensor.on; // Update some configuration values - sensor.on = false; + sensor.on = !initalOnState; const result = await hue.sensors.updateSensorConfig(sensor) , updatedSensor = await hue.sensors.get(sensor.id) ; expect(result).to.be.true; - expect(updatedSensor).to.have.property('on', false); + expect(updatedSensor).to.have.property('on', !initalOnState); }); }); @@ -187,7 +189,7 @@ describe('Hue API #sensors', () => { let sensorId; beforeEach('', async () => { - const sensor = new CLIPOpenClose(getSensorData('testSensorConfig')) + const sensor = createClipOpenCloseSensor('testSensorConfig') , result = await hue.sensors.createSensor(sensor) ; sensorId = result.id; @@ -198,9 +200,8 @@ describe('Hue API #sensors', () => { }); - it('should update the state', async () => { + it('should update the state', async function () { const sensor = await hue.sensors.get(sensorId); - expect(sensor).to.have.property('open', false); // Update some state values @@ -209,14 +210,13 @@ describe('Hue API #sensors', () => { const result = await hue.sensors.updateSensorState(sensor) , updatedSensor = await hue.sensors.get(sensor.id) ; - - expect(result).to.be.true; + expect(result).to.have.property('open').to.be.true; expect(updatedSensor).to.have.property('open', true); expect(updatedSensor).to.have.property('lastupdated').to.exist; }); - it('should update a subset of states when filtered', async() => { + it('should update a subset of states when filtered', async () => { const sensor = await hue.sensors.get(sensorId); expect(sensor).to.have.property('open', false); @@ -229,7 +229,7 @@ describe('Hue API #sensors', () => { , updatedSensor = await hue.sensors.get(sensor.id) ; - expect(result).to.be.true; + expect(Object.keys(result)).to.have.length(0); expect(updatedSensor).to.have.property('open', false); expect(updatedSensor).to.have.property('lastupdated').to.exist; }); diff --git a/lib/api/Users.test.js b/lib/api/Users.test.js index 99d0a15..b443081 100644 --- a/lib/api/Users.test.js +++ b/lib/api/Users.test.js @@ -82,7 +82,7 @@ describe('Hue API #users', () => { it('should get all the users', async () => { const allUsers = await authenticatedHue.users.getAll(); - console.log(JSON.stringify(allUsers, null, 2)); + // console.log(JSON.stringify(allUsers, null, 2)); expect(allUsers).to.be.instanceof(Array); diff --git a/lib/api/http/endpoints/groups.js b/lib/api/http/endpoints/groups.js index 51134b2..0dafc8a 100644 --- a/lib/api/http/endpoints/groups.js +++ b/lib/api/http/endpoints/groups.js @@ -4,55 +4,10 @@ const ApiEndpoint = require('./endpoint') , ApiError = require('../../../ApiError') , util = require('../util') , GroupIdPlaceholder = require('../placeholders/GroupIdPlaceholder') - , Group = require('../../../bridge-model/Group') - , GroupState = require('../../../bridge-model/lightstate/GroupState') + , model = require('../../../model') + , GroupState = require('../../../model/lightstate/GroupState') ; -// Valid classes for room class in the bridge -const ROOM_CLASSES = [ - 'Living room', - 'Kitchen', - 'Dining', - 'Bedroom', - 'Kids bedroom', - 'Bathroom', - 'Nursery', - 'Recreation', - 'Office', - 'Gym', - 'Hallway', - 'Toilet', - 'Front door', - 'Garage', - 'Terrace', - 'Garden', - 'Driveway', - 'Carport', - 'Other', - // The following are valid in 1.30 and higher of the API - 'Home', - 'Downstairs', - 'Upstairs', - 'Top floor', - 'Attic', - 'Guest room', - 'Staircase', - 'Lounge', - 'Man cave', - 'Computer', - 'Studio', - 'Music', - 'TV', - 'Reading', - 'Closet', - 'Storage', - 'Laundry room', - 'Balcony', - 'Porch', - 'Barbecue', - 'Pool', -]; - module.exports = { getAllGroups: new ApiEndpoint() @@ -111,11 +66,13 @@ module.exports = { .postProcess(util.wasSuccessful), }; + function buildGroupsResult(result) { const groups = []; Object.keys(result).forEach(groupId => { - groups.push(new Group(result[groupId], groupId)); + const group = model.createFromBridge('group', groupId, result[groupId]); + groups.push(group); }); return groups; @@ -151,19 +108,29 @@ function buildGroupStateBody(data) { } function buildGroupBody(parameters) { + const group = parameters.group; + + if (! group) { + throw new ApiError('A group must be provided'); + } + const result = { type: 'application/json', body: { - name: parameters.name, - lights: parameters.lights ? asStringArray(parameters.lights): [], - type: parameters.type, + name: group.name, + type: group.type, } }; - if (parameters.type === 'Room' || parameters.type === 'Zone') { - result.body.class = _validateRoom(parameters.room); - } else if (parameters.type === 'LightGroup') { - result.body.recycle = getRecycle(parameters); + const lights = util.asStringArray(group.lights); + if (lights) { + result.body.lights = lights; + } + + if (group.type === 'Room' || group.type === 'Zone') { + result.body.class = group.class; + } else if (group.type === 'LightGroup') { + result.body.recycle = group.recycle; } return result; @@ -173,20 +140,18 @@ function buildGroupAttributeBody(parameters) { const body = {} , groupAttributes = parameters ? parameters.groupAttributes : null; - //TODO may be better suited to a class here - if (groupAttributes) { - if (groupAttributes.name) { - body.name = groupAttributes.name; // TODO string 0..32 - } + const group = model.createFromBridge('group', null, groupAttributes) + , payload = group.getHuePayload() + ; - if (groupAttributes.lights) { - body.lights = asStringArray(groupAttributes.lights); //TODO array of at least one element and must be an existing light otherwise error 7 returned + ['name', 'class'].forEach(attr => { + if (groupAttributes[attr]) { + body[attr] = payload[attr]; } + }); - if (groupAttributes.class) { - //TODO test what happens if you do this on something that is not a room - body.class = _validateRoom(groupAttributes.class); - } + if (payload.lights) { + body.lights = util.asStringArray(payload.lights); //TODO array of at least one element and must be an existing light otherwise error 7 returned } return { @@ -205,52 +170,14 @@ function buildStreamBody(parameters) { return { type: 'application/json', body: body - } + }; } -function getRecycle(parameters) { - if (parameters) { - if (Object.hasOwnProperty('recycle')) { - return parameters.recycle; - } - } - return true; -} function buildGroup(data, requestParameters) { - if (requestParameters) { - return new Group(data, requestParameters.id); - } else { - return new Group(data); - } -} - -//TODO a utils function -function asStringArray(value) { - if (Array.isArray(value)) { - const result = []; - - value.forEach(val => { - result.push(`${val}`); - }); - - return result; - } else { - return [`${value}`]; - } + return model.createFromBridge('group', requestParameters.id, data); } -function _validateRoom(room) { - if (!room) { - room = 'Other'; - } - - if (ROOM_CLASSES.includes(room)) { - return room; - } else { - throw new ApiError(`${room} is not a valid class.`); - } -} function validateGroupDeletion(result) { if (!util.wasSuccessful(result)) { diff --git a/lib/api/http/endpoints/lights.js b/lib/api/http/endpoints/lights.js index 2b93739..02df359 100644 --- a/lib/api/http/endpoints/lights.js +++ b/lib/api/http/endpoints/lights.js @@ -1,14 +1,14 @@ -'use strict'; - const ApiEndpoint = require('./endpoint') , LightIdPlaceholder = require('../placeholders/LightIdPlaceholder') - , LightState = require('../../../bridge-model/lightstate/LightState') + , LightState = require('../../../model/lightstate/LightState') , ApiError = require('../../../ApiError') , util = require('../util') - , lightBuilder = require('../../../bridge-model/devices/lights') + , model = require('../../../model') , rgb = require('../../../rgb') ; +'use strict'; + module.exports = { getAllLights: new ApiEndpoint() @@ -73,7 +73,8 @@ function buildLightsResult(result) { if (result) { Object.keys(result).forEach(function (id) { - lights.push(lightBuilder.create(result[id], id)); + const light = model.createFromBridge('light', id, result[id]); + lights.push(light); }); } @@ -81,20 +82,13 @@ function buildLightsResult(result) { } function buildLightNamePayload(parameters) { - const nameMaxLength = 32 - , result = {type: 'application/json'} - , name = parameters['name'] - ; - - if (name && name.length > 0) { - result.body = {name: name}; - - if (name.length > nameMaxLength) { - throw new ApiError('Light name is too long'); - } - } + // Set the name on a Light instance so that it can be validated using parameter constraints there + const light = model.createFromBridge('light', 0, parameters); - return result; + return { + type: 'application/json', + body: {name: light.name}, + }; } function injectLightId(result, requestParameters) { diff --git a/lib/api/http/endpoints/resourcelinks.js b/lib/api/http/endpoints/resourcelinks.js new file mode 100644 index 0000000..9fc3e9a --- /dev/null +++ b/lib/api/http/endpoints/resourcelinks.js @@ -0,0 +1,131 @@ +'use strict'; + +const ApiEndpoint = require('./endpoint') + , ResourceLinkPlaceholder = require('../placeholders/ResourceLinkPlaceholder') + , model = require('../../../model') + , ApiError = require('../../../ApiError') + , util = require('../util') +; + +module.exports = { + + getAll: new ApiEndpoint() + .get() + .uri('//resourcelinks') + .acceptJson() + .pureJson() + .postProcess(buildResourceLinkResults), + + getResourceLink: new ApiEndpoint() + .get() + .uri('//resourcelinks/') + .placeholder(new ResourceLinkPlaceholder()) + .acceptJson() + .pureJson() + .postProcess(buildResourceLink), + + createResourceLink: new ApiEndpoint() + .post() + .uri('//resourcelinks') + .payload(createResourceLinkPayload) + .acceptJson() + .pureJson() + .postProcess(buildCreateResourceLinkResult), + + updateResourceLink: new ApiEndpoint() + .put() + .uri('//resourcelinks/') + .placeholder(new ResourceLinkPlaceholder()) + .payload(buildResourceLinkUpdatePayload) + .acceptJson() + .pureJson() + .postProcess(util.extractUpdatedAttributes), + + deleteResourceLink: new ApiEndpoint() + .delete() + .uri('//resourcelinks/') + .placeholder(new ResourceLinkPlaceholder()) + .acceptJson() + .pureJson() + .postProcess(util.wasSuccessful) +}; + + +function buildResourceLinkResults(data) { + const resourceLinks = []; + + if (data) { + Object.keys(data).forEach(id => { + resourceLinks.push(model.createFromBridge('resourcelink', id, data[id])); + }); + } + + return resourceLinks; +} + +function buildResourceLink(data, requestParameters) { + if (data) { + return model.createFromBridge('resourcelink', requestParameters.id, data); + } + return null; +} + +function buildCreateResourceLinkResult(result) { + const hueErrors = util.parseErrors(result); //TODO not sure if this still gets called as the request handles some of this + + if (hueErrors) { + throw new ApiError(`Error creating resourcelink: ${hueErrors[0].description}`, hueErrors[0]); + } + + return {id: Number(result[0].success.id)}; +} + +function buildResourceLinkUpdatePayload(parameters) { + const resourceLink = parameters.resourceLink; + + if (!resourceLink) { + throw new ApiError('No ResourceLink provided'); + } else if (!model.isResourceLinkInstance(resourceLink)) { + throw new ApiError('Must provide a valid ResourceLink object'); + } + + const body = buildResourceLinkBody(resourceLink); + // Cannot change the owner, type or recycle values + delete body.type; + delete body.recycle; + delete body.owner; + + return { + type: 'application/json', + body: body + }; +} + +function createResourceLinkPayload(parameters) { + const resourceLink = parameters.resourceLink; + + if (!resourceLink) { + throw new ApiError('No ResourceLink provided'); + } else if (!model.isResourceLinkInstance(resourceLink)) { + throw new ApiError('Must provide a valid ResourceLink object'); + } + + const body = buildResourceLinkBody(resourceLink); + if (!body.links || body.links.length === 0) { + throw new ApiError('You must provide a ResourceLink with some links defined'); + } + + return { + type: 'application/json', + body: body + }; +} + +function buildResourceLinkBody(resourceLink) { + const data = resourceLink.getHuePayload(); + + // Cannot have an ID in the create payload + delete data.id; + + return data; +} \ No newline at end of file diff --git a/lib/api/http/endpoints/rules.js b/lib/api/http/endpoints/rules.js index 3a31fc0..a37b957 100644 --- a/lib/api/http/endpoints/rules.js +++ b/lib/api/http/endpoints/rules.js @@ -2,7 +2,7 @@ const ApiEndpoint = require('./endpoint') , RuleIdPlaceholder = require('../placeholders/RuleIdPlaceholder') - , Rule = require('../../../bridge-model/rules/Rule') + , model = require('../../../model') , ApiError = require('../../../ApiError') , util = require('../util') ; @@ -14,7 +14,7 @@ module.exports = { .acceptJson() .uri('//rules') .pureJson() - .postProcess(buildRuleResults), + .postProcess(createAllRulesResult), getRule: new ApiEndpoint() .get() @@ -22,15 +22,15 @@ module.exports = { .uri('/rules/') .placeholder(new RuleIdPlaceholder()) .pureJson() - .postProcess(buildRuleResult), + .postProcess(createGetRuleResult), createRule: new ApiEndpoint() .post() .acceptJson() .uri('//rules') .pureJson() - .payload(buildRulePayload) - .postProcess(buildCreateRuleResult), + .payload(createRulePayload) + .postProcess(getCreateRuleResult), updateRule: new ApiEndpoint() .put() @@ -38,7 +38,7 @@ module.exports = { .uri('//rules/') .placeholder(new RuleIdPlaceholder()) .pureJson() - .payload(buildRuleUpdatePayload) + .payload(createRuleUpdatePayload) .postProcess(util.extractUpdatedAttributes), deleteRule: new ApiEndpoint() @@ -51,23 +51,19 @@ module.exports = { }; - -function buildRuleResults(result) { +function createAllRulesResult(result) { let rules = []; Object.keys(result).forEach(function (id) { - rules.push(new Rule(result[id], id)); + const rule = model.createFromBridge('rule', id, result[id]); + rules.push(rule); }); return rules; } -function buildRuleResult(data, requestParameters) { - if (requestParameters) { - return new Rule(data, requestParameters.id); - } else { - return new Rule(data); - } +function createGetRuleResult(data, requestParameters) { + return model.createFromBridge('rule', requestParameters.id, data); } function validateRuleDeletion(result) { @@ -77,35 +73,17 @@ function validateRuleDeletion(result) { return true; } -function buildRulePayload(parameters) { +function createRulePayload(parameters) { const rule = parameters.rule; if (!rule) { throw new ApiError('No rule provided'); - } else if (!(rule instanceof Rule)) { + } else if (!model.isRuleInstance(rule)) { throw new ApiError('Must provide a valid Rule object'); } - // Recycle can only be set upon creation - const body = { - name: clipName(rule.name), - recycle: rule.recycle || false, - }; - - body.conditions = rule.getConditionsPayload(); - body.actions = rule.getActionsPayload(); - - if (!body.name) { - throw new ApiError('A name must be provided for the Rule'); - } - - if (!body.conditions || body.conditions.length < 1) { - throw new ApiError('A Rule must have at least one condition'); - } - - if (!body.actions || body.actions.length < 1) { - throw new ApiError('A Rule must have at least one action'); - } + const body = rule.getHuePayload(); + validateRulePayloadForBridge(body); return { type: 'application/json', @@ -113,29 +91,23 @@ function buildRulePayload(parameters) { }; } -function buildRuleUpdatePayload(parameters) { +function createRuleUpdatePayload(parameters) { const rule = parameters.rule; if (!rule) { throw new ApiError('No rule provided'); - } else if (!(rule instanceof Rule)) { + } else if (!model.isRuleInstance(rule)) { throw new ApiError('Must provide a valid Rule object'); } - const body = { - name: clipName(rule.name) - }; + const hueRule = rule.getHuePayload() + , body = { + name: hueRule.name, + conditions: hueRule.conditions, + actions: hueRule.actions + }; - body.conditions = rule.getConditionsPayload(); - body.actions = rule.getActionsPayload(); - - if (!body.conditions || body.conditions.length < 1) { - throw new ApiError('A Rule must have at least one condition'); - } - - if (!body.actions || body.actions.length < 1) { - throw new ApiError('A Rule must have at least one action'); - } + validateRulePayloadForBridge(body); return { type: 'application/json', @@ -143,7 +115,7 @@ function buildRuleUpdatePayload(parameters) { }; } -function buildCreateRuleResult(result) { +function getCreateRuleResult(result) { const hueErrors = util.parseErrors(result); //TODO not sure if this still gets called as the request handles some of this if (hueErrors) { @@ -153,12 +125,16 @@ function buildCreateRuleResult(result) { return {id: result[0].success.id}; } -//TODO should look to use parameters on the Rule instance to capture this earlier -function clipName(name) { - if (name) { - if (name.length > 32) { - return name.substring(0, 31); - } +function validateRulePayloadForBridge(rule) { + if (!rule.name) { + throw new ApiError('A name must be provided for the Rule'); + } + + if (!rule.conditions || rule.conditions.length < 1) { + throw new ApiError('A Rule must have at least one condition'); + } + + if (!rule.actions || rule.actions.length < 1) { + throw new ApiError('A Rule must have at least one action'); } - return name; } \ No newline at end of file diff --git a/lib/api/http/endpoints/scenes.js b/lib/api/http/endpoints/scenes.js index 2cc7c54..939955b 100644 --- a/lib/api/http/endpoints/scenes.js +++ b/lib/api/http/endpoints/scenes.js @@ -3,8 +3,8 @@ const ApiEndpoint = require('./endpoint') , SceneIdPlaceholder = require('../placeholders/SceneIdPlaceholder') , LightIdPlacehoder = require('../placeholders/LightIdPlaceholder') - , Scene = require('../../../bridge-model/Scene') - , SceneLightState = require('../../../bridge-model/lightstate/SceneLightState') + , model = require('../../../model') + , SceneLightState = require('../../../model/lightstate/SceneLightState') , ApiError = require('../../../ApiError') , util = require('../util') ; @@ -23,7 +23,7 @@ module.exports = { .acceptJson() .uri('//scenes') .pureJson() - .payload(buildScenePayload) + .payload(getCreateScenePayload) .postProcess(buildCreateSceneResult), updateScene: new ApiEndpoint() @@ -67,18 +67,21 @@ function buildScenesResult(result) { let scenes = []; Object.keys(result).forEach(function (id) { - scenes.push(new Scene(result[id], id)); + const data = result[id] + , type = data.type.toLowerCase() + ; + console.log(JSON.stringify(data, null, 2)); + + const scene = model.createFromBridge(type, id, data); + scenes.push(scene); }); return scenes; } function buildSceneResult(data, requestParameters) { - if (requestParameters) { - return new Scene(data, requestParameters.id); - } else { - return new Scene(data); - } + const type = data.type.toLowerCase(); + return model.createFromBridge(type, requestParameters.id, data); } function validateSceneDeletion(result) { @@ -88,23 +91,22 @@ function validateSceneDeletion(result) { return true; } -function buildScenePayload(parameters) { +function getCreateScenePayload(parameters) { const scene = parameters.scene; if (!scene) { throw new ApiError('No scene provided'); - } else if (!(scene instanceof Scene)) { + } else if (!model.isSceneInstance(scene)) { throw new ApiError('Must provide a valid Scene object'); } - const body = scene.payload; - // Recycle is a required parameter when creating a new scene - if (!body.recycle) { - body.recycle = false; - } - - // Prevent the create from overwriting an existing scene by removing the id value + const body = scene.getHuePayload(); + // Remove properties that are not used is creation delete body.id; + delete body.locked; + delete body.owner; + delete body.lastupdated; + delete body.version; return { type: 'application/json', @@ -136,14 +138,22 @@ function buildBasicSceneUpdatePayload(parameters) { if (!scene) { throw new ApiError('No scene provided'); - } else if (!(scene instanceof Scene)) { + } else if (!model.isSceneInstance(scene)) { throw new ApiError('Must provide a valid Scene object'); } - const body = scene.payload; + const scenePayload = scene.getHuePayload() + , body = {} + ; + + // Extract the properties that we are allowed to update as per the API docs + ['name', 'lights', 'lightstates', 'storelightstate'].forEach(key => { + const value = scenePayload[key]; - // It is not possible to modify the type, so remove it if present - delete body.type; + if (value !== null) { + body[key] = value; + } + }); return { type: 'application/json', diff --git a/lib/api/http/endpoints/schedules.js b/lib/api/http/endpoints/schedules.js index 7290a58..6e99316 100644 --- a/lib/api/http/endpoints/schedules.js +++ b/lib/api/http/endpoints/schedules.js @@ -2,7 +2,7 @@ const ApiEndpoint = require('./endpoint') , ScheduleIdPlaceholder = require('../placeholders/ScheduleIdPlaceholder') - , Schedule = require('../../../bridge-model/Schedule') + , model = require('../../../model') , ApiError = require('../../../ApiError') , util = require('../util') ; @@ -87,7 +87,7 @@ function buildSchedulePayload(parameters) { if (!schedule) { throw new ApiError('Schedule to create must be provided'); - } else if (!(schedule instanceof Schedule)) { + } else if (!model.isSceneInstance(schedule)) { throw new ApiError('You must provide a valid instance of a Schedule to be created'); } @@ -102,7 +102,7 @@ function buildScheduleAttributeBody(parameters) { , updatedSchedule = parameters ? parameters.schedule : null; if (updatedSchedule) { - if (updatedSchedule instanceof Schedule) { + if (model.isScheduleInstance(updatedSchedule)) { Object.assign(body, updatedSchedule.payload); } else { Object.assign(body, updatedSchedule);//TODO need to convert to schedule then validate diff --git a/lib/api/http/endpoints/sensors.js b/lib/api/http/endpoints/sensors.js index bf0f0e4..9b216e2 100644 --- a/lib/api/http/endpoints/sensors.js +++ b/lib/api/http/endpoints/sensors.js @@ -1,9 +1,8 @@ 'use strict'; const ApiEndpoint = require('./endpoint') - , Sensor = require('../../../bridge-model/devices/sensors/Sensor') , SensorIdPlaceholder = require('../placeholders/SensorIdPlaceholder') - , sensorBuilder = require('../../../bridge-model/devices/sensors') + , model = require('../../../model') , ApiError = require('../../../ApiError') , util = require('../util') ; @@ -80,7 +79,7 @@ module.exports = { .payload(buildSensorStatePayload) .acceptJson() .pureJson() - .postProcess(util.wasSuccessful), + .postProcess(util.extractUpdatedAttributes), }; @@ -89,13 +88,16 @@ function buildSensorPayload(parameters) { if (!sensor) { throw new ApiError('Sensor to create must be provided'); - } else if (!(sensor instanceof Sensor)) { + } else if (!model.isSensorInstance(sensor)) { throw new ApiError('You must provide a valid instance of a Sensor to be created'); } + const payload = sensor.getHuePayload(); + delete payload.id; + return { type: 'application/json', - body: sensor.payload + body: payload }; } @@ -104,7 +106,11 @@ function buildAllSensorsResult(data) { if (data) { Object.keys(data).forEach(id => { - result.push(sensorBuilder.build(data[id], id)); + const sensorData = data[id] + , type = sensorData.type.toLowerCase() + , sensor = model.createFromBridge(type, id, sensorData) + ; + result.push(sensor); }); } @@ -112,7 +118,8 @@ function buildAllSensorsResult(data) { } function createSensorResponse(data, requestParameters) { - return sensorBuilder.build(data, requestParameters.id); + const type = data.type.toLowerCase(); + return model.createFromBridge(type, requestParameters.id, data); } function createNewSensorResponse(data) { @@ -123,11 +130,13 @@ function createNewSensorResponse(data) { if (data) { Object.keys(data).forEach(key => { - if (key === 'lastscan') { result.lastscan = data.lastscan; } else { - result.sensors.push(new Sensor(data[key], key)); + const type = data.type.toLowerCase() + , sensor = model.createFromBridge(type, key, data[key], key) + ; + result.sensors.push(sensor); } }); } @@ -163,11 +172,13 @@ function buildSensorConfigPayload(parameters) { throw new ApiError('A sensor must be provided'); } - if (!(parameters.sensor instanceof Sensor)) { + if (!model.isSensorInstance(parameters.sensor)) { throw new ApiError('Sensor parameter is not a valid type, must be a Sensor'); } - const body = parameters.sensor.config; + const sensor = parameters.sensor.getHuePayload() + , body = sensor.config + ; // Remove any parameters that we are not able to set, at least from experience at the time of writing delete body.reachable; @@ -183,7 +194,7 @@ function buildSensorStatePayload(parameters) { throw new ApiError('A sensor must be provided'); } - if (!(parameters.sensor instanceof Sensor)) { + if (!model.isSensorInstance(parameters.sensor)) { throw new ApiError('Sensor parameter is not a valid type, must be a Sensor'); } @@ -193,12 +204,11 @@ function buildSensorStatePayload(parameters) { // Limit the updates to the specified stateNames parameters.filterStateNames.forEach(stateName => { - const value = parameters.sensor.state[stateName]; - body.stateName = value; + body.stateName = parameters.sensor.state[stateName]; }) } else { // Just copy all the state values, as these have not been filtered - body = parameters.sensor.state; + body = parameters.sensor.getHuePayload().state; } // Remove any parameters that we are not able to set, at least from experience at the time of writing diff --git a/lib/api/http/placeholders/GroupIdPlaceholder.js b/lib/api/http/placeholders/GroupIdPlaceholder.js index db70a0d..fc620eb 100644 --- a/lib/api/http/placeholders/GroupIdPlaceholder.js +++ b/lib/api/http/placeholders/GroupIdPlaceholder.js @@ -1,11 +1,13 @@ 'use strict'; -const NumberPlaceholder = require('./NumberPlaceholder') +const Placeholder = require('./Placeholder') + , types = require('../../../types') ; -module.exports = class GroupIdPlaceholder extends NumberPlaceholder { +module.exports = class GroupIdPlaceholder extends Placeholder { constructor(name) { super('id', name); + this.typeDefinition = types.uint16({name: 'group id', optional: false}); } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/LightIdPlaceholder.js b/lib/api/http/placeholders/LightIdPlaceholder.js index 777ff8e..92dc8b6 100644 --- a/lib/api/http/placeholders/LightIdPlaceholder.js +++ b/lib/api/http/placeholders/LightIdPlaceholder.js @@ -1,11 +1,14 @@ 'use strict'; -const NumberPlaceholder = require('./NumberPlaceholder'); +const Placeholder = require('./Placeholder') + , types = require('../../../types') +; -module.exports = class LightIdPlaceholder extends NumberPlaceholder { +module.exports = class LightIdPlaceholder extends Placeholder { constructor(name) { super('id', name); + this.typeDefinition = types.uint16({name: 'light id', optional: false}); } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/NumberPlaceholder.js b/lib/api/http/placeholders/NumberPlaceholder.js deleted file mode 100644 index b007b24..0000000 --- a/lib/api/http/placeholders/NumberPlaceholder.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const Placeholder = require('./PlaceHolder') - , ApiError = require('../../../ApiError') -; - -module.exports = class NumberPlaceholder extends Placeholder { - - constructor(defaultName, name) { - super(defaultName, name); - } - - getValue(parameters) { - const value = super.getValue(parameters); - - if (typeof value !== 'number' && typeof value !== 'string') { - throw new ApiError('id must be a number or string'); - } - - const result = Number.parseInt(value); - - if (Number.isNaN(result)) { - throw new ApiError(`id is not an integer '${value}'`); - } - - return result; - } -}; \ No newline at end of file diff --git a/lib/api/http/placeholders/PlaceHolder.js b/lib/api/http/placeholders/PlaceHolder.js deleted file mode 100644 index 0713a24..0000000 --- a/lib/api/http/placeholders/PlaceHolder.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - - -module.exports = class PlaceHolder { - - constructor(defaultName, name) { - if (name) { - this._name = name; - } else { - this._name = defaultName; - } - } - - get name() { - return this._name; - } - - inject(uri, parameters) { - const placeholderText = `<${this.name}>`; - - if (uri.indexOf(placeholderText) > -1) { - return uri.replace(placeholderText, this.getValue(parameters)); - } - return uri; - } - - getValue(parameters) { - return parameters ? parameters[this._name] : null; - } -}; \ No newline at end of file diff --git a/lib/api/http/placeholders/Placeholder.js b/lib/api/http/placeholders/Placeholder.js new file mode 100644 index 0000000..440ab77 --- /dev/null +++ b/lib/api/http/placeholders/Placeholder.js @@ -0,0 +1,43 @@ +'use strict'; + +const ApiError = require('../../../ApiError'); + + +module.exports = class Placeholder { + + constructor(defaultName, name) { + this._name = name || defaultName; + } + + get name() { + return this._name; + } + + get typeDefinition() { + return this._type; + } + + set typeDefinition(type) { + this._type = type; + } + + inject(uri, parameters) { + const placeholderText = `<${this.name}>`; + + if (uri.indexOf(placeholderText) > -1) { + return uri.replace(placeholderText, this.getValue(parameters)); + } + return uri; + } + + getValue(parameters) { + const typeDefinition = this.typeDefinition; + + if (!typeDefinition) { + throw new ApiError('No type definition has been specified for placeholder'); + } + + const value = parameters ? parameters[this._name] : null; + return typeDefinition.getValue(value); + } +}; \ No newline at end of file diff --git a/lib/api/http/placeholders/ResourceLinkPlaceholder.js b/lib/api/http/placeholders/ResourceLinkPlaceholder.js new file mode 100644 index 0000000..6891ac3 --- /dev/null +++ b/lib/api/http/placeholders/ResourceLinkPlaceholder.js @@ -0,0 +1,14 @@ +'use strict'; + +const Placeholder = require('./Placeholder') + , types = require('../../../types') +; + + +module.exports = class ResourceLinkPlaceholder extends Placeholder { + + constructor(name) { + super('id', name); + this.typeDefinition = types.uint16({name: 'resourcelink id', optional: false}); + } +}; \ No newline at end of file diff --git a/lib/api/http/placeholders/RuleIdPlaceholder.js b/lib/api/http/placeholders/RuleIdPlaceholder.js index d6f0459..c1c4cf8 100644 --- a/lib/api/http/placeholders/RuleIdPlaceholder.js +++ b/lib/api/http/placeholders/RuleIdPlaceholder.js @@ -1,11 +1,13 @@ 'use strict'; -const NumberPlaceholder = require('./NumberPlaceholder') +const Placeholder = require('./Placeholder') + , types = require('../../../types') ; -module.exports = class RuleIdPlaceholder extends NumberPlaceholder { +module.exports = class RuleIdPlaceholder extends Placeholder { constructor(name) { super('id', name); + this.typeDefinition = types.uint16({name: 'rule id', optional: false}); } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/SceneIdPlaceholder.js b/lib/api/http/placeholders/SceneIdPlaceholder.js index 3c223d3..5c27219 100644 --- a/lib/api/http/placeholders/SceneIdPlaceholder.js +++ b/lib/api/http/placeholders/SceneIdPlaceholder.js @@ -1,11 +1,13 @@ 'use strict'; -const StringPlaceholder = require('./StringPlaceholder') +const Placeholder = require('./Placeholder') + , types = require('../../../types') ; -module.exports = class SceneIdPlaceholder extends StringPlaceholder { +module.exports = class SceneIdPlaceholder extends Placeholder { constructor(name) { super('id', name); + this.typeDefinition = types.string({name: 'scene id', optional: false}); } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/ScheduleIdPlaceholder.js b/lib/api/http/placeholders/ScheduleIdPlaceholder.js index 570c4c1..a88768c 100644 --- a/lib/api/http/placeholders/ScheduleIdPlaceholder.js +++ b/lib/api/http/placeholders/ScheduleIdPlaceholder.js @@ -1,11 +1,13 @@ 'use strict'; -const NumberPlaceholder = require('./NumberPlaceholder') +const Placeholder = require('./Placeholder') + , types = require('../../../types') ; -module.exports = class ScheduleIdPlaceholder extends NumberPlaceholder { +module.exports = class ScheduleIdPlaceholder extends Placeholder { constructor(name) { super('id', name); + this.typeDefinition = types.uint16({name: 'schedule id', optional: false}); } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/SensorIdPlaceholder.js b/lib/api/http/placeholders/SensorIdPlaceholder.js index 4fa45fa..5da00a8 100644 --- a/lib/api/http/placeholders/SensorIdPlaceholder.js +++ b/lib/api/http/placeholders/SensorIdPlaceholder.js @@ -1,11 +1,13 @@ 'use strict'; -const NumberPlaceholder = require('./NumberPlaceholder') +const Placeholder = require('./Placeholder') + , types = require('../../../types') ; -module.exports = class SensorIdPlaceholder extends NumberPlaceholder { +module.exports = class SensorIdPlaceholder extends Placeholder { constructor(name) { super('id', name); + this.typeDefinition = types.uint16({name: 'sensor id', optional: false}); } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/StringPlaceholder.js b/lib/api/http/placeholders/StringPlaceholder.js deleted file mode 100644 index faa51dc..0000000 --- a/lib/api/http/placeholders/StringPlaceholder.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -const Placeholder = require('./PlaceHolder') - , ApiError = require('../../../ApiError') -; - -module.exports = class StringPlaceholder extends Placeholder { - - constructor(defaultName, name) { - super(defaultName, name); - } - - getValue(parameters) { - const value = super.getValue(parameters); - - if (typeof value !== 'string') { - throw new ApiError('id must be a string'); - } - - return value; - } -}; \ No newline at end of file diff --git a/lib/api/http/placeholders/UsernamePlaceholder.js b/lib/api/http/placeholders/UsernamePlaceholder.js index f780907..c58e45b 100644 --- a/lib/api/http/placeholders/UsernamePlaceholder.js +++ b/lib/api/http/placeholders/UsernamePlaceholder.js @@ -1,7 +1,7 @@ 'use strict'; -const Placeholder = require('./PlaceHolder') - , ApiError = require('../../../ApiError') +const Placeholder = require('./Placeholder') + , types = require('../../../types') ; @@ -9,14 +9,6 @@ module.exports = class UsernamePlaceholder extends Placeholder { constructor(name) { super('username', name); - } - - getValue(parameters) { - const value = super.getValue(parameters); - - if (typeof value !== 'string') { - throw new ApiError('Username must be of type string'); - } - return value; + this.typeDefinition = types.string({name: 'username', minLength: 1, optional: false}); } }; diff --git a/lib/api/http/util.js b/lib/api/http/util.js index f3b9870..b2564a4 100644 --- a/lib/api/http/util.js +++ b/lib/api/http/util.js @@ -8,6 +8,7 @@ module.exports = { parseErrors: parseErrors, wasSuccessful: wasSuccessful, extractUpdatedAttributes: extractUpdatedAttributes, + asStringArray: asStringArray }; @@ -79,4 +80,21 @@ function extractUpdatedAttributes(result) { } +function asStringArray(value) { + if (!value) { + return null; + } + + if (Array.isArray(value)) { + const result = []; + + value.forEach(val => { + result.push(`${val}`); + }); + + return result; + } else { + return [`${value}`]; + } +} diff --git a/lib/api/index.js b/lib/api/index.js index f7d1cdc..85aab90 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -15,11 +15,4 @@ module.exports.createLocal = function(host, port) { module.exports.createInsecureLocal = function(host, port) { return new LocalInsecureBootstrap(host, port); -}; - -//TODO need to deprecate this part of the API in favour of using createLocal and .connect() -module.exports.create = function (host, username, clientkey, timeout, port) { - console.error('create() is deprecated, use createRemote(), createLocal() or createInsecureLocal() instead.'); - - return module.exports.createLocal(host, port).connect(username, timeout); }; \ No newline at end of file diff --git a/lib/api/index.test.js b/lib/api/index.test.js index 609f666..162405f 100644 --- a/lib/api/index.test.js +++ b/lib/api/index.test.js @@ -39,13 +39,4 @@ describe('Hue API #lights', () => { expect(api).to.not.be.null; }); }); - - describe('#create()', () => { - - it('should get an insecure local connection', async () => { - const api = await v3Api.create(hueLocalIpAddress, testValues.username); - expect(api).to.not.be.null; - }); - }); - }); \ No newline at end of file diff --git a/lib/api/stateCache.js b/lib/api/stateCache.js index 6026272..da9a3fd 100644 --- a/lib/api/stateCache.js +++ b/lib/api/stateCache.js @@ -1,6 +1,7 @@ 'use strict'; -const lightBuilder = require('../bridge-model/devices/lights'); +const model = require('../model') +; class Cache { @@ -16,7 +17,7 @@ class Cache { if (!light) { let lightData = this.data.lights[id]; if (lightData) { - light = lightBuilder.create(lightData, id); + light = model.createFromBridge('light', id, lightData); this._lights[id] = light; } } diff --git a/lib/bridge-model/BridgeObject.js b/lib/bridge-model/BridgeObject.js deleted file mode 100644 index 32b881e..0000000 --- a/lib/bridge-model/BridgeObject.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -module.exports = class BridgeObject { - - constructor(data, id) { - this._rawData = Object.assign({}, data); - this._id = id; - } - - get name() { - return this.getRawDataValue('name'); - } - - //TODO this is here for schedule and rule, see that it really belongs here - set name(value) { - return this._updateRawDataValue('name', value); - } - - get id() { - return this._id; - } - - get bridgeData() { - // Return a copy so that it cannot be modified from outside - return Object.assign({}, this._rawData); - } - - toString() { - return `${this.constructor.name}\n id: ${this.id}`; - } - - toStringDetailed() { - let result = this.toString(); - - Object.keys(this._rawData).forEach(key => { - result += `\n ${key}: ${JSON.stringify(this._rawData[key])}`; - }); - - return result; - } - - getRawDataValue(key) { - //TODO use dot notation to get nested values - const path = key.split('.'); - - let target = this._rawData - , value = null - ; - - path.forEach(part => { - if (target != null) { - value = target[part]; - target = value; - } else { - target = null; - } - }); - - return value; - // return this._rawData[key]; - } - - _updateRawDataValue(name, value) { - this._rawData[name] = value; - return this; - } -}; diff --git a/lib/bridge-model/BridgeObjectWithNumberId.js b/lib/bridge-model/BridgeObjectWithNumberId.js deleted file mode 100644 index e35a1e3..0000000 --- a/lib/bridge-model/BridgeObjectWithNumberId.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -const BridgeObject = require('./BridgeObject'); - -module.exports = class BridgeObjectWithNumberId extends BridgeObject { - - constructor(data, id) { - super(data, Number(id)); - } -}; diff --git a/lib/bridge-model/Group.js b/lib/bridge-model/Group.js deleted file mode 100644 index 73a9d39..0000000 --- a/lib/bridge-model/Group.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -const BridgeObject = require('./BridgeObjectWithNumberId'); - -module.exports = class Group extends BridgeObject { - - get lights() { - const raw = this.getRawDataValue('lights'); - - if (raw) { - const result = []; - raw.forEach(value => { - result.push(Number(value)); - }); - return result; - } else { - return raw; - } - } - - get type() { - return this.getRawDataValue('type'); - } - - get action() { - //TODO this is a lightstate - return this.getRawDataValue('action'); - } - - get recycle() { - return this.getRawDataValue('recycle'); - } - - get sensors() { - //TODO check what this actually returns when there is one - return this.getRawDataValue('sensors'); - } - - get state() { - return this.getRawDataValue('state'); - } - - get class() { - return this.getRawDataValue('class') || 'Other'; - } - - get locations() { - // TODO locations are specific to a room - return this.getRawDataValue('locations'); - } - - get stream() { - return this.getRawDataValue('stream'); - } - - get modelid() { - return this.getRawDataValue('modelid'); - } - - get uniqueid() { - return this.getRawDataValue('uniqueid'); - } -}; diff --git a/lib/bridge-model/Scene.js b/lib/bridge-model/Scene.js deleted file mode 100644 index dbd2ce4..0000000 --- a/lib/bridge-model/Scene.js +++ /dev/null @@ -1,214 +0,0 @@ -'use strict'; - -const ApiError = require('../ApiError') - , BridgeObject = require('./BridgeObject') -; - -module.exports = class Scene extends BridgeObject { - - constructor(data, id) { - super(data, id); - - if (!data || !data.recycle) { - this.recycle = false; - } - } - - get group() { - return this.getRawDataValue('group'); - } - - set group(id) { - this.type = 'GroupScene'; - this._updateRawDataValue('group', id); - return this; - } - - get lights() { - return this.getRawDataValue('lights'); - } - - set lights(lightIds) { - this.type = 'LightScene'; - - const value = createStringValueArray(lightIds); - this._updateRawDataValue('lights', value); - - return this; - } - - get lightstates() { - return this.getRawDataValue('lightstates'); - } - - set lightstates(value) { - //TODO needs to be an {id: {}, id: {}} type object - this._updateRawDataValue('type', null); - this._updateRawDataValue('lightstates', value); - return this; - } - - get type() { - return this.getRawDataValue('type'); - } - - set type(value) { - if (['LightScene', 'GroupScene'].indexOf(value) === -1) { - throw new ApiError(`Invalid type for scene, '${value}'`); - } - this._updateRawDataValue('type', value); - return this; - } - - get owner() { - return this.getRawDataValue('owner'); - } - - get recycle() { - return this.getRawDataValue('recycle'); - } - - set recycle(value) { - this._updateRawDataValue('recycle', !!value); - return this; - } - - get locked() { - return this.getRawDataValue('locked'); - } - - get appdata() { - // Complex object of version, data - return this.getRawDataValue('appdata'); - } - - set appdata(value) { - //TODO needs to be an object with version and data fields, add validation here - this._updateRawDataValue('appdata', value); - return this; - } - - // get transitiontime() { - // return this.getRawDataValue('transitiontime'); - // } - // - // set transitiontime(value) { - // // TODO validate the transition time - // this._updateRawDataValue('transitiontime', Number(value)); - // return this; - // } - - set picture(value) { - this._updateRawDataValue('picture', value); - return this; - } - - get picture() { - return this.getRawDataValue('picture'); - } - - get lastupdated() { - return this.getRawDataValue('lastupdated'); - } - - get version() { - return this.getRawDataValue('version'); - } - - get payload() { - const result = this._generateCommonPayload(); - - if (this.isLightStateScene()) { - Object.assign(result, this._generateLightStatePayload()); - } else if (this.isGroupScene()) { - Object.assign(result, this._generateGroupScenePayload()); - } else if (this.isLightScene()) { - Object.assign(result, this._generateLightScenePayload()); - // } else { - // throw new ApiError('Scene is not in a valid state'); - } - - return result; - } - - isGroupScene() { - return this.type === 'GroupScene'; - } - - isLightScene() { - return this.type === 'LightScene'; - } - - isLightStateScene() { - return this.lightstates && !!this.type; - } - - _generateLightStatePayload() { - const result = {}; - - //TODO I think that the setting of light states is done in a separate call - - return result; - } - - _generateLightScenePayload() { - const result = {}; - result.lights = this.lights; - result.type = this.type; - - return result; - } - - _generateGroupScenePayload() { - const result = {}; - result.group = this.group; - result.type = this.type; - - return result; - } - - _generateCommonPayload() { - const values = this.bridgeData - , result = {} - ; - - ['name', 'recycle', 'appdata', 'picture', 'transitiontime'].forEach(key => { - if (values[key]) { - result[key] = values[key]; - } - }); - - return result; - } -}; - - -/** - * Creates a String Value Array from the provided values. - * @param values The values to convert to a String value Array. - * @returns {Array} of strings. - */ -function createStringValueArray(values) { - const result = []; - - if (Array.isArray(values)) { - values.forEach(function (value) { - result.push(_asStringValue(value)); - }); - } else { - result.push(_asStringValue(values)); - } - - return result; -} - -function _asStringValue(value) { - let result; - - if (typeof (value) === 'string') { - result = value; - } else { - result = String(value); - } - return result; -} \ No newline at end of file diff --git a/lib/bridge-model/devices/Device.js b/lib/bridge-model/devices/Device.js deleted file mode 100644 index 41beb4c..0000000 --- a/lib/bridge-model/devices/Device.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const BridgeObject = require('../BridgeObjectWithNumberId'); - -module.exports = class Device extends BridgeObject { - - constructor(data, id) { - super(data, id); - } - - get type() { - return this.getRawDataValue('type'); - } - - get modelid() { - return this.getRawDataValue('modelid'); - } - - get manufacturername() { - return this.getRawDataValue('manufacturername'); - } - - get uniqueid() { - return this.getRawDataValue('uniqueid'); - } -}; diff --git a/lib/bridge-model/devices/lights/ColorLight.js b/lib/bridge-model/devices/lights/ColorLight.js deleted file mode 100644 index 4e6c515..0000000 --- a/lib/bridge-model/devices/lights/ColorLight.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const Light = require('./Light'); - - -class ColorLight extends Light { - - constructor(data, id) { - super(data, id); - } -} -module.exports = ColorLight; \ No newline at end of file diff --git a/lib/bridge-model/devices/lights/ColorTemperatureLight.js b/lib/bridge-model/devices/lights/ColorTemperatureLight.js deleted file mode 100644 index 0d7c274..0000000 --- a/lib/bridge-model/devices/lights/ColorTemperatureLight.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const Light = require('./Light'); - - -class ColorTemperatureLight extends Light { - - constructor(data, id) { - super(data, id); - } -} -module.exports = ColorTemperatureLight; \ No newline at end of file diff --git a/lib/bridge-model/devices/lights/DimmableLight.js b/lib/bridge-model/devices/lights/DimmableLight.js deleted file mode 100644 index cf2a1d1..0000000 --- a/lib/bridge-model/devices/lights/DimmableLight.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - - -const Light = require('./Light'); - - -class DimmableLight extends Light { - - constructor(data, id) { - super(data, id); - } -} -module.exports = DimmableLight; \ No newline at end of file diff --git a/lib/bridge-model/devices/lights/ExtendedColorLight.js b/lib/bridge-model/devices/lights/ExtendedColorLight.js deleted file mode 100644 index e30e01c..0000000 --- a/lib/bridge-model/devices/lights/ExtendedColorLight.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const Light = require('./Light'); - - -class ExtendedColorLight extends Light { - - constructor(data, id) { - super(data, id); - } -} -module.exports = ExtendedColorLight; \ No newline at end of file diff --git a/lib/bridge-model/devices/lights/Light.js b/lib/bridge-model/devices/lights/Light.js deleted file mode 100644 index 8e9cf07..0000000 --- a/lib/bridge-model/devices/lights/Light.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; - -const BridgeObject = require('../Device') - , colorGamuts = require('./color-gamuts') -; - -const MODEL_TO_COLOR_GAMUT = { - 'LCT001': 'B', - 'LCT007': 'B', - 'LCT010': 'C', - 'LCT014': 'C', - 'LCT015': 'C', - 'LCT016': 'C', - 'LCT002': 'B', - 'LCT003': 'B', - 'LCT011': 'C', - // 'LCT024': 'C', //TODO this can be read from capabilities.control.colorgamut and colorgamuttype now - 'LTW011': '2200K-6500K', - 'LST001': 'A', - 'LLC010': 'A', - 'LLC011': 'A', - 'LLC012': 'A', - 'LLC006': 'A', - 'LLC005': 'A', - 'LLC007': 'A', - 'LLC014': 'A', - 'LLC013': 'A', - 'LLM001': 'B', - 'LLM010': '2200K-6500K', - 'LLM011': '2200K-6500K', - 'LTW001': '2200K-6500K', - 'LTW004': '2200K-6500K', - 'LTW010': '2200K-6500K', - 'LTW015': '2200K-6500K', - 'LTW013': '2200K-6500K', - 'LTW014': '2200K-6500K', - 'LLC020': 'C', - 'LST002': 'C', - 'LCT012': 'C', - 'LTW012': '2200K-6500K', - - // Lamps - 'LTP001': '2200K-6500K', - 'LTP002': '2200K-6500K', - 'LTP003': '2200K-6500K', - 'LTP004': '2200K-6500K', - 'LTP005': '2200K-6500K', - 'LTF001': '2200K-6500K', - 'LTF002': '2200K-6500K', - 'LTC001': '2200K-6500K', - 'LTC002': '2200K-6500K', - 'LTC003': '2200K-6500K', - 'LTC004': '2200K-6500K', - 'LTC011': '2200K-6500K', - 'LTC012': '2200K-6500K', - 'LTD001': '2200K-6500K', - 'LTD002': '2200K-6500K', - 'LFF001': '2200K-6500K', - 'LTT001': '2200K-6500K', - 'LDT001': '2200K-6500K', -}; - - -module.exports = class Light extends BridgeObject { - - constructor(data, id) { - super(data, id); - - // Newer Hue devices report their own color gamuts - let colorGamutType = this.getRawDataValue('capabilities.control.colorgamuttype'); - if (!colorGamutType) { - colorGamutType = MODEL_TO_COLOR_GAMUT[this.modelid]; - } - this.mappedColorGamut = colorGamutType; - } - - get productid() { - return this.getRawDataValue('productId'); - } - - get colorGamut() { - if (this.mappedColorGamut && this.mappedColorGamut !== '2200K-6500K') { - let colorGamut = this.getRawDataValue('capabilities.control.colorgamut'); - if (colorGamut) { - // The color gamut is reported by the device, use that - return colorGamuts.getColorGamut(colorGamut); - } else { - return colorGamuts[this.mappedColorGamut]; - } - } else { - return null; - } - } - - getSupportedStates() { - const states = Object.keys(this.getRawDataValue('state')); - - // transitiontime is no longer provided in the light state raw data values from the Hue API - states.push('transitiontime'); - - // If there is a corresponding settings, then include the _inc variant - ['bri', 'sat', 'hue', 'ct', 'xy'].forEach(key => { - if (states.indexOf(key) > -1) { - states.push(`${key}_inc`); - } - }); - - return states; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/lights/OnOffLight.js b/lib/bridge-model/devices/lights/OnOffLight.js deleted file mode 100644 index 854dc54..0000000 --- a/lib/bridge-model/devices/lights/OnOffLight.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const Light = require('./Light'); - - -class OnOffLight extends Light { - - constructor(data, id) { - super(data, id); - } -} -module.exports = OnOffLight; \ No newline at end of file diff --git a/lib/bridge-model/devices/lights/index.js b/lib/bridge-model/devices/lights/index.js deleted file mode 100644 index 7f47a18..0000000 --- a/lib/bridge-model/devices/lights/index.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -const ApiError = require('../../../ApiError') - , ColorLight = require('./ColorLight') - , ExtendedColorLight = require('./ExtendedColorLight') - , ColorTemperatureLight = require('./ColorTemperatureLight') - , DimmableLight = require('./DimmableLight') - , OnOffLight = require('./OnOffLight') -; - - -module.exports.create = function(data, id) { - if (!data) { - throw new ApiError('No data provided to build a device from'); - } - - if (data.type) { - const type = data.type.toLowerCase(); - - switch(type) { - case 'color light': - return new ColorLight(data, id); - - case 'extended color light': - return new ExtendedColorLight(data, id); - - case 'color temperature light': - return new ColorTemperatureLight(data, id); - - case 'dimmable light': - return new DimmableLight(data, id); - - default: - // Fall back to using regex matching before reporting an unknown light type - if (/^on\/off/.test(type)) { - return new OnOffLight(data, id); - } else if (/^dimmable/.test(type)) { - return new DimmableLight(data, id); - } - - throw new ApiError(`Unknown type, ${data.type}`); - } - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/CLIPCommon.js b/lib/bridge-model/devices/sensors/CLIPCommon.js deleted file mode 100644 index ebee60b..0000000 --- a/lib/bridge-model/devices/sensors/CLIPCommon.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -const CLIPSensor = require('./CLIPSensor'); - -module.exports = class CLIPCommon extends CLIPSensor { - - constructor(clipType, data, id) { - super(clipType, data, id); - } - - get on() { - return this.config.on; - } - - set on(value) { - this._updateConfigAttribute('on', value); - return this; - } - - get reachable() { - return this.config.reachable; - } - - set reachable(value) { - this._updateConfigAttribute('reachable', value); - return this; - } - - get battery() { - return this.config.battery; - } - - set battery(value) { - this._updateConfigAttribute('battery', value); - return this; - } - - get url() { - return this.config.url; - } - - set url(value) { - this._updateConfigAttribute('url', value); - return this; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/CLIPGenericFlag.js b/lib/bridge-model/devices/sensors/CLIPGenericFlag.js deleted file mode 100644 index 99e1385..0000000 --- a/lib/bridge-model/devices/sensors/CLIPGenericFlag.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -const CLIPCommon = require('./CLIPCommon'); - -module.exports = class CLIPGenericFlag extends CLIPCommon { - - constructor(data, id) { - //TODO perform validation on data values? - super('CLIPGenericFlag', data, id); - } - - // Boolean validation required - - get flag() { - return this.state.flag; - } - - set flag(value) { - this._updateStateAttribute('flag', !!value); - return this; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/CLIPGenericStatus.js b/lib/bridge-model/devices/sensors/CLIPGenericStatus.js deleted file mode 100644 index 3a918e9..0000000 --- a/lib/bridge-model/devices/sensors/CLIPGenericStatus.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -const CLIPCommon = require('./CLIPCommon'); - -module.exports = class CLIPGenericStatus extends CLIPCommon { - - constructor(data, id) { - //TODO perform validation on data values? - super('CLIPGenericStatus', data, id); - } - - // Integer validation required - - get status() { - return this.state.status; - } - - set status(value) { - this._updateStateAttribute('status', value); - return this; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/CLIPHumidity.js b/lib/bridge-model/devices/sensors/CLIPHumidity.js deleted file mode 100644 index 1913ce5..0000000 --- a/lib/bridge-model/devices/sensors/CLIPHumidity.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -const CLIPCommon = require('./CLIPCommon'); - -module.exports = class CLIPHumidity extends CLIPCommon { - - constructor(data, id) { - //TODO perform validation on data values? - super('CLIPHumidity', data, id); - } - - get humidity() { - return this.state.humidity; - } - - set humidity(value) { - //TODO Current humidity 0.01% steps (e.g. 2000 is 20%)The bridge does not enforce range/resolution. - this._updateStateAttribute('humidity', value); - return this; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/CLIPLightlevel.js b/lib/bridge-model/devices/sensors/CLIPLightlevel.js deleted file mode 100644 index 2018f02..0000000 --- a/lib/bridge-model/devices/sensors/CLIPLightlevel.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -const CLIPSensor = require('./CLIPSensor'); - -module.exports = class CLIPLightlevel extends CLIPSensor { - - constructor(data, id) { - //TODO perform validation on data values? - super('CLIPLightlevel', data, id); - } - - get tholddark() { - return this.config.tholddark; - } - - set tholddark(value) { - this._updateConfigAttribute('tholddark', value); - return this; - } - - get thresholdDark() { - return this.tholddark; - } - - get tholdoffset() { - return this.state.tholdoffset; - } - - set tholdoffset(value) { - if (value >= 1) { - this._updateConfigAttribute(value); - } else { - throw new Error('Invalid offset must be >= 1'); - } - return this; - } - - get lightlevel() { - return this.state.lightlevel; - } - - set lightlevel(value) { - this._updateStateAttribute('lightlevel', value); - return this; - } - - get dark() { - return this.state.dark; - } - - set dark(value) { - this._updateStateAttribute('dark', !!value); - return this; - } - - get daylight() { - return this.state.daylight; - } - - set daylight(value) { - this._updateStateAttribute('daylight', !!value); - return this; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/CLIPOpenClose.js b/lib/bridge-model/devices/sensors/CLIPOpenClose.js deleted file mode 100644 index 16a5a4f..0000000 --- a/lib/bridge-model/devices/sensors/CLIPOpenClose.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const CLIPCommon = require('./CLIPCommon'); - -module.exports = class CLIPOpenClose extends CLIPCommon { - - constructor(data, id) { - //TODO perform validation on data values? - super('CLIPOpenClose', data, id); - } - - get open() { - return this.state.open; - } - - set open(value) { - this._updateStateAttribute('open', value); - return this; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/CLIPPresence.js b/lib/bridge-model/devices/sensors/CLIPPresence.js deleted file mode 100644 index 9921458..0000000 --- a/lib/bridge-model/devices/sensors/CLIPPresence.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const CLIPCommon = require('./CLIPCommon'); - -module.exports = class CLIPPresence extends CLIPCommon { - - constructor(data, id) { - //TODO perform validation on data values? - super('CLIPPresence', data, id); - } - - get presence() { - return this.state.presence; - } - - set presence(value) { - this._updateStateAttribute('presence', !!value); - return this; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/CLIPSensor.js b/lib/bridge-model/devices/sensors/CLIPSensor.js deleted file mode 100644 index 8ae1868..0000000 --- a/lib/bridge-model/devices/sensors/CLIPSensor.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -const Sensor = require('./Sensor'); - -module.exports = class CLIPSensor extends Sensor { - - constructor(clipType, data, id) { - //TODO perform validation on data values? - super(clipType, data, id); - } - - get payload() { - const payload = super.payload; - payload.type = this.type; - return payload; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/CLIPSwitch.js b/lib/bridge-model/devices/sensors/CLIPSwitch.js deleted file mode 100644 index 316a21a..0000000 --- a/lib/bridge-model/devices/sensors/CLIPSwitch.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const CLIPSensor = require('./CLIPSensor'); - -module.exports = class CLIPSwitch extends CLIPSensor { - - constructor(data, id) { - //TODO perform validation on data values? - super('CLIPSwitch', data, id); - } - - get buttonevent() { - return this.state['buttonevent']; - } - - set buttonevent(value) { - this._updateStateAttribute('buttonevent', value); - return this; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/CLIPTemperature.js b/lib/bridge-model/devices/sensors/CLIPTemperature.js deleted file mode 100644 index 96a0f67..0000000 --- a/lib/bridge-model/devices/sensors/CLIPTemperature.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const CLIPCommon = require('./CLIPCommon.js'); - -module.exports = class CLIPTemperature extends CLIPCommon { - - constructor(data, id) { - //TODO perform validation on data values? - super('CLIPTemperature', data, id); - } - - get temperature() { - return this.state.temperature; - } - - set temperature(value) { - this._updateStateAttribute('temperature', value); - return this; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/Daylight.js b/lib/bridge-model/devices/sensors/Daylight.js deleted file mode 100644 index dcf0cbb..0000000 --- a/lib/bridge-model/devices/sensors/Daylight.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -const Sensor = require('./Sensor'); - -module.exports = class Daylight extends Sensor { - - constructor(data, id) { - super('Daylight', data, id); - } - - get on() { - return this.config.on; - } - - set on(value) { - this._updateConfigAttribute('on', value); - return this; - } - - get long() { - // Hidden from bridge to protect privacy - return this.configured; - } - - set long(value) { - this._updateConfigAttribute('long', value); - return this; - } - - get lat() { - // Hidden from bridge to protect privacy - return this.configured; - } - - set lat(value) { - this._updateConfigAttribute('lat', value); - return this; - } - - get configured() { - return this.config.configured; - } - - set configured(value) { - this._updateConfigAttribute('configured', !!value); - return this; - } - - get sunriseoffset() { - return this.config.sunriseoffset; - } - - set sunriseoffset(value) { - this._updateConfigAttribute('sunriseoffset', value); - return this; - } - - get sunsetoffset() { - return this.config.sunriseoffset; - } - - set sunsetoffset(value) { - this._updateConfigAttribute('sunsetoffset', value); - return this; - } - - get daylight() { - return this.state.daylight; - } - - set daylight(value) { - this._updateStateAttribute('daylight', !!value); - return this; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/Sensor.js b/lib/bridge-model/devices/sensors/Sensor.js deleted file mode 100644 index 40ee9a5..0000000 --- a/lib/bridge-model/devices/sensors/Sensor.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -const BridgeObject = require('../Device') - , ApiError = require('../../../ApiError') -; - -module.exports = class Sensor extends BridgeObject { - - constructor(type, data, id) { - super(data, id); - - this._type = type; - - //TODO need to populate these from the data above - this._configAttributes = Object.assign({}, data.config); - this._stateAttributes = Object.assign({}, data.state); - } - - get type() { - return this._type; - } - - get state() { - return Object.assign({}, this._stateAttributes); - } - - get config() { - return Object.assign({}, this._configAttributes); - } - - get swversion() { - return this.getRawDataValue('swversion'); - } - - get lastupdated() { - return this.state.lastupdated; - } - - get attributeNames() { - return Object.keys(this._stateAttributes); - } - - _updateStateAttribute(name, value) { - //TODO add validation on name and value - this._stateAttributes[name] = value; - } - - _updateConfigAttribute(name, value) { - //TODO add validation on name and value - this._configAttributes[name] = value; - } - - get payload() { - const self = this - , payload = {} - ; - - ['name', 'modelid', 'swversion', 'uniqueid', 'manufacturername'].forEach(key => { - const value = self.getRawDataValue(key); - if (!value) { - throw new ApiError(`Mandatory Sensor parameter '${key}' is missing.`); - } - payload[key] = value; - }); - - const state = this.state; - if (Object.keys(state).length > 0) { - payload.state = state; - } - - const config = this.config; - if (Object.keys(config).length > 0) { - payload.config = config; - } - - return payload; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/ZLLLightLevel.js b/lib/bridge-model/devices/sensors/ZLLLightLevel.js deleted file mode 100644 index 057562f..0000000 --- a/lib/bridge-model/devices/sensors/ZLLLightLevel.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -const Sensor = require('./Sensor'); - -module.exports = class ZLLLightlevel extends Sensor { - - constructor(data, id) { - super('ZLLLightlevel', data, id); - } - - get tholddark() { - return this.config.tholddark; - } - - set tholddark(value) { - this._updateConfigAttribute('tholddark', value); - return this; - } - - get thresholdDark() { - return this.tholddark; - } - - get tholdoffset() { - return this.state.tholdoffset; - } - - set tholdoffset(value) { - if (value >= 1) { - this._updateConfigAttribute(value); - } else { - throw new Error('Invalid offset must be >= 1'); - } - return this; - } - - get lightlevel() { - return this.state.lightlevel; - } - - set lightlevel(value) { - this._updateStateAttribute('lightlevel', value); - return this; - } - - get dark() { - return this.state.dark; - } - - set dark(value) { - this._updateStateAttribute('dark', !!value); - return this; - } - - get daylight() { - return this.state.daylight; - } - - // set daylight(value) { - // this._updateStateAttribute('daylight', !!value); - // return this; - // } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/ZLLPresence.js b/lib/bridge-model/devices/sensors/ZLLPresence.js deleted file mode 100644 index 2e0740f..0000000 --- a/lib/bridge-model/devices/sensors/ZLLPresence.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -const Sensor = require('./Sensor'); - -// Hue Motion Sensor -module.exports = class ZLLPresense extends Sensor { - - constructor(data, id) { - super('ZLLPresense', data, id); - } - - get on() { - return this.config.on; - } - - set on(value) { - this._updateConfigAttribute('on', value); - return this; - } - - get battery() { - return this.config.battery; - } - - set battery(value) { - this._updateConfigAttribute('battery', value); - return this; - } - - get alert() { - return this.config.alert; - } - - set alert(value) { - this._updateConfigAttribute('alert', value); - return this; - } - - get reachable() { - return this.config.reachable; - } - - set reachable(value) { - this._updateConfigAttribute('reachable', value); - return this; - } - - get sensitivity() { - return this.config.sensitivity; - } - - set sensitivity(value) { - this._updateConfigAttribute('sensitivity', value); - return this; - } - - get sensitivitymax() { - return this.config.sensitivitymax; - } - - set sensitivitymax(value) { - this._updateConfigAttribute('sensitivitymax', value); - return this; - } - - get presence() { - return this.state.presence; - } - - get lastupdated() { - return this.state.lastupdated; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/ZLLSwitch.js b/lib/bridge-model/devices/sensors/ZLLSwitch.js deleted file mode 100644 index 98d9c5d..0000000 --- a/lib/bridge-model/devices/sensors/ZLLSwitch.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const Sensor = require('./Sensor'); - -// Hue Dimmer Switch -module.exports = class ZLLSwitch extends Sensor { - - constructor(data, id) { - super('ZLLSwitch', data, id); - } - - get on() { - return this.config.on; - } - - set on(value) { - this._updateConfigAttribute('on', value); - return this; - } - - get battery() { - return this.config.battery; - } - - set battery(value) { - this._updateConfigAttribute('battery', value); - return this; - } - - get alert() { - return this.config.alert; - } - - set alert(value) { - this._updateConfigAttribute('alert', value); - return this; - } - - get buttonevent() { - return this.config.buttonevent; - } - - // //TODO not sure that we can actually set these - // set buttonevent(value) { - // //TODO there is validation we could perform 1000, 1001, 1002 1003 for buttons 2000, 3000 and 4000 - // this._updateStateAttribute('buttonevent', value); - // return this; - // } - - get lastupdated() { - return this.state.lastupdated; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/ZLLTemperature.js b/lib/bridge-model/devices/sensors/ZLLTemperature.js deleted file mode 100644 index 35502e0..0000000 --- a/lib/bridge-model/devices/sensors/ZLLTemperature.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -const Sensor = require('./Sensor'); - -module.exports = class ZLLTemperature extends Sensor { - - constructor(data, id) { - super('ZLLTemperature', data, id); - } - - get temperature() { - return this.state.temperature; - } -}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/index.js b/lib/bridge-model/devices/sensors/index.js deleted file mode 100644 index e9b3bb6..0000000 --- a/lib/bridge-model/devices/sensors/index.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -const ApiError = require('../../../ApiError') - , CLIPGenericFlag = require('./CLIPGenericFlag') - , CLIPGenericStatus = require('./CLIPGenericStatus.js') - , CLIPHumidity = require('./CLIPHumidity') - , CLIPLightlevel = require('./CLIPLightlevel') - , CLIPOpenClose = require('./CLIPOpenClose') - , CLIPPresence = require('./CLIPPresence') - , CLIPSwitch = require('./CLIPSwitch') - , CLIPTemperature = require('./CLIPTemperature') -; - - -function buildSensor(data, id) { - if (! data) { - throw new ApiError('Sensor data must be provided'); - } - - if (! data.type) { - throw new ApiError('All sensors require a "type" parameter.'); - } - - const type = data.type; - - // TODO refactor this once we expose the Zigbee sensors too, as we will have to have required them all at that point. - // This will fail if the code is ever minimized, at which point could be replaced by a switch statement - try { - const SensorClass = require(`./${type}`); - return new SensorClass(data, id); - } catch (err) { - throw new ApiError(`Failed to resolve a sensor class for type: ${type}`); - } -} - - -module.exports = { - build: buildSensor, - - clip: { - GenericFlag: CLIPGenericFlag, - GenericStatus: CLIPGenericStatus, - Humidity: CLIPHumidity, - Lightlevel: CLIPLightlevel, - OpenClose: CLIPOpenClose, - Presence: CLIPPresence, - Switch: CLIPSwitch, - Temperature: CLIPTemperature, - } -}; \ No newline at end of file diff --git a/lib/bridge-model/index.js b/lib/bridge-model/index.js deleted file mode 100644 index 773b2ad..0000000 --- a/lib/bridge-model/index.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const lightStates = require('./lightstate/index') - , sensors = require('./devices/sensors') - , Scene = require('./Scene') - , rules = require('./rules') -; - -module.exports = { - lightStates: lightStates, - sensors: sensors, - Scene: Scene, - - rules: rules, -}; diff --git a/lib/bridge-model/rules/index.js b/lib/bridge-model/rules/index.js deleted file mode 100644 index 8a80f18..0000000 --- a/lib/bridge-model/rules/index.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -const Rule = require('./Rule') - // , RuleAction = require('./actions/RuleAction') - // , RuleCondition = require('./conditions/RuleCondition') - , conditionOperators = require('./conditions/operators/index') - - , SensorCondition = require('./conditions/SensorCondition') - , GroupCondition = require('./conditions/GroupCondition') - - , GroupStateAction = require('./actions/GroupStateAction') - , LightStateAction = require('./actions/LightStateAction') - , SensorStateAction = require('./actions/SensorStateAction') - , SceneAction = require('./actions/SceneAction') - ; - - -module.exports = { - Rule: Rule, - - //TODO questionable - // RuleAction: RuleAction, - // RuleCondition: RuleCondition, - - conditions: { - sensor: function(sensor) { - return new SensorCondition(sensor); - }, - - group: function(id) { - return new GroupCondition(id); - }, - - operators: conditionOperators, - }, - - actions: { - light: function(id) { - return new LightStateAction(id); - }, - - group: function(id) { - return new GroupStateAction(id); - }, - - sensor: function(id) { - return new SensorStateAction(id); - }, - - scene: function(id) { - return new SceneAction(id); - } - } -}; \ No newline at end of file diff --git a/lib/model/BridgeObject.js b/lib/model/BridgeObject.js new file mode 100644 index 0000000..c23c7b7 --- /dev/null +++ b/lib/model/BridgeObject.js @@ -0,0 +1,146 @@ +'use strict'; + +const ApiError = require('../ApiError.js'); + +module.exports = class BridgeObject { + + constructor(attributes, id) { + this._attributes = {}; + this._data = {}; + + attributes.forEach(attr => { + this._attributes[attr.name] = attr; + }); + + // Validate that we have an id definition + if (!this._attributes.id) { + throw new ApiError('All bridge objects must have an "id" definition'); + } + this.setAttributeValue('id', id); + } + + getAttributeValue(name) { + const definition = this._attributes[name]; + + if (definition) { + return definition.getValue(this._data[name]); + } else { + throw new ApiError(`Requesting value for invalid attribute '${name}'`); + } + } + + setAttributeValue(name, value) { + const definition = this._attributes[name]; + + if (definition) { + this._data[definition.name] = definition.getValue(value); + } else { + throw new ApiError(`Attempted to set attribute '${name}', but do not have a definition registered`); + } + + return this; + } + + get id() { + return this.getAttributeValue('id'); + } + + getJsonPayload() { + const data = this._bridgeData; + + data.node_hue_api = { + type: this.constructor.name.toLowerCase(), + version: 1 + }; + + return data; + } + + getHuePayload() { + const result = {}; + + Object.keys(this._attributes).forEach(name => { + const value = this.getAttributeValue(name); + if (value !== null && value !== undefined) { + result[name] = value; + } + }); + + return result; + // return this._bridgeData; + } + + toString() { + return `${this.constructor.name}\n id: ${this.id}`; + } + + toStringDetailed() { + let result = this.toString(); + + Object.keys(this._data).forEach(key => { + if (key !== 'id') { + result += `\n ${key}: ${JSON.stringify(this._data[key])}`; + } + }); + + return result; + } + + _populate(data) { + const self = this; + + //TODO Maybe need to support api data and bridge data separately in this call, but treat as the same for now + + if (data) { + Object.keys(data).forEach(key => { + if (self._attributes[key]) { + self.setAttributeValue(key, data[key]); + } + }); + } + + // Store this so we can do a diff on it later to help with support of new devices and changes in the API results in the future + this._populationData = data; + + return self; + } + + get _bridgeData() { + // Return a copy so that it cannot be modified from outside + return Object.assign({}, this._data); + } + + //TODO utility function, move out + static getRawDataValue(key, data) { + //TODO use dot notation to get nested values + const path = key.split('.'); + + let target = data + , value = null + ; + + path.forEach(part => { + if (target != null) { + value = target[part]; + target = value; + } else { + target = null; + } + }); + + return value; + } + + // TODO util function + static mergeAttributes() { + let result = []; + + Array.from(arguments).forEach(arg => { + if (arg) { + result = result.concat(arg); + } + }); + + return result; + } +}; diff --git a/lib/model/Group.js b/lib/model/Group.js new file mode 100644 index 0000000..8100a8f --- /dev/null +++ b/lib/model/Group.js @@ -0,0 +1,144 @@ +'use strict'; + +const BridgeObject = require('./BridgeObject') + , types = require('../types') +; + +const ROOM_CLASSES = [ + 'Living room', + 'Kitchen', + 'Dining', + 'Bedroom', + 'Kids bedroom', + 'Bathroom', + 'Nursery', + 'Recreation', + 'Office', + 'Gym', + 'Hallway', + 'Toilet', + 'Front door', + 'Garage', + 'Terrace', + 'Garden', + 'Driveway', + 'Carport', + 'Other', + // The following are valid in 1.30 and higher of the API + 'Home', + 'Downstairs', + 'Upstairs', + 'Top floor', + 'Attic', + 'Guest room', + 'Staircase', + 'Lounge', + 'Man cave', + 'Computer', + 'Studio', + 'Music', + 'TV', + 'Reading', + 'Closet', + 'Storage', + 'Laundry room', + 'Balcony', + 'Porch', + 'Barbecue', + 'Pool', +]; + + +const ATTRIBUTES = [ + types.int8({name: 'id'}), + types.string({name: 'name', min: 0, max: 32}), + types.choice({name: 'type', validValues: ['LightGroup', 'Luminaire', 'LightSource', 'Room', 'Entertainment', 'Zone'], defaultValue: 'LightGroup'}), + types.list({name: 'lights', minEntries: 0, listType: types.uint8({name: 'lightId'})}), //TODO a room can be empty, but all others require at least one + types.list({name: 'sensors', minEntries: 0, listType: types.string({name: 'sensorId'})}), + types.object({name: 'action'}), + types.object({name: 'state'}), + types.object({name: 'presence', types: [types.string({name: 'lastupdated'}), types.boolean({name: 'presence'}), types.boolean({name: 'presence_all'})]}), + types.object({name: 'lightlevel', types: [ + types.string({name: 'lastupdated'}), + types.boolean({name: 'dark'}), + types.boolean({name: 'dark_all'}), + types.boolean({name: 'daylight'}), + types.boolean({name: 'daylight_any'}), + types.uint16({name: 'lightlevel'}), + types.uint16({name: 'lightlevel_min'}), + types.uint16({name: 'lightlevel_max'}), + ] + }), + types.boolean({name: 'recycle', defaultValue: false}), + types.choice({name: 'class', defaultValue: 'Other', validValues: ROOM_CLASSES}), + types.object({name: 'locations'}) +]; + +module.exports = class Group extends BridgeObject { + + constructor(id) { + super(ATTRIBUTES, id); + } + + get name() { + return this.getAttributeValue('name'); + } + + set name(value) { + return this.setAttributeValue('name', value); + } + + set lights(value) { + return this.setAttributeValue('lights', value); + } + + get lights() { + return this.getAttributeValue('lights'); + } + + set type(value) { + return this.setAttributeValue('type', value); + } + + get type() { + return this.getAttributeValue('type'); + } + + get action() { + // //TODO this is a lightstate + // return this.getRawDataValue('action'); + return this.getAttributeValue('action'); + } + + get recycle() { + return this.getAttributeValue('recycle'); + } + + get sensors() { + //TODO check what this actually returns when there is one + return this.getAttributeValue('sensors'); + } + + get state() { + return this.getAttributeValue('state'); + } + + set class(value) { + return this.setAttributeValue('class', value); + } + + get class() { + return this.getAttributeValue('class'); + } + + get locations() { + // TODO locations are specific to a room + return this.getAttributeValue('locations'); + } + + get stream() { + return this.getAttributeValue('stream'); + } + + //TODO need more getters +}; \ No newline at end of file diff --git a/lib/model/Light.js b/lib/model/Light.js new file mode 100644 index 0000000..d9a848b --- /dev/null +++ b/lib/model/Light.js @@ -0,0 +1,179 @@ +'use strict'; + +const BridgeObject = require('./BridgeObject') + , colorGamuts = require('./colorGamuts') + , types = require('../types') +; + +const MODEL_TO_COLOR_GAMUT = { + 'LCT001': 'B', + 'LCT007': 'B', + 'LCT010': 'C', + 'LCT014': 'C', + 'LCT015': 'C', + 'LCT016': 'C', + 'LCT002': 'B', + 'LCT003': 'B', + 'LCT011': 'C', + // 'LCT024': 'C', //TODO this can be read from capabilities.control.colorgamut and colorgamuttype now + 'LTW011': '2200K-6500K', + 'LST001': 'A', + 'LLC010': 'A', + 'LLC011': 'A', + 'LLC012': 'A', + 'LLC006': 'A', + 'LLC005': 'A', + 'LLC007': 'A', + 'LLC014': 'A', + 'LLC013': 'A', + 'LLM001': 'B', + 'LLM010': '2200K-6500K', + 'LLM011': '2200K-6500K', + 'LTW001': '2200K-6500K', + 'LTW004': '2200K-6500K', + 'LTW010': '2200K-6500K', + 'LTW015': '2200K-6500K', + 'LTW013': '2200K-6500K', + 'LTW014': '2200K-6500K', + 'LLC020': 'C', + 'LST002': 'C', + 'LCT012': 'C', + 'LTW012': '2200K-6500K', + + // Lamps + 'LTP001': '2200K-6500K', + 'LTP002': '2200K-6500K', + 'LTP003': '2200K-6500K', + 'LTP004': '2200K-6500K', + 'LTP005': '2200K-6500K', + 'LTF001': '2200K-6500K', + 'LTF002': '2200K-6500K', + 'LTC001': '2200K-6500K', + 'LTC002': '2200K-6500K', + 'LTC003': '2200K-6500K', + 'LTC004': '2200K-6500K', + 'LTC011': '2200K-6500K', + 'LTC012': '2200K-6500K', + 'LTD001': '2200K-6500K', + 'LTD002': '2200K-6500K', + 'LFF001': '2200K-6500K', + 'LTT001': '2200K-6500K', + 'LDT001': '2200K-6500K', +}; + + +const ATTRIBUTES = [ + types.uint8({name: 'id'}), + types.string({name: 'name', min: 0, max: 32}), + types.string({name: 'type'}), + types.string({name: 'modelid'}), + types.string({name: 'manufacturername'}), + types.string({name: 'uniqueid'}), + types.string({name: 'productname'}), + types.string({name: 'productid'}), + types.object({name: 'state'}), + types.object({name: 'capabilities'}), + types.object({name: 'config'}), + types.object({name: 'swupdate'}), //TODO state and lastinstall + types.string({name: 'swversion'}), +]; + +module.exports = class Light extends BridgeObject { + + constructor(id) { + super(ATTRIBUTES, id); + } + + get id() { + return this.getAttributeValue('id'); + } + + get name() { + return this.getAttributeValue('name'); + } + + set name(value) { + return this.setAttributeValue('name', value); + } + + get type() { + return this.getAttributeValue('type'); + } + + get modelid() { + return this.getAttributeValue('modelid'); + } + + get manufacturername() { + return this.getAttributeValue('manufacturername'); + } + + get uniqueid() { + return this.getAttributeValue('uniqueid'); + } + + get productid() { + return this.getAttributeValue('productid'); + } + + get productname() { + return this.getAttributeValue('productname'); + } + + get swversion() { + return this.getAttributeValue('swversion'); + } + + get swupdate() { + return this.getAttributeValue('swupdate'); + } + + get state() { + return this.getAttributeValue('state'); + } + + get colorGamut() { + if (this.mappedColorGamut && this.mappedColorGamut !== '2200K-6500K') { + return colorGamuts.getColorGamut(this.mappedColorGamut); + } else { + return null; + } + } + + getSupportedStates() { + const states = Object.keys(this.state); + + // transitiontime is no longer provided in the light state raw data values from the Hue API + states.push('transitiontime'); + + // If there is a corresponding settings, then include the xxx_inc variant + ['bri', 'sat', 'hue', 'ct', 'xy'].forEach(key => { + if (states.indexOf(key) > -1) { + states.push(`${key}_inc`); + } + }); + + return states; + } + + _populate(data) { + if (data) { + this.mappedColorGamut = getColorGamut(data); + } else { + this.mappedColorGamut = null; + } + + return super._populate(data); + } +}; + +function getColorGamut(data) { + // Newer Hue devices report their own color gamuts under 'capabilities.control.colorgamuttype' + let colorGamutType = BridgeObject.getRawDataValue('capabilities.control.colorgamuttype', data); + + if (!colorGamutType) { + colorGamutType = MODEL_TO_COLOR_GAMUT[data.modelid]; + } + + return colorGamutType; +} \ No newline at end of file diff --git a/lib/model/ResourceLink.js b/lib/model/ResourceLink.js new file mode 100644 index 0000000..c39fa69 --- /dev/null +++ b/lib/model/ResourceLink.js @@ -0,0 +1,209 @@ +'use strict'; + +const ApiError = require('../ApiError') + , BridgeObject = require('./BridgeObject') + , parameters = require('../types') +; + +// All the valid resource types that the Hue API documentation provides +const VALID_RESOURCELINK_TYPES = [ + 'lights', + 'sensors', + 'groups', + 'scenes', + 'rules', + 'schedules', + 'resourcelinks', +]; + + +const ATTRIBUTES = [ + parameters.uint16({name: 'id'}), + parameters.string({name: 'name', min: 1, max: 32}), + parameters.string({name: 'description', min: 0, max: 64}), + parameters.choice({name: 'type', validValues: ['Link'], defaultValue: 'Link'}), + parameters.uint16({name: 'classid'}), + parameters.string({name: 'owner'}), // Supposedly 10 minimum length but "none" is set as this value in my bridge + parameters.boolean({name: 'recycle'}), + // links: //TODO too complex as these are not stored in bridge format +]; + + +module.exports = class ResourceLink extends BridgeObject { + + constructor(id) { + super(ATTRIBUTES, id); + this.resetLinks(); + } + + set name(value) { + return this.setAttributeValue('name', value); + } + + get name() { + return this.getAttributeValue('name'); + } + + get description() { + return this.getAttributeValue('description'); + } + + set description(value) { + return this.setAttributeValue('description', value); + } + + get type() { + return this.getAttributeValue('type'); + } + + get classid() { + return this.getAttributeValue('classid'); + } + + set classid(value) { + return this.setAttributeValue('classid', value); + } + + get owner() { + return this.getAttributeValue('owner'); + } + + get recycle() { + return this.getAttributeValue('recycle'); + } + + set recycle(value) { + return this.setAttributeValue('recycle', value); + } + + get links() { + // Prevent editing of the link representation + return Object.assign({}, this._links); + } + + resetLinks() { + this._links = {}; + return this; + } + + addLink(type, id) { + const links = this._links + , validatedLinkType = validateLinkType(type) + ; + + if (! links[validatedLinkType]) { + links[validatedLinkType] = []; + } + + links[validatedLinkType].push(id); + + return this; + } + + removeLink(type, id) { + const links = this._links + , validatedLinkType = validateLinkType(type) + , linkType = links[validatedLinkType] + ; + + if (linkType) { + const idx = linkType.indexOf(`${id}`); + if (idx > -1) { + linkType.splice(idx, 1); + } + } + + return this; + } + + toStringDetailed() { + let result = super.toStringDetailed(); + + const links = this.links; + result += `\n links: ${JSON.stringify(links)}`; + + return result; + } + + getJsonPayload() { + const dataLinks = this.links + , data = super.getJsonPayload(); + + // Add the links to the object + data.links = JSON.parse(JSON.stringify(dataLinks)); + + return data; + } + + getHuePayload() { + const data = super.getHuePayload() + , resourceLinkLinks = this.links + , links = [] + ; + + // Convert the links back into the Hue Bride address form + Object.keys(resourceLinkLinks).forEach(resource => { + const resourceIds = resourceLinkLinks[resource]; + if (resourceIds) { + resourceIds.forEach(resourceId => { + links.push(`/${resource}/${resourceId}`); + }); + } + }); + data.links = links; + + return data; + } + + _populate(data) { + // Links are taken apart and separated out from the data + const rawData = Object.assign({}, data); + const linkData = rawData.links; + delete rawData.links; + + super._populate(rawData); + this._links = processLinks(linkData); + } +}; + + +function processLinks(linkData) { + const result = {}; + + if (linkData) { + linkData.forEach(link => { + const parts = /\/(.*)\/(.*)/.exec(link) + , linkType = parts[1] + , linkId = parts[2] + ; + + const validatedLinkType = validateLinkType(linkType); + + let links = result[validatedLinkType]; + if (!links) { + links = []; + result[validatedLinkType] = links; + } + + links.push(linkId); + }); + } + + return result; +} + + +function validateLinkType(type) { + if (!type) { + throw new ApiError('A ResourceLink Type must be provided'); + } + const typeLowerCase = type.toLowerCase() + , idx = VALID_RESOURCELINK_TYPES.indexOf(typeLowerCase) + ; + + if (idx === -1) { + throw new ApiError(`Invalid resource link type ${type}`); + } + + return typeLowerCase; +} \ No newline at end of file diff --git a/lib/model/ResourceLink.test.js b/lib/model/ResourceLink.test.js new file mode 100644 index 0000000..fad89c1 --- /dev/null +++ b/lib/model/ResourceLink.test.js @@ -0,0 +1,101 @@ +'use strict'; + +const expect = require('chai').expect + , ResourceLink = require('./ResourceLink') +; + +describe('ResourceLink', () => { + + const resourceLinkData = { + name: 'HueLabs 2.0', + description: 'All installed formulas', + type: 'Link', + classid: 1, + owner: '985692e7-6abf-4043-b20e-30d75c0ab864', + recycle: false, + links: [ + '/resourcelinks/48840', + '/resourcelinks/62738', + '/scenes/ABCD', + '/scenes/XYZ', + '/schedules/3', + '/sensors/188136', + '/sensors/4578921', + '/groups/10' + ] + }; + + describe('#create()', () => { + + it('should create a resource link', () => { + const resourceLink = new ResourceLink(0); + resourceLink._populate(resourceLinkData); + + expect(resourceLink).to.have.property('id').to.equal(0); + expect(resourceLink).to.have.property('name').to.equal(resourceLinkData.name); + expect(resourceLink).to.have.property('description').to.equal(resourceLinkData.description); + expect(resourceLink).to.have.property('type').to.equal(resourceLinkData.type); + expect(resourceLink).to.have.property('classid').to.equal(resourceLinkData.classid); + expect(resourceLink).to.have.property('owner').to.equal(resourceLinkData.owner); + expect(resourceLink).to.have.property('recycle').to.equal(resourceLinkData.recycle); + + expect(resourceLink).to.have.property('links'); + expect(resourceLink.links).to.have.property('resourcelinks').to.be.an.instanceOf(Array).to.have.length(2); + expect(resourceLink.links).to.have.property('scenes').to.be.an.instanceOf(Array).to.have.length(2); + expect(resourceLink.links).to.have.property('schedules').to.be.an.instanceOf(Array).to.have.length(1); + expect(resourceLink.links).to.have.property('sensors').to.be.an.instanceOf(Array).to.have.length(2); + expect(resourceLink.links).to.have.property('groups').to.be.an.instanceOf(Array).to.have.length(1); + }); + }); + + + describe('adding/removing links', () => { + + let resourceLink; + + beforeEach(() => { + resourceLink = new ResourceLink(); + resourceLink._populate(resourceLinkData); + }); + + + it('should remove an existing link', () => { + expect(resourceLink.links).to.have.property('groups').to.have.length(1); + resourceLink.removeLink('groups', 10); + expect(resourceLink.links).to.have.property('groups').to.have.length(0); + }); + + it('should remove an existing link when there are multiples', () => { + expect(resourceLink.links).to.have.property('sensors').to.have.length(2); + resourceLink.removeLink('sensors', 188136); + expect(resourceLink.links).to.have.property('sensors').to.have.length(1); + }); + + it('should not remove anything it cannot match', () => { + const originalLinks = Object.assign({}, resourceLink.links); + + resourceLink.removeLink('sensors', 0); + resourceLink.removeLink('groups', 48840); + resourceLink.removeLink('schedules', 19); + + expect(resourceLink.links).to.deep.equals(originalLinks) + }); + + + it('should add a new group link', () => { + const originalLinksGroupsCount = resourceLink.links.groups.length + , groupId = 1234978 + ; + + resourceLink.addLink('groups', groupId); + expect(resourceLink.links).to.have.property('groups').to.have.length(originalLinksGroupsCount + 1); + + const groups = resourceLink.links.groups; + expect(groups[groups.length - 1]).to.equal(groupId); + }); + + + //TODO add links + }); + +}); \ No newline at end of file diff --git a/lib/bridge-model/Schedule.js b/lib/model/Schedule.js similarity index 52% rename from lib/bridge-model/Schedule.js rename to lib/model/Schedule.js index a4536f4..83e976a 100644 --- a/lib/bridge-model/Schedule.js +++ b/lib/model/Schedule.js @@ -1,16 +1,32 @@ 'use strict'; const ApiError = require('../ApiError') - , BridgeObjectWithNumberId = require('./BridgeObjectWithNumberId') + , BridgeObject = require('./BridgeObject') , BridgeTime = require('./datetime/BridgeTime') , dateTime = require('./datetime') + , parameters = require('../types') ; -module.exports = class Schedule extends BridgeObjectWithNumberId { + +const ATTRIBUTES = [ + parameters.string({name: 'name', min: 0, max: 32, optional: true}), + parameters.string({name: 'description', min: 0, max: 64, optional: true}), + parameters.object({name: 'command', optional: false}), //TODO address, method, body + parameters.string({name: 'time'}), + parameters.string({name: 'created'}), + parameters.choice({name: 'status', validValues: ['enabled', 'disabled'], defaultValue: 'enabled'}), + parameters.boolean({name: 'autodelete', defaultValue: true}), + // parameters.string({name: 'localtime'}), //TODO need to use a time based object on this, need to define the parameters + parameters.boolean({name: 'recycle', defaultValue: false}), +]; + + +module.exports = class Schedule extends BridgeObject { constructor(data, id) { - super(data, id); + super(ATTRIBUTES, data, id); + //TODO turn this into a parameter if (data && data['localtime']) { this._localtime = dateTime.create(data['localtime']); } else { @@ -19,20 +35,20 @@ module.exports = class Schedule extends BridgeObjectWithNumberId { } get description() { - return this.getRawDataValue('description'); + return this.getAttributeValue('description'); } set description(value) { - return this._updateRawDataValue('description', value); + return this.setAttributeValue('description', value); } get command() { //TODO this is complex object, address, method, body - return this.getRawDataValue('command'); + return this.getAttributeValue('command'); } set command(value) { - return this._updateRawDataValue('command', value); + return this.setAttributeValue('command', value); } get localtime() { @@ -51,34 +67,31 @@ module.exports = class Schedule extends BridgeObjectWithNumberId { } get status() { - return this.getRawDataValue('status'); + return this.getAttributeValue('status'); } set status(value) { - if (['enabled', 'disabled'].indexOf(value) === -1) { - throw new ApiError(`Invalid status value: ${value}`); - } - return this._updateRawDataValue('status', value); + return this.setAttributeValue('status', value); } get autodelete() { - return this.getRawDataValue('autodelete'); + return this.getAttributeValue('autodelete'); } set autodelete(value) { - return this._updateRawDataValue('autodelete', !!value); + return this.setAttributeValue('autodelete', value); } get recycle() { - return this.getRawDataValue('recycle'); + return this.getAttributeValue('recycle'); } set recycle(value) { - return this._updateRawDataValue('recylce', !!value); + return this.setAttributeValue('recylce', value); } get created() { - return this.getRawDataValue('created'); + return this.getAttributeValue('created'); } get payload() { diff --git a/lib/model/Schedule.test.js b/lib/model/Schedule.test.js new file mode 100644 index 0000000..719f0e5 --- /dev/null +++ b/lib/model/Schedule.test.js @@ -0,0 +1,15 @@ +'use strict'; + +const expect = require('chai').expect + , Schedule = require('./Schedule') +; + +describe('Schedule', () => { + + describe('#create()', () => { + + }); + + //TODO need tests + +}); diff --git a/lib/bridge-model/devices/lights/color-gamuts.js b/lib/model/colorGamuts.js similarity index 100% rename from lib/bridge-model/devices/lights/color-gamuts.js rename to lib/model/colorGamuts.js diff --git a/lib/bridge-model/datetime/AbsoluteTime.js b/lib/model/datetime/AbsoluteTime.js similarity index 100% rename from lib/bridge-model/datetime/AbsoluteTime.js rename to lib/model/datetime/AbsoluteTime.js diff --git a/lib/bridge-model/datetime/AbsoluteTime.test.js b/lib/model/datetime/AbsoluteTime.test.js similarity index 100% rename from lib/bridge-model/datetime/AbsoluteTime.test.js rename to lib/model/datetime/AbsoluteTime.test.js diff --git a/lib/bridge-model/datetime/BridgeTime.js b/lib/model/datetime/BridgeTime.js similarity index 100% rename from lib/bridge-model/datetime/BridgeTime.js rename to lib/model/datetime/BridgeTime.js diff --git a/lib/bridge-model/datetime/DateTimeUtil.js b/lib/model/datetime/DateTimeUtil.js similarity index 100% rename from lib/bridge-model/datetime/DateTimeUtil.js rename to lib/model/datetime/DateTimeUtil.js diff --git a/lib/bridge-model/datetime/HueDate.js b/lib/model/datetime/HueDate.js similarity index 100% rename from lib/bridge-model/datetime/HueDate.js rename to lib/model/datetime/HueDate.js diff --git a/lib/bridge-model/datetime/HueDate.test.js b/lib/model/datetime/HueDate.test.js similarity index 100% rename from lib/bridge-model/datetime/HueDate.test.js rename to lib/model/datetime/HueDate.test.js diff --git a/lib/bridge-model/datetime/HueTime.js b/lib/model/datetime/HueTime.js similarity index 100% rename from lib/bridge-model/datetime/HueTime.js rename to lib/model/datetime/HueTime.js diff --git a/lib/bridge-model/datetime/HueTime.test.js b/lib/model/datetime/HueTime.test.js similarity index 100% rename from lib/bridge-model/datetime/HueTime.test.js rename to lib/model/datetime/HueTime.test.js diff --git a/lib/bridge-model/datetime/RandomizedTime.js b/lib/model/datetime/RandomizedTime.js similarity index 100% rename from lib/bridge-model/datetime/RandomizedTime.js rename to lib/model/datetime/RandomizedTime.js diff --git a/lib/bridge-model/datetime/RandomizedTimer.js b/lib/model/datetime/RandomizedTimer.js similarity index 100% rename from lib/bridge-model/datetime/RandomizedTimer.js rename to lib/model/datetime/RandomizedTimer.js diff --git a/lib/bridge-model/datetime/RecurringRandomizedTime.js b/lib/model/datetime/RecurringRandomizedTime.js similarity index 100% rename from lib/bridge-model/datetime/RecurringRandomizedTime.js rename to lib/model/datetime/RecurringRandomizedTime.js diff --git a/lib/bridge-model/datetime/RecurringRandomizedTimer.js b/lib/model/datetime/RecurringRandomizedTimer.js similarity index 100% rename from lib/bridge-model/datetime/RecurringRandomizedTimer.js rename to lib/model/datetime/RecurringRandomizedTimer.js diff --git a/lib/bridge-model/datetime/RecurringTime.js b/lib/model/datetime/RecurringTime.js similarity index 100% rename from lib/bridge-model/datetime/RecurringTime.js rename to lib/model/datetime/RecurringTime.js diff --git a/lib/bridge-model/datetime/RecurringTimer.js b/lib/model/datetime/RecurringTimer.js similarity index 100% rename from lib/bridge-model/datetime/RecurringTimer.js rename to lib/model/datetime/RecurringTimer.js diff --git a/lib/bridge-model/datetime/TimeInterval.js b/lib/model/datetime/TimeInterval.js similarity index 100% rename from lib/bridge-model/datetime/TimeInterval.js rename to lib/model/datetime/TimeInterval.js diff --git a/lib/bridge-model/datetime/Timer.js b/lib/model/datetime/Timer.js similarity index 100% rename from lib/bridge-model/datetime/Timer.js rename to lib/model/datetime/Timer.js diff --git a/lib/bridge-model/datetime/Timer.test.js b/lib/model/datetime/Timer.test.js similarity index 100% rename from lib/bridge-model/datetime/Timer.test.js rename to lib/model/datetime/Timer.test.js diff --git a/lib/bridge-model/datetime/index.js b/lib/model/datetime/index.js similarity index 100% rename from lib/bridge-model/datetime/index.js rename to lib/model/datetime/index.js diff --git a/lib/model/index.js b/lib/model/index.js new file mode 100644 index 0000000..7956db6 --- /dev/null +++ b/lib/model/index.js @@ -0,0 +1,238 @@ +'use strict'; + +const ApiError = require('../ApiError') + , lightStates = require('./lightstate') + + , Light = require('./Light') + + , Group = require('./Group') + + , ResourceLink = require('./ResourceLink') + + , Scene = require('./scenes/Scene') + , LightScene = require('./scenes/LightScene') + , GroupScene = require('./scenes/GroupScene') + + , Schedule = require('./Schedule') + + , Rule = require('./rules/Rule') + , SensorCondition = require('./rules/conditions/SensorCondition') + , GroupCondition = require('./rules/conditions/GroupCondition') + , GroupStateAction = require('./rules/actions/GroupStateAction') + , LightStateAction = require('./rules/actions/LightStateAction') + , SensorStateAction = require('./rules/actions/SensorStateAction') + , SceneAction = require('./rules/actions/SceneAction') + , conditionOperators = require('./rules/conditions/operators/index') + + , Sensor = require('./sensors/Sensor') + , CLIPGenericFlag = require('./sensors/CLIPGenericFlag') + , CLIPGenericStatus = require('./sensors/CLIPGenericStatus') + , CLIPHumidity = require('./sensors/CLIPHumidity') + , CLIPLightlevel = require('./sensors/CLIPLightlevel') + , CLIPOpenCLose = require('./sensors/CLIPOpenClose') + , CLIPPresence = require('./sensors/CLIPPresence') + , CLIPSwitch = require('./sensors/CLIPSwitch') + , CLIPTemperature = require('./sensors/CLIPTemperature') + , Daylight = require('./sensors/Daylight') + , ZGPSwitch = require('./sensors/ZGPSwitch') + , ZLLLightlevel = require('./sensors/ZLLLightlevel') + , ZLLPresence = require('./sensors/ZLLPresence') + , ZLLSwitch = require('./sensors/ZLLSwitch') + , ZLLTemperature = require('./sensors/ZLLTemperature') +; + +const TYPES_TO_MODEL = { + light: Light, + group: Group, + resourcelink: ResourceLink, + // scene: Scene, // This is abstract and should not be instantiated + lightscene: LightScene, + groupscene: GroupScene, + schedule: Schedule, + rule: Rule, + clipgenericflag: CLIPGenericFlag, + clipgenericstatus: CLIPGenericStatus, + cliphumidity: CLIPHumidity, + cliplightlevel: CLIPLightlevel, + clipopenclose: CLIPOpenCLose, + clippresence: CLIPPresence, + clipswitch: CLIPSwitch, + cliptemperature: CLIPTemperature, + daylight: Daylight, + zgpswitch: ZGPSwitch, + zlllightlevel: ZLLLightlevel, + zllpresence: ZLLPresence, + zllswitch: ZLLSwitch, + zlltemperature: ZLLTemperature +}; + +module.exports.lightStates = lightStates; + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Instance Check Functions + +//TODO this may be questionable +module.exports.isSceneInstance = function(obj) { + return obj instanceof Scene; +}; + +module.exports.isGroupSceneInstance = function(obj) { + return obj instanceof GroupScene; +}; + +module.exports.isLightSceneInstance = function(obj) { + return obj instanceof LightScene; +}; + +module.exports.isRuleInstance = function(obj) { + return obj instanceof Rule; +}; + +module.exports.isResourceLinkInstance = function(obj) { + return obj instanceof ResourceLink; +}; + +module.exports.isScheduleInstance = function(obj) { + return obj instanceof Schedule; +}; + +module.exports.isSensorInstance = function(obj) { + return obj instanceof Sensor; +}; + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Sensors + +module.exports.createCLIPGenericFlagSensor = function() { + return new CLIPGenericFlag(); +}; + +module.exports.createCLIPGenericStatusSensor = function() { + return new CLIPGenericStatus(); +}; + +module.exports.createCLIPHumiditySensor = function() { + return new CLIPHumidity(); +}; + +module.exports.createCLIPLightlevelSensor = function() { + return new CLIPLightlevel(); +}; + +module.exports.createCLIPOpenCloseSensor = function() { + return new CLIPOpenCLose(); +}; + +module.exports.createCLIPPresenceSensor = function() { + return new CLIPPresence(); +}; + +module.exports.createCLIPTemperatureSensor = function() { + return new CLIPTemperature(); +}; + +module.exports.createCLIPSwitchSensor = function() { + return new CLIPSwitch(); +}; + + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Scenes + +module.exports.createLightScene = function() { + return new LightScene(); +}; + +module.exports.createGroupScene = function() { + return new GroupScene(); +}; + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Rules + +module.exports.createRule = function() { + return new Rule(); +}; + +module.exports.ruleConditions = { + sensor: function(sensor) { + return new SensorCondition(sensor); + }, + + group: function(id) { + return new GroupCondition(id); + }, +}; + +module.exports.ruleActions = { + light: function(id) { + return new LightStateAction(id); + }, + + group: function(id) { + return new GroupStateAction(id); + }, + + sensor: function(id) { + return new SensorStateAction(id); + }, + + scene: function(id) { + return new SceneAction(id); + } +}; + +module.exports.ruleConditionOperators = conditionOperators; + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ResourceLinks + +module.exports.createResourceLink = function() { + return new ResourceLink(); +}; + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Creation Functions - Generic + +module.exports.createFromBridge = function(type, id, payload) { + const ModelObject = TYPES_TO_MODEL[type]; + + if (!ModelObject) { + throw new ApiError(`Unknown type '${type}' to create Bridge Model Object from.`); + } + + //TODO would be useful to flag this as populated via hue api, not generic JSON + + const instance = new ModelObject(id); + instance._populate(payload); + return instance; +}; + + +//TODO probably rename this +module.exports.create = function (payload) { + if (!payload) { + throw new ApiError('No payload provided to build object from'); + } + + const payloadDataType = payload.node_hue_api; + if (!payloadDataType) { + throw new ApiError('Missing payload Data Type definition'); + } + + const type = payloadDataType.type + , version = payloadDataType.version || 1 + ; + + if (! type) { + throw new ApiError('Invalid payload, missing type from the Data Type'); + } + + //TODO build the object +}; diff --git a/lib/bridge-model/lightstate/BaseStates.js b/lib/model/lightstate/BaseStates.js similarity index 100% rename from lib/bridge-model/lightstate/BaseStates.js rename to lib/model/lightstate/BaseStates.js diff --git a/lib/bridge-model/lightstate/CommonStates.js b/lib/model/lightstate/CommonStates.js similarity index 100% rename from lib/bridge-model/lightstate/CommonStates.js rename to lib/model/lightstate/CommonStates.js diff --git a/lib/bridge-model/lightstate/CommonStates.test.js b/lib/model/lightstate/CommonStates.test.js similarity index 89% rename from lib/bridge-model/lightstate/CommonStates.test.js rename to lib/model/lightstate/CommonStates.test.js index 4756088..504876e 100644 --- a/lib/bridge-model/lightstate/CommonStates.test.js +++ b/lib/model/lightstate/CommonStates.test.js @@ -186,14 +186,15 @@ describe('Hue API #lightState', () => { testFailure(-1, RANGE_ERROR_STRING); }); - it('shoudl fail on 65536', () => { + it('should fail on 65536', () => { testFailure(65536, RANGE_ERROR_STRING); }); - it('should fail if nothing specified', () => { - testFailure(null, RANGE_ERROR_STRING); - testFailure(undefined, RANGE_ERROR_STRING); - }); + // THis is optional so it will not fail + // it('should fail if nothing specified', () => { + // testFailure(null, RANGE_ERROR_STRING); + // testFailure(undefined, RANGE_ERROR_STRING); + // }); }); }); @@ -205,13 +206,13 @@ describe('Hue API #lightState', () => { validateSatState(expected); } - function testFailure(functionName, value) { + function testFailure(functionName, value, errorString) { try { state[functionName](value); expect.fail('Should have thrown an error'); } catch (err) { expect(err).to.exist; - expect(err.message).to.contain(RANGE_ERROR_STRING); + expect(err.message).to.contain(errorString); } } @@ -239,17 +240,18 @@ describe('Hue API #lightState', () => { it('should fail on -1', () => { - testFailureSat(-1); + testFailureSat(-1, RANGE_ERROR_STRING); }); it('should fail on 255', () => { - testFailureSat(255); + testFailureSat(255, RANGE_ERROR_STRING); }); - it('fail if nothing specified', () => { - testFailureSat(null); - testFailureSat(undefined); - }); + // THis is optional so it will not fail + // it('fail if nothing specified', () => { + // testFailureSat(null); + // testFailureSat(undefined); + // }); }); describe('#saturation()', () => { @@ -386,9 +388,10 @@ describe('Hue API #lightState', () => { testFailure(501., RANGE_ERROR_STRING); }); - it('should fail if nothing provided', () => { - testFailure(null, RANGE_ERROR_STRING); - }); + // This is optional so it will not fail + // it('should fail if nothing provided', () => { + // testFailure(null, RANGE_ERROR_STRING); + // }); }); // describe("#colorTemperature", () => { @@ -545,24 +548,24 @@ describe('Hue API #lightState', () => { }); - describe("#alertLong", () => { + describe('#alertLong', () => { - it("should set a long alert", () => { + it('should set a long alert', () => { testAlert('alertLong', 'lselect'); }); }); - describe("#alertShort()", () => { + describe('#alertShort()', () => { - it("should set a short alert", () => { + it('should set a short alert', () => { state.alertShort(); - validateAlertState("select"); + validateAlertState('select'); }); }); - describe("#alertNone()", () => { + describe('#alertNone()', () => { - it ('should clear alert', () => { + it('should clear alert', () => { state.alertNone(); validateAlertState('none'); }); @@ -783,9 +786,9 @@ describe('Hue API #lightState', () => { describe('state: bri_inc', () => { - function testBrightnessIncrement(functionName, value) { + function testBrightnessIncrement(functionName, value, expected) { state[functionName](value); - validateBrightnessIncrement(value); + validateBrightnessIncrement(expected); } function testFailureBrightnessIncrement(functionName, value, expected) { @@ -800,8 +803,9 @@ describe('Hue API #lightState', () => { describe('#bri_inc()', () => { - function test(value) { - testBrightnessIncrement('bri_inc', value, value); + function test(value, expected) { + const expectedValue = (expected === undefined) ? value : expected; + testBrightnessIncrement('bri_inc', value, expectedValue); } function testFailure(value) { @@ -869,9 +873,9 @@ describe('Hue API #lightState', () => { describe('state: sat_inc', () => { - function testSaturationIncrement(functionName, value) { + function testSaturationIncrement(functionName, value, expected) { state[functionName](value); - validateSaturationIncrement(value); + validateSaturationIncrement(expected); } function testFailureSaturationIncrement(functionName, value, expected) { @@ -886,8 +890,9 @@ describe('Hue API #lightState', () => { describe('#sat_inc()', () => { - function test(value) { - testSaturationIncrement('sat_inc', value); + function test(value, expected) { + const expectedValue = (expected === undefined) ? value : expected; + testSaturationIncrement('sat_inc', value, expectedValue); } function testFailure(value) { @@ -923,7 +928,7 @@ describe('Hue API #lightState', () => { describe('#incrementSaturation()', () => { function test(value) { - testSaturationIncrement('incrementSaturation', value); + testSaturationIncrement('incrementSaturation', value, value); } function testFailure(value) { @@ -955,9 +960,9 @@ describe('Hue API #lightState', () => { describe('state: hue_inc', () => { - function testHueIncrement(functionName, value) { + function testHueIncrement(functionName, value, expected) { state[functionName](value); - validateHueIncrement(value); + validateHueIncrement(expected); } function testFailureHueIncrement(functionName, value, expected) { @@ -972,8 +977,9 @@ describe('Hue API #lightState', () => { describe('#sat_inc()', () => { - function test(value) { - testHueIncrement('hue_inc', value); + function test(value, expected) { + const expectedValue = (expected === undefined) ? value : expected; + testHueIncrement('hue_inc', value, expectedValue); } function testFailure(value) { @@ -1008,8 +1014,9 @@ describe('Hue API #lightState', () => { describe('#incrementHue()', () => { - function test(value) { - testHueIncrement('incrementHue', value); + function test(value, expected) { + const expectedValue = (expected === undefined) ? value : expected; + testHueIncrement('incrementHue', value, expectedValue); } function testFailure(value) { @@ -1045,9 +1052,9 @@ describe('Hue API #lightState', () => { describe('state: ct_inc', () => { - function testCtIncrement(functionName, value) { + function testCtIncrement(functionName, value, expectedValue) { state[functionName](value); - validateCtIncrement(value); + validateCtIncrement(expectedValue); } function testFailureCtIncrement(functionName, value, expected) { @@ -1062,8 +1069,9 @@ describe('Hue API #lightState', () => { describe('#ct_inc()', () => { - function test(value) { - testCtIncrement('ct_inc', value); + function test(value, expected) { + const expectedValue = (expected === undefined) ? value : expected; + testCtIncrement('ct_inc', value, expectedValue); } function testFailure(value) { @@ -1098,8 +1106,9 @@ describe('Hue API #lightState', () => { describe('#incrementCt()', () => { - function test(value) { - testCtIncrement('incrementCt', value); + function test(value, expected) { + const expectedValue = (expected === undefined) ? value : expected; + testCtIncrement('incrementCt', value, expectedValue); } function testFailure(value) { @@ -1133,8 +1142,9 @@ describe('Hue API #lightState', () => { describe('#incrementColorTemp()', () => { - function test(value) { - testCtIncrement('incrementColorTemp', value); + function test(value, expected) { + const expectedValue = (expected === undefined) ? value : expected; + testCtIncrement('incrementColorTemp', value, expectedValue); } function testFailure(value) { @@ -1168,8 +1178,9 @@ describe('Hue API #lightState', () => { describe('#incrementColourTemp()', () => { - function test(value) { - testCtIncrement('incrementColorTemp', value); + function test(value, expected) { + const expectedValue = (expected === undefined) ? value : expected; + testCtIncrement('incrementColorTemp', value, expectedValue); } function testFailure(value) { @@ -1236,8 +1247,8 @@ describe('Hue API #lightState', () => { testXYIncrementWithValues('xy_inc', x, y); } - function testFailure(functionName, value) { - testFailureXYIncrement('xy_inc', value, RANGE_ERROR_STRING); + function testFailure(x, y) { + testFailureXYIncrement('xy_inc', [x, y], RANGE_ERROR_STRING); } it('should set [-0.5, -0.5]', () => { @@ -1256,7 +1267,7 @@ describe('Hue API #lightState', () => { testWithValues(0, 0); }); - it('should fail on (-0.6, 0)', () => { + it('should fail on (-0.6, -0.5)', () => { testFailure(-0.6, -0.5); }); }); @@ -1272,7 +1283,7 @@ describe('Hue API #lightState', () => { testXYIncrementWithValues('xy_inc', x, y); } - function testFailure(functionName, value) { + function testFailure(value) { testFailureXYIncrement('xy_inc', value, RANGE_ERROR_STRING); } @@ -1289,17 +1300,12 @@ describe('Hue API #lightState', () => { }); it('should fail with invalid values', () => { - testFailure([0,1]); + testFailure([0, 1]); }); }); }); - - - - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/lib/bridge-model/lightstate/GroupState.js b/lib/model/lightstate/GroupState.js similarity index 100% rename from lib/bridge-model/lightstate/GroupState.js rename to lib/model/lightstate/GroupState.js diff --git a/lib/bridge-model/lightstate/LightState.js b/lib/model/lightstate/LightState.js similarity index 100% rename from lib/bridge-model/lightstate/LightState.js rename to lib/model/lightstate/LightState.js diff --git a/lib/bridge-model/lightstate/SceneLightState.js b/lib/model/lightstate/SceneLightState.js similarity index 100% rename from lib/bridge-model/lightstate/SceneLightState.js rename to lib/model/lightstate/SceneLightState.js diff --git a/lib/bridge-model/lightstate/States.js b/lib/model/lightstate/States.js similarity index 94% rename from lib/bridge-model/lightstate/States.js rename to lib/model/lightstate/States.js index 22de25d..d9441b8 100644 --- a/lib/bridge-model/lightstate/States.js +++ b/lib/model/lightstate/States.js @@ -2,11 +2,11 @@ const LIGHT_STATE_PARAMETERS = require('./stateParameters') , ApiError = require('../../ApiError') - , IntegerType = require('../../parameters/IntegerType') + , types = require('../../types') ; -const PERCENTAGE = new IntegerType({}, null, 0, 100) - , DEGREES = new IntegerType({}, null, 0, 360) +const PERCENTAGE = types.uint8({name: 'percentage', min: 0, max: 100}) + , DEGREES = types.uint8({name: 'degrees', min: 0, max: 360}) ; diff --git a/lib/bridge-model/lightstate/index.js b/lib/model/lightstate/index.js similarity index 100% rename from lib/bridge-model/lightstate/index.js rename to lib/model/lightstate/index.js diff --git a/lib/bridge-model/lightstate/stateParameters.js b/lib/model/lightstate/stateParameters.js similarity index 74% rename from lib/bridge-model/lightstate/stateParameters.js rename to lib/model/lightstate/stateParameters.js index 2d36305..2246a2e 100644 --- a/lib/bridge-model/lightstate/stateParameters.js +++ b/lib/model/lightstate/stateParameters.js @@ -1,53 +1,54 @@ 'use strict'; -const parameterTypes = require('../../parameters'); +const types = require('../../types'); module.exports = { - on: parameterTypes.boolean({ + on: types.boolean({ name: 'on', optional: true }), - bri: parameterTypes.uint8({ + bri: types.uint8({ name: 'bri', min: 1, max: 254, optional: true }), - hue: parameterTypes.uint16({ + hue: types.uint16({ name: 'hue', optional: true }), - sat: parameterTypes.uint8({ + sat: types.uint8({ name: 'sat', min: 0, max: 254, optional: true }), - xy: parameterTypes.list({ + xy: types.list({ name: 'xy', minEntries: 2, maxEntries: 2, - type: parameterTypes.float({ + listType: types.float({ name: 'xyValue', min: 0, - max: 1 + max: 1, + optional: false }), optional: true }), - ct: parameterTypes.uint16({ + ct: types.uint16({ name: 'ct', min: 153, max: 500, optional: true }), - alert: parameterTypes.choice({ + alert: types.choice({ name: 'alert', type: 'string', defaultValue: 'none', @@ -55,7 +56,7 @@ module.exports = { optional: true }), - effect: parameterTypes.choice({ + effect: types.choice({ name: 'effect', type: 'string', defaultValue: 'none', @@ -63,53 +64,54 @@ module.exports = { optional: true }), - transitiontime: parameterTypes.uint16({ + transitiontime: types.uint16({ name: 'transitiontime', defaultValue: 4, optional: true }), - bri_inc: parameterTypes.int8({ + bri_inc: types.int8({ name: 'bri_inc', min: -254, max: 254, optional: true }), - sat_inc: parameterTypes.int8({ + sat_inc: types.int8({ name: 'sat_inc', min: -254, max: 254, optional: true }), - hue_inc: parameterTypes.int16({ + hue_inc: types.int16({ name: 'hue_inc', min: -65534, max: 65534, optional: true }), - ct_inc: parameterTypes.int16({ + ct_inc: types.int16({ name: 'ct_inc', min: -65534, max: 65534, optional: true }), - xy_inc: parameterTypes.list({ + xy_inc: types.list({ name: 'xy_inc', minEntries: 2, maxEntries: 2, - type: parameterTypes.float({ + listType: types.float({ name: 'xyValue', min: -0.5, - max: 0.5 + max: 0.5, + optional: false, }), optional: true }), - scene: parameterTypes.string({ + scene: types.string({ name: 'scene', type: 'string', optional: true @@ -139,14 +141,12 @@ module.exports = { // RGB // This is a custom state, and can only be applied we we know the light details, so is stored like a normal state - rgb: parameterTypes.list({ + rgb: types.list({ name: 'rgb', minEntries: 3, maxEntries: 3, - type: parameterTypes.uint8({ - name: 'rgbValue', - min: 0, - max: 255 + listType: types.uint8({ + name: 'rgbValue' }), }), diff --git a/lib/bridge-model/rules/Rule.js b/lib/model/rules/Rule.js similarity index 58% rename from lib/bridge-model/rules/Rule.js rename to lib/model/rules/Rule.js index ea6e293..33d22a8 100644 --- a/lib/bridge-model/rules/Rule.js +++ b/lib/model/rules/Rule.js @@ -3,42 +3,64 @@ const BridgeObject = require('../BridgeObject') , ruleActions = require('./actions/index') , ruleConditions = require('./conditions/index') + , parameters = require('../../types') ; +const ATTRIBUTES = [ + parameters.string({name: 'id'}), + parameters.string({name: 'name', maxLength: 32}), + parameters.string({name: 'owner'}), + parameters.string({name: 'created'}), + parameters.boolean({name: 'recycle'}), + parameters.string({name: 'lasttrigered'}), + parameters.string({name: 'timestriggered'}), + parameters.choice({name: 'status', validValues: ['enabled', 'disabled', 'resourcedeleted'], defaultValue: 'enabled'}), + // conditions and actions are handled separately +]; + module.exports = class Rule extends BridgeObject { - constructor(data, id) { - let cleanedData = Object.assign({}, data); - delete cleanedData.conditions; - delete cleanedData.actions; + constructor(id) { + super(ATTRIBUTES, id); - super(cleanedData, id); - this._conditions = buildConditions(data ? data.conditions : null); - this._actions = buildActions(data ? data.actions : null); + this._conditions = buildConditions(); + this._actions = buildActions(); + } + + get name() { + return this.getAttributeValue('name'); + } + + set name(value) { + return this.setAttributeValue('name', value); + } + + get created() { + return this.getAttributeValue('created'); } get owner() { - return this.getRawDataValue('owner'); + return this.getAttributeValue('owner'); } get lasttriggered() { - return this.getRawDataValue('lasttriggered'); + return this.getAttributeValue('lasttriggered'); } get timestriggered() { - return this.getRawDataValue('timestriggered'); + return this.getAttributeValue('timestriggered'); } get status() { - return this.getRawDataValue('status'); + return this.getAttributeValue('status'); } set recycle(val) { - this._updateRawDataValue('recycle', !!val); + return this.setAttributeValue('recycle', val); } get recycle() { - return this.getRawDataValue('recycle'); + return this.getAttributeValue('recycle'); } get conditions() { @@ -98,6 +120,30 @@ module.exports = class Rule extends BridgeObject { return result; } + + _populate(data) { + super._populate(data); + this._conditions = buildConditions(data ? data.conditions : null); + this._actions = buildActions(data ? data.actions : null); + } + + getHuePayload() { + const data = super.getHuePayload(); + + data.conditions = this.getConditionsPayload(); + data.actions = this.getActionsPayload(); + + return data; + } + + getJsonPayload() { + const data = super.getJsonPayload(); + + data.conditions = this.getConditionsPayload(); + data.actions = this.getActionsPayload(); + + return data; + } }; diff --git a/lib/bridge-model/rules/actions/GroupStateAction.js b/lib/model/rules/actions/GroupStateAction.js similarity index 100% rename from lib/bridge-model/rules/actions/GroupStateAction.js rename to lib/model/rules/actions/GroupStateAction.js diff --git a/lib/bridge-model/rules/actions/LightStateAction.js b/lib/model/rules/actions/LightStateAction.js similarity index 100% rename from lib/bridge-model/rules/actions/LightStateAction.js rename to lib/model/rules/actions/LightStateAction.js diff --git a/lib/bridge-model/rules/actions/RuleAction.js b/lib/model/rules/actions/RuleAction.js similarity index 94% rename from lib/bridge-model/rules/actions/RuleAction.js rename to lib/model/rules/actions/RuleAction.js index 524a6d7..bf6ff57 100644 --- a/lib/bridge-model/rules/actions/RuleAction.js +++ b/lib/model/rules/actions/RuleAction.js @@ -46,6 +46,6 @@ module.exports = class RuleAction { } toString() { - return JSON.stringify(this.payload) + return JSON.stringify(this.payload); } }; \ No newline at end of file diff --git a/lib/bridge-model/rules/actions/SceneAction.js b/lib/model/rules/actions/SceneAction.js similarity index 100% rename from lib/bridge-model/rules/actions/SceneAction.js rename to lib/model/rules/actions/SceneAction.js diff --git a/lib/bridge-model/rules/actions/ScheduleStateAction.js b/lib/model/rules/actions/ScheduleStateAction.js similarity index 100% rename from lib/bridge-model/rules/actions/ScheduleStateAction.js rename to lib/model/rules/actions/ScheduleStateAction.js diff --git a/lib/bridge-model/rules/actions/SensorStateAction.js b/lib/model/rules/actions/SensorStateAction.js similarity index 100% rename from lib/bridge-model/rules/actions/SensorStateAction.js rename to lib/model/rules/actions/SensorStateAction.js diff --git a/lib/bridge-model/rules/actions/index.js b/lib/model/rules/actions/index.js similarity index 100% rename from lib/bridge-model/rules/actions/index.js rename to lib/model/rules/actions/index.js diff --git a/lib/bridge-model/rules/conditions/GroupCondition.js b/lib/model/rules/conditions/GroupCondition.js similarity index 100% rename from lib/bridge-model/rules/conditions/GroupCondition.js rename to lib/model/rules/conditions/GroupCondition.js diff --git a/lib/bridge-model/rules/conditions/GroupCondition.test.js b/lib/model/rules/conditions/GroupCondition.test.js similarity index 100% rename from lib/bridge-model/rules/conditions/GroupCondition.test.js rename to lib/model/rules/conditions/GroupCondition.test.js diff --git a/lib/bridge-model/rules/conditions/RuleCondition.js b/lib/model/rules/conditions/RuleCondition.js similarity index 100% rename from lib/bridge-model/rules/conditions/RuleCondition.js rename to lib/model/rules/conditions/RuleCondition.js diff --git a/lib/bridge-model/rules/conditions/SensorCondition.js b/lib/model/rules/conditions/SensorCondition.js similarity index 97% rename from lib/bridge-model/rules/conditions/SensorCondition.js rename to lib/model/rules/conditions/SensorCondition.js index 85bbfec..71b5ddf 100644 --- a/lib/bridge-model/rules/conditions/SensorCondition.js +++ b/lib/model/rules/conditions/SensorCondition.js @@ -1,7 +1,7 @@ 'use strict'; const RuleCondition = require('./RuleCondition') - , Sensor = require('../../devices/sensors/Sensor') + , Sensor = require('../../sensors/Sensor') , ApiError = require('../../../ApiError') , conditionOperators = require('./operators/index') ; @@ -77,7 +77,7 @@ function validateSensorAttribute(sensor, attributeName) { throw new ApiError('No Sensor provided to get attribute from'); } - const allAttributes = sensor.attributeNames; + const allAttributes = sensor.getStateAttributeNames(); if (allAttributes.indexOf(attributeName) > -1) { return attributeName; } else { diff --git a/lib/bridge-model/rules/conditions/SensorCondition.test.js b/lib/model/rules/conditions/SensorCondition.test.js similarity index 90% rename from lib/bridge-model/rules/conditions/SensorCondition.test.js rename to lib/model/rules/conditions/SensorCondition.test.js index 06d84dd..5360760 100644 --- a/lib/bridge-model/rules/conditions/SensorCondition.test.js +++ b/lib/model/rules/conditions/SensorCondition.test.js @@ -3,10 +3,8 @@ //TODO finish off tests const expect = require('chai').expect - , CLIPOpenClose = require('../../devices/sensors/CLIPOpenClose') - , CLIPSwitch = require('../../devices/sensors/CLIPSwitch') , operators = require('./operators/index') - + , model = require('../../index') , SensorCondition = require('./SensorCondition') ; @@ -33,7 +31,7 @@ describe('SensorCondition', () => { describe('CLIPOpenClose Sensor', () => { - const sensor = new CLIPOpenClose({state: {open: false}}, 1); + const sensor = model.createFromBridge('clipopenclose', 1, {state: {open: false}}); it('should create condition for changed', () => { const sensorCondition = new SensorCondition(sensor).when('open').changed() @@ -53,7 +51,7 @@ describe('SensorCondition', () => { describe('CLIPSwitch Sensor', () => { - const sensor = new CLIPSwitch({state: {buttonevent: 0}}); + const sensor = model.createFromBridge('clipswitch', 0, {state: {buttonevent: 0}}); it('should create condition for changed', () => { const sensorCondition = new SensorCondition(sensor).when('buttonevent').changed() diff --git a/lib/bridge-model/rules/conditions/index.js b/lib/model/rules/conditions/index.js similarity index 100% rename from lib/bridge-model/rules/conditions/index.js rename to lib/model/rules/conditions/index.js diff --git a/lib/bridge-model/rules/conditions/operators/Ddx.js b/lib/model/rules/conditions/operators/Ddx.js similarity index 100% rename from lib/bridge-model/rules/conditions/operators/Ddx.js rename to lib/model/rules/conditions/operators/Ddx.js diff --git a/lib/bridge-model/rules/conditions/operators/Dx.js b/lib/model/rules/conditions/operators/Dx.js similarity index 100% rename from lib/bridge-model/rules/conditions/operators/Dx.js rename to lib/model/rules/conditions/operators/Dx.js diff --git a/lib/bridge-model/rules/conditions/operators/Equals.js b/lib/model/rules/conditions/operators/Equals.js similarity index 100% rename from lib/bridge-model/rules/conditions/operators/Equals.js rename to lib/model/rules/conditions/operators/Equals.js diff --git a/lib/bridge-model/rules/conditions/operators/Equals.test.js b/lib/model/rules/conditions/operators/Equals.test.js similarity index 100% rename from lib/bridge-model/rules/conditions/operators/Equals.test.js rename to lib/model/rules/conditions/operators/Equals.test.js diff --git a/lib/bridge-model/rules/conditions/operators/GreaterThan.js b/lib/model/rules/conditions/operators/GreaterThan.js similarity index 100% rename from lib/bridge-model/rules/conditions/operators/GreaterThan.js rename to lib/model/rules/conditions/operators/GreaterThan.js diff --git a/lib/bridge-model/rules/conditions/operators/In.js b/lib/model/rules/conditions/operators/In.js similarity index 100% rename from lib/bridge-model/rules/conditions/operators/In.js rename to lib/model/rules/conditions/operators/In.js diff --git a/lib/bridge-model/rules/conditions/operators/LessThan.js b/lib/model/rules/conditions/operators/LessThan.js similarity index 100% rename from lib/bridge-model/rules/conditions/operators/LessThan.js rename to lib/model/rules/conditions/operators/LessThan.js diff --git a/lib/bridge-model/rules/conditions/operators/NotIn.js b/lib/model/rules/conditions/operators/NotIn.js similarity index 100% rename from lib/bridge-model/rules/conditions/operators/NotIn.js rename to lib/model/rules/conditions/operators/NotIn.js diff --git a/lib/bridge-model/rules/conditions/operators/NotStable.js b/lib/model/rules/conditions/operators/NotStable.js similarity index 100% rename from lib/bridge-model/rules/conditions/operators/NotStable.js rename to lib/model/rules/conditions/operators/NotStable.js diff --git a/lib/bridge-model/rules/conditions/operators/RuleConditionOperator.js b/lib/model/rules/conditions/operators/RuleConditionOperator.js similarity index 100% rename from lib/bridge-model/rules/conditions/operators/RuleConditionOperator.js rename to lib/model/rules/conditions/operators/RuleConditionOperator.js diff --git a/lib/bridge-model/rules/conditions/operators/Stable.js b/lib/model/rules/conditions/operators/Stable.js similarity index 100% rename from lib/bridge-model/rules/conditions/operators/Stable.js rename to lib/model/rules/conditions/operators/Stable.js diff --git a/lib/bridge-model/rules/conditions/operators/index.js b/lib/model/rules/conditions/operators/index.js similarity index 100% rename from lib/bridge-model/rules/conditions/operators/index.js rename to lib/model/rules/conditions/operators/index.js diff --git a/lib/model/scenes/GroupScene.js b/lib/model/scenes/GroupScene.js new file mode 100644 index 0000000..fa1f268 --- /dev/null +++ b/lib/model/scenes/GroupScene.js @@ -0,0 +1,35 @@ +'use strict'; + +const Scene = require('./Scene') + , types = require('../../types') +; + +const ATTRIBUTES = [ + types.string({name: 'group'}), + types.list({name: 'lights', optional: true, minEntries: 1, listType: types.string({name: 'lightId'})}), + types.object({name: 'lightstates'}), +]; + + +module.exports = class GroupScene extends Scene { + + constructor(attributes, type, id) { + super(ATTRIBUTES, 'GroupScene', id); + } + + get group() { + return this.getAttributeValue('group'); + } + + set group(id) { + return this.setAttributeValue('group', id); + } + + get lights() { + return this.getAttributeValue('lights'); + } + + get lightstates() { + return this.getAttributeValue('lightstates'); + } +}; \ No newline at end of file diff --git a/lib/model/scenes/LightScene.js b/lib/model/scenes/LightScene.js new file mode 100644 index 0000000..4e71a2a --- /dev/null +++ b/lib/model/scenes/LightScene.js @@ -0,0 +1,34 @@ +'use strict'; + +const Scene = require('./Scene') + , types = require('../../types') +; + +const ATTRIBUTES = [ + types.list({name: 'lights', minEntries: 1, listType: types.string({name: 'lightId'})}), + types.object({name: 'lightstates'}), +]; + + +module.exports = class LightScene extends Scene { + + constructor(id) { + super(ATTRIBUTES, 'LightScene', id); + } + + get lights() { + return this.getAttributeValue('lights'); + } + + set lights(lightIds) { + return this.setAttributeValue('lights', lightIds); + } + + get lightstates() { + return this.getAttributeValue('lightstates'); + } + + set lightstates(value) { + return this.setAttributeValue('lightstates', value); + } +}; \ No newline at end of file diff --git a/lib/model/scenes/Scene.js b/lib/model/scenes/Scene.js new file mode 100644 index 0000000..3dcfc78 --- /dev/null +++ b/lib/model/scenes/Scene.js @@ -0,0 +1,93 @@ +'use strict'; + +const BridgeObject = require('../BridgeObject') + , types = require('../../types') +; + +const ATTRIBUTES = [ + types.string({name: 'id', min: 1, max: 16}), + types.string({name: 'name', min: 1, max: 32}), + types.choice({name: 'type', validValues: ['LightScene', 'GroupScene'], defaultValue: 'LightScene'}), + types.string({name: 'owner'}), + types.boolean({name: 'recycle', defaultValue: false}), + types.boolean({name: 'locked'}), + types.object({name: 'appdata', types: [types.int8({name: 'version'}), types.string({name: 'data', min: 1, max: 16, optional: true})]}), + types.string({name: 'picture', min: 0, max: 16}), + types.string({name: 'lastupdated'}), //TODO this is a time stamp but it can be treated as a string here + types.int8({name: 'version'}), +]; + + +module.exports = class Scene extends BridgeObject { + + constructor(attributes, type, id) { + super(BridgeObject.mergeAttributes(ATTRIBUTES, attributes), id); + + this.setAttributeValue('type', type); + } + + get name() { + return this.getAttributeValue('name'); + } + + set name(value) { + return this.setAttributeValue('name', value) + } + + // get lightstates() { + // return this.getAttributeValue('lightstates'); + // } + // + // set lightstates(value) { + // //TODO needs to be updated + // + // // //TODO needs to be an {id: {}, id: {}} type object + // // this._updateRawDataValue('type', null); + // // return this._updateRawDataValue('lightstates', value); + // } + + get type() { + return this.getAttributeValue('type'); + } + + get owner() { + return this.getAttributeValue('owner'); + } + + get recycle() { + return this.getAttributeValue('recycle'); + } + + set recycle(value) { + return this.setAttributeValue('recycle', value); + } + + get locked() { + return this.getAttributeValue('locked'); + } + + get appdata() { + // Complex object of version, data + return this.getAttributeValue('appdata'); + } + + set appdata(value) { + return this.setAttributeValue('appdata', value); + } + + set picture(value) { + return this.setAttributeValue('picture', value); + } + + get picture() { + return this.getAttributeValue('picture'); + } + + get lastupdated() { + return this.getAttributeValue('lastupdated'); + } + + get version() { + return this.getAttributeValue('version'); + } +}; \ No newline at end of file diff --git a/lib/bridge-model/Scene.test.js b/lib/model/scenes/Scene.test.js similarity index 77% rename from lib/bridge-model/Scene.test.js rename to lib/model/scenes/Scene.test.js index 54adde1..226c8b5 100644 --- a/lib/bridge-model/Scene.test.js +++ b/lib/model/scenes/Scene.test.js @@ -4,9 +4,11 @@ const expect = require('chai').expect , Scene = require('./Scene') ; -describe('Scene', () => { +//TODO these tests need to be broken up to LightScene and GroupScene - describe('#create()', () => { +describe.skip('Scene', () => { + + describe.skip('#create()', () => { it('should create a simple scene', () => { const scene = new Scene({name: 'hello'}); @@ -19,7 +21,7 @@ describe('Scene', () => { const scene = new Scene({name: 'hello'}, 0); expect(scene).to.have.property('name').to.equal('hello'); - expect(scene).to.have.property('id').to.equal(0); + expect(scene).to.have.property('id').to.equal('0'); }); }); @@ -31,9 +33,7 @@ describe('Scene', () => { const scene = new Scene() , appdata = { version: 1, - data: { - name: 'secret' - } + data: 'a secret value' } ; scene.appdata = appdata; diff --git a/lib/model/sensors/CLIPGenericFlag.js b/lib/model/sensors/CLIPGenericFlag.js new file mode 100644 index 0000000..2273cdb --- /dev/null +++ b/lib/model/sensors/CLIPGenericFlag.js @@ -0,0 +1,26 @@ +'use strict'; + +const CLIPSensor = require('./CLIPSensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = []; + +const STATE_ATTRIBUTES = [ + types.boolean({name: 'flag'}), +]; + +module.exports = class CLIPGenericFlag extends CLIPSensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + get flag() { + return this.getStateAttributeValue('flag'); + } + + set flag(value) { + return this._updateStateAttributeValue('flag', value); + } +}; \ No newline at end of file diff --git a/lib/model/sensors/CLIPGenericStatus.js b/lib/model/sensors/CLIPGenericStatus.js new file mode 100644 index 0000000..ae4c249 --- /dev/null +++ b/lib/model/sensors/CLIPGenericStatus.js @@ -0,0 +1,26 @@ +'use strict'; + +const CLIPSensor = require('./CLIPSensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = []; + +const STATE_ATTRIBUTES = [ + types.int16({name: 'status'}), +]; + +module.exports = class CLIPGenericStatus extends CLIPSensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + get status() { + return this.getStateAttributeValue('status'); + } + + set status(value) { + return this._updateStateAttributeValue('status', value); + } +}; \ No newline at end of file diff --git a/lib/model/sensors/CLIPHumidity.js b/lib/model/sensors/CLIPHumidity.js new file mode 100644 index 0000000..10a6e67 --- /dev/null +++ b/lib/model/sensors/CLIPHumidity.js @@ -0,0 +1,27 @@ +'use strict'; + +const CLIPSensor = require('./CLIPSensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = []; + +const STATE_ATTRIBUTES = [ + types.uint16({name: 'humidity'}), +]; + +module.exports = class CLIPHumidity extends CLIPSensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + get humidity() { + return this.getStateAttributeValue('humidity'); + } + + set humidity(value) { + //TODO Current humidity 0.01% steps (e.g. 2000 is 20%)The bridge does not enforce range/resolution. + return this._updateStateAttributeValue('humidity', value); + } +}; \ No newline at end of file diff --git a/lib/model/sensors/CLIPLightlevel.js b/lib/model/sensors/CLIPLightlevel.js new file mode 100644 index 0000000..d580649 --- /dev/null +++ b/lib/model/sensors/CLIPLightlevel.js @@ -0,0 +1,71 @@ +'use strict'; + +const CLIPSensor = require('./CLIPSensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = [ + types.uint16({name: 'tholddark', defaultValue: 16000}), + types.uint16({name: 'tholdoffset', minValue: 1, defaultValue: 7000}), +]; + +const STATE_ATTRIBUTES = [ + types.uint16({name: 'lightlevel'}), + types.boolean({name: 'dark'}), + types.boolean({name: 'daylight'}), +]; + +module.exports = class CLIPLightlevel extends CLIPSensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + get tholddark() { + return this.getConfigAttributeValue('tholddark'); + } + + set tholddark(value) { + return this._updateConfigAttributeValue('tholddark', value); + } + + get thresholdDark() { + return this.tholddark; + } + + get tholdoffset() { + return this.getConfigAttributeValue('tholdoffset'); + } + + set tholdoffset(value) { + return this._updateConfigAttributeValue('tholdoffset', value); + } + + get thresholdOffset() { + return this.tholdoffset; + } + + get lightlevel() { + return this.getStateAttributeValue('lightlevel'); + } + + set lightlevel(value) { + return this._updateStateAttributeValue('lightlevel', value); + } + + get dark() { + return this.getStateAttributeValue('dark'); + } + + set dark(value) { + return this._updateStateAttributeValue('dark', value); + } + + get daylight() { + return this.getStateAttributeValue('daylight'); + } + + set daylight(value) { + return this._updateStateAttributeValue('daylight', value); + } +}; \ No newline at end of file diff --git a/lib/model/sensors/CLIPOpenClose.js b/lib/model/sensors/CLIPOpenClose.js new file mode 100644 index 0000000..8277aea --- /dev/null +++ b/lib/model/sensors/CLIPOpenClose.js @@ -0,0 +1,26 @@ +'use strict'; + +const CLIPSensor = require('./CLIPSensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = []; + +const STATE_ATTRIBUTES = [ + types.boolean({name: 'open'}) +]; + +module.exports = class CLIPOpenClose extends CLIPSensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + get open() { + return this.getStateAttributeValue('open'); + } + + set open(value) { + return this._updateStateAttributeValue('open', value); + } +}; \ No newline at end of file diff --git a/lib/model/sensors/CLIPPresence.js b/lib/model/sensors/CLIPPresence.js new file mode 100644 index 0000000..ffebc08 --- /dev/null +++ b/lib/model/sensors/CLIPPresence.js @@ -0,0 +1,26 @@ +'use strict'; + +const CLIPSensor = require('./CLIPSensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = []; + +const STATE_ATTRIBUTES = [ + types.boolean({name: 'presence'}), +]; + +module.exports = class CLIPPresence extends CLIPSensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + get presence() { + return this.getStateAttributeValue('presence'); + } + + set presence(value) { + return this._updateStateAttributeValue('presence', value); + } +}; \ No newline at end of file diff --git a/lib/bridge-model/devices/sensors/CLIPPresence.test.js b/lib/model/sensors/CLIPPresence.test.js similarity index 93% rename from lib/bridge-model/devices/sensors/CLIPPresence.test.js rename to lib/model/sensors/CLIPPresence.test.js index 2611df7..fc07dd7 100644 --- a/lib/bridge-model/devices/sensors/CLIPPresence.test.js +++ b/lib/model/sensors/CLIPPresence.test.js @@ -19,15 +19,18 @@ describe('CLIPPresence', () => { presence: false } } - , sensor = new CLIPPresence(data, 1) + , sensor = new CLIPPresence(1); ; + sensor._populate(data); expect(sensor).to.have.property('type').to.equal('CLIPPresence'); expect(sensor).to.have.property('name').to.equal(data.name); + expect(sensor).to.have.property('on').to.be.false; expect(sensor).to.have.property('url').to.equal(data.config.url); expect(sensor).to.have.property('battery').to.equal(data.config.battery); expect(sensor).to.have.property('reachable').to.be.true; + expect(sensor).to.have.property('presence').to.be.false; }); }); \ No newline at end of file diff --git a/lib/model/sensors/CLIPSensor.js b/lib/model/sensors/CLIPSensor.js new file mode 100644 index 0000000..8c2614e --- /dev/null +++ b/lib/model/sensors/CLIPSensor.js @@ -0,0 +1,58 @@ +'use strict'; + +const Sensor = require('./Sensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = [ + types.boolean({name: 'reachable'}), + types.uint8({name: 'battery', optional: true}), + types.string({name: 'url', minLength: 0, maxLength: 64}), +]; + +module.exports = class CLIPSensor extends Sensor { + + constructor(configAttributes, stateAttributes, id) { + super(Sensor.mergeAttributes(CONFIG_ATTRIBUTES, configAttributes), stateAttributes, id); + } + + get reachable() { + return this.getConfigAttributeValue('reachable'); + } + + set reachable(value) { + return this._updateConfigAttributeValue('reachable', value); + } + + get battery() { + return this.getConfigAttributeValue('battery'); + } + + set battery(value) { + return this._updateConfigAttributeValue('battery', value); + } + + get url() { + return this.getConfigAttributeValue('url'); + } + + set url(value) { + return this.setAttributeValue('url', value); + } + + set modelid(value) { + return this.setAttributeValue('modelid', value); + } + + set swversion(value) { + return this.setAttributeValue('swversion', value); + } + + set uniqueid(value) { + return this.setAttributeValue('uniqueid', value); + } + + set manufacturername(value) { + return this.setAttributeValue('manufacturername', value); + } +}; \ No newline at end of file diff --git a/lib/model/sensors/CLIPSwitch.js b/lib/model/sensors/CLIPSwitch.js new file mode 100644 index 0000000..6ad684e --- /dev/null +++ b/lib/model/sensors/CLIPSwitch.js @@ -0,0 +1,27 @@ +'use strict'; + +const CLIPSensor = require('./CLIPSensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = []; + +const STATE_ATTRIBUTES = [ + types.uint16({name: 'buttonevent'}), +]; + + +module.exports = class CLIPSwitch extends CLIPSensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + get buttonevent() { + return this.getStateAttributeValue('buttonevent'); + } + + set buttonevent(value) { + return this._updateStateAttributeValue('buttonevent', value); + } +}; \ No newline at end of file diff --git a/lib/model/sensors/CLIPSwitch.test.js b/lib/model/sensors/CLIPSwitch.test.js new file mode 100644 index 0000000..9d3b652 --- /dev/null +++ b/lib/model/sensors/CLIPSwitch.test.js @@ -0,0 +1,44 @@ +'use strict'; + +const expect = require('chai').expect + , CLIPSwitch = require('./CLIPSwitch') +; + +describe('CLIPSwitch', () => { + + const SENSOR_DATA = { + 'state': { + 'buttonevent': 0, + 'lastupdated': 'none' + }, + 'config': { + 'on': false, + 'reachable': true + }, + 'name': 'test switch', + 'type': 'CLIPSwitch', + 'modelid': 'TESTSENSOR', + 'manufacturername': 'node-hue-api', + 'swversion': '1.0', + 'uniqueid': '1-2-3-4', + 'recycle': true + }; + + + + it('should create one from valid data', () => { + const sensor = new CLIPSwitch(1); + sensor._populate(SENSOR_DATA); + + // expect(sensor).to.have.property('type').to.equal('CLIPPresence'); + expect(sensor).to.have.property('name').to.equal(SENSOR_DATA.name); + + expect(sensor).to.have.property('on').to.be.false; + expect(sensor).to.have.property('url').to.equal(null); + expect(sensor).to.have.property('battery').to.equal(null); + expect(sensor).to.have.property('reachable').to.be.true; + + expect(sensor).to.have.property('buttonevent').to.equal(SENSOR_DATA.state.buttonevent); + expect(sensor).to.have.property('lastupdated'); + }); +}); \ No newline at end of file diff --git a/lib/model/sensors/CLIPTemperature.js b/lib/model/sensors/CLIPTemperature.js new file mode 100644 index 0000000..bbdf86c --- /dev/null +++ b/lib/model/sensors/CLIPTemperature.js @@ -0,0 +1,26 @@ +'use strict'; + +const CLIPSensor = require('./CLIPSensor.js') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = []; + +const STATE_ATTRIBUTES = [ + types.int16({name: 'temperature'}), +]; + +module.exports = class CLIPTemperature extends CLIPSensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + get temperature() { + return this.getStateAttributeValue('temperature'); + } + + set temperature(value) { + return this._updateStateAttributeValue('temperature', value); + } +}; \ No newline at end of file diff --git a/lib/model/sensors/Daylight.js b/lib/model/sensors/Daylight.js new file mode 100644 index 0000000..31c09b9 --- /dev/null +++ b/lib/model/sensors/Daylight.js @@ -0,0 +1,67 @@ +'use strict'; + +const Sensor = require('./Sensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = [ + types.boolean({name: 'configured'}), + types.int8({name: 'sunriseoffset', defaultValue: 30, minValue: -120, maxValue: 120}), + types.int8({name: 'sunsetoffset', defaultValue: -30, minValue: -120, maxValue: 120}), + types.string({name: 'long'}), //TODO Can only set this, regex match required for this + types.string({name: 'lat'}), //TODO Can only set this + ] + , STATE_ATTRIBUTES = [ + types.boolean({name: 'daylight'}), + types.string({name: 'lastupdated'}), + ] +; + + +module.exports = class Daylight extends Sensor { + + constructor(id) { + super( CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + set long(value) { + this._updateConfigAttributeValue('long', value); + return this; + } + + set lat(value) { + this._updateConfigAttributeValue('lat', value); + return this; + } + + get configured() { + return this.getConfigAttributeValue('configured'); + } + + get sunriseoffset() { + return this.getConfigAttributeValue('sunriseoffset'); + } + + set sunriseoffset(value) { + this._updateConfigAttributeValue('sunriseoffset', value); + return this; + } + + get sunsetoffset() { + return this.getConfigAttributeValue('sunsetoffset'); + } + + set sunsetoffset(value) { + this._updateConfigAttributeValue('sunsetoffset', value); + return this; + } + + get daylight() { + return this.getStateAttributeValue('daylight'); + } + + set daylight(value) { + this._updateStateAttributeValue('daylight', !!value); + return this; + } +}; \ No newline at end of file diff --git a/lib/model/sensors/Daylight.test.js b/lib/model/sensors/Daylight.test.js new file mode 100644 index 0000000..c57b188 --- /dev/null +++ b/lib/model/sensors/Daylight.test.js @@ -0,0 +1,83 @@ +'use strict'; + +const expect = require('chai').expect + , Daylight = require('./Daylight') +; + +describe('Sensor :: Daylight', () => { + + const DATA = { + 'state': { + 'daylight': false, + 'lastupdated': '2019-11-06T15:56:00' + }, + 'config': { + 'on': true, + 'configured': true, + 'sunriseoffset': 30, + 'sunsetoffset': -30 + }, + 'name': 'Daylight', + 'type': 'Daylight', + 'modelid': 'PHDL00', + 'manufacturername': 'Philips', + 'swversion': '1.0' + }; + + const ID = 1; + + let sensor; + + beforeEach(() => { + sensor = new Daylight(ID); + sensor._populate(DATA); + }); + + + it('should create a Daylight Sensor using valid data', () => { + expect(sensor).to.have.property('id').to.equal(ID); + + expect(sensor).to.have.property('name').to.equal(DATA.name); + expect(sensor).to.have.property('type').to.equal(DATA.type); + expect(sensor).to.have.property('modelid').to.equal(DATA.modelid); + expect(sensor).to.have.property('manufacturername').to.equal(DATA.manufacturername); + expect(sensor).to.have.property('swversion').to.equal(DATA.swversion); + + expect(sensor).to.property('on').to.equal(DATA.config.on); + expect(sensor).to.property('configured').to.equal(DATA.config.configured); + expect(sensor).to.property('sunsetoffset').to.equal(DATA.config.sunsetoffset); + expect(sensor).to.property('sunriseoffset').to.equal(DATA.config.sunriseoffset); + + expect(sensor).to.have.property('daylight').to.equal(DATA.state.daylight); + expect(sensor).to.have.property('lastupdated').to.equal(DATA.state.lastupdated); + }); + + + describe('#on', () => { + + it('should update', () => { + expect(sensor).to.have.property('on').to.equal(DATA.config.on); + + sensor.on = !DATA.config.on; + expect(sensor).to.have.property('on').to.equal(!DATA.config.on); + + sensor.on = DATA.config.on; + expect(sensor).to.have.property('on').to.equal(DATA.config.on); + }); + }); + + + describe('#on', () => { + + it('should update', () => { + expect(sensor).to.have.property('on').to.equal(DATA.config.on); + + sensor.on = !DATA.config.on; + expect(sensor).to.have.property('on').to.equal(!DATA.config.on); + + sensor.on = DATA.config.on; + expect(sensor).to.have.property('on').to.equal(DATA.config.on); + }); + }); + +}); \ No newline at end of file diff --git a/lib/model/sensors/Sensor.js b/lib/model/sensors/Sensor.js new file mode 100644 index 0000000..ca8e284 --- /dev/null +++ b/lib/model/sensors/Sensor.js @@ -0,0 +1,199 @@ +'use strict'; + +const BridgeObject = require('../BridgeObject') + , parameters = require('../../types') +; + + +const COMMON_ATTRIBUTES = [ + parameters.int8({name: 'id'}), + parameters.string({name: 'name'}), + parameters.string({name: 'type'}), + parameters.string({name: 'modelid'}), + parameters.string({name: 'manufacturername'}), + parameters.string({name: 'uniqueid'}), + parameters.string({name: 'swversion'}), + parameters.string({name: 'swconfigid'}), //TODO this is not present on many devices + parameters.object({name: 'capabilities'}), +]; + +const COMMON_STATE_ATTRIBUTES = [ + parameters.string({name: 'lastupdated', defaultValue: 'none'}), +]; + +const COMMON_CONFIG_ATTRIBUTES = [ + parameters.boolean({name: 'on', defaultValue: true}), +]; + +module.exports = class Sensor extends BridgeObject { + + //TODO consider removing data from here as we have _populate to do this + constructor(configAttributes, stateAttributes, id, data) { + const stateAttribute = parameters.object({ + name: 'state', + types: Sensor.mergeAttributes(COMMON_STATE_ATTRIBUTES, stateAttributes) + }) + , configAttribute = parameters.object({ + name: 'config', + types: Sensor.mergeAttributes(COMMON_CONFIG_ATTRIBUTES, configAttributes) + }) + , allAttributes = Sensor.mergeAttributes(COMMON_ATTRIBUTES, stateAttribute, configAttribute) + ; + + super(allAttributes, id); + this._populate(data); + + // inject the name of the class as the type for the sensor + this.setAttributeValue('type', this.constructor.name); + + this._configAttributes = {}; + configAttribute.types.forEach(attr => { + this._configAttributes[attr.name] = attr; + }); + + this._stateAttributes = {}; + stateAttribute.types.forEach(attr => { + this._stateAttributes[attr.name] = attr; + }); + } + + set name(value) { + return this.setAttributeValue('name', value); + } + + get name() { + return this.getAttributeValue('name'); + } + + get modelid() { + return this.getAttributeValue('modelid'); + } + + get manufacturername() { + return this.getAttributeValue('manufacturername'); + } + + get swversion() { + return this.getAttributeValue('swversion'); + } + + get swconfigid() { + return this.getAttributeValue('swconfigid'); + } + + get type() { + return this.getAttributeValue('type'); + } + + get uniqueid() { + return this.getAttributeValue('uniqueid'); + } + + get capabilities() { + return this.getAttributeValue('capabilities'); + } + + get lastupdated() { + return this.getStateAttributeValue('lastupdated'); + } + + get on() { + return this.getConfigAttributeValue('on'); + } + + set on(value) { + this._updateConfigAttributeValue('on', value); + return this; + } + + getConfig() { + return this.getAttributeValue('config'); + } + + getConfigAttribute(name) { + return this._configAttributes[name]; + } + + getStateAttribute(name) { + return this._stateAttributes[name]; + } + + getStateAttributeNames() { + return Object.keys(this._stateAttributes); + } + + getConfigAttributeValue(name) { + const config = this.getAttributeValue('config') + , definition = this.getConfigAttribute(name) + ; + + if (definition) { + return definition.getValue(config[name]); + } else { + const value = config[name]; + if (value !== undefined) { + return value; + } + } + + return null; + } + + getStateAttributeValue(name) { + const state = this.getAttributeValue('state') + , definition = this.getStateAttribute(name) + ; + + if (definition) { + return definition.getValue(state[name]); + } else { + const value = state[name]; + if (value !== undefined) { + return value; + } + } + + return null; + } + + _updateStateAttributeValue(name, value) { + let state = this.getAttributeValue('state') || {}; + state[name] = value; + + // The object we are working on is a copy, so we need to set it back on the sensor, which will use the types to validate + return this.setAttributeValue('state', state); + } + + _updateConfigAttributeValue(name, value) { + const config = this.getAttributeValue('config') || {}; + config[name] = value; + + // The object we are working on is a copy, so we need to set it back on the sensor, which will use the types to validate + return this.setAttributeValue('config', config); + } + + getHuePayload() { + const data = super.getHuePayload(); + + Sensor.removeNullValues(data.config); + Sensor.removeNullValues(data.state); + + return data; + } + + + + //TODO util function + static removeNullValues(data) { + if (data) { + Object.keys(data).forEach(key => { + const value = data[key]; + if (value === null) { + delete data[key]; + } + }); + } + } +}; + + diff --git a/lib/bridge-model/devices/sensors/ZGPSwitch.js b/lib/model/sensors/ZGPSwitch.js similarity index 62% rename from lib/bridge-model/devices/sensors/ZGPSwitch.js rename to lib/model/sensors/ZGPSwitch.js index f886399..ff7dfb1 100644 --- a/lib/bridge-model/devices/sensors/ZGPSwitch.js +++ b/lib/model/sensors/ZGPSwitch.js @@ -1,29 +1,28 @@ 'use strict'; const Sensor = require('./Sensor') - , ApiError = require('../../../ApiError') + , types = require('../../types') ; -// Hue Tap Switch -module.exports = class ZGPSwitch extends Sensor { +const CONFIG_ATTRIBUTES = []; - constructor(data, id) { - super('ZGPSwitch', data, id); - } +const STATE_ATTRIBUTES = [ + types.uint16({name: 'buttonevent'}) +]; - get on() { - return this.config.on; - } - set on(value) { - this._updateConfigAttribute('on', value); +// Hue Tap Switch - Zigbee Green Power Switch +module.exports = class ZGPSwitch extends Sensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); } get buttonevent() { - return this.state.buttonevent; + return this.getStateAttributeValue('buttonevent'); } - // //TODO not sure that we can actually set these + // //TODO not sure that we can actually set these, create a test to see // set buttonevent(value) { // // Bridge does nto enforce these values, but the following correspond to each of the 4 buttons on the Hue Tap // if (value === 34 || value === 16 || value === 17 || value === 18) { diff --git a/lib/model/sensors/ZGPSwitch.test.js b/lib/model/sensors/ZGPSwitch.test.js new file mode 100644 index 0000000..65568d8 --- /dev/null +++ b/lib/model/sensors/ZGPSwitch.test.js @@ -0,0 +1,112 @@ +'use strict'; + +const expect = require('chai').expect + , ZGPSwitch = require('./ZGPSwitch') +; + +describe('Sensor :: ZGPSwitch', () => { + + const DATA = { + state: { + "buttonevent": 34, + "lastupdated": "2019-10-03T15:34:35" + }, + "swupdate": { + "state": "notupdatable", + "lastinstall": null + }, + "config": { + "on": true + }, + "name": "Hue Tap 1", + "type": "ZGPSwitch", + "modelid": "ZGPSWITCH", + "manufacturername": "Philips", + "productname": "Hue tap switch", + "diversityid": "d8cde5d5-0eef-4b95-b0f0-71ddd2952af4", + "uniqueid": "00:00:00:00:00:40:07:c7-f2", + "capabilities": { + "certified": true, + "primary": true, + "inputs": [ + { + "repeatintervals": [], + "events": [ + { + "buttonevent": 34, + "eventtype": "initial_press" + } + ] + }, + { + "repeatintervals": [], + "events": [ + { + "buttonevent": 16, + "eventtype": "initial_press" + } + ] + }, + { + "repeatintervals": [], + "events": [ + { + "buttonevent": 17, + "eventtype": "initial_press" + } + ] + }, + { + "repeatintervals": [], + "events": [ + { + "buttonevent": 18, + "eventtype": "initial_press" + } + ] + } + ] + } + }; + + const ID = 2; + + let sensor; + + beforeEach(() => { + sensor = new ZGPSwitch(ID); + sensor._populate(DATA); + }); + + + it('should create a ZGPSwitch Sensor using valid data', () => { + expect(sensor).to.have.property('id').to.equal(ID); + + expect(sensor).to.have.property('name').to.equal(DATA.name); + expect(sensor).to.have.property('type').to.equal(DATA.type); + expect(sensor).to.have.property('modelid').to.equal(DATA.modelid); + expect(sensor).to.have.property('manufacturername').to.equal(DATA.manufacturername); + + expect(sensor).to.property('on').to.equal(DATA.config.on); + + expect(sensor).to.have.property('buttonevent').to.equal(DATA.state.buttonevent); + expect(sensor).to.have.property('lastupdated').to.equal(DATA.state.lastupdated); + }); + + + describe('#on', () => { + + it('should update', () => { + expect(sensor).to.have.property('on').to.equal(DATA.config.on); + + sensor.on = !DATA.config.on; + expect(sensor).to.have.property('on').to.equal(!DATA.config.on); + + sensor.on = DATA.config.on; + expect(sensor).to.have.property('on').to.equal(DATA.config.on); + }); + }); + + //TODO test other properties of the sensor + +}); \ No newline at end of file diff --git a/lib/model/sensors/ZLLLightlevel.js b/lib/model/sensors/ZLLLightlevel.js new file mode 100644 index 0000000..711ddd6 --- /dev/null +++ b/lib/model/sensors/ZLLLightlevel.js @@ -0,0 +1,74 @@ +'use strict'; + +const Sensor = require('./Sensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = [ + types.uint16({name: 'tholddark', defaultValue: 16000}), + types.uint16({name: 'tholdoffset', minValue: 1, defaultValue: 7000}), +]; + +const STATE_ATTRIBUTES = [ + types.uint16({name: 'lightlevel'}), + types.boolean({name: 'dark'}), + types.boolean({name: 'daylight'}), +]; + +module.exports = class ZLLLightlevel extends Sensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + get tholddark() { + return this.getConfigAttributeValue('tholddark'); + } + + set tholddark(value) { + return this._updateConfigAttributeValue('tholddark', value); + } + + get thresholdDark() { + return this.tholddark; + } + + get tholdoffset() { + return this.getConfigAttributeValue('tholdoffset'); + } + + set tholdoffset(value) { + return this._updateConfigAttributeValue('tholdoffset', value); + } + + get thresholdOffset() { + return this.tholdoffset; + } + + get lightlevel() { + return this.getStateAttributeValue('lightlevel'); + } + + set lightlevel(value) { + return this._updateStateAttributeValue('lightlevel', value); + } + + get dark() { + return this.getStateAttributeValue('dark'); + } + + //TODO validate we can set this + // set dark(value) { + // return this._updateStateAttributeValue('dark', value); + // } + + get daylight() { + return this.getStateAttributeValue('daylight'); + } + + //TODO validate we can set this + // set daylight(value) { + // this._updateStateAttribute('daylight', !!value); + // return this; + // } +}; \ No newline at end of file diff --git a/lib/model/sensors/ZLLPresence.js b/lib/model/sensors/ZLLPresence.js new file mode 100644 index 0000000..045346e --- /dev/null +++ b/lib/model/sensors/ZLLPresence.js @@ -0,0 +1,69 @@ +'use strict'; + +const Sensor = require('./Sensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = [ + types.uint8({name: 'battery'}), + types.choice({name: 'alert', validValues: ['none', 'select', 'lselect'], defaultValue: 'none'}), + types.boolean({name: 'reachable'}), + types.uint16({name: 'sensitivity'}), + types.uint16({name: 'sensitivitymax'}), +]; + +const STATE_ATTRIBUTES = [ + types.boolean({name: 'presence'}), +]; + +// Hue Motion Sensor +module.exports = class ZLLPresense extends Sensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + get battery() { + return this.getConfigAttributeValue('battery'); + } + + set battery(value) { + return this._updateConfigAttributeValue('battery', value); + } + + get alert() { + return this.getConfigAttributeValue('alert'); + } + + set alert(value) { + return this._updateConfigAttributeValue('alert', value); + } + + get reachable() { + return this.getConfigAttributeValue('reachable'); + } + + set reachable(value) { + return this._updateConfigAttributeValue('reachable', value); + } + + get sensitivity() { + return this.getConfigAttributeValue('sensitivity'); + } + + set sensitivity(value) { + return this._updateConfigAttributeValue('sensitivity', value); + } + + get sensitivitymax() { + return this.getConfigAttributeValue('sensitivitymax'); + } + + set sensitivitymax(value) { + return this._updateConfigAttributeValue('sensitivitymax', value); + } + + get presence() { + return this.getStateAttributeValue('presence'); + } +}; \ No newline at end of file diff --git a/lib/model/sensors/ZLLSwitch.js b/lib/model/sensors/ZLLSwitch.js new file mode 100644 index 0000000..2af4891 --- /dev/null +++ b/lib/model/sensors/ZLLSwitch.js @@ -0,0 +1,56 @@ +'use strict'; + +const Sensor = require('./Sensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = [ + types.boolean({name: 'reachable'}), + types.uint8({name: 'battery'}), + types.choice({name: 'alert', validValues: ['none', 'select', 'lselect'], defaultValue: 'none'}), + types.list({name: 'pending', listType: types.string({name: 'pendingChange'}), minEntries: 0}) +]; + +const STATE_ATTRIBUTES = [ + types.boolean({name: 'buttonevent'}), +]; + +// Hue Dimmer Switch +module.exports = class ZLLSwitch extends Sensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + get battery() { + return this.getConfigAttributeValue('battery'); + } + + set battery(value) { + return this._updateConfigAttributeValue('battery', value); + } + + get alert() { + return this.getConfigAttributeValue('alert'); + } + + set alert(value) { + return this._updateConfigAttributeValue('alert', value); + } + + get reachable() { + return this.getConfigAttributeValue('reachable'); + } + + set reachable(value) { + return this._updateConfigAttributeValue('reachable', value); + } + + get pending() { + return this.getConfigAttributeValue('pending'); + } + + get buttonevent() { + return this.getStateAttributeValue('buttonevent'); + } +}; \ No newline at end of file diff --git a/lib/model/sensors/ZLLTemperature.js b/lib/model/sensors/ZLLTemperature.js new file mode 100644 index 0000000..25cce67 --- /dev/null +++ b/lib/model/sensors/ZLLTemperature.js @@ -0,0 +1,23 @@ +'use strict'; + +const Sensor = require('./Sensor') + , types = require('../../types') +; + +const CONFIG_ATTRIBUTES = []; + +const STATE_ATTRIBUTES = [ + types.int16({name: 'temperature'}), +]; + + +module.exports = class ZLLTemperature extends Sensor { + + constructor(id) { + super(CONFIG_ATTRIBUTES, STATE_ATTRIBUTES, id); + } + + get temperature() { + return this.getStateAttributeValue('temperature'); + } +}; \ No newline at end of file diff --git a/lib/parameters/BooleanType.js b/lib/parameters/BooleanType.js deleted file mode 100644 index 7da1bad..0000000 --- a/lib/parameters/BooleanType.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const ParameterType = require('./ParameterType'); - -module.exports = class BooleanType extends ParameterType { - - constructor(config) { - super(config); - this.type = 'boolean'; - } - - getValue(value) { - return Boolean(value); - } -}; \ No newline at end of file diff --git a/lib/parameters/ChoiceType.js b/lib/parameters/ChoiceType.js deleted file mode 100644 index 554332e..0000000 --- a/lib/parameters/ChoiceType.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const ParameterType = require('./ParameterType') - , ApiError = require('../ApiError') -; - -module.exports = class ChoiceType extends ParameterType { - - constructor(config) { - super(config); - - this.allowedValues = config.validValues; - this.defaultValue = config.defaultValue; - } - - getValue(val) { - if (!val) { - if (this.defaultValue) { - return this.defaultValue; - } else { - throw new ApiError('No value provided and no sensible default for type'); - } - } else { - if (this.allowedValues.indexOf(val) > -1) { - return val; - } else { - throw new ApiError(`Value '${val}' is not one of the allowed values [${this.allowedValues}]`); - } - } - } -}; diff --git a/lib/parameters/Int16Type.js b/lib/parameters/Int16Type.js deleted file mode 100644 index 6cc6d7f..0000000 --- a/lib/parameters/Int16Type.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -const IntegerType = require('./IntegerType'); - -module.exports = class Int16Type extends IntegerType { - - constructor(config) { - super(config, 'int16', -65535, 65535); - } -}; \ No newline at end of file diff --git a/lib/parameters/Int8Type.js b/lib/parameters/Int8Type.js deleted file mode 100644 index d6dcfeb..0000000 --- a/lib/parameters/Int8Type.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -const IntegerType = require('./IntegerType'); - -module.exports = class Int8Type extends IntegerType { - - constructor(config) { - super(config, 'int8', -255, 255); - } -}; \ No newline at end of file diff --git a/lib/parameters/IntegerType.js b/lib/parameters/IntegerType.js deleted file mode 100644 index 80e2446..0000000 --- a/lib/parameters/IntegerType.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -const RangedNumberType = require('./RangedNumberType'); - -module.exports = class IntegerType extends RangedNumberType { - - constructor(config, type, typeMin, typeMax) { - super(config, typeMin, typeMax); - this.type = type || 'integer'; - } -}; diff --git a/lib/parameters/ListType.js b/lib/parameters/ListType.js deleted file mode 100644 index 729a746..0000000 --- a/lib/parameters/ListType.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const ParameterType = require('./ParameterType') - , ApiError = require('../ApiError') -; - -module.exports = class ListType extends ParameterType { - - constructor(props) { - if (props.minEntries === null || props.minEntries === undefined) { - throw new ApiError('minEntries is required for a list type'); - } - - if (props.maxEntries === null || props.maxEntries === undefined) { - throw new ApiError('maxEntries is required for a list type'); - } - - super(props); - this.minEntries = props.minEntries; - this.maxEntries = props.maxEntries; - - //TODO validate that this value is a type - this.valueType = props.type; - } - - getValue() { - //TODO need to check the optional flag - let listValues; - - if (arguments.length === 1) { - if (Array.isArray(arguments[0])) { - listValues = arguments[0]; - } else { - throw new ApiError('Unexpected list type value'); - } - } else { - listValues = Array.from(arguments); - } - - // Validate the number of entries - const length = listValues.length; - if (length < this.minEntries && length > this.maxEntries) { - throw new ApiError(`The number of entries for the list, "${length}" is outside the range of min=${this.minEntries} max=${this.maxEntries}`); - } - - // Validate the values in the list - const result = []; - listValues.forEach(valueObj => { - result.push(this.valueType.getValue(valueObj)); - }); - return result; - } -}; diff --git a/lib/parameters/ParameterType.js b/lib/parameters/ParameterType.js deleted file mode 100644 index 3d4dd72..0000000 --- a/lib/parameters/ParameterType.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -module.exports = class ParameterType { - - constructor(config) { - this._name = config.name; - this.type = config.type; - this.isOptional = config.optional || true; - this.defaultValue = config.defaultValue; - } - - get name() { - return this._name; - } - - isOptional() { - return this.isOptional; - } - - getDefaultValue() { - return this.defaultValue || null; - } - - hasDefaultValue() { - return this.defaultValue !== null && this.defaultValue !== undefined && this.defaultValue !== Number.NaN; - } - - getValue(val) { - if (val) { - return val; - } else { - if (this.hasDefaultValue()) { - return this.getDefaultValue(); - } else { - return val; - } - } - } -}; \ No newline at end of file diff --git a/lib/parameters/RangedNumberType.js b/lib/parameters/RangedNumberType.js deleted file mode 100644 index 9b2dbbc..0000000 --- a/lib/parameters/RangedNumberType.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -const ParameterType = require('./ParameterType') - , ApiError = require('../ApiError'); - -module.exports = class RangedNumberType extends ParameterType { - - constructor(config, typeMin, typeMax) { - super(config); - - if (config.min !== undefined && config.min !== null) { - this.min = config.min; - } else { - this.min = typeMin; - } - - if (config.max !== undefined && config.max != null) { - this.max = config.max; - } else{ - this.max = typeMax; - } - } - - isValueValid(value) { - if (RangedNumberType.isValueDefined(value)) { - return value >= this.min && value <= this.max; - } else { - return false; - } - } - - static isValueDefined(value) { - return value !== null && value !== undefined && value !== Number.NaN; - } - - getValue(value) { - if (this.hasDefaultValue() && !RangedNumberType.isValueDefined(value)) { - return this.getDefaultValue(); - } else { - if (this.isValueValid(value)) { - return value; - } else { - throw new ApiError(`Value, '${value}' is not within allowed limits: min=${this.min} max=${this.max}`); - } - } - } - - getMinValue() { - return this.min; - } - - getMaxValue() { - return this.max; - } - - getRange() { - // return this.max - this.min; //TODO brightness has a lower bound of 1, which can generate quirks - return this.max; - } -}; \ No newline at end of file diff --git a/lib/parameters/StringType.js b/lib/parameters/StringType.js deleted file mode 100644 index b985005..0000000 --- a/lib/parameters/StringType.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const ParameterType = require('./ParameterType'); - -module.exports = class StringType extends ParameterType { - - constructor(config) { - super(config); - } - - getValue(value) { - const result = super.getValue(value); - return String(result); - } -}; \ No newline at end of file diff --git a/lib/parameters/UInt16Type.js b/lib/parameters/UInt16Type.js deleted file mode 100644 index 534616d..0000000 --- a/lib/parameters/UInt16Type.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -const IntegerType = require('./IntegerType'); - -module.exports = class UInt16Type extends IntegerType { - - constructor(config) { - super(config, 'uint16', 0, 65535); - } -}; \ No newline at end of file diff --git a/lib/parameters/UInt8Type.js b/lib/parameters/UInt8Type.js deleted file mode 100644 index 0d4116c..0000000 --- a/lib/parameters/UInt8Type.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const IntegerType = require('./IntegerType'); - -module.exports = class UInt8Type extends IntegerType { - constructor(config) { - super(config, 'uint8', 0, 255); - } -}; \ No newline at end of file diff --git a/lib/rgb.test.js b/lib/rgb.test.js index 883e68f..9607adc 100644 --- a/lib/rgb.test.js +++ b/lib/rgb.test.js @@ -2,7 +2,7 @@ const expect = require('chai').expect , rgb = require('./rgb') - , colorGamut = require('./bridge-model/devices/lights/color-gamuts') + , colorGamut = require('./model/colorGamuts') ; describe('RGB Conversion', () => { diff --git a/lib/types/Boolean.test.js b/lib/types/Boolean.test.js new file mode 100644 index 0000000..64dfbef --- /dev/null +++ b/lib/types/Boolean.test.js @@ -0,0 +1,84 @@ +'use strict'; + +const expect = require('chai').expect + , BooleanType = require('./BooleanType') +; + +describe('BooleanType', () => { + + + it('should have a type of boolean', () => { + const name = 'My Boolean Type' + , type = new BooleanType({name: name}); + + expect(type.name).to.equal(name); + expect(type.type).to.equal('boolean'); + }); + + + describe('no default value', () => { + + const type = new BooleanType({name: 'mytype'}); + + it('should return a value for true', () => { + expect(type.getValue(true)).to.be.true; + }); + + it('should return a value for false', () => { + expect(type.getValue(false)).to.be.false; + }); + + it('should convert null', () => { + expect(type.getValue(null)).to.equal(null); + }); + + it('should convert undefined', () => { + expect(type.getValue(undefined)).to.equal(undefined); + }); + }); + + + describe('with default value of false', () => { + + const type = new BooleanType({name: 'mytype', defaultValue: false}); + + it('should return a value for true', () => { + expect(type.getValue(true)).to.be.true; + }); + + it('should return a value for false', () => { + expect(type.getValue(false)).to.be.false; + }); + + it('should convert null', () => { + expect(type.getValue(null)).to.equal(false); + }); + + it('should convert undefined', () => { + expect(type.getValue(undefined)).to.equal(false); + }); + }); + + + describe('with default value of true', () => { + + const type = new BooleanType({name: 'mytype', defaultValue: true}); + + it('should return a value for true', () => { + expect(type.getValue(true)).to.be.true; + }); + + it('should return a value for false', () => { + expect(type.getValue(false)).to.be.false; + }); + + it('should convert null', () => { + expect(type.getValue(null)).to.equal(true); + }); + + it('should convert undefined', () => { + expect(type.getValue(undefined)).to.equal(true); + }); + }); + +}); \ No newline at end of file diff --git a/lib/types/BooleanType.js b/lib/types/BooleanType.js new file mode 100644 index 0000000..6cd71c7 --- /dev/null +++ b/lib/types/BooleanType.js @@ -0,0 +1,26 @@ +'use strict'; + +const Type = require('./Type'); + +module.exports = class BooleanType extends Type { + + constructor(config) { + super(Object.assign({type: 'boolean'}, config)); + } + + getValue(val) { + if (Type.isValueDefined(val)) { + return Boolean(val); + } else { + if (this.hasDefaultValue()) { + return Boolean(this.defaultValue); + } else { + if (this.optional) { + return val; + } else { + throw new ApiError(`No value provided and '${this.name}' is not optional`); + } + } + } + } +}; \ No newline at end of file diff --git a/lib/types/ChoiceType.js b/lib/types/ChoiceType.js new file mode 100644 index 0000000..646d0ac --- /dev/null +++ b/lib/types/ChoiceType.js @@ -0,0 +1,30 @@ +'use strict'; + +const Type = require('./Type') + , ApiError = require('../ApiError') +; + +module.exports = class ChoiceType extends Type { + + constructor(config) { + super(Object.assign({type: 'choice'}, config)); + + const validValues = config.validValues; + if (!Type.isValueDefined(validValues)) { + throw new ApiError('validValues config property is required for choice type'); + } + this._allowedValues = validValues; + } + + get validValues() { + return this._allowedValues; + } + + _convertToType(val) { + if (this.validValues.indexOf(val) > -1) { + return val; + } else { + throw new ApiError(`Value '${val}' is not one of the allowed values [${this.validValues}]`); + } + } +}; diff --git a/lib/types/ChoiceType.test.js b/lib/types/ChoiceType.test.js new file mode 100644 index 0000000..f55dead --- /dev/null +++ b/lib/types/ChoiceType.test.js @@ -0,0 +1,175 @@ +'use strict'; + +const expect = require('chai').expect + , ChoiceType = require('./ChoiceType') +; + + +describe('ChoiceType', () => { + + it('should fail to create a type with no choices', () => { + try { + new ChoiceType({name: 'type'}); + } catch(err) { + expect(err.message).to.contain('validValues config property') + } + }); + + function testFailure(type, val, errMessage) { + try { + type.getValue(val); + expect.fail('Should not get here'); + } catch (err) { + expect(err.message).to.contain(errMessage); + } + } + + + describe('#getValue()', () => { + + describe('string choices, optional, no default', () => { + + let choice; + + before(() => { + choice = new ChoiceType({ + name: 'Simple Choice', + validValues: [ + 'a', + 'b', + 'c', + ] + }); + }); + + it('should return a valid type value', () => { + expect(choice.getValue('a')).to.equal('a'); + expect(choice.getValue('b')).to.equal('b'); + expect(choice.getValue('c')).to.equal('c'); + }); + + it('should process null', () => { + expect(choice.getValue(null)).to.be.null; + }); + + it('should process undefined', () => { + expect(choice.getValue(undefined)).to.be.null; + }); + + it('should fail on choice not in valid values', () => { + testFailure(choice, 'z', 'is not one of the allowed values'); + }); + }); + + + describe('string choices, not optional, no default', () => { + + let choice; + + before(() => { + choice = new ChoiceType({ + name: 'Simple Choice', + optional: false, + validValues: [ + 'a', + 'b', + 'c', + ] + }); + }); + + it('should return a valid type value', () => { + expect(choice.getValue('a')).to.equal('a'); + expect(choice.getValue('b')).to.equal('b'); + expect(choice.getValue('c')).to.equal('c'); + }); + + it('should fail on null', () => { + testFailure(choice, null, 'is not optional'); + }); + + it('should fail on undefined', () => { + testFailure(choice, undefined, 'is not optional'); + }); + + it('should fail on choice not in valid values', () => { + testFailure(choice, 'z', 'is not one of the allowed values'); + }); + }); + + + describe('string choices, not optional, with default', () => { + + let choice; + + before(() => { + choice = new ChoiceType({ + name: 'Simple Choice', + optional: false, + defaultValue: 'a', + validValues: [ + 'a', + 'b', + 'c', + ] + }); + }); + + it('should return a valid type value', () => { + expect(choice.getValue('a')).to.equal('a'); + expect(choice.getValue('b')).to.equal('b'); + expect(choice.getValue('c')).to.equal('c'); + }); + + it('should fail on null', () => { + expect(choice.getValue(null)).to.equal('a'); + }); + + it('should fail on undefined', () => { + expect(choice.getValue(undefined)).to.equal('a'); + }); + + it('should fail on choice not in valid values', () => { + testFailure(choice, 'z', 'is not one of the allowed values'); + }); + }); + + + describe('integer choices, not optional, with default', () => { + + let choice; + + before(() => { + choice = new ChoiceType({ + name: 'Simple Choice', + optional: false, + defaultValue: 1, + validValues: [ + 0, + 1, + 2, + ] + }); + }); + + it('should return a valid type value', () => { + expect(choice.getValue(1)).to.equal(1); + expect(choice.getValue(2)).to.equal(2); + expect(choice.getValue(0)).to.equal(0); + }); + + it('should fail on null', () => { + expect(choice.getValue(null)).to.equal(1); + }); + + it('should fail on undefined', () => { + expect(choice.getValue(undefined)).to.equal(1); + }); + + it('should fail on choice not in valid values', () => { + testFailure(choice, 'z', 'is not one of the allowed values'); + }); + }); + + }); +}); \ No newline at end of file diff --git a/lib/parameters/FloatType.js b/lib/types/FloatType.js similarity index 65% rename from lib/parameters/FloatType.js rename to lib/types/FloatType.js index 99b69fc..7308dc1 100644 --- a/lib/parameters/FloatType.js +++ b/lib/types/FloatType.js @@ -5,7 +5,6 @@ const RangedNumberType = require('./RangedNumberType'); module.exports = class FloatType extends RangedNumberType { constructor(config) { - super(config, Number.MIN_VALUE, Number.MAX_VALUE); - this.type = 'float'; + super(Object.assign({type: 'float'}, config), -Number.MAX_VALUE, Number.MAX_VALUE); } }; \ No newline at end of file diff --git a/lib/types/FloatType.test.js b/lib/types/FloatType.test.js new file mode 100644 index 0000000..97f908d --- /dev/null +++ b/lib/types/FloatType.test.js @@ -0,0 +1,221 @@ +'use strict'; + +const expect = require('chai').expect + , FloatType = require('./FloatType') +; + + +const MAX_VALUE = Number.MAX_VALUE + , MIN_VALUE = -Number.MAX_VALUE + , OVER_MAX_VALUE = Number.POSITIVE_INFINITY + , UNDER_MIN_VALUE = Number.NEGATIVE_INFINITY +; + + +describe('FloatType', () => { + + describe('constructor', () => { + + it('should create a type', () => { + const name = 'my_type_of_float' + , type = new FloatType({name: name}) + ; + + expect(type).to.have.property('name').to.equal(name); + expect(type).to.have.property('type').to.equal('float'); + }); + }); + + + function testFailure(type, value, message) { + try { + type.getValue(value); + expect.fail('should not get here'); + } catch (err) { + expect(err.message).to.contain(message); + } + } + + describe('#getValue()', () => { + + describe('optional, no default', () => { + let intType = new FloatType({ + name: 'floatType', + optional: true + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process -1', () => { + expect(intType.getValue(-1)).to.equal(-1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process -255', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under min value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(null); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(null); + }); + }); + }); + + + describe('not optional, no default', () => { + + let intType = new FloatType({ + name: 'int8Type', + optional: false + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process -1', () => { + expect(intType.getValue(-1)).to.equal(-1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process min value', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under min value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should fail on null', () => { + testFailure(intType, null, 'is not optional'); + }); + + it('should fail on undefined', () => { + testFailure(intType, null, 'is not optional'); + }); + }); + + + describe('not optional, with default', () => { + + const DEFAULT_VALUE = 10; + + let intType = new FloatType({ + name: 'int8Type', + optional: false, + defaultValue: DEFAULT_VALUE + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process -1', () => { + expect(intType.getValue(-1)).to.equal(-1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process min value', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under max value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + }); + + + describe('not optional, with default and different max/min values', () => { + + const DEFAULT_VALUE = 10; + + let intType = new FloatType({ + name: 'int8Type', + optional: false, + defaultValue: DEFAULT_VALUE, + min: 10, + max: 15 + }); + + + it('should fail on 0', () => { + testFailure(intType, 0, 'not within allowed limits'); + }); + + it('should process 10', () => { + expect(intType.getValue(10)).to.equal(10); + }); + + it('should process 15', () => { + expect(intType.getValue(15)).to.equal(15); + }); + + it('should fail on 16', () => { + testFailure(intType, 16, 'not within allowed limits'); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + }); +}); + + diff --git a/lib/types/Int16Type.js b/lib/types/Int16Type.js new file mode 100644 index 0000000..e134a86 --- /dev/null +++ b/lib/types/Int16Type.js @@ -0,0 +1,14 @@ +'use strict'; + +const RangedNumberType = require('./RangedNumberType'); + +module.exports = class Int16Type extends RangedNumberType { + + constructor(config) { + super(Object.assign({type: 'int16'}, config), -65535, 65535); + } + + _convertToType(val) { + return parseInt(val); + } +}; \ No newline at end of file diff --git a/lib/types/Int16Type.test.js b/lib/types/Int16Type.test.js new file mode 100644 index 0000000..6f191fd --- /dev/null +++ b/lib/types/Int16Type.test.js @@ -0,0 +1,234 @@ +'use strict'; + +const expect = require('chai').expect + , Int16Type = require('./Int16Type') +; + +const MAX_VALUE = 65535 + , MIN_VALUE = -65535 + , OVER_MAX_VALUE = MAX_VALUE + 1 + , UNDER_MIN_VALUE = MIN_VALUE - 1 +; + +describe('Int16Type', () => { + + describe('constructor', () => { + + it('should create a type', () => { + const name = 'my_int_type' + , type = new Int16Type({name: name}) + ; + + expect(type).to.have.property('name').to.equal(name); + expect(type).to.have.property('type').to.equal('int16'); + }); + }); + + function testFailure(type, value, message) { + try { + type.getValue(value); + expect.fail('should not get here'); + } catch (err) { + expect(err.message).to.contain(message); + } + } + + describe('#getValue()', () => { + + describe('optional, no default', () => { + let intType = new Int16Type({ + name: 'intType', + optional: true + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process -1', () => { + expect(intType.getValue(-1)).to.equal(-1); + }); + + it('should process 1.2 as 1', () => { + expect(intType.getValue(1.2)).to.equal(1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process -255', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under min value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(null); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(null); + }); + }); + }); + + + describe('not optional, no default', () => { + + let intType = new Int16Type({ + name: 'int8Type', + optional: false + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process -1', () => { + expect(intType.getValue(-1)).to.equal(-1); + }); + + it('should process 1.2 as 1', () => { + expect(intType.getValue(1.2)).to.equal(1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process min value', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under min value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should fail on null', () => { + testFailure(intType, null, 'is not optional'); + }); + + it('should fail on undefined', () => { + testFailure(intType, null, 'is not optional'); + }); + }); + + + describe('not optional, with default', () => { + + const DEFAULT_VALUE = 10; + + let intType = new Int16Type({ + name: 'int8Type', + optional: false, + defaultValue: DEFAULT_VALUE + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process -1', () => { + expect(intType.getValue(-1)).to.equal(-1); + }); + + it('should process 1.2 as 1', () => { + expect(intType.getValue(1.2)).to.equal(1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process min value', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under max value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + }); + + + describe('not optional, with default and different max/min values', () => { + + const DEFAULT_VALUE = 10; + + let intType = new Int16Type({ + name: 'int8Type', + optional: false, + defaultValue: DEFAULT_VALUE, + min: 10, + max: 15 + }); + + + it('should fail on 0', () => { + testFailure(intType, 0, 'not within allowed limits'); + }); + + it('should process 10', () => { + expect(intType.getValue(10)).to.equal(10); + }); + + it('should process 15', () => { + expect(intType.getValue(15)).to.equal(15); + }); + + it('should fail on 16', () => { + testFailure(intType, 16, 'not within allowed limits'); + }); + + it('should process 11.2 as 11', () => { + expect(intType.getValue(11.2)).to.equal(11); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + }); +}); + + diff --git a/lib/types/Int8Type.js b/lib/types/Int8Type.js new file mode 100644 index 0000000..2c80a53 --- /dev/null +++ b/lib/types/Int8Type.js @@ -0,0 +1,14 @@ +'use strict'; + +const RangedNumberType = require('./RangedNumberType'); + +module.exports = class Int8Type extends RangedNumberType { + + constructor(config) { + super(Object.assign({type: 'int8'}, config), -255, 255); + } + + _convertToType(val) { + return parseInt(val); + } +}; \ No newline at end of file diff --git a/lib/types/Int8Type.test.js b/lib/types/Int8Type.test.js new file mode 100644 index 0000000..8def600 --- /dev/null +++ b/lib/types/Int8Type.test.js @@ -0,0 +1,237 @@ +'use strict'; + +const expect = require('chai').expect + , Int8Type = require('./Int8Type') +; + + +const MAX_VALUE = 255 + , MIN_VALUE = -255 + , OVER_MAX_VALUE = MAX_VALUE + 1 + , UNDER_MIN_VALUE = MIN_VALUE - 1 +; + + +describe('Int8Type', () => { + + describe('constructor', () => { + + it('should create a type', () => { + const name = 'my_int_type' + , type = new Int8Type({name: name}) + ; + + expect(type).to.have.property('name').to.equal(name); + expect(type).to.have.property('type').to.equal('int8'); + }); + }); + + + function testFailure(type, value, message) { + try { + type.getValue(value); + expect.fail('should not get here'); + } catch (err) { + expect(err.message).to.contain(message); + } + } + + describe('#getValue()', () => { + + describe('optional, no default', () => { + let intType = new Int8Type({ + name: 'int8Type', + optional: true + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process -1', () => { + expect(intType.getValue(-1)).to.equal(-1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process -255', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should process 1.2 as 1', () => { + expect(intType.getValue(1.2)).to.equal(1); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under min value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(null); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(null); + }); + }); + }); + + + describe('not optional, no default', () => { + + let intType = new Int8Type({ + name: 'int8Type', + optional: false + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process -1', () => { + expect(intType.getValue(-1)).to.equal(-1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process min value', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should process 1.2 as 1', () => { + expect(intType.getValue(1.2)).to.equal(1); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under min value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should fail on null', () => { + testFailure(intType, null, 'is not optional'); + }); + + it('should fail on undefined', () => { + testFailure(intType, null, 'is not optional'); + }); + }); + + + describe('not optional, with default', () => { + + const DEFAULT_VALUE = 10; + + let intType = new Int8Type({ + name: 'int8Type', + optional: false, + defaultValue: DEFAULT_VALUE + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process -1', () => { + expect(intType.getValue(-1)).to.equal(-1); + }); + + it('should process 1.2 as 1', () => { + expect(intType.getValue(1.2)).to.equal(1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process min value', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under max value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + }); + + + describe('not optional, with default and different max/min values', () => { + + const DEFAULT_VALUE = 10; + + let intType = new Int8Type({ + name: 'int8Type', + optional: false, + defaultValue: DEFAULT_VALUE, + min: 10, + max: 15 + }); + + + it('should fail on 0', () => { + testFailure(intType, 0, 'not within allowed limits'); + }); + + it('should process 10', () => { + expect(intType.getValue(10)).to.equal(10); + }); + + it('should process 15', () => { + expect(intType.getValue(15)).to.equal(15); + }); + + it('should fail on 16', () => { + testFailure(intType, 16, 'not within allowed limits'); + }); + + it('should process 10.5 as 10', () => { + expect(intType.getValue(10.5)).to.equal(10); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + }); +}); + + diff --git a/lib/types/ListType.js b/lib/types/ListType.js new file mode 100644 index 0000000..ce8ef7e --- /dev/null +++ b/lib/types/ListType.js @@ -0,0 +1,77 @@ +'use strict'; + +const Type = require('./Type') + , ApiError = require('../ApiError') +; + +module.exports = class ListType extends Type { + + constructor(config) { + if (config.minEntries === null || config.minEntries === undefined) { + throw new ApiError('minEntries is required for a list type'); + } + + // if (props.maxEntries === null || props.maxEntries === undefined) { + // throw new ApiError('maxEntries is required for a list type'); + // } + + super(Object.assign({type: 'list'}, config)); + this.minEntries = config.minEntries; + this.maxEntries = config.maxEntries; + + const type = config.listType; + if (!(type instanceof Type)) { + throw new ApiError(`listType must be an instance of a Type, not ${type}`); + } + this._listType = type; + } + + get listType() { + return this._listType; + } + + getValue() { + const listValues = super.getValue.apply(this, Array.from(arguments)); + + if (!Type.isValueDefined(listValues)) { + // Validate the min entries requirement is met + if (this.minEntries === 0) { + return listValues; + } else { + throw new ApiError(`Type ${this.name}, minEntries requirement not satisfied, required ${this.minEntries}, but have null object`); + } + } + + // Value is defined, so validate it according to specification + const length = listValues.length; + if (length < this.minEntries) { + throw new ApiError(`The number of entries for the list, "${length}" is less than required minimum of ${this.minEntries}`); + } + + if (this.maxEntries && length > this.maxEntries) { + throw new ApiError(`The number of entries for the list, ${length}, is greater than required maximum of ${this.maxEntries}`); + } + + return listValues; + } + + _convertToType(val) { + if (!Type.isValueDefined(val)) { + return null; + } + + const result = [] + , type = this.listType + ; + + if (Array.isArray(val)) { + val.forEach(value => { + result.push(type.getValue(value)); + }); + } else { + result.push(type.getValue(val)) + } + + return result; + } +}; diff --git a/lib/types/ListType.test.js b/lib/types/ListType.test.js new file mode 100644 index 0000000..07d560f --- /dev/null +++ b/lib/types/ListType.test.js @@ -0,0 +1,312 @@ +'use strict'; + +const expect = require('chai').expect + , ListType = require('./ListType') + , StringType = require('./StringType') + , FloatType = require('./FloatType') +; + +//TODO increase coverage of permutations of min/max/required + +describe('ListType', () => { + + it('should fail to create a type with missing minValues property', () => { + try { + new ListType({name: 'type'}); + } catch (err) { + expect(err.message).to.contain('minEntries is required'); + } + }); + + it('should fail to create a type with invalid types property', () => { + try { + new ListType({name: 'type', minEntries: 0, types: 'hello'}); + } catch (err) { + expect(err.message).to.contain('listType must be an instance of a Type'); + } + }); + + function testFailure(type, val, errMessage) { + try { + type.getValue(val); + expect.fail('Should not get here'); + } catch (err) { + expect(err.message).to.contain(errMessage); + } + } + + + describe('#getValue()', () => { + + describe('list of strings', () => { + + describe('optional with minEntries = 0', () => { + let type; + + before(() => { + type = new ListType({ + name: 'string list', + optional: true, + minEntries: 0, + listType: new StringType({name: 'stringEntry', optional: true}) + }); + }); + + + it('should process null', () => { + expect(type.getValue(null)).to.be.null; + }); + + it('should process undefined', () => { + expect(type.getValue(undefined)).to.be.null; + }); + + it('should process a string value', () => { + expect(type.getValue('hello')).to.have.members(['hello']); + }); + + it('should process a string array', () => { + const members = ['a', 'b', 'c', '1']; + expect(type.getValue(members)).to.have.members(members); + }); + + it('should process integers into string array', () => { + const values = [1, 2, 3] + , strings = values.map(val => `${val}`) + ; + expect(type.getValue(values)).to.have.members(strings); + }); + }); + + + describe('optional with minEntries = 1', () => { + let type; + + before(() => { + type = new ListType({ + name: 'string list', + optional: true, + minEntries: 1, + listType: new StringType({name: 'stringEntry', optional: true}) + }); + }); + + + it('should fail on null', () => { + testFailure(type, null, 'minEntries requirement not satisfied'); + }); + + it('should fail on undefined', () => { + testFailure(type, undefined, 'minEntries requirement not satisfied'); + }); + + it('should process a string value', () => { + expect(type.getValue('hello')).to.have.members(['hello']); + }); + + it('should process a string array', () => { + const members = ['a', 'b', 'c', '1']; + expect(type.getValue(members)).to.have.members(members); + }); + + it('should process integers into string array', () => { + const values = [1, 2, 3] + , strings = values.map(val => `${val}`) + ; + expect(type.getValue(values)).to.have.members(strings); + }); + }); + + + describe('not optional with minEntries = 0', () => { + let type; + + before(() => { + type = new ListType({ + name: 'string list', + optional: false, + minEntries: 0, + listType: new StringType({name: 'stringEntry', optional: true}) + }); + }); + + + it('should fail on null', () => { + testFailure(type, null, 'is not optional'); + }); + + it('should fail on undefined', () => { + testFailure(type, undefined, 'is not optional'); + }); + + it('should process a string value', () => { + expect(type.getValue('hello')).to.have.members(['hello']); + }); + + it('should process a string array', () => { + const members = ['a', 'b', 'c', '1']; + expect(type.getValue(members)).to.have.members(members); + }); + + it('should process integers into string array', () => { + const values = [1, 2, 3] + , strings = values.map(val => `${val}`) + ; + expect(type.getValue(values)).to.have.members(strings); + }); + }); + + + describe('not optional with minEntries = 1', () => { + let type; + + before(() => { + type = new ListType({ + name: 'string list', + optional: false, + minEntries: 1, + listType: new StringType({name: 'stringEntry', optional: true}) + }); + }); + + + it('should fail on null', () => { + testFailure(type, null, 'is not optional'); + }); + + it('should fail on undefined', () => { + testFailure(type, undefined, 'is not optional'); + }); + + it('should process a string value', () => { + expect(type.getValue('hello')).to.have.members(['hello']); + }); + + it('should process a string array', () => { + const members = ['a', 'b', 'c', '1']; + expect(type.getValue(members)).to.have.members(members); + }); + + it('should process integers into string array', () => { + const values = [1, 2, 3] + , strings = values.map(val => `${val}`) + ; + expect(type.getValue(values)).to.have.members(strings); + }); + }); + }); + + + describe('list of floats', () => { + + describe('not optional with minEntries = 0', () => { + let type; + + before(() => { + type = new ListType({ + name: 'float list', + optional: false, + minEntries: 0, + listType: new FloatType({name: 'floatEntry', optional: true}) + }); + }); + + + it('should fail on null', () => { + testFailure(type, null, 'is not optional'); + }); + + it('should fail on undefined', () => { + testFailure(type, undefined, 'is not optional'); + }); + + it('should fail on a string value', () => { + testFailure(type, 'hello', 'not a parsable number value'); + }); + + it('should process a string integer value', () => { + expect(type.getValue('1')).to.have.members([1]); + }); + + it('should process a string float value', () => { + expect(type.getValue('1.5')).to.have.members([1.5]); + }); + + it('should process a string array of floats', () => { + const members = ['1', '2', '3.5'] + , expected = members.map(val => Number(val)) + , result = type.getValue(members) + ; + expect(result).to.have.members(expected); + }); + + it('should process an array of floats', () => { + const members = [1, 2.1, 0.5] + , result = type.getValue(members) + ; + expect(result).to.have.members(members); + }); + + it('should process a mixed array of floats as numbers and strings', () => { + const members = [1, 2.1, 0.5, '3', '12.67'] + , expected = members.map(val => Number(val)) + , result = type.getValue(members) + ; + expect(result).to.have.members(expected); + }); + + it('should fail with invalid values', () => { + testFailure(type, [1.1, '1.1.1'], '\'1.1.1\' is not a parsable number'); + }); + }); + + + describe('max entries', () => { + + let type; + + before(() => { + type = new ListType({ + name: 'float list', + optional: false, + minEntries: 0, + maxEntries: 2, + listType: new FloatType({name: 'floatEntry', optional: true}) + }); + }); + + it('should fail on null', () => { + testFailure(type, null, 'is not optional'); + }); + + it('should fail on undefined', () => { + testFailure(type, undefined, 'is not optional'); + }); + + it('should fail on a string value', () => { + testFailure(type, 'hello', 'not a parsable number value'); + }); + + it('should process a string integer value', () => { + expect(type.getValue('1')).to.have.members([1]); + }); + + it('should process a string float value', () => { + expect(type.getValue('1.5')).to.have.members([1.5]); + }); + + it('should process an array of floats within limits', () => { + const members = [1, 2.1] + , result = type.getValue(members) + ; + expect(result).to.have.members(members); + }); + + it('should fail on too many entries', () => { + testFailure(type, [1, 2.1, 1.5], 'greater than required maximum'); + }); + }); + + }); + }); +}); \ No newline at end of file diff --git a/lib/types/ObjectType.js b/lib/types/ObjectType.js new file mode 100644 index 0000000..119d3fd --- /dev/null +++ b/lib/types/ObjectType.js @@ -0,0 +1,96 @@ +'use strict'; + +const Type = require('./Type') + , ApiError = require('../ApiError') +; + +module.exports = class ObjectType extends Type { + + constructor(config) { + super(Object.assign({type: 'object'}, config)); + + const types = config.types; + + if (!Type.isValueDefined(types)) { + this._types = null; + this._childRequiredKeys = []; + } else { + if (!Array.isArray(types)) { + throw new ApiError('types definition must be an Array of types'); + } + + const childRequiredKeys = []; + types.forEach(type => { + if (!(type instanceof Type)) { + throw new ApiError(`type specified as ${JSON.stringify(type)} is not an instance of Type class`); + } + + if (!type.optional) { + childRequiredKeys.push(type.name); + } + }); + + this._types = types; + this._childRequiredKeys = childRequiredKeys; + } + } + + get types() { + return this._types; + } + + get childRequiredKeys() { + return this._childRequiredKeys; + } + + _convertToType(val) { + const result = this._getObject(val); + this._validateRequiredKeys(result); + + + if (Object.keys(result).length === 0) { + if (this.optional) { + return null; + } else { + throw new ApiError(`Empty object created from data provided, but the object is not optional`); + } + } + + return result; + } + + _getObject(val) { + // We have a free form object type + if (!this.types) { + return Object.assign({}, val); + } + + const result = {}; + // Build the object based off the definitions for the keys + this.types.forEach(typeAttribute => { + const name = typeAttribute.name + , typeValue = typeAttribute.getValue(val[name]) + ; + + if (Type.isValueDefined(typeValue)) { + result[name] = typeValue; + } + }); + return result; + } + + + _validateRequiredKeys(result) { + if (this.childRequiredKeys.length > 0) { + const valueKeys = Object.keys(result); + + this.childRequiredKeys.forEach(requiredKey => { + if (valueKeys.indexOf(requiredKey) === -1) { + throw new ApiError(`Required key '${requiredKey}' is missing from the object`); + } + }); + } + } +}; + + diff --git a/lib/types/ObjectType.test.js b/lib/types/ObjectType.test.js new file mode 100644 index 0000000..dbfa900 --- /dev/null +++ b/lib/types/ObjectType.test.js @@ -0,0 +1,261 @@ +'use strict'; + +const expect = require('chai').expect + , ObjectType = require('./ObjectType') + , Int8Type = require('./Int8Type') + , StringType = require('./StringType') +; + + +describe('ObjectType', () => { + + describe('constructor', () => { + + it('should create a type', () => { + const name = 'custom_object_type' + , type = new ObjectType({name: name, types: [new Int8Type({name: 'id'})]}); + ; + + expect(type).to.have.property('name').to.equal(name); + expect(type).to.have.property('type').to.equal('object'); + }); + }); + + + describe('#getValue()', () => { + + function testFailure(type, data, expectedMessage) { + try { + type.getValue(data); + expect.fail('should not get here'); + } catch(err) { + expect(err.message).to.contain(expectedMessage); + } + } + + describe('with no defaults', () => { + + let type; + + before(() => { + type = new ObjectType({ + name: 'object_type_no_default', + types: [ + new Int8Type({name: 'id'}), + new StringType({name: 'name'}) + ] + }) + }); + + it('should process empty object', () => { + const result = type.getValue({}); + + expect(result).to.be.null; + }); + + it('should process null', () => { + const result = type.getValue(null); + expect(result).to.be.null; + }); + + it('should process undefined', () => { + const result = type.getValue(undefined); + expect(result).to.be.null; + }); + + it('should process a matching object', () => { + const data = {id: 100, name: 'sensor'} + , result = type.getValue(data) + ; + + expect(result).to.deep.equal(data); + }); + + it('should process an object with extra keys', () => { + const data = {id: 100, name: 'sensor', description: 'An extra key on the payload'} + , result = type.getValue(data) + ; + + expect(result).to.not.have.property('description'); + expect(result).to.deep.equal({id: data.id, name: data.name}); + }); + }); + + + describe('with defaults', () => { + + let type; + + before(() => { + type = new ObjectType({ + name: 'object_type_no_default', + types: [ + new Int8Type({name: 'id', defaultValue: 0}), + new StringType({name: 'name'}) + ] + }) + }); + + it('should process empty object', () => { + const result = type.getValue({}); + + expect(result).to.have.property('id').to.equal(0); + expect(result).to.not.have.property('name'); + }); + + it('should process null', () => { + const result = type.getValue(null); + expect(result).to.be.null; + }); + + it('should process undefined', () => { + const result = type.getValue(undefined); + expect(result).to.be.null; + }); + + it('should process a matching object', () => { + const data = {id: 100, name: 'sensor'} + , result = type.getValue(data) + ; + + expect(result).to.deep.equal(data); + }); + + it('should process an object with extra keys', () => { + const data = {id: 100, name: 'sensor', description: 'An extra key on the payload'} + , result = type.getValue(data) + ; + + expect(result).to.not.have.property('description'); + expect(result).to.deep.equal({id: data.id, name: data.name}); + }); + }); + + + describe('with non-optional values', () => { + + let type; + + before(() => { + type = new ObjectType({ + name: 'object_type_no_default', + types: [ + new Int8Type({name: 'id', optional: false}), + new StringType({name: 'name', optional: false}) + ] + }) + }); + + it('should fail empty object', () => { + testFailure(type, {}, '\'id\' is not optional'); + }); + + it('should fail on missing id', () => { + testFailure(type, {name: 'sensor'}, '\'id\' is not optional'); + }); + + it('should fail on missing name', () => { + testFailure(type, {id: 100}, '\'name\' is not optional'); + }); + + it('should fail on missing name', () => { + testFailure(type, {id: 100}, '\'name\' is not optional'); + }); + + it('should fail on missing name and id', () => { + testFailure(type, {idValue: 100, nameValue: 'hello'}, '\'id\' is not optional'); + }); + + it('should process null', () => { + const result = type.getValue(null); + expect(result).to.be.null; + }); + + it('should process undefined', () => { + const result = type.getValue(undefined); + expect(result).to.be.null; + }); + + it('should process a matching object', () => { + const data = {id: 100, name: 'sensor'} + , result = type.getValue(data) + ; + + expect(result).to.deep.equal(data); + }); + + it('should process an object with extra keys', () => { + const data = {id: 100, name: 'sensor', description: 'An extra key on the payload'} + , result = type.getValue(data) + ; + + expect(result).to.not.have.property('description'); + expect(result).to.deep.equal({id: data.id, name: data.name}); + }); + }); + + + describe('with non-optional object', () => { + + let type; + + before(() => { + type = new ObjectType({ + name: 'object_type_no_default', + types: [ + new Int8Type({name: 'id', optional: true}), + new StringType({name: 'name', optional: true}) + ], + optional: false, + }) + }); + + it('should fail empty object', () => { + testFailure(type, {}, 'Empty object created from data provided'); + }); + + it('should process with only name key', () => { + const data = {name: 'sensor'} + , result = type.getValue(data) + ; + expect(result).to.deep.equal(data); + }); + + it('should process with only id key', () => { + const data = {id: 1} + , result = type.getValue(data) + ; + expect(result).to.deep.equal(data); + }); + + it('should fail on missing name and id', () => { + testFailure(type, {idValue: 100, nameValue: 'hello'}, 'Empty object created'); + }); + + it('should process null', () => { + testFailure(type, null, 'is not optional'); + }); + + it('should process undefined', () => { + testFailure(type, undefined, 'is not optional'); + }); + + it('should process a matching object', () => { + const data = {id: 100, name: 'sensor'} + , result = type.getValue(data) + ; + + expect(result).to.deep.equal(data); + }); + + it('should process an object with extra keys', () => { + const data = {id: 100, name: 'sensor', description: 'An extra key on the payload'} + , result = type.getValue(data) + ; + + expect(result).to.not.have.property('description'); + expect(result).to.deep.equal({id: data.id, name: data.name}); + }); + }); + }); +}); \ No newline at end of file diff --git a/lib/types/RangedNumberType.js b/lib/types/RangedNumberType.js new file mode 100644 index 0000000..dced61b --- /dev/null +++ b/lib/types/RangedNumberType.js @@ -0,0 +1,65 @@ +'use strict'; + +const Type = require('./Type') + , ApiError = require('../ApiError'); + +module.exports = class RangedNumberType extends Type { + + constructor(config, typeMin, typeMax) { + super(config); + + if (Type.isValueDefined(config.min)) { + this.min = config.min; + } else { + this.min = typeMin; + } + + if (Type.isValueDefined(config.max)) { + this.max = config.max; + } else{ + this.max = typeMax; + } + } + + getValue(value) { + const numberValue = super.getValue(value); + + // Value has been checked in the super function and is optional + if (numberValue === null) { + return null; + } + + // Invalid input value + if (Number.isNaN(numberValue)) { + throw new ApiError(`Failure to convert value for ${this.name}, value, '${value}' is not a parsable number'`); + } + + if (this.isValueInRange(numberValue)) { + return numberValue; + } else { + throw new ApiError(`Value, '${numberValue}' is not within allowed limits: min=${this.getMinValue()} max=${this.getMaxValue()} for '${this.name}'`); + } + } + + _convertToType(val) { + return Number(val); + } + + isValueInRange(value) { + return value >= this.getMinValue() && value <= this.getMaxValue(); + } + + getMinValue() { + return this.min; + } + + getMaxValue() { + return this.max; + } + + //TODO check this is still in use + getRange() { + // return this.max - this.min; //TODO brightness has a lower bound of 1, which can generate quirks + return this.max; + } +}; \ No newline at end of file diff --git a/lib/types/StringType.js b/lib/types/StringType.js new file mode 100644 index 0000000..6097489 --- /dev/null +++ b/lib/types/StringType.js @@ -0,0 +1,66 @@ +'use strict'; + +const Type = require('./Type') + , ApiError = require('../ApiError') +; + + +module.exports = class StringType extends Type { + + constructor(config) { + super(Object.assign({type: 'string'}, config)); + + if (Type.isValueDefined(config.min)) { + this.min = config.min; + } else { + this.min = null; + } + + if (Type.isValueDefined(config.max)) { + this.max = config.max; + } else{ + this.max = null; + } + } + + get minLength() { + return this.min; + } + + get maxLength() { + return this.max; + } + + getValue(value) { + const checkedValue = super.getValue(value) + , isValueDefined = Type.isValueDefined(checkedValue) + , optional = this.optional + ; + + // If we are optional and have no value, prevent further checks as they will fail + if (optional && !isValueDefined) { + return checkedValue; + } + + // 0 will not trigger this, but it is not a problem in this context + if (this.minLength) { + if (!isValueDefined) { + throw new ApiError(`No value provided for ${this.name}, must have a minimum length of ${this.minLength}`); + } else if (checkedValue.length < this.min) { + throw new ApiError(`'${value}' for ${this.name}, does not meet minimum length requirement of ${this.minLength}`); + } + } + + // 0 will not trigger this, but it is not a problem in this context, although max length of 0 is not really valid + if (this.maxLength) { + if (isValueDefined && checkedValue.length > this.maxLength) { + throw new ApiError(`'${value}' for ${this.name}, does not meet maximum length requirement of ${this.maxLength}`); + } + } + return checkedValue; + } + + _convertToType(val) { + return `${val}`; + } +}; \ No newline at end of file diff --git a/lib/types/StringType.test.js b/lib/types/StringType.test.js new file mode 100644 index 0000000..81c1051 --- /dev/null +++ b/lib/types/StringType.test.js @@ -0,0 +1,350 @@ +'use strict'; + +const expect = require('chai').expect + , StringType = require('./StringType') +; + +describe('StringType', () => { + + it('should create one with a name and type', () => { + const name = 'myType' + , type = 'custom' + , result = new StringType({name: name, type: type}) + ; + + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('type').to.equal(type); + expect(result).to.have.property('optional').to.be.true; + expect(result).to.have.property('defaultValue').to.be.null; + }); + + it('should create one with a name, type and defaultValue', () => { + const name = 'myType' + , type = 'custom' + , defaultValue = 'hello' + , result = new StringType({name: name, type: type, defaultValue: defaultValue}) + ; + + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('type').to.equal(type); + expect(result).to.have.property('optional').to.be.true; + expect(result).to.have.property('defaultValue').to.equal(defaultValue); + }); + + it('should create one with a name, type and optional', () => { + const name = 'myType' + , type = 'custom' + , optional = false + , result = new StringType({name: name, type: type, optional: optional}) + ; + + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('type').to.equal(type); + expect(result).to.have.property('optional').to.equal(optional); + expect(result).to.have.property('defaultValue').to.be.null; + }); + + + it('should create one with a name, type, optional and defaultValue', () => { + const name = 'myType' + , type = 'custom' + , defaultValue = 'hello' + , optional = false + , result = new StringType({name: name, type: type, defaultValue: defaultValue, optional: optional}) + ; + + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('type').to.equal(type); + expect(result).to.have.property('optional').to.equal(optional); + expect(result).to.have.property('defaultValue').to.equal(defaultValue); + }); + + it('should create one with a min length value', () => { + const name = 'myType' + , type = 'custom' + , min = 1 + , result = new StringType({name: name, type: type, min: min}) + ; + + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('type').to.equal(type); + expect(result).to.have.property('minLength').to.equal(min); + expect(result).to.have.property('maxLength').to.equal(null); + }); + + it('should create one with a max length value', () => { + const name = 'myType' + , type = 'custom' + , max = 100 + , result = new StringType({name: name, type: type, max: max}) + ; + + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('type').to.equal(type); + expect(result).to.have.property('minLength').to.equal(null); + expect(result).to.have.property('maxLength').to.equal(max); + }); + + it('should create one with a min and max length value', () => { + const name = 'myType' + , type = 'custom' + , min = 10 + , max = 100 + , result = new StringType({name: name, type: type, min: min, max: max}) + ; + + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('type').to.equal(type); + expect(result).to.have.property('minLength').to.equal(min); + expect(result).to.have.property('maxLength').to.equal(max); + }); + + + describe('#getValue()', () => { + + function testFailure(type, val, errMessageContent) { + try { + type.getValue(val); + expect.fail('Should not get here'); + } catch(err) { + expect(err.message).to.contain(errMessageContent) + } + } + + describe('with type of no default and is optional', () => { + + const type = new StringType({name: 'custom', type: 'custom', optional: true}); + + it('should return a value for 0', () => { + expect(type.getValue(0)).to.equal('0'); + }); + + it('should return a value for 1', () => { + expect(type.getValue(1)).to.equal('1'); + }); + + it('should return a value for "hello"', () => { + expect(type.getValue('hello')).to.equal('hello'); + }); + + it('should return a value for null', () => { + expect(type.getValue(null)).to.equal(null); + }); + + it('should return a value for undefined', () => { + expect(type.getValue(undefined)).to.equal(null); + }); + + it('should return a value for object', () => { + const obj = {name: 'object', value: 'testing'}; + expect(type.getValue(obj)).to.equal(`${obj}`); + }); + }); + + + describe('with type with a default and is optional', () => { + + const DEFAULT_VALUE = 'my-default-value'; + + const type = new StringType({name: 'custom', type: 'custom', optional: true, defaultValue: DEFAULT_VALUE}); + + it('should return a value for 0', () => { + expect(type.getValue(0)).to.equal('0'); + }); + + it('should return a value for 1', () => { + expect(type.getValue(1)).to.equal('1'); + }); + + it('should return a value for "hello"', () => { + expect(type.getValue('hello')).to.equal('hello'); + }); + + it('should return a value for null', () => { + expect(type.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should return a value for undefined', () => { + expect(type.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + + it('should return a value for object', () => { + const obj = {name: 'object', value: 'testing'}; + expect(type.getValue(obj)).to.equal(`${obj}`); + }); + }); + + + describe('with type with no default and is not optional', () => { + + const type = new StringType({name: 'custom', type: 'custom', optional: false}); + + it('should return a value for 0', () => { + expect(type.getValue(0)).to.equal('0'); + }); + + it('should return a value for 1', () => { + expect(type.getValue(1)).to.equal('1'); + }); + + it('should return a value for "hello"', () => { + expect(type.getValue('hello')).to.equal('hello'); + }); + + it('should return a value for null', () => { + testFailure(type, null, 'is not optional'); + }); + + it('should return a value for undefined', () => { + testFailure(type, undefined, 'is not optional'); + }); + + it('should return a value for object', () => { + const obj = {name: 'object', value: 'testing'}; + expect(type.getValue(obj)).to.equal(`${obj}`); + }); + }); + + + describe('with type with no default and is not optional', () => { + + const DEFAULT_VALUE = 'my-default-value'; + + const type = new StringType({name: 'custom', type: 'custom', optional: false, defaultValue: DEFAULT_VALUE}); + + it('should return a value for 0', () => { + expect(type.getValue(0)).to.equal('0'); + }); + + it('should return a value for 1', () => { + expect(type.getValue(1)).to.equal('1'); + }); + + it('should return a value for "hello"', () => { + expect(type.getValue('hello')).to.equal('hello'); + }); + + it('should return a value for null', () => { + expect(type.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should return a value for undefined', () => { + expect(type.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + + it('should return a value for object', () => { + const obj = {name: 'object', value: 'testing'}; + expect(type.getValue(obj)).to.equal(`${obj}`); + }); + }); + + + describe('with type with min and max range values', () => { + + const MIN_LENGTH = 2 + , MAX_LENGTH = 10 + , STRING_LONGER_THAN_MAX = 'hello world this is a really long string' + , STRING_SHORTER_THAN_MIN = 'a' + , STRING_VALID = 'abc12' + , NUMBER_VALID = 1000 + ; + + describe('min range only', () => { + + const type = new StringType({name: 'custom', type: 'custom', optional: true, min: MIN_LENGTH}); + + it('should return a value for a valid number', () => { + expect(type.getValue(NUMBER_VALID)).to.equal(`${NUMBER_VALID}`); + }); + + it ('should fail on a string that is too short', () => { + testFailure(type, STRING_SHORTER_THAN_MIN, 'does not meet minimum length'); + }); + + it ('should work on a long string', () => { + expect(type.getValue(STRING_LONGER_THAN_MAX)).to.equal(STRING_LONGER_THAN_MAX); + }); + + it('should work on null', () => { + expect(type.getValue(null)).to.be.null; + }); + + it('should work on undefined', () => { + expect(type.getValue(undefined)).to.be.null; + }); + + it('should return a value for object', () => { + const obj = { a: 'b', b: 1 }; + expect(type.getValue(obj)).to.equal(`${obj}`); + }); + }); + + + describe('max range only', () => { + + const type = new StringType({name: 'custom', type: 'custom', optional: true, max: MAX_LENGTH}); + + it('should return a value for a valid number', () => { + expect(type.getValue(NUMBER_VALID)).to.equal(`${NUMBER_VALID}`); + }); + + it ('should fail on a string that is too long', () => { + testFailure(type, STRING_LONGER_THAN_MAX, 'maximum length requirement'); + }); + + it ('should work on a short string', () => { + expect(type.getValue(STRING_SHORTER_THAN_MIN)).to.equal(STRING_SHORTER_THAN_MIN); + }); + + it('should fail on null', () => { + expect(type.getValue(null)).to.equal(null); + }); + + it('should fail on undefined', () => { + expect(type.getValue(undefined)).to.equal(null); + }); + + it('should return a value for object', () => { + testFailure(type, { a: 'b', b: 1 }, 'maximum length requirement'); + }); + + }); + + + describe('min and max range', () => { + + const type = new StringType({name: 'custom', type: 'custom', optional: true, min: MIN_LENGTH, max: MAX_LENGTH}); + + + it('should return a value for a valid number', () => { + expect(type.getValue(NUMBER_VALID)).to.equal(`${NUMBER_VALID}`); + }); + + it('should return a value for a valid length string', () => { + expect(type.getValue(STRING_VALID)).to.equal(STRING_VALID); + }); + + it ('should fail on a string that is too short', () => { + testFailure(type, STRING_SHORTER_THAN_MIN, 'does not meet minimum length'); + }); + + it ('should fail ona string that is too long', () => { + testFailure(type, STRING_LONGER_THAN_MAX, 'does not meet maximum length'); + }); + + it('should work on null', () => { + expect(type.getValue(null)).to.be.null; + }); + + it('should work on undefined', () => { + expect(type.getValue(undefined)).to.be.null; + }); + + it('should fail for object', () => { + testFailure(type, { a: 'b', b: 1 }, 'maximum length requirement'); + }); + }); + + }); + }); +}); \ No newline at end of file diff --git a/lib/types/Type.js b/lib/types/Type.js new file mode 100644 index 0000000..6dceffa --- /dev/null +++ b/lib/types/Type.js @@ -0,0 +1,67 @@ +'use strict'; + +const ApiError = require('../ApiError'); + +module.exports = class Type { + + static isValueDefined(value) { + return value !== null && value !== undefined && value !== Number.NaN; + } + + constructor(config) { + if (!config.name) { + throw new ApiError('A name must be specified'); + } + this._name = config.name; + + if (!config.type) { + throw new ApiError('A type must be specified'); + } + this._type = config.type; + + // Optional configuration values + this._optional = Type.isValueDefined(config.optional) ? config.optional : true; + this._defaultValue = Type.isValueDefined(config.defaultValue) ? config.defaultValue : null; // null is considered unset in a Type + } + + get name() { + return this._name; + } + + get type() { + return this._type; + } + + get defaultValue() { + return this._defaultValue; + } + + get optional() { + return this._optional; + } + + hasDefaultValue() { + return Type.isValueDefined(this.defaultValue); + } + + getValue(val) { + if (Type.isValueDefined(val)) { + return this._convertToType(val); + } else { + if (this.hasDefaultValue()) { + return this._convertToType(this.defaultValue); + } else { + if (this.optional) { + // Value not defined (i.e. null or undefined or Number.NaN) + return null; + } else { + throw new ApiError(`No value provided and '${this.name}' is not optional`); + } + } + } + } + + _convertToType(val) { + return val; + } +}; \ No newline at end of file diff --git a/lib/types/Type.test.js b/lib/types/Type.test.js new file mode 100644 index 0000000..79489ba --- /dev/null +++ b/lib/types/Type.test.js @@ -0,0 +1,203 @@ +'use strict'; + +const expect = require('chai').expect + , Type = require('./Type') +; + +describe('Type', () => { + + it('should create one with a name and type', () => { + const name = 'myType' + , type = 'custom' + , result = new Type({name: name, type: type}) + ; + + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('type').to.equal(type); + expect(result).to.have.property('optional').to.be.true; + expect(result).to.have.property('defaultValue').to.be.null; + }); + + it('should create one with a name, type and defaultValue', () => { + const name = 'myType' + , type = 'custom' + , defaultValue = 'hello' + , result = new Type({name: name, type: type, defaultValue: defaultValue}) + ; + + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('type').to.equal(type); + expect(result).to.have.property('optional').to.be.true; + expect(result).to.have.property('defaultValue').to.equal(defaultValue); + }); + + it('should create one with a name, type and optional', () => { + const name = 'myType' + , type = 'custom' + , optional = false + , result = new Type({name: name, type: type, optional: optional}) + ; + + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('type').to.equal(type); + expect(result).to.have.property('optional').to.equal(optional); + expect(result).to.have.property('defaultValue').to.be.null; + }); + + + it('should create one with a name, type, optional and defaultValue', () => { + const name = 'myType' + , type = 'custom' + , defaultValue = 'hello' + , optional = false + , result = new Type({name: name, type: type, defaultValue: defaultValue, optional: optional}) + ; + + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('type').to.equal(type); + expect(result).to.have.property('optional').to.equal(optional); + expect(result).to.have.property('defaultValue').to.equal(defaultValue); + }); + + + describe('#getValue()', () => { + + describe('with type of no default and is optional', () => { + + const type = new Type({name: 'custom', type: 'custom', optional: true}); + + it('should return a value for 0', () => { + expect(type.getValue(0)).to.equal(0); + }); + + it('should return a value for 1', () => { + expect(type.getValue(1)).to.equal(1); + }); + + it('should return a value for "hello"', () => { + expect(type.getValue('hello')).to.equal('hello'); + }); + + it('should return a value for null', () => { + expect(type.getValue(null)).to.equal(null); + }); + + it('should return a value for undefined', () => { + expect(type.getValue(undefined)).to.equal(null); + }); + + it('should return a value for object', () => { + const obj = {name: 'object', value: 'testing'}; + expect(type.getValue(obj)).to.equal(obj); + }); + }); + + + describe('with type with a default and is optional', () => { + + const DEFAULT_VALUE = 'my-default-value'; + + const type = new Type({name: 'custom', type: 'custom', optional: true, defaultValue: DEFAULT_VALUE}); + + it('should return a value for 0', () => { + expect(type.getValue(0)).to.equal(0); + }); + + it('should return a value for 1', () => { + expect(type.getValue(1)).to.equal(1); + }); + + it('should return a value for "hello"', () => { + expect(type.getValue('hello')).to.equal('hello'); + }); + + it('should return a value for null', () => { + expect(type.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should return a value for undefined', () => { + expect(type.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + + it('should return a value for object', () => { + const obj = {name: 'object', value: 'testing'}; + expect(type.getValue(obj)).to.equal(obj); + }); + }); + + + describe('with type with no default and is not optional', () => { + + const type = new Type({name: 'custom', type: 'custom', optional: false}); + + it('should return a value for 0', () => { + expect(type.getValue(0)).to.equal(0); + }); + + it('should return a value for 1', () => { + expect(type.getValue(1)).to.equal(1); + }); + + it('should return a value for "hello"', () => { + expect(type.getValue('hello')).to.equal('hello'); + }); + + it('should return a value for null', () => { + try { + type.getValue(null); + expect.fail('Should not get here'); + } catch (err) { + expect(err.message).to.contain('is not optional'); + } + }); + + it('should return a value for undefined', () => { + try { + type.getValue(undefined); + expect.fail('Should not get here'); + } catch (err) { + expect(err.message).to.contain('is not optional'); + } + }); + + it('should return a value for object', () => { + const obj = {name: 'object', value: 'testing'}; + expect(type.getValue(obj)).to.equal(obj); + }); + }); + + + describe('with type with no default and is not optional', () => { + + const DEFAULT_VALUE = 'my-default-value'; + + const type = new Type({name: 'custom', type: 'custom', optional: false, defaultValue: DEFAULT_VALUE}); + + it('should return a value for 0', () => { + expect(type.getValue(0)).to.equal(0); + }); + + it('should return a value for 1', () => { + expect(type.getValue(1)).to.equal(1); + }); + + it('should return a value for "hello"', () => { + expect(type.getValue('hello')).to.equal('hello'); + }); + + it('should return a value for null', () => { + expect(type.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should return a value for undefined', () => { + expect(type.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + + it('should return a value for object', () => { + const obj = {name: 'object', value: 'testing'}; + expect(type.getValue(obj)).to.equal(obj); + }); + }); + + }); +}); \ No newline at end of file diff --git a/lib/types/UInt16Type.js b/lib/types/UInt16Type.js new file mode 100644 index 0000000..8b2726d --- /dev/null +++ b/lib/types/UInt16Type.js @@ -0,0 +1,14 @@ +'use strict'; + +const RangedNumberType = require('./RangedNumberType'); + +module.exports = class UInt16Type extends RangedNumberType { + + constructor(config) { + super(Object.assign({type: 'uint16'}, config), 0, 65535); + } + + _convertToType(val) { + return parseInt(val); + } +}; \ No newline at end of file diff --git a/lib/types/UInt16Type.test.js b/lib/types/UInt16Type.test.js new file mode 100644 index 0000000..7e8cb54 --- /dev/null +++ b/lib/types/UInt16Type.test.js @@ -0,0 +1,222 @@ +'use strict'; + +const expect = require('chai').expect + , UInt16Type = require('./UInt16Type') +; + +const MAX_VALUE = 65535 + , MIN_VALUE = 0 + , OVER_MAX_VALUE = MAX_VALUE + 1 + , UNDER_MIN_VALUE = MIN_VALUE - 1 +; + +describe('UInt16Type', () => { + + describe('constructor', () => { + + it('should create a type', () => { + const name = 'my_int_type' + , type = new UInt16Type({name: name}) + ; + + expect(type).to.have.property('name').to.equal(name); + expect(type).to.have.property('type').to.equal('uint16'); + }); + }); + + function testFailure(type, value, message) { + try { + type.getValue(value); + expect.fail('should not get here'); + } catch (err) { + expect(err.message).to.contain(message); + } + } + + describe('#getValue()', () => { + + describe('optional, no default', () => { + let intType = new UInt16Type({ + name: 'int8Type', + optional: true + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process 1.2 as 1', () => { + expect(intType.getValue(1.2)).to.equal(1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process -255', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under min value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(null); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(null); + }); + }); + }); + + + describe('not optional, no default', () => { + + let intType = new UInt16Type({ + name: 'int8Type', + optional: false + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process 1.2 as 1', () => { + expect(intType.getValue(1.2)).to.equal(1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process min value', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under min value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should fail on null', () => { + testFailure(intType, null, 'is not optional'); + }); + + it('should fail on undefined', () => { + testFailure(intType, null, 'is not optional'); + }); + }); + + + describe('not optional, with default', () => { + + const DEFAULT_VALUE = 10; + + let intType = new UInt16Type({ + name: 'int8Type', + optional: false, + defaultValue: DEFAULT_VALUE + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process 1.2 as 1', () => { + expect(intType.getValue(1.2)).to.equal(1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process min value', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under max value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + }); + + + describe('not optional, with default and different max/min values', () => { + + const DEFAULT_VALUE = 10; + + let intType = new UInt16Type({ + name: 'intType', + optional: false, + defaultValue: DEFAULT_VALUE, + min: 10, + max: 15 + }); + + + it('should fail on 0', () => { + testFailure(intType, 0, 'not within allowed limits'); + }); + + it('should process 10', () => { + expect(intType.getValue(10)).to.equal(10); + }); + + it('should process 15', () => { + expect(intType.getValue(15)).to.equal(15); + }); + + it('should fail on 16', () => { + testFailure(intType, 16, 'not within allowed limits'); + }); + + it('should process 12.1 as 12', () => { + expect(intType.getValue(12.1)).to.equal(12); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + }); +}); + + diff --git a/lib/types/UInt8Type.js b/lib/types/UInt8Type.js new file mode 100644 index 0000000..f7ddf43 --- /dev/null +++ b/lib/types/UInt8Type.js @@ -0,0 +1,14 @@ +'use strict'; + +const RangedNumberType = require('./RangedNumberType'); + +module.exports = class UInt8Type extends RangedNumberType { + + constructor(config) { + super(Object.assign({type: 'uint8'}, config), 0, 255); + } + + _convertToType(val) { + return parseInt(val); + } +}; \ No newline at end of file diff --git a/lib/types/UInt8Type.test.js b/lib/types/UInt8Type.test.js new file mode 100644 index 0000000..ba9ba91 --- /dev/null +++ b/lib/types/UInt8Type.test.js @@ -0,0 +1,222 @@ +'use strict'; + +const expect = require('chai').expect + , UInt8Type = require('./UInt8Type') +; + +const MAX_VALUE = 255 + , MIN_VALUE = 0 + , OVER_MAX_VALUE = MAX_VALUE + 1 + , UNDER_MIN_VALUE = MIN_VALUE - 1 +; + +describe('UInt8Type', () => { + + describe('constructor', () => { + + it('should create a type', () => { + const name = 'my_int_type' + , type = new UInt8Type({name: name}) + ; + + expect(type).to.have.property('name').to.equal(name); + expect(type).to.have.property('type').to.equal('uint8'); + }); + }); + + function testFailure(type, value, message) { + try { + type.getValue(value); + expect.fail('should not get here'); + } catch (err) { + expect(err.message).to.contain(message); + } + } + + describe('#getValue()', () => { + + describe('optional, no default', () => { + let intType = new UInt8Type({ + name: 'intType', + optional: true + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process 1.2 as 1', () => { + expect(intType.getValue(1.2)).to.equal(1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process -255', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under min value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(null); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(null); + }); + }); + }); + + + describe('not optional, no default', () => { + + let intType = new UInt8Type({ + name: 'int8Type', + optional: false + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process 1.2 as 1', () => { + expect(intType.getValue(1.2)).to.equal(1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process min value', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under min value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should fail on null', () => { + testFailure(intType, null, 'is not optional'); + }); + + it('should fail on undefined', () => { + testFailure(intType, null, 'is not optional'); + }); + }); + + + describe('not optional, with default', () => { + + const DEFAULT_VALUE = 10; + + let intType = new UInt8Type({ + name: 'int8Type', + optional: false, + defaultValue: DEFAULT_VALUE + }); + + + it('should process 0', () => { + expect(intType.getValue(0)).to.equal(0); + }); + + it('should process 1', () => { + expect(intType.getValue(1)).to.equal(1); + }); + + it('should process 1.2 as 1', () => { + expect(intType.getValue(1.2)).to.equal(1); + }); + + it('should process max value', () => { + expect(intType.getValue(MAX_VALUE)).to.equal(MAX_VALUE); + }); + + it('should process min value', () => { + expect(intType.getValue(MIN_VALUE)).to.equal(MIN_VALUE); + }); + + it('should fail on over max value', () => { + testFailure(intType, OVER_MAX_VALUE, 'not within allowed limits'); + }); + + it('should fail on under max value', () => { + testFailure(intType, UNDER_MIN_VALUE, 'not within allowed limits'); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + }); + + + describe('not optional, with default and different max/min values', () => { + + const DEFAULT_VALUE = 10; + + let intType = new UInt8Type({ + name: 'int8Type', + optional: false, + defaultValue: DEFAULT_VALUE, + min: 10, + max: 15 + }); + + + it('should fail on 0', () => { + testFailure(intType, 0, 'not within allowed limits'); + }); + + it('should process 10', () => { + expect(intType.getValue(10)).to.equal(10); + }); + + it('should process 15', () => { + expect(intType.getValue(15)).to.equal(15); + }); + + it('should fail on 16', () => { + testFailure(intType, 16, 'not within allowed limits'); + }); + + it('should process 12.9999 as 12', () => { + expect(intType.getValue(12.9999)).to.equal(12); + }); + + it('should process null', () => { + expect(intType.getValue(null)).to.equal(DEFAULT_VALUE); + }); + + it('should process undefined', () => { + expect(intType.getValue(undefined)).to.equal(DEFAULT_VALUE); + }); + }); +}); + + diff --git a/lib/parameters/index.js b/lib/types/index.js similarity index 90% rename from lib/parameters/index.js rename to lib/types/index.js index 8a41ff1..23e422a 100644 --- a/lib/parameters/index.js +++ b/lib/types/index.js @@ -9,6 +9,7 @@ const StringType = require('./StringType') , Int16Type = require('./Int16Type') , FloatType = require('./FloatType') , ListType = require('./ListType') + , ObjectType = require('./ObjectType') ; module.exports = { @@ -46,6 +47,10 @@ module.exports = { string: function(config) { return new StringType(config); + }, + + object: function(config) { + return new ObjectType(config); } }; diff --git a/lib/v3.js b/lib/v3.js index a3e8260..75956e9 100644 --- a/lib/v3.js +++ b/lib/v3.js @@ -2,7 +2,8 @@ const api = require('./api/index') , discovery = require('./api/discovery/index') - , model = require('./bridge-model/index') + , bridgeModel = require('./model') + , ApiError = require('./ApiError') ; // Definition of the v3 API for node-hue-api @@ -10,8 +11,69 @@ module.exports = { api: api, discovery: discovery, - lightStates: model.lightStates, - sensors: model.sensors, - Scene: model.Scene, - rules: model.rules, -}; \ No newline at end of file + //TODO think about removing this and deferring to the model + lightStates: bridgeModel.lightStates, + + model: bridgeModel, + + sensors: sensorsObject( + 'Sensors are now contained in the v3.model interface\n' + + 'You can use the v3.model.createCLIP[xxx]Sensor() where [xxx] is the type of Sensor to instantiate a sensor.' + ), + + Scene: classRemoved( + 'Scenes are no longer exposed as a class.\n' + + 'Create a Scene using v3.model.createLightScene() or v3.model.createGroupScene()' + ), + + rules: rulesObject( + 'Rules are now exposed under the v3.model interface.\n' + + 'Create a rule using v3.model.createRule()\n' + + 'Create a RuleCondition using v3.model.ruleConditions.[sensor|group]()\n' + + 'Create a RuleAction using v3.mode.ruleActions.[light|group|sensor|scene]\n' + ), +}; + +function sensorsObject(msg) { + return { + clip: { + GenericFlag: classRemoved(msg), + OpenClose: classRemoved(msg), + GenericStatus: classRemoved(msg), + Humidity: classRemoved(msg), + Lightlevel: classRemoved(msg), + Presence: classRemoved(msg), + Switch: classRemoved(msg), + Temperature: classRemoved(msg), + } + } +} + +function rulesObject(msg) { + return { + Rule: classRemoved(msg), + conditions: { + group: functionRemoved(msg), + sensor: functionRemoved(msg), + }, + actions: { + light: functionRemoved(msg), + group: functionRemoved(msg), + scene: functionRemoved(msg), + }, + }; +} + +function functionRemoved(msg) { + return function () { + throw new ApiError(msg); + }; +} + +function classRemoved(msg) { + return class RemovedClass { + constructor() { + throw new ApiError(msg); + } + }; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 25c8499..8dded52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -192,22 +192,44 @@ "dev": true }, "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dev": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -309,33 +331,28 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", + "integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==", "dev": true, "requires": { "es-to-primitive": "^1.2.0", "function-bind": "^1.1.1", "has": "^1.0.3", + "has-symbols": "^1.0.0", "is-callable": "^1.1.4", "is-regex": "^1.0.4", - "object-keys": "^1.0.12" + "object-inspect": "^1.6.0", + "object-keys": "^1.1.1", + "string.prototype.trimleft": "^2.1.0", + "string.prototype.trimright": "^2.1.0" } }, "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "requires": { "is-callable": "^1.1.4", @@ -488,21 +505,6 @@ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "dev": true }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, "external-editor": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", @@ -566,14 +568,6 @@ "dev": true, "requires": { "is-buffer": "~2.0.3" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", - "dev": true - } } }, "flat-cache": { @@ -636,15 +630,6 @@ "resolved": "https://registry.npmjs.org/get-ssl-certificate/-/get-ssl-certificate-2.3.3.tgz", "integrity": "sha512-aKYXS1S5+2IYw4W5+lKC/M+lvaNYPe0PhnQ144NWARcBg35H3ZvyVZ6y0LNGtiAxggFBHeO7LaVGO4bgHK4g1Q==" }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "glob": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", @@ -687,9 +672,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, "he": { @@ -783,12 +768,6 @@ } } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "is-buffer": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", @@ -827,12 +806,6 @@ "has": "^1.0.1" } }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, "is-symbol": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", @@ -876,15 +849,6 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -920,34 +884,6 @@ "chalk": "^2.0.1" } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "dependencies": { - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - } - } - }, "mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", @@ -979,9 +915,9 @@ } }, "mocha": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", - "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.2.tgz", + "integrity": "sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==", "dev": true, "requires": { "ansi-colors": "3.2.3", @@ -1004,9 +940,9 @@ "supports-color": "6.0.0", "which": "1.3.1", "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" + "yargs": "13.3.0", + "yargs-parser": "13.1.1", + "yargs-unparser": "1.6.0" }, "dependencies": { "debug": { @@ -1083,26 +1019,17 @@ }, "dependencies": { "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true } } }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", "dev": true }, "object-keys": { @@ -1165,45 +1092,16 @@ "wordwrap": "~1.0.0" } }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -1275,16 +1173,6 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -1358,12 +1246,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "semver": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz", - "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==", - "dev": true - }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -1418,6 +1300,26 @@ "strip-ansi": "^4.0.0" } }, + "string.prototype.trimleft": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", + "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", + "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -1427,12 +1329,6 @@ "ansi-regex": "^3.0.0" } }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -1570,48 +1466,40 @@ "dev": true }, "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "dev": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" }, "dependencies": { "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" } }, "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "^2.0.0" + "ansi-regex": "^4.1.0" } } } @@ -1638,22 +1526,21 @@ "dev": true }, "yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", "dev": true, "requires": { - "cliui": "^4.0.0", + "cliui": "^5.0.0", "find-up": "^3.0.0", "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" + "yargs-parser": "^13.1.1" }, "dependencies": { "ansi-regex": { @@ -1685,9 +1572,9 @@ } }, "yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -1695,58 +1582,14 @@ } }, "yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", "dev": true, "requires": { "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" - }, - "dependencies": { - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } + "lodash": "^4.17.15", + "yargs": "^13.3.0" } } } diff --git a/package.json b/package.json index a870887..3cb26c5 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,11 @@ ], "main": "index.js", "scripts": { - "test-bridge-model": "mocha --recursive \"lib/bridge-model/**/*.test.js\"", + "test-model": "mocha --recursive \"lib/model/**/*.test.js\"", "test-api": "mocha --recursive \"lib/api/**/*.test.js\"", - "test-old-api": "mocha --recursive \"test/**/*-tests.js\"" + "test-types": "mocha --recursive \"lib/types/*.test.js\"", + "test-old-api": "mocha --recursive \"test/**/*-tests.js\"", + "generate-typescript-definitions": "npx typescript index.js --allowJs --declaration --emitDeclarationOnly" }, "repository": { "type": "git", @@ -29,8 +31,7 @@ "devDependencies": { "chai": "~4.2", "eslint": "^5.16.0", - "mocha": "~6.1.4", - "semver": "~6.0" + "mocha": "~6.2.2" }, "engines": { "node": ">= 10.0.0" diff --git a/test/scene-tests.js b/test/scene-tests.js index da1399b..914f3f0 100644 --- a/test/scene-tests.js +++ b/test/scene-tests.js @@ -3,7 +3,7 @@ const expect = require('chai').expect , HueApi = require('..').HueApi , hueScene = require('..').scene - , Scene = require('../lib/bridge-model/Scene') + , Scene = require('../lib/model/scenes/Scene') , lightState = require('..').lightState , testValues = require('./support/testValues.js') ; diff --git a/test/schedule-tests.js b/test/schedule-tests.js index d656496..eea0d46 100644 --- a/test/schedule-tests.js +++ b/test/schedule-tests.js @@ -5,7 +5,7 @@ const expect = require('chai').expect , HueApi = hue.api , scheduledEventBuilder = hue.scheduledEvent , testValues = require('./support/testValues.js') - , AbsoluteTime = require('../lib/bridge-model/datetime/AbsoluteTime') + , AbsoluteTime = require('../lib/model/datetime/AbsoluteTime') ; describe('Hue API', function () { From beecd2609b5eade5c1a12d7dfd3bff210e227ebc Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 18 Nov 2019 20:57:51 +0000 Subject: [PATCH 02/35] Changes to support TypeScript definitions --- Changelog.md | 4 + README.md | 59 +- README_old.md | 2422 ----------------- index.js | 11 - lib/ApiError.js | 12 + lib/HueError.js | 15 + lib/api/Groups.test.js | 2 +- lib/api/http/RemoteApi.js | 2 +- lib/api/http/Transport.js | 2 +- lib/api/http/endpoints/configuration.js | 2 +- lib/api/http/endpoints/groups.js | 2 +- lib/api/http/endpoints/lights.js | 2 +- lib/api/http/endpoints/resourcelinks.js | 2 +- lib/api/http/endpoints/rules.js | 2 +- lib/api/http/endpoints/scenes.js | 2 +- lib/api/http/endpoints/schedules.js | 2 +- lib/api/http/endpoints/sensors.js | 2 +- lib/model/BridgeObject.js | 69 +- lib/model/Light.js | 3 +- lib/model/ResourceLink.js | 5 + lib/model/Schedule.js | 57 +- lib/model/rules/Rule.js | 1 + lib/model/rules/actions/RuleAction.js | 6 + lib/model/rules/conditions/operators/Ddx.js | 12 - lib/model/rules/conditions/operators/Dx.js | 12 - .../rules/conditions/operators/Equals.js | 13 - .../rules/conditions/operators/Equals.test.js | 46 - .../rules/conditions/operators/GreaterThan.js | 12 - lib/model/rules/conditions/operators/In.js | 12 - .../rules/conditions/operators/LessThan.js | 12 - lib/model/rules/conditions/operators/NotIn.js | 12 - .../rules/conditions/operators/NotStable.js | 12 - .../rules/conditions/operators/Stable.js | 12 - lib/model/rules/conditions/operators/index.js | 29 +- lib/model/scenes/Scene.js | 3 +- lib/model/sensors/CLIPSensor.js | 3 +- lib/model/sensors/Sensor.js | 7 +- lib/types/ListType.test.js | 4 +- lib/{api/http => }/util.js | 43 +- package.json | 5 +- test/getConfiguration-test.js | 242 -- test/getLightState-tests.js | 59 - test/groups-test.js | 459 ---- test/lights-tests.js | 72 - test/lightstate-tests.js | 274 -- test/locateBridge-tests.js | 46 - test/newLights-test.js | 50 - test/registeredUsers-test.js | 57 - test/registration-tests.js | 82 - test/rgb-tests.js | 97 - test/scene-object-test.js | 116 - test/scene-tests.js | 478 ---- test/schedule-tests.js | 296 -- test/scheduledEvent-tests.js | 236 -- test/searchForLights-test.js | 46 - test/sensor-tests.js | 24 - test/setLightName-tests.js | 46 - test/setLightState-tests.js | 279 -- 58 files changed, 194 insertions(+), 5700 deletions(-) delete mode 100644 README_old.md delete mode 100644 lib/model/rules/conditions/operators/Ddx.js delete mode 100644 lib/model/rules/conditions/operators/Dx.js delete mode 100644 lib/model/rules/conditions/operators/Equals.js delete mode 100644 lib/model/rules/conditions/operators/Equals.test.js delete mode 100644 lib/model/rules/conditions/operators/GreaterThan.js delete mode 100644 lib/model/rules/conditions/operators/In.js delete mode 100644 lib/model/rules/conditions/operators/LessThan.js delete mode 100644 lib/model/rules/conditions/operators/NotIn.js delete mode 100644 lib/model/rules/conditions/operators/NotStable.js delete mode 100644 lib/model/rules/conditions/operators/Stable.js rename lib/{api/http => }/util.js (73%) delete mode 100644 test/getConfiguration-test.js delete mode 100644 test/getLightState-tests.js delete mode 100644 test/groups-test.js delete mode 100644 test/lights-tests.js delete mode 100644 test/lightstate-tests.js delete mode 100644 test/locateBridge-tests.js delete mode 100644 test/newLights-test.js delete mode 100644 test/registeredUsers-test.js delete mode 100644 test/registration-tests.js delete mode 100644 test/rgb-tests.js delete mode 100644 test/scene-object-test.js delete mode 100644 test/scene-tests.js delete mode 100644 test/schedule-tests.js delete mode 100644 test/scheduledEvent-tests.js delete mode 100644 test/searchForLights-test.js delete mode 100644 test/sensor-tests.js delete mode 100644 test/setLightName-tests.js delete mode 100644 test/setLightState-tests.js diff --git a/Changelog.md b/Changelog.md index bb383bd..bdb3b07 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,8 @@ # Change Log ## 4.0.0 +- Deprecated v2 API and shim and modules removed from library + - `v3.api` removed the `create` function as it was deprecated, use `createRemote()` fro the remote API, `createLocal()` for the local API or `createInsecureLocal()` for non-hue bridges that do not support https connections @@ -49,6 +51,8 @@ The function call to instantiate the sensors also no longer take an object to set various attributes of the sensor, you need to call the approriate setter on the class now to se the attribute, e.g. `sensor.manufacturername = 'node-hue-api-sensor';` + +- TypeScript definitions added to the library ## 3.4.1 - Fixing issue with the lookup for the Hue motion sensor, issue #146 diff --git a/README.md b/README.md index 0539950..565e07c 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,8 @@ The library fully supports `local network` and `remote internet` access to the H ## Contents -- [Change Log](#change-log) - - [2.x](#2x) - - [3.x](#3x) - - [2.x Backwards Compatibility Shim](#2x-backwards-compatibility-shim) +- [Change Log](#change-log) - [Installation](#installation) -- [v2 API](docs/v2_api.md) - for backwards compatibility with 2.x versions of the library (deprecated) - [v3 API](#v3-api) - new API introduced in 3.x versions of the library - [Discovering Local Hue Bridges](docs/discovery.md) - [Remote API Support](docs/remoteApi.md) @@ -54,63 +50,14 @@ The library fully supports `local network` and `remote internet` access to the H For a list of changes, please refer to the change log; [Changes](Changelog.md) - -### 2.x -The library was originally written well before Promises and Async functions were part of the Javascript language (as well -as callbacks being the Node.js standard at the time). The `2.x` versions of the library heavily used `callbacks` and -`Q promises` for all the functions of the API. - -It was getting difficult to continue to support the new features of the bridge in this manner, and there was a lot of -unnecessary dependencies that were being dragged around, some of which were abandoned, e.g. `traits` and `Q`. - -You can continue to use the old 2.x release versions of this library, but the final release is `2.4.6` and no new -features will be added to this. - -There is a shim layer in the `3.x` releases that provides a drop in to match about 95% of the v2 API, see -[here](#2x-backwards-compatibility-shim) for more details. - - - -### 3.x -In version `3.x` the library was rewritten to adopt up to date Javascript language features (ES6) and remove a number of -now defunct dependencies. - -This has resulted in the removal of the older `callbacks` and Q `promises` from the code base and a brand new API that -includes a number of missing pieces of the the Philips Hue Bridge which were not available under the `2.x` versions, -e.g. Sensors support. - -The rewrite of the API using up to date language constructs has resulted in some significant speed increases from a -code execution stand point as well as introducing improved functionality around utility functions like setting RGB values -on lights (which are not explicitly supported in the Philips Hue REST API). - -#### 2.x Backwards Compatibility Shim -There is a backwards compatibility shim provided in the `3.x` releases to allow existing (`2.x`) users of -the library some time to transition existing code over to the updated API. - -This does have some minor breaking changes in some edge case features, but the majority of the core library -functions are shimmed to use the new API code behind a backwards compatible layer that provides a shimmed layer of -`callback`s and `Q` style promises as per the original API. - -Please consult the [backwards compatibility changes](docs/v3_backwards_compatibility.md) for details on changes that had -to be made that will change the v2 API. - -_Note: You are strongly encouraged to migrate off this, as it will be completely removed in the `4.x` releases, also all new -features will only be added to the `v3` going forward._ - -_Note: This shim will print out on `console.error` a number of warnings about the deprecated function calls that exist and -provide some details on what you can do to remove them._ - -This shim layer will be removed in the `4.x` release versions of the library. - - ## Installation -NodeJS using npm: +Node.js using npm: ``` $ npm install node-hue-api ``` -NodeJS using yarn: +Node.js using yarn: ``` $ yarn install node-hue-api ``` diff --git a/README_old.md b/README_old.md deleted file mode 100644 index 98ef2a4..0000000 --- a/README_old.md +++ /dev/null @@ -1,2422 +0,0 @@ -# Node Hue API - -[![npm](https://img.shields.io/npm/v/node-hue-api.svg)](http://npmjs.org/node-hue-api) - -An API library for Node.js that interacts with the Philips Hue Bridge to control Philips Hue Light Bulbs and -Philips Living Color Lamps. - -This library abstracts away the actual Philips Hue Bridge REST API and provides all of the features of the Phillips API and -a number of useful functions to control the lights and bridge remotely. - -The library supports both function ``callbacks`` and Q ``promises`` for all the functions of the API. -So for each function in the API, if a callback is provided, then a callback will be used to return any results -or notification of success, in a true Node.js fashion. If the callback is omitted then a promise will be returned for -use in chaining or in most cases simpler handling of the results. - -When using Q ``promises``, it is necessary to call ``done()`` on any promises that are returned, otherwise errors can be -swallowed silently. - -## Table of Contents -- [Change Log](#change-log) -- [Work In Progress](#work-in-progress) -- [Breaking Changes in 2.0.x](#breaking-changes-in-20x) -- [Philips Hue Resources](#philips-hue-resources) -- [Installation](#installation) -- [Examples](#examples) -- [Finding the Lights Attached to the Bridge](#finding-the-lights-attached-to-the-bridge) -- [Interacting with a Hue Light or Living Color Lamp](#interacting-with-a-hue-light-or-living-color-lamp) -- [Using LightState to Build States](#using-lightstate-to-build-states) -- [Turning a Light On/Off using LightState](#turning-a-light-onoff-using-lightstate) -- [Setting Light States using custom JSON Object](#setting-light-states-using-custom-json-object) -- [Getting the Current Status/State for a Light](#getting-the-current-statusstate-for-a-light) -- [Working with Groups](#working-with-groups) -- [Working with Schedules](#working-with-schedules) -- [Working with scenes](#working-with-scenes) -- [Advanced Options](#advanced-options) -- [License](#license) - -## Change Log -For a list of changes, please refer to the change log; -[Changes](Changelog.md) - - -## Work In Progress -There are still some missing pieces to the library which includes; -* Rules API -* Improved handling of settings/commands for Schedules - - -## Breaking Changes in 2.0.x -In version `2.0.x` breaking changes were made to the API so that the library could better integrate with the `1.11` -version of Hue Bridge's software. This involved a number of new API endpoints and changes to types of activities that -had been wrapped by this library, which the bridge endpoints now offer. - -The majority of the changes in the API were with respect to the scenes, as these were stored inside the bridge, instead -of in the lights. As such the scene APIs have been modified to expose this new functionality. - -### Node.js 0.10.x and Early 0.12.x Issues -In version `2.0.x` the HTTP library was swapped out with a new one `axios`. This library requires that there is a -`promise` present in Node.js. -Versions 0.10.x and some early versions of 0.12.x will require a shim to work with the node-hue-api library now. - -If you get an error stack trace like the following, you will need the promise shim; - -``` -node_modules/axios/lib/axios.js:45 -var promise = Promise.resolve(config); -^ -ReferenceError: Promise is not defined -``` - -The shim dependency is available at [https://github.com/stefanpenner/es6-promise] and can be installed via npm using -``npm install es6-promise`` - -Once you have the dependency, you will need to add the following into your code to apply the polyfill: - -```js -require('es6-promise').polyfill(); -``` - - -## Philips Hue Resources - -There are a number of resources where users have detailed documentation on the Philips Hue Bridge; - - The Official Phillips Hue Documentation - - Unofficial Hue Documentation: - - Hue Hackers Mailing List: - - StackOverflow: - - -## Installation - -NodeJS application using npm: -``` -$ npm install node-hue-api -``` - -## Examples - -### Locating a Philips Hue Bridge -There are two functions available to find the Phillips Hue Bridges on the network ``nupnpSearch()`` and ``upnpSearch()``. -Both of these methods are useful if you do not know the IP Address of the bridge already. - -The official Hue documentation recommends an approach to finding bridges by using both UPnP and N-UPnP in parallel -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), - in which case you can fall back to the slower -``upnpSearch()`` function. - -This function is considerably faster to resolve the bridges < 500ms compared to 5 seconds to perform a full search on my -own network. - -```js -var hue = require("node-hue-api"); - -var displayBridges = function(bridge) { - console.log("Hue Bridges Found: " + JSON.stringify(bridge)); -}; - -// -------------------------- -// Using a promise -hue.nupnpSearch().then(displayBridges).done(); - -// -------------------------- -// Using a callback -hue.nupnpSearch(function(err, result) { - if (err) throw err; - displayBridges(result); -}); -``` - -The results from this call will be of the form; -``` -Hue Bridges Found: [{"id":"001788fffe096103","ipaddress":"192.168.2.129","name":"Philips Hue","mac":"00:00:00:00:00"}] -``` - - -#### upnpSearch or searchForBridges() -This API function utilizes a network scan for the SSDP responses of devices on a network. It is the only method that does not -support callbacks, and is only in the API as a fallback since Phillips provided a quicker discovery method once the API was -officially released. - -```js -var hue = require("node-hue-api"), - timeout = 2000; // 2 seconds - -var displayBridges = function(bridge) { - console.log("Hue Bridges Found: " + JSON.stringify(bridge)); -}; - -hue.upnpSearch(timeout).then(displayBridges).done(); -``` -A timeout can be provided to the function to increase/decrease the amount of time that it waits for responses from the -search request, by default this is set to 5 seconds (the above example sets this to 2 seconds). - -The results from this function call will be of the form; -``` -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 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 -now do this via the API too if you already have an existing user account on the Bridge). - -This library offer two functions to register new devices/users with the Hue Bridge. These are detailed below. - - -### Bridge Configuration -You can obtain a summary of the configuration of the Bridge using the ``config()`` or ``getConfig()`` functions; - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.config().then(displayResult).done(); -// using getConfig() alias -api.getConfig().then(displayResult).done(); - -// -------------------------- -// Using a callback -api.config(function(err, config) { - if (err) throw err; - displayResult(config); -}); -// using getConfig() alias -api.getConfig(function(err, config) { - if (err) throw err; - displayResult(config); -}); -``` - -This will provide results detailing the configuration of the bridge (IP Address, Name, Link Button Status, Defined Users, etc...); -``` -{ - "name": "Philips hue", - "zigbeechannel": 11, - "bridgeid": "xxxxxxx", - "mac": "00:xx:88:xx:f3:xx", - "dhcp": false, - "ipaddress": "192.168.2.245", - "netmask": "255.255.255.0", - "gateway": "192.168.2.1", - "proxyaddress": "none", - "proxyport": 0, - "UTC": "2017-01-04T20:01:21", - "localtime": "2017-01-04T20:01:21", - "timezone": "Europe/London", - "modelid": "BSB002", - "datastoreversion": "59", - "swversion": "01036659", - "apiversion": "1.16.0", - "swupdate": { - "updatestate": 0, - "checkforupdate": false, - "devicetypes": { - "bridge": false, - "lights": [], - "sensors": [] - }, - "url": "", - "text": "", - "notify": false - }, - "linkbutton": false, - "portalservices": true, - "portalconnection": "connected", - "portalstate": { - "signedon": true, - "incoming": true, - "outgoing": true, - "communication": "disconnected" - }, - "factorynew": false, - "replacesbridgeid": "xxxxxxxxxxx", - "backup": { - "status": "idle", - "errorcode": 0 - }, - "whitelist": { - ... - } -} -``` - -If you invoke the ``config()`` or ``connect()`` functions with an invalid user account (i.e. one that is not valid) then -results of the name and software version will be returned from the bridge with no other information; -``` - - "apiversion": "1.16.0" - "bridgeid": "xxxxxxxxxxx" - "datastoreversion": "59" - "factorynew": false - "mac": "00:xx:88:xx:f3:xx" - "modelid": "BSB002" - "name": "Philips hue" - "replacesbridgeid": "xxxxxxxxxxx" - "swversion": "01036659" -} -``` -For this reason, if you want to validate that the user account used to connect to the bridge is correct, you will have to -look for a field that is not present in the above result, like the ``zigbeechannel``, ``ipaddress`` or ``linkbutton`` would be good -properties to check. - -//TODO Need to document setting config value and timezones - -### Timezones -To obtain the valid timezones for the bridge, you can use the ``getTimezones()`` or ``timezones()`` function. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.getTimezones().then(displayResult).done(); -// or using 'timezones' alias -api.timezones().then(displayResult).done(); - -// -------------------------- -// Using a callback -api.getTimezones(function(err, config) { - if (err) throw err; - displayResult(config); -}); -// or using 'timezones' alias -api.timezones(function(err, config) { - if (err) throw err; - displayResult(config); -}); -``` - -//TODO setting a time zone - - -### 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` and `version` 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 host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.getVersion().then(displayResult).done(); -// or using 'version' alias -api.version().then(displayResult).done(); - -// -------------------------- -// Using a callback -api.getVersion(function(err, config) { - if (err) throw err; - displayResult(config); -}); -// or using 'version' alias -api.version(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. - -```js -var HueApi = require("node-hue-api").HueApi; - -var host = "192.168.2.129", - userDescription = "device description goes here"; - -var displayUserResult = function(result) { - console.log("Created user: " + JSON.stringify(result)); -}; - -var displayError = function(err) { - console.log(err); -}; - -var hue = new HueApi(); - -// -------------------------- -// Using a promise -hue.registerUser(host, userDescription) - .then(displayUserResult) - .fail(displayError) - .done(); - -// -------------------------- -// Using a callback (with default description and auto generated username) -hue.createUser(host, function(err, user) { - if (err) throw err; - displayUserResult(user); -}); -``` - -The description for the user account is optional, if you do nto provide one, then the default of "Node.js API" will be set. - -There is a convenience method, if you have a existing user account when you register a new user, that will programmatically -press the link button for you. See the details for the function ``pressLinkButton()`` for more details. - - -#### Registration Output/Error -When registering a new user you will get the username created, or an error that will likely be due to not pressing the -link button on the Bridge. - -If the link button was NOT pressed on the bridge, then you will get an ``ApiError`` thrown, which will be captured by the displayError function in the above examples. -``` -Api Error: link button not pressed -``` - -If the link button was pressed you should get a response that will provide you with a hash to use as the username for connecting with the Hue Bridge, e.g. -``` -033a6feb77750dc770ec4a4487a9e8db -``` - - -### Bridge Description -You can obtain the UPnP/Discovery description details of the Bridge using the function ``description()`` or -``getDescription()``. The result of this will be the contents of the `/description.xml` converted into a JSON object. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.description().then(displayResult).done(); -// using alias getDescription() -api.getDescription().then(displayResult).done(); - -// -------------------------- -// Using a callback -api.description(function(err, config) { - if (err) throw err; - displayResult(config); -}); -// using alias getDescription() -api.getDescription(function(err, config) { - if (err) throw err; - displayResult(config); -}); -``` - - -### Validating a Connection to a Philips Hue Bridge -To connect to a Philips Hue Bridge and obtain some basic details about it you can use the any -of the following functions; -* ``config()`` or ``getConfig()`` -* ``version()`` or ``getVersion()`` - -The details of the results of these functions are provided above. - - -### Obtaining the Complete State of the Bridge -If you have a valid user account in the Bridge, then you can obtain the complete status of the bridge using -``fullState()`` or ``getFullState()``. -This function is computationally expensive on the bridge and should not be invoked frequently. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.getFullState().then(displayResult).done(); -// or alias fullState() -api.fullState().then(displayResult).done(); - -// -------------------------- -// Using a callback -api.getFullState(function(err, config) { - if (err) throw err; - displayResult(config); -}); -// or alias fullState() -api.fullState(function(err, config) { - if (err) throw err; - displayResult(config); -}); -``` - -This will produce a JSON response similar to the following (large parts have been removed from the result below); -``` -{ - "lights": { - "5": { - "state": { - "on": false, - "bri": 0, - "hue": 6144, - "sat": 254, - "xy": [ - 0.6376, - 0.3563 - ], - "alert": "none", - "effect": "none", - "colormode": "hs", - "reachable": true - }, - "type": "Color light", - "name": "Living Color TV", - "modelid": "LLC007", - "swversion": "4.6.0.8274", - "pointsymbol": { - "1": "none", - "2": "none", - "3": "none", - "4": "none", - "5": "none", - "6": "none", - "7": "none", - "8": "none" - } - } - }, - "groups": { - "1": { - "action": { - "on": false, - "bri": 63, - "hue": 65527, - "sat": 253, - "xy": [ - 0.6736, - 0.3221 - ], - "ct": 500, - "effect": "none", - "colormode": "ct" - }, - "lights": [ - "1", - "2", - "3" - ], - "name": "NodejsApiTest" - } - }, - "config": { - ... - "whitelist": { - "51780342fd7746f2fb4e65c30b91d7": { - "last use date": "2013-05-29T20:29:51", - "create date": "2013-05-29T20:29:51", - "name": "Node.js API" - }, - "08a902b95915cdd9b75547cb50892dc4": { - "last use date": "1987-01-06T22:53:37", - "create date": "2013-04-02T13:39:18", - "name": "Node Hue Api Tests User" - } - }, - "swversion": "01005825" - ... - }, - "schedules": { - "1": { - "name": "Updated Name", - "description": "Like anyone really needs a wake up on Xmas day...", - "command": { - "address": "/api/08a902b95915cdd9b75547cb50892dc4/lights/5/state", - "body": { - "on": true - }, - "method": "PUT" - }, - "time": "2014-01-01T07:00:30", - "created": "1970-01-01T00:00:00" - } - }, - "scenes": {} -``` - -### Obtaining Registered Users/Devices -To obtain the details for all the registered users/devices for a Hue Bridge you can use the ``registeredUsers()`` function. -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129"; -var username = "08a902b95915cdd9b75547cb50892dc4"; -var api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.registeredUsers().then(displayResult).done(); - -// -------------------------- -// Using a callback -api.registeredUsers(function(err, config) { - if (err) throw err; - displayResult(config); -}); -``` -This will produce a JSON response that has a root key of "devices" that has an array of registered devices/users for the Bridge. An example of the result is shown below -``` -{ - "devices": [ - { - "name": "Node API", - "username": "083b2f780c78555d532b78544f135798", - "created": "2013-01-02T19:17:02", - "accessed": "2012-12-24T20:18:55" - }, - { - "name": "iPad", - "username": "279c26146e3318997d69a8a66330b5f5", - "created": "2012-12-24T14:05:25", - "accessed": "2013-01-04T21:37:29" - }, - { - "name": "iPhone", - "username": "fcb0a47cd664f0cbaa34d36def54577d", - "created": "2012-12-24T17:13:54", - "accessed": "2013-01-03T20:50:40" - } - ] -} -```` - -### Deleting a User/Device -To delete a user or device from the Bridge, you will need an existing user account to authenticate as, and then you can call -``deleteUser()`` or ``unregisterUser()`` to remove a user from the Bridge Whitelist; - -```js -var HueApi = require("node-hue-api").HueApi; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4"; - -var displayUserResult = function(result) { - console.log("Deleted user: " + JSON.stringify(result)); -}; - -var displayError = function(err) { - console.log(err); -}; - -var hue = new HueApi(host, username); - -// -------------------------- -// Using a promise -hue.deleteUser("2b997aae306f15a734d8d1c2315d47cb") - .then(displayUserResult) - .fail(displayError) - .done(); - -// -------------------------- -// Using a callback -hue.unregisterUser("1ab7d44219e64c373b4b915e34494443", function(err, user) { - if (err) throw err; - displayUserResult(user); -}); -``` -Which will result in a ``true`` result if the user was removed, or an error if any other result occurs (i.e. the user does not exist) as shown below; -``` -{ - message: 'resource, /config/whitelist/2b997aae306f15a734d8d1c2315d47cb, not available', - type: 3, - address: '/config/whitelist/2b997aae306f15a734d8d1c2315d47cb' -} -``` - - -## Finding the Lights Attached to the Bridge -To find all the lights that are registered with the Hue Bridge, so that you might be able to interact with them, you can use the ``lights()`` function. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.lights() - .then(displayResult) - .done(); - -// -------------------------- -// Using a callback -api.lights(function(err, lights) { - if (err) throw err; - displayResult(lights); -}); -``` - -This will output a JSON object that will provide details of the lights that the Hue Bridge knows about; -``` -{ - "lights": [ - { - "id": "1", - "name": "Lounge Living Color", - "type": "Extended color light", - "modelid": "LCT001", - "manufacturername": "Phillips", - "uniqueid": "00:17:88:01:xx:xx:xx:xx-xx", - "swversion": "66013452", - "state": { - "on": true, - "bri": 202, - "hue": 11315, - "sat": 237, - "effect": "none", - "xy": [ - 0.5534, - 0.4239 - ], - "alert": "none", - "colormode": "xy", - "reachable": true - } - }, - { - "id": "2", - "name": "Right Bedside", - "type": "Extended color light", - "modelid": "LCT001", - "manufacturername": "Phillips", - "uniqueid": "00:17:88:01:xx:xx:xx:xx-xx", - "swversion": "66013452", - "state": { - "on": true, - "bri": 202, - "hue": 11315, - "sat": 237, - "effect": "none", - "xy": [ - 0.5534, - 0.4239 - ], - "alert": "none", - "colormode": "xy", - "reachable": true - } - }, - { - "id": "3", - "name": "Left Bedside", - "type": "Extended color light", - "modelid": "LCT001", - "manufacturername": "Phillips", - "uniqueid": "00:17:88:01:xx:xx:xx:xx-xx", - "swversion": "66013452", - "state": { - "on": true, - "bri": 202, - "hue": 11315, - "sat": 237, - "effect": "none", - "xy": [ - 0.5534, - 0.4239 - ], - "alert": "none", - "colormode": "xy", - "reachable": true - } - } - ] -} -``` -The `id` values are what you will need to use to interact with the light directly and set the states on it (like on/off, color, etc...). - -## Interacting with a Hue Light or Living Color Lamp -The library provides a function, __setLightState()__, that allows you to set the various states on a light connected to the Hue Bridge. -You can either provide a JSON object that contains the values to set the various state values, or you can use the provided __lightState__ object in the library to build the state object ot pass to the function. See below for examples. - -## Using LightState to Build States -The __lightState__ object provides a fluent way to build up a simple or complex light states that you can pass to a light. - -The majority of the various states that you can set on a Hue Light or Living Color lamp are available from this object. - -### LightState Options -The __lightState__ object provides the following methods that can be used to build various states (all of which can be combined); - -The LightState object, provides functions with the same name of the underlying Hue Bridge API properties for lights, -which take values documented in the official Phillips Hue Lights API: - -| Function | Details | -|:-------------|:---------------------| -| `on(value)` | Sets the `on` state, where the value is `true` or `false`| -| `bri(value)` | Sets the brightness, where value from 0 to 255 | -| `hue(value)` | Sets the hue, where value from 0 to 65535 | -| `sat(value)` | Sets the saturation value from 0 to 255 | -| `xy(x, y)` | Sets the xy value where x and y is from 0 to 1 in the Philips Color co-ordinate system | -| `ct(colorTemperature)` | Set the color temperature to a value between 153 and 500 | -| `alert(value)` | Sets the alert state to value `none`, `select` or `lselect`. If no parameter is passed will default to `none`. | -| `effect(effectName)` | Sets the effect on the light(s) where `effectName` is either `none` or `colorloop`. | -| `transitiontime(int)` | Sets a transition time to a multiple of 100 milliseconds, e.g. 4 means 400ms | -| `bri_inc(value)`| Increments/Decrements the brightness by the value specified. Accepts values -254 to 254. | -| `sat_inc(value)`| Increments/Decrements the saturation by the value specified. Accepts values -254 to 254. | -| `hue_inc(value)`| Increments/Decrements the hue by the value specified. Accepts values -65534 to 65534. | -| `ct_inc(value)` | Increments/Decrements the color temperature by the value specified. Accepts values -65534 to 65534. | -| `xy_inc(value)` | Increments/Decrements the xy co-ordinate by the value specified. Accepts values -0.5 to 0.5. | - -There are also a number of convenience functions to provide extra functionality or a more natural language for building -up a desired Light State: - -| Function | Details | -|:---------|:--------| -| `turnOn()` | Turn the lights on | -| `turnOff()` |Turn the lights off | -| `off()` |Turn the lights off | -| `brightness(percentage)` |Set the brightness from 0% to 100% (0% is not off)| -| `incrementBrightness(value)` |Alias for the `bri_inc()` function above | -| `colorTemperature(ct)` |Alias for the `ct()` function above| -| `colourTemperature(ct)` |Alias for the `ct()` function above| -| `colorTemp(ct)`| Alias for the `ct()` function above| -| `colourTemp(ct)` |Alias for the `ct()` function above| -| `incrementColorTemp(value)` |Alias for the `ct_inc()` function above | -| `incrementColorTemperature(value)` |Alias for the `ct_inc()` function above | -| `incrementColourTemp(value)` |Alias for the `ct_inc()` function above | -| `incrementColourTemperature(value)` |Alias for the `ct_inc()` function above | -| `saturation(percentage)`| Set the saturation as a percentage value between 0 and 100| -| `incrementSaturation(value)` |Alias for the `sat_inc()` function above | -| `incrementXY(value)` |Alias for the `xy_inc()` function above | -| `incrementHue(value)` |Alias for the `hue_inc()` function above | -| `shortAlert()` |Flashes the light(s) once| -| `alertShort()` |Flashes the light(s) once| -| `longAlert()` |Flashes the light(s) 10 times| -| `alertLong()` |Flashes the light(s) 10 times| -| `transitionTime(int)` |Sets a transition time to a multiple of 100 milliseconds, e.g. 4 means 400ms | -| `transition(milliseconds)` |Specify a specific transition time| -| `transitiontime_milliseconds(milliseconds)` | Sets a transition time in milliseconds (will be rounded to the closest 100ms | -| `transitionSlow()` |A slow transition of 800ms| -| `transitionFast()` | A fast transition of 200ms| -| `transitionInstant()` |A transition of 0ms| -| `transitionDefault()` |A transition time of the bridge default (400ms)| -| `white(colorTemp, briPercent)` | where colorTemp is a value between 154 (cool) and 500 (warm) and briPercent is 0 to 100| -| `hsl(hue, sat, luminosity)` | Where hue is a value from 0 to 359, sat is a saturation percent value from 0 to 100, and luminosity is from 0 to 100| -| `hsb(hue, sat, brightness)` | Where hue is a value from 0 to 359, sat is a saturation percent value from 0 to 100, and brightness is from 0 to 100| -| `rgb(r, g, b)` | Sets an RGB value from integers 0-255| -| `rgb([r, g, b])` | Sets an RGB value from an array of integer values 0-255| -| `colorLoop()` | Starts a color loop effect (rotates through all available hues at the current saturation level)| -| `colourLoop()` | Starts a color loop effect (rotates through all available hues at the current saturation level)| -| `effectColorLoop()` | Starts a color loop effect (rotates through all available hues at the current saturation level)| -| `effectColourLoop()` | Starts a color loop effect (rotates through all available hues at the current saturation level)| -| `copy()`| Allows you to create an independent copy of the LightState| -| `reset()` | Will completely reset/remove all provided values| - - -### Creating Complex States -The LightState object provides a simple way to build up JSON object to set multiple values on a Hue Light. - -To turn on a light and set it to a warm white color; -```js -var hue = require("node-hue-api"), - HueApi = hue.HueApi, - lightState = hue.lightState; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - state; - -// Set light state to 'on' with warm white value of 500 and brightness set to 100% -state = lightState.create().on().white(500, 100); - -// -------------------------- -// Using a promise -api.setLightState(5, state) - .then(displayResult) - .done(); - -// -------------------------- -// Using a callback -api.setLightState(5, state, function(err, lights) { - if (err) throw err; - displayResult(lights); -}); -``` - -The __lightState__ object will ensure that the values passed into the various state functions are correctly bounded to avoid -errors when setting them. For example the color temperature value (which determines the white value) must be between 154 and 500. If you pass in a value outside of this range then the lightState function call will set it to the closest valid value. - -Currently the __lightState__ object will combine together all the various state values that get set by the various function calls. This means that if you do create a combination of conflicting values, like __on__ and __off__ the last one set will be the actual value provided in the corresponding JSON object; - -```js -// This will result in a JSON object for the state that sets the brightness to 100% but turn the light "off" -state = lightState.create().on().brightness(100).off(); -``` - -When using __lightState__ it is currently recommended to create a new state object each time you want to build a new state, otherwise you will get a combination of all the previous settings as well as the new values. - - -## Turning a Light On/Off using LightState - -```js -var hue = require("node-hue-api"), - HueApi = hue.HueApi, - lightState = hue.lightState; - -var displayResult = function(result) { - console.log(result); -}; - -var displayError = function(err) { - console.error(err); -}; - -var host = "192.168.2.129", - username = "033a6feb77750dc770ec4a4487a9e8db", - api = new HueApi(host, username), - state = lightState.create(); - -// -------------------------- -// Using a promise - -// Set the lamp with id '2' to on -api.setLightState(2, state.on()) - .then(displayResult) - .fail(displayError) - .done(); - -// Now turn off the lamp -api.setLightState(2, state.off()) - .then(displayResult) - .fail(displayError) - .done(); - -// -------------------------- -// Using a callback -// Set the lamp with id '2' to on -api.setLightState(2, state.on(), function(err, result) { - if (err) throw err; - displayResult(result); -}); - -// Now turn off the lamp -api.setLightState(2, state.off(), function(err, result) { - if (err) throw err; - displayResult(result); -}); -``` - -If the function call is successful, then you should get a response of ``true``. If the call fails then an ``ApiError`` -will be generated with the failure details. - - -## Setting Light States using custom JSON Object -You can pass in your own JSON object that contain the setting(s) that you wish to pass to the light via the bridge. If -you do this, then a LightState object will be created from the passed in object, so that it can be properly validated -and only valid values are passed to the bridge. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(result); -}; - -var displayError = function(err) { - console.error(err); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); -api.setLightState(2, {"on": true}) // provide a value of false to turn off - .then(displayResult) - .fail(displayError) - .done(); -``` - -If the function call is successful, then you should get a response of true. If the call fails then an ``ApiError`` will be generated with the failure details. - - -## Getting the Current Status/State for a Light -To obtain the current state of a light from the Hue Bridge you can use the `lightStatus()` or `getLightStatus()` function; - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayStatus = function(status) { - console.log(JSON.stringify(status, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// Obtain the Status of Light '5' - -// -------------------------- -// Using a promise -api.lightStatus(5) - .then(displayStatus) - .done(); - -// -------------------------- -// Using a callback -api.lightStatus(5, function(err, result) { - if (err) throw err; - displayStatus(result); -}); -``` - -This will produce a JSON object detailing the status of the lamp; -``` -{ - "state": { - "on": true, - "bri": 254, - "hue": 34515, - "sat": 236, - "xy": [ - 0.3138, - 0.3239 - ], - "ct": 153, - "alert": "none", - "effect": "none", - "colormode": "ct", - "reachable": true - }, - "type": "Extended color light", - "name": "Left Bedside", - "modelid": "LCT001", - "swversion": "65003148", - "pointsymbol": { - "1": "none", - "2": "none", - "3": "none", - "4": "none", - "5": "none", - "6": "none", - "7": "none", - "8": "none" - } -} -``` - -### Obtaining the RGB Value for a Light -There is a function to provide the complete light status along with an approximated RGB value (which is a rough conversion -of the xy state of a light into an RGB value). This is not a perfect conversion but does get close to the current color -of the lamp. - -To obtain the status with the RGB approximation of a lamp use `lightStatusWithRGB()` or `getLightStatusWithRGB()`. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(result); -}; - -var displayError = function(err) { - console.error(err); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); -api.lightStatusWithRGB(1) - .then(displayResult) - .fail(displayError) - .done(); -``` - -```js -{ - state: { - rgb: [ 255, 249, 221 ], - on: true, - bri: 254, - hue: 38265, - sat: 92, - effect: 'none', - xy: [ 0.3362, 0.3604 ], - alert: 'none', - colormode: 'xy', - reachable: true - }, - type: 'Color light', - name: 'Living Color Floor', - modelid: 'LLC007', - manufacturername: 'Philips', - uniqueid: '00:17:88:01:00:1b:21:a3-0b', - swversion: '4.6.0.8274' -} -``` - - -### Searching for New Lights -When you have added new lights to the system, you need to invoke a search to discover these new lights to allow the Bridge -to interact with them. The ``searchForNewLights()`` function will invoke a search for any new lights to be added to the -system. - -When you invoke a scan for any new lights in the system, the previous search results are destroyed. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.searchForNewLights() - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.searchForNewLights(function(err, result) { - if (err) throw err; - displayResults(result); -}); -``` -The result from this call should be ``true`` if a search was successfully triggered. It can take some time for the search -to complete. - -### Obtaining Newly Discovered Lights -Once a search has been completed, then the newly discovered lights can be obtained using the ``newLights()`` call. -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.newLights() - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.newLights(function(err, result) { - if (err) throw err; - displayResults(result); -}); -``` -The results from this call should be the new lights that were found during the previous search, and a ``lastscan`` value -that will be the date that the last scan was performed, which could be ``none`` if a search has never been performed. -``` -{ - "lastscan": "2013-06-15T14:45:23" -} -``` - -### Naming Lights -It is possible to name a light using the ``setLightName()`` function; -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.setLightName(5, "A new Name") - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.setLightName(5, "Living Color TV", function(err, result) { - if (err) throw err; - displayResults(result); -}); -``` -If the call is successful, then ``true`` will be returned by the function call, otherwise a ``ApiError`` will result. - - -## Working with 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; - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// Obtain all the defined groups in the Bridge - -// -------------------------- -// Using a promise -api.groups() - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.groups(function(err, result) { - if (err) throw err; - displayResults(result); -}); -``` - -This will produce an array of values detailing the id and names of the groups; -``` -[ - { - "id": "0", - "name": "Lightset 0", - "type": "LightGroup" - }, - { - "id": "1", - "name": "VRC 1", - "lights": [ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8" - ], - "type": "LightGroup", - "action": { - "on": false, - "bri": 162, - "hue": 13088, - "sat": 213, - "effect": "none", - "xy": [ - 0.5134, - 0.4149 - ], - "ct": 467, - "alert": "none", - "colormode": "xy" - } - } -] -``` -Please note, 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 the other groups in the results returned from a call to `groups()`. - -If you need to get the full details of the __Lightset 0__ groups, then you can obtain that by using the `getGroup()` -function, using an id argument of `0`. - -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; -* `luminaires` Will obtain only the *Luminaire* 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 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; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.getGroup(0) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.getGroup(0, function(err, result) { - if (err) throw err; - displayResults(result); -}); -``` - -Which will return produce a result like; -``` -{ - "id": "0", - "name": "Lightset 0", - "lights": [ - "1", - "2", - "3", - "4", - "5" - ], - "type": "LightGroup", - "lastAction": { - "on": true, - "bri": 128, - "hue": 6144, - "sat": 254, - "xy": [ - 0.6376, - 0.3563 - ], - "ct": 500, - "effect": "none", - "colormode": "ct" - } -} -``` - -### Setting the Light State for a Group -A function ``setGroupLightState()`` exists for interacting with a group of lights to be able to set all the lights to a -particular state. This function is identical to that of the ``setLightState()`` function above, except that it works on -groups instead of a single light. - - -### Create a New Group -To create a new group use the __createGroup(name, lightIds)__ function; - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// Create a new Group on the bridge - -// -------------------------- -// Using a promise -api.createGroup("a new group", [4, 5]) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.createGroup("group name", [1, 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" -} -``` - - -### 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; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// Update the name of the group - -// -------------------------- -// Using a promise -api.updateGroup(1, "new group name") - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.updateGroup(1, "new group name", function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -Changing the lights associated with an existing group; -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -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. - -// -------------------------- -// Using a promise -api.updateGroup(1, [1, 2, 3]) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.updateGroup(1, [1, 2, 3], function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -Changing both the name and the lights for an existing group; -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -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. - -// -------------------------- -// Using a promise -api.updateGroup(1, "group name", [4, 5]) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.updateGroup(1, "group name", [4, 5], function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - - -### 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 -possible to delete groups, but use at your own risk *(you may have to reset the bridge to factory defaults if something -goes wrong)*. - -To delete a group use the ``deleteGroup()`` function; - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// Create a new Group on the bridge - -// -------------------------- -// Using a promise -api.deleteGroup(3) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.deleteGroup(4, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` -This function call will return a ``true`` result in the promise chain if successful, otherwise an error will be thrown. - - -## Working with Schedules - -### Obtaining all the Defined Schedules -To obtain all the defined schedules on the Hue Bridge use the ``schedules()`` function. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.schedules() - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.schedules(function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -The function will return a promise that will provide an array of objects, each containing the complete details fo the schedule; -``` -[ - { - "id": "9067578731131353", - "name": "Alarm", - "description": "Peter Wakeup", - "command": { - "address": "/api/yeM5QamRRFNXfv13/groups/0/action", - "body": { - "scene": "f0f7c51a6-on-7" - }, - "method": "PUT" - }, - "localtime": "W124/T06:10:00", - "time": "W124/T06:10:00", - "created": "2015-11-18T22:19:16", - "status": "disabled" - }, - ... -] -``` - -### Obtaining the details of a Schedule -To obtain the details of a schedule use the ``getSchedule(id)`` function; - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - scheduleId = 1; - -// -------------------------- -// Using a promise -api.getSchedule(scheduleId) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.getSchedule(scheduleId, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -The promise returned by the function will return the details of the schedule in the following format; -``` -{ - "id": "9067578731131353", - "name": "Alarm", - "description": "Peter Wakeup", - "command": { - "address": "/api/yeM5QamRRFNXfv13/groups/0/action", - "body": { - "scene": "f0f7c51a6-on-7" - }, - "method": "PUT" - }, - "localtime": "W124/T06:10:00", - "time": "W124/T06:10:00", - "created": "2015-11-18T22:19:16", - "status": "disabled" -} -``` - -### Creating a Schedule -Creating a schedule requires just two elements, a time at which to trigger the schedule and the command that will be -triggered when the schedule is run. -There are other optional values of a name and a description that can be provided to make the schedule easier to identify. - -There are two functions that can be invoked to create a new schedule (which are identically implemented); -- ``scheduleEvent(event, cb)`` -- ``createSchedule(event, cb)`` - -These functions both take an object the wraps up the scheduled event to be created. There are only two required properties -of the object, ``time`` and ``command``, with option properties ``name`` and ``description``. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - scheduledEvent; - -scheduledEvent = { - "name": "Sample Schedule", - "description": "A sample scheduled event to switch on a light", - "time": "2013-12-24T00:00:00", - "command": { - "address": "/api/08a902b95915cdd9b75547cb50892dc4/lights/5/state", - "method" : "PUT", - "body" : { - "on": true - } - } -}; - -// -------------------------- -// Using a promise -api.scheduleEvent(scheduledEvent) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.createSchedule(scheduledEvent, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -The result returned by the promise when creating a new schedule will be that of the ``id`` for the newly created schedule; -``` -{ - "id": "1" -} -``` - -The ``command`` value must be a Hue Bridge API endpoint for it to correctly function, which means it must start with -``/api//``. For now if using this function, you will have to use the exact API end point as specified in -the Phillips Hue REST API. - -To help with building a schedule and to perform some basic checking to ensure that values are correct/valid there is a -helper module ``scheduleEvent`` which can be used the build a valid schedule object. - - -### Using ScheduleEvent to build a Schedule -The ``scheduleEvent`` module/function is used to build up a schedule that the Hue Bridge can understand. It is not a -requirement when creating schedules, but can eliminate some of the basic errors that can result when creating a schedule. - -To obtain a scheduleEvent instance; -```js -var scheduleEvent = require("node-hue-api").scheduledEvent; - -var mySchedule = scheduleEvent.create(); -``` - -This will give you a schedule object that has the following functions available to build a schedule; -- ``withName(String)`` which will set a name for the schedule (optional) -- ``withDescription(String)`` which will set a description for the schedule (optional) -- ``withCommand(command)`` which will set the command object that the schedule will run -- ``on()``, ``at()``, ``when()`` which all take a string or Date value to specify the time the schedule will run, if -passing a string it must be valid when parsed by ``Date.parse()`` - -The ``command`` object currently has to be specified as the Hue Bridge API documentation states which is of the form; -``` -{ - "address": "/api/08a902b95915cdd9b75547cb50892dc4/lights/5/state", - "method" : "PUT", - "body" : { - "on": true - } -} -``` -The above example command will switch on the light with id ``5`` for the username ``08a902b95915cdd9b75547cb50892dc4``. - -If you use the ``withCommand()`` function then the ``address`` will be undergo basic validation to ensure it is an -endpoint for the Hue Bridge which is a common mistake to make when crafting your own values. - -Once a scheduleEvent has been built it can be passEd directly to the ``createSchedule()``, ``scheduleEvent()`` or -``updateSchedule()`` function calls in the Hue API. - -For example to create a new schedule that will turn on the light with id 5 at 07:00 on the 25th December 2013; -```js -var hue = require("node-hue-api"), - HueApi = hue.HueApi, - scheduleEvent = hue.scheduledEvent; - -var displayResult = function (result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - mySchedule; - -mySchedule = scheduleEvent.create() - .withName("Xmas Day Wake Up") - .withDescription("Like anyone really needs a wake up on Xmas day...") - .withCommand( - { - "address": "/api/08a902b95915cdd9b75547cb50892dc4/lights/5/state", - "method" : "PUT", - "body" : { - "on": true - } - }) - .on("2013-12-25T07:00:00"); - -// -------------------------- -// Using a promise -api.createSchedule(mySchedule) - .then(displayResult) - .done(); - -// -------------------------- -// Using a callback -api.createSchedule(mySchedule, function(err, result) { - if (err) throw err; - displayResult(result); -}); -``` - - -### Updating a Schedule -You can update an existing schedule using the ``updateSchedule()`` function; - -```js -var hue = require("node-hue-api"), - HueApi = hue.HueApi, - scheduleEvent = hue.scheduledEvent; - -var displayResult = function (result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - scheduleId = 1, - updatedValues; - -updatedValues = { - "name": "Updated Name", - "time": "January 1, 2014 07:00:30" -}; - -// -------------------------- -// Using a promise -api.updateSchedule(scheduleId, updatedValues) - .then(displayResult) - .done(); - -// -------------------------- -// Using a callback -api.updateSchedule(scheduleId, updatedValues, function(err, result) { - if (err) throw err; - displayResult(result); -}); -``` - -The result from the promise will be an object with the properties of the schedule that were updated and ``true`` as the -value of each one that was successful. -``` -{ - "name": true, - "time": true -} -``` - - -### Deleting a Schedule -All schedules in the Hue Bridge are removed once they are triggered. To remove an impending schedule use the ``deleteSchedule()`` -function; - -```js -var hue = require("node-hue-api"), - HueApi = hue.HueApi; - -var displayResult = function (result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - scheduleId = 1; - -// -------------------------- -// Using a promise -api.deleteSchedule(scheduleId) - .then(displayResult) - .done(); - -// -------------------------- -// Using a callback -api.deleteSchedule(scheduleId, function(err, result) { - if (err) throw err; - displayResult(result); -}); -``` - -If the deletion was successful, then ``true`` will be returned from the promise, otherwise an ``ApiError`` will be thrown, -as in the case if the schedule does not exist. - - -## Working with scenes -The Hue Bridge can store up to 200 scenes internally. There is currently no way to delete a scene from the API once it -is created, although old unused scenes will get overwritten. - -Additionally, bridge scenes should not be confused with the preset scenes stored in the Android and iOS apps. In the -apps these scenes are stored internally. Once activated though, they may then appear as a bridge scene. - - - -### Obtaining all the Defined scenes -To obtain all the defined bridge scenes on the Hue Bridge use the ``scenes()`` or ``getScenes()`` functions: - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.scenes() - .then(displayResults) - .done(); -// Using 'getScenes' alias -api.getScenes() - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.scenes(function(err, result){ - if (err) throw err; - displayResults(result); -}); -// Using 'getScenes' alias -api.getScenes(function(err, result){ - if (err) throw err; - displayResults(result); -``` - -The function will return an Array of scene definitions consisting of ``id``, ``name`` and ``lights``; -``` -[ - { - "name": "Tap scene 1", - "lights": [ - "1", - "2", - "3" - ], - "owner": "none", - "recycle": true, - "locked": true, - "appdata": {}, - "picture": "", - "lastupdated": null, - "version": 1, - "id": "OFF-TAP-1" - }, - { - "name": "Tap scene 3", - "lights": [ - "1", - "2", - "3" - ], - "owner": "none", - "recycle": true, - "locked": true, - "appdata": {}, - "picture": "", - "lastupdated": null, - "version": 1, - "id": "TAP-3" - }, - { - "name": "Tap scene 4", - "lights": [ - "1", - "2", - "3" - ], - "owner": "none", - "recycle": true, - "locked": true, - "appdata": {}, - "picture": "", - "lastupdated": null, - "version": 1, - "id": "TAP-4" - }, - { - "name": "Blue rain on 0", - "lights": [ - "1", - "2", - "3" - ], - "owner": "none", - "recycle": true, - "locked": true, - "appdata": {}, - "picture": "", - "lastupdated": null, - "version": 1, - "id": "74f97e0ba-on-0" - } -] -``` - -### Get a Scene -You can obtain a specific scene using the id of the scene and the ``scene()`` or ``getScene()`` function: - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - sceneId = "OFF-TAP-1" - ; - -// -------------------------- -// Using a promise -api.scene(sceneId) - .then(displayResults) - .done(); -// Using 'getScene' alias -api.getScene(sceneId) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.scene(sceneId, function(err, result){ - if (err) throw err; - displayResults(result); -}); -// Using 'getScene' alias -api.getScene(sceneId, function(err, result){ - if (err) throw err; - displayResults(result); -}; -``` - -The functions will return a result of the scene definition, like the following: -``` -{ - "OFF-TAP-1": { - "name": "Tap scene 1", - "lights": [ - "1", - "2", - "3", - "4" - ], - "owner": "none", - "recycle": true, - "locked": true, - "appdata": {}, - "picture": "", - "lastupdated": null, - "version": 1 - } -``` - -### Creating a Scene -The original function `createScene()` was implemented to support the older `1.2.x` version of the Hue Bridge Scene -creation. In version `2.0.x` of this library, it was modified to support both the old an new versions of scene creation. - -If you call this function using a an `array` of light ids and a `name` it will call the `createBasicScene()` function. -Alternatively if you call this function with a `Scene` object, then the `createAdvancedScene()` function will be invoked. - -See below for the specifics of the parameters and results. - - -### Creating a Basic Scene -There are multiple definitions on scenes, some of which are stored in the Bridge, others are stored inside the iOS and -Android applications. This API can only interact and define scenes that are stored inside the Hue Bridge. - -As of version `1.11` of the Hue Bridge Software, all scenes are now stored inside the bridge. - -When creating a new scene, the current state of the lights that are being included become the state of the lights when -you activate/recall the scene in the future. - -When you create a scene via the API function ``createBasicScene()``, the scene will get an ``id`` that is created by the -bridge. - -```js -var hue = require("node-hue-api") - , HueApi = hue.HueApi - , hueScene = hue.scene - ; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.245" - , username = "08a902b95915cdd9b75547cb50892dc4" - , api = new HueApi(host, username) - , scene = hueScene.create() - ; - -scene.withName("My Scene") - .withLights([1, 2, 3]) - .withTransitionTime(500) - ; - -// -------------------------- -// Using a promise -api.createAdvancedScene(scene) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.createAdvancedScene(scene, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -When a new scene is created, you will get a result back with the id of the created scene of the form; -``` -{ - "id": "jsoaw-58sk2" -} -``` - -The ``name`` value is optional, if one is not specified, then it will be set as the ``id`` that is generated. This is a -feature of the underlying Hue Bridge, so may change in a future firmware update. - - -### Creating an Advanced Scene -With the advent of version `1.11` Hue Bridge software version, the creation of Scenes was officially added and the number - of parameters available when creating a scene changed. - -The scenes are now stored inside the bridge itself, and to support the multiple combination of parameters available you -need to provide the settings in a `Scene` object. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - sceneName = "My New Scene", - lightIds = [1, 2, 3, 4, 5, 6, 7] - ; - -// -------------------------- -// Using a promise -api.createBasicScene(lightIds, sceneName) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.scene(lightIds, sceneName, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -When a new scene is created, you will get a result back with the id of the created scene of the form; -``` -{ - "id": "jsoaw-58sk2" -} -``` - -### Scene Object -The Scene object allows you to define a number of parameters that can be used to define a scene as of the `1.11.x` Hue -Bridge firmware. - -To obtain a `scene` instance; -```js -var hueScene = require("node-hue-api").scene; - -var myScene = scene.create(); -``` - -This will give you a `Scene` object that has the following functions available to build a scene; -- ``withName(String)`` which will set a name for the scene (optional) -- ``withLights([])`` which will set the array of light ids for the scene -- ``withTransitionTime(ms)`` which will set the transtion time in milliseconds for the lights (optional) -- ``withRecycle(boolean)`` which will set the recycle flag on the scene (optional) -- ``withAppData(Object)`` which will set the application data for the scene e.g. ``{data: "some data", version: "1.0"}`` (optional) -- ``withPicture(String)`` a base64 encoded string which is the picture for the scene (optional) - -An example of building a complex scene; -```js -var hueScene = require("node-hue-api").scene; - -var scene = hueScene.create() - .withName("my scene") - .withLights([1, 2, 3]) - .withTransitionTime(2000) - .withAppData({data: "My own application data value for the scene"}) - ; -``` - - -### Updating/Modifying an Existing Scene -You can update an existing scene by using the ``updateScene()`` or ``modifyScene()`` function. When updating the scene -you can set the ``name``, ``lights`` or store the current light states of the lights in the scene. Any combination of -these parameters are possible. - -```js -var hue = require("node-hue-api") - , HueApi = hue.HueApi - , hueScene = hue.scene - ; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.245" - , username = "08a902b95915cdd9b75547cb50892dc4" - , api = new HueApi(host, username) - , sceneId = "iimpLsd4yVuVnjy" - , sceneUpdates = hueScene.create() - ; - -sceneUpdates.withName("My Scene") - .withLights([1, 2, 3]) - ; - -// -------------------------- -// Using a promise -api.modifyScene(sceneId, sceneUpdates) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.modifyScene(sceneId, sceneUpdates, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -When the scene is modified/updated, you will get a response containing the values modified; -``` -{ - "name": true, - "lights": true -} -``` - -Each value that is modified will report a ``true`` or ``false`` value if the parameter was changed. - - -### Set a Light State for a Light in a Scene -If you need to set a different light state for a light that is part of scene (that is a different state to what it was -in when the original scene was created), then you can use the `setSceneLightState()`, ``updateSceneLightState()`` -or ``modifySceneLightState()`` function. - -This function allows you to specify the desired values for a single light in a scene, if you want to set the state for -multiple bulbs, you will have to set it on each one individually. - -```js -var HueApi = require("node-hue-api").HueApi - , lightState = require("node-hue-api").lightState - ; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.245", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - sceneId = "node-hue-api-2", - lightId = 1, - state = lightState.create().on().hue(2000) - ; - -// -------------------------- -// Using a promise -api.setSceneLightState(sceneId, lightId, state) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.setSceneLightState(sceneId, lightId, state, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -The results from setting light sate values will be the name of each value being set followed by a value of `true` if -the change in the value was successful; - -``` -{ - "on": true, - "hue": true -} -``` - - -### Activating or Recalling a Scene -To recall or activate a scene (synonyms for the same activity) use the ``activateScene()`` or ``recallScene()`` function. - -When a scense is being made active, it is possible to also filter the lights in the scene using a group definition to -limit the lights that will be affected by the scene activation. -This means you could have defined a scene for all your bulbs, but if you apply a group filter that includes only, say -the lounge lights, then the scene will be activated only on the lounge lights. - -If a group filter is not specified (it is an optional parameter) then the API does no filtering on the lights in the -scene when it is activated. - -```js -var HueApi = require("node-hue-api").HueApi - , lightState = require("node-hue-api").lightState - ; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - sceneId = "node-hue-api-2", - lightId = 1, - state = lightState.create().on().hue(2000) - ; - -// -------------------------- -// Using a promise -api.activateScene(sceneId) - .then(displayResults) - .done(); -// using the "recallScene" alias -api.recallScene(sceneId) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.activateScene(sceneId, function(err, result) { - if (err) throw err; - displayResults(result); -}); -// using the "recallScene" alias -api.recallScene(sceneId, function(err, result) { - if (err) throw err; - displayResults(result); -}); -``` - -When a Scene is successfully activated/recalled, the result will be `true`. - -### Scenes by Name -There is no sensible way to dealing with scenes by name currently (firmware version 1.5+) as it is possible to define -multiple scenes with the same name (in fact in testing even editing a scene in the iOS app created a new scene on the -bridge). - -The scene `id` is the only reliable and consistent way to interact with scene activation/recalling. - - -## 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. - -The way to set a maximum timeout when interacting with the bridge is done when you instantiate the ``HueApi``. - -```js -var hue = require("node-hue-api"), - HueApi = hue.HueApi; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - timeout = 20000 // timeout in milliseconds - api; - -api = new HueApi(host, username, timeout); -``` - -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, username, timeout, port); -``` - - -## License -Copyright 2013-2015. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this library except in compliance with the License. - -You may obtain a copy of the License at . - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/index.js b/index.js index 8342851..c94f20b 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,6 @@ const v3 = require('./lib/v3') , ApiError = require('./lib/ApiError') - , oldApi = require('./hue-api/shim') ; module.exports = { @@ -14,14 +13,4 @@ module.exports = { // This was present in the old API, may need to deprecate it ApiError: ApiError, - - // Older API for backwards compatibility, will be removed in v4.x - api: oldApi.api, - HueApi: oldApi.api, - BridgeApi: oldApi.api, - lightState: oldApi.lightState, - scheduledEvent: oldApi.scheduledEvent, - scene: oldApi.scene, - upnpSearch: oldApi.upnpSearch, - nupnpSearch: oldApi.nupnpSearch, }; diff --git a/lib/ApiError.js b/lib/ApiError.js index 52162c3..2f3932b 100644 --- a/lib/ApiError.js +++ b/lib/ApiError.js @@ -34,18 +34,30 @@ class ApiError extends Error { return this._hueError; } + /** + * @returns {number} + */ getHueErrorType() { return this._hueError ? this._hueError.type : -1; } + /** + * @returns {string | null} + */ getHueErrorAddress() { return this._hueError ? this._hueError.address : null; } + /** + * @returns {string | null} + */ getHueErrorDescription() { return this._hueError ? this._hueError.description : null; } + /** + * @returns {string | null} + */ getHueErrorMessage() { return this._hueError ? this._hueError.message : null; } diff --git a/lib/HueError.js b/lib/HueError.js index c66ed07..6773133 100644 --- a/lib/HueError.js +++ b/lib/HueError.js @@ -21,18 +21,30 @@ class HueError { this.payload = payload; } + /** + * @returns {number} + */ get type() { return this.payload.type || -1; } + /** + * @returns {string} + */ get address() { return this.payload.address; } + /** + * @returns {string} + */ get description() { return this.payload.description; } + /** + * @returns {string} + */ get message() { let str = this.payload.message , type = this.type @@ -46,6 +58,9 @@ class HueError { return str; } + /** + * @returns {*} + */ get rawError() { return this.payload; } diff --git a/lib/api/Groups.test.js b/lib/api/Groups.test.js index 4f048f1..ba816a1 100644 --- a/lib/api/Groups.test.js +++ b/lib/api/Groups.test.js @@ -95,7 +95,7 @@ describe('Hue API #groups', () => { await hue.groups.get('ab62c6'); expect.fail('should not get here'); } catch (err) { - expect(err.message).to.contain('not a parsable number value'); + expect(err.message).to.contain('not a parsable number'); } }); }); diff --git a/lib/api/http/RemoteApi.js b/lib/api/http/RemoteApi.js index abfb189..4f52669 100644 --- a/lib/api/http/RemoteApi.js +++ b/lib/api/http/RemoteApi.js @@ -4,7 +4,7 @@ const crypto = require('crypto') , axios = require('axios') , ApiError = require('../../ApiError') , OAuthTokens = require('./OAuthTokens') - , util = require('./util') + , util = require('../../util') ; // This class is a bit different to the other endpoints currently as they operate in a digest challenge for the most diff --git a/lib/api/http/Transport.js b/lib/api/http/Transport.js index 1e2c902..6e5705e 100644 --- a/lib/api/http/Transport.js +++ b/lib/api/http/Transport.js @@ -2,7 +2,7 @@ const ApiError = require('../../ApiError') , HueError = require('../../HueError') - , util = require('./util') + , util = require('../../util') ; const DEBUG = /node-hue-api/.test(process.env.NODE_DEBUG); diff --git a/lib/api/http/endpoints/configuration.js b/lib/api/http/endpoints/configuration.js index b010b58..733d571 100644 --- a/lib/api/http/endpoints/configuration.js +++ b/lib/api/http/endpoints/configuration.js @@ -2,7 +2,7 @@ const ApiEndpoint = require('./endpoint') , UsernamePlaceholder = require('../placeholders/UsernamePlaceholder') - , util = require('../util') + , util = require('../../../util') , ApiError = require('../../../ApiError') ; diff --git a/lib/api/http/endpoints/groups.js b/lib/api/http/endpoints/groups.js index 0dafc8a..93becb9 100644 --- a/lib/api/http/endpoints/groups.js +++ b/lib/api/http/endpoints/groups.js @@ -2,7 +2,7 @@ const ApiEndpoint = require('./endpoint') , ApiError = require('../../../ApiError') - , util = require('../util') + , util = require('../../../util') , GroupIdPlaceholder = require('../placeholders/GroupIdPlaceholder') , model = require('../../../model') , GroupState = require('../../../model/lightstate/GroupState') diff --git a/lib/api/http/endpoints/lights.js b/lib/api/http/endpoints/lights.js index 02df359..e3fc4a4 100644 --- a/lib/api/http/endpoints/lights.js +++ b/lib/api/http/endpoints/lights.js @@ -2,7 +2,7 @@ const ApiEndpoint = require('./endpoint') , LightIdPlaceholder = require('../placeholders/LightIdPlaceholder') , LightState = require('../../../model/lightstate/LightState') , ApiError = require('../../../ApiError') - , util = require('../util') + , util = require('../../../util') , model = require('../../../model') , rgb = require('../../../rgb') ; diff --git a/lib/api/http/endpoints/resourcelinks.js b/lib/api/http/endpoints/resourcelinks.js index 9fc3e9a..5c2074c 100644 --- a/lib/api/http/endpoints/resourcelinks.js +++ b/lib/api/http/endpoints/resourcelinks.js @@ -4,7 +4,7 @@ const ApiEndpoint = require('./endpoint') , ResourceLinkPlaceholder = require('../placeholders/ResourceLinkPlaceholder') , model = require('../../../model') , ApiError = require('../../../ApiError') - , util = require('../util') + , util = require('../../../util') ; module.exports = { diff --git a/lib/api/http/endpoints/rules.js b/lib/api/http/endpoints/rules.js index a37b957..ae7fc50 100644 --- a/lib/api/http/endpoints/rules.js +++ b/lib/api/http/endpoints/rules.js @@ -4,7 +4,7 @@ const ApiEndpoint = require('./endpoint') , RuleIdPlaceholder = require('../placeholders/RuleIdPlaceholder') , model = require('../../../model') , ApiError = require('../../../ApiError') - , util = require('../util') + , util = require('../../../util') ; module.exports = { diff --git a/lib/api/http/endpoints/scenes.js b/lib/api/http/endpoints/scenes.js index 939955b..a9c900d 100644 --- a/lib/api/http/endpoints/scenes.js +++ b/lib/api/http/endpoints/scenes.js @@ -6,7 +6,7 @@ const ApiEndpoint = require('./endpoint') , model = require('../../../model') , SceneLightState = require('../../../model/lightstate/SceneLightState') , ApiError = require('../../../ApiError') - , util = require('../util') + , util = require('../../../util') ; module.exports = { diff --git a/lib/api/http/endpoints/schedules.js b/lib/api/http/endpoints/schedules.js index 6e99316..421cecf 100644 --- a/lib/api/http/endpoints/schedules.js +++ b/lib/api/http/endpoints/schedules.js @@ -4,7 +4,7 @@ const ApiEndpoint = require('./endpoint') , ScheduleIdPlaceholder = require('../placeholders/ScheduleIdPlaceholder') , model = require('../../../model') , ApiError = require('../../../ApiError') - , util = require('../util') + , util = require('../../../util') ; diff --git a/lib/api/http/endpoints/sensors.js b/lib/api/http/endpoints/sensors.js index 9b216e2..737959c 100644 --- a/lib/api/http/endpoints/sensors.js +++ b/lib/api/http/endpoints/sensors.js @@ -4,7 +4,7 @@ const ApiEndpoint = require('./endpoint') , SensorIdPlaceholder = require('../placeholders/SensorIdPlaceholder') , model = require('../../../model') , ApiError = require('../../../ApiError') - , util = require('../util') + , util = require('../../../util') ; module.exports = { diff --git a/lib/model/BridgeObject.js b/lib/model/BridgeObject.js index c23c7b7..4392c4a 100644 --- a/lib/model/BridgeObject.js +++ b/lib/model/BridgeObject.js @@ -2,8 +2,16 @@ const ApiError = require('../ApiError.js'); +/** + * @typedef { import('../types/Type') } Type + * @type {BridgeObject} + */ module.exports = class BridgeObject { + /** + * @param attributes {Array.} + * @param id {number | null} + */ constructor(attributes, id) { this._attributes = {}; this._data = {}; @@ -19,6 +27,10 @@ module.exports = class BridgeObject { this.setAttributeValue('id', id); } + /** + * @param name {string} + * @returns {*} + */ getAttributeValue(name) { const definition = this._attributes[name]; @@ -29,6 +41,11 @@ module.exports = class BridgeObject { } } + /** + * @param name {string} + * @param value {*} + * @returns {BridgeObject} + */ setAttributeValue(name, value) { const definition = this._attributes[name]; @@ -41,6 +58,9 @@ module.exports = class BridgeObject { return this; } + /** + * @returns {string | number} + */ get id() { return this.getAttributeValue('id'); } @@ -70,10 +90,16 @@ module.exports = class BridgeObject { // return this._bridgeData; } + /** + * @returns {string} + */ toString() { return `${this.constructor.name}\n id: ${this.id}`; } + /** + * @returns {string} + */ toStringDetailed() { let result = this.toString(); @@ -86,6 +112,11 @@ module.exports = class BridgeObject { return result; } + /** + * @param data {*} + * @returns {BridgeObject} + * @private + */ _populate(data) { const self = this; @@ -105,42 +136,12 @@ module.exports = class BridgeObject { return self; } + /** + * @returns {any | {}} + * @private + */ get _bridgeData() { // Return a copy so that it cannot be modified from outside return Object.assign({}, this._data); } - - //TODO utility function, move out - static getRawDataValue(key, data) { - //TODO use dot notation to get nested values - const path = key.split('.'); - - let target = data - , value = null - ; - - path.forEach(part => { - if (target != null) { - value = target[part]; - target = value; - } else { - target = null; - } - }); - - return value; - } - - // TODO util function - static mergeAttributes() { - let result = []; - - Array.from(arguments).forEach(arg => { - if (arg) { - result = result.concat(arg); - } - }); - - return result; - } }; diff --git a/lib/model/Light.js b/lib/model/Light.js index d9a848b..2fbf762 100644 --- a/lib/model/Light.js +++ b/lib/model/Light.js @@ -3,6 +3,7 @@ const BridgeObject = require('./BridgeObject') , colorGamuts = require('./colorGamuts') , types = require('../types') + , util = require('../util') ; const MODEL_TO_COLOR_GAMUT = { @@ -169,7 +170,7 @@ module.exports = class Light extends BridgeObject { function getColorGamut(data) { // Newer Hue devices report their own color gamuts under 'capabilities.control.colorgamuttype' - let colorGamutType = BridgeObject.getRawDataValue('capabilities.control.colorgamuttype', data); + let colorGamutType = util.getValueForKey('capabilities.control.colorgamuttype', data); if (!colorGamutType) { colorGamutType = MODEL_TO_COLOR_GAMUT[data.modelid]; diff --git a/lib/model/ResourceLink.js b/lib/model/ResourceLink.js index c39fa69..195f243 100644 --- a/lib/model/ResourceLink.js +++ b/lib/model/ResourceLink.js @@ -155,6 +155,10 @@ module.exports = class ResourceLink extends BridgeObject { return data; } + /** + * @param data {*} + * @private + */ _populate(data) { // Links are taken apart and separated out from the data const rawData = Object.assign({}, data); @@ -163,6 +167,7 @@ module.exports = class ResourceLink extends BridgeObject { super._populate(rawData); this._links = processLinks(linkData); + return this; } }; diff --git a/lib/model/Schedule.js b/lib/model/Schedule.js index 83e976a..3dc8f87 100644 --- a/lib/model/Schedule.js +++ b/lib/model/Schedule.js @@ -87,38 +87,39 @@ module.exports = class Schedule extends BridgeObject { } set recycle(value) { - return this.setAttributeValue('recylce', value); + return this.setAttributeValue('recycle', value); } get created() { return this.getAttributeValue('created'); } - get payload() { - const self = this - , payload = {} - ; - - // Mandatory values - ['command'].forEach(key => { - const value = self.getRawDataValue(key); - if (!value) { - throw new ApiError(`Mandatory Schedule parameter ${key} is missing.`); - } - payload[key] = value; - }); - - // Mandatory localtime value - payload.localtime = this._localtime.toString(); - - // Optional values - ['name', 'description', 'autodelete', 'status', 'recycle'].forEach(key => { - const value = self.getRawDataValue(key); - if (value) { - payload[key] = value; - } - }); - - return payload; - } + //TODO this needs to be built in to the getBridgePayload() + // get payload() { + // const self = this + // , payload = {} + // ; + // + // // Mandatory values + // ['command'].forEach(key => { + // const value = self.getRawDataValue(key); + // if (!value) { + // throw new ApiError(`Mandatory Schedule parameter ${key} is missing.`); + // } + // payload[key] = value; + // }); + // + // // Mandatory localtime value + // payload.localtime = this._localtime.toString(); + // + // // Optional values + // ['name', 'description', 'autodelete', 'status', 'recycle'].forEach(key => { + // const value = self.getRawDataValue(key); + // if (value) { + // payload[key] = value; + // } + // }); + // + // return payload; + // } }; \ No newline at end of file diff --git a/lib/model/rules/Rule.js b/lib/model/rules/Rule.js index 33d22a8..243a546 100644 --- a/lib/model/rules/Rule.js +++ b/lib/model/rules/Rule.js @@ -125,6 +125,7 @@ module.exports = class Rule extends BridgeObject { super._populate(data); this._conditions = buildConditions(data ? data.conditions : null); this._actions = buildActions(data ? data.actions : null); + return this; } getHuePayload() { diff --git a/lib/model/rules/actions/RuleAction.js b/lib/model/rules/actions/RuleAction.js index bf6ff57..df0c2c1 100644 --- a/lib/model/rules/actions/RuleAction.js +++ b/lib/model/rules/actions/RuleAction.js @@ -16,10 +16,16 @@ module.exports = class RuleAction { this._method = method || null; } + /** + * @return {string} + */ get address() { throw new ApiError('Not implemented'); } + /** + * @return {*} + */ get body() { throw new ApiError('Not implemented'); } diff --git a/lib/model/rules/conditions/operators/Ddx.js b/lib/model/rules/conditions/operators/Ddx.js deleted file mode 100644 index 4caf3be..0000000 --- a/lib/model/rules/conditions/operators/Ddx.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const RuleConditionOperator = require('./RuleConditionOperator'); - -class Ddx extends RuleConditionOperator { - - constructor() { - super('ddx'); - } -} - -module.exports = new Ddx(); \ No newline at end of file diff --git a/lib/model/rules/conditions/operators/Dx.js b/lib/model/rules/conditions/operators/Dx.js deleted file mode 100644 index 9c3fbe8..0000000 --- a/lib/model/rules/conditions/operators/Dx.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const RuleConditionOperator = require('./RuleConditionOperator'); - -class Dx extends RuleConditionOperator { - - constructor() { - super('dx'); - } -} - -module.exports = new Dx(); \ No newline at end of file diff --git a/lib/model/rules/conditions/operators/Equals.js b/lib/model/rules/conditions/operators/Equals.js deleted file mode 100644 index bc67b0e..0000000 --- a/lib/model/rules/conditions/operators/Equals.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -const RuleConditionOperator = require('./RuleConditionOperator'); - - -class Equals extends RuleConditionOperator { - - constructor() { - super('eq', ['=', '==', 'equals', '===']); - } -} - -module.exports = new Equals(); \ No newline at end of file diff --git a/lib/model/rules/conditions/operators/Equals.test.js b/lib/model/rules/conditions/operators/Equals.test.js deleted file mode 100644 index 843bf86..0000000 --- a/lib/model/rules/conditions/operators/Equals.test.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , Equals = require('./Equals') -; - -describe('RuleConditionOperator #Equals', () => { - - describe('#matches()', () => { - - it('should match Hue type', () => { - expect(Equals.matches('eq')).to.be.true; - }); - - it('should match "equals"', () => { - expect(Equals.matches('equals')).to.be.true; - }); - - it('should match "="', () => { - expect(Equals.matches('=')).to.be.true; - }); - - it('should match "=="', () => { - expect(Equals.matches('==')).to.be.true; - }); - - it('should match "==="', () => { - expect(Equals.matches('===')).to.be.true; - }); - - it('should not match invalid values', () => { - expect(Equals.matches('')).to.be.false; - expect(Equals.matches('equ')).to.be.false; - expect(Equals.matches('!=')).to.be.false; - expect(Equals.matches('not equals')).to.be.false; - expect(Equals.matches('not eq')).to.be.false; - }); - }); - - describe('#type()', () => { - - it('should provide correct hue condition typpe', () => { - expect(Equals.type).to.equal('eq'); - }); - }); -}); \ No newline at end of file diff --git a/lib/model/rules/conditions/operators/GreaterThan.js b/lib/model/rules/conditions/operators/GreaterThan.js deleted file mode 100644 index fff78fe..0000000 --- a/lib/model/rules/conditions/operators/GreaterThan.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const RuleConditionOperator = require('./RuleConditionOperator'); - -class GreaterThan extends RuleConditionOperator { - - constructor() { - super('gt'); - } -} - -module.exports = new GreaterThan(); \ No newline at end of file diff --git a/lib/model/rules/conditions/operators/In.js b/lib/model/rules/conditions/operators/In.js deleted file mode 100644 index 26dac7d..0000000 --- a/lib/model/rules/conditions/operators/In.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const RuleConditionOperator = require('./RuleConditionOperator'); - -class In extends RuleConditionOperator { - - constructor() { - super('in'); - } -} - -module.exports = new In(); \ No newline at end of file diff --git a/lib/model/rules/conditions/operators/LessThan.js b/lib/model/rules/conditions/operators/LessThan.js deleted file mode 100644 index 4efa192..0000000 --- a/lib/model/rules/conditions/operators/LessThan.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const RuleConditionOperator = require('./RuleConditionOperator'); - -class LessThan extends RuleConditionOperator { - - constructor() { - super('lt'); - } -} - -module.exports = new LessThan(); \ No newline at end of file diff --git a/lib/model/rules/conditions/operators/NotIn.js b/lib/model/rules/conditions/operators/NotIn.js deleted file mode 100644 index d491b0f..0000000 --- a/lib/model/rules/conditions/operators/NotIn.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const RuleConditionOperator = require('./RuleConditionOperator'); - -class NotIn extends RuleConditionOperator { - - constructor() { - super('not in'); - } -} - -module.exports = new NotIn(); \ No newline at end of file diff --git a/lib/model/rules/conditions/operators/NotStable.js b/lib/model/rules/conditions/operators/NotStable.js deleted file mode 100644 index 785d7f5..0000000 --- a/lib/model/rules/conditions/operators/NotStable.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const RuleConditionOperator = require('./RuleConditionOperator'); - -class NotStable extends RuleConditionOperator { - - constructor() { - super('not stable'); - } -} - -module.exports = new NotStable(); \ No newline at end of file diff --git a/lib/model/rules/conditions/operators/Stable.js b/lib/model/rules/conditions/operators/Stable.js deleted file mode 100644 index ed9e532..0000000 --- a/lib/model/rules/conditions/operators/Stable.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const RuleConditionOperator = require('./RuleConditionOperator'); - -class Stable extends RuleConditionOperator { - - constructor() { - super('stable'); - } -} - -module.exports = new Stable(); \ No newline at end of file diff --git a/lib/model/rules/conditions/operators/index.js b/lib/model/rules/conditions/operators/index.js index b244c6e..9130c18 100644 --- a/lib/model/rules/conditions/operators/index.js +++ b/lib/model/rules/conditions/operators/index.js @@ -1,15 +1,16 @@ 'use strict'; -const RuleConditionOperator = require('./RuleConditionOperator') - , Equals = require('./Equals') - , Dx = require('./Dx') - , Ddx = require('./Ddx') - , Stable = require('./Stable') - , NotStable = require('./NotStable') - , In = require('./In') - , NotIn = require('./NotIn') - , LessThan = require('./LessThan') - , GreaterThan = require('./GreaterThan') +const RuleConditionOperator = require('./RuleConditionOperator'); + +const Equals = new RuleConditionOperator('eq', ['=', '==', 'equals', '===']) + , Dx = new RuleConditionOperator('dx') + , Ddx =new RuleConditionOperator('ddx') + , Stable = new RuleConditionOperator('stable') + , NotStable = new RuleConditionOperator('not stable') + , In = new RuleConditionOperator('in') + , NotIn = new RuleConditionOperator('not in') + , LessThan = new RuleConditionOperator('lt', ['<']) + , GreaterThan = new RuleConditionOperator('gt' ['>']) ; module.exports.getOperator = function(value) { @@ -32,11 +33,19 @@ module.exports.getOperator = function(value) { }; module.exports.equals = Equals; + module.exports.changed = Dx; + module.exports.changedDelayed = Ddx; + module.exports.greaterThan = GreaterThan; + module.exports.lessThan = LessThan; + module.exports.stable = Stable; + module.exports.notStable = NotStable; + module.exports.in = In; + module.exports.notIn = NotIn; \ No newline at end of file diff --git a/lib/model/scenes/Scene.js b/lib/model/scenes/Scene.js index 3dcfc78..e7e0c5b 100644 --- a/lib/model/scenes/Scene.js +++ b/lib/model/scenes/Scene.js @@ -2,6 +2,7 @@ const BridgeObject = require('../BridgeObject') , types = require('../../types') + , util = require('../../util') ; const ATTRIBUTES = [ @@ -21,7 +22,7 @@ const ATTRIBUTES = [ module.exports = class Scene extends BridgeObject { constructor(attributes, type, id) { - super(BridgeObject.mergeAttributes(ATTRIBUTES, attributes), id); + super(util.flatten(ATTRIBUTES, attributes), id); this.setAttributeValue('type', type); } diff --git a/lib/model/sensors/CLIPSensor.js b/lib/model/sensors/CLIPSensor.js index 8c2614e..d72eec4 100644 --- a/lib/model/sensors/CLIPSensor.js +++ b/lib/model/sensors/CLIPSensor.js @@ -2,6 +2,7 @@ const Sensor = require('./Sensor') , types = require('../../types') + , util = require('../../util') ; const CONFIG_ATTRIBUTES = [ @@ -13,7 +14,7 @@ const CONFIG_ATTRIBUTES = [ module.exports = class CLIPSensor extends Sensor { constructor(configAttributes, stateAttributes, id) { - super(Sensor.mergeAttributes(CONFIG_ATTRIBUTES, configAttributes), stateAttributes, id); + super(util.flatten(CONFIG_ATTRIBUTES, configAttributes), stateAttributes, id); } get reachable() { diff --git a/lib/model/sensors/Sensor.js b/lib/model/sensors/Sensor.js index ca8e284..323146f 100644 --- a/lib/model/sensors/Sensor.js +++ b/lib/model/sensors/Sensor.js @@ -2,6 +2,7 @@ const BridgeObject = require('../BridgeObject') , parameters = require('../../types') + , util = require('../../util') ; @@ -31,13 +32,13 @@ module.exports = class Sensor extends BridgeObject { constructor(configAttributes, stateAttributes, id, data) { const stateAttribute = parameters.object({ name: 'state', - types: Sensor.mergeAttributes(COMMON_STATE_ATTRIBUTES, stateAttributes) + types: util.flatten(COMMON_STATE_ATTRIBUTES, stateAttributes) }) , configAttribute = parameters.object({ name: 'config', - types: Sensor.mergeAttributes(COMMON_CONFIG_ATTRIBUTES, configAttributes) + types: util.flatten(COMMON_CONFIG_ATTRIBUTES, configAttributes) }) - , allAttributes = Sensor.mergeAttributes(COMMON_ATTRIBUTES, stateAttribute, configAttribute) + , allAttributes = util.flatten(COMMON_ATTRIBUTES, stateAttribute, configAttribute) ; super(allAttributes, id); diff --git a/lib/types/ListType.test.js b/lib/types/ListType.test.js index 07d560f..a06eeec 100644 --- a/lib/types/ListType.test.js +++ b/lib/types/ListType.test.js @@ -221,7 +221,7 @@ describe('ListType', () => { }); it('should fail on a string value', () => { - testFailure(type, 'hello', 'not a parsable number value'); + testFailure(type, 'hello', 'not a parsable number'); }); it('should process a string integer value', () => { @@ -284,7 +284,7 @@ describe('ListType', () => { }); it('should fail on a string value', () => { - testFailure(type, 'hello', 'not a parsable number value'); + testFailure(type, 'hello', 'not a parsable number'); }); it('should process a string integer value', () => { diff --git a/lib/api/http/util.js b/lib/util.js similarity index 73% rename from lib/api/http/util.js rename to lib/util.js index b2564a4..397d9c9 100644 --- a/lib/api/http/util.js +++ b/lib/util.js @@ -1,14 +1,16 @@ 'use strict'; -const ApiError = require('../../ApiError.js') - , HueError = require('../../HueError') //TODO consider remove the use of this here now +const ApiError = require('./ApiError.js') + , HueError = require('./HueError') //TODO consider remove the use of this here now ; module.exports = { parseErrors: parseErrors, wasSuccessful: wasSuccessful, extractUpdatedAttributes: extractUpdatedAttributes, - asStringArray: asStringArray + asStringArray: asStringArray, + flatten: mergeArrays, + getValueForKey: getValueforKey }; @@ -79,7 +81,7 @@ function extractUpdatedAttributes(result) { } } - +//TODO the type system could replace this function now function asStringArray(value) { if (!value) { return null; @@ -98,3 +100,36 @@ function asStringArray(value) { } } +function getValueforKey(key, data) { + //Use dot notation to get nested values + const path = key.split('.'); + + let target = data + , value = null + ; + + path.forEach(part => { + if (target != null) { + value = target[part]; + target = value; + } else { + target = null; + } + }); + + return value; +} + +function mergeArrays() { + // TODO this can be replaced with [[], [], ...].flat under Node.js 12+ + let result = []; + + Array.from(arguments).forEach(arg => { + if (arg) { + result = result.concat(arg); + } + }); + + return result; +} + diff --git a/package.json b/package.json index 3cb26c5..aacf006 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,9 @@ "test-model": "mocha --recursive \"lib/model/**/*.test.js\"", "test-api": "mocha --recursive \"lib/api/**/*.test.js\"", "test-types": "mocha --recursive \"lib/types/*.test.js\"", - "test-old-api": "mocha --recursive \"test/**/*-tests.js\"", - "generate-typescript-definitions": "npx typescript index.js --allowJs --declaration --emitDeclarationOnly" + "clean-ts-definitions": "npx rimraf **/*.d.ts", + "generate-ts-definitions": "npm run clean-ts-definitions && npx typescript index.js --allowJs --declaration --emitDeclarationOnly", + "prepublishOnly": "npm run generate-ts-definitions" }, "repository": { "type": "git", diff --git a/test/getConfiguration-test.js b/test/getConfiguration-test.js deleted file mode 100644 index 8e47fd3..0000000 --- a/test/getConfiguration-test.js +++ /dev/null @@ -1,242 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , semver = require('semver') - , HueApi = require('..').BridgeApi - , testValues = require('./support/testValues.js') -; - - -describe('Hue API', function () { - - const hue = new HueApi(testValues.host, testValues.username); - - this.timeout(5000); - - describe('#config', function () { - - function validateConfigResults(results) { - expect(results).to.be.an.instanceOf(Object); - - expect(results).to.have.property('name'); - expect(results).to.have.property('mac'); - 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(); - }) - .done(); - }); - - it('using #callback', function (finished) { - hue.config(function (err, results) { - expect(err).to.be.null; - - validateConfigResults(results); - finished(); - }); - }); - }); - - - describe('version', 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'); - expect(semver.gte(results.version.api, '1.31.0')).to.be.true; - - expect(results.version).to.have.property('software'); - expect(parseInt(results.version.software)).to.at.least(1931140050); - } - - function testPromise(name, done) { - hue[name]().then(function (results) { - validateVersionResults(results); - done(); - }) - .done(); - } - - function testCallback(name, done) { - hue[name](function (err, results) { - expect(err).to.be.null; - - validateVersionResults(results); - done(); - }); - } - - describe('#version', function () { - - it('using #promise', function (done) { - testPromise('version', done); - }); - - it('using #callback', function (done) { - testCallback('version', done); - }); - }); - - describe('#getVersion', function () { - - it('using #promise', function (done) { - testPromise('getVersion', done); - }); - - it('using #callback', function (done) { - testCallback('getVersion', done); - }); - }); - }); - - - describe('description', function () { - - function validateDescription(desc) { - expect(desc).to.have.property('version'); - expect(desc.version).to.have.property('major', '1'); - expect(desc.version).to.have.property('minor', '0'); - - expect(desc).to.have.property('name').to.equal(`Philips hue (${testValues.host})`); - expect(desc).to.have.property('manufacturer').to.equal('Royal Philips Electronics'); - - expect(desc).to.have.property('model'); - expect(desc.model).to.have.property('name').to.equal('Philips hue bridge 2015'); - expect(desc.model).to.have.property('description').to.equal('Philips hue Personal Wireless Lighting'); - expect(desc.model).to.have.property('number'); - expect(desc.model).to.have.property('serial'); - - expect(desc).to.have.property('icons'); - expect(desc.icons).to.have.length(1); - } - - function testPromise(name, done) { - hue[name].call(hue) - .then(function (description) { - validateDescription(description); - done(); - }) - .done(); - } - - function testCallback(name, done) { - hue[name].apply(hue, [function (err, result) { - expect(err).to.be.null; - validateDescription(result); - done(); - }]); - } - - describe('#description()', function () { - - it('using #promise', function (done) { - testPromise('description', done); - }); - - it('using #callback', function (done) { - testCallback('description', done); - }); - }); - - describe('#getDescription()', function () { - - it('using #promise', function (done) { - testPromise('getDescription', done); - }); - - it('using #callback', function (done) { - testCallback('getDescription', done); - }); - }); - }); - - describe('full state', function () { - - function validateFullState(state) { - expect(state).to.have.property('lights'); - expect(state).to.have.property('groups'); - expect(state).to.have.property('config'); - expect(state).to.have.property('schedules'); - expect(state).to.have.property('scenes'); - expect(state).to.have.property('rules'); - expect(state).to.have.property('sensors'); - } - - function testCallback(fnName, done) { - hue[fnName].apply(hue, [(function (err, result) { - expect(err).to.be.null; - validateFullState(result); - done(); - })] - ); - } - - function testPromise(fnName, done) { - hue[fnName].call(hue) - .then(function (state) { - validateFullState(state); - done(); - }) - .done(); - } - - describe('#fullState()', function () { - - it('using #promise', function (done) { - testPromise('fullState', done); - }); - - it('using #callback', function (done) { - testCallback('fullState', done); - }); - }); - - describe('#getFullState()', function () { - - it('using #promise', function (done) { - testPromise('fullState', done); - }); - - it('using #callback', function (done) { - testCallback('getFullState', done); - }); - }); - }); -}); \ No newline at end of file diff --git a/test/getLightState-tests.js b/test/getLightState-tests.js deleted file mode 100644 index c572f63..0000000 --- a/test/getLightState-tests.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , HueApi = require('../').BridgeApi - , testValues = require('./support/testValues.js') -; - -describe('Hue API', function () { - - const hue = new HueApi(testValues.host, testValues.username); - - describe('#lightStatus', function () { - - describe('#promise', function () { - - it('should get status of light', function (done) { - function checkResults(results) { - _validateLightsResult(results, done); - } - - hue.lightStatus(1).then(checkResults).done(); - }); - }); - - - describe('#callback', function () { - - it('should get status of light', function (done) { - hue.lightStatus(1, function (err, results) { - if (err) { - throw err; - } - - _validateLightsResult(results, done); - }); - }); - }); - }); -}); - -function _validateLightsResult(results, cb) { - expect(results).to.have.property('type'); - expect(results).to.have.property('name'); - expect(results).to.have.property('modelid'); - expect(results).to.have.property('swversion'); - - expect(results).to.have.property('state'); - expect(results.state).to.have.property('on'); - expect(results.state).to.have.property('bri'); - expect(results.state).to.have.property('hue'); - expect(results.state).to.have.property('sat'); - expect(results.state).to.have.property('xy'); - expect(results.state).to.have.property('alert'); - expect(results.state).to.have.property('effect'); - expect(results.state).to.have.property('colormode'); - expect(results.state).to.have.property('reachable'); - - cb(); -} \ No newline at end of file diff --git a/test/groups-test.js b/test/groups-test.js deleted file mode 100644 index 2f8cb2a..0000000 --- a/test/groups-test.js +++ /dev/null @@ -1,459 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , HueApi = require('../').api - , testValues = require('./support/testValues.js') - , lightState = require('../').lightState -; - - -describe('Hue API', function () { - - // Set a maximum timeout for all the tests - this.timeout(5000); - - var hue = new HueApi(testValues.host, testValues.username); - - describe('group tests', function () { - - describe('#groups', function () { - - function validateAllGroups(results) { - expect(results).to.be.instanceOf(Array); - expect(results).to.have.length.greaterThan(0); - - // The first result should always be that of the all lights group - 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'); - - expect(results[1]).to.have.property('id'); - expect(results[1]).to.have.property('type'); - expect(results[1]).to.have.property('name'); - expect(results[1]).to.have.property('lights'); - expect(results[1]).to.have.property('action'); - } - - it('using #promise should retrieve all groups', async () => { - const allGroups = await hue.groups(); - validateAllGroups(allGroups); - }); - - it('using #callback should retrieve all groups', function (finished) { - hue.groups(function (err, results) { - expect(err).to.be.null; - validateAllGroups(results); - finished(); - }); - }); - }); - - describe('#luminaires', 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', 0); - expect(groupDetails).to.have.property('name', 'Lightset 0'); - - expect(groupDetails).to.have.property('lights').to.be.instanceOf(Array); - expect(groupDetails.lights).to.have.length.greaterThan(20); - } - - function failTest() { - throw new Error('Should not be called'); - } - - function validateGroupNotExist(id, finished) { - return function (err) { - const hueError = err.getHueError(); - expect(hueError).to.not.be.null; - - expect(hueError).to.have.property('type').to.equal(3); - expect(hueError).to.have.property('message').to.contain('resource'); - expect(hueError).to.have.property('message').to.contain('not available'); - expect(hueError).to.have.property('message').to.contain(id); - - if (finished) { - finished(); - } - }; - } - - describe('using #promise', function () { - - it('should obtain \'All Lights\' group details', function (finished) { - function validate(results) { - validateAllLightsResult(results); - finished(); - } - - hue.getGroup(0).then(validate).done(); - }); - - it('should fail for a group id that does not exist', function (finished) { - hue.getGroup(99) - .then(failTest) - .catch(validateGroupNotExist(99, finished)) - .done(); - }); - - it('should fail for group Id 999', function (finished) { - hue.getGroup(999) - .then(failTest) - .fail(validateGroupNotExist(999, finished)) - .done(); - }); - }); - - describe('using #callback', function () { - - it('should obtain \'All Lights\' group details', function (finished) { - hue.getGroup(0, function (err, results) { - expect(err).to.be.null; - validateAllLightsResult(results); - finished(); - }); - }); - - it('should fail for a group id that does not exist', function (finished) { - hue.getGroup(99, function (err, result) { - expect(result).to.be.null; - validateGroupNotExist(99, finished)(err); - }); - }); - - it('should fail for group Id 999', function (finished) { - hue.getGroup(999, function (err, result) { - expect(result).to.be.null; - validateGroupNotExist(999, finished)(err); - }); - }); - }); - }); - - - describe('#createGroup #deleteGroup', function () { - - describe('should createGroup a group and then delete it', function () { - - const groupName = 'NodejsApiTest' - , lightIds = [1, 2, 3] - ; - - function validateMissingGroup(err, createdGroupId) { - expect(err.message).to.contain('resource, /groups/' + createdGroupId); - expect(err.message).to.contain('not available'); - } - - it('using #promise', async () => { - - function deleteGroup(id) { - return hue.deleteGroup(id) - .then(deleted => { - expect(deleted).to.be.true; - - return hue.getGroup(id) - .then(function () { - throw new Error('Should not be called'); - }) - .fail(function (err) { - validateMissingGroup(err, id); - }); - }); - } - - - // A Round Robin trip through creating a group, validating it and then deleting it and checking for - // removal. This covers createGroup and delete API functions. - await hue.createGroup(groupName, lightIds) - .then(newGroup => { - expect(newGroup).to.have.property('id'); - const createdGroupId = newGroup.id; - - return hue.getGroup(newGroup.id) - .then(group => { - expect(group).to.have.property('id').to.equal(createdGroupId); - - // if a duplicate is found a " x" will be appended hence why this check is not equals - expect(group).to.have.property('name').to.have.string(groupName); - - expect(group).to.have.property('lights').to.be.instanceOf(Array); - expect(group.lights).to.have.members(lightIds); - - return group.id; - }); - }) - .then(deleteGroup) - .done(); - }); - - it('using #callback', function (done) { - hue.createGroup(groupName, lightIds, function (err, result) { - expect(err).to.be.null; - const createdGroupId = result.id; - - hue.deleteGroup(createdGroupId, function (err, result) { - expect(err).to.be.null; - expect(result).to.be.true; - - hue.getGroup(createdGroupId, function (err, res) { - validateMissingGroup(err, createdGroupId); - expect(res).to.be.null; - done(); - }); - }); - }); - }); - }); - }); - - - describe('#updateGroup', function () { - - const origName = 'UpdateTests' - , origLights = [1, 2] - , updateName = 'renamedGroupName' - , updateLights = [3, 4, 5] - ; - - let groupId - ; - - // Create a group to test on - beforeEach(async () => { - await hue.createGroup(origName, origLights) - .then(function (result) { - groupId = result.id; - }); - }); - - // Remove the created group after each test - afterEach(async () => { - await hue.deleteGroup(groupId); - }); - - 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(getGroup) - .then(validateGroup(updateName, origLights)) - .then(finished) - .done(); - }); - - it('should update only the lights in a group', function (finished) { - hue.updateGroup(groupId, updateLights) - .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(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; - - getGroup() - .then(validateGroup(updateName, origLights)) - .then(finished); - }); - }); - - 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; - - getGroup() - .then(validateGroup(origName, updateLights)) - .then(finished); - }); - }); - - 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; - - getGroup() - .then(validateGroup(updateName, updateLights)) - .then(finished); - }); - }); - - 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; - - getGroup() - .then(validateGroup(updateName, origLights)) - .then(finished) - }); - }); - }); - }); - - //TODO these tests need better validation around the body that is generated to be sent to the bridge - describe('#setGroupLightState', function () { - - it('using #promise', function (finished) { - hue.setGroupLightState(0, lightState.create().off()) - .then(function (result) { - expect(result).to.be.true; - finished(); - }) - .done(); - }); - - it('using #callback', function (finished) { - hue.setGroupLightState(0, lightState.create().off(), function (err, result) { - expect(err).to.be.null; - expect(result).to.be.true; - finished(); - }); - }); - }); - - describe('#updateGroup', function () { - - it('should fail on invalid group id', function (finished) { - const failIfCalled = function () { - throw new Error('The function call should have produced an error for invalid group id'); - }, - checkError = function (err) { - const hueError = err.getHueError(); - expect(hueError).to.have.property('type', 3); - expect(hueError.message).to.contain('not available'); - finished(); - }; - - hue.updateGroup(99, 'a name') - .then(failIfCalled) - .fail(checkError) - .done(); - }); - }); - - // describe('#_getGroupLightsByType', function () { - // - // it('should work for group 0', function (done) { - // hue._getGroupLightsByType(0) - // .then(function (map) { - // - // //TODO sort this test - // console.log(JSON.stringify(map, null, 2)); - // - // done(); - // }) - // .done(); - // }); - // }); - }); -}); \ No newline at end of file diff --git a/test/lights-tests.js b/test/lights-tests.js deleted file mode 100644 index c979f26..0000000 --- a/test/lights-tests.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , HueApi = require('../').api - , testValues = require('./support/testValues.js') -; - -describe('Hue API', function () { - - const hue = new HueApi(testValues.host, testValues.username); - - describe('#lights()', function () { - - describe('#promise', function () { - - it('should find some', function (done) { - hue.lights().then(_validateLightsResult(done)).done(); - }); - }); - - describe('#callback', function () { - - it('should find some lights', function (done) { - hue.lights(function (err, results) { - expect(err).to.be.null; - _validateLightsResult(done)(results); - }); - }); - }); - }); - - describe('#getLights()', function () { - - describe('#promise', function () { - - it('should find some', function (done) { - hue.lights().then(_validateLightsResult(done)).done(); - }); - }); - - describe('#callback', function () { - - it('should find some lights', function (done) { - hue.lights(function (err, results) { - expect(err).to.be.null; - _validateLightsResult(done)(results); - }); - }); - }); - }); -}); - -function _validateLightsResult(cb) { - return function (results) { - expect(results).to.have.property('lights'); - expect(results.lights).to.have.length.at.least(testValues.lightsCount); - _validateLight(results.lights[0]); - cb(); - }; -} - -function _validateLight(light) { - // console.log(JSON.stringify(light, null, 2)); - expect(light).to.have.property('id'); - expect(light).to.have.property('name'); - expect(light).to.have.property('modelid'); - expect(light).to.have.property('type'); - expect(light).to.have.property('swversion'); - expect(light).to.have.property('uniqueid'); - expect(light).to.have.property('manufacturername'); - expect(light).to.have.property('state'); -} \ No newline at end of file diff --git a/test/lightstate-tests.js b/test/lightstate-tests.js deleted file mode 100644 index 110ff0d..0000000 --- a/test/lightstate-tests.js +++ /dev/null @@ -1,274 +0,0 @@ -'use strict'; - -//TODO these tests need to be folded into the new LightState tests - -const expect = require('chai').expect - , lightState = require('..').lightState -; - -describe('#LightState', function () { - - let state; - - beforeEach(function () { - state = lightState.create(); - }); - - - describe('creation', function () { - - it('should getOperator an empty object', function () { - expect(state).to.exist; - expect(state.getPayload()).to.be.empty; - }); - }); - - - describe('alert', function () { - - describe('#alert', function () { - - it('should be a short when not specified', function () { - state.alert(); - validateAlertState('none'); - }); - - it('should be short when false', function () { - state.alert('select'); - validateAlertState('select'); - }); - - it('should be long when true', function () { - state.alert('lselect'); - validateAlertState('lselect'); - }); - }); - }); - - - describe('#white', function () { - - function test(temp, bright, expectedCt, expectedBri) { - state.white(temp, bright); - validateBriState(expectedBri); - validateCTState(expectedCt); - } - - it('should set ct=153, bri=50%', function () { - test(153, 50, 153, 127); - }); - - it('should set ct=500, bri=100%', function () { - test(500, 100, 500, 254); - }); - - // it('should set ct=0 to ct 153', function () { - // test(0, 0, 153, 0); - // }); - - // it('should set ct=600 to ct 500', function () { - // test(600, 0, 500, 0); - // }); - - // it('should set bri=-10% to bri 0%', function () { - // test(153, -10, 153, 0); - // }); - - // it('should set bri=150% to bri 100%', function () { - // test(153, 150, 153, 254); - // }); - }); - - - describe('#hsb', function () { - - function test(h, s, b, expectedHue, expectedSat, expectedBri) { - state.hsb(h, s, b); - validateHueState(expectedHue); - validateSatState(expectedSat); - validateBriState(expectedBri); - } - - it('should set (0, 0, 0)', function () { - test(0, 0, 0, 0, 0, 1); - }); - - it('should set (360, 100, 100)', function () { - test(360, 100, 100, 65535, 254, 254); - }); - - it('should set (180, 50, 25)', function () { - test(180, 50, 25, 32768, 127, 64); - }); - - //TODO validate limits on each parameter - }); - - - describe('#hsl', function () { - - function test(h, s, l, expectedHue, expectedSat, expectedBri) { - state.hsl(h, s, l); - validateHueState(expectedHue); - validateSatState(expectedSat); - validateBriState(expectedBri); - } - - it('should set (0, 0, 0)', function () { - test(0, 0, 0, 0, 0, 1); - }); - - it('should set (360, 100, 100)', function () { - test(360, 100, 100, 65535, 0, 254); - }); - - it('should set (180, 50, 25)', function () { - test(180, 50, 25, 32768, 170, 97); - }); - - //TODO validate limits on each parameter - }); - - - describe('#rgb', function () { - - function test(r, g, b, expectedRGB) { - state.rgb(r, g, b); - validateRGBState(expectedRGB); - } - - it('should set (255, 255, 255)', function () { - test(255, 255, 255, [255, 255, 255]); - }); - - it('should set (255, 255, 255)', function () { - test(255, 255, 255, [255, 255, 255]); - }); - - it('should set (0, 255, 0) to [0, 255, 0]', function () { - test(0, 255, 0, [0, 255, 0]); - }); - - it('should set via an array [r, g, b]', function () { - test([10, 20, 30], null, null, [10, 20, 30]); - }); - }); - - - describe('chaining states', function () { - - it('should chain on().ct(200)', function () { - state.on().ct(200); - - validateOnState(true); - validateCTState(200); - }); - - it('should chain on().off().off().on()', function () { - state.on().off().off().on(); - - validateOnState(true); - }); - - describe('using #reset', function () { - - it('set values, reset, then specify more values', function () { - state.on().hue(0); - validateOnState(true); - validateHueState(0); - - state.reset().ct(211); - validateCTState(211); - expect(state.getPayload()).to.not.have.property('on'); - expect(state.getPayload()).to.not.have.property('hue'); - - }); - }); - }); - - //TODO maybe need to look to make this pass - // describe('loading from values object', function () { - // - // it('should load {on: true, effect: \'colorloop\'}', function () { - // state = lightState.getOperator({on: true, effect: 'colorloop'}); - // - // validateStateProperties('on', 'effect'); - // validateEffectState('colorloop'); - // validateOnState(true); - // }); - // - // it('should only load valid values', function () { - // var data = { - // on: false, - // name: 'hello world', - // sat: 0, - // alert: 'none', - // scan: true - // }; - // - // state = lightState.getOperator(data); - // validateStateProperties('on', 'sat', 'alert'); - // validateOnState(false); - // validateSatState(0); - // validateAlertState('none'); - // }); - // - // it('should convert invalid property values', function () { - // state = lightState.getOperator({effect: 'disco'}); - // - // validateStateProperties('effect'); - // validateEffectState('none'); - // }); - // - // it('should load rgb', function () { - // state = lightState.getOperator({rgb: [0, 0, 255]}); - // validateRGBState([0, 0, 255]); - // }); - // }); - - function validateAlertState(expected) { - expect(state.getPayload()).to.have.property('alert', expected); - } - - function validateBriState(expected) { - expect(state.getPayload()).to.have.property('bri', expected); - } - - function validateHueState(expected) { - expect(state.getPayload()).to.have.property('hue', expected); - } - - function validateSatState(expected) { - expect(state.getPayload()).to.have.property('sat', expected); - } - - function validateCTState(expected) { - expect(state.getPayload()).to.have.property('ct', expected); - } - - function validateEffectState(expected) { - expect(state.getPayload()).to.have.property('effect', expected); - } - - function validateRGBState(expected) { - const payload = state.getPayload(); - - expect(payload).to.have.property('rgb'); - expect(payload.rgb).to.be.instanceOf(Array); - expect(payload.rgb).to.have.members(expected); - } - - function validateOnState(expected) { - expect(state.getPayload()).to.have.property('on', expected); - } - - function validateStateProperties(expected) { - const payload = state.getPayload() - , actualKeys = Object.keys(payload) - , expectedKeys = Array.prototype.slice.apply(arguments) - ; - - expect(actualKeys).to.have.members(expectedKeys); - expect(actualKeys).to.have.length(expectedKeys.length); - } -}); diff --git a/test/locateBridge-tests.js b/test/locateBridge-tests.js deleted file mode 100644 index 3718cbb..0000000 --- a/test/locateBridge-tests.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , hue = require('../') - , testValues = require('./support/testValues.js') -; - - -describe('Hue API', function () { - - describe('#discovery', function () { - - describe('#searchForBridges', function () { - this.timeout(8000); - - it('should find my bridge on the Network', function (done) { - hue.searchForBridges(testValues.locateTimeout).then(_validateBridgeResults(done)).done(); - }); - }); - - describe('#locateBridges', function () { - - it('should find my bridge on the Network using #promise', function (done) { - hue.locateBridges().then(_validateBridgeResults(done)).done(); - }); - - it('should find my bridge on the Network using #callback', function (done) { - hue.locateBridges(function (err, results) { - expect(err).to.be.null; - _validateBridgeResults(done)(results); - }); - }); - }); - }); -}); - -function _validateBridgeResults(finished) { - return function (results) { - expect(results).to.be.instanceOf(Array); - expect(results).to.have.length.at.least(1); - - expect(results[0]).to.have.property('ipaddress').to.equal(testValues.host); - - finished(); - }; -} \ No newline at end of file diff --git a/test/newLights-test.js b/test/newLights-test.js deleted file mode 100644 index 18345fe..0000000 --- a/test/newLights-test.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , HueApi = require('../').HueApi - , testValues = require('./support/testValues.js') -; - -describe('Hue API', function () { - - describe('#newLights', function () { - - describe('#promise', function () { - - it('should get new lights', function (done) { - var hue = new HueApi(testValues.host, testValues.username); - - function checkResults(results) { - _validateLightsResult(results, done); - } - - hue.newLights().then(checkResults); - }); - }); - - - describe('#callback', function () { - - it('should get new lights', function (done) { - var hue = new HueApi(testValues.host, testValues.username); - - hue.newLights(function (err, results) { - if (err) { - throw err; - } - - _validateLightsResult(results, done); - }); - }); - }); - }); -}); - -function _validateLightsResult(results, cb) { -// console.log(JSON.stringify(results)); - expect(results).to.exist; - expect(results).to.have.property('lastscan'); - // none, active, or timestamp - - cb(); -} \ No newline at end of file diff --git a/test/registeredUsers-test.js b/test/registeredUsers-test.js deleted file mode 100644 index c156bf0..0000000 --- a/test/registeredUsers-test.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -const HueApi = require('../').HueApi - , testValues = require('./support/testValues.js') - , expect = require('chai').expect -; - - -describe('Hue API', function () { - - - describe('#registeredUsers', function () { - - var hue = new HueApi(testValues.host, testValues.username); - - function validateUsers(results) { - console.log(JSON.stringify(results, null, 2)); - var testUser = null; - - expect(results).to.have.property('devices'); - expect(results.devices).to.be.instanceOf(Array); - - results.devices.forEach(function (user) { - expect(user).to.have.property('username'); - expect(user).to.have.property('name'); - expect(user).to.have.property('created'); - expect(user).to.have.property('accessed'); - - if (user.username === testValues.username) { - testUser = user; - } - }); - - expect(testUser).to.not.be.null; - } - - it('using #promise', function (finished) { - - hue.registeredUsers() - .then(validateUsers) - .then(function () { - finished(); - }) - .done(); - }); - - - it('using #callback', function (finished) { - hue.registeredUsers(function (err, results) { - expect(err).to.be.null; - validateUsers(results); - finished(); - }); - }); - }); - -}); \ No newline at end of file diff --git a/test/registration-tests.js b/test/registration-tests.js deleted file mode 100644 index 71c1b01..0000000 --- a/test/registration-tests.js +++ /dev/null @@ -1,82 +0,0 @@ -'use strict'; - -const Hue = require('../').api - , testValues = require('./support/testValues.js') - , expect = require('chai').expect -; - - -describe('Hue API', function () { - - // Set a maximum timeout for all the tests - this.timeout(5000); - - const hue = new Hue(testValues.host, testValues.username) - , disconnectedHue = new Hue(testValues.host); - - describe('#registerUser', function () { - - let createdUser = null; - - // Press the Link Button before running the tests to add the user - beforeEach('Press Bridge Link Button', async () => { - await hue.pressLinkButton(); - }); - - afterEach('Remove Existing User', async () => { - if (createdUser) { - console.log('Removing user: ' + createdUser); - await hue.deleteUser(createdUser); - createdUser = null; - } - }); - - - describe('should register a new user', function () { - - it('using #promise', function (finished) { - disconnectedHue.createUser('test_device') - .then(function (result) { - expect(result).to.exist; - createdUser = result; - finished(); - }) - .done(); - }); - - it('using #callback', function (finished) { - disconnectedHue.createUser('simple_user', function (err, result) { - expect(err).to.be.null; - - expect(result).to.exist; - createdUser = result; - finished(); - }); - }); - }); - - - describe('should register a user with no values provided', function () { - - it('using #promise', function (finished) { - disconnectedHue.createUser() - .then(function (result) { - expect(result).to.exist; - createdUser = result; - finished(); - }) - .done(); - }); - - it('using #callback', function (finished) { - disconnectedHue.registerUser(function (err, result) { - expect(err).to.be.null; - - expect(result).to.exist; - createdUser = result; - finished(); - }); - }); - }); - }); -}); \ No newline at end of file diff --git a/test/rgb-tests.js b/test/rgb-tests.js deleted file mode 100644 index 41f5b37..0000000 --- a/test/rgb-tests.js +++ /dev/null @@ -1,97 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , HueApi = require('../').HueApi - , lightState = require('../').lightState - , testValues = require('./support/testValues.js') -; - -describe('Hue API', function () { - - - describe('#setLightState to RGB', function () { - - const hue = new HueApi(testValues.host, testValues.username) - , hueLightId = testValues.hueLightId - ; - - //TODO do this for types A, B and C color gamut - describe('for Hue Bulb', function () { - - it('should set 255,0,0', function (done) { - const state = lightState.create() - .on() - .rgb(255, 0, 0); - - hue.setLightState(hueLightId, state) - .then(validateLightStateChange(hueLightId)) - .then(validateXY(done, 0.6484, 0.3309)) - .done(); - }); - - it('should set 255,255,255', function (done) { - const state = lightState - .create() - .on() - .rgb(255, 255, 255); - - hue.setLightState(hueLightId, state) - .then(validateLightStateChange(hueLightId)) - .then(validateXY(done, 0.3362, 0.3604)) - .done(); - }); - - it('should set 0,0,0', function (done) { - const state = lightState - .create() - .on() - .rgb(0, 0, 0); - - hue.setLightState(hueLightId, state) - .then(validateLightStateChange(hueLightId)) - .then(validateXY(done, 0.1532, 0.0475)) - .done(); - }); - }); - - - describe('for Lux Bulb', function () { - - const id = testValues.luxLightId; - - it('should fail when trying to set rgb', function (done) { - const state = lightState - .create() - .on() - .rgb(255, 0, 0); - - hue.setLightState(id, state) - .then(function () { - throw new Error('Lux should error on rgb/xy value'); - }).fail(function (err) { - expect(err.message).to.contain('Cannot set an RGB color on a light that does not support a Color Gamut'); - done(); - }) - .done(); - }); - }); - - function validateLightStateChange(id) { - return function (result) { - expect(result).to.be.true; - return hue.lightStatus(id); - }; - } - - function validateXY(done, x, y) { - return function (data) { - const ls = data.state; - expect(ls).to.have.property('on', true); - expect(ls).to.have.property('xy'); - expect(ls.xy[0]).to.equal(x); - expect(ls.xy[1]).to.equal(y); - done(); - }; - } - }); -}); \ No newline at end of file diff --git a/test/scene-object-test.js b/test/scene-object-test.js deleted file mode 100644 index 647f949..0000000 --- a/test/scene-object-test.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , Scene = require('..').scene - , testValues = require('./support/testValues') - ; - -describe('Scene', function () { - var scene; - - beforeEach(function () { - scene = Scene.create(); - }); - - describe('creation', function () { - - it('should instantiate from an object', function () { - var data = { - name: 'my scene', - lights: [1, 2] - } - , scene = Scene.create(data) - ; - - expect(scene).to.have.property('name', data.name); - expect(scene).to.have.property('lights').with.members(['1', '2']); - }); - }); - - describe('#withName()', function () { - - it('should set a name of \'node-scene\'', function () { - scene.withName('node-scene'); - - expect(scene).to.have.property('name', 'node-scene'); - }); - }); - - describe('#withLights', function () { - - it('should set light IDs from an array', function () { - var ids = [1, 2, 3]; - scene.withLights(ids); - - expect(scene).to.have.property('lights').with.members(['1', '2', '3']); - }); - - it('should set light IDs from an integer', function () { - scene.withLights(1); - expect(scene).to.have.property('lights').with.members(['1']); - }); - - it('should set the light IDs from multiple integers', function () { - scene.withLights(1, 2, 3); - expect(scene).to.have.property('lights').with.members(['1', '2', '3']); - }); - }); - - describe('#withTransitionTime', function () { - - it('should set a transition time value of 5000', function () { - scene.withTransitionTime(5000); - - expect(scene).to.have.property('transitiontime', 5000); - }); - }); - - describe('#withPicture()', function () { - - it('should set a picture', function () { - var pictureData = 'ABC123DEF456'; - scene.withPicture(pictureData); - - expect(scene).to.have.property('picture', pictureData); - }); - }); - - describe('#withAppData()', function () { - - it('should set data', function () { - var data = 'My App Data'; - scene.withAppData(data); - - expect(scene).to.have.property('appdata'); - expect(scene.appdata).to.have.property('data', data); - expect(scene.appdata).to.have.property('version', 1); - }); - }); - - describe('#withRecycle()', function() { - - it ('should set the recycle flag', function() { - scene.withRecycle(false); - - expect(scene).to.have.property('recycle'); - expect(scene).to.have.property('recycle').to.be.false; - }); - }); - - describe('with chained functions', function() { - - it('should createGroup a complex scene', function() { - var name = 'a new scene' - , pictureData = '1234556677A' - ; - - scene.withName(name) - .withLights(1) - .withPicture(pictureData); - - expect(scene).to.have.property('name', name); - expect(scene).to.have.property('lights').with.members(['1']); - expect(scene).to.have.property('picture', pictureData); - }); - }); -}); \ No newline at end of file diff --git a/test/scene-tests.js b/test/scene-tests.js deleted file mode 100644 index 914f3f0..0000000 --- a/test/scene-tests.js +++ /dev/null @@ -1,478 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , HueApi = require('..').HueApi - , hueScene = require('..').scene - , Scene = require('../lib/model/scenes/Scene') - , lightState = require('..').lightState - , testValues = require('./support/testValues.js') - ; - -describe('Hue API', function () { - - const hue = new HueApi(testValues.host, testValues.username); - - describe('get bridge scenes', function () { - - function validateScenesResult(scenes) { - let scene; - - expect(scenes).to.be.instanceOf(Array); - - scene = scenes[0]; - expect(scene).to.have.property('id'); - expect(scene).to.have.property('name'); - expect(scene).to.have.property('lights').to.be.instanceOf(Array); - expect(scene).to.have.property('owner'); - expect(scene).to.have.property('locked'); - expect(scene).to.have.property('lastupdated'); - expect(scene).to.have.property('picture'); - expect(scene).to.have.property('appdata'); - } - - async function testPromise(fnName) { - const scenes = await hue[fnName].call(hue); - validateScenesResult(scenes); - } - - function testCallback(fnName, done) { - hue[fnName].apply(hue, [function (err, result) { - expect(err).to.be.null; - validateScenesResult(result); - done(); - } - ]); - } - - describe('#scenes', function () { - - it('using #promise', async () => { - await testPromise('scenes'); - }); - - it('using #callback', function (done) { - testCallback('scenes', done); - }); - }); - - describe('#getScenes', function () { - - it('using #promise', async () => { - await testPromise('getScenes'); - }); - - it('using #callback', function (done) { - testCallback('getScenes', done); - }); - }); - }); - - //TODO need to test getting a scene, but one we create - // describe('get a scene', function () { - // - // describe('#scene()', function () { - // - // var sceneId = testValues.validScene.id; - // - // function validateResult(cb) { - // return function (result) { - // expect(result).to.exist; - // - // expect(result).to.have.property('id', sceneId); - // expect(result).to.have.property('name', testValues.validScene.name); - // expect(result).to.have.property('lights'); - // expect(result.lights).to.be.instanceOf(Array); - // - // cb(); - // }; - // } - // - // it('using #promise', function (done) { - // hue.scene(sceneId) - // .then(validateResult(done)) - // .done(); - // }); - // - // it('using #callback', function (done) { - // hue.scene(sceneId, function (err, result) { - // expect(err).to.be.null; - // - // validateResult(done)(result); - // }); - // }); - // }); - // - // describe('#getScene()', function () { - // var sceneId = testValues.validScene.id; - // - // function validateResult(cb) { - // return function (result) { - // expect(result).to.exist; - // - // expect(result).to.have.property('id', sceneId); - // expect(result).to.have.property('name', testValues.validScene.name); - // expect(result).to.have.property('lights'); - // expect(result.lights).to.be.instanceof(Array); - // expect(result.lights).to.have.members(testValues.validScene.lights); - // - // cb(); - // }; - // } - // - // it('using #promise', function (done) { - // hue.getScene(sceneId) - // .then(validateResult(done)) - // .done(); - // }); - // - // it('using #callback', function (done) { - // hue.getScene(sceneId, function (err, result) { - // expect(err).to.be.null; - // - // validateResult(done)(result); - // }); - // }); - // }); - // }); - - - describe('create scene', function () { - - describe('#createScene() with 1.2.x compatibility', function () { - - const name = 'node-hue-test-scene' - , lightIds = ['1', '2'] - ; - - describe('with name and lights', function () { - - function validateResult(cb) { - return function (result) { - expect(result).to.have.property('id'); - cb(); - }; - } - - it('using #promise', function (done) { - const scene = new Scene(); - scene.name = name; - scene.lights = lightIds; - - hue.createScene(scene) - .then(validateResult(done)) - .done(); - }); - - it('using #callback', function (done) { - const scene = new Scene(); - scene.name = name; - scene.lights = lightIds; - - hue.createScene(scene, function (err, result) { - expect(err).to.be.null; - validateResult(done)(result); - }); - }); - }); - }); - - describe('#createScene() with 1.11.x compatibility', function () { - - describe('with name, lights and transition', function () { - - const name = 'node-hue-scene-with-transition' - , lightIds = ['1'] - , transitionTime = 1000 - , scene = hueScene.create() - .withName(name) - .withLights(lightIds) - .withTransitionTime(transitionTime) - .getScene() - ; - - function validateScene(cb) { - return function (createdScene) { - expect(createdScene).to.have.property('name', name); - expect(createdScene).to.have.property('lights').with.members(lightIds); - expect(createdScene).to.have.property('version', 2); - - expect(createdScene).to.have.property('lightstates'); - expect(createdScene.lightstates).to.have.property(1); - expect(createdScene.lightstates[1]).to.have.property('transitiontime', transitionTime); - - cb(); - }; - } - - it('using #promise', function (done) { - hue.createScene(scene) - .then(function (createdScene) { - expect(createdScene).to.have.property('id'); - return hue.getScene(createdScene.id); - }) - .then(validateScene(done)) - .done(); - }); - - it('using #callback', function (done) { - hue.createScene(scene, function (err, result) { - expect(err).to.be.null; - expect(result).to.have.property('id'); - - hue.scene(result.id, function (err, createdScene) { - expect(err).to.be.null; - validateScene(done)(createdScene); - }); - }); - }); - }); - }); - }); - - - describe('modifying/updating scenes', function () { - - var originalName = 'nha-00' - , originalLightIds = [1, 2, 3] - , scene = hueScene.create().withName(originalName).withLights(originalLightIds).getScene() - , sceneId - ; - - beforeEach(function (done) { - hue.createScene(scene) - .then(function (createdScene) { - sceneId = createdScene.id; - done(); - }) - .done(); - }); - - afterEach(function(done) { - hue.deleteScene(sceneId) - .then(function() { - done(); - }) - .done(); - }); - - describe('#modifyScene()', function () { - - describe('update name', function () { - - var updateScene = hueScene.create({name: 'nha-01'}); - - function validateResult(cb) { - return function (result) { - expect(result).to.exist; - expect(result).to.have.property('name', true); - cb(); - }; - } - - it('using #promise', function (done) { - hue.updateScene(sceneId, updateScene) - .then(validateResult(done)) - .done(); - }); - - it('using #callback', function (done) { - hue.updateScene(sceneId, updateScene, function (err, result) { - expect(err).to.be.null; - validateResult(done)(result); - }); - }); - }); - - describe('update lights', function() { - - var updateScene = hueScene.create({lights: [4, 5, 6]}); - - function validateResult(cb) { - return function (result) { - expect(result).to.exist; - expect(result).to.have.property('lights', true); - cb(); - }; - } - - it('using #promise', function (done) { - hue.updateScene(sceneId, updateScene) - .then(validateResult(done)) - .done(); - }); - - it('using #callback', function (done) { - hue.updateScene(sceneId, updateScene, function (err, result) { - expect(err).to.be.null; - validateResult(done)(result); - }); - }); - }); - - //TODO this no longer functions as part of breaking changes to API - // describe('update scene states to \'current\'', function () { - // - // function validateResult(cb) { - // return function (result) { - // expect(result).to.exist; - // expect(result).to.have.property('storelightstate', true); - // cb(); - // }; - // } - // - // it('using #promise', function (done) { - // hue.updateScene(sceneId, null, true) - // .then(validateResult(done)) - // .done(); - // }); - // - // it('using #callback', function (done) { - // hue.updateScene(sceneId, {}, true, function (err, result) { - // expect(err).to.be.null; - // validateResult(done)(result); - // }); - // }); - // }); - - describe('update name and lights', function () { - - var updateScene = hueScene.create({name: 'nha-01', lights: [2]}); - - function validateResult(cb) { - return function (result) { - expect(result).to.exist; - expect(result).to.have.property('name', true); - expect(result).to.have.property('lights', true); - cb(); - }; - } - - it('using #promise', function (done) { - hue.updateScene(sceneId, updateScene) - .then(validateResult(done)) - .done(); - }); - - it('using #callback', function (done) { - hue.updateScene(sceneId, updateScene, function (err, result) { - expect(err).to.be.null; - validateResult(done)(result); - }); - }); - }); - }); - }); - - - describe('modifying scene light states', function () { - - describe('#modifySceneLightState()', function () { - - var originalName = 'nha-05' - , originalLightIds = [1, 2] - , lightId = 1 - , scene = hueScene.create().withName(originalName).withLights(originalLightIds).getScene() - , sceneId - ; - - beforeEach(function (done) { - hue.createScene(scene) - .then(function (createdScene) { - sceneId = createdScene.id; - done(); - }) - .done(); - }); - - afterEach(function(done) { - hue.deleteScene(sceneId) - .then(function() { - done(); - }) - .done(); - }); - - describe('update existing', function () { - - it('using #promise', function (done) { - var state = lightState - .create() - .on() - .hue(0); - - hue.modifySceneLightState(sceneId, lightId, state) - .then(function (results) { - expect(results).to.have.property('on', true); - expect(results).to.have.property('hue', true); - - done(); - }) - .done(); - }); - - it('using #callback', function (done) { - var state = lightState - .create() - .on() - .hue(10000); - - hue.setSceneLightState(sceneId, 2, state, function (err, results) { - expect(err).to.be.null; - - expect(results).to.have.property('on', true); - expect(results).to.have.property('hue', true); - - done(); - }); - }); - }); - }); - }); - - - describe('activating scenes', function () { - - describe('#activateScene()', function () { - - var scene = hueScene.create().withLights([1]).getScene() - , sceneId - ; - - before(function (done) { - hue.createScene(scene) - .then(function (createdScene) { - sceneId = createdScene.id; - done(); - }) - .done(); - }); - - after(function(done) { - hue.deleteScene(sceneId) - .then(function() { - done(); - }) - .done(); - }); - - it('using #promise', function (done) { - hue.activateScene(sceneId) - .then(function (result) { - expect(result).to.be.true; - done(); - }) - .done(); - }); - - it('using #callback', function (done) { - hue.activateScene(sceneId, function (err, result) { - expect(err).to.be.null; - expect(result).to.be.true; - done(); - }); - }); - }); - }); - - //TODO need more tests -}); \ No newline at end of file diff --git a/test/schedule-tests.js b/test/schedule-tests.js deleted file mode 100644 index eea0d46..0000000 --- a/test/schedule-tests.js +++ /dev/null @@ -1,296 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , hue = require('..') - , HueApi = hue.api - , scheduledEventBuilder = hue.scheduledEvent - , testValues = require('./support/testValues.js') - , AbsoluteTime = require('../lib/model/datetime/AbsoluteTime') -; - -describe('Hue API', function () { - - const hue = new HueApi(testValues.host, testValues.username) - , validCommand = { - 'address': `/api/${testValues.username}/lights/1/state`, - 'method': 'PUT', - 'body': { - 'on': true - } - } - ; - - function generateTestScheduleEvent() { - const time = new AbsoluteTime(); - time.fromDate(new Date(Date.now() + (1 * 60 * 1000))); - - return scheduledEventBuilder.create() - .withName('createTest') - .withCommand(validCommand) - .at(time) - .getSchedule(); - } - - - describe('scheduleTests', function () { - - describe('#getSchedules()', function () { - - describe('should list all scheduled events', function () { - - function validateSchedules(schedules) { - expect(schedules).to.be.an.instanceof(Array); - expect(schedules).to.have.length.at.least(1); - - expect(schedules[0]).to.have.property('id'); - expect(schedules[0]).to.have.property('name'); - expect(schedules[0]).to.have.property('localtime'); - expect(schedules[0]).to.have.property('command'); - expect(schedules[0]).to.have.property('created'); - } - - it('using #promise', async () => { - const schedules = await hue.getSchedules(); - validateSchedules(schedules); - }); - - it('using #callback', function (done) { - hue.getSchedules(function (err, res) { - expect(err).to.be.null; - validateSchedules(res); - done(); - }); - }); - }); - }); - - - describe('#scheduleEvent', function () { - - describe('should schedule a valid event', function () { - - let id = null - , event - ; - - beforeEach(() => { - event = generateTestScheduleEvent(); - }); - - afterEach(async () => { - if (id !== null) { - await hue.deleteSchedule(id); - } - }); - - it('using #promise', async () => { - const result = await hue.scheduleEvent(event); - expect(result).to.have.property('id'); - }); - - it('using #callback', function (done) { - hue.scheduleEvent(event, function (err, result) { - expect(err).to.be.null; - expect(result).to.have.property('id'); - done(); - }); - }); - }); - }); - - - describe('#deleteSchedule', function () { - - let existingScheduleId; - - beforeEach(async () => { - const schedule = await hue.createSchedule(generateTestScheduleEvent()); - existingScheduleId = schedule.id; - }); - - afterEach(async () => { - if (existingScheduleId) { - // In case the test failed in some weird way - try { - await hue.deleteSchedule(existingScheduleId); - } catch (err) { - // Ignore - } - } - }); - - describe('should remove existing schedule', function () { - - it('using #promise', async () => { - const result = await hue.deleteSchedule(existingScheduleId); - expect(result).to.be.true; - }); - - - it('using #callback', function (done) { - hue.deleteSchedule(existingScheduleId, function (err, result) { - expect(err).to.be.null; - expect(result).to.be.true; - done(); - }); - }); - }); - }); - - - describe('#updateSchedule', function () { - - let scheduleId; - - function validateUpdate(keys) { - return function (result) { - keys.forEach(function (key) { - expect(result).to.have.property(key).to.be.true; - }); - }; - } - - function validAlternativeCommand() { - return { - 'address': '/api/0/lights/1/state', - 'method': 'PUT', - 'body': { - 'on': false - } - }; - } - - beforeEach(async () => { - const schedule = await hue.createSchedule(generateTestScheduleEvent()); - scheduleId = schedule.id; - }); - - afterEach(async () => { - await hue.deleteSchedule(scheduleId); - }); - - - describe('using #promise', function () { - - it('should update an existing schedule name', async () => { - const result = await hue.updateSchedule(scheduleId, {'name': 'xxxxxxxxxxxxxxx'}); - validateUpdate(['name'])(result); - }); - - it('should update an existing schedule description', async () => { - const result = await hue.updateSchedule(scheduleId, - {'description': 'A new description value for an existing schedule on the Bridge'}); - validateUpdate(['description'])(result); - }); - - it('should update an existing schedule time', async () => { - const result = await hue.updateSchedule(scheduleId, {'localtime': '2020-04-10T07:42:13'}); - validateUpdate(['localtime'])(result); - }); - - it('should update an existing schedule command', async () => { - const result = await hue.updateSchedule(scheduleId, {'command': validAlternativeCommand()}); - validateUpdate(['command'])(result); - }); - - it('should update multiple values in an existing schedule', async () => { - const updates = { - 'name': 'New Name', - 'description': 'Does Something', - 'command': { - 'address': '/api/0/lights/invalid', - 'method': 'GET', - 'body': {} - } - } - , time = new AbsoluteTime() - ; - - time.value = new Date(Date.now() + (60 * 60 * 1000) + (5 * 60 * 1000)); - updates.localtime = time.toString(); - - const result = await hue.updateSchedule(scheduleId, updates); - validateUpdate(Object.keys(updates))(result); - }); - - it('should error when not updating any valid fields', async () => { - try { - await hue.updateSchedule(scheduleId, {'notName': '', 'desc': ''}); - expect.fail('Should have thrown an error'); - } catch (error) { - expect(error.message).to.contain('parameter, notName, not available'); - } - }); - }) - ; - - - // describe('using #callback', function () { - // - // this.timeout(5000); - // - // it('should update an existing schedule name', function (finished) { - // - // hue.updateSchedule(scheduleId, {'name': 'xxxxxxxxxxxxxxx'}, function (err, results) { - // expect(err).to.be.null; - // validateUpdate(['name'])(results); - // finished(); - // }); - // }); - // - // it('should update an existing schedule description', function (finished) { - // hue.updateSchedule(scheduleId, - // {'description': 'A new description value for an existing schedule on the Bridge'}, - // function (err, results) { - // expect(err).to.be.null; - // validateUpdate(['description'])(results); - // finished(); - // }); - // }); - // - // it('should update an existing schedule time', function (finished) { - // hue.updateSchedule(scheduleId, {'localtime': 'December 12, 2020, 12:01:33'}, function (err, results) { - // expect(err).to.be.null; - // validateUpdate(['localtime'])(results); - // finished(); - // }); - // }); - // - // it('should update an existing schedule command', function (finished) { - // hue.updateSchedule(scheduleId, {'command': validAlternativeCommand()}, function (err, results) { - // expect(err).to.be.null; - // validateUpdate(['command'])(results); - // finished(); - // }); - // }); - // - // it('should update multiple values in an existing schedule', function (finished) { - // var updates = { - // 'name': 'New Name', - // 'description': 'Does Something', - // 'localtime': Date.now() + (60 * 60 * 1000) + (5 * 60 * 1000), - // 'command': { - // 'address': '/api/0/lights/invalid', - // 'method': 'GET', - // 'body': {} - // } - // }; - // - // hue.updateSchedule(scheduleId, updates, function (err, results) { - // expect(err).to.be.null; - // validateUpdate(Object.keys(updates))(results); - // finished(); - // }); - // }); - // - // it('should error when not updating any valid fields', function (finished) { - // hue.updateSchedule(scheduleId, {'notName': '', 'desc': ''}, function (err, results) { - // expect(err.message).to.contain('No valid values for updating the schedule'); - // expect(results).to.be.null; - // finished(); - // }); - // }); - // }); - }); - }); -}); \ No newline at end of file diff --git a/test/scheduledEvent-tests.js b/test/scheduledEvent-tests.js deleted file mode 100644 index bf4d61f..0000000 --- a/test/scheduledEvent-tests.js +++ /dev/null @@ -1,236 +0,0 @@ -const expect = require('chai').expect - , schedule = require('..').scheduledEvent - , ApiError = require('..').ApiError - , testValues = require('./support/testValues') -; - -describe('ScheduleEvent', function () { - var scheduledEvent; - - beforeEach(function () { - scheduledEvent = schedule.create(); - }); - - describe('creation', function () { - - it('should createGroup an object', function () { - expect(scheduledEvent).to.exist; - // expect(scheduledEvent).to.be.empty; - }); - }); - - - describe('time value', function () { - - it('should accept valid string time value', function () { - let timeString = '2013-08-12T12:00:00'; - - scheduledEvent.on(timeString); - - const schedule = scheduledEvent.getSchedule(); - expect(schedule).to.have.property('localtime'); - - expect(schedule.localtime.toString()).to.equal(timeString); - - timeString = '2011-01-01T00:00:01'; - scheduledEvent.on(timeString); - - const updatedSchedule = scheduledEvent.getSchedule(); - expect(updatedSchedule).to.have.property('localtime'); - expect(updatedSchedule.localtime.toString()).to.equal(timeString); - }); - - it('should convert valid Date values from strings', function () { - var timeString = 'October 13, 1975 11:13:00'; //BST which is UTC+1 - - scheduledEvent.on(timeString); - expect(scheduledEvent).to.have.property('localtime').to.equal('1975-10-13T10:13:00'); - - timeString = 'Wed, 09 Aug 1995 00:00:00 GMT'; - scheduledEvent.on(timeString); - expect(scheduledEvent).to.have.property('localtime').to.equal('1995-08-09T00:00:00'); - }); - - it('should not accept invalid date strings', function () { - try { - scheduledEvent.on('1995-00:00T00:00:00'); - throw new Error('should have got a parsing error'); - } catch (error) { - if (error instanceof ApiError) { - expect(error.message).to.contain('Invalid time value'); - } else { - throw error; - } - } - }); - - it('should accept a valid Date instance', function () { - var time = new Date(); - - scheduledEvent.on(time); - expect(scheduledEvent).to.have.property('localtime').to.equal(time.toJSON().substr(0, 19)); - - time = new Date(2007, 12, 1, 12, 30, 31); - scheduledEvent.on(time); - expect(scheduledEvent).to.have.property('localtime').to.equal(time.toJSON().substr(0, 19)); - }); - - it('should accept a recurring time', function () { - var recurringTime = 'W122/T01:00:00'; - - scheduledEvent.atRecurringTime(recurringTime); - expect(scheduledEvent).to.have.property('localtime', recurringTime); - }); - }); - - - describe('withName()', function () { - - var maxNameLength = testValues.maxScheduleNameLength; - - it('should accept a name', function () { - scheduledEvent.withName('Simple Event'); - expect(scheduledEvent).to.have.property('name').to.equal('Simple Event'); - }); - - it('should shorten really long names and shorten it', function () { - var name = 'A really long name that is longer than the allowed ' + maxNameLength + ' characters for a name'; - scheduledEvent.withName(name); - expect(scheduledEvent).to.have.property('name').with.length(maxNameLength); - expect(scheduledEvent.name).to.equal(name.substr(0, maxNameLength)); - }); - }); - - - describe('withDescription()', function () { - - it('should accept a description', function () { - var descriptionValue = 'A description is a longer string value compared with name'; - - scheduledEvent.withDescription(descriptionValue); - expect(scheduledEvent).to.have.property('description').to.equal(descriptionValue); - }); - - it('should accept a really long description and shorten it', function () { - var descriptionValue = 'A description is a longer string value compared with name but this one is too ' + - 'long as it should only be 64 characters in total'; - - scheduledEvent.withDescription(descriptionValue); - expect(scheduledEvent).to.have.property('description').to.have.length(64); - expect(scheduledEvent.description).to.equal(descriptionValue.substr(0, 64)); - }); - }); - - - describe('withCommand()', function () { - - it('should take a command string', function () { - var commandValue = '{' + - ' "address": "/api/0/groups/1/action",' + - ' "method": "PUT",' + - ' "body": { "on": true }' + - '}'; - - scheduledEvent.withCommand(commandValue); - expect(scheduledEvent).to.have.property('command'); - _verifyCommandsMatch(scheduledEvent.command, commandValue); - }); - - it('should take a command object', function () { - var commandValue = { - 'address': '/api/1/groups/0/action', - 'method': 'PUT', - 'body': { - 'on': false - } - }; - - scheduledEvent.withCommand(commandValue); - expect(scheduledEvent).to.have.property('command'); - _verifyCommandsMatch(scheduledEvent.command, commandValue); - }); - }); - - - describe('withEnabledState()', function () { - - it('should set an enabled state', function () { - scheduledEvent.withEnabledState(true); - expect(scheduledEvent).to.have.property('status', 'enabled'); - }); - - it('should set a disabled state', function () { - scheduledEvent.withEnabledState(false); - expect(scheduledEvent).to.have.property('status', 'disabled'); - }); - }); - - - describe('createGroup() from object', function () { - - it('should load name and description values', function () { - var values = { - 'name': 'my object', - 'description': 'An object to populate a schedule', - 'ignore': 'a value to ignore' - }; - - scheduledEvent = schedule.create(values); - expect(scheduledEvent).to.have.property('name', values.name); - expect(scheduledEvent).to.have.property('description', values.description); - - expect(scheduledEvent.ignore).to.be.undefined; - - expect(scheduledEvent.time).to.be.undefined; - expect(scheduledEvent.command).to.be.undefined; - }); - - it('should load the time', function () { - var values = { - 'time': (new Date()).toJSON().substr(0, 19) - }; - - scheduledEvent = schedule.create(values); - expect(scheduledEvent).to.have.property('localtime').to.equal(values.time); - }); - - it('should load a command', function () { - var values = { - 'command': '{"address":"/api/a/path/goes/here","method":"PUT","body":{}}' - }; - - scheduledEvent = schedule.create(values); - expect(scheduledEvent).to.have.property('command'); - _verifyCommandsMatch(scheduledEvent.command, values.command); - }); - - - it('should load a status', function () { - var values = { - status: 'enabled' - }; - - scheduledEvent = schedule.create(values); - expect(scheduledEvent).to.have.property('status', 'enabled'); - }); - - //TODO test a completely formed object, name, description, command and time - }); -}); - -function _verifyCommandsMatch(scheduleCommand, expectedCommand) { - var convert = function (value) { - var result; - if (typeof (value) === 'string') { - result = JSON.parse(value); - } else { - result = value; - } - return result; - }, - - cmdScheduled = convert(scheduleCommand), - cmdExpected = convert(expectedCommand); - - expect(cmdScheduled).to.deep.equal(cmdExpected); -} \ No newline at end of file diff --git a/test/searchForLights-test.js b/test/searchForLights-test.js deleted file mode 100644 index cdd5cb2..0000000 --- a/test/searchForLights-test.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , HueApi = require('..').api - , testValues = require('./support/testValues.js') -; - -describe('Hue API', function () { - - const hue = new HueApi(testValues.host, testValues.username); - - describe('#searchForLights', function () { - - describe('#promise', function () { - - it('should get initiate a search', function (done) { - - function checkResults(results) { - _validateLightsResult(results, done); - } - - hue.searchForNewLights().then(checkResults).done(); - }); - }); - - - describe('#callback', function () { - - it('should initiate a search', function (done) { - - hue.searchForNewLights(function (err, results) { - if (err) { - throw err; - } - - _validateLightsResult(results, done); - }); - }); - }); - }); -}); - -function _validateLightsResult(result, cb) { - expect(result).to.be.true; - cb(); -} \ No newline at end of file diff --git a/test/sensor-tests.js b/test/sensor-tests.js deleted file mode 100644 index c39dc6c..0000000 --- a/test/sensor-tests.js +++ /dev/null @@ -1,24 +0,0 @@ -var expect = require("chai").expect - , HueApi = require("../").api - , testValues = require("./support/testValues.js") - ; - -describe("Hue API", function () { - - var hue = new HueApi(testValues.host, testValues.username); - - describe("#sensors()", function() { - - it("should obtain all the sensors in the bridge", function() { - return hue.sensors() - .then(function(results) { - expect(results.sensors).to.be.instanceOf(Array); - - expect(results.sensors[0]).to.have.property("id", 1); - expect(results.sensors[0]).to.have.property("name", "Daylight"); - expect(results.sensors[0]).to.have.property("type", "Daylight"); - expect(results.sensors[0]).to.have.property("manufacturername", "Philips"); - }); - }); - }); -}); \ No newline at end of file diff --git a/test/setLightName-tests.js b/test/setLightName-tests.js deleted file mode 100644 index 9e7262f..0000000 --- a/test/setLightName-tests.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -var expect = require('chai').expect - , HueApi = require('..').api - , testValues = require('./support/testValues.js') -; - -describe('Hue API', function () { - - const hue = new HueApi(testValues.host, testValues.username); - - - describe('#setLightName', function () { - - describe('#promise', function () { - - it('should set name', function (done) { - - function checkResults(results) { - _validateLightsResult(results, done); - } - - hue.setLightName(1, 'A New Name').then(checkResults).done(); - }); - }); - - describe('#callback', function () { - - it('should set name', function (done) { - - hue.setLightName(1, 'TV Right', function (err, results) { - if (err) { - throw err; - } - - _validateLightsResult(results, done); - }); - }); - }); - }); -}); - -function _validateLightsResult(result, cb) { - expect(result).to.be.true; - cb(); -} \ No newline at end of file diff --git a/test/setLightState-tests.js b/test/setLightState-tests.js deleted file mode 100644 index 845c0ca..0000000 --- a/test/setLightState-tests.js +++ /dev/null @@ -1,279 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , HueApi = require('..').HueApi - , lightState = require('..').lightState - , testValues = require('./support/testValues.js') -; - -describe('Hue API', function () { - - describe('#setLightState', function () { - - const hue = new HueApi(testValues.host, testValues.username) - , lightId = testValues.testLightId - ; - - let state; - - beforeEach(function () { - state = lightState.create(); //TODO refactor - }); - - - describe('turn light on', function () { - - it('using #promise', function (done) { - var checkResults = function (results) { - expect(results).to.be.true; - done(); - }; - - state.on(); - hue.setLightState(lightId, state).then(checkResults).done(); - }); - - it('using #callback', function (done) { - state.on(); - hue.setLightState(lightId, state, function (err, result) { - expect(err).to.be.null; - expect(result).to.be.true; - done(); - }); - }); - }); - - - describe('set alert state', function () { - - it('using #promise', function (done) { - var checkResults = function (results) { - expect(results).to.be.true; - done(); - }; - - state.alert(); - hue.setLightState(lightId, state).then(checkResults).done(); - }); - - it('using #callback', function (done) { - state.alert(); - hue.setLightState(lightId, state, function (err, result) { - expect(err).to.be.null; - expect(result).to.be.true; - done(); - }); - }); - }); - - describe('set brightness increment', function () { - - beforeEach(async () => { - const initialState = lightState.create().on().bri(200); - - await hue.setLightState(lightId, initialState) - .then(function (result) { - expect(result).to.be.true; - }); - }); - - async function test(value, expected) { - let initialBrightness; - - state.bri_inc(value); - - await hue.getLightStatus(lightId) - .then(initialState => { - initialBrightness = initialState.state.bri; - return hue.setLightState(lightId, state); - }) - .then(result => { - expect(result).to.be.true; - return hue.getLightStatus(lightId); - }) - .then(result => { - if (!expected) { - expected = initialBrightness + value; - } - console.log(`Initial: ${initialBrightness} incremented by ${value} resulted in ${result.state.bri}`); - expect(result.state.bri).to.equal(expected); - }); - } - - it('should increment by 1', async () => { - test(1); - }); - - it('should decrement by 20', async () => { - test(-20); - }); - - it('should decrement by 254', async () => { - test(-254, 0); - }); - - it('should increment by 500', async () => { - test(500, 254); - }); - }); - - describe('set multiple states', function () { - - it('using #promise', function (done) { - var checkResults = function (results) { - expect(results).to.be.true; - done(); - }; - - state.on().white(200, 100); - hue.setLightState(lightId, state).then(checkResults).done(); - }); - - it('using #callback', function (done) { - state.on().brightness(50); - hue.setLightState(lightId, state, function (err, result) { - expect(err).to.be.null; - expect(result).to.be.true; - done(); - }); - }); - }); - - describe('set hsb(0, 100, 100)', function () { - - it('using #promise', async () => { - const state = lightState.create().on().hsb(0, 100, 100) - , result = await hue.setLightState(lightId, state) - , stateResult = await hue.getLightStatus(lightId) - ; - - expect(result).to.be.true; - - expect(stateResult.state).to.have.property('hue', 0); - expect(stateResult.state).to.have.property('sat', 254); - expect(stateResult.state).to.have.property('bri', 254); - }); - }); - - describe('set hsl(0, 100, 100)', function () { - - it('using #promise', async () => { - const state = lightState.create().on().hsl(0, 100, 100) - , result = await hue.setLightState(lightId, state) - , stateResult = await hue.getLightStatus(lightId) - ; - - expect(result).to.be.true; - validateHSBState(0, 0, 254)(stateResult.state); - }); - }); - - describe('set hsl(0, 100, 50)', function () { - - it('using #promise', async () => { - const state = lightState.create().on().hsl(0, 100, 50) - , result = await hue.setLightState(lightId, state) - , stateResult = await hue.getLightStatus(lightId) - ; - - expect(result).to.be.true; - validateHSBState(0, 254, 254)(stateResult.state); - }); - - }); - - //TODO need to put this back in and cater for callbacks - // it("should report error on an invalid state", function (done) { - // function checkError(error) { - // // We should have a well defined error object - // - // expect(error.message).to.contain("invalid value"); - // expect(error.message).to.contain("parameter, bri"); - // - // expect(error.type).to.equal(7); - // - // expect(error.address).to.equal("/lights/2/state/bri"); - // - // expect(error.stack).to.not.be.empty; - // done(); - // } - // - // function failIfCalled() { - // assert.fail("Should not have been called"); - // } - // - // hue.setLightState(2, {"bri": 10000}).then(failIfCalled).fail(checkError).done(); - // }); - - describe('turn light off', function () { - - beforeEach(function (done) { - hue.setLightState(lightId, {on: true}) - .then(function () { - done(); - }) - .done(); - }); - - it('using #promise', function (done) { - var checkResults = function (results) { - expect(results).to.be.true; - done(); - }; - - state.off(); - hue.setLightState(lightId, state).then(checkResults).done(); - }); - - it('using #callback', function (done) { - state.off(); - hue.setLightState(lightId, state, function (err, result) { - expect(err).to.be.null; - expect(result).to.be.true; - done(); - }); - }); - }); - - //TODO turn this into a proper test that can validate the colours correctly - //describe("with single state shared across multiple lights", function() { - // - // it("should set on and xy values", function(done) { - // state.on().rgb(255, 0, 0); - // - // hue.setLightState(4, state) - // .then(function(){ - // return hue.setLightState(5, state); - // }) - // .then(function() { - // done(); - // }) - // .done(); - // }); - //}); - - //TODO complete the error checking - describe('using an invalid light id', function () { - - it('should fail with appropriate message', async () => { - const state = lightState.create().on().bri(100); - - await hue.setLightState(0, state) - .then(function () { - throw new Error('should not be called'); - }).catch(err => { - expect(err.message).to.contain('id:0'); - expect(err.message).to.contain('was not found'); - }); - }); - }); - }); - - function validateHSBState(hue, sat, bri) { - return function (state) { - expect(state).to.have.property('hue', hue); - expect(state).to.have.property('sat', sat); - expect(state).to.have.property('bri', bri); - }; - } -}); \ No newline at end of file From 915e68056ddfbc4c3c6c8f456da168cf589738b7 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 18 Nov 2019 21:11:59 +0000 Subject: [PATCH 03/35] 4.0.0-alpha --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8dded52..cb7774f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "node-hue-api", - "version": "3.4.1", + "version": "4.0.0-alpha", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index aacf006..e828c56 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-hue-api", "description": "Philips Hue API Library for Node.js", - "version": "3.4.1", + "version": "4.0.0-alpha", "author": "Peter Murray ", "contributors": [ { From efe89817e2a0e5890d245cfe95fb8dabde3684ec Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 18 Nov 2019 21:16:39 +0000 Subject: [PATCH 04/35] Adding typescript definition files into published package --- .npmignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..cad93dc --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +!*.d.ts \ No newline at end of file From e3c9e5e48cd4af7cc822b5b600411a7062307e9f Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 18 Nov 2019 21:16:51 +0000 Subject: [PATCH 05/35] 4.0.0-alpha-1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb7774f..36a4898 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "node-hue-api", - "version": "4.0.0-alpha", + "version": "4.0.0-alpha-1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e828c56..e060723 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-hue-api", "description": "Philips Hue API Library for Node.js", - "version": "4.0.0-alpha", + "version": "4.0.0-alpha-1", "author": "Peter Murray ", "contributors": [ { From 5556ef63228eaeeeb9e044c8dd750b2f57e18b30 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 18 Nov 2019 21:20:05 +0000 Subject: [PATCH 06/35] Excluding .idea directory from package --- .npmignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.npmignore b/.npmignore index cad93dc..8c9bb85 100644 --- a/.npmignore +++ b/.npmignore @@ -1 +1,2 @@ -!*.d.ts \ No newline at end of file +!*.d.ts +.idea \ No newline at end of file From 8d55ddd7505b1b61449396f39f5a0116e109f754 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 18 Nov 2019 21:20:25 +0000 Subject: [PATCH 07/35] 4.0.0-alpha-2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 36a4898..cdd3b0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "node-hue-api", - "version": "4.0.0-alpha-1", + "version": "4.0.0-alpha-2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e060723..2c75512 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-hue-api", "description": "Philips Hue API Library for Node.js", - "version": "4.0.0-alpha-1", + "version": "4.0.0-alpha-2", "author": "Peter Murray ", "contributors": [ { From cc6770a559ffd940af48d4ea2f4e559d55cc9190 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Tue, 19 Nov 2019 13:54:28 +0000 Subject: [PATCH 08/35] Removing v2 API --- hue-api/LightStateShim.js | 282 ---------- hue-api/SceneBuilder.js | 61 --- hue-api/ScheduledEventBuilder.js | 125 ----- hue-api/index.js | 871 ------------------------------- hue-api/meethue-agent.js | 99 ---- hue-api/shim.js | 171 ------ hue-api/utils.js | 62 --- 7 files changed, 1671 deletions(-) delete mode 100644 hue-api/LightStateShim.js delete mode 100644 hue-api/SceneBuilder.js delete mode 100644 hue-api/ScheduledEventBuilder.js delete mode 100644 hue-api/index.js delete mode 100644 hue-api/meethue-agent.js delete mode 100644 hue-api/shim.js delete mode 100644 hue-api/utils.js diff --git a/hue-api/LightStateShim.js b/hue-api/LightStateShim.js deleted file mode 100644 index f0a9cfc..0000000 --- a/hue-api/LightStateShim.js +++ /dev/null @@ -1,282 +0,0 @@ -'use strict'; - -const LightState = require('../lib/model/lightstate/LightState') -; - -module.exports = class LightStateShim { - - constructor (values) { - this.lightState = new LightState(); - if (values) { - this.lightState.populate(values); - } - } - - payload() { - return this.lightState.getPayload(); - } - - isLightState(obj) { - return obj && (obj instanceof LightStateShim || obj instanceof LightState); - } - - reset() { - this.lightState.reset(); - return this; - } - - clear() { - return this.reset(); - } - - copy() { - const copy = new LightStateShim(); - copy.lightState.populate(this.lightState.getPayload()); - return copy; - } - - strict() { - // THis has no meaning any more we are always strict with the contents of the payload - return this; - } - - isStrict() { - return true; - } - - on(on) { - this.lightState.on(on); - return this; - } - - bri(bri) { - this.lightState.bri(bri); - return this; - } - - hue(hue) { - this.lightState.hue(hue); - return this; - } - - sat(value) { - this.lightState.sat(value); - return this; - } - - xy(x, y) { - this.lightState.xy(x, y); - return this; - } - - ct(value) { - this.lightState.ct(value); - return this; - } - - alert(value) { - this.lightState.alert(value); - return this; - } - - effect(value) { - this.lightState.effect(value); - return this; - } - - transistiontime(value) { - this.lightState.transitiontime(value); - return this; - } - - bri_inc(value) { - this.lightState.bri_inc(value); - return this; - } - - sat_inc(value) { - this.lightState.sat_inc(value); - return this; - } - - hue_inc(value) { - this.lightState.hue_inc(value); - return this; - } - - ct_inc(value) { - this.lightState.ct_inc(value); - return this; - } - - xy_inc(value) { - this.lightState.xy_inc(value); - return this; - } - - - turnOn() { - return this.on(true); - } - - off() { - return this.on(false); - } - - turnOff() { - return this.off(); - } - - incrementBrightness(value) { - return this.bri_inc(value); - } - - colorTemperature(value) { - return this.ct(value); - } - - colourTemperature(value) { - return this.ct(value); - } - - colorTemp(value) { - return this.ct(value); - } - - colourTemp(value) { - return this.ct(value); - } - - incrementColorTemp(value) { - return this.ct_inc(value); - } - - incrementColorTemperature(value) { - return this.ct_inc(value); - } - - incrementColourTemp(value) { - return this.ct_inc(value); - } - - incrementColourTemperature(value) { - return this.ct_inc(value); - } - - incrementHue(value) { - return this.hue_inc(value); - } - - incrementXY(value) { - return this.xy_inc(value); - } - - incrementSaturation(value) { - return this.sat_inc(value); - } - - brightness(value) { - this.lightState.brightness(value); - return this; - } - - saturation(value) { - this.lightState.saturation(value); - return this; - } - - white(colorTemp, brightPercent) { - this.lightState.white(colorTemp, brightPercent); - return this; - } - - hsl(h, s, l) { - this.lightState.hsl(h, s, l); - return this; - } - - hsb(h, s, b) { - this.lightState.hsb(h, s, b); - return this; - } - - rgb(r, g, b) { - this.lightState.rgb(r, g, b); - return this; - } - - colorLoop() { - return this.effect('colorloop'); - } - - colourLoop() { - return this.colorLoop(); - } - - effectColorLoop() { - return this.colorLoop(); - } - - effectColourLoop() { - return this.colorLoop(); - } - - shortAlert() { - this.lightState.alertShort(); - return this; - } - - alertShort() { - this.lightState.alertShort(); - return this; - } - - longAlert() { - this.lightState.alertLong(); - return this; - } - - alertLong() { - this.lightState.alertLong(); - return this; - } - - transitionTime(value) { - return this.transistiontime(value); - } - - transition(value) { - this.lightState.transition(value); - return this; - } - - transitiontime_milliseconds(value) { - this.lightState.transition(value); - return this; - } - - transitionTime_milliseconds(value) { - this.lightState.transition(value); - return this; - } - - transitionSlow() { - this.lightState.transitionSlow(); - return this; - } - - transitionFast() { - this.lightState.transitionFast(); - return this; - } - - transitionInstant() { - this.lightState.transitionInstant(); - return this; - } - - transitionDefault() { - this.lightState.transitionDefault(); - return this; - } -} \ No newline at end of file diff --git a/hue-api/SceneBuilder.js b/hue-api/SceneBuilder.js deleted file mode 100644 index 502332c..0000000 --- a/hue-api/SceneBuilder.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -const Scene = require('../lib/model/scenes/Scene'); - - -const SceneBuilder = function() { - this._scene = new Scene(); -}; -module.exports = SceneBuilder; - -SceneBuilder.prototype.getScene = function() { - return this._scene; -}; - -SceneBuilder.prototype.withName = function (name) { - this._scene.name = name; - return this; -}; - -SceneBuilder.prototype.withLights = function (lightIds) { - let ids; - - if (Array.isArray(lightIds)) { - ids = lightIds; - } else { - ids = Array.prototype.slice.call(arguments); - } - - this._scene.lights = ids; - return this; -}; - -SceneBuilder.prototype.withTransitionTime = function (milliseconds) { - this._scene.transitiontime = milliseconds; - return this; -}; - -SceneBuilder.prototype.withAppData = function (data) { - let appData; - - if (data.version) { - appData = data; - } else { - appData = { - version: 1, - data: data - }; - } - this._scene.appdata = appData; - return this; -}; - -SceneBuilder.prototype.withPicture = function (picture) { - this._scene.picture = picture; - return this; -}; - -SceneBuilder.prototype.withRecycle = function (recycle) { - this._scene.recycle = recycle; - return this; -}; \ No newline at end of file diff --git a/hue-api/ScheduledEventBuilder.js b/hue-api/ScheduledEventBuilder.js deleted file mode 100644 index ff959d1..0000000 --- a/hue-api/ScheduledEventBuilder.js +++ /dev/null @@ -1,125 +0,0 @@ -'use strict'; - -//TODO this is a bit of a mess now and has invalid references, probably document to use new objects... - -const ApiError = require('../lib/ApiError') - , Schedule = require('../lib/model/Schedule') - , dateTime = require('../lib/model/datetime/index') - ; - - -module.exports = class ScheduledEventBuilder { - - constructor() { - this._schedule = new Schedule(); - } - - getSchedule() { - return this._schedule; - } - - withName(value) { - this._schedule.name = value; - return this; - } - - withDescription(value) { - this._schedule.description = value; - return this; - } - - withCommand(value) { - const type = typeof(value); - let commandObject = null; - - // The command is limited to 90 characters, so if a string is passed, convert it to an object and back into JSON. - if (type === 'string') { - commandObject = JSON.parse(value); - } else { - commandObject = value; - } - this._schedule.command = _validateCommand(commandObject); - return this; - } - - withEnabledState(value) { - this._schedule.status = value; - return this; - } - - at(value) { - this._schedule.localtime = dateTime.create(value); - return this; - } - - on(value) { - return this.at(value); - } - - when(value) { - return this.at(value); - } - - atRandomizedTime(value) { - throw new ApiError('Not implemented'); - } - - atRecurringTime(value) { - throw new ApiError('Not implemented'); - } - - atRecurringRandomizedTime(value) { - throw new ApiError('Not implemented'); - } -}; - - - - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// TODO move these elsewhere or remove entirely as they belong to other objects - -/** - * Perform validation on a command object to verify it can be considered valid. - * @param commandObject the object representing the command to be validated. - * @throws {ApiError} if a problem is encountered with the command - * @private - */ -function _validateCommand(commandObject) { - let address - , method - , body - ; - - if (commandObject) { - address = commandObject.address; - if (address) { - //TODO expand the regex to match proper end points valid for schedules - var addressPattern = /^\/api\/\.*\/\.*/; - if (addressPattern.test(address)) { - throw new ApiError('The \'address\' property must begin with \'/api\' to be a valid endpoint'); - } - } else { - throw new ApiError('The \'address\' property must be specified.'); - } - - //TODO link this with the valid endpoints in the address - method = commandObject.method; - if (!commandObject.method) { - throw new ApiError('The \'method\' must be specified.'); - } - - body = commandObject.body; - if (!commandObject.body) { - throw new ApiError('The \'body\' property must be specified.'); - } - } else { - throw new ApiError('Command is not defined'); - } - - return { - address: address, - method: method, - body: body - }; -} \ No newline at end of file diff --git a/hue-api/index.js b/hue-api/index.js deleted file mode 100644 index 9f376fd..0000000 --- a/hue-api/index.js +++ /dev/null @@ -1,871 +0,0 @@ -'use strict'; - -const utils = require('./utils') - , SceneBuilder = require('./SceneBuilder') - , ScheduleBuilder = require('./ScheduledEventBuilder') - , LightStateShim = require('./LightStateShim') - - // New API - , newApi = require('../lib/api/index') - , discovery = require('../lib/api/discovery') - , LightState = require('./LightStateShim') - , SceneLightState = require('../lib/model/lightstate/SceneLightState') -; - -function HueApi(config) { - const self = this; - self._config = config; - self._initializing = newApi.create(config.hostname, config.username, config.timeout, config.port) - .then(api => { - self._api = api; - self._initializing = null; - }); -} - -HueApi.prototype._getNewApi = function () { - const self = this; - - return new Promise((resolve) => { - if (self._initializing) { - self._initializing.then(() => { - resolve(self._api); - }); - } else { - resolve(self._api); - } - }); -}; - -module.exports = function (host, username, timeout, port) { - const config = { - hostname: host, - username: username, - }; - - if (timeout) { - config.timeout = timeout; - } - - if (port) { - config.port = port; - } - return new HueApi(config); -}; - - -/** - * 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. - */ -HueApi.prototype.getVersion = function (cb) { - //TODO this could be done via the cached state - const promise = this._getNewApi().then(api => { - return api.configuration.getAll() - .then(state => { - return { - name: state.config.name, - version: { - api: state.config.apiversion, - software: state.config.swversion - } - }; - }); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.version = HueApi.prototype.getVersion; - - -/** - * Loads the description for the Philips Hue. - * - * @param cb An optional callback function if you don't want to be informed via a promise. - * @return {Q.promise} A promise that will be provided with a description object, or {null} if a callback was provided. - */ -HueApi.prototype.description = function (cb) { - const promise = discovery.description(this._config.hostname); - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getDescription = HueApi.prototype.description; - - -/** - * Reads the bridge configuration and returns it as a JSON object. - * - * @param cb An optional callback function to use if you do not want to use the promise for results. - * @return {Q.promise} A promise with the result, or if a callback function was provided. - */ -HueApi.prototype.config = function (cb) { - const promise = this._getNewApi().then(api => { - return api.configuration.get(); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getConfig = HueApi.prototype.config; - - -/** - * Obtains the complete state for the Bridge. This is considered to be a very expensive operation and should not be invoked - * frequently. The results detail all config, users, groups, schedules and lights for the system. - * - * @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) { - const promise = this._getNewApi().then(api => { - return api.configuration.getAll(); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.fullState = HueApi.prototype.getFullState; - - -/** - * Allows a new user/device to be registered with the Philips Hue Bridge. This will return the name of the user that was - * created by the function call. - * - * This function does not require the HueApi to have been initialized with a host or username. It does however require - * the end user to have pressed the link button on the bridge, before invoking this function. - * - * @param deviceName The name of the device (human readable) limited to 19 characters. - * @param cb An optional callback function to use if you do not want a promise returned. - * @return {Q.promise} A promise with the result, or if a callback was provided. - */ -HueApi.prototype.registerUser = function (deviceDescription, cb) { - let deviceName = null; - - if (utils.isFunction(deviceDescription)) { - cb = deviceDescription; - } else { - deviceName = deviceDescription; - } - - if (!deviceName) { - deviceName = 'app'; - } - - const promise = this._getNewApi().then(api => { - return api.configuration.createUser('node_hue_api', deviceName, false); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.createUser = HueApi.prototype.registerUser; - - -/** - * Presses the Link Button on the Bridge (without the user actually having to do it). If successful then {true} will be - * returned as the result. - * - * @param cb An optional callback function to use if you do not want to use the promise returned. - * @return {Q.promise} A promise with the result, or if a callback was provided. - */ -HueApi.prototype.pressLinkButton = function (cb) { - const promise = this._getNewApi().then(api => { - return api.configuration.pressLinkButton(); - }); - - return utils.promiseOrCallback(promise, cb); -}; - - -/** - * Deletes an existing user from the Phillips Hue Bridge. - * - * @param username The username of the user to delete. - * @param cb An optional callback function to use if you do not want to get the result via a promise chain. - * @returns {Q.promise} A promise with the result of the deletion, or if a callback was provided. - */ -HueApi.prototype.deleteUser = function (username, cb) { - const promise = this._getNewApi().then(api => { - return api.configuration.deleteUser(username); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.unregisterUser = HueApi.prototype.deleteUser; - - -/** - * Obtain a list of registered "users" or "devices" that can interact with the Philips Hue. - * - * @param cb An optional callback function if you do not want to use the promise to obtain the results. - * @return A promise that will provide the results of registered users, or if a callback was provided. - */ -HueApi.prototype.registeredUsers = function (cb) { - function processUsers(result) { - const list = result.whitelist, - devices = []; - - if (list) { - Object.keys(list).forEach(function (key) { - let device; - if (list.hasOwnProperty(key)) { - device = list[key]; - devices.push( - { - 'name': device.name, - 'username': key, - 'created': device['createGroup date'], - 'accessed': device['last use date'] - } - ); - } - }); - } - return {'devices': devices}; - } - - const promise = this.config().then(processUsers); - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getRegisteredUsers = HueApi.prototype.registeredUsers; - - -/** - * Obtains the details of the individual sensors that are attached to the Philips Hue. - * - * @param cb An optional callback function to use if you do not want a promise returned. - * @return A promise that will be provided with the lights object, or {null} if a callback function was provided. - */ -HueApi.prototype.sensors = function (cb) { - const promise = this._getNewApi().then(api => { - return api.sensors.getAll().then(sensors => {return {sensors: sensors};}); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getSensors = HueApi.prototype.sensors; - -/** - * Obtains the details of the individual lights that are attached to the Philips Hue. - * - * @param cb An optional callback function to use if you do not want a promise returned. - * @return A promise that will be provided with the lights object, or {null} if a callback function was provided. - */ -HueApi.prototype.lights = function (cb) { - function generateResponseData(data) { - const result = []; - - data.forEach(light => { - result.push(Object.assign({id: light.id}, light.bridgeData)); - }); - - return {lights: result}; - } - - const promise = this._getNewApi().then(api => { - return api.lights.getAll(); - }).then(generateResponseData); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getLights = HueApi.prototype.lights; - - -/** - * Obtains the status of the specified light. - * - * @param id The id of the light as an integer, this value will be parsed into an integer value so can be a {String} or - * {Number} value. - * @param cb An optional callback function to use if you do not want a promise returned. - * @return A promise that will be provided with the light status, or {null} if a callback function was provided. - */ -HueApi.prototype.lightStatus = function (id, cb) { - const promise = this._getNewApi().then(api => { - return api.lights.getLightAttributesAndState(id); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getLightStatus = HueApi.prototype.lightStatus; - - -/** - * Obtains the new lights found by the bridge, dependant upon the last search. - * - * @param cb An optional callback function to use if you do not want a promise returned. - * @return A promise that will be provided with the new lights search result, or {null} if a callback function was provided. - */ -HueApi.prototype.newLights = function (cb) { - const promise = this._getNewApi() - .then(api => { - return api.lights.getNew(); - }); - - return utils.nativePromiseOrCallback(promise, cb); -}; -HueApi.prototype.getNewLights = HueApi.prototype.newLights; - -/** - * Starts a search for new lights. - * - * @param cb An optional callback function to use if you do not want a promise returned. - * @return A promise that will be provided with the new lights, or {null} if a callback function was provided. - */ -HueApi.prototype.searchForNewLights = function (cb) { - const promise = this._getNewApi().then(api => { - return api.lights.searchForNew(); - }); - - return utils.promiseOrCallback(promise, cb); -}; - - -/** - * Sets the name of a light on the Bridge. - * - * @param id The ID of the light to set the name for. - * @param name The name to apply to the light. - * @param cb An optional callback function to use if you do not want a promise returned. - * @return A promise that will be provided with the results of setting the name, or {null} if a callback function was provided. - */ -HueApi.prototype.setLightName = function (id, name, cb) { - const promise = this._getNewApi().then(api => { - return api.lights.rename(id, name); - }); - - return utils.promiseOrCallback(promise, cb); -}; - - -/** - * Sets the light state to the provided values. - * - * @param id The id of the light which is an integer or a value that can be parsed into an integer value. - * @param stateValues {Object} containing the properties and values to set on the light. - * @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. - */ -HueApi.prototype.setLightState = function (id, stateValues, cb) { - const self = this, - promise = self._getNewApi().then(api => { - const lightState = _getNewLightState(id, stateValues); - return api.lights.setLightState(id, lightState); - }); - - return utils.promiseOrCallback(promise, cb); -}; - - -/** - * Sets the light state to the provided values for an entire group. - * - * @param id The id of the group which is an integer or a value that can be parsed into an integer value. - * @param stateValues {Object} containing the properties and values to set on the light. - * @param cb An optional callback function to use if you do not want to use a promise for the results. - * @return {Q.promise} A promise that will set the specified state on the group, or {null} if a callback was provided. - */ -HueApi.prototype.setGroupLightState = function (id, stateValues, cb) { - const promise = this._getNewApi() - .then(api => { - return api.groups.setGroupState(id, stateValues); - }); - - return utils.promiseOrCallback(promise, cb); -}; - -/** - * Obtains the details of the individual sensors that are attached to the Philips Hue. - * - * @param cb An optional callback function to use if you do not want a promise returned. - * @return A promise that will be provided with the lights object, or {null} if a callback function was provided. - */ -HueApi.prototype.sensors = function (cb) { - var options = this._defaultOptions(), - promise; - - promise = http.invoke(sensorsApi.getAllSensors, options); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getSensors = HueApi.prototype.sensors; - - -/** - * Obtains the status of the specified sensor. - * - * @param id The id of the sensor as an integer, this value will be parsed into an integer value so can be a {String} or - * {Number} value. - * @param cb An optional callback function to use if you do not want a promise returned. - * @return A promise that will be provided with the sensor status, or {null} if a callback function was provided. - */ -HueApi.prototype.sensorStatus = function (id, cb) { - var options = this._defaultOptions(), - promise; - - promise = _setSensorIdOption(options, id); - - if (!promise) { - promise = http.invoke(sensorsApi.getSensorAttributesAndState, options); - } - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getSensorStatus = HueApi.prototype.sensorStatus; - -/** - * Sets the name of a sensor on the Bridge. - * - * @param id The ID of the sensor to set the name for. - * @param name The name to apply to the sensor. - * @param cb An optional callback function to use if you do not want a promise returned. - * @return A promise that will be provided with the results of setting the name, or {null} if a callback function was provided. - */ -HueApi.prototype.setSensorName = function (id, name, cb) { - var options = this._defaultOptions(), - promise; - - promise = _setSensorIdOption(options, id); - - options.values = { - "name": name - }; - - if (!promise) { - promise = http.invoke(sensorsApi.renameSensor, options); - } - return utils.promiseOrCallback(promise, 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 obtain the groups, or {null} if a callback was provided. - */ -HueApi.prototype.groups = function (cb) { - const promise = this._getNewApi() - .then(api => { - return api.groups.getAll(); - }); - - return utils.nativePromiseOrCallback(promise, cb); -}; -HueApi.prototype.getGroups = HueApi.prototype.groups; -HueApi.prototype.getAllGroups = HueApi.prototype.groups; - - -/** - * Obtains all the Luminaires 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 luminaires, or {null} if a callback was provided. - */ -HueApi.prototype.luminaires = function (cb) { - const promise = this._getNewApi().then(api => { - return api.groups.getLuminaires(); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getLuminaires = HueApi.prototype.luminaires; - - -/** - * 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) { - const promise = this._getNewApi().then(api => { - return api.groups.getLightSources(); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getLightSources = HueApi.prototype.lightSources; - - -/** - * 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) { - const promise = this._getNewApi().then(api => { - return api.groups.getLightGroups(); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getLightGroups = HueApi.prototype.lightGroups; - - -/** - * Obtains the details for a specified group in a format of {id: {*}, name: {*}, lights: [], lastAction: {*}}. - * - * @param id {Number} or {String} which is the id of the group to get the details for. - * @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. - */ -HueApi.prototype.getGroup = function (id, cb) { - const promise = this._getNewApi() - .then(api => { - return api.groups.get(id); - }); - - return utils.nativePromiseOrCallback(promise, 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) { - // var result = { - // id: String(id), - // name: group.name, - // type: group.type, - // lights: group.lights, - // lastAction: group.action - // }; - - // if (group.type === 'Luminaire' && group.modelid) { - // result.modelid = group.modelid; - // } - - // return result; - // } -}; -HueApi.prototype.group = HueApi.prototype.getGroup; - - -/** - * Updates a light group to the specified name and/or lights ids. The name and light ids can be specified independently or - * together when calling this function. - * - * @param id The id of the group to update the name and/or light ids associated with it. - * @param name {String} The name of the group - * @param lightIds {Array} An array of light ids to be assigned to the group. If any of the ids are not present in the - * bridge the creation will fail with an error being produced. - * @param cb An optional callback function to use if you do not want to use a promise for the results. - * @return A promise with a result of if the update was successful, or null if a callback was provided. - */ -HueApi.prototype.updateGroup = function (id, name, lightIds, cb) { - // Due to name and lightIds being "optional" we have to re-parse the arguments to get the right ones - const parameters = [].slice.call(arguments, 1) - , payload = {} - ; - - parameters.forEach(function (param) { - if (param instanceof Function) { - cb = param; - } else if (Array.isArray(param)) { - payload.lights = utils.createStringValueArray(param); - } else if (param === undefined || param === null) { - // Ignore it - } else { - payload.name = param; - } - }); - - const promise = this._getNewApi() - .then(api => { - return api.groups.update(id, payload); - }); - - return utils.nativePromiseOrCallback(promise, cb); -}; - - -/** - * Creates a new light Group. - * - * @param name The name of the group that we are creating, limited to 16 characters. - * @param lightIds {Array} of ids for the lights to be included in the group. - * @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 return the id of the group that was created, or null if a callback was provided. - */ -HueApi.prototype.createGroup = function (name, lightIds, cb) { - const promise = this._getNewApi() - .then(api => { - return api.groups.createGroup(name, lightIds); - }); - - return utils.nativePromiseOrCallback(promise, cb); -}; - - -/** - * Deletes a group with the specified id, returning if the action was successful. - * - * @param id The id of the group to delete. - * @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 return if the deletion was successful, or null if a callback was provided. - */ -HueApi.prototype.deleteGroup = function (id, cb) { - const promise = this._getNewApi() - .then(api => { - return api.groups.deleteGroup(id); - }); - - return utils.nativePromiseOrCallback(promise, cb); -}; - - -/** - * Gets the schedules on the Bridge, as an array of {"id": {String}, "name": {String}} 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 return the results or if a callback was provided. - */ -HueApi.prototype.schedules = function (cb) { - const promise = this._getNewApi().then(api => { - return api.schedules.getAll(); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getSchedules = HueApi.prototype.schedules; - - -/** - * Gets the specified schedule by id, which is in an identical format the the Hue API documentation, with the addition - * of an "id" value for the schedule. - * - * @param id The id of the schedule to retrieve. - * @param cb An optional callback function to use if you do not want to use a promise for the results. - * @returns A promise that will return the results or if a callback was provided. - */ -HueApi.prototype.getSchedule = function (id, cb) { - const promise = this._getNewApi().then(api => { - return api.schedules.get(id); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.schedule = HueApi.prototype.getSchedule; - - -/** - * Creates a one time scheduled event. The results from this function is the id of the created schedule. The bridge only - * supports 100 schedules, so once they are triggered, they are removed from the bridge. - * - * @param schedule {ScheduledEvent} - * @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 return the id value of the schedule that was created, or if a callback was provided. - */ -HueApi.prototype.scheduleEvent = function (schedule, cb) { - const promise = this._getNewApi().then(api => { - - - - return api.schedules.createSchedule(schedule); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.createSchedule = HueApi.prototype.scheduleEvent; - - -/** - * Deletes a schedule by id, returning {true} if the deletion was successful. - * - * @param id of the schedule - * @param cb An option callback function to use if you do not want to use a promise for the results. - * @return {Q.promise} A promise that will return the result of the deletion, or if a callback was provided. - */ -HueApi.prototype.deleteSchedule = function (id, cb) { - const promise = this._getNewApi().then(api => { - return api.schedules.deleteSchedule(id); - }); - - return utils.promiseOrCallback(promise, cb); -}; - - -/** - * Updates an existing schedule event with the provided details. - * - * @param id The id of the schedule being updated. - * @param schedule The object containing the details to update for the existing schedule event. - * @param cb An optional callback function to use if you do not want to deal with a promise for the results. - * @return {Q.promise} A promise that will return the result, or if a callback was provided. - */ -HueApi.prototype.updateSchedule = function (id, schedule, cb) { - const promise = this._getNewApi().then(api => { - return api.schedules.update(id, schedule); - }); - - return utils.promiseOrCallback(promise, cb); -}; - - -/** - * Gets the scenes on the Bridge, as an array of {"id": {String}, "name": {String}, "lights": {Array}, "active": {Boolean}} - * 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 return the results or if a callback was provided. - */ -HueApi.prototype.scenes = function (cb) { - const promise = this._getNewApi().then(api => { - return api.scenes.getAll(); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getScenes = HueApi.prototype.scenes; - - -/** - * Obtains a scene by a given id. - * @param sceneId {String} The id of the scene to obtain. - * @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 return the scene or if a callback was provided. - */ -HueApi.prototype.scene = function (sceneId, cb) { - const promise = this._getNewApi().then(api => { - return api.scenes.get(sceneId); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.getScene = HueApi.prototype.scene; - -/** - * Deletes a Scene (that is stored inside the bridge, not in the lights). - * @param sceneId The ID for the scene to delete - * @param cb An optional callback function to use if you do not want to use a promise for the results. - * @returns {*} A promise that will return the result from deleting the scene or null if a callback was provided. - */ -HueApi.prototype.deleteScene = function (sceneId, cb) { - const promise = this._getNewApi().then(api => { - return api.scenes.deleteScene(sceneId); - }); - - return utils.promiseOrCallback(promise, cb); -}; - -/** - * Creates a new Scene. - * When the scene is created, it will store the current state of the lights and will use those "current" settings - * when the scene is recalled/activated later. - ** - * @param scene {Scene} The scene to be created. - * - * @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 return the id of the scene that was created (as well as the values that make up the scene), - * or null if a callback was provided. - */ -HueApi.prototype.createScene = function (scene, cb) { - const promise = this._getNewApi().then(api => { - - if (scene instanceof SceneBuilder) { - scene = scene.getScene(); - } - - return api.scenes.createScene(scene); - }); - - return utils.promiseOrCallback(promise, cb); -}; - - -/** - * Update the lights and/or name associated with a scene (or will createGroup a new one if the - * sceneId is not present in the bridge). - * - * @param sceneId {String} The id for the scene in the bridge - * @param scene {Scene} The update scene to use to set update the existing one with - * - * @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 return the id of the scene that was updated and the light ids that are now set, - * or null if a callback was provided. - */ -HueApi.prototype.updateScene = function (sceneId, scene, cb) { - const promise = this._getNewApi().then(api => { - - if (scene instanceof SceneBuilder) { - scene = scene.getScene(); - } - - return api.scenes.update(sceneId, scene); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.modifyScene = HueApi.prototype.updateScene; - -/** - * Modifies the light state of one of the lights in a scene. - * - * @param sceneId The scene id, which if it does not exist a new scene will be created. - * @param lightId integer The id of light that is having the state values set. - * @param stateValues {Object} containing the properties and values to set on the light. - * - * @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 return the state values on the light, or {null} if a callback was provided. - */ -HueApi.prototype.setSceneLightState = function (sceneId, lightId, stateValues, cb) { - const promise = this._getNewApi().then(api => { - const sceneLightState = new SceneLightState(); - - if (stateValues instanceof LightState) { - sceneLightState.populate(stateValues.getPayload()); - } else { - sceneLightState.populate(stateValues); - } - return api.scenes.updateLightState(sceneId, lightId, sceneLightState); - }); - - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.updateSceneLightState = HueApi.prototype.setSceneLightState; -HueApi.prototype.modifySceneLightState = HueApi.prototype.setSceneLightState; - - -/** - * Helper-function that recalls a scene for a group using setGroupLightState. Reason for existence is simplicity for - * user. - * - * @param sceneId The id of the scene to activate, which is an integer or a value that can be parsed into an integer value. - * @param groupIdFilter An optional group filter to apply to the scene, to select a sub set of the lights in the scene. This can - * be {null} or {undefined} to not apply a filter. - * @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 activate the scene, or {null} if a callback was provided. - */ -HueApi.prototype.activateScene = function (sceneId, groupIdFilter, cb) { - var promise; - - if (utils.isFunction(groupIdFilter)) { - cb = groupIdFilter; - groupIdFilter = null; - } - - try { - groupIdFilter = Number(groupIdFilter, 10); - if (isNaN(groupIdFilter)) { - groupIdFilter = 0; - } - } catch (err) { - groupIdFilter = 0; - } - - promise = this.setGroupLightState(groupIdFilter, {scene: sceneId}); - return utils.promiseOrCallback(promise, cb); -}; -HueApi.prototype.recallScene = HueApi.prototype.activateScene; - - - -//////////////////////////////////////////////////////////////////////////////////////////////// -// PRIVATE FUNCTIONS -//////////////////////////////////////////////////////////////////////////////////////////////// - - -// TODO this is just a transition function until we deprecate the API -function _getNewLightState(lightId, stateValues) { - if (stateValues instanceof LightStateShim) { - return stateValues.lightState; - } else if (stateValues instanceof LightState) { - return stateValues; - } else { - const newLightState = new LightState(); - newLightState.populate(stateValues); - return newLightState; - } -} diff --git a/hue-api/meethue-agent.js b/hue-api/meethue-agent.js deleted file mode 100644 index 3ebc791..0000000 --- a/hue-api/meethue-agent.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -const Agent = require('https').Agent -; - -// Obtains an HTTPS Agent that will accept the discovery.meethue.com TLS certificate -module.exports.getAgent = () => { - const discoveryAgent = new Agent(); - - discoveryAgent.options.ca = '-----BEGIN CERTIFICATE-----\n' + - 'MIIGLTCCBRWgAwIBAgIQDRFjcdUbFwTU3xg1Bre3yzANBgkqhkiG9w0BAQsFADBN\n' + - 'MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\n' + - 'aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTkwNjE0MDAwMDAwWhcN\n' + - 'MjAwNjEzMTIwMDAwWjByMQswCQYDVQQGEwJOTDESMBAGA1UEBxMJRWluZGhvdmVu\n' + - 'MSEwHwYDVQQKExhTaWduaWZ5IE5ldGhlcmxhbmRzIEIuVi4xDDAKBgNVBAsTA0h1\n' + - 'ZTEeMBwGA1UEAxMVZGlzY292ZXJ5Lm1lZXRodWUuY29tMIIBIjANBgkqhkiG9w0B\n' + - 'AQEFAAOCAQ8AMIIBCgKCAQEAxaH5IfwVRAH/2/y4SlGOXTiLjXXm8Zto8YJsMVmv\n' + - '2rqzxA3niZZfJ6SNFJUqWQ+M1Lhcn+yhKp//nrPiG4jUh7m0CWAQ+EgzrNkiYa9U\n' + - 'vC/yGqm2pwxDuPoT90ZdTHgH7+One1bNMn+YNH1/S+sLLf4I1vDwblijynN43cE1\n' + - '02/HbCkAQ0PajRwOqsq8nxWTE6UV5qOWmkvlQuUQAC0zPd9a3bMax0PtY9Ld9ikL\n' + - '2qoM+YbVcL7WvaS4YWm92z4bSZK/CIQmZYiB1Xat5TYQg0YxM1Iqj3wYV7lTdWgD\n' + - 'rfqds5z6I2jGZr72neI7BwJU6j+fHyc8wPbsudjYrDpnBQIDAQABo4IC4jCCAt4w\n' + - 'HwYDVR0jBBgwFoAUD4BhHIIxYdUvKOeNRji0LOHG2eIwHQYDVR0OBBYEFAgIvzOi\n' + - 'I5OaFQIExTAvb41w54ENMCAGA1UdEQQZMBeCFWRpc2NvdmVyeS5tZWV0aHVlLmNv\n' + - 'bTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC\n' + - 'MGsGA1UdHwRkMGIwL6AtoCuGKWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zc2Nh\n' + - 'LXNoYTItZzYuY3JsMC+gLaArhilodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc3Nj\n' + - 'YS1zaGEyLWc2LmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwBATAqMCgGCCsGAQUF\n' + - 'BwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAECAjB8Bggr\n' + - 'BgEFBQcBAQRwMG4wJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNv\n' + - 'bTBGBggrBgEFBQcwAoY6aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lD\n' + - 'ZXJ0U0hBMlNlY3VyZVNlcnZlckNBLmNydDAJBgNVHRMEAjAAMIIBBQYKKwYBBAHW\n' + - 'eQIEAgSB9gSB8wDxAHcA7ku9t3XOYLrhQmkfq+GeZqMPfl+wctiDAMR7iXqo/csA\n' + - 'AAFrVySi8gAABAMASDBGAiEAsQVAcK6mBEQy5gU4dgYdpzdGiU1SdzVHWi1ixio+\n' + - '9WoCIQDd0UcKTiUmaVlOznXwfLzwCtKyIyDWqMF7bhlTKzdz9wB2AId1v+dZfPiM\n' + - 'Q5lfvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABa1ckny4AAAQDAEcwRQIhAKmIpwg4\n' + - 'w9nYb62Y45CWzsBOVp2Gy+7F4nAyf80MjXi8AiAkkXumIKT5ixe4v697u+dRcaxm\n' + - '75EgRp51ifMIJKmwaTANBgkqhkiG9w0BAQsFAAOCAQEAXTejYipPw7rmH+IYqR4r\n' + - 'CmcaDTI3CUvr05QSCPsrD7/rkS/SDP2Z0ZRqHyfDUhYkxrrrBJMcDd28sLm//YJ+\n' + - '+gbGfu+L24cXn3iXyBRliLvFCTRwZfA+347C97y2L0IFQV0S5YSHUr2u7PqG7Jcl\n' + - 'qF5rLPgOYcBNxkjuksqhZZfElVzwwtc/qTAsPAn4PKnJ/gtcb2Dj+41BgZ/lFGDP\n' + - 'Cv/SjNQxw6gNHG5UtEu9qJ0xFVjAC9E13yHezDjKpZs7aBgAD8sWTffp1jJAVkyU\n' + - 'mHpUGHe72H75CnlF6jGu2djDcLkXrWWqd/vw6vFx4oMgBwCHF8IWexUIbJQ5LUHZ\n' + - 'iQ==\n' + - '-----END CERTIFICATE-----\n' + - '\n' + - '-----BEGIN CERTIFICATE-----\n' + - 'MIIElDCCA3ygAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADBh\n' + - 'MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n' + - 'd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n' + - 'QTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaME0xCzAJBgNVBAYTAlVT\n' + - 'MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJzAlBgNVBAMTHkRpZ2lDZXJ0IFNIQTIg\n' + - 'U2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n' + - 'ANyuWJBNwcQwFZA1W248ghX1LFy949v/cUP6ZCWA1O4Yok3wZtAKc24RmDYXZK83\n' + - 'nf36QYSvx6+M/hpzTc8zl5CilodTgyu5pnVILR1WN3vaMTIa16yrBvSqXUu3R0bd\n' + - 'KpPDkC55gIDvEwRqFDu1m5K+wgdlTvza/P96rtxcflUxDOg5B6TXvi/TC2rSsd9f\n' + - '/ld0Uzs1gN2ujkSYs58O09rg1/RrKatEp0tYhG2SS4HD2nOLEpdIkARFdRrdNzGX\n' + - 'kujNVA075ME/OV4uuPNcfhCOhkEAjUVmR7ChZc6gqikJTvOX6+guqw9ypzAO+sf0\n' + - '/RR3w6RbKFfCs/mC/bdFWJsCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8C\n' + - 'AQAwDgYDVR0PAQH/BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYY\n' + - 'aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6\n' + - 'Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwN6A1\n' + - 'oDOGMWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RD\n' + - 'QS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v\n' + - 'd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHQYDVR0OBBYEFA+AYRyCMWHVLyjnjUY4tCzh\n' + - 'xtniMB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA0GCSqGSIb3DQEB\n' + - 'CwUAA4IBAQAjPt9L0jFCpbZ+QlwaRMxp0Wi0XUvgBCFsS+JtzLHgl4+mUwnNqipl\n' + - '5TlPHoOlblyYoiQm5vuh7ZPHLgLGTUq/sELfeNqzqPlt/yGFUzZgTHbO7Djc1lGA\n' + - '8MXW5dRNJ2Srm8c+cftIl7gzbckTB+6WohsYFfZcTEDts8Ls/3HB40f/1LkAtDdC\n' + - '2iDJ6m6K7hQGrn2iWZiIqBtvLfTyyRRfJs8sjX7tN8Cp1Tm5gr8ZDOo0rwAhaPit\n' + - 'c+LJMto4JQtV05od8GiG7S5BNO98pVAdvzr508EIDObtHopYJeS4d60tbvVS3bR0\n' + - 'j6tJLp07kzQoH3jOlOrHvdPJbRzeXDLz\n' + - '-----END CERTIFICATE-----\n' + - '\n' + - '-----BEGIN CERTIFICATE-----\n' + - 'MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\n' + - 'MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n' + - 'd3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n' + - 'QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\n' + - 'MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n' + - 'b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\n' + - '9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\n' + - 'CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\n' + - 'nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n' + - '43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\n' + - 'T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\n' + - 'gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\n' + - 'BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\n' + - 'TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\n' + - 'DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\n' + - 'hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\n' + - '06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\n' + - 'PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\n' + - 'YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\n' + - 'CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n' + - '-----END CERTIFICATE-----\n'; - - return discoveryAgent; -}; \ No newline at end of file diff --git a/hue-api/shim.js b/hue-api/shim.js deleted file mode 100644 index d4ba3cb..0000000 --- a/hue-api/shim.js +++ /dev/null @@ -1,171 +0,0 @@ -'use strict'; - -const Hue = require('./index') - , ScheduledEventBuilder = require('./ScheduledEventBuilder') - , SceneBuilder = require('./SceneBuilder') - , LightStateShim = require('./LightStateShim') - - // The new APIs - , discovery = require('../lib/api/discovery') -; - -/* - -This module provides the necessary shimming to make the original 2.x version of the code base some what compatible with -the new v3 API, thereby preventing the user base from having to rewrite all their code on day one of the release of 3.x. - - */ - -const UPNP_SEARCH_WARNING = 'Function is deprecated, use require(\'node-hue-api\').discovery.upnpSearch() instead' - , NUPNP_SEARCH_WARNING = 'Function is deprecated, use require(\'node-hue-api\').discovery.nupnpSearch() instead' -; - -let patched = false; - -function patchPromise() { - if (!patched) { - // This will mess with the Global Promise, which is less than ideal but is the only way to provide 'Q' like promise compatibility - Promise.prototype.done = function () { - console.error('\nThe promises used by this library are now native JavaScript promises, not Q promises.\n' + - 'Please remove the use of the ".done()" function in your promise chains.\n' - ); - }; - - Promise.prototype.fail = Promise.prototype.catch; - patched = true; - } -} - -function api() { - console.error( - '********************************************************************************\n' + - 'Backwards compatibility shim for node-hue-api.\n\n' + - 'This shim provides a limited backporting of the features available in the updated API in v3.x.\n' + - 'This will be removed in v4.x of node-hue-api.\n\n' + - 'You need to migrate your code to use the new API available via import\n' + - ' require("node-hue-api").v3\n\n' + - 'Please consult the documentation at https://github.com/peter-murray/node-hue-api for the documentation on the new API.\n' + - '********************************************************************************\n' - ); - patchPromise(); - return Hue.apply(Hue, arguments); -} - - -function searchDeprecated(fn, message) { - return function () { - patchPromise(); - console.error(message); - return fn.apply(this, Array.from(arguments)); - }; -} - - -module.exports = { - HueApi: api, - BridgeApi: api, - api: api, - - //TODO deprecate - lightState: { - create: function (values) { - return new LightStateShim(values); - }, - }, - - //TODO deprecate - scheduledEvent: { - create: createScheduledEvent - }, - - //TODO deprecate - scene: { - create: createScene - }, - - searchForBridges: searchDeprecated(discovery.upnpSearch, UPNP_SEARCH_WARNING), - upnpSearch: searchDeprecated(discovery.upnpSearch, UPNP_SEARCH_WARNING), - - locateBridges: searchDeprecated(discovery.upnpSearch, NUPNP_SEARCH_WARNING), - nupnpSearch: searchDeprecated(discovery.upnpSearch, NUPNP_SEARCH_WARNING), -}; - - -function createScheduledEvent() { - let builder, - arg; - - if (arguments.length === 0) { - builder = new ScheduledEventBuilder(); - } else { - arg = arguments[0]; - if (arg instanceof ScheduledEventBuilder) { - builder = arg; - } else { - builder = new ScheduledEventBuilder(); - - // try to populate the new schedule using any values that match schedule properties - if (arg.name) { - builder.withName(arg.name); - } - - if (arg.description) { - builder.withDescription(arg.description); - } - - if (arg.time || arg.localtime) { - builder.at(arg.time || arg.localtime); - } - - if (arg.command) { - builder.withCommand(arg.command); - } - - if (arg.status) { - builder.withEnabledState(arg.status); - } - } - } - - return builder; -} - -function createScene() { - let scene - , arg - ; - - if (arguments.length === 0) { - scene = new SceneBuilder(); - } else { - arg = arguments[0]; - - if (arg instanceof SceneBuilder) { - scene = arg; - } else { - scene = new SceneBuilder(); - - // try to populate the new scene using any values that match scene properties - if (arg.name) { - scene.withName(arg.name); - } - - if (arg.lights) { - scene.withLights(arg.lights); - } - - if (arg.transitionTime) { - scene.withTransitionTime(arg.transitionTime); - } - - if (arg.data || arg.appData) { - scene.withAppData(arg.data || arg.appData); - } - - if (arg.picture) { - scene.withPicture(arg.picture); - } - } - } - return scene; -} \ No newline at end of file diff --git a/hue-api/utils.js b/hue-api/utils.js deleted file mode 100644 index 73c173d..0000000 --- a/hue-api/utils.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -module.exports.nativePromiseOrCallback = function (promise, cb) { - let promiseResult = promise; - - if (cb && typeof cb === 'function') { - module.exports.resolvePromise(promise, cb); - // Do not return the promise, as the callbacks will have forced it to resolve - promiseResult = null; - } - - return promiseResult; -}; - -/** - * Checks the callback and if it is valid, will resolve the promise an utilize the callback to inform of results, - * otherwise the promise is returned to the caller to chain. - * - * @param promise The promise being invoked - * @param cb The callback function, which is optional - * @returns {*} The promise if there is not a valid callback, or null, if the callback is used to resolve the promise. - */ -module.exports.promiseOrCallback = function (promise, cb) { - const promiseResult = promise; - - if (cb && typeof cb === 'function') { - module.exports.resolvePromise(promise, cb); - // Do not return the promise, as the callbacks will have forced it to resolve - return null; - } - - return promiseResult; -}; - -/** - * Terminates a promise chain and invokes a callback with the results. - * - * @param promise The promise to terminate - * @param callback The callback function to invoke - */ -module.exports.resolvePromise = function (promise, callback) { - function resolveValue(value) { - if (callback) { - callback(null, value); - } - } - - function resolveError(err) { - if (callback) { - callback(err, null); - } - } - - promise.catch(resolveError).then(resolveValue); -}; - - -module.exports.isFunction = function (object) { - var getClass = {}.toString; - - return object && getClass.call(object) === '[object Function]'; -}; \ No newline at end of file From e15be6a0462dfa330bf7757e7b091b7d6a28442d Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Tue, 19 Nov 2019 16:37:21 +0000 Subject: [PATCH 09/35] Adding ability to run tests on commit --- .github/workflows/modejs.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/modejs.yml diff --git a/.github/workflows/modejs.yml b/.github/workflows/modejs.yml new file mode 100644 index 0000000..c077663 --- /dev/null +++ b/.github/workflows/modejs.yml @@ -0,0 +1,27 @@ +name: Node CI + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [10.x, 12.x] + + steps: + - uses: actions/checkout@v1 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: npm test + run: | + npm ci + npm run test-types --if-present + npm run test-model --if-present + env: + CI: true \ No newline at end of file From e42664a09c578c616921b9e05a3ff7584254c764 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Wed, 27 Nov 2019 22:11:44 +0000 Subject: [PATCH 10/35] - Making cache compatible with ID related changes and support Light as well as Light ID as the id parameter --- lib/api/stateCache.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/api/stateCache.js b/lib/api/stateCache.js index da9a3fd..e2b6ba7 100644 --- a/lib/api/stateCache.js +++ b/lib/api/stateCache.js @@ -1,8 +1,10 @@ 'use strict'; const model = require('../model') + , LightIdPlaceHolder = require('../placeholders/LightIdPlaceholder') ; +const LIGHT_ID_PLACEHOLDER = new LightIdPlaceHolder(); class Cache { @@ -12,13 +14,15 @@ class Cache { } getLight(id) { - let light = this._lights[id]; + const lightId = LIGHT_ID_PLACEHOLDER.getValue({id: id}); + + let light = this._lights[lightId]; if (!light) { - let lightData = this.data.lights[id]; + let lightData = this.data.lights[lightId]; if (lightData) { - light = model.createFromBridge('light', id, lightData); - this._lights[id] = light; + light = model.createFromBridge('light', lightId, lightData); + this._lights[lightId] = light; } } From b52715c3d2b6b2a08396cf400d31d96bf2c0ff2a Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Wed, 27 Nov 2019 22:13:12 +0000 Subject: [PATCH 11/35] - Adding support to use Objects as well as number/string for IDs - Moving module up to higher level based on new use --- .../http => }/placeholders/GroupIdPlaceholder.js | 11 ++++++++++- .../http => }/placeholders/LightIdPlaceholder.js | 11 ++++++++++- lib/{api/http => }/placeholders/Placeholder.js | 14 ++++++++++++-- .../placeholders/ResourceLinkPlaceholder.js | 11 ++++++++++- .../http => }/placeholders/RuleIdPlaceholder.js | 11 ++++++++++- .../http => }/placeholders/SceneIdPlaceholder.js | 11 ++++++++++- .../placeholders/ScheduleIdPlaceholder.js | 11 ++++++++++- .../http => }/placeholders/SensorIdPlaceholder.js | 11 ++++++++++- .../http => }/placeholders/UsernamePlaceholder.js | 2 +- 9 files changed, 83 insertions(+), 10 deletions(-) rename lib/{api/http => }/placeholders/GroupIdPlaceholder.js (51%) rename lib/{api/http => }/placeholders/LightIdPlaceholder.js (51%) rename lib/{api/http => }/placeholders/Placeholder.js (65%) rename lib/{api/http => }/placeholders/ResourceLinkPlaceholder.js (51%) rename lib/{api/http => }/placeholders/RuleIdPlaceholder.js (51%) rename lib/{api/http => }/placeholders/SceneIdPlaceholder.js (51%) rename lib/{api/http => }/placeholders/ScheduleIdPlaceholder.js (51%) rename lib/{api/http => }/placeholders/SensorIdPlaceholder.js (51%) rename lib/{api/http => }/placeholders/UsernamePlaceholder.js (88%) diff --git a/lib/api/http/placeholders/GroupIdPlaceholder.js b/lib/placeholders/GroupIdPlaceholder.js similarity index 51% rename from lib/api/http/placeholders/GroupIdPlaceholder.js rename to lib/placeholders/GroupIdPlaceholder.js index fc620eb..40bf89e 100644 --- a/lib/api/http/placeholders/GroupIdPlaceholder.js +++ b/lib/placeholders/GroupIdPlaceholder.js @@ -1,7 +1,8 @@ 'use strict'; const Placeholder = require('./Placeholder') - , types = require('../../../types') + , types = require('../types') + , model = require('../model') ; module.exports = class GroupIdPlaceholder extends Placeholder { @@ -10,4 +11,12 @@ module.exports = class GroupIdPlaceholder extends Placeholder { super('id', name); this.typeDefinition = types.uint16({name: 'group id', optional: false}); } + + _getParameterValue(parameter) { + if (model.isGroupInstance(parameter)) { + return parameter.id; + } else { + return super._getParameterValue(parameter); + } + } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/LightIdPlaceholder.js b/lib/placeholders/LightIdPlaceholder.js similarity index 51% rename from lib/api/http/placeholders/LightIdPlaceholder.js rename to lib/placeholders/LightIdPlaceholder.js index 92dc8b6..4e951e4 100644 --- a/lib/api/http/placeholders/LightIdPlaceholder.js +++ b/lib/placeholders/LightIdPlaceholder.js @@ -1,7 +1,8 @@ 'use strict'; const Placeholder = require('./Placeholder') - , types = require('../../../types') + , types = require('../types') + , model = require('../model') ; @@ -11,4 +12,12 @@ module.exports = class LightIdPlaceholder extends Placeholder { super('id', name); this.typeDefinition = types.uint16({name: 'light id', optional: false}); } + + _getParameterValue(parameter) { + if (model.isLightInstance(parameter)) { + return parameter.id; + } else { + return super._getParameterValue(parameter); + } + } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/Placeholder.js b/lib/placeholders/Placeholder.js similarity index 65% rename from lib/api/http/placeholders/Placeholder.js rename to lib/placeholders/Placeholder.js index 440ab77..f13d306 100644 --- a/lib/api/http/placeholders/Placeholder.js +++ b/lib/placeholders/Placeholder.js @@ -1,6 +1,6 @@ 'use strict'; -const ApiError = require('../../../ApiError'); +const ApiError = require('../ApiError'); module.exports = class Placeholder { @@ -37,7 +37,17 @@ module.exports = class Placeholder { throw new ApiError('No type definition has been specified for placeholder'); } - const value = parameters ? parameters[this._name] : null; + const parameter = parameters ? parameters[this.name] : null; + const value = this._getParameterValue(parameter); return typeDefinition.getValue(value); } + + _getParameterValue(parameter) { + return parameter; + } + + toString() { + const type = this.typeDefinition; + return `${this.name}: { type:${type.type}, optional:${type.optional}, defaultValue:${type.defaultValue} }`; + } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/ResourceLinkPlaceholder.js b/lib/placeholders/ResourceLinkPlaceholder.js similarity index 51% rename from lib/api/http/placeholders/ResourceLinkPlaceholder.js rename to lib/placeholders/ResourceLinkPlaceholder.js index 6891ac3..4d4b0a1 100644 --- a/lib/api/http/placeholders/ResourceLinkPlaceholder.js +++ b/lib/placeholders/ResourceLinkPlaceholder.js @@ -1,7 +1,8 @@ 'use strict'; const Placeholder = require('./Placeholder') - , types = require('../../../types') + , types = require('../types') + , model = require('../model') ; @@ -11,4 +12,12 @@ module.exports = class ResourceLinkPlaceholder extends Placeholder { super('id', name); this.typeDefinition = types.uint16({name: 'resourcelink id', optional: false}); } + + _getParameterValue(parameter) { + if (model.isResourceLinkInstance(parameter)) { + return parameter.id; + } else { + return super._getParameterValue(parameter); + } + } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/RuleIdPlaceholder.js b/lib/placeholders/RuleIdPlaceholder.js similarity index 51% rename from lib/api/http/placeholders/RuleIdPlaceholder.js rename to lib/placeholders/RuleIdPlaceholder.js index c1c4cf8..764d654 100644 --- a/lib/api/http/placeholders/RuleIdPlaceholder.js +++ b/lib/placeholders/RuleIdPlaceholder.js @@ -1,7 +1,8 @@ 'use strict'; const Placeholder = require('./Placeholder') - , types = require('../../../types') + , types = require('../types') + , model = require('../model') ; module.exports = class RuleIdPlaceholder extends Placeholder { @@ -10,4 +11,12 @@ module.exports = class RuleIdPlaceholder extends Placeholder { super('id', name); this.typeDefinition = types.uint16({name: 'rule id', optional: false}); } + + _getParameterValue(parameter) { + if (model.isRuleInstance(parameter)) { + return parameter.id; + } else { + return super._getParameterValue(parameter); + } + } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/SceneIdPlaceholder.js b/lib/placeholders/SceneIdPlaceholder.js similarity index 51% rename from lib/api/http/placeholders/SceneIdPlaceholder.js rename to lib/placeholders/SceneIdPlaceholder.js index 5c27219..7a63dca 100644 --- a/lib/api/http/placeholders/SceneIdPlaceholder.js +++ b/lib/placeholders/SceneIdPlaceholder.js @@ -1,7 +1,8 @@ 'use strict'; const Placeholder = require('./Placeholder') - , types = require('../../../types') + , types = require('../types') + , model = require('../model') ; module.exports = class SceneIdPlaceholder extends Placeholder { @@ -10,4 +11,12 @@ module.exports = class SceneIdPlaceholder extends Placeholder { super('id', name); this.typeDefinition = types.string({name: 'scene id', optional: false}); } + + _getParameterValue(parameter) { + if (model.isSceneInstance(parameter)) { + return parameter.id; + } else { + return super._getParameterValue(parameter); + } + } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/ScheduleIdPlaceholder.js b/lib/placeholders/ScheduleIdPlaceholder.js similarity index 51% rename from lib/api/http/placeholders/ScheduleIdPlaceholder.js rename to lib/placeholders/ScheduleIdPlaceholder.js index a88768c..45738eb 100644 --- a/lib/api/http/placeholders/ScheduleIdPlaceholder.js +++ b/lib/placeholders/ScheduleIdPlaceholder.js @@ -1,7 +1,8 @@ 'use strict'; const Placeholder = require('./Placeholder') - , types = require('../../../types') + , types = require('../types') + , model = require('../model') ; module.exports = class ScheduleIdPlaceholder extends Placeholder { @@ -10,4 +11,12 @@ module.exports = class ScheduleIdPlaceholder extends Placeholder { super('id', name); this.typeDefinition = types.uint16({name: 'schedule id', optional: false}); } + + _getParameterValue(parameter) { + if (model.isScheduleInstance(parameter)) { + return parameter.id; + } else { + return super._getParameterValue(parameter); + } + } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/SensorIdPlaceholder.js b/lib/placeholders/SensorIdPlaceholder.js similarity index 51% rename from lib/api/http/placeholders/SensorIdPlaceholder.js rename to lib/placeholders/SensorIdPlaceholder.js index 5da00a8..6bf0429 100644 --- a/lib/api/http/placeholders/SensorIdPlaceholder.js +++ b/lib/placeholders/SensorIdPlaceholder.js @@ -1,7 +1,8 @@ 'use strict'; const Placeholder = require('./Placeholder') - , types = require('../../../types') + , types = require('../types') + , model = require('../model') ; module.exports = class SensorIdPlaceholder extends Placeholder { @@ -10,4 +11,12 @@ module.exports = class SensorIdPlaceholder extends Placeholder { super('id', name); this.typeDefinition = types.uint16({name: 'sensor id', optional: false}); } + + _getParameterValue(parameter) { + if (model.isSensorInstance(parameter)) { + return parameter.id; + } else { + return super._getParameterValue(parameter); + } + } }; \ No newline at end of file diff --git a/lib/api/http/placeholders/UsernamePlaceholder.js b/lib/placeholders/UsernamePlaceholder.js similarity index 88% rename from lib/api/http/placeholders/UsernamePlaceholder.js rename to lib/placeholders/UsernamePlaceholder.js index c58e45b..8d2dc83 100644 --- a/lib/api/http/placeholders/UsernamePlaceholder.js +++ b/lib/placeholders/UsernamePlaceholder.js @@ -1,7 +1,7 @@ 'use strict'; const Placeholder = require('./Placeholder') - , types = require('../../../types') + , types = require('../types') ; From 3cb551addea4d8eb33d5b794bbb75b50462214f1 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Wed, 27 Nov 2019 22:14:03 +0000 Subject: [PATCH 12/35] - Adding deprecatedFunction() to output deprecation messages for functions that have been deprecated --- lib/util.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/util.js b/lib/util.js index 397d9c9..3590cb2 100644 --- a/lib/util.js +++ b/lib/util.js @@ -8,9 +8,10 @@ module.exports = { parseErrors: parseErrors, wasSuccessful: wasSuccessful, extractUpdatedAttributes: extractUpdatedAttributes, - asStringArray: asStringArray, + toStringArray: asStringArray, flatten: mergeArrays, - getValueForKey: getValueforKey + getValueForKey: getValueforKey, + deprecatedFunction: deprecatedFunction, }; @@ -133,3 +134,11 @@ function mergeArrays() { return result; } +function deprecatedFunction(version, func, message) { + console.log(`**************************************************************************************************`); + console.log(`Deprecated Function Usage: ${func}\n`); + console.log(` ${message}\n`); + console.log(` Function will be removed from node-hue-api in version ${version}`); + console.log(`**************************************************************************************************`); +} + From 7e275017929fb326c47ffa715c126071e0bb23f5 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Wed, 27 Nov 2019 22:17:07 +0000 Subject: [PATCH 13/35] - Adding timeout to description loading (as API can become unresponsive) - Added new config function to access the unauthenticated API endpoint for loading data instead of the discovery XML file (which responds more reliably) --- lib/api/discovery/bridge-validation.js | 61 ++++++++++++++------------ 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/lib/api/discovery/bridge-validation.js b/lib/api/discovery/bridge-validation.js index 1b2706d..a65495b 100644 --- a/lib/api/discovery/bridge-validation.js +++ b/lib/api/discovery/bridge-validation.js @@ -21,13 +21,42 @@ const FRIENDLY_NAME = /(.*)<\/friendlyName/sm , ICON_URL = /(.*)<\/url>/sm ; +const DATA_TIMEOUT = 6000; -module.exports.getBridgeDescription = (bridge) => { +module.exports.getBridgeConfig = (bridge, timeout) => { + const ipAddress = bridge.internalipaddress; + + return axios.request({ + method: 'get', + url: `http://${ipAddress}/api/config`, + timeout: timeout | DATA_TIMEOUT, + json: true, + }) + .catch(err => { + throw new ApiError(`Problem connecting to bridge '${ipAddress}'`) + }) + .then(res => { + if (res.status !== 200) { + throw new ApiError(`Unexpected status when getting unauthenticated configuration date from bridge at ${ipAddress}`); + } + + const result = {}; + result.name = res.data.name; + result.ipaddress= ipAddress; + result.modelid = res.data.modelid; + result.swversion = res.data.swversion; + + return result; + }) +}; + +module.exports.getBridgeDescription = (bridge, timeout) => { const ipAddress = bridge.internalipaddress; return axios.request({ method: 'GET', url: `http://${ipAddress}/description.xml`, + timeout: timeout | DATA_TIMEOUT, headers: { accept: 'text/xml' } @@ -74,7 +103,7 @@ module.exports.parseXmlDescription = (data) => { function extractValue(name, data, regex) { const matched = regex.exec(data); - if (! matched) { + if (!matched) { return null; } @@ -140,30 +169,4 @@ function getSpecVersion(data) { }; } return null; -} - -// TODO remove this, left in place for reference to the old payload parsing via XML library -// if (data.root.device[0].iconList -// && data.root.device[0].iconList[0] -// && data.root.device[0].iconList[0].icon) { -// icons = []; -// -// data.root.device[0].iconList[0].icon.forEach(function (icon) { -// icons.push({ -// mimetype: icon.mimetype[0], -// height: icon.height[0], -// width: icon.width[0], -// depth: icon.depth[0], -// url: icon.url[0] -// }); -// }); -// -// result.icons = icons; -// } -// -// deferred.resolve(result); -// } -// }); -// -// return deferred.promise; -// } \ No newline at end of file +} \ No newline at end of file From fd1fdbc6673f92c5b13ca699fde8c5016eb10dd7 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Wed, 27 Nov 2019 22:26:00 +0000 Subject: [PATCH 14/35] - Making N-UPnP functionality more responsive and in line with API use instead of using older UPnP XML endpoint --- lib/api/discovery/bridge-validation.js | 2 +- lib/api/discovery/index.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/api/discovery/bridge-validation.js b/lib/api/discovery/bridge-validation.js index a65495b..03c6bef 100644 --- a/lib/api/discovery/bridge-validation.js +++ b/lib/api/discovery/bridge-validation.js @@ -33,7 +33,7 @@ module.exports.getBridgeConfig = (bridge, timeout) => { json: true, }) .catch(err => { - throw new ApiError(`Problem connecting to bridge '${ipAddress}'`) + throw new ApiError(`Problem connecting to bridge '${ipAddress}': ${err.message}`) }) .then(res => { if (res.status !== 200) { diff --git a/lib/api/discovery/index.js b/lib/api/discovery/index.js index 230357a..152615b 100644 --- a/lib/api/discovery/index.js +++ b/lib/api/discovery/index.js @@ -13,7 +13,7 @@ module.exports.upnpSearch = function (timeout) { }; module.exports.nupnpSearch = function () { - return nupnp.nupnp().then(loadDescriptions); + return nupnp.nupnp().then(loadConfigurations); }; module.exports.description = function (ipAddress) { @@ -26,11 +26,18 @@ module.exports.description = function (ipAddress) { }); }; - function loadDescriptions(results) { const promises = results.map(result => { return bridgeValidator.getBridgeDescription(result); }); + return Promise.all(promises); +} + +function loadConfigurations(results) { + const promises = results.map(result => { + return bridgeValidator.getBridgeConfig(result); + }); + return Promise.all(promises); } \ No newline at end of file From 4baf7b35f96590f1606bbd32c909db2783b8be25 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Fri, 29 Nov 2019 17:33:52 +0000 Subject: [PATCH 15/35] - Finishing off all the time patterns that the bridge can support --- lib/model/datetime/RandomizedTimer.js | 1 - lib/model/datetime/RecurringTimer.js | 1 - lib/model/datetime/TimeInterval.js | 1 - lib/model/timePatterns/AbsoluteTime.js | 88 ++++++++++++ lib/model/timePatterns/AbsoluteTime.test.js | 58 ++++++++ lib/model/timePatterns/BridgeTime.js | 5 + lib/model/timePatterns/HueDate.js | 132 ++++++++++++++++++ lib/model/timePatterns/HueDate.test.js | 54 +++++++ lib/model/timePatterns/HueTime.js | 88 ++++++++++++ lib/model/timePatterns/HueTime.test.js | 87 ++++++++++++ lib/model/timePatterns/RandomizedTime.js | 112 +++++++++++++++ lib/model/timePatterns/RandomizedTime.test.js | 58 ++++++++ lib/model/timePatterns/RandomizedTimer.js | 85 +++++++++++ .../timePatterns/RandomizedTimer.test.js | 81 +++++++++++ .../timePatterns/RecurringRandomizedTime.js | 98 +++++++++++++ .../RecurringRandomizedTime.test.js | 65 +++++++++ .../timePatterns/RecurringRandomizedTimer.js | 100 +++++++++++++ .../RecurringRandomizedTimer.test.js | 81 +++++++++++ lib/model/timePatterns/RecurringTime.js | 106 ++++++++++++++ lib/model/timePatterns/RecurringTime.test.js | 64 +++++++++ lib/model/timePatterns/RecurringTimer.js | 82 +++++++++++ lib/model/timePatterns/RecurringTimer.test.js | 100 +++++++++++++ lib/model/timePatterns/TimeInterval.js | 111 +++++++++++++++ lib/model/timePatterns/TimeInterval.test.js | 80 +++++++++++ lib/model/timePatterns/Timer.js | 64 +++++++++ .../{datetime => timePatterns}/Timer.test.js | 36 +++-- lib/model/timePatterns/index.js | 104 ++++++++++++++ lib/model/timePatterns/timeUtil.js | 34 +++++ 28 files changed, 1958 insertions(+), 18 deletions(-) delete mode 100644 lib/model/datetime/RandomizedTimer.js delete mode 100644 lib/model/datetime/RecurringTimer.js delete mode 100644 lib/model/datetime/TimeInterval.js create mode 100644 lib/model/timePatterns/AbsoluteTime.js create mode 100644 lib/model/timePatterns/AbsoluteTime.test.js create mode 100644 lib/model/timePatterns/BridgeTime.js create mode 100644 lib/model/timePatterns/HueDate.js create mode 100644 lib/model/timePatterns/HueDate.test.js create mode 100644 lib/model/timePatterns/HueTime.js create mode 100644 lib/model/timePatterns/HueTime.test.js create mode 100644 lib/model/timePatterns/RandomizedTime.js create mode 100644 lib/model/timePatterns/RandomizedTime.test.js create mode 100644 lib/model/timePatterns/RandomizedTimer.js create mode 100644 lib/model/timePatterns/RandomizedTimer.test.js create mode 100644 lib/model/timePatterns/RecurringRandomizedTime.js create mode 100644 lib/model/timePatterns/RecurringRandomizedTime.test.js create mode 100644 lib/model/timePatterns/RecurringRandomizedTimer.js create mode 100644 lib/model/timePatterns/RecurringRandomizedTimer.test.js create mode 100644 lib/model/timePatterns/RecurringTime.js create mode 100644 lib/model/timePatterns/RecurringTime.test.js create mode 100644 lib/model/timePatterns/RecurringTimer.js create mode 100644 lib/model/timePatterns/RecurringTimer.test.js create mode 100644 lib/model/timePatterns/TimeInterval.js create mode 100644 lib/model/timePatterns/TimeInterval.test.js create mode 100644 lib/model/timePatterns/Timer.js rename lib/model/{datetime => timePatterns}/Timer.test.js (65%) create mode 100644 lib/model/timePatterns/index.js create mode 100644 lib/model/timePatterns/timeUtil.js diff --git a/lib/model/datetime/RandomizedTimer.js b/lib/model/datetime/RandomizedTimer.js deleted file mode 100644 index 5863a4a..0000000 --- a/lib/model/datetime/RandomizedTimer.js +++ /dev/null @@ -1 +0,0 @@ -//TODO complete this \ No newline at end of file diff --git a/lib/model/datetime/RecurringTimer.js b/lib/model/datetime/RecurringTimer.js deleted file mode 100644 index 5863a4a..0000000 --- a/lib/model/datetime/RecurringTimer.js +++ /dev/null @@ -1 +0,0 @@ -//TODO complete this \ No newline at end of file diff --git a/lib/model/datetime/TimeInterval.js b/lib/model/datetime/TimeInterval.js deleted file mode 100644 index 5863a4a..0000000 --- a/lib/model/datetime/TimeInterval.js +++ /dev/null @@ -1 +0,0 @@ -//TODO complete this \ No newline at end of file diff --git a/lib/model/timePatterns/AbsoluteTime.js b/lib/model/timePatterns/AbsoluteTime.js new file mode 100644 index 0000000..f45bc08 --- /dev/null +++ b/lib/model/timePatterns/AbsoluteTime.js @@ -0,0 +1,88 @@ +'use strict'; + +const timeUtil = require('./timeUtil') + , HueTime = require('./HueTime') + , HueDate = require('./HueDate') + , BridgeTime = require('./BridgeTime') + , ApiError = require('../../ApiError') +; + +const ABSOLUTE_TIME_REGEX = new RegExp(`${timeUtil.getDatePattern()}T${timeUtil.getTimePattern()}`); + +module.exports = class AbsoluteTime extends BridgeTime { + + constructor(value) { + super(); + this._time = new HueTime(); + this._date = new HueDate(); + + if (value) { + this.value = value; + } + } + + static matches(value) { + return ABSOLUTE_TIME_REGEX.test(value); + } + + set value(value) { + if (value instanceof AbsoluteTime) { + return this.value = value.toString(); + } else if (value instanceof Date) { + this._time.fromDate(value); + this._date.fromDate(value); + return this; + } + + const parsed = ABSOLUTE_TIME_REGEX.exec(value); + if (parsed) { + const time = this._time; + time.hours = parsed.groups.hours; + time.minutes = parsed.groups.minutes; + time.seconds = parsed.groups.seconds; + + const date = this._date; + date.year = parsed.groups.year; + date.month = parsed.groups.month; + date.day = parsed.groups.day; + + return this; + } + + throw new ApiError(`Cannot create an absolute time from ${value}`); + } + + year(value) { + this._date.year = value; + return this; + } + + month(value) { + this._date.month = value; + return this; + } + + day(value) { + this._date.day = value; + return this; + } + + hours(value) { + this._time.hours = value; + return this; + } + + minutes(value) { + this._time.minutes = value; + return this; + } + + seconds(value) { + this._time.seconds = value; + return this; + } + + toString() { + return `${this._date.toString()}T${this._time.toString()}`; + } +}; \ No newline at end of file diff --git a/lib/model/timePatterns/AbsoluteTime.test.js b/lib/model/timePatterns/AbsoluteTime.test.js new file mode 100644 index 0000000..fe4b1e3 --- /dev/null +++ b/lib/model/timePatterns/AbsoluteTime.test.js @@ -0,0 +1,58 @@ +'use strict'; + +const expect = require('chai').expect + , AbsoluteTime = require('./AbsoluteTime') + , HueDate = require('./HueDate') + , HueTime = require('./HueTime') +; + +describe('AbsoluteTime', () => { + + + describe('constructor', () => { + + it('should create an AbsoluteTime from no parameters', () => { + const absoluteTime = new AbsoluteTime() + , time = new HueTime() + , date = new HueDate() + ; + + expect(absoluteTime.toString()).to.equal(`${date.toString()}T${time.toString()}`); + }); + + it('should create from a valid string', () => { + const timeString = '1977-08-12T12:00:00' + , absoluteTime = new AbsoluteTime(timeString) + ; + + expect(absoluteTime.toString()).to.equal(timeString); + }); + + it('should create from a Date object', () => { + const date = new Date() + , absoluteTime = new AbsoluteTime(date) + ; + + expect(absoluteTime.toString()).to.equal(fromDate(date)); + }); + + it('should create from setters', () => { + const absoluteTime = new AbsoluteTime(); + absoluteTime.year(1977).month(12).day(1).hours(23).minutes(12).seconds(31); + expect(absoluteTime.toString()).to.equal('1977-12-01T23:12:31'); + }); + + //TODO allow cloning from another AbsoluteTime? + }); +}); + +function fromDate(date) { + const hours = `${date.getUTCHours()}`.padStart(2, '0') + , minutes = `${date.getUTCMinutes()}`.padStart(2, '0') + , seconds = `${date.getUTCSeconds()}`.padStart(2, '0') + , month = `${date.getUTCMonth() + 1}`.padStart(2, '0') + , day = `${date.getUTCDate()}`.padStart(2, '0') + ; + + return `${date.getFullYear()}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} \ No newline at end of file diff --git a/lib/model/timePatterns/BridgeTime.js b/lib/model/timePatterns/BridgeTime.js new file mode 100644 index 0000000..4a36a4c --- /dev/null +++ b/lib/model/timePatterns/BridgeTime.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = class BridgeTime { + +}; \ No newline at end of file diff --git a/lib/model/timePatterns/HueDate.js b/lib/model/timePatterns/HueDate.js new file mode 100644 index 0000000..dd4a313 --- /dev/null +++ b/lib/model/timePatterns/HueDate.js @@ -0,0 +1,132 @@ +'use strict'; + +const ApiError = require('../../ApiError') + , BridgeObject = require('../BridgeObject') + , types = require('../../types') + , dateTimeUtil = require('./timeUtil') +; + +const DATE_STRING_REGEX = new RegExp(`^${dateTimeUtil.getDatePattern()}`); + +const MONTHS = [ + 'January', + 'Feburary', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' +]; + + +const ATTRIBUTES = [ + types.uint8({name: 'year', min: 1900, max: 3000}), + types.choice({name: 'month', validValues: MONTHS}), + types.uint8({name: 'day', min: 0, max: 31}), +]; + + +module.exports = class HueDate extends BridgeObject{ + + constructor(value) { + super(ATTRIBUTES); + + if (value instanceof Date) { + this.fromDate(value); + } else { + this.fromString(value); + } + } + + get year() { + return this.getAttributeValue('year'); + } + + get yearString() { + return `${this.year}`; + } + + set year(value) { + return this.setAttributeValue('year', value); + } + + get month() { + const idx = MONTHS.indexOf(this.getAttributeValue('month')); + + //TODO make mandatory then will not need this? + if(idx === -1) { + throw new ApiError(`Month value has not been set`); + } + + return idx + 1; + } + + get monthString() { + const month = this.month; + return `${month}`.padStart(2, '0'); + } + + /** + * Sets the Month for the Date. + * @param value {number | string} If a number, it is a 1 based index on the month number (1 === Jan), otherwise as a String the name of the month. + * @returns {BridgeObject} + */ + set month(value) { + const monthNumber = new Number(value); + if (Number.isNaN(monthNumber)) { + return this.setAttributeValue('month', value); + } else { + const monthName = MONTHS[monthNumber - 1]; + return this.setAttributeValue('month', monthName); + } + } + + get day() { + return this.getAttributeValue('day'); + } + + get dayString() { + return `${this.day}`.padStart(2, '0'); + } + + set day(value) { + return this.setAttributeValue('day', value); + } + + toString() { + return `${this.yearString}-${this.monthString}-${this.dayString}`; + } + + fromString(value) { + if (!value) { + return this.fromDate(new Date()); + } else { + const parsed = DATE_STRING_REGEX.exec(value); + if (parsed) { + this.year = parsed.groups.year; + this.month = parsed.groups.month; + this.day = parsed.groups.day; + return this; + } + return this.fromDate(new Date()); + } + } + + fromDate(value) { + const year = value.getUTCFullYear() + , month = value.getUTCMonth() + , day = value.getUTCDate() + ; + + this.year = year; + this.month = month + 1; + this.day = day; + + return this; + } +}; \ No newline at end of file diff --git a/lib/model/timePatterns/HueDate.test.js b/lib/model/timePatterns/HueDate.test.js new file mode 100644 index 0000000..ba80bbf --- /dev/null +++ b/lib/model/timePatterns/HueDate.test.js @@ -0,0 +1,54 @@ +'use strict'; + +const expect = require('chai').expect + , HueDate = require('./HueDate') +; + +describe('HueDate', () => { + + + describe('constructor', () => { + + it('should create a HueDate', () => { + const date = new HueDate() + , now = new Date() + ; + + expect(date.year).to.equal(now.getUTCFullYear()); + expect(date.month).to.equal(now.getUTCMonth() + 1); + expect(date.day).to.equal(now.getUTCDate()); + }); + + it('should create one from a Date', () => { + const myDate = new Date('1977-08-12') + , date = new HueDate(myDate) + ; + + expect(date.year).to.equal(myDate.getUTCFullYear()); + expect(date.month).to.equal(myDate.getUTCMonth() + 1); + expect(date.day).to.equal(myDate.getUTCDate()); + }); + + it('should create one from a Date', () => { + const myDate = '1977-08-12' + , date = new HueDate(myDate) + ; + + expect(date.year).to.equal(1977); + expect(date.month).to.equal(8); + expect(date.day).to.equal(12); + }); + }); + + + // describe('#fromString()', () => { + // + // }); + // + // + // describe('#fromDate()', () => { + // + // }); + + //TODO test more boundaries +}); \ No newline at end of file diff --git a/lib/model/timePatterns/HueTime.js b/lib/model/timePatterns/HueTime.js new file mode 100644 index 0000000..18fef1d --- /dev/null +++ b/lib/model/timePatterns/HueTime.js @@ -0,0 +1,88 @@ +'use strict'; + +const ApiError = require('../../ApiError') + , BridgeObject = require('../BridgeObject') + , types = require('../../types') + , dateTimeUtil = require('./timeUtil') +; + +const ATTRIBUTES = [ + types.uint8({name: 'hours', min: 0, max: 23}), + types.uint8({name: 'minutes', min: 0, max: 59}), + types.uint8({name: 'seconds', min: 0, max: 59}), +]; + +const TIME_REGEX = new RegExp(dateTimeUtil.getTimePattern()); + +module.exports = class HueTime extends BridgeObject { + + constructor(time) { + super(ATTRIBUTES); + + if (time instanceof Date){ + this.fromDate(time); + } else { + this.fromString(time || '00:00:00'); + } + } + + get hours() { + return this.getAttributeValue('hours'); + } + + get hoursString() { + return `${this.hours}`.padStart(2, '0'); + } + + set hours(value) { + return this.setAttributeValue('hours', value); + } + + get minutes() { + return this.getAttributeValue('minutes'); + } + + get minutesString() { + return `${this.minutes}`.padStart(2, '0'); + } + + set minutes(value) { + return this.setAttributeValue('minutes', value); + } + + get seconds() { + return this.getAttributeValue('seconds'); + } + + get secondsString() { + return `${this.seconds}`.padStart(2, '0'); + } + + set seconds(value) { + return this.setAttributeValue('seconds', value); + } + + toString() { + return `${this.hoursString}:${this.minutesString}:${this.secondsString}`; + } + + fromString(value) { + const parsed = TIME_REGEX.exec(value); + + if (parsed) { + this.hours = parsed.groups.hours; + this.minutes = parsed.groups.minutes; + this.seconds = parsed.groups.seconds; + } else { + throw new ApiError(`Invalid time format string "${value}"`); + } + } + + fromDate(value) { + //TODO should validate we have a Date first + this.hours = value.getHours(); + this.minutes = value.getMinutes(); + this.seconds = value.getSeconds(); + } +}; + diff --git a/lib/model/timePatterns/HueTime.test.js b/lib/model/timePatterns/HueTime.test.js new file mode 100644 index 0000000..35775c8 --- /dev/null +++ b/lib/model/timePatterns/HueTime.test.js @@ -0,0 +1,87 @@ +'use strict'; + +const expect = require('chai').expect + , HueTime = require('./HueTime') +; + +describe('HueTime', () => { + + const validTimeStrings = [ + '00:00:00', + '00:00:01', + '00:00:59', + '00:59:00', + '00:01:00', + '23:00:00', + '23:59:59', + ]; + + const validDates = [ + new Date(), + new Date(' August 12, 1977 15:00:01') + ]; + +//TODO test boundaries + + describe('construction', () => { + + it('should create from an empty string', () => { + const time = new HueTime(); + + expect(time.toString()).to.equal('00:00:00'); + }); + + it('should create a time from a string', () => { + const timeString = '12:32:47' + , time = new HueTime(timeString); + + expect(time.toString()).to.equal(timeString); + }); + + it('should create a time from a date', () => { + const date = new Date() + , time = new HueTime(date); + + expect(time.hours).to.equal(date.getHours()); + expect(time.minutes).to.equal(date.getMinutes()); + expect(time.seconds).to.equal(date.getSeconds()); + }); + }); + + describe('setting hours, minutes, seconds', () => { + + it('should set to values', () => { + const time = new HueTime(); + time.seconds = 1; + + expect(time.toString()).to.equal('00:00:01'); + }); + + }); + + describe('#fromString()', () => { + + it('should process valid values', () => { + validTimeStrings.forEach(validTime => { + const time = new HueTime(); + time.fromString(validTime); + + expect(time.toString()).to.equal(validTime); + }); + }); + }); + + describe('#fromDate()', () => { + + it('should process valid values', () => { + validDates.forEach(date => { + const time = new HueTime(); + time.fromDate(date); + + expect(time.hours).to.equal(date.getHours()); + expect(time.minutes).to.equal(date.getMinutes()); + expect(time.seconds).to.equal(date.getSeconds()); + }); + }); + }); +}); \ No newline at end of file diff --git a/lib/model/timePatterns/RandomizedTime.js b/lib/model/timePatterns/RandomizedTime.js new file mode 100644 index 0000000..770a9bb --- /dev/null +++ b/lib/model/timePatterns/RandomizedTime.js @@ -0,0 +1,112 @@ +'use strict'; + +const timeUtil = require('./timeUtil') + , HueTime = require('./HueTime') + , HueDate = require('./HueDate') + , BridgeTime = require('./BridgeTime') + , ApiError = require('../../ApiError') +; + +const RANDOMIZED_TIME_REGEX = new RegExp(`^${timeUtil.getDatePattern()}T${timeUtil.getTimePattern()}A${timeUtil.getTimePattern('random')}$`); + +module.exports = class RandomizedTime extends BridgeTime { + + constructor(value) { + super(); + this._time = new HueTime(); + this._date = new HueDate(); + this._random = new HueTime(); + + if (value) { + this.value = value; + } + } + + static matches(value) { + return RANDOMIZED_TIME_REGEX.test(value); + } + + set value(value) { + if (value instanceof RandomizedTime) { + return this.value = value.toString(); + } else if (value instanceof Date) { + this._time.fromDate(value); + this._date.fromDate(value); + this._random = new HueTime(); + return this; + } + + const parsed = RANDOMIZED_TIME_REGEX.exec(value); + if (parsed) { + const time = this._time; + time.hours = parsed.groups.hours; + time.minutes = parsed.groups.minutes; + time.seconds = parsed.groups.seconds; + + const date = this._date; + date.year = parsed.groups.year; + date.month = parsed.groups.month; + date.day = parsed.groups.day; + + const random = this._random; + random.hours = parsed.groups.randomhours; + random.minutes = parsed.groups.randomminutes; + random.seconds = parsed.groups.randomseconds; + + return this; + } + + throw new ApiError(`Cannot create a randomized time from ${value}`); + } + + year(value) { + this._date.year = value; + return this; + } + + month(value) { + this._date.month = value; + return this; + } + + day(value) { + this._date.day = value; + return this; + } + + hours(value) { + this._time.hours = value; + return this; + } + + minutes(value) { + this._time.minutes = value; + return this; + } + + seconds(value) { + this._time.seconds = value; + return this; + } + + randomHours(value) { + this._random.hours = value; + return this; + } + + randomMinutes(value) { + this._random.minutes = value; + return this; + } + + randomSeconds(value) { + this._random.seconds = value; + return this; + } + + toString() { + return `${this._date.toString()}T${this._time.toString()}A${this._random.toString()}`; + } + + //TODO need to add support for random elements, along with other values hours, minutes, etc... +}; \ No newline at end of file diff --git a/lib/model/timePatterns/RandomizedTime.test.js b/lib/model/timePatterns/RandomizedTime.test.js new file mode 100644 index 0000000..db91f24 --- /dev/null +++ b/lib/model/timePatterns/RandomizedTime.test.js @@ -0,0 +1,58 @@ +'use strict'; + +const expect = require('chai').expect + , RandomizedTime = require('./RandomizedTime') + , HueDate = require('./HueDate') + , HueTime = require('./HueTime') +; + +describe('RandomizedTime', () => { + + + describe('constructor', () => { + + it('should create an RandomizedTime from no parameters', () => { + const randomizedTime = new RandomizedTime() + , time = new HueTime() + , date = new HueDate() + ; + + expect(randomizedTime.toString()).to.equal(`${date.toString()}T${time.toString()}A${time.toString()}`); + }); + + it('should create from a valid string', () => { + const timeString = '1977-08-12T12:00:00A00:00:10' + , randomizedTime = new RandomizedTime(timeString) + ; + + expect(randomizedTime.toString()).to.equal(timeString); + }); + + it('should create from a Date object', () => { + const date = new Date() + , randomizedTime = new RandomizedTime(date) + ; + + expect(randomizedTime.toString()).to.equal(`${fromDate(date)}A00:00:00`); + }); + + it('should create from setters', () => { + const time = new RandomizedTime(); + time.year(1977).month(12).day(1).hours(23).minutes(12).seconds(31).randomHours(1).randomMinutes(2).randomSeconds(3); + expect(time.toString()).to.equal('1977-12-01T23:12:31A01:02:03'); + }); + + //TODO allow cloning from another RandomizedTime? + }); +}); + +function fromDate(date) { + const hours = `${date.getUTCHours()}`.padStart(2, '0') + , minutes = `${date.getUTCMinutes()}`.padStart(2, '0') + , seconds = `${date.getUTCSeconds()}`.padStart(2, '0') + , month = `${date.getUTCMonth() + 1}`.padStart(2, '0') + , day = `${date.getUTCDate()}`.padStart(2, '0') + ; + + return `${date.getFullYear()}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} \ No newline at end of file diff --git a/lib/model/timePatterns/RandomizedTimer.js b/lib/model/timePatterns/RandomizedTimer.js new file mode 100644 index 0000000..529acc8 --- /dev/null +++ b/lib/model/timePatterns/RandomizedTimer.js @@ -0,0 +1,85 @@ +'use strict'; + +const timeUtil = require('./timeUtil') + , HueTime = require('./HueTime') + , BridgeTime = require('./BridgeTime') + , ApiError = require('../../ApiError') +; + + +const RANDOMIZED_TIMER_REGEX = new RegExp(`^PT${timeUtil.getTimePattern()}A${timeUtil.getTimePattern('random')}$`); + + +module.exports = class RandomizedTimer extends BridgeTime { + + constructor(value) { + super(); + this._time = new HueTime(); + this._random = new HueTime(); + + if (value) { + this.value = value; + } + } + + static matches(value) { + return RANDOMIZED_TIMER_REGEX.test(value); + } + + set value(value) { + if (value instanceof RandomizedTimer) { + this.value = value.toString(); + return this; + } + + const parsed = RANDOMIZED_TIMER_REGEX.exec(value); + if (parsed) { + const time = this._time; + time.hours = parsed.groups.hours; + time.minutes = parsed.groups.minutes; + time.seconds = parsed.groups.seconds; + + const random = this._random; + random.hours = parsed.groups.randomhours; + random.minutes = parsed.groups.randomminutes; + random.seconds = parsed.groups.randomseconds; + return this; + } + + throw new ApiError(`Cannot create a RandomizedTimer from ${value}`); + } + + hours(value) { + this._time.hours = value; + return this; + } + + minutes(value) { + this._time.minutes = value; + return this; + } + + seconds(value) { + this._time.seconds = value; + return this; + } + + randomHours(value) { + this._random.hours = value; + return this; + } + + randomMinutes(value) { + this._random.minutes = value; + return this; + } + + randomSeconds(value) { + this._random.seconds = value; + return this; + } + + toString() { + return `PT${this._time.toString()}A${this._random.toString()}`; + } +}; \ No newline at end of file diff --git a/lib/model/timePatterns/RandomizedTimer.test.js b/lib/model/timePatterns/RandomizedTimer.test.js new file mode 100644 index 0000000..0932c41 --- /dev/null +++ b/lib/model/timePatterns/RandomizedTimer.test.js @@ -0,0 +1,81 @@ +'use strict'; + +const expect = require('chai').expect + , RandomizedTimer = require('./RandomizedTimer') + , ApiError = require('../../ApiError') +; + + +describe('#RandomizedTimer', () => { + + it('should create a valid empty timer', () => { + const result = new RandomizedTimer(); + expect(result.toString()).to.equal('PT00:00:00A00:00:00'); + }); + + it('should create a from a valid string', () => { + const time = 'PT06:15:30A00:00:01' + , result = new RandomizedTimer(time) + ; + expect(result.toString()).to.equal(time); + }); + + it('should create a valid timer from function calls', () => { + const result = new RandomizedTimer(); + result.hours(23).minutes(59).seconds(59); + expect(result.toString()).to.equal('PT23:59:59A00:00:00'); + }); + + it('should reject invalid hour value', () => { + const result = new RandomizedTimer(); + + try { + result.hours(-1); + expect.fail('should have errored'); + } catch (err) { + expect(err).to.be.instanceOf(ApiError); + expect(err.message).to.include('not within allowed limits'); + } + }); + + it('should reject invalid minute value', () => { + const result = new RandomizedTimer(); + + try { + result.minutes(100); + expect.fail('should have errored'); + } catch (err) { + expect(err).to.be.instanceOf(ApiError); + expect(err.message).to.include('not within allowed limit'); + } + }); + + it('should reject invalid second value', () => { + const result = new RandomizedTimer(); + + try { + result.seconds(60); + expect.fail('should have errored'); + } catch (err) { + expect(err).to.be.instanceOf(ApiError); + expect(err.message).to.include('not within allowed limits'); + } + }); + + it('should create from another timer', () => { + const source = new RandomizedTimer(); + source.hours(1).minutes(30).seconds(59).randomHours(2); + + const result = new RandomizedTimer(); + result.value = source; + expect(result.toString()).to.equal('PT01:30:59A02:00:00'); + }); + + it('should create from a valid timer string', () => { + const result = new RandomizedTimer() + , value = 'PT12:01:01A10:01:59' + ; + result.value = value; + expect(result.toString()).to.equal(value); + }); +}); \ No newline at end of file diff --git a/lib/model/timePatterns/RecurringRandomizedTime.js b/lib/model/timePatterns/RecurringRandomizedTime.js new file mode 100644 index 0000000..aa6bb5a --- /dev/null +++ b/lib/model/timePatterns/RecurringRandomizedTime.js @@ -0,0 +1,98 @@ +'use strict'; + +const RecurringTime = require('./RecurringTime') + , ApiError = require('../../ApiError') + , HueTime = require('./HueTime') + , dateUtil = require('./timeUtil') +; + +const RECURRING_RANDOMIZED_TIME_REGEX = new RegExp(`^W(?[0-9]{3})/T${dateUtil.getTimePattern()}A${dateUtil.getTimePattern('random')}$`); + +module.exports = class RecurringRandomizedTime extends RecurringTime { + + constructor() { + super(); + this._random = new HueTime(); + + if (arguments.length > 0) { + this.setValue.apply(this, Array.from(arguments)); + } + } + + static matches(value) { + return RECURRING_RANDOMIZED_TIME_REGEX.test(value); + } + + setValue() { + // This is all a little convoluted due to large number of parameters it supports, could do with some work on making + // it clearer as to the path (although tests do provide coverage). + let weekdays = null + , date = null + ; + + if (arguments.length > 1) { + weekdays = arguments[0]; + date = arguments[1]; + } else if (arguments.length === 1) { + const argOne = arguments[0]; + if (argOne instanceof RecurringRandomizedTime) { + return this.setValue(argOne.toString()); + } else if (argOne instanceof Date) { + date = argOne + } else if (Number.isInteger(argOne)) { + weekdays = argOne; + } + } + + if (date) { + this._time.fromDate(date); + } + + if (weekdays) { + this.weekdays(weekdays); + } + + const parsed = RECURRING_RANDOMIZED_TIME_REGEX.exec(arguments[0]); + if (parsed) { + const time = this._time; + time.hours = parsed.groups.hours; + time.minutes = parsed.groups.minutes; + time.seconds = parsed.groups.seconds; + + this.weekdays(parsed.groups.weekdays); + + const random = this._random; + random.hours = parsed.groups.randomhours; + random.minutes = parsed.groups.randomminutes; + random.seconds = parsed.groups.randomseconds; + + return this; + } + + if (!weekdays && !date) { + const values = Array.from(arguments).join(', '); + throw new ApiError(`Cannot create an recurring time from ${values}`); + } + + return this; + } + + randomHours(value) { + this._random.hours = value; + return this; + } + + randomMinutes(value) { + this._random.minutes = value; + return this; + } + + randomSeconds(value) { + this._random.seconds = value; + return this; + } + + toString() { + return `${super.toString()}A${this._random.toString()}`; + } +} \ No newline at end of file diff --git a/lib/model/timePatterns/RecurringRandomizedTime.test.js b/lib/model/timePatterns/RecurringRandomizedTime.test.js new file mode 100644 index 0000000..f384fc1 --- /dev/null +++ b/lib/model/timePatterns/RecurringRandomizedTime.test.js @@ -0,0 +1,65 @@ +'use strict'; + +const expect = require('chai').expect + , RecurringRandomizedTime = require('./RecurringRandomizedTime') + , HueTime = require('./HueTime') + , timePatterns = require('./index') +; + +describe('RecurringRandomizedTime', () => { + + describe('constructor', () => { + + it('should create an RecurringTime from no parameters', () => { + const recurringTime = new RecurringRandomizedTime() + , time = new HueTime() + ; + expect(recurringTime.toString()).to.equal(`W${timePatterns.weekdays.ALL}/T${time.toString()}A${time.toString()}`); + }); + + it('should create from a valid string', () => { + const timeString = 'W001/T12:00:00A03:30:10' + , recurringTime = new RecurringRandomizedTime(timeString) + ; + expect(recurringTime.toString()).to.equal(timeString); + }); + + it('should create from a Date object', () => { + const date = new Date() + , recurringTime = new RecurringRandomizedTime(date) + ; + expect(recurringTime.toString()).to.equal(`W${timePatterns.weekdays.ALL}/T${fromDate(date)}A00:00:00`); + }); + + it('should create from weekdays and date parameter', () => { + const date = new Date() + , weekdays = timePatterns.weekdays.MONDAY | timePatterns.weekdays.TUESDAY | timePatterns.weekdays.WEDNESDAY + , recurringTime = new RecurringRandomizedTime(weekdays, date) + ; + expect(recurringTime.toString()).to.equal(`W${padWeekday(weekdays)}/T${fromDate(date)}A00:00:00`); + }); + + it('should create from setters', () => { + const time = new RecurringRandomizedTime(); + time.weekdays(timePatterns.weekdays.MONDAY) + .hours(23).minutes(12).seconds(32) + .randomHours(1).randomMinutes(2).randomSeconds(59); + expect(time.toString()).to.equal(`W${padWeekday(timePatterns.weekdays.MONDAY)}/T23:12:32A01:02:59`); + }); + + //TODO allow cloning from another RecurringRandomizedTime? + }); +}); + +function padWeekday(val) { + return `${val}`.padStart(3, '0'); +} + +function fromDate(date) { + const hours = `${date.getUTCHours()}`.padStart(2, '0') + , minutes = `${date.getUTCMinutes()}`.padStart(2, '0') + , seconds = `${date.getUTCSeconds()}`.padStart(2, '0') + ; + + return `${hours}:${minutes}:${seconds}`; +} \ No newline at end of file diff --git a/lib/model/timePatterns/RecurringRandomizedTimer.js b/lib/model/timePatterns/RecurringRandomizedTimer.js new file mode 100644 index 0000000..b42270c --- /dev/null +++ b/lib/model/timePatterns/RecurringRandomizedTimer.js @@ -0,0 +1,100 @@ +'use strict'; + +const timeUtil = require('./timeUtil') + , HueTime = require('./HueTime') + , BridgeTime = require('./BridgeTime') + , ApiError = require('../../ApiError') + , types = require('../../types') +; + + +const RECURRING_RANDOMIZED_TIMER_REGEX = new RegExp(`^R(?[0-9]{0,2})/PT${timeUtil.getTimePattern()}A${timeUtil.getTimePattern('random')}$`); + +const REOCCURRANCE_ATTRIBUTE = types.uint8({name: 'reoccurs', min: 0, max: 99, defaultValue: 0, optional: true}); + +module.exports = class RecurringRandomizedTimer extends BridgeTime { + + constructor(value) { + super(); + this._time = new HueTime(); + this._random = new HueTime(); + this.reoccurs(); + + if (value) { + this.value = value; + } + } + + static matches(value) { + return RECURRING_RANDOMIZED_TIMER_REGEX.test(value); + } + + set value(value) { + if (value instanceof RecurringRandomizedTimer) { + this.value = value.toString(); + return this; + } + + const parsed = RECURRING_RANDOMIZED_TIMER_REGEX.exec(value); + if (parsed) { + const time = this._time; + time.hours = parsed.groups.hours; + time.minutes = parsed.groups.minutes; + time.seconds = parsed.groups.seconds; + + const random = this._random; + random.hours = parsed.groups.randomhours; + random.minutes = parsed.groups.randomminutes; + random.seconds = parsed.groups.randomseconds; + + this.reoccurs(parsed.groups.times.length === 0 ? 0 : parsed.groups.times); + return this; + } + + throw new ApiError(`Cannot create a RandomizedTimer from ${value}`); + } + + hours(value) { + this._time.hours = value; + return this; + } + + minutes(value) { + this._time.minutes = value; + return this; + } + + seconds(value) { + this._time.seconds = value; + return this; + } + + randomHours(value) { + this._random.hours = value; + return this; + } + + randomMinutes(value) { + this._random.minutes = value; + return this; + } + + randomSeconds(value) { + this._random.seconds = value; + return this; + } + + reoccurs(value) { + this._reocurrance = REOCCURRANCE_ATTRIBUTE.getValue(value); + return this; + } + + toString() { + const reoccurs = this._reocurrance; + let limit = ''; + if (reoccurs !== 0) { + limit = `${reoccurs}`.padStart(2, '0'); + } + return `R${limit}/PT${this._time.toString()}A${this._random.toString()}`; + } +}; \ No newline at end of file diff --git a/lib/model/timePatterns/RecurringRandomizedTimer.test.js b/lib/model/timePatterns/RecurringRandomizedTimer.test.js new file mode 100644 index 0000000..b7c1a53 --- /dev/null +++ b/lib/model/timePatterns/RecurringRandomizedTimer.test.js @@ -0,0 +1,81 @@ +'use strict'; + +const expect = require('chai').expect + , RecurringRandomizedTimer = require('./RecurringRandomizedTimer') + , ApiError = require('../../ApiError') +; + + +describe('#RecurringRandomizedTimer', () => { + + it('should create a valid empty timer', () => { + const result = new RecurringRandomizedTimer(); + expect(result.toString()).to.equal('R/PT00:00:00A00:00:00'); + }); + + it('should create a from a valid string', () => { + const time = 'R/PT06:15:30A00:00:01' + , result = new RecurringRandomizedTimer(time) + ; + expect(result.toString()).to.equal(time); + }); + + it('should create a valid timer from function calls', () => { + const result = new RecurringRandomizedTimer(); + result.hours(23).minutes(59).seconds(59).reoccurs(1); + expect(result.toString()).to.equal('R01/PT23:59:59A00:00:00'); + }); + + it('should reject invalid hour value', () => { + const result = new RecurringRandomizedTimer(); + + try { + result.hours(-1); + expect.fail('should have errored'); + } catch (err) { + expect(err).to.be.instanceOf(ApiError); + expect(err.message).to.include('not within allowed limits'); + } + }); + + it('should reject invalid minute value', () => { + const result = new RecurringRandomizedTimer(); + + try { + result.minutes(100); + expect.fail('should have errored'); + } catch (err) { + expect(err).to.be.instanceOf(ApiError); + expect(err.message).to.include('not within allowed limit'); + } + }); + + it('should reject invalid second value', () => { + const result = new RecurringRandomizedTimer(); + + try { + result.seconds(60); + expect.fail('should have errored'); + } catch (err) { + expect(err).to.be.instanceOf(ApiError); + expect(err.message).to.include('not within allowed limits'); + } + }); + + it('should create from another timer', () => { + const source = new RecurringRandomizedTimer(); + source.hours(1).minutes(30).seconds(59).randomHours(2); + + const result = new RecurringRandomizedTimer(); + result.value = source; + expect(result.toString()).to.equal('R/PT01:30:59A02:00:00'); + }); + + it('should create from a valid timer string', () => { + const result = new RecurringRandomizedTimer() + , value = 'R50/PT12:01:01A10:01:59' + ; + result.value = value; + expect(result.toString()).to.equal(value); + }); +}); \ No newline at end of file diff --git a/lib/model/timePatterns/RecurringTime.js b/lib/model/timePatterns/RecurringTime.js new file mode 100644 index 0000000..9e26a45 --- /dev/null +++ b/lib/model/timePatterns/RecurringTime.js @@ -0,0 +1,106 @@ +'use strict'; + +const BridgeTime = require('./BridgeTime') + , timeUtil = require('./timeUtil') + , HueTime = require('./HueTime') + , ApiError = require('../../ApiError') + , types = require('../../types') +; + + +const RECURRING_TIME_REGEX = new RegExp(`^W(?[0-9]{3})/T${timeUtil.getTimePattern()}$`); + +const WEEKDAY_ATTRIBUTE = types.uint8({name: 'weekdays', min: 1, max: timeUtil.weekdays.ALL}); + +module.exports = class RecurringTime extends BridgeTime { + + constructor(weekdays, value) { + super(); + + this._time = new HueTime(); + this._weekdays = timeUtil.weekdays.ALL; + + if (arguments.length > 0) { + this.setValue.apply(this, Array.from(arguments)); + } + } + + static matches(value) { + return RECURRING_TIME_REGEX.test(value); + } + + setValue() { + // This is all a little convoluted due to large number of parameters it supports, could do with some work on making + // it clearer as to the path (although tests do provide coverage). + let weekdays = null + , date = null + ; + + if (arguments.length > 1) { + weekdays = arguments[0]; + date = arguments[1]; + } else if (arguments.length === 1) { + const argOne = arguments[0]; + if (argOne instanceof RecurringTime) { + return this.setValue(argOne.toString()); + } else if (argOne instanceof Date) { + date = argOne + } else if (Number.isInteger(argOne)) { + weekdays = argOne; + } + } + + if (date) { + this._time.fromDate(date); + } + + if (weekdays) { + this.weekdays(weekdays); + } + + const parsed = RECURRING_TIME_REGEX.exec(arguments[0]); + if (parsed) { + const time = this._time; + time.hours = parsed.groups.hours; + time.minutes = parsed.groups.minutes; + time.seconds = parsed.groups.seconds; + this.weekdays(parsed.groups.weekdays); + return this; + } + + if (!weekdays && !date) { + const values = Array.from(arguments).join(', '); + throw new ApiError(`Cannot create an recurring time from ${values}`); + } + + return this; + } + + get weekdaysString() { + return `${this._weekdays}`.padStart(3, '0'); + } + + weekdays(value) { + this._weekdays = WEEKDAY_ATTRIBUTE.getValue(value); + return this; + } + + hours(value) { + this._time.hours = value; + return this; + } + + minutes(value) { + this._time.minutes = value; + return this; + } + + seconds(value) { + this._time.seconds = value; + return this; + } + + toString() { + return `W${this.weekdaysString}/T${this._time.toString()}`; + } +}; \ No newline at end of file diff --git a/lib/model/timePatterns/RecurringTime.test.js b/lib/model/timePatterns/RecurringTime.test.js new file mode 100644 index 0000000..5b133f7 --- /dev/null +++ b/lib/model/timePatterns/RecurringTime.test.js @@ -0,0 +1,64 @@ +'use strict'; + +const expect = require('chai').expect + , RecurringTime = require('./RecurringTime') + , HueTime = require('./HueTime') + , timePatterns = require('./index') +; + +describe('RecurringTime', () => { + + describe('constructor', () => { + + it('should create an RecurringTime from no parameters', () => { + const recurringTime = new RecurringTime() + , time = new HueTime() + ; + + expect(recurringTime.toString()).to.equal(`W${timePatterns.weekdays.ALL}/T${time.toString()}`); + }); + + it('should create from a valid string', () => { + const timeString = 'W001/T12:00:00' + , recurringTime = new RecurringTime(timeString) + ; + expect(recurringTime.toString()).to.equal(timeString); + }); + + it('should create from a Date object', () => { + const date = new Date() + , recurringTime = new RecurringTime(date) + ; + expect(recurringTime.toString()).to.equal(`W${timePatterns.weekdays.ALL}/T${fromDate(date)}`); + }); + + it('should create from weekdays and date parameter', () => { + const date = new Date() + , weekdays = timePatterns.weekdays.MONDAY | timePatterns.weekdays.TUESDAY | timePatterns.weekdays.WEDNESDAY + , recurringTime = new RecurringTime(weekdays, date) + ; + expect(recurringTime.toString()).to.equal(`W${padWeekday(weekdays)}/T${fromDate(date)}`); + }); + + it('should create from setters', () => { + const time = new RecurringTime(); + time.hours(23).minutes(12).seconds(32).weekdays(timePatterns.weekdays.MONDAY); + expect(time.toString()).to.equal(`W${padWeekday(timePatterns.weekdays.MONDAY)}/T23:12:32`); + }); + + //TODO allow cloning from another RecurringTime? + }); +}); + +function padWeekday(val) { + return `${val}`.padStart(3, '0'); +} + +function fromDate(date) { + const hours = `${date.getUTCHours()}`.padStart(2, '0') + , minutes = `${date.getUTCMinutes()}`.padStart(2, '0') + , seconds = `${date.getUTCSeconds()}`.padStart(2, '0') + ; + + return `${hours}:${minutes}:${seconds}`; +} \ No newline at end of file diff --git a/lib/model/timePatterns/RecurringTimer.js b/lib/model/timePatterns/RecurringTimer.js new file mode 100644 index 0000000..d644eca --- /dev/null +++ b/lib/model/timePatterns/RecurringTimer.js @@ -0,0 +1,82 @@ +'use strict'; + +const timeUtil = require('./timeUtil') + , HueTime = require('./HueTime') + , BridgeTime = require('./BridgeTime') + , ApiError = require('../../ApiError') + , types = require('../../types') +; + + +const RECURRING_TIMER_REGEX = new RegExp(`^R(?[0-9]{0,2})/PT${timeUtil.getTimePattern()}$`); + +const REOCCURRANCE_ATTRIBUTE = types.uint8({name: 'reoccurs', min: 0, max: 99, defaultValue: 0, optional: true}); + +module.exports = class RecurringTimer extends BridgeTime { + + constructor(value) { + super(); + this._time = new HueTime(); + this.reoccurs(); + + if (value) { + this.value = value; + } + } + + static matches(value) { + return RECURRING_TIMER_REGEX.test(value); + } + + set value(value) { + if (value instanceof RecurringTimer) { + this.value = value.toString(); + } else if (value instanceof Date) { + this._time.fromDate(value); + return this; + } + + const parsed = RECURRING_TIMER_REGEX.exec(value); + if (parsed) { + const time = this._time; + time.hours = parsed.groups.hours; + time.minutes = parsed.groups.minutes; + time.seconds = parsed.groups.seconds; + + this.reoccurs(parsed.groups.times.length === 0 ? 0 : parsed.groups.times); + return this; + } + + throw new ApiError(`Cannnot create a Timer from ${value}`); + } + + hours(value) { + this._time.hours = value; + return this; + } + + minutes(value) { + this._time.minutes = value; + return this; + } + + seconds(value) { + this._time.seconds = value; + return this; + } + + reoccurs(value) { + this._reocurrance = REOCCURRANCE_ATTRIBUTE.getValue(value); + return this; + } + + toString() { + const reoccurs = this._reocurrance; + let limit = ''; + if (reoccurs !== 0) { + limit = `${reoccurs}`.padStart(2, '0'); + } + + return `R${limit}/PT${this._time.toString()}`; + } +}; \ No newline at end of file diff --git a/lib/model/timePatterns/RecurringTimer.test.js b/lib/model/timePatterns/RecurringTimer.test.js new file mode 100644 index 0000000..1dd4f96 --- /dev/null +++ b/lib/model/timePatterns/RecurringTimer.test.js @@ -0,0 +1,100 @@ +'use strict'; + +const expect = require('chai').expect + , RecurringTimer = require('./RecurringTimer') + , ApiError = require('../../ApiError') +; + + +describe('#RecurringTimer', () => { + + it('should create a valid empty timer', () => { + const result = new RecurringTimer(); + expect(result.toString()).to.equal('R/PT00:00:00'); + }); + + it('should create a from a valid string', () => { + const time = 'R/PT06:15:30' + , result = new RecurringTimer(time) + ; + expect(result.toString()).to.equal(time); + }); + + it('should create a valid timer from function calls', () => { + const result = new RecurringTimer(); + result.hours(23).minutes(59).seconds(59); + expect(result.toString()).to.equal('R/PT23:59:59'); + }); + + it('should reject invalid hour value', () => { + const result = new RecurringTimer(); + + try { + result.hours(-1); + expect.fail('should have errored'); + } catch (err) { + expect(err).to.be.instanceOf(ApiError); + expect(err.message).to.include('not within allowed limits'); + } + }); + + it('should reject invalid minute value', () => { + const result = new RecurringTimer(); + + try { + result.minutes(100); + expect.fail('should have errored'); + } catch (err) { + expect(err).to.be.instanceOf(ApiError); + expect(err.message).to.include('not within allowed limit'); + } + }); + + it('should reject invalid second value', () => { + const result = new RecurringTimer(); + + try { + result.seconds(60); + expect.fail('should have errored'); + } catch (err) { + expect(err).to.be.instanceOf(ApiError); + expect(err.message).to.include('not within allowed limits'); + } + }); + + it('should create from another timer', () => { + const source = new RecurringTimer(); + source.hours(1).minutes(30).seconds(59); + + const result = new RecurringTimer(); + result.value = source; + expect(result.toString()).to.equal('R/PT01:30:59'); + }); + + it('should create from a valid timer string', () => { + const result = new RecurringTimer() + , value = 'R/PT12:01:01' + ; + result.value = value; + expect(result.toString()).to.equal(value); + }); + + describe('repeating limited times', () => { + + it('should work from string value', () => { + const str = 'R02/PT00:00:10' + , timer = new RecurringTimer(str) + ; + expect(timer.toString()).to.equal(str); + }); + + it('should work from setting values', () => { + const timer = new RecurringTimer(); + + timer.minutes(10).reoccurs(65); + expect(timer.toString()).to.equal('R65/PT00:10:00'); + }); + }); + + +}); \ No newline at end of file diff --git a/lib/model/timePatterns/TimeInterval.js b/lib/model/timePatterns/TimeInterval.js new file mode 100644 index 0000000..8e54662 --- /dev/null +++ b/lib/model/timePatterns/TimeInterval.js @@ -0,0 +1,111 @@ +'use strict'; + +const BridgeTime = require('./BridgeTime') + , timeUtil = require('./timeUtil') + , HueTime = require('./HueTime') + , ApiError = require('../../ApiError') + , types = require('../../types') +; + + +const TIME_INTERVAL_REGEX = new RegExp(`^W(?[0-9]{3})/T${timeUtil.getTimePattern('from')}/T${timeUtil.getTimePattern('to')}$`); + +const WEEKDAY_ATTRIBUTE = types.uint8({name: 'weekdays', min: 1, max: timeUtil.weekdays.ALL}); + +module.exports = class TimeInterval extends BridgeTime { + + constructor() { + super(); + + this._from = new HueTime(); + this._to = new HueTime(); + this._weekdays = timeUtil.weekdays.ALL; + + if (arguments.length > 0) { + this.setValue.apply(this, Array.from(arguments)); + } + } + + static matches(value) { + return TIME_INTERVAL_REGEX.test(value); + } + + setValue() { + const argOne = arguments[0]; + if (argOne instanceof TimeInterval) { + return this.setValue(argOne.toString()); + } + + const parsed = TIME_INTERVAL_REGEX.exec(arguments[0]); + if (parsed) { + const from = this._from; + from.hours = parsed.groups.fromhours; + from.minutes = parsed.groups.fromminutes; + from.seconds = parsed.groups.fromseconds; + + const to = this._to; + to.hours = parsed.groups.tohours; + to.minutes = parsed.groups.tominutes; + to.seconds = parsed.groups.toseconds; + + this.weekdays(parsed.groups.weekdays); + return this; + } + + const args = Array.from(arguments).join(', '); + throw new ApiError(`Cannot create a TimeInterval from ${args}`); + } + + get weekdaysString() { + return `${this._weekdays}`.padStart(3, '0'); + } + + weekdays(value) { + this._weekdays = WEEKDAY_ATTRIBUTE.getValue(value); + return this; + } + + from(date) { + this._from.fromDate(date); + return this; + } + + fromHours(value) { + this._from.hours = value; + return this; + } + + fromMinutes(value) { + this._from.minutes = value; + return this; + } + + fromSeconds(value) { + this._from.seconds = value; + return this; + } + + to(date) { + this._to.fromDate(date); + return this; + } + + toHours(value) { + this._to.hours = value; + return this; + } + + toMinutes(value) { + this._to.minutes = value; + return this; + } + + toSeconds(value) { + this._to.seconds = value; + return this; + } + + toString() { + return `W${this.weekdaysString}/T${this._from.toString()}/T${this._to.toString()}`; + } +}; \ No newline at end of file diff --git a/lib/model/timePatterns/TimeInterval.test.js b/lib/model/timePatterns/TimeInterval.test.js new file mode 100644 index 0000000..aa98693 --- /dev/null +++ b/lib/model/timePatterns/TimeInterval.test.js @@ -0,0 +1,80 @@ +'use strict'; + +const expect = require('chai').expect + , TimeInterval = require('./TimeInterval') + , HueTime = require('./HueTime') + , timePatterns = require('./index') +; + +describe('TimeInterval', () => { + + describe('constructor', () => { + + it('should create an RecurringTime from no parameters', () => { + const recurringTime = new TimeInterval() + , time = new HueTime() + ; + + expect(recurringTime.toString()).to.equal(`W${timePatterns.weekdays.ALL}/T${time.toString()}/T${time.toString()}`); + }); + + it('should create from a valid string', () => { + const timeString = 'W001/T12:00:00/T13:00:00' + , recurringTime = new TimeInterval(timeString) + ; + expect(recurringTime.toString()).to.equal(timeString); + }); + + it('should fail create from a Date object', () => { + try { + new TimeInterval(new Date()); + expect.fail('should not get here'); + } catch (err) { + expect(err.message).to.contain('Cannot'); + } + }); + + it('should create from weekdays and date parameter', () => { + + }); + + it('should create from setters', () => { + const time = new TimeInterval(); + time.fromHours(23).fromMinutes(12).fromSeconds(32).weekdays(timePatterns.weekdays.MONDAY); + expect(time.toString()).to.equal(`W${padWeekday(timePatterns.weekdays.MONDAY)}/T23:12:32/T00:00:00`); + }); + + it('should create from setters', () => { + const time = new TimeInterval(); + + time.weekdays(timePatterns.weekdays.MONDAY | timePatterns.weekdays.FRIDAY) + .fromHours(12) + .toHours(14).toMinutes(30) + ; + expect(time.toString()).to.equal(`W${padWeekday(timePatterns.weekdays.MONDAY | timePatterns.weekdays.FRIDAY)}/T12:00:00/T14:30:00`); + }); + + it('should be able to be set using dates', () => { + const time = new TimeInterval() + , from = new Date('2019-01-01T02:31:01') + , to = new Date('2019-02-03T02:32:40') + ; + + time.weekdays(timePatterns.weekdays.SUNDAY).from(from).to(to); + expect(time.toString()).to.equal(`W${padWeekday(timePatterns.weekdays.SUNDAY)}/T${fromDate(from)}/T${fromDate(to)}`); + }); + }); +}); + +function padWeekday(val) { + return `${val}`.padStart(3, '0'); +} + +function fromDate(date) { + const hours = `${date.getUTCHours()}`.padStart(2, '0') + , minutes = `${date.getUTCMinutes()}`.padStart(2, '0') + , seconds = `${date.getUTCSeconds()}`.padStart(2, '0') + ; + + return `${hours}:${minutes}:${seconds}`; +} \ No newline at end of file diff --git a/lib/model/timePatterns/Timer.js b/lib/model/timePatterns/Timer.js new file mode 100644 index 0000000..dc7c839 --- /dev/null +++ b/lib/model/timePatterns/Timer.js @@ -0,0 +1,64 @@ +'use strict'; + +const timeUtil = require('./timeUtil') + , HueTime = require('./HueTime') + , BridgeTime = require('./BridgeTime') + , ApiError = require('../../ApiError') +; + + +const TIMER_REGEX = new RegExp(`^PT${timeUtil.getTimePattern()}$`); + + +module.exports = class Timer extends BridgeTime { + + constructor(value) { + super(); + this._time = new HueTime(); + + if (value) { + this.value = value; + } + } + + static matches(value) { + return TIMER_REGEX.test(value); + } + + set value(value) { + if (value instanceof Timer) { + this.value = value.toString(); + return this; + } + + const parsed = TIMER_REGEX.exec(value); + if (parsed) { + const time = this._time; + time.hours = parsed.groups.hours; + time.minutes = parsed.groups.minutes; + time.seconds = parsed.groups.seconds; + return this; + } + + throw new ApiError(`Cannot create a Timer from ${value}`); + } + + hours(value) { + this._time.hours = value; + return this; + } + + minutes(value) { + this._time.minutes = value; + return this; + } + + seconds(value) { + this._time.seconds = value; + return this; + } + + toString() { + return `PT${this._time.toString()}`; + } +}; \ No newline at end of file diff --git a/lib/model/datetime/Timer.test.js b/lib/model/timePatterns/Timer.test.js similarity index 65% rename from lib/model/datetime/Timer.test.js rename to lib/model/timePatterns/Timer.test.js index 505659e..253c277 100644 --- a/lib/model/datetime/Timer.test.js +++ b/lib/model/timePatterns/Timer.test.js @@ -2,7 +2,7 @@ const expect = require('chai').expect , Timer = require('./Timer') - , ApiError = require('../../../index').ApiError + , ApiError = require('../../ApiError') ; @@ -10,13 +10,19 @@ describe('#Timer', () => { it('should create a valid empty timer', () => { const result = new Timer(); - expect(result).to.be.instanceOf(Timer); expect(result.toString()).to.equal('PT00:00:00'); }); + it('should create a from a valid string', () => { + const time = 'PT06:15:30' + , result = new Timer(time) + ; + expect(result.toString()).to.equal(time); + }); + it('should create a valid timer from function calls', () => { const result = new Timer(); - result.hour(23).minute(59).second(59); + result.hours(23).minutes(59).seconds(59); expect(result.toString()).to.equal('PT23:59:59'); }); @@ -24,11 +30,11 @@ describe('#Timer', () => { const result = new Timer(); try { - result.hour(-1); + result.hours(-1); expect.fail('should have errored'); - } catch(err) { + } catch (err) { expect(err).to.be.instanceOf(ApiError); - expect(err.message).to.include('Invalid hour value'); + expect(err.message).to.include('not within allowed limits'); } }); @@ -36,11 +42,11 @@ describe('#Timer', () => { const result = new Timer(); try { - result.minute(100); + result.minutes(100); expect.fail('should have errored'); - } catch(err) { + } catch (err) { expect(err).to.be.instanceOf(ApiError); - expect(err.message).to.include('Invalid minute value'); + expect(err.message).to.include('not within allowed limit'); } }); @@ -48,19 +54,19 @@ describe('#Timer', () => { const result = new Timer(); try { - result.second(60); + result.seconds(60); expect.fail('should have errored'); - } catch(err) { + } catch (err) { expect(err).to.be.instanceOf(ApiError); - expect(err.message).to.include('Invalid second value'); + expect(err.message).to.include('not within allowed limits'); } }); it('should create from another timer', () => { - const source = new Timer().hour(1).minute(30).second(59) - , result = new Timer() - ; + const source = new Timer(); + source.hours(1).minutes(30).seconds(59); + const result = new Timer(); result.value = source; expect(result.toString()).to.equal('PT01:30:59'); }); diff --git a/lib/model/timePatterns/index.js b/lib/model/timePatterns/index.js new file mode 100644 index 0000000..e908f92 --- /dev/null +++ b/lib/model/timePatterns/index.js @@ -0,0 +1,104 @@ +'use strict'; + +const BridgeTime = require('./BridgeTime') + , AbsoluteTime = require('./AbsoluteTime') + , RandomizedTime = require('./RandomizedTime') + , RecurringTime = require('./RecurringTime') + , RecurringRandomizedTime = require('./RecurringRandomizedTime') + , TimeInterval = require('./TimeInterval') + , Timer = require('./Timer') + , RecurringTimer = require('./RecurringTimer') + , RandomizedTimer = require('./RandomizedTimer') + , RecurringRandomizedTimer = require('./RecurringRandomizedTimer') + , ApiError = require('../../ApiError') + , timeUtil = require('./timeUtil') +; + + +module.exports = { + + weekdays: timeUtil.weekdays, + + isRecurring(value) { + if (value instanceof BridgeTime) { + return value instanceof RecurringTime + || value instanceof RecurringRandomizedTime + || value instanceof RecurringTimer + || value instanceof RecurringRandomizedTimer; + } else { + return RecurringTime.matches(value) + || RecurringRandomizedTime.matches(value) + || RecurringTimer.matches(value) + || RecurringRandomizedTimer.matches(value); + } + }, + + createAbsoluteTime(value) { + return new AbsoluteTime(value); + }, + + createRandomizedTime(value) { + return new RandomizedTime(value); + }, + + createRecurringTime(weekdays, value) { + return new RecurringTime(weekdays, value); + }, + + createRecurringRandomizedTime(value) { + return new RecurringRandomizedTime(value); + }, + + createTimeInterval(value) { + return new TimeInterval(value); + }, + + createTimer(value) { + return new Timer(value); + }, + + createRecurringTimer(value) { + return new RecurringTimer(value); + }, + + createRandomizedTimer(value) { + return new RandomizedTimer(value); + }, + + createRecurringRandomizedTimer(value) { + return new RecurringRandomizedTimer(value); + }, + + createFromString(str) { + if (AbsoluteTime.matches(str)) { + return new AbsoluteTime(str); + } else if (RecurringTime.matches(str)) { + return new RecurringTime(str); + } else if (Timer.matches(str)) { + return new Timer(str); + } else if (RandomizedTime.matches(str)) { + return new RandomizedTime(str); + } else if (RecurringRandomizedTime.matches(str)) { + return new RecurringRandomizedTime(str); + } else if (RecurringTimer.matches(str)) { + return new RecurringTimer(str); + } else if (RandomizedTimer.matches(str)) { + return new RandomizedTimer(str); + } else if (RecurringRandomizedTimer.matches(str)) { + return new RecurringRandomizedTimer(str); + } else { + throw new ApiError(`Unable to determine a valid time pattern for string: "${str}"`); + } + }, + + isTimePattern(str) { + return AbsoluteTime.matches(str) + || RecurringTime.matches(str) + || RandomizedTime.matches(str) + || RecurringRandomizedTime.matches(str) + || Timer.matches(str) + || RecurringTimer.matches(str) + || RandomizedTimer.matches(str) + || RecurringRandomizedTimer.matches(str); + } +}; \ No newline at end of file diff --git a/lib/model/timePatterns/timeUtil.js b/lib/model/timePatterns/timeUtil.js new file mode 100644 index 0000000..3965ee3 --- /dev/null +++ b/lib/model/timePatterns/timeUtil.js @@ -0,0 +1,34 @@ +'use strict'; + +module.exports = { + + getTimePattern: (name) => { + const two_digits = '[0-9]{2}' + , prefix = name || '' + ; + + return `(?<${prefix}hours>${two_digits}):(?<${prefix}minutes>${two_digits}):(?<${prefix}seconds>${two_digits})` + }, + + getDatePattern: (name) => { + const two_digits = '[0-9]{2}' + , four_digits = '[0-9]{4}' + , prefix = name || '' + ; + return `(?<${prefix}year>${four_digits})-(?<${prefix}month>${two_digits})-(?<${prefix}day>${two_digits})` + }, + + weekdays: { + MONDAY: 64, + TUESDAY: 32, + WEDNESDAY: 16, + THURSDAY: 8, + FRIDAY: 4, + SATURDAY: 2, + SUNDAY: 1, + + WEEKDAY: 124, + WEEKEND: 3, + ALL: 127, + } +}; From 03ee65ec5f21fd52569bb9dc595d430277d0c84e Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Sat, 30 Nov 2019 14:37:46 +0000 Subject: [PATCH 16/35] - Fixing issues with arguments from the timePatterns usage. --- lib/model/timePatterns/RecurringTime.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/model/timePatterns/RecurringTime.js b/lib/model/timePatterns/RecurringTime.js index 9e26a45..6a2b708 100644 --- a/lib/model/timePatterns/RecurringTime.js +++ b/lib/model/timePatterns/RecurringTime.js @@ -14,7 +14,7 @@ const WEEKDAY_ATTRIBUTE = types.uint8({name: 'weekdays', min: 1, max: timeUtil.w module.exports = class RecurringTime extends BridgeTime { - constructor(weekdays, value) { + constructor() { super(); this._time = new HueTime(); @@ -36,11 +36,13 @@ module.exports = class RecurringTime extends BridgeTime { , date = null ; - if (arguments.length > 1) { - weekdays = arguments[0]; - date = arguments[1]; - } else if (arguments.length === 1) { - const argOne = arguments[0]; + const args = Array.from(arguments).filter(arg => arg !== undefined && arg !== null); + + if (args.length > 1) { + weekdays = args[0]; + date = args[1]; + } else if (args.length === 1) { + const argOne = args[0]; if (argOne instanceof RecurringTime) { return this.setValue(argOne.toString()); } else if (argOne instanceof Date) { From 26260154d94ce1cd8519b9a0cd78bb3bb6d43b4d Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Sat, 30 Nov 2019 14:39:22 +0000 Subject: [PATCH 17/35] - Creation of various types of Groups so that attributes can be appropriately applied --- lib/model/groups/Entertainment.js | 82 +++++++ lib/model/groups/Group.js | 145 +++++++++++++ lib/model/groups/Group.test.js | 345 ++++++++++++++++++++++++++++++ lib/model/groups/LightGroup.js | 26 +++ lib/model/groups/Lightsource.js | 30 +++ lib/model/groups/Luminaire.js | 24 +++ lib/model/groups/Room.js | 42 ++++ lib/model/groups/Zone.js | 42 ++++ 8 files changed, 736 insertions(+) create mode 100644 lib/model/groups/Entertainment.js create mode 100644 lib/model/groups/Group.js create mode 100644 lib/model/groups/Group.test.js create mode 100644 lib/model/groups/LightGroup.js create mode 100644 lib/model/groups/Lightsource.js create mode 100644 lib/model/groups/Luminaire.js create mode 100644 lib/model/groups/Room.js create mode 100644 lib/model/groups/Zone.js diff --git a/lib/model/groups/Entertainment.js b/lib/model/groups/Entertainment.js new file mode 100644 index 0000000..6f2a449 --- /dev/null +++ b/lib/model/groups/Entertainment.js @@ -0,0 +1,82 @@ +'use strict'; + +const Group = require('./Group') + , types = require('../../types') +; + +const ATTRIBUTES = [ + types.string({name: 'type', defaultValue: 'Entertainment'}), + // iOS Hue application is defaulting the types to TV currently, also only TV and Other seem to work out of all the classes + types.choice({name: 'class', defaultValue: 'TV', validValues: ['TV', 'Other']}), + types.object({ + name: 'stream', + types: [ + types.string({name: 'proymode'}), + types.string({name: 'proxynode'}), + types.boolean({name: 'active'}), + types.string({name: 'owner'}), + ] + }), + types.object({name: 'locations'}), + types.list({name: 'lights', minEntries: 0, listType: types.string({name: 'lightId'})}), +]; + +/** + * A Group of lights that can be utilized in an Entertainment situation for streaming. + * + * There are limitations on which lights can be added to an Entertainment Group, as they need to support the ability + * to stream, which requires newer lights in the hue ecosystem. + * + * @type {Entertainment} + */ +module.exports = class Entertainment extends Group { + + constructor(id) { + super(ATTRIBUTES, id); + } + + set lights(value) { + return this.setAttributeValue('lights', value); + } + + /** @return {Array.string} */ + get lights() { + return this.getAttributeValue('lights'); + } + + /** + * @param value {string} + * @returns {Entertainment} + */ + set class(value) { + return this.setAttributeValue('class', value); + } + + /** + * @returns {string} + */ + get class() { + return this.getAttributeValue('class'); + } + + /** + * Obtains details of the stream on the Entertainment Group. + * + * @typedef Stream + * @type {object} + * @property proxymode {string} + * @property proxynode {string} + * @property active {boolean} The status of whether or not the stream is active + * @property owner {string} The owner (user id) of the stream if it is active + * + * @returns @type {Stream} + */ + get stream() { + return this.getAttributeValue('stream'); + } + + // TODO consider unpacking this in to something more user friendly + get locations() { + return this.getAttributeValue('locations'); + } +}; diff --git a/lib/model/groups/Group.js b/lib/model/groups/Group.js new file mode 100644 index 0000000..153759c --- /dev/null +++ b/lib/model/groups/Group.js @@ -0,0 +1,145 @@ +'use strict'; + +const BridgeObjectWithId = require('../BridgeObjectWithId') + , ApiError = require('../../ApiError') + , types = require('../../types') + , util = require('../../util') +; + +const ROOM_CLASSES = [ + 'Living room', + 'Kitchen', + 'Dining', + 'Bedroom', + 'Kids bedroom', + 'Bathroom', + 'Nursery', + 'Recreation', + 'Office', + 'Gym', + 'Hallway', + 'Toilet', + 'Front door', + 'Garage', + 'Terrace', + 'Garden', + 'Driveway', + 'Carport', + 'Other', + // The following are valid in 1.30 and higher of the API + 'Home', + 'Downstairs', + 'Upstairs', + 'Top floor', + 'Attic', + 'Guest room', + 'Staircase', + 'Lounge', + 'Man cave', + 'Computer', + 'Studio', + 'Music', + 'TV', + 'Reading', + 'Closet', + 'Storage', + 'Laundry room', + 'Balcony', + 'Porch', + 'Barbecue', + 'Pool', +]; + + +const ATTRIBUTES = [ + types.int8({name: 'id'}), + types.string({name: 'name', min: 0, max: 32}), + types.list({name: 'sensors', minEntries: 0, listType: types.string({name: 'sensorId'})}), + types.object({name: 'action'}), //TODO a lightstate + types.object({ + name: 'state', + types: [ + types.boolean({name: 'all_on'}), + types.boolean({name: 'any_on'}), + ] + }), + // Only present if the group contains a ZLLPresence or CLIPPresence + types.object({ + name: 'presence', + types: [ + types.string({name: 'lastupdated'}), + types.boolean({name: 'presence'}), + types.boolean({name: 'presence_all'}) + ] + }), + // Only present if the group contains a ZLLLightlevel or CLIPLightLevel + types.object({ + name: 'lightlevel', types: [ + types.string({name: 'lastupdated'}), + types.boolean({name: 'dark'}), + types.boolean({name: 'dark_all'}), + types.boolean({name: 'daylight'}), + types.boolean({name: 'daylight_any'}), + types.uint16({name: 'lightlevel'}), + types.uint16({name: 'lightlevel_min'}), + types.uint16({name: 'lightlevel_max'}), + ] + }), + types.boolean({name: 'recycle', defaultValue: false}), +]; + +module.exports = class Group extends BridgeObjectWithId { + + constructor(attributes, id) { + super(util.flatten(ATTRIBUTES, attributes), id); + + if (!this._attributes.type) { + throw new ApiError('Missing a valid type attribute for the Group'); + } + + if (!this._attributes.lights) { + throw new ApiError('Missing a valid lights attribute for the Group'); + } + } + + get name() { + return this.getAttributeValue('name'); + } + + set name(value) { + return this.setAttributeValue('name', value); + } + + set sensors(value) { + return this.setAttributeValue('sensors', value); + } + + /** @return {Array.string} */ + get sensors() { + return this.getAttributeValue('sensors'); + } + + /** @return {string} */ + get type() { + return this.getAttributeValue('type'); + } + + get action() { + // //TODO this is a lightstate + // return this.getRawDataValue('action'); + return this.getAttributeValue('action'); + } + + /** @return {boolean} */ + get recycle() { + return this.getAttributeValue('recycle'); + } + + get state() { + return this.getAttributeValue('state'); + } + + static getAllGroupClasses() { + return ROOM_CLASSES; + } +}; \ No newline at end of file diff --git a/lib/model/groups/Group.test.js b/lib/model/groups/Group.test.js new file mode 100644 index 0000000..9b3fe38 --- /dev/null +++ b/lib/model/groups/Group.test.js @@ -0,0 +1,345 @@ +'use strict'; + +const expect = require('chai').expect + , model = require('../index') +; + + +describe('Bridge Model - Group', () => { + + describe('#createFromJson()', () => { + + describe('LightGroup', () => { + + const LIGHTGROUP_PAYLOAD = { + "id": 1, + "name": "VRC 1", + "lights": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "sensors": [], + "type": "LightGroup", + "state": { + "all_on": false, + "any_on": true + }, + "recycle": false, + "action": { + "on": false, + "bri": 61, + "hue": 14988, + "sat": 141, + "effect": "none", + "xy": [ + 0.4575, + 0.4101 + ], + "ct": 366, + "alert": "select", + "colormode": "ct" + }, + "node_hue_api": { + "type": "group", + "version": 1 + } + }; + + it('should process valid payload', () => { + const payload = LIGHTGROUP_PAYLOAD + , group = model.createFromJson(payload) + ; + + expect(model.isGroupInstance(group)).to.be.true; + + expect(group).to.have.property('id').to.equal(payload.id); + expect(group).to.have.property('name').to.equal(payload.name); + expect(group).to.have.property('lights').to.have.members(payload.lights.map(id => Number(id))); + expect(group).to.have.property('sensors').to.be.empty; + expect(group).to.have.property('type').to.equal(payload.type); + expect(group).to.have.property('recycle').to.equal(payload.recycle); + expect(group).to.have.property('action').to.deep.equal(payload.action); + }); + }); + + describe('Zone', () => { + + const ZONE_PAYLOAD = { + "id": 1, + "name": "Testing Zone Creation", + "lights": [2, 3, 4], + "sensors": [], + "type": "Zone", + "state": {"all_on": false, "any_on": true}, + "recycle": false, + "class": "Other", + "action": { + "on": false, + "bri": 254, + "hue": 0, + "sat": 0, + "effect": "none", + "xy": [0.3804, 0.3768], + "ct": 366, + "alert": "select", + "colormode": "ct" + }, + "node_hue_api": {"type": "group", "version": 1} + } + + it('should process valid payload', () => { + const payload = ZONE_PAYLOAD + , group = model.createFromJson(payload) + ; + + expect(model.isGroupInstance(group)).to.be.true; + + expect(group).to.have.property('id').to.equal(payload.id); + expect(group).to.have.property('name').to.equal(payload.name); + expect(group).to.have.property('lights').to.have.members(payload.lights.map(id => Number(id))); + expect(group).to.have.property('sensors').to.be.empty; + expect(group).to.have.property('type').to.equal(payload.type); + expect(group).to.have.property('recycle').to.equal(payload.recycle); + expect(group).to.have.property('class').to.equal(payload.class); + expect(group).to.have.property('action').to.deep.equal(payload.action); + }) + }); + + //TODO + }); + + + describe('#createFromBridge()', () => { + + describe('LightGroup', () => { + + const LIGHTGROUP_PAYLOAD = { + "name": "VRC 1", + "lights": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8" + ], + "sensors": [], + "type": "LightGroup", + "state": { + "all_on": false, + "any_on": true + }, + "recycle": false, + "action": { + "on": false, + "bri": 61, + "hue": 14988, + "sat": 141, + "effect": "none", + "xy": [ + 0.4575, + 0.4101 + ], + "ct": 366, + "alert": "select", + "colormode": "ct" + } + }; + + it('should process valid payload', () => { + const id = 1 + , payload = LIGHTGROUP_PAYLOAD + , group = model.createFromBridge('group', id, payload) + ; + + expect(model.isGroupInstance(group)).to.be.true; + + expect(group).to.have.property('id').to.equal(id); + expect(group).to.have.property('name').to.equal(payload.name); + expect(group).to.have.property('lights').to.have.members(payload.lights.map(id => Number(id))); + expect(group).to.have.property('sensors').to.be.empty; + expect(group).to.have.property('type').to.equal(payload.type); + expect(group).to.have.property('recycle').to.equal(payload.recycle); + expect(group).to.have.property('action').to.deep.equal(payload.action); + //TODO state, all_on and any_on + }); + }); + + + describe('Zone', () => { + + const ZONE_PAYLOAD = { + "name": "Testing Zone Creation", + "lights": [ + "2", + "3", + "4" + ], + "sensors": [], + "type": "Zone", + "state": { + "all_on": false, + "any_on": true + }, + "recycle": false, + "class": "Other", + "action": { + "on": false, + "bri": 254, + "hue": 0, + "sat": 0, + "effect": "none", + "xy": [ + 0.3804, + 0.3768 + ], + "ct": 366, + "alert": "select", + "colormode": "ct" + } + }; + + + it('should process valid payload', () => { + const id = 1 + , payload = ZONE_PAYLOAD + , group = model.createFromBridge('group', id, payload) + ; + + expect(model.isGroupInstance(group)).to.be.true; + expect(group).to.have.property('id').to.equal(id); + expect(group).to.have.property('name').to.equal(payload.name); + expect(group).to.have.property('lights').to.have.members(payload.lights.map(id => Number(id))); + expect(group).to.have.property('sensors').to.be.empty; + expect(group).to.have.property('type').to.equal(payload.type); + expect(group).to.have.property('recycle').to.equal(payload.recycle); + expect(group).to.have.property('class').to.equal(payload.class); + expect(group).to.have.property('action').to.deep.equal(payload.action); + }); + }); + + + describe('Room', () => { + + const ROOM_PAYLAOD = { + "name": "Bedroom Lamps", + "lights": [ + "7", + "8" + ], + "sensors": [], + "type": "Room", + "state": { + "all_on": false, + "any_on": false + }, + "recycle": false, + "class": "Other", + "action": { + "on": false, + "bri": 61, + "hue": 14988, + "sat": 141, + "effect": "none", + "xy": [ + 0.4575, + 0.4101 + ], + "ct": 366, + "alert": "select", + "colormode": "ct" + } + } + }); + + + describe('Entertainment', () => { + + const ENTERTAINMENT_PAYLOAD = { + "name": "Lounge Entertainment", + "lights": [ + "18", + "37", + "38", + "17" + ], + "sensors": [], + "type": "Entertainment", + "state": { + "all_on": true, + "any_on": true + }, + "recycle": false, + "class": "TV", + "stream": { + "proxymode": "manual", + "proxynode": "/lights/22", + "active": false, + "owner": null + }, + "locations": { + "17": [ + -0.65, + -0.84, + 0 + ], + "18": [ + 0.69, + -0.85, + 0 + ], + "37": [ + -0.51, + 0.85, + 0 + ], + "38": [ + 0.44, + 0.84, + 0 + ] + }, + "action": { + "on": true, + "bri": 102, + "hue": 2595, + "sat": 127, + "effect": "none", + "xy": [ + 0.5095, + 0.3624 + ], + "ct": 459, + "alert": "select", + "colormode": "hs" + } + }; + + it('should process valid payload', () => { + const id = 1 + , payload = ENTERTAINMENT_PAYLOAD + , group = model.createFromBridge('group', id, payload) + ; + + expect(model.isGroupInstance(group)).to.be.true; + expect(group).to.have.property('id').to.equal(id); + expect(group).to.have.property('name').to.equal(payload.name); + expect(group).to.have.property('lights').to.have.members(payload.lights.map(id => Number(id))); + expect(group).to.have.property('sensors').to.be.empty; + expect(group).to.have.property('type').to.equal(payload.type); + expect(group).to.have.property('recycle').to.equal(payload.recycle); + expect(group).to.have.property('class').to.equal(payload.class); + expect(group).to.have.property('stream').to.deep.equal(payload.class); + expect(group).to.have.property('locations').to.deep.equal(payload.locations); + expect(group).to.have.property('action').to.deep.equal(payload.action); + console.log(JSON.stringify(group)); + }); + }); + }); +}); \ No newline at end of file diff --git a/lib/model/groups/LightGroup.js b/lib/model/groups/LightGroup.js new file mode 100644 index 0000000..8550e84 --- /dev/null +++ b/lib/model/groups/LightGroup.js @@ -0,0 +1,26 @@ +'use strict'; + +const Group = require('./Group') + , types = require('../../types') +; + +const ATTRIBUTES = [ + types.string({name: 'type', defaultValue: 'LightGroup'}), + types.list({name: 'lights', minEntries: 1, listType: types.string({name: 'lightId'})}), +]; + +module.exports = class LightGroup extends Group { + + constructor(id) { + super(ATTRIBUTES, id); + } + + set lights(value) { + return this.setAttributeValue('lights', value); + } + + /** @return {Array.string} */ + get lights() { + return this.getAttributeValue('lights'); + } +}; \ No newline at end of file diff --git a/lib/model/groups/Lightsource.js b/lib/model/groups/Lightsource.js new file mode 100644 index 0000000..6a04591 --- /dev/null +++ b/lib/model/groups/Lightsource.js @@ -0,0 +1,30 @@ +'use strict'; + +const Group = require('./Group') + , types = require('../../types') +; + +// TODO Do not have an example of this the API documentation is not correct as it refers to a standard LightGroup definition + +const ATTRIBUTES = [ + types.string({name: 'type', defaultValue: 'Lightsource'}), + types.list({name: 'lights', minEntries: 1, listType: types.string({name: 'lightId'})}), +]; + + +module.exports = class Luminaire extends Group { + + constructor(id) { + super(ATTRIBUTES, id); + } + + /** @return {Array.string} */ + get lights() { + return this.getAttributeValue('lights'); + } + + // TODO need to get one of these to test if we can set on it + // set lights(value) { + // return this.setAttributeValue('lights', value); + // } +}; \ No newline at end of file diff --git a/lib/model/groups/Luminaire.js b/lib/model/groups/Luminaire.js new file mode 100644 index 0000000..ccf6d87 --- /dev/null +++ b/lib/model/groups/Luminaire.js @@ -0,0 +1,24 @@ +'use strict'; + +const Group = require('./Group') + , types = require('../../types') +; + + +const ATTRIBUTES = [ + types.string({name: 'type', defaultValue: 'Luminaire'}), + types.list({name: 'lights', minEntries: 1, listType: types.string({name: 'lightId'})}), +]; + + +module.exports = class Luminaire extends Group { + + constructor(id) { + super(ATTRIBUTES, id); + } + + /** @return {Array.string} */ + get lights() { + return this.getAttributeValue('lights'); + } +}; \ No newline at end of file diff --git a/lib/model/groups/Room.js b/lib/model/groups/Room.js new file mode 100644 index 0000000..f53a1ed --- /dev/null +++ b/lib/model/groups/Room.js @@ -0,0 +1,42 @@ +'use strict'; + +const Group = require('./Group') + , types = require('../../types') +; + + +const ATTRIBUTES = [ + types.string({name: 'type', defaultValue: 'Room'}), + types.choice({name: 'class', defaultValue: 'Other', validValues: Group.getAllGroupClasses()}), + types.list({name: 'lights', minEntries: 0, listType: types.string({name: 'lightId'})}), +]; + + +module.exports = class Room extends Group { + + constructor(id) { + super(ATTRIBUTES, id); + } + + /** @return {Array.string} */ + get lights() { + return this.getAttributeValue('lights'); + } + + set lights(value) { + return this.setAttributeValue('lights', value); + } + + /** + * @param value {string} + * @returns {Room} + */ + set class(value) { + return this.setAttributeValue('class', value); + } + + /** @returns {string} */ + get class() { + return this.getAttributeValue('class'); + } +}; \ No newline at end of file diff --git a/lib/model/groups/Zone.js b/lib/model/groups/Zone.js new file mode 100644 index 0000000..6d76b24 --- /dev/null +++ b/lib/model/groups/Zone.js @@ -0,0 +1,42 @@ +'use strict'; + +const Group = require('./Group') + , types = require('../../types') +; + + +const ATTRIBUTES = [ + types.string({name: 'type', defaultValue: 'Zone'}), + types.choice({name: 'class', defaultValue: 'Other', validValues: Group.getAllGroupClasses()}), + types.list({name: 'lights', minEntries: 0, listType: types.string({name: 'lightId'})}), +]; + + +module.exports = class Zone extends Group { + + constructor(id) { + super(ATTRIBUTES, id); + } + + /** @return {Array.string} */ + get lights() { + return this.getAttributeValue('lights'); + } + + set lights(value) { + return this.setAttributeValue('lights', value); + } + + /** + * @param value {string} + * @returns {Zone} + */ + set class(value) { + return this.setAttributeValue('class', value); + } + + /** @returns {string} */ + get class() { + return this.getAttributeValue('class'); + } +}; \ No newline at end of file From f4404b65d437ec42d8bf3294be702a1f8e3d7637 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Sat, 30 Nov 2019 14:40:29 +0000 Subject: [PATCH 18/35] - BridgeObject and BridgeObjectWithId separation as there are some objects that do not have IDs like capabilities. --- lib/model/BridgeObject.js | 58 +-- lib/model/BridgeObjectWithId.js | 66 ++++ lib/model/Capabilities.js | 161 +++++++++ lib/model/Group.js | 144 -------- lib/model/Light.js | 18 +- lib/model/ResourceLink.js | 4 +- lib/model/Schedule.js | 101 ++---- lib/model/actions/BridgeAction.js | 51 +++ lib/model/actions/GroupStateAction.js | 56 +++ lib/model/actions/LightStateAction.js | 57 +++ lib/model/actions/SceneAction.js | 53 +++ .../actions/ScheduleStateAction.js | 6 +- lib/model/actions/SensorStateAction.js | 51 +++ lib/model/{rules => }/actions/index.js | 8 +- lib/model/datetime/AbsoluteTime.js | 90 ----- lib/model/datetime/AbsoluteTime.test.js | 36 -- lib/model/datetime/BridgeTime.js | 12 - lib/model/datetime/DateTimeUtil.js | 34 -- lib/model/datetime/HueDate.js | 54 --- lib/model/datetime/HueDate.test.js | 15 - lib/model/datetime/HueTime.js | 53 --- lib/model/datetime/HueTime.test.js | 15 - lib/model/datetime/RandomizedTime.js | 10 - lib/model/datetime/RecurringRandomizedTime.js | 1 - .../datetime/RecurringRandomizedTimer.js | 1 - lib/model/datetime/RecurringTime.js | 67 ---- lib/model/datetime/Timer.js | 54 --- lib/model/datetime/index.js | 27 -- lib/model/index.js | 134 +++++-- lib/model/index.test.js | 335 ++++++++++++++++++ lib/model/lightstate/States.js | 1 + lib/model/rules/Rule.js | 8 +- lib/model/rules/actions/GroupStateAction.js | 34 -- lib/model/rules/actions/LightStateAction.js | 34 -- lib/model/rules/actions/RuleAction.js | 57 --- lib/model/rules/actions/SceneAction.js | 30 -- lib/model/rules/actions/SensorStateAction.js | 30 -- lib/model/rules/conditions/GroupCondition.js | 2 +- lib/model/scenes/GroupScene.js | 2 +- lib/model/scenes/Scene.js | 4 +- lib/model/sensors/Sensor.js | 4 +- 41 files changed, 1017 insertions(+), 961 deletions(-) create mode 100644 lib/model/BridgeObjectWithId.js create mode 100644 lib/model/Capabilities.js delete mode 100644 lib/model/Group.js create mode 100644 lib/model/actions/BridgeAction.js create mode 100644 lib/model/actions/GroupStateAction.js create mode 100644 lib/model/actions/LightStateAction.js create mode 100644 lib/model/actions/SceneAction.js rename lib/model/{rules => }/actions/ScheduleStateAction.js (78%) create mode 100644 lib/model/actions/SensorStateAction.js rename lib/model/{rules => }/actions/index.js (94%) delete mode 100644 lib/model/datetime/AbsoluteTime.js delete mode 100644 lib/model/datetime/AbsoluteTime.test.js delete mode 100644 lib/model/datetime/BridgeTime.js delete mode 100644 lib/model/datetime/DateTimeUtil.js delete mode 100644 lib/model/datetime/HueDate.js delete mode 100644 lib/model/datetime/HueDate.test.js delete mode 100644 lib/model/datetime/HueTime.js delete mode 100644 lib/model/datetime/HueTime.test.js delete mode 100644 lib/model/datetime/RandomizedTime.js delete mode 100644 lib/model/datetime/RecurringRandomizedTime.js delete mode 100644 lib/model/datetime/RecurringRandomizedTimer.js delete mode 100644 lib/model/datetime/RecurringTime.js delete mode 100644 lib/model/datetime/Timer.js delete mode 100644 lib/model/datetime/index.js create mode 100644 lib/model/index.test.js delete mode 100644 lib/model/rules/actions/GroupStateAction.js delete mode 100644 lib/model/rules/actions/LightStateAction.js delete mode 100644 lib/model/rules/actions/RuleAction.js delete mode 100644 lib/model/rules/actions/SceneAction.js delete mode 100644 lib/model/rules/actions/SensorStateAction.js diff --git a/lib/model/BridgeObject.js b/lib/model/BridgeObject.js index 4392c4a..7db0ffb 100644 --- a/lib/model/BridgeObject.js +++ b/lib/model/BridgeObject.js @@ -10,21 +10,14 @@ module.exports = class BridgeObject { /** * @param attributes {Array.} - * @param id {number | null} */ - constructor(attributes, id) { + constructor(attributes) { this._attributes = {}; this._data = {}; attributes.forEach(attr => { this._attributes[attr.name] = attr; }); - - // Validate that we have an id definition - if (!this._attributes.id) { - throw new ApiError('All bridge objects must have an "id" definition'); - } - this.setAttributeValue('id', id); } /** @@ -58,55 +51,21 @@ module.exports = class BridgeObject { return this; } - /** - * @returns {string | number} - */ - get id() { - return this.getAttributeValue('id'); - } - - getJsonPayload() { - const data = this._bridgeData; - - data.node_hue_api = { - type: this.constructor.name.toLowerCase(), - version: 1 - }; - - return data; - } - - getHuePayload() { - const result = {}; - - Object.keys(this._attributes).forEach(name => { - const value = this.getAttributeValue(name); - if (value !== null && value !== undefined) { - result[name] = value; - } - }); - - return result; - // return this._bridgeData; - } - /** * @returns {string} */ toString() { - return `${this.constructor.name}\n id: ${this.id}`; + return `${this.constructor.name}`; } /** * @returns {string} */ toStringDetailed() { - let result = this.toString(); + let result = `${this.constructor.name}`; Object.keys(this._data).forEach(key => { - if (key !== 'id') { - result += `\n ${key}: ${JSON.stringify(this._data[key])}`; - } + result += `\n ${key}: ${JSON.stringify(this._data[key])}`; }); return result; @@ -135,13 +94,4 @@ module.exports = class BridgeObject { return self; } - - /** - * @returns {any | {}} - * @private - */ - get _bridgeData() { - // Return a copy so that it cannot be modified from outside - return Object.assign({}, this._data); - } }; diff --git a/lib/model/BridgeObjectWithId.js b/lib/model/BridgeObjectWithId.js new file mode 100644 index 0000000..d07826f --- /dev/null +++ b/lib/model/BridgeObjectWithId.js @@ -0,0 +1,66 @@ +'use strict'; + +const BridgeObject = require('./BridgeObject') + , ApiError = require('../ApiError.js') +; + +/** + * @typedef { import('../types/Type') } Type + * @type {BridgeObjectWithId} + */ +module.exports = class BridgeObjectWithId extends BridgeObject { + + /** + * @param attributes {Array.} + * @param id {number | null} + */ + constructor(attributes, id) { + super(attributes); + + // Validate that we have an id definition + if (!this._attributes.id) { + throw new ApiError('All bridge objects must have an "id" definition'); + } + this.setAttributeValue('id', id); + } + + /** + * @returns {string | number} + */ + get id() { + return this.getAttributeValue('id'); + } + + getJsonPayload() { + const data = this._bridgeData; + + data.node_hue_api = { + type: this.constructor.name.toLowerCase(), + version: 1 + }; + + return data; + } + + getHuePayload() { + const result = {}; + + Object.keys(this._attributes).forEach(name => { + const value = this.getAttributeValue(name); + if (value !== null && value !== undefined) { + result[name] = value; + } + }); + + return result; + } + + /** + * @returns {any | {}} + * @private + */ + get _bridgeData() { + // Return a copy so that it cannot be modified from outside + return Object.assign({}, this._data); + } +}; diff --git a/lib/model/Capabilities.js b/lib/model/Capabilities.js new file mode 100644 index 0000000..b0083d3 --- /dev/null +++ b/lib/model/Capabilities.js @@ -0,0 +1,161 @@ +'use strict'; + +const BridgeObject = require('./BridgeObject') + , types = require('../types') +; + +const ATTRIBUTES = [ + types.object({ + name: 'lights', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + ] + }), + types.object({ + name: 'sensors', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + types.object({ + name: 'clip', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + ] + }), + types.object({ + name: 'zll', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + ] + }), + types.object({ + name: 'zgp', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + ] + }) + ] + }), + types.object({ + name: 'groups', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + ] + }), + types.object({ + name: 'scenes', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + types.object({ + name: 'lightstates', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + ] + }), + ] + }), + types.object({ + name: 'schedules', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + ] + }), + types.object({ + name: 'rules', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + types.object({ + name: 'conditions', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + ] + }), + types.object({ + name: 'actions', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + ] + }), + ] + }), + types.object({ + name: 'resourcelinks', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + ] + }), + types.object({ + name: 'streaming', + types: [ + types.uint16({name: 'available'}), + types.uint16({name: 'total'}), + types.uint16({name: 'channels'}), + ] + }), + types.object({ + name: 'timezones', + types: [ + types.list({name: 'values', minEntries: 0, listType: types.string({name: 'timezone'})}), + ] + }), +]; + + +module.exports = class Capabilities extends BridgeObject { + + constructor() { + super(ATTRIBUTES); + } + + get lights() { + return this.getAttributeValue('lights'); + } + + get sensors() { + return this.getAttributeValue('sensors'); + } + + get groups() { + return this.getAttributeValue('groups'); + } + + get scenes() { + return this.getAttributeValue('scenes'); + } + + get schedules() { + return this.getAttributeValue('schedules'); + } + + get rules() { + return this.getAttributeValue('rules'); + } + + get resourceLinks() { + return this.getAttributeValue('resourcelinks'); + } + + get resourcelinks() { + return this.getAttributeValue('resourcelinks'); + } + + get streaming() { + return this.getAttributeValue('streaming'); + } + + get timezones() { + return this.getAttributeValue('timezones').values; + } +}; \ No newline at end of file diff --git a/lib/model/Group.js b/lib/model/Group.js deleted file mode 100644 index 8100a8f..0000000 --- a/lib/model/Group.js +++ /dev/null @@ -1,144 +0,0 @@ -'use strict'; - -const BridgeObject = require('./BridgeObject') - , types = require('../types') -; - -const ROOM_CLASSES = [ - 'Living room', - 'Kitchen', - 'Dining', - 'Bedroom', - 'Kids bedroom', - 'Bathroom', - 'Nursery', - 'Recreation', - 'Office', - 'Gym', - 'Hallway', - 'Toilet', - 'Front door', - 'Garage', - 'Terrace', - 'Garden', - 'Driveway', - 'Carport', - 'Other', - // The following are valid in 1.30 and higher of the API - 'Home', - 'Downstairs', - 'Upstairs', - 'Top floor', - 'Attic', - 'Guest room', - 'Staircase', - 'Lounge', - 'Man cave', - 'Computer', - 'Studio', - 'Music', - 'TV', - 'Reading', - 'Closet', - 'Storage', - 'Laundry room', - 'Balcony', - 'Porch', - 'Barbecue', - 'Pool', -]; - - -const ATTRIBUTES = [ - types.int8({name: 'id'}), - types.string({name: 'name', min: 0, max: 32}), - types.choice({name: 'type', validValues: ['LightGroup', 'Luminaire', 'LightSource', 'Room', 'Entertainment', 'Zone'], defaultValue: 'LightGroup'}), - types.list({name: 'lights', minEntries: 0, listType: types.uint8({name: 'lightId'})}), //TODO a room can be empty, but all others require at least one - types.list({name: 'sensors', minEntries: 0, listType: types.string({name: 'sensorId'})}), - types.object({name: 'action'}), - types.object({name: 'state'}), - types.object({name: 'presence', types: [types.string({name: 'lastupdated'}), types.boolean({name: 'presence'}), types.boolean({name: 'presence_all'})]}), - types.object({name: 'lightlevel', types: [ - types.string({name: 'lastupdated'}), - types.boolean({name: 'dark'}), - types.boolean({name: 'dark_all'}), - types.boolean({name: 'daylight'}), - types.boolean({name: 'daylight_any'}), - types.uint16({name: 'lightlevel'}), - types.uint16({name: 'lightlevel_min'}), - types.uint16({name: 'lightlevel_max'}), - ] - }), - types.boolean({name: 'recycle', defaultValue: false}), - types.choice({name: 'class', defaultValue: 'Other', validValues: ROOM_CLASSES}), - types.object({name: 'locations'}) -]; - -module.exports = class Group extends BridgeObject { - - constructor(id) { - super(ATTRIBUTES, id); - } - - get name() { - return this.getAttributeValue('name'); - } - - set name(value) { - return this.setAttributeValue('name', value); - } - - set lights(value) { - return this.setAttributeValue('lights', value); - } - - get lights() { - return this.getAttributeValue('lights'); - } - - set type(value) { - return this.setAttributeValue('type', value); - } - - get type() { - return this.getAttributeValue('type'); - } - - get action() { - // //TODO this is a lightstate - // return this.getRawDataValue('action'); - return this.getAttributeValue('action'); - } - - get recycle() { - return this.getAttributeValue('recycle'); - } - - get sensors() { - //TODO check what this actually returns when there is one - return this.getAttributeValue('sensors'); - } - - get state() { - return this.getAttributeValue('state'); - } - - set class(value) { - return this.setAttributeValue('class', value); - } - - get class() { - return this.getAttributeValue('class'); - } - - get locations() { - // TODO locations are specific to a room - return this.getAttributeValue('locations'); - } - - get stream() { - return this.getAttributeValue('stream'); - } - - //TODO need more getters -}; \ No newline at end of file diff --git a/lib/model/Light.js b/lib/model/Light.js index 2fbf762..e6b355c 100644 --- a/lib/model/Light.js +++ b/lib/model/Light.js @@ -1,6 +1,6 @@ 'use strict'; -const BridgeObject = require('./BridgeObject') +const BridgeObjectWithId = require('./BridgeObjectWithId') , colorGamuts = require('./colorGamuts') , types = require('../types') , util = require('../util') @@ -75,11 +75,19 @@ const ATTRIBUTES = [ types.object({name: 'state'}), types.object({name: 'capabilities'}), types.object({name: 'config'}), - types.object({name: 'swupdate'}), //TODO state and lastinstall + types.object({ + name: 'swupdate', + types: [ + types.string({name: 'state'}), + types.string({name: 'lastinstall'}), + ] + }), types.string({name: 'swversion'}), ]; -module.exports = class Light extends BridgeObject { +//TODO add support for making it eassier to set power failure modes config.startup.mode = 'powerfail' + +module.exports = class Light extends BridgeObjectWithId { constructor(id) { super(ATTRIBUTES, id); @@ -133,6 +141,10 @@ module.exports = class Light extends BridgeObject { return this.getAttributeValue('state'); } + get capabilities() { + return this.getAttributeValue('capabilities'); + } + get colorGamut() { if (this.mappedColorGamut && this.mappedColorGamut !== '2200K-6500K') { return colorGamuts.getColorGamut(this.mappedColorGamut); diff --git a/lib/model/ResourceLink.js b/lib/model/ResourceLink.js index 195f243..fd7b42f 100644 --- a/lib/model/ResourceLink.js +++ b/lib/model/ResourceLink.js @@ -1,7 +1,7 @@ 'use strict'; const ApiError = require('../ApiError') - , BridgeObject = require('./BridgeObject') + , BridgeObjectWithId = require('./BridgeObjectWithId') , parameters = require('../types') ; @@ -29,7 +29,7 @@ const ATTRIBUTES = [ ]; -module.exports = class ResourceLink extends BridgeObject { +module.exports = class ResourceLink extends BridgeObjectWithId { constructor(id) { super(ATTRIBUTES, id); diff --git a/lib/model/Schedule.js b/lib/model/Schedule.js index 3dc8f87..f1cb6be 100644 --- a/lib/model/Schedule.js +++ b/lib/model/Schedule.js @@ -1,37 +1,45 @@ 'use strict'; -const ApiError = require('../ApiError') - , BridgeObject = require('./BridgeObject') - , BridgeTime = require('./datetime/BridgeTime') - , dateTime = require('./datetime') - , parameters = require('../types') +const BridgeObjectWithId = require('./BridgeObjectWithId') + , types = require('../types') ; const ATTRIBUTES = [ - parameters.string({name: 'name', min: 0, max: 32, optional: true}), - parameters.string({name: 'description', min: 0, max: 64, optional: true}), - parameters.object({name: 'command', optional: false}), //TODO address, method, body - parameters.string({name: 'time'}), - parameters.string({name: 'created'}), - parameters.choice({name: 'status', validValues: ['enabled', 'disabled'], defaultValue: 'enabled'}), - parameters.boolean({name: 'autodelete', defaultValue: true}), - // parameters.string({name: 'localtime'}), //TODO need to use a time based object on this, need to define the parameters - parameters.boolean({name: 'recycle', defaultValue: false}), + types.uint8({name: 'id'}), + types.string({name: 'name', min: 0, max: 32, optional: true}), + types.string({name: 'description', min: 0, max: 64, optional: true}), + types.object({ + name: 'command', + optional: false, + types: [ + types.string({name: 'address', optional: false}), + types.choice({name: 'method', optional: false, validValues: ['POST', 'PUT', 'DELETE']}), + types.object({name: 'body', optional: false}), + ], + }), + types.string({name: 'time'}), + types.timePattern({name: 'localtime'}), + types.string({name: 'created'}), + types.choice({name: 'status', validValues: ['enabled', 'disabled'], defaultValue: 'enabled'}), + types.boolean({name: 'autodelete', defaultValue: true}), + types.boolean({name: 'recycle', defaultValue: false}), + types.string({name: 'starttime'}), ]; -module.exports = class Schedule extends BridgeObject { +module.exports = class Schedule extends BridgeObjectWithId { - constructor(data, id) { - super(ATTRIBUTES, data, id); + constructor(id) { + super(ATTRIBUTES, id); + } + + get name() { + return this.getAttributeValue('name'); + } - //TODO turn this into a parameter - if (data && data['localtime']) { - this._localtime = dateTime.create(data['localtime']); - } else { - this._localtime = null; - } + set name(value) { + return this.setAttributeValue('name', value) } get description() { @@ -43,7 +51,6 @@ module.exports = class Schedule extends BridgeObject { } get command() { - //TODO this is complex object, address, method, body return this.getAttributeValue('command'); } @@ -51,19 +58,16 @@ module.exports = class Schedule extends BridgeObject { return this.setAttributeValue('command', value); } - get localtime() { - return this._localtime; + get time() { + return this.getAttributeValue('time'); } - set localtime(time) { - if (time instanceof BridgeTime) { - this._localtime = time; - } else { - //TODO may need to properly cater for javascript Date - this._localtime = dateTime.create(time.toString()); - } + get localtime() { + return this.getAttributeValue('localtime'); + } - return this; + set localtime(value) { + return this.setAttributeValue('localtime', value); } get status() { @@ -93,33 +97,4 @@ module.exports = class Schedule extends BridgeObject { get created() { return this.getAttributeValue('created'); } - - //TODO this needs to be built in to the getBridgePayload() - // get payload() { - // const self = this - // , payload = {} - // ; - // - // // Mandatory values - // ['command'].forEach(key => { - // const value = self.getRawDataValue(key); - // if (!value) { - // throw new ApiError(`Mandatory Schedule parameter ${key} is missing.`); - // } - // payload[key] = value; - // }); - // - // // Mandatory localtime value - // payload.localtime = this._localtime.toString(); - // - // // Optional values - // ['name', 'description', 'autodelete', 'status', 'recycle'].forEach(key => { - // const value = self.getRawDataValue(key); - // if (value) { - // payload[key] = value; - // } - // }); - // - // return payload; - // } }; \ No newline at end of file diff --git a/lib/model/actions/BridgeAction.js b/lib/model/actions/BridgeAction.js new file mode 100644 index 0000000..20bdd44 --- /dev/null +++ b/lib/model/actions/BridgeAction.js @@ -0,0 +1,51 @@ +'use strict'; + +const BridgeObject = require('../BridgeObject') + , types = require('../../types') + , util = require('../../util') + , ApiError = require('../../ApiError') +; + +const ATTRIBUTES = [ + types.choice({name: 'method', validValues: ['PUT', 'POST', 'DELETE']}), + types.choice({name: 'target', validValues: ['rule', 'schedule']}), +]; + +module.exports = class BridgeAction extends BridgeObject { + + constructor(attributes, method) { + super(util.flatten(ATTRIBUTES, attributes)); + this.withMethod(method); + } + + get method() { + return this.getAttributeValue('method'); + } + + //TODO maybe unnecessary + get isRuleAction() { + return this.getAttributeValue('target') === 'rule'; + } + + //TODO maybe unnecessary + get isScheduleAction() { + return this.getAttributeValue('target') === 'schedule'; + } + + withMethod(value) { + return this.setAttributeValue('method', value); + } + + //TODO revisit this + get payload() { + return { + address: this.address, + method: this.method, + body: this.body + }; + } + + toString() { + return JSON.stringify(this.payload); + } +}; \ No newline at end of file diff --git a/lib/model/actions/GroupStateAction.js b/lib/model/actions/GroupStateAction.js new file mode 100644 index 0000000..7a518bb --- /dev/null +++ b/lib/model/actions/GroupStateAction.js @@ -0,0 +1,56 @@ +'use strict'; + +const BridgeAction = require('./BridgeAction') + , GroupState = require('../lightstate/GroupState') + , ApiError = require('../../ApiError') + , types = require('../../types') + , GroupIdPlaceHolder = require('../../placeholders/GroupIdPlaceholder') +; + +const GROUP_ID = new GroupIdPlaceHolder(); + +const ATTRIBUTES = [ + types.uint8({name: 'group'}), + types.object({name: 'state'}), //TODO this is an actual GroupState object +]; + + +module.exports = class GroupStateAction extends BridgeAction { + + constructor(group) { + super(ATTRIBUTES, 'PUT'); + this.group = group; + } + + get address() { + return `/groups/${this.group}/action`; + } + + get group() { + return this.getAttributeValue('group'); + } + + set group(value) { + const groupId = GROUP_ID.getValue({id: value}); + this.setAttributeValue('group', groupId); + } + + withState(state) { + let value = state; + + if (!(state instanceof GroupState)) { + value = new GroupState().populate(state); + } + + this.setAttributeValue('state', value.getPayload()); + return this; + } + + get body() { + const state = this.getAttributeValue('state'); + if (state) { + return state; + } + throw new ApiError('No state has been set on the GroupStateAction'); + } +}; diff --git a/lib/model/actions/LightStateAction.js b/lib/model/actions/LightStateAction.js new file mode 100644 index 0000000..43748e4 --- /dev/null +++ b/lib/model/actions/LightStateAction.js @@ -0,0 +1,57 @@ +'use strict'; + +const BridgeAction = require('./BridgeAction') + , LightState = require('../lightstate/LightState') + , ApiError = require('../../ApiError') + , types = require('../../types') + , LightIdPlaceholder = require('../../placeholders/LightIdPlaceholder') +; + +const LIGHT_ID = new LightIdPlaceholder(); + +const ATTRIBUTES = [ + types.uint8({name: 'light'}), + types.object({name: 'body'}), + types.object({name: 'state'}), //TODO this is an actual LightState object, could utilize another type +]; + + +module.exports = class LightStateAction extends BridgeAction { + + constructor(light) { + super(ATTRIBUTES, 'PUT'); + this.light = light; + } + + get address() { + return `/lights/${this.light}/state`; + } + + get light() { + return this.getAttributeValue('light') + } + + set light(value) { + const lightId = LIGHT_ID.getValue({id: value}); + this.setAttributeValue('light', lightId); + } + + withState(state) { + let value = state; + + if (!(state instanceof LightState)) { + value = new LightState().populate(state); + } + + this.setAttributeValue('state', value.getPayload()); + return this; + } + + get body() { + const state = this.getAttributeValue('state'); + if (state) { + return state; + } + throw new ApiError('No state has been set on the LightStateAction'); + } +}; diff --git a/lib/model/actions/SceneAction.js b/lib/model/actions/SceneAction.js new file mode 100644 index 0000000..f22563b --- /dev/null +++ b/lib/model/actions/SceneAction.js @@ -0,0 +1,53 @@ +'use strict'; + +const BridgeAction = require('./BridgeAction') + , ApiError = require('../../ApiError') + , types = require('../../types') + , SceneIdPlaceholder = require('../../placeholders/SceneIdPlaceholder') +; + + +const SCENE_ID = new SceneIdPlaceholder(); + +const ATTRIBUTES = [ + types.string({name: 'scene'}), + types.object({name: 'body'}), + types.object({name: 'state'}), +]; + + +module.exports = class SceneAction extends BridgeAction { + + constructor(scene) { + super(ATTRIBUTES, 'PUT'); + + this.scene = scene; + } + + get address() { + return `/scenes/${this.scene}`; + } + + get scene() { + return this.getAttributeValue('scene'); + } + + set scene(value) { + const sceneId = SCENE_ID.getValue({id: value}); + this.setAttributeValue('scene', sceneId); + } + + withState(data) { + // Sensor state varies wildly, so just take data here, maybe consider building payloads later on... + this.setAttributeValue('state', data); + return this; + } + + get body() { + const state = this.getAttributeValue('state'); + if (state) { + return state; + } + throw new ApiError('No state has been set on the SceneAction'); + } +}; diff --git a/lib/model/rules/actions/ScheduleStateAction.js b/lib/model/actions/ScheduleStateAction.js similarity index 78% rename from lib/model/rules/actions/ScheduleStateAction.js rename to lib/model/actions/ScheduleStateAction.js index af79ac5..68ece8f 100644 --- a/lib/model/rules/actions/ScheduleStateAction.js +++ b/lib/model/actions/ScheduleStateAction.js @@ -1,10 +1,10 @@ 'use strict'; -const RuleAction = require('./RuleAction') - , ApiError = require('../../../ApiError') +const BridgeAction = require('./BridgeAction') + , ApiError = require('../../ApiError') ; -module.exports = class ScheduleStateAction extends RuleAction { +module.exports = class ScheduleStateAction extends BridgeAction { constructor(id) { super(id, 'PUT'); diff --git a/lib/model/actions/SensorStateAction.js b/lib/model/actions/SensorStateAction.js new file mode 100644 index 0000000..bbf5bfe --- /dev/null +++ b/lib/model/actions/SensorStateAction.js @@ -0,0 +1,51 @@ +'use strict'; + +const BridgeAction = require('./BridgeAction') + , ApiError = require('../../ApiError') + , types = require('../../types') + , SensorIdPlaceholder = require('../../placeholders/SensorIdPlaceholder') +; + +const SENSOR_ID = new SensorIdPlaceholder(); + +const ATTRIBUTES = [ + types.uint8({name: 'sensor'}), + types.object({name: 'body'}), + types.object({name: 'state'}), +]; + + +module.exports = class SensorStateAction extends BridgeAction { + + constructor(sensor) { + super(ATTRIBUTES, 'PUT'); + this.sensor = sensor; + } + + get address() { + return `/sensors/${this.sensor}/state`; + } + + get sensor() { + return this.getAttributeValue('sensor'); + } + + set sensor(value) { + const sensorId = SENSOR_ID.getValue({id: value}); + this.setAttributeValue('sensor', sensorId); + } + + withState(value) { + // Sensor state varies wildly, so just take data here, maybe consider building payloads later on... + this.setAttributeValue('state', value); + return this; + } + + get body() { + const state = this.getAttributeValue('state'); + if (state) { + return state; + } + throw new ApiError('No state has been set on the SensorStateAction'); + } +}; diff --git a/lib/model/rules/actions/index.js b/lib/model/actions/index.js similarity index 94% rename from lib/model/rules/actions/index.js rename to lib/model/actions/index.js index 57aad98..b52d7da 100644 --- a/lib/model/rules/actions/index.js +++ b/lib/model/actions/index.js @@ -1,7 +1,7 @@ 'use strict'; -const ApiError = require('../../../ApiError') - , RuleAction = require('./RuleAction') +const ApiError = require('../../ApiError') + , RuleAction = require('./BridgeAction') , LightStateAction = require('./LightStateAction') , GroupStateAction = require('./GroupStateAction') , SensorStateAction = require('./SensorStateAction') @@ -56,9 +56,9 @@ function createLightStateAction(address, body) { function createGroupAction(address, body) { const match = REGEX_GROUP_ACTION.exec(address) - , id = match[1] + , group = match[1] ; - return new GroupStateAction(id).withState(body); + return new GroupStateAction(group).withState(body); } function createSensorStateAction(address, body) { diff --git a/lib/model/datetime/AbsoluteTime.js b/lib/model/datetime/AbsoluteTime.js deleted file mode 100644 index d305e87..0000000 --- a/lib/model/datetime/AbsoluteTime.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -const dateUtil = require('./DateTimeUtil') - , HueTime = require('./HueTime') - , HueDate = require('./HueDate') - , BridgeTime = require('./BridgeTime') - , ApiError = require('../../../index').ApiError -; - -module.exports = class AbsoluteTime extends BridgeTime { - - constructor() { - super(dateUtil.regex.AbsoluteTime); - this._time = new HueTime(); - this._date = new HueDate(); - } - - set value(value) { - if (value instanceof Date) { - this.fromDate(value); - } else if (value instanceof HueTime) { - this._time = value; - } else if (value instanceof HueDate) { - this._date = value; - } else if (value instanceof AbsoluteTime) { - //TODO need a cleaner clone function - this._time.fromString(value._time.toString()); - this._date.fromString(value._date.toString()); - } else { - this.fromString(value); - } - return this; - } - - year(value) { - this._date.year(value); - return this; - } - - month(value) { - this._date.month(value); - return this; - } - - day(value) { - this._date.day(value); - return this; - } - - hour(value) { - this._time.hour(value); - return this; - } - - minute(value) { - this._time.minute(value); - return this; - } - - second(value) { - this._time.second(value); - return this; - } - - toString() { - return `${this._date.toString()}T${this._time.toString()}`; - } - - fromString(value) { - const parsed = this.validationRegex.exec(value); - if (parsed) { - this._date.fromString(value); - this._time.fromString(value); - } else { - throw new ApiError(`Invalid value format ${value}`); - } - } - - fromDate(value) { - if (value instanceof Date) { - this._date.fromDate(value); - //TODO daylight savings is a real pain with this - this._time.fromDate(value); - } else { - //TODO try to parse the date first? - - throw new ApiError(`Invalid Date object provided: ${value}`); - } - } -}; \ No newline at end of file diff --git a/lib/model/datetime/AbsoluteTime.test.js b/lib/model/datetime/AbsoluteTime.test.js deleted file mode 100644 index a1bd0fb..0000000 --- a/lib/model/datetime/AbsoluteTime.test.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , AbsoluteTime = require('./AbsoluteTime') -; - -describe('AbsoluteTime', () => { - - it('should create a time from a string', () => { - const value = '2018-02-02T00:00:00' - , time = new AbsoluteTime() - ; - - time.value = value; - expect(time.toString()).to.equal(value); - }); - - it('should create a time from a date object', () => { - const rawString = '2019-12-08T12:30:01' - , time = new AbsoluteTime() - ; - - time.value = new Date(rawString); - expect(time.toString()).to.equal(rawString); - }); - - it('should create a time from a date only', () => { - const date = '1977-08-12' - , time = new AbsoluteTime() - ; - - time.fromDate(new Date(date)); - // Use the following check to deal with daylight savings issues in conversion - expect(time.toString()).to.include(`${date}T`); - }); -}); \ No newline at end of file diff --git a/lib/model/datetime/BridgeTime.js b/lib/model/datetime/BridgeTime.js deleted file mode 100644 index 668b6c1..0000000 --- a/lib/model/datetime/BridgeTime.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -module.exports = class BridgeTime { - - constructor(validationRegex) { - this._validationRegex = validationRegex; - } - - get validationRegex() { - return this._validationRegex; - } -}; \ No newline at end of file diff --git a/lib/model/datetime/DateTimeUtil.js b/lib/model/datetime/DateTimeUtil.js deleted file mode 100644 index 4e14ce3..0000000 --- a/lib/model/datetime/DateTimeUtil.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const patterns = { - time: '(\\d{2}):(\\d{2}):(\\d{2})', - weekday: 'W([0-9]{1,3})', - date: '(\\d{4})-(\\d{2})-(\\d{2})' -}; - - -module.exports = { - - patterns: patterns, - - regex: { - RecurringTime: new RegExp(`${patterns.weekday}/T${patterns.time}`), - AbsoluteTime: new RegExp(`${patterns.date}T${patterns.time}`), - Timer: new RegExp(`PT${patterns.time}`), - - //TODO expand once classes are implemented - }, - - - - timerRecurringCount: new RegExp(`R\\d{2}/PT${patterns.time}`), - timerRecurring: new RegExp(`R/PT${patterns.time}`), - timerRecurringRandomized: new RegExp(`R\\d{2}/PT${patterns.time}A${patterns.time}`), - timerRecurringCountRandom: new RegExp(`R\\d{2}/PT${patterns.time}A${patterns.time}`), - - randomized: new RegExp(`${patterns.date}T${patterns.time}A${patterns.time}`), - - recurringRandomized: new RegExp(`${patterns.weekday}/T${patterns.time}A${patterns.time}`), - - timerRandom: new RegExp(`PT${patterns.time}A${patterns.time}`), -}; diff --git a/lib/model/datetime/HueDate.js b/lib/model/datetime/HueDate.js deleted file mode 100644 index ed4f018..0000000 --- a/lib/model/datetime/HueDate.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -const ApiError = require('../../../index').ApiError -; - -module.exports = class HueDate { - - constructor() { - this._date = new Date(); - this._date.setUTCFullYear(0); - this._date.setUTCMonth(0); - this._date.setUTCDate(1); - } - - year(value) { - if (value < 0) { - throw new ApiError(`Invalid year value: ${value}, must be greater than 0`); - } - this._date.setUTCFullYear(value); - return this; - } - - month(value) { - if (value < 1 || value > 12) { - throw new ApiError(`Invalid month value: ${value}, must be between 1 and 12`); - } - this._date.setUTCMonth(value - 1); - return this; - } - - day(value) { - if (value < 1 || value > 31) { - throw new ApiError(`Invalid day value: ${value}, must be between 1 and 31`); - } - this._date.setUTCDate(value); - return this; - } - - toString() { - return this._date.toISOString().substr(0, 10); - } - - fromString(value) { - const parsed = new Date(value); - return this.fromDate(parsed); - } - - fromDate(value) { - this._date.setUTCFullYear(value.getUTCFullYear()); - this._date.setUTCDate(value.getUTCDate()); - this._date.setUTCMonth(value.getUTCMonth()); - return this; - } -}; \ No newline at end of file diff --git a/lib/model/datetime/HueDate.test.js b/lib/model/datetime/HueDate.test.js deleted file mode 100644 index edca9e5..0000000 --- a/lib/model/datetime/HueDate.test.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , HueDate = require('./HueDate') -; - -describe('HueDate', () => { - - it('should create a time from a string', () => { - const time = new HueDate().year(1977).month(8).day(12); - expect(time.toString()).to.equal('1977-08-12'); - }); - - //TODO test boundaries -}); \ No newline at end of file diff --git a/lib/model/datetime/HueTime.js b/lib/model/datetime/HueTime.js deleted file mode 100644 index c54c0dd..0000000 --- a/lib/model/datetime/HueTime.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const ApiError = require('../../../index').ApiError; - -module.exports = class HueTime { - - constructor() { - this._date = new Date(); - this._date.setHours(0); - this._date.setMinutes(0); - this._date.setSeconds(0); - } - - hour(value) { - if (value < 0 || value > 23) { - throw new ApiError(`Invalid hour value: ${value}, must be between 0 and 23`); - } - this._date.setHours(value); - return this; - } - - minute(value) { - if (value < 0 || value > 59) { - throw new ApiError(`Invalid minute value: ${value}, must be between 0 and 59`); - } - this._date.setMinutes(value); - return this; - } - - second(value) { - if (value < 0 || value > 59) { - throw new ApiError(`Invalid second value: ${value}, must be between 0 and 59`); - } - this._date.setSeconds(value); - return this; - } - - toString() { - return this._date.toTimeString().substr(0, 8); - } - - fromString(value) { - const parsed = new Date(value); - return this.fromDate(parsed); - } - - fromDate(value) { - this._date.setHours(value.getHours()); - this._date.setMinutes(value.getMinutes()); - this._date.setSeconds(value.getSeconds()); - return this; - } -}; \ No newline at end of file diff --git a/lib/model/datetime/HueTime.test.js b/lib/model/datetime/HueTime.test.js deleted file mode 100644 index c791a58..0000000 --- a/lib/model/datetime/HueTime.test.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , HueTime = require('./HueTime') -; - -describe('HueTime', () => { - - it('should create a time from a string', () => { - const time = new HueTime().hour(0).minute(0).second(1); - expect(time.toString()).to.equal('00:00:01'); - }); - - //TODO test boundaries -}); \ No newline at end of file diff --git a/lib/model/datetime/RandomizedTime.js b/lib/model/datetime/RandomizedTime.js deleted file mode 100644 index af9de62..0000000 --- a/lib/model/datetime/RandomizedTime.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -//TODO complete this - -module.exports = class RandomizedTime { - - constructor(time) { - this._rawData = time; - } -}; diff --git a/lib/model/datetime/RecurringRandomizedTime.js b/lib/model/datetime/RecurringRandomizedTime.js deleted file mode 100644 index 5863a4a..0000000 --- a/lib/model/datetime/RecurringRandomizedTime.js +++ /dev/null @@ -1 +0,0 @@ -//TODO complete this \ No newline at end of file diff --git a/lib/model/datetime/RecurringRandomizedTimer.js b/lib/model/datetime/RecurringRandomizedTimer.js deleted file mode 100644 index 5863a4a..0000000 --- a/lib/model/datetime/RecurringRandomizedTimer.js +++ /dev/null @@ -1 +0,0 @@ -//TODO complete this \ No newline at end of file diff --git a/lib/model/datetime/RecurringTime.js b/lib/model/datetime/RecurringTime.js deleted file mode 100644 index 6125e1e..0000000 --- a/lib/model/datetime/RecurringTime.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -const BridgeTime = require('./BridgeTime') - , dateTimeUtil = require('./DateTimeUtil') - , HueTime = require('./HueTime') - , ApiError = require('../../ApiError') -; - -//TODO need a nice way to specify days -const DAYS = { - MONDAY: 1, - TUESDAY: 2, - WEDNESDAY: 4, - THURSDAY: 8, - FRIDAY: 16, - SATURDAY: 32, - SUNDAY: 64 -}; - -module.exports = class RecurringTime extends BridgeTime { - - constructor() { - super(dateTimeUtil.regex.RecurringTime); - this._time = new HueTime(); - this._weekdays = 0; - } - - weekdays(value) { - if (value < 128) { - this._weekdays = value; - } else { - throw new ApiError(`Invalid weekday value bitmask provided '${value}'`); - } - return this; - } - - hour(value) { - this._time.hour(value); - return this; - } - - minute(value) { - this._time.minute(value); - return this; - } - - second(value) { - this._time.second(value); - return this; - } - - toString() { - return `W${this._weekdays}/T${this._time.toString()}`; - } - - fromString(value) { - const parsed = this.validationRegex.exec(value); - if (parsed) { - this._weekdays = parsed[1]; - this._time.hour(parsed[2]); - this._time.minute(parsed[3]); - this._time.second(parsed[4]); - } else { - throw new ApiError(`Invalid value format ${value}`); - } - } -}; \ No newline at end of file diff --git a/lib/model/datetime/Timer.js b/lib/model/datetime/Timer.js deleted file mode 100644 index e868dde..0000000 --- a/lib/model/datetime/Timer.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -const dateUtil = require('./DateTimeUtil') - , HueTime = require('./HueTime') - , BridgeTime = require('./BridgeTime') - , ApiError = require('../../ApiError') -; - -module.exports = class Timer extends BridgeTime { - - constructor() { - super(dateUtil.regex.Timer); - this._time = new HueTime(); - } - - set value(value) { - if (value instanceof HueTime) { - this._time = value; - } else { - this.fromString(value); - } - return this; - } - - hour(value) { - this._time.hour(value); - return this; - } - - minute(value) { - this._time.minute(value); - return this; - } - - second(value) { - this._time.second(value); - return this; - } - - toString() { - return `PT${this._time.toString()}`; - } - - fromString(value) { - const parsed = this.validationRegex.exec(value); - if (parsed) { - this._time.hour(parsed[1]); - this._time.minute(parsed[2]); - this._time.second(parsed[3]); - } else { - throw new ApiError(`Invalid value format ${value}`); - } - } -}; \ No newline at end of file diff --git a/lib/model/datetime/index.js b/lib/model/datetime/index.js deleted file mode 100644 index db3efec..0000000 --- a/lib/model/datetime/index.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const dateTimeUtil = require('./DateTimeUtil') - , ApiError = require('../../ApiError') -; - -module.exports.create = function(str) { - let matched = null; - - Object.keys(dateTimeUtil.regex).forEach(key => { - const regex = dateTimeUtil.regex[key]; - if (!matched) { - if (regex.test(str)) { - matched = key; - } - } - }); - - if (! matched) { - throw new ApiError(`Could not match string '${str}' to a valid Hue Time Pattern`); - } - - // TODO this would not survive minification - const time = new (require(`./${matched}`)); - time.fromString(str); - return time; -}; \ No newline at end of file diff --git a/lib/model/index.js b/lib/model/index.js index 7956db6..9c2b393 100644 --- a/lib/model/index.js +++ b/lib/model/index.js @@ -1,11 +1,21 @@ 'use strict'; const ApiError = require('../ApiError') + , util = require('../util') , lightStates = require('./lightstate') + , timePatterns = require('./timePatterns') + + , Capabilities = require('./Capabilities') , Light = require('./Light') - , Group = require('./Group') + , Group = require('./groups/Group') + , Entertainment = require('./groups/Entertainment') + , LightGroup = require('./groups/LightGroup') + , Lightsource = require('./groups/Lightsource') + , Luminaire = require('./groups/Luminaire') + , Room = require('./groups/Room') + , Zone = require('./groups/Zone') , ResourceLink = require('./ResourceLink') @@ -18,10 +28,10 @@ const ApiError = require('../ApiError') , Rule = require('./rules/Rule') , SensorCondition = require('./rules/conditions/SensorCondition') , GroupCondition = require('./rules/conditions/GroupCondition') - , GroupStateAction = require('./rules/actions/GroupStateAction') - , LightStateAction = require('./rules/actions/LightStateAction') - , SensorStateAction = require('./rules/actions/SensorStateAction') - , SceneAction = require('./rules/actions/SceneAction') + , GroupStateAction = require('./actions/GroupStateAction') + , LightStateAction = require('./actions/LightStateAction') + , SensorStateAction = require('./actions/SensorStateAction') + , SceneAction = require('./actions/SceneAction') , conditionOperators = require('./rules/conditions/operators/index') , Sensor = require('./sensors/Sensor') @@ -43,13 +53,25 @@ const ApiError = require('../ApiError') const TYPES_TO_MODEL = { light: Light, - group: Group, + + capabilities: Capabilities, + + entertainment: Entertainment, + lightgroup: LightGroup, + lightsource: Lightsource, + luminaire: Luminaire, + room: Room, + zone: Zone, + resourcelink: ResourceLink, - // scene: Scene, // This is abstract and should not be instantiated + lightscene: LightScene, groupscene: GroupScene, + schedule: Schedule, + rule: Rule, + clipgenericflag: CLIPGenericFlag, clipgenericstatus: CLIPGenericStatus, cliphumidity: CLIPHumidity, @@ -67,12 +89,16 @@ const TYPES_TO_MODEL = { }; module.exports.lightStates = lightStates; +module.exports.timePatterns = timePatterns; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Instance Check Functions -//TODO this may be questionable +module.exports.isLightInstance= function(obj) { + return obj instanceof Light; +}; + module.exports.isSceneInstance = function(obj) { return obj instanceof Scene; }; @@ -101,6 +127,30 @@ module.exports.isSensorInstance = function(obj) { return obj instanceof Sensor; }; +module.exports.isGroupInstance = function(obj) { + return obj instanceof Group; +}; + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Groups + +module.exports.createEntertainment = function() { + return new Entertainment(); +}; + +module.exports.createLightGroup = function() { + return new LightGroup(); +}; + +module.exports.createRoom = function() { + return new Room(); +}; + +module.exports.createZone = function() { + return new Zone(); +}; + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Sensors @@ -151,6 +201,36 @@ module.exports.createGroupScene = function() { }; +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Schedules + +module.exports.createSchedule = function() { + return new Schedule(); +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Actions + +module.exports.actions = { + light: function(light) { + return new LightStateAction(light); + }, + + group: function(group) { + return new GroupStateAction(group); + }, + + sensor: function(sensor) { + return new SensorStateAction(sensor); + }, + + scene: function(scene) { + return new SceneAction(scene); + } +}; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Rules @@ -168,26 +248,30 @@ module.exports.ruleConditions = { }, }; +module.exports.ruleConditionOperators = conditionOperators; + module.exports.ruleActions = { - light: function(id) { - return new LightStateAction(id); + light: function(light) { + util.deprecatedFunction('5.x', 'model.ruleActions.light(light)', 'Use model.actions.light(light) instead'); + return new LightStateAction(light); }, - group: function(id) { - return new GroupStateAction(id); + group: function(group) { + util.deprecatedFunction('5.x', 'model.ruleActions.group(group)', 'Use model.actions.group(group) instead'); + return new GroupStateAction(group); }, - sensor: function(id) { - return new SensorStateAction(id); + sensor: function(sensor) { + util.deprecatedFunction('5.x', 'model.ruleActions.sensor(sensor)', 'Use model.actions.sensor(sensor) instead'); + return new SensorStateAction(sensor); }, - scene: function(id) { - return new SceneAction(id); + scene: function(scene) { + util.deprecatedFunction('5.x', 'model.ruleActions.scene(scene)', 'Use model.actions.scene(scene) instead'); + return new SceneAction(scene); } }; -module.exports.ruleConditionOperators = conditionOperators; - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // ResourceLinks @@ -207,16 +291,13 @@ module.exports.createFromBridge = function(type, id, payload) { throw new ApiError(`Unknown type '${type}' to create Bridge Model Object from.`); } - //TODO would be useful to flag this as populated via hue api, not generic JSON - const instance = new ModelObject(id); instance._populate(payload); return instance; }; -//TODO probably rename this -module.exports.create = function (payload) { +module.exports.createFromJson = function (payload) { if (!payload) { throw new ApiError('No payload provided to build object from'); } @@ -227,12 +308,17 @@ module.exports.create = function (payload) { } const type = payloadDataType.type - , version = payloadDataType.version || 1 + , version = payloadDataType.version || 0 ; if (! type) { throw new ApiError('Invalid payload, missing type from the Data Type'); } - //TODO build the object + if (version === 0) { + throw new ApiError(`Unsupported version number ${version}, for JSON payload`); + } + + // Default to using bridge data construction until we diverge + return module.exports.createFromBridge(type, payload.id, payload); }; diff --git a/lib/model/index.test.js b/lib/model/index.test.js new file mode 100644 index 0000000..c8f4e77 --- /dev/null +++ b/lib/model/index.test.js @@ -0,0 +1,335 @@ +'use strict'; + +const expect = require('chai').expect + , model = require('./index') +; + + +describe('Bridge Model', () => { + + describe('#createFromJson()', () => { + + describe('Light', () => { + + const VALID_LIGHT_PAYLOAD = { + 'id': 1, + 'state': { + 'on': true, + 'bri': 1, + 'hue': 16312, + 'sat': 64, + 'effect': 'none', + 'xy': [ + 0.4523, + 0.4228 + ], + 'alert': 'select', + 'colormode': 'xy', + 'mode': 'homeautomation', + 'reachable': true + }, + 'swupdate': { + 'state': 'notupdatable', + 'lastinstall': null + }, + 'type': 'Color light', + 'name': 'Lounge Living Color', + 'modelid': 'LLC001', + 'manufacturername': 'Philips', + 'productname': 'LivingColors', + 'capabilities': { + 'certified': true, + 'control': { + 'colorgamuttype': 'A', + 'colorgamut': [ + [ + 0.704, + 0.296 + ], + [ + 0.2151, + 0.7106 + ], + [ + 0.138, + 0.08 + ] + ] + }, + 'streaming': { + 'renderer': false, + 'proxy': false + } + }, + 'config': { + 'archetype': 'floorshade', + 'function': 'decorative', + 'direction': 'omnidirectional' + }, + 'uniqueid': '00:aa:11:01:00:09:d0:b1-0b', + 'swversion': '2.0.0.5206', + 'node_hue_api': { + 'type': 'light', + 'version': 1 + } + }; + + it('should create model object from valid payload', () => { + const light = model.createFromJson(VALID_LIGHT_PAYLOAD); + + expect(light).to.have.property('id').to.equal(VALID_LIGHT_PAYLOAD.id); + expect(light).to.have.property('name').to.equal(VALID_LIGHT_PAYLOAD.name); + expect(light).to.have.property('type').to.equal(VALID_LIGHT_PAYLOAD.type); + expect(light).to.have.property('modelid').to.equal(VALID_LIGHT_PAYLOAD.modelid); + expect(light).to.have.property('manufacturername').to.equal(VALID_LIGHT_PAYLOAD.manufacturername); + expect(light).to.have.property('productname').to.equal(VALID_LIGHT_PAYLOAD.productname); + expect(light).to.have.property('capabilities').to.deep.equal(VALID_LIGHT_PAYLOAD.capabilities); + expect(light).to.have.property('uniqueid').to.equal(VALID_LIGHT_PAYLOAD.uniqueid); + expect(light).to.have.property('swversion').to.equal(VALID_LIGHT_PAYLOAD.swversion); + }); + }); + }); + + + describe('#createFromBridge()', () => { + + + describe('Light', () => { + + const VALID_LIGHT_PAYLOAD = { + 'state': { + 'on': true, + 'bri': 1, + 'hue': 16312, + 'sat': 64, + 'effect': 'none', + 'xy': [ + 0.4523, + 0.4228 + ], + 'alert': 'select', + 'colormode': 'xy', + 'mode': 'homeautomation', + 'reachable': true + }, + 'swupdate': { + 'state': 'notupdatable', + 'lastinstall': null + }, + 'type': 'Color light', + 'name': 'Lounge Living Color', + 'modelid': 'LLC001', + 'manufacturername': 'Philips', + 'productname': 'LivingColors', + 'capabilities': { + 'certified': true, + 'control': { + 'colorgamuttype': 'A', + 'colorgamut': [ + [ + 0.704, + 0.296 + ], + [ + 0.2151, + 0.7106 + ], + [ + 0.138, + 0.08 + ] + ] + }, + 'streaming': { + 'renderer': false, + 'proxy': false + } + }, + 'config': { + 'archetype': 'floorshade', + 'function': 'decorative', + 'direction': 'omnidirectional' + }, + 'uniqueid': '00:aa:11:01:00:09:d0:b1-0b', + 'swversion': '2.0.0.5206' + }; + + it('should create a light from a valid payload', () => { + const id = 1 + , light = model.createFromBridge('light', id, VALID_LIGHT_PAYLOAD); + + expect(light).to.have.property('id').to.equal(1); + expect(light).to.have.property('name').to.equal(VALID_LIGHT_PAYLOAD.name); + expect(light).to.have.property('type').to.equal(VALID_LIGHT_PAYLOAD.type); + expect(light).to.have.property('modelid').to.equal(VALID_LIGHT_PAYLOAD.modelid); + expect(light).to.have.property('manufacturername').to.equal(VALID_LIGHT_PAYLOAD.manufacturername); + expect(light).to.have.property('productname').to.equal(VALID_LIGHT_PAYLOAD.productname); + expect(light).to.have.property('capabilities').to.deep.equal(VALID_LIGHT_PAYLOAD.capabilities); + expect(light).to.have.property('uniqueid').to.equal(VALID_LIGHT_PAYLOAD.uniqueid); + expect(light).to.have.property('swversion').to.equal(VALID_LIGHT_PAYLOAD.swversion); + }); + }); + + + describe('Group', () => { + + const VALID_LIGHT_GROUP_PAYLOAD = { + "name": "VRC 1", + "lights": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8" + ], + "sensors": [], + "type": "LightGroup", + "state": { + "all_on": false, + "any_on": true + }, + "recycle": false, + "action": { + "on": false, + "bri": 61, + "hue": 14988, + "sat": 141, + "effect": "none", + "xy": [ + 0.4575, + 0.4101 + ], + "ct": 366, + "alert": "select", + "colormode": "ct" + } + } + , VALID_ZONE_GROUP_PAYLOAD = { + "name": "Testing Zone Creation", + "lights": [ + "2", + "3", + "4" + ], + "sensors": [], + "type": "Zone", + "state": { + "all_on": false, + "any_on": true + }, + "recycle": false, + "class": "Other", + "action": { + "on": false, + "bri": 254, + "hue": 0, + "sat": 0, + "effect": "none", + "xy": [ + 0.3804, + 0.3768 + ], + "ct": 366, + "alert": "select", + "colormode": "ct" + } + } + , VALID_ROOM_GROUP_PAYLOAD = { + "name": "Bedroom Lamps", + "lights": [ + "7", + "8" + ], + "sensors": [], + "type": "Room", + "state": { + "all_on": false, + "any_on": false + }, + "recycle": false, + "class": "Other", + "action": { + "on": false, + "bri": 61, + "hue": 14988, + "sat": 141, + "effect": "none", + "xy": [ + 0.4575, + 0.4101 + ], + "ct": 366, + "alert": "select", + "colormode": "ct" + } + } + , VALID_ENTERTAINMENT_PAYLOAD = { + "name": "Lounge Entertainment", + "lights": [ + "18", + "37", + "38", + "17" + ], + "sensors": [], + "type": "Entertainment", + "state": { + "all_on": true, + "any_on": true + }, + "recycle": false, + "class": "TV", + "stream": { + "proxymode": "manual", + "proxynode": "/lights/22", + "active": false, + "owner": null + }, + "locations": { + "17": [ + -0.65, + -0.84, + 0 + ], + "18": [ + 0.69, + -0.85, + 0 + ], + "37": [ + -0.51, + 0.85, + 0 + ], + "38": [ + 0.44, + 0.84, + 0 + ] + }, + "action": { + "on": true, + "bri": 102, + "hue": 2595, + "sat": 127, + "effect": "none", + "xy": [ + 0.5095, + 0.3624 + ], + "ct": 459, + "alert": "select", + "colormode": "hs" + } + } + ; + + it('should create a group from a valid payload', () => { + + }); + + }) + }); +}); \ No newline at end of file diff --git a/lib/model/lightstate/States.js b/lib/model/lightstate/States.js index d9441b8..2cb0a0a 100644 --- a/lib/model/lightstate/States.js +++ b/lib/model/lightstate/States.js @@ -118,6 +118,7 @@ module.exports = class States { } }; +//TODO this is now in the utils package function flatten(array) { const flattened = []; !(function flat(array) { diff --git a/lib/model/rules/Rule.js b/lib/model/rules/Rule.js index 243a546..554edd2 100644 --- a/lib/model/rules/Rule.js +++ b/lib/model/rules/Rule.js @@ -1,7 +1,7 @@ 'use strict'; -const BridgeObject = require('../BridgeObject') - , ruleActions = require('./actions/index') +const BridgeObjectWithId = require('../BridgeObjectWithId') + , actions = require('../actions') , ruleConditions = require('./conditions/index') , parameters = require('../../types') ; @@ -18,7 +18,7 @@ const ATTRIBUTES = [ // conditions and actions are handled separately ]; -module.exports = class Rule extends BridgeObject { +module.exports = class Rule extends BridgeObjectWithId { constructor(id) { super(ATTRIBUTES, id); @@ -165,7 +165,7 @@ function buildConditions(conditions) { } function buildAction(action) { - return ruleActions.create(action); + return actions.create(action); } function buildActions(actions) { diff --git a/lib/model/rules/actions/GroupStateAction.js b/lib/model/rules/actions/GroupStateAction.js deleted file mode 100644 index 06fc04a..0000000 --- a/lib/model/rules/actions/GroupStateAction.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const RuleAction = require('./RuleAction') - , GroupState = require('../../lightstate/GroupState') - , ApiError = require('../../../ApiError') -; - -module.exports = class GroupStateAction extends RuleAction { - - constructor(id) { - super(id, 'PUT'); - this._state = null; - } - - get address() { - return `/groups/${this.id}/action`; - } - - withState(state) { - if (state instanceof GroupState) { - this._state = state; - } else { - this._state = new GroupState().populate(state); - } - return this; - } - - get body() { - if (this._state) { - return this._state.getPayload(); - } - throw new ApiError('No state has been set on the Rule Action'); - } -}; \ No newline at end of file diff --git a/lib/model/rules/actions/LightStateAction.js b/lib/model/rules/actions/LightStateAction.js deleted file mode 100644 index a0fc938..0000000 --- a/lib/model/rules/actions/LightStateAction.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const RuleAction = require('./RuleAction') - , LightState = require('../../lightstate/LightState') - , ApiError = require('../../../ApiError') -; - -module.exports = class LightStateAction extends RuleAction { - - constructor(id) { - super(id, 'PUT'); - this._state = null; - } - - get address() { - return `/lights/${this.id}/state`; - } - - get body() { - if (this._state) { - return this._state.getPayload(); - } - throw new ApiError('No state has been set on the Rule Action'); - } - - withState(state) { - if (state instanceof LightState) { - this._state = state; - } else { - this._state = new LightState().populate(state); - } - return this; - } -}; diff --git a/lib/model/rules/actions/RuleAction.js b/lib/model/rules/actions/RuleAction.js deleted file mode 100644 index df0c2c1..0000000 --- a/lib/model/rules/actions/RuleAction.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -const ApiError = require('../../../ApiError') - , BridgeObject = require('../../BridgeObject') -; - -module.exports = class RuleAction { - - constructor(id, method) { - if (id instanceof BridgeObject) { - this._id = id.id; - } else { - this._id = id; - } - - this._method = method || null; - } - - /** - * @return {string} - */ - get address() { - throw new ApiError('Not implemented'); - } - - /** - * @return {*} - */ - get body() { - throw new ApiError('Not implemented'); - } - - get id() { - return this._id; - } - - get method() { - return this._method; - } - - withMethod(method) { - this._method = method; - return this; - } - - get payload() { - return { - address: this.address, - method: this.method, - body: this.body - }; - } - - toString() { - return JSON.stringify(this.payload); - } -}; \ No newline at end of file diff --git a/lib/model/rules/actions/SceneAction.js b/lib/model/rules/actions/SceneAction.js deleted file mode 100644 index 7ba51f2..0000000 --- a/lib/model/rules/actions/SceneAction.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -const RuleAction = require('./RuleAction') - , ApiError = require('../../../ApiError') -; - -module.exports = class SceneAction extends RuleAction { - - constructor(id) { - super(id, 'PUT'); - this._state = null; - } - - get address() { - return `/scenes/${this.id}`; - } - - withState(data) { - // Sensor state varies wildly, so just take data here, maybe consider building payloads later on... - this._state = data; - return this; - } - - get body() { - if (this._state) { - return this._state; - } - throw new ApiError('No state has been set on the Rule Action'); - } -}; diff --git a/lib/model/rules/actions/SensorStateAction.js b/lib/model/rules/actions/SensorStateAction.js deleted file mode 100644 index 2031636..0000000 --- a/lib/model/rules/actions/SensorStateAction.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -const RuleAction = require('./RuleAction') - , ApiError = require('../../../ApiError') -; - -module.exports = class SensorStateAction extends RuleAction { - - constructor(id) { - super(id, 'PUT'); - this._state = null; - } - - get address() { - return `/sensors/${this.id}/state`; - } - - withState(data) { - // Sensor state varies wildly, so just take data here, maybe consider building payloads later on... - this._state = data; - return this; - } - - get body() { - if (this._state) { - return this._state; - } - throw new ApiError('No state has been set on the Rule Action'); - } -}; diff --git a/lib/model/rules/conditions/GroupCondition.js b/lib/model/rules/conditions/GroupCondition.js index 00a28ba..5319775 100644 --- a/lib/model/rules/conditions/GroupCondition.js +++ b/lib/model/rules/conditions/GroupCondition.js @@ -1,7 +1,7 @@ 'use strict'; const RuleCondition = require('./RuleCondition') - , Group = require('../../Group') + , Group = require('../../groups/Group') , ApiError = require('../../../ApiError') , conditionOperators = require('./operators/index') ; diff --git a/lib/model/scenes/GroupScene.js b/lib/model/scenes/GroupScene.js index fa1f268..7df2f38 100644 --- a/lib/model/scenes/GroupScene.js +++ b/lib/model/scenes/GroupScene.js @@ -13,7 +13,7 @@ const ATTRIBUTES = [ module.exports = class GroupScene extends Scene { - constructor(attributes, type, id) { + constructor(id) { super(ATTRIBUTES, 'GroupScene', id); } diff --git a/lib/model/scenes/Scene.js b/lib/model/scenes/Scene.js index e7e0c5b..66c9e3d 100644 --- a/lib/model/scenes/Scene.js +++ b/lib/model/scenes/Scene.js @@ -1,6 +1,6 @@ 'use strict'; -const BridgeObject = require('../BridgeObject') +const BridgeObjectWithId = require('../BridgeObjectWithId') , types = require('../../types') , util = require('../../util') ; @@ -19,7 +19,7 @@ const ATTRIBUTES = [ ]; -module.exports = class Scene extends BridgeObject { +module.exports = class Scene extends BridgeObjectWithId { constructor(attributes, type, id) { super(util.flatten(ATTRIBUTES, attributes), id); diff --git a/lib/model/sensors/Sensor.js b/lib/model/sensors/Sensor.js index 323146f..9750d60 100644 --- a/lib/model/sensors/Sensor.js +++ b/lib/model/sensors/Sensor.js @@ -1,6 +1,6 @@ 'use strict'; -const BridgeObject = require('../BridgeObject') +const BridgeObjectWithId = require('../BridgeObjectWithId') , parameters = require('../../types') , util = require('../../util') ; @@ -26,7 +26,7 @@ const COMMON_CONFIG_ATTRIBUTES = [ parameters.boolean({name: 'on', defaultValue: true}), ]; -module.exports = class Sensor extends BridgeObject { +module.exports = class Sensor extends BridgeObjectWithId { //TODO consider removing data from here as we have _populate to do this constructor(configAttributes, stateAttributes, id, data) { From 07437fe7310cfd0aeec5ff87101a067882795731 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Sat, 30 Nov 2019 14:41:08 +0000 Subject: [PATCH 19/35] - Adding rate limiting to the bridge communications --- lib/api/http/Transport.js | 56 +++++++++++++++------------------------ 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/lib/api/http/Transport.js b/lib/api/http/Transport.js index 6e5705e..26b4f1e 100644 --- a/lib/api/http/Transport.js +++ b/lib/api/http/Transport.js @@ -1,15 +1,22 @@ 'use strict'; -const ApiError = require('../../ApiError') +const Bottleneck = require('bottleneck') + , ApiError = require('../../ApiError') , HueError = require('../../HueError') , util = require('../../util') ; const DEBUG = /node-hue-api/.test(process.env.NODE_DEBUG); +// The limiter configuration if nothing is specified +const DEFAULT_LIMITER_CONFIG = { + maxConcurrent: 4, + minTime: 50, +}; + module.exports = class Transport { - constructor(username, axios) { + constructor(username, axios, queueConfig) { this._username = username; this._axios = axios; @@ -27,10 +34,21 @@ module.exports = class Transport { return config; }); } + + this.configureLimiter(queueConfig || DEFAULT_LIMITER_CONFIG); + } + + configureLimiter(config) { + this._limiter = new Bottleneck(config); + } + + get limiter() { + return this._limiter; } execute(api, parameters) { let self = this + , limiter = this.limiter , axios = self._axios , requestParameters = Object.assign({username: self._username}, parameters) , promise @@ -40,31 +58,17 @@ module.exports = class Transport { throw new Error('An API must be provided'); } - promise = axios.request(api.getRequest(requestParameters)) + promise = limiter.schedule(() => {return axios.request(api.getRequest(requestParameters))}) .catch(err => { throw extractError(err, err.response); }) .then(res => { - //TODO this hue bridge can be successful, but still return an error object here + // Errors can be contained in the object payload from a successful response code. const errors = util.parseErrors(res.data); if (errors) { throw new ApiError(errors[0]); } return res.data; - // }) - // .catch(function (response) { - // let error; - // - // if (response instanceof Error) { - // error = response; - // // Axios hides the error message data, so expose it, if it is there - // if (error.response && error.response.data) { - // error.message = error.response.data; - // } - // } else { - // error = generateResponseError(response); - // } - // throw error; }); if (api.getErrorHandler()) { @@ -87,22 +91,6 @@ module.exports = class Transport { } }; - -// function generateResponseError(response) { -// let err = new Error(); -// -// err.statusCode = response.status; -// err.headers = response.headers; -// -// if (response.data) { -// err.message = response.data; -// } else { -// err.message = 'Unexpected status code: ' + response.status; -// } -// -// return err; -// } - function extractError(err, response) { if (!response) { throw new ApiError(err.message); From 97f546e15ce3ddeab9a44cb679f57bae7b99b070 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Sat, 30 Nov 2019 14:41:55 +0000 Subject: [PATCH 20/35] - Fixing name of workflow --- .github/workflows/{modejs.yml => nodejs.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{modejs.yml => nodejs.yml} (100%) diff --git a/.github/workflows/modejs.yml b/.github/workflows/nodejs.yml similarity index 100% rename from .github/workflows/modejs.yml rename to .github/workflows/nodejs.yml From 3667a1b18aac3635f528d3c9877c217ce2c3168e Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Sat, 30 Nov 2019 14:42:21 +0000 Subject: [PATCH 21/35] - Introduction of a time pattern type --- lib/types/TimePatternType.js | 40 ++++++++++++++++++++++++++++++++++++ lib/types/index.js | 17 +++++++-------- 2 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 lib/types/TimePatternType.js diff --git a/lib/types/TimePatternType.js b/lib/types/TimePatternType.js new file mode 100644 index 0000000..5926bfc --- /dev/null +++ b/lib/types/TimePatternType.js @@ -0,0 +1,40 @@ +'use strict'; + +const Type = require('./Type') + , timePatterns = require('../model/timePatterns') + , BridgeTime = require('../model/timePatterns/BridgeTime') + , ApiError = require('../ApiError') +; + + +module.exports = class TimePatternType extends Type { + + constructor(config) { + super(Object.assign({type: 'timePattern'}, config)); + } + + getValue(value) { + const checkedValue = super.getValue(value) + , isValueDefined = Type.isValueDefined(checkedValue) + , optional = this.optional + ; + + // If we are optional and have no value, prevent further checks as they will fail + if (optional && !isValueDefined) { + return checkedValue; + } + + if (value instanceof BridgeTime) { + return value.toString(); + } else if (timePatterns.isTimePattern(value)) { + return timePatterns.createFromString(value).toString(); + } else { + //TODO may need to cater for a string + throw new ApiError(`Cannot convert value "${value}" to a valid TimePatten`); + } + } + + _convertToType(val) { + return timePatterns.createFromString(`${val}`); + } +}; \ No newline at end of file diff --git a/lib/types/index.js b/lib/types/index.js index 23e422a..3714c1c 100644 --- a/lib/types/index.js +++ b/lib/types/index.js @@ -51,14 +51,11 @@ module.exports = { object: function(config) { return new ObjectType(config); - } -}; - - - - - - - - + }, + timePattern: function(config) { + //TODO need to require this here as the underlying time patterns use this module to define parts of the time formats which creates a cyclic dependency around this module + const TimePatternType = require('./TimePatternType'); + return new TimePatternType(config); + }, +}; From ffe8472dc9b6857bfa1394e714acd43284f9b924 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Sat, 30 Nov 2019 14:43:10 +0000 Subject: [PATCH 22/35] - Standarization of the API endpoints and adding JSDoc and deprecations for generating TypeScript definitions --- lib/api/Capabilities.js | 2 +- lib/api/Capabilities.test.js | 40 ++ lib/api/Groups.js | 172 ++++-- lib/api/Groups.test.js | 514 +++++++++++++---- lib/api/Lights.js | 120 +++- lib/api/Lights.test.js | 721 ++++++++++++++---------- lib/api/ResourceLinks.js | 38 +- lib/api/ResourceLinks.test.js | 22 +- lib/api/Rules.js | 44 +- lib/api/Rules.test.js | 195 +++++-- lib/api/Scenes.js | 80 ++- lib/api/Scenes.test.js | 83 +-- lib/api/Schedules.js | 76 ++- lib/api/Schedules.test.js | 390 +++++++++++++ lib/api/Sensors.js | 70 ++- lib/api/Sensors.test.js | 28 + lib/api/Users.js | 35 +- lib/api/Users.test.js | 2 +- lib/api/http/endpoints/capabilities.js | 12 +- lib/api/http/endpoints/configuration.js | 2 +- lib/api/http/endpoints/endpoint.js | 18 +- lib/api/http/endpoints/groups.js | 62 +- lib/api/http/endpoints/lights.js | 29 +- lib/api/http/endpoints/resourcelinks.js | 13 +- lib/api/http/endpoints/rules.js | 13 +- lib/api/http/endpoints/scenes.js | 5 +- lib/api/http/endpoints/schedules.js | 98 ++-- lib/api/http/endpoints/sensors.js | 20 +- lib/api/index.test.js | 2 +- 29 files changed, 2232 insertions(+), 674 deletions(-) create mode 100644 lib/api/Capabilities.test.js create mode 100644 lib/api/Schedules.test.js diff --git a/lib/api/Capabilities.js b/lib/api/Capabilities.js index 49c1923..74d2a6b 100644 --- a/lib/api/Capabilities.js +++ b/lib/api/Capabilities.js @@ -11,6 +11,6 @@ module.exports = class Capabilities extends ApiDefinition { } getAll() { - return this.execute(capabilitiesApi.getAll); + return this.execute(capabilitiesApi.getAll, {baseUrl: this.hueApi._getConfig().baseUrl}); } }; \ No newline at end of file diff --git a/lib/api/Capabilities.test.js b/lib/api/Capabilities.test.js new file mode 100644 index 0000000..390dfd5 --- /dev/null +++ b/lib/api/Capabilities.test.js @@ -0,0 +1,40 @@ +'use strict'; + +const expect = require('chai').expect + , v3Api = require('../v3').api + , discovery = require('../v3').discovery + , testValues = require('../../test/support/testValues.js') +; + + +describe('Hue API #capabilities', () => { + + let hue; + + before(async () => { + const searchResults = await discovery.nupnpSearch(); + expect(searchResults).to.have.length.at.least(1); + + const localApi = v3Api.createLocal(searchResults[0].ipaddress); + hue = await localApi.connect(testValues.username); + }); + + + describe('#getAll()', () => { + + it('should get all capabilities', async () => { + const capabilities = await hue.capabilities.getAll(); + + expect(capabilities).to.have.property('lights'); + expect(capabilities).to.have.property('sensors'); + expect(capabilities).to.have.property('groups'); + expect(capabilities).to.have.property('scenes'); + expect(capabilities).to.have.property('schedules'); + expect(capabilities).to.have.property('rules'); + expect(capabilities).to.have.property('resourcelinks'); + expect(capabilities).to.have.property('streaming'); + + expect(capabilities.timezones).to.be.instanceOf(Array); + }); + }); +}); \ No newline at end of file diff --git a/lib/api/Groups.js b/lib/api/Groups.js index 7969900..7526090 100644 --- a/lib/api/Groups.js +++ b/lib/api/Groups.js @@ -1,8 +1,10 @@ 'use strict'; -const groupsApi = require('./http/endpoints/groups') +const Bottleneck = require('bottleneck') + , groupsApi = require('./http/endpoints/groups') , ApiDefinition = require('./http/ApiDefinition.js') - , Group = require('../model/Group') + , model = require('../model') + , util = require('../util') ; @@ -10,8 +12,15 @@ module.exports = class Groups extends ApiDefinition { constructor(hueApi) { super(hueApi); + + // Set up a limiter on the group state changes from the library to once per second as per guidance documentation + this._groupStateLimiter = new Bottleneck({maxConcurrent: 1, minTime: 1000}); } + /** + * Gets all the groups from the bridge. + * @returns {Promise} + */ getAll() { // Lightset 0 (all lights) is a special case, so retrieve the bridge's definition of that and prepend to the // existing group definitions to provide the complete list of groups. @@ -25,96 +34,200 @@ module.exports = class Groups extends ApiDefinition { }); } - get(id) { + /** + * @param id {int | Group} + * @returns {Promise} + */ + getGroup(id) { return this.execute(groupsApi.getGroupAttributes, {id: id}); } + /** + * @param id {int | Group} + * @returns {Promise} + */ + get(id) { + util.deprecatedFunction('5.x', 'groups.get(id)', 'Use groups.getGroup(id) instead.'); + return this.getGroup(id); + } + + /** + * @deprecated Use getGroupByName(name) instead. + * @param name {string} + * @returns {Promise} + */ getByName(name) { + util.deprecatedFunction('5.x', 'groups.getByName(name)', 'Use groups.getGroupByName(name) instead.'); + return this.getByName(name); + } + + /** + * @param name {string} + * @returns {Promise} + */ + getGroupByName(name) { return this.getAll() .then(allGroups => { return allGroups.filter(group => group.name === name); }); } - // TODO need to support the creation of Zones, Entertainment as well. - + /** + * Creates a group + * @param group {Entertainment | LightGroup | Room | Zone | Group} + * @returns {Promise} The Group that was created on the bridge. + */ + createGroup(group) { + const self = this; - createGroup(name, lights) { - const group = new Group(); - group.name = name; - group.lights = lights; - group.type = 'LightGroup'; + if (arguments.length === 1 && model.isGroupInstance(group)) { + return this.execute(groupsApi.createGroup, {group: group}) + .then(result => { + return self.getGroup(result.id); + }); + } - return this._create(group); + util.deprecatedFunction('5.x', 'groups.createGroup(name, lights)', 'Use groups.createGroup(group) instead.'); + const newGroup = model.createLightGroup(); + newGroup.name = arguments[0]; + newGroup.lights = arguments[1]; + return self.createGroup(newGroup); } + /** + * @deprecated use createGroup(group) instead + */ createRoom(name, lights, roomClass) { - const group = new Group(); + util.deprecatedFunction('5.x', 'groups.createRoom(name, lights, roomClass)', 'Use groups.createGroup(group) instead.'); + + const group = model.createRoom(); group.name = name; group.lights = lights; - group.type = 'Room'; group.class = roomClass; - - return this._create(group); + return this.createGroup(group); } + /** + * @deprecated use createGroup(group) instead + */ createZone(name, lights, roomClass) { - const group = new Group(); + util.deprecatedFunction('5.x', 'groups.createZone(name, lights, roomClass)', 'Use groups.createGroup(group) instead.'); + + const group = model.createZone(); group.name = name; group.lights = lights; - group.type = 'Zone'; group.class = roomClass; - return this._create(group); + return this.createGroup(group); + } + + /** + * Update the Group attributes on the bridge for the specified Group object. + * @param group {Group} The group with updates to be updated on the bridge. + * @returns {Promise} + */ + updateGroupAttributes(group) { + return this.execute(groupsApi.setGroupAttributes, {id: group.id, group: group}); } - //TODO support the creation of other groups, Entertainment + /** + * Update the Group attributes on the bridge for the specified Group object. + * @param group {Group} The group with updates to be updated on the bridge. + * @returns {Promise} + */ updateAttributes(id, data) { - //TODO use a group object here? - return this.execute(groupsApi.setGroupAttributes, {id: id, groupAttributes: data}); + util.deprecatedFunction('5.x', 'groups.updateAttributes(id, data)', 'Use groups.updateGroupAttributes(group) instead.'); + return this.execute(groupsApi.setGroupAttributes, {id: id, group: data}); } + /** + * + * @param id {number | Group | LightGroup | Zone | Room | Entertainment} The id or Group instance to delete + * @returns {Promise} + */ deleteGroup(id) { - //TODO support a group object? return this.execute(groupsApi.deleteGroup, {id: id}); } + /** + * @param id {int | Group} + * @returns {Promise} + */ getGroupState(id) { - return this.get(id).then(group => {return group.state;}); + return this.get(id).then(group => { + return group.state; + }); } + /** + * + * @param id {int | Group} + * @param state {GroupLightState | Object} + * @returns {Promise<*>>} + */ setGroupState(id, state) { - return this.execute(groupsApi.setGroupState, {id: id, state: state}); + const self = this; + return self._groupStateLimiter.schedule(() => { + return self.execute(groupsApi.setGroupState, {id: id, state: state}); + }); } + /** + * @returns {Promise} + */ getLightGroups() { return this._getByType('LightGroup'); } - getLuminaires() { + /** + * @returns {Promise} + */ + getLuminaries() { return this._getByType('Luminaire'); } + /** + * @returns {Promise} + */ getLightSources() { return this._getByType('Lightsource'); } + /** + * @returns {Promise} + */ getRooms() { return this._getByType('Room'); } + /** + * @returns {Promise} + */ getZones() { return this._getByType('Zone'); } + /** + * @returns {Promise} + */ getEntertainment() { return this._getByType('Entertainment'); } + /** + * Enables the streaming functionality on an Entertainment Group + * @param id {int | Entertainment} + * @returns {Promise} + */ enableStreaming(id) { return this.execute(groupsApi.setStreaming, {id: id, active: true}); } + /** + * Disabled the streaming functionality on an Entertainment Group + * @param id {int | Entertainment} + * @returns {Promise} + */ disableStreaming(id) { return this.execute(groupsApi.setStreaming, {id: id, active: false}); } @@ -125,13 +238,4 @@ module.exports = class Groups extends ApiDefinition { return groups.filter(group => group.type === type); }); } - - _create(group) { - const self = this; - - return this.execute(groupsApi.createGroup, {group: group}) - .then(result => { - return self.get(result.id); - }); - } }; diff --git a/lib/api/Groups.test.js b/lib/api/Groups.test.js index ba816a1..68c64f1 100644 --- a/lib/api/Groups.test.js +++ b/lib/api/Groups.test.js @@ -3,14 +3,17 @@ const expect = require('chai').expect , v3Api = require('../v3').api , discovery = require('../v3').discovery - , testValues = require('../../test/support/testValues.js') //TODO move these - , GroupState = require('../model/lightstate/GroupState') + , testValues = require('../../test/support/testValues.js') + , model = require('../model') + , util = require('../util') ; -const TEST_GROUP_NAME = 'd4b3af3ab4df72d10666726d'; +const NEW_GROUP_NAME = 'd4b3af3ab4df72d10666726d'; -describe('Hue API #groups', () => { +describe('Hue API #groups', function () { + + this.timeout(5000); const GROUP_LIGHTS = [2, 3] , ZONE_LIGHTS = [2, 3, 4] @@ -18,15 +21,11 @@ describe('Hue API #groups', () => { let hue; - before(() => { - return discovery.nupnpSearch() - .then(searchResults => { - const localApi = v3Api.createLocal(searchResults[0].ipaddress); - return localApi.connect(testValues.username) - .then(api => { - hue = api; - }); - }); + before(async () => { + const searchResults = await discovery.nupnpSearch(); + expect(searchResults).to.have.length.at.least(1); + const localApi = v3Api.createLocal(searchResults[0].ipaddress); + hue = await localApi.connect(testValues.username); }); @@ -52,7 +51,7 @@ describe('Hue API #groups', () => { }); - describe('#get()', () => { + describe('#getGroup()', () => { let existingGroup; @@ -64,7 +63,7 @@ describe('Hue API #groups', () => { }); it('should get the All Lights Group', async () => { - const result = await hue.groups.get(0); + const result = await hue.groups.getGroup(0); expect(result.name).to.equal('Lightset 0'); expect(result.id).to.equal(0); @@ -72,7 +71,7 @@ describe('Hue API #groups', () => { it('should get a room group', async () => { - const result = await hue.groups.get(existingGroup.id); + const result = await hue.groups.getGroup(existingGroup.id); expect(result).to.have.property('id').to.equal(existingGroup.id); expect(result).to.have.property('name').to.equal(existingGroup.name); @@ -82,7 +81,7 @@ describe('Hue API #groups', () => { it('should fail to resolve a group for an invalid id number', async () => { try { - const result = await hue.groups.get(65534); + await hue.groups.getGroup(65534); expect.fail('should not get here'); } catch (err) { expect(err.message).to.contain('not available'); @@ -92,7 +91,7 @@ describe('Hue API #groups', () => { it('should fail to resolve a group for an invalid id as a string', async () => { try { - await hue.groups.get('ab62c6'); + await hue.groups.getGroup('ab62c6'); expect.fail('should not get here'); } catch (err) { expect(err.message).to.contain('not a parsable number'); @@ -101,12 +100,12 @@ describe('Hue API #groups', () => { }); - describe('#getByName()', () => { + describe('#getGroupByName()', () => { it('should get an existing group', async () => { const allGroups = await hue.groups.getAll() , targetGroupName = allGroups[1].name - , groups = await hue.groups.getByName(targetGroupName) + , groups = await hue.groups.getGroupByName(targetGroupName) ; expect(groups).to.be.instanceof(Array); @@ -119,7 +118,7 @@ describe('Hue API #groups', () => { describe('creating groups', () => { function deleteExistingGroups(name) { - return hue.groups.getByName(name) + return hue.groups.getGroupByName(name) .then(matchedGroups => { if (matchedGroups && matchedGroups.length > 0) { const promises = []; @@ -136,67 +135,137 @@ describe('Hue API #groups', () => { } beforeEach('remove group before creation', async () => { - await deleteExistingGroups(TEST_GROUP_NAME); + await deleteExistingGroups(NEW_GROUP_NAME); }); afterEach('cleanup created group', async () => { - await deleteExistingGroups(TEST_GROUP_NAME); + await deleteExistingGroups(NEW_GROUP_NAME); + }); + + describe('#createRoom()', () => { + + it('should work on deprecated function', async () => { + const name = NEW_GROUP_NAME + , lights = [] + ; + + const result = await hue.groups.createRoom(name, lights); + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('lights').to.have.members(util.toStringArray(lights)); + }); }); + describe('#createZone()', () => { + + it('should work on deprecated function', async () => { + const name = NEW_GROUP_NAME + , lights = GROUP_LIGHTS + ; + + const result = await hue.groups.createZone(name, lights); + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('lights').to.have.members(util.toStringArray(lights)); + }); + }); describe('#createGroup()', () => { - describe('simple group creation', () => { + describe('old deprecated parameters', () => { - it('should createGroup a new group', async () => { - const lights = GROUP_LIGHTS - , result = await hue.groups.createGroup(TEST_GROUP_NAME, lights) + it('should create a new group', async () => { + const name = NEW_GROUP_NAME + , lights = GROUP_LIGHTS ; + const result = await hue.groups.createGroup(name, lights); + expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('lights').to.have.members(util.toStringArray(lights)); + }); + }); + + describe('LightGroup', () => { + + it('should create a new group', async () => { + const group = model.createLightGroup(); + group.name = NEW_GROUP_NAME; + group.lights = GROUP_LIGHTS; + + const result = await hue.groups.createGroup(group); + expect(model.isGroupInstance(result)).to.be.true; expect(result).to.have.property('id').to.be.greaterThan(0); - expect(result).to.have.property('name').to.equal(TEST_GROUP_NAME); - expect(result).to.have.property('lights').to.have.members(lights); + expect(result).to.have.property('name').to.equal(NEW_GROUP_NAME); + expect(result).to.have.property('lights').to.have.members(util.toStringArray(GROUP_LIGHTS)); + expect(result).to.have.property('recycle').to.be.false; }); }); }); - describe('#createRoom()', () => { + describe('Room', () => { it('should create a new room', async () => { - const name = TEST_GROUP_NAME - , lights = [] + const name = NEW_GROUP_NAME , roomClass = 'Gym' - , result = await hue.groups.createRoom(name, lights, roomClass) ; + const room = model.createRoom(); + room.name = NEW_GROUP_NAME; + room.class = roomClass; + + const result = await hue.groups.createGroup(room); + expect(model.isGroupInstance(result)).to.be.true; expect(result).to.have.property('id').to.be.greaterThan(0); expect(result).to.have.property('name').to.equal(name); + expect(result).to.have.property('type').to.equal('Room'); expect(result).to.have.property('class').to.equal(roomClass); + expect(result).to.have.property('lights').to.be.empty; }); it('should create a room when only providing a name', async () => { - const result = await hue.groups.createRoom(TEST_GROUP_NAME); + const room = model.createRoom(); + room.name = NEW_GROUP_NAME; + const result = await hue.groups.createGroup(room); + expect(model.isGroupInstance(result)).to.be.true; expect(result).to.have.property('id').to.be.greaterThan(0); - expect(result).to.have.property('name').to.equal(TEST_GROUP_NAME); + expect(result).to.have.property('name').to.equal(NEW_GROUP_NAME); + expect(result).to.have.property('type').to.equal('Room'); expect(result).to.have.property('class').to.equal('Other'); }); }); - describe('#createZone()', () => { + describe('Zone', () => { it('should create a new zone', async () => { - const name = 'Testing Zone Creation' - , lights = ZONE_LIGHTS - , result = await hue.groups.createZone(name, lights) - ; + const zone = model.createZone(); + zone.name = NEW_GROUP_NAME; + zone.lights = ZONE_LIGHTS; + const result = await hue.groups.createGroup(zone); + expect(model.isGroupInstance(result)).to.be.true; expect(result).to.have.property('id').to.be.greaterThan(0); - expect(result).to.have.property('name').to.equal(name); - expect(result).to.have.property('lights').to.have.members(lights); + expect(result).to.have.property('name').to.equal(NEW_GROUP_NAME); + expect(result).to.have.property('lights').to.have.members(util.toStringArray(ZONE_LIGHTS)); + expect(result).to.have.property('type').to.equal('Zone'); + }); + }); + + + describe('Entertainment', () => { + + it('should create a new entertainment group', async () => { + const entertainment = model.createEntertainment(); + entertainment.name = NEW_GROUP_NAME; + // entertainment.lights = []; + + const result = await hue.groups.createGroup(entertainment); + expect(model.isGroupInstance(result)).to.be.true; + expect(result).to.have.property('id').to.be.greaterThan(0); + expect(result).to.have.property('name').to.equal(NEW_GROUP_NAME); + expect(result).to.have.property('lights').to.be.empty; + expect(result).to.have.property('type').to.equal('Entertainment'); }); }); }); @@ -204,117 +273,366 @@ describe('Hue API #groups', () => { describe('#deleteGroup()', () => { - const groupName = 'TestGroupToBeDeleted'; + const DELETE_GROUP_NAME = 'TestGroupToBeDeleted'; let groupToBeDeleted; - beforeEach('createGroup group to be deleted', async () => { - const result = await hue.groups.createGroup(groupName, GROUP_LIGHTS); - groupToBeDeleted = result.id; + + afterEach(async() => { + if (groupToBeDeleted) { + const groups = await hue.groups.getGroupByName(DELETE_GROUP_NAME); + + let promises; + if (groups && groups.length > 0) { + promises = groups.map(group => hue.groups.deleteGroup(group)); + } + + if (promises) { + await Promise.all(promises); + } + } }); - it('should delete test group', async () => { - const result = await hue.groups.deleteGroup(groupToBeDeleted); - expect(result).to.be.true; + + describe('LightGroup', () => { + + beforeEach('create LightGroup', async () => { + const group = model.createLightGroup(); + group.name = DELETE_GROUP_NAME; + group.lights = GROUP_LIGHTS; + + groupToBeDeleted = await hue.groups.createGroup(group); + }); + + + it('should delete using a group object', async () => { + const result = await hue.groups.deleteGroup(groupToBeDeleted); + expect(result).to.be.true; + }); + + it('should delete using a group id', async () => { + const result = await hue.groups.deleteGroup(groupToBeDeleted.id); + expect(result).to.be.true; + }); + }); + + + describe('Zone', () => { + + beforeEach('create Zone', async () => { + const group = model.createZone(); + group.name = DELETE_GROUP_NAME; + group.lights = ZONE_LIGHTS; + + groupToBeDeleted = await hue.groups.createGroup(group); + }); + + + it('should delete using a group object', async () => { + const result = await hue.groups.deleteGroup(groupToBeDeleted); + expect(result).to.be.true; + }); + + it('should delete using a group id', async () => { + const result = await hue.groups.deleteGroup(groupToBeDeleted.id); + expect(result).to.be.true; + }); + }); + + + describe('Room', () => { + + beforeEach('create Room', async () => { + const group = model.createRoom(); + group.name = DELETE_GROUP_NAME; + + groupToBeDeleted = await hue.groups.createGroup(group); + }); + + + it('should delete using a group object', async () => { + const result = await hue.groups.deleteGroup(groupToBeDeleted); + expect(result).to.be.true; + }); + + it('should delete using a group id', async () => { + const result = await hue.groups.deleteGroup(groupToBeDeleted.id); + expect(result).to.be.true; + }); + }); - //TODO test deletion of room + describe('Entertainment', () => { + + beforeEach('create Entertainment Group', async () => { + const group = model.createEntertainment(); + group.name = DELETE_GROUP_NAME; + + groupToBeDeleted = await hue.groups.createGroup(group); + }); + + + it('should delete using a group object', async () => { + const result = await hue.groups.deleteGroup(groupToBeDeleted); + expect(result).to.be.true; + }); + + it('should delete using a group id', async () => { + const result = await hue.groups.deleteGroup(groupToBeDeleted.id); + expect(result).to.be.true; + }); + }); }); describe('#updateAttributes()', () => { - //TODO need to deal with tests for updates for room class + let group = null; - describe('groups', () => { + afterEach('delete updated group', async () => { + if (group) { + await hue.groups.deleteGroup(group); + } + }); - const initialGroupName = 'updateGroupTest' - , initialGroupLights = GROUP_LIGHTS - ; - let groupId; + describe('LightGroup', () => { beforeEach('createGroup group for update', async () => { - const result = await hue.groups.createGroup(initialGroupName, initialGroupLights); - groupId = result.id; + const newGroup = model.createLightGroup(); + newGroup.name = 'LightGroup Updates'; + newGroup.lights = [2]; - console.log(JSON.stringify(result.getHuePayload(), null, 2)); - }); - - afterEach('delete test group for update', async () => { - if (groupId) { - await hue.groups.deleteGroup(groupId); - } + group = await hue.groups.createGroup(newGroup); }); it('should update the name', async () => { - const newName = `renamed-new ${Date.now()}` - , result = await hue.groups.updateAttributes(groupId, {name: newName}) - , group = await hue.groups.get(groupId) + const newName = `renamed-new ${Date.now()}`; + group.name = newName; + + const result = await hue.groups.updateGroupAttributes(group) + , updatedGroup = await hue.groups.get(group.id) ; expect(result).to.be.true; - expect(group).to.have.property('id').to.equal(groupId); - expect(group).to.have.property('name').to.equal(newName); - expect(group).to.have.property('lights').to.have.members(initialGroupLights); + expect(updatedGroup).to.have.property('id').to.equal(group.id); + expect(updatedGroup).to.have.property('name').to.equal(newName); + expect(updatedGroup).to.have.property('lights').to.have.members(group.lights); }); it('should update the lights', async () => { - const newLights = [2, 3, 4] - , result = await hue.groups.updateAttributes(groupId, {lights: newLights}) - , group = await hue.groups.get(groupId) + const newLights = [2, 3, 4]; + group.lights = newLights; + + const result = await hue.groups.updateGroupAttributes(group) + , updatedGroup = await hue.groups.get(group) ; expect(result).to.be.true; - expect(group).to.have.property('id').to.equal(groupId); - expect(group).to.have.property('name').to.equal(initialGroupName); - expect(group).to.have.property('lights').to.have.members(newLights); + expect(updatedGroup).to.have.property('id').to.equal(group.id); + expect(updatedGroup).to.have.property('name').to.equal(group.name); + expect(updatedGroup).to.have.property('lights').to.have.members(util.toStringArray(newLights)); }); it('should update the name and lights', async () => { const newLights = [4, 5] , newName = `renamed-${Date.now()}` - , result = await hue.groups.updateAttributes(groupId, {name: newName, lights: newLights}) - , group = await hue.groups.get(groupId) + ; + group.name = newName; + group.lights = newLights; + + const result = await hue.groups.updateGroupAttributes(group) + , updatedGroup = await hue.groups.get(group) + ; + + expect(result).to.be.true; + expect(updatedGroup).to.have.property('id').to.equal(group.id); + expect(updatedGroup).to.have.property('name').to.equal(newName); + expect(updatedGroup).to.have.property('lights').to.have.members(util.toStringArray(newLights)); + }); + }); + + + describe('Room', () => { + + beforeEach('create room for update', async () => { + const newGroup = model.createRoom(); + newGroup.name = 'Custom Room'; + newGroup.class = 'Gym'; + + group = await hue.groups.createGroup(newGroup); + }); + + + it('should update the name', async () => { + const name = 'Custom Room Updated'; + + group.name = name; + + const result = await hue.groups.updateGroupAttributes(group) + , updatedGroup = await hue.groups.get(group) + ; + + expect(result).to.be.true; + expect(updatedGroup).to.have.property('id').to.equal(group.id); + expect(updatedGroup).to.have.property('name').to.equal(name); + }); + + // lights can only be assigned to a single room and all mine are assigned + it.skip('should update the lights', async () => { + const lights = [4]; + + group.lights = lights; + + const result = await hue.groups.updateAttributes(group) + , updatedGroup = await hue.groups.get(group) + ; + + expect(result).to.be.true; + expect(updatedGroup).to.have.property('id').to.equal(group.id); + expect(updatedGroup).to.have.property('lights').to.equal(util.toStringArray(lights)); + }); + + it('should change the class', async () => { + group.class = 'Other'; + const result = await hue.groups.updateGroupAttributes(group) + , updatedGroup = await hue.groups.get(group) + ; + + expect(result).to.be.true; + expect(updatedGroup).to.have.property('id').to.equal(group.id); + expect(updatedGroup).to.have.property('class').to.equal('Other'); + }); + }); + + + describe('Zone', () => { + + beforeEach('create zone for update', async () => { + const newGroup = model.createZone(); + newGroup.name = 'Custom Zone'; + newGroup.class = 'Lounge'; + + group = await hue.groups.createGroup(newGroup); + }); + + + it('should update the name', async () => { + const name = 'Custom Zone Updated'; + + group.name = name; + + const result = await hue.groups.updateGroupAttributes(group) + , updatedGroup = await hue.groups.get(group) ; expect(result).to.be.true; - expect(group).to.have.property('id').to.equal(groupId); - expect(group).to.have.property('name').to.equal(newName); - expect(group).to.have.property('lights').to.have.members(newLights); + expect(updatedGroup).to.have.property('id').to.equal(group.id); + expect(updatedGroup).to.have.property('name').to.equal(name); }); - //TODO validate that we cannot change the class attribute + it('should update the lights', async () => { + const lights = [4]; + + group.lights = lights; + + const result = await hue.groups.updateGroupAttributes(group) + , updatedGroup = await hue.groups.get(group) + ; + + expect(result).to.be.true; + expect(updatedGroup).to.have.property('id').to.equal(group.id); + expect(updatedGroup).to.have.property('lights').to.have.members(util.toStringArray(lights)); + }); + + it('should change the class', async () => { + group.class = 'Nursery'; + const result = await hue.groups.updateGroupAttributes(group) + , updatedGroup = await hue.groups.get(group) + ; + + expect(result).to.be.true; + expect(updatedGroup).to.have.property('id').to.equal(group.id); + expect(updatedGroup).to.have.property('class').to.equal('Nursery'); + }); }); - describe.skip('rooms', () => { - //TODO need to complete this, need to createGroup a room that does not exist and then update it, including updating the class + describe('Entertainment', () => { + + beforeEach('create entertainment for update', async () => { + const newGroup = model.createEntertainment(); + newGroup.name = 'Custom Entertainment'; + + group = await hue.groups.createGroup(newGroup); + }); + + + it('should update the name', async () => { + const name = 'Custom Name'; + + group.name = name; + + const result = await hue.groups.updateGroupAttributes(group) + , updatedGroup = await hue.groups.get(group) + ; + + expect(result).to.be.true; + expect(updatedGroup).to.have.property('id').to.equal(group.id); + expect(updatedGroup).to.have.property('name').to.equal(name); + }); + + it('should update the lights', async () => { + const lights = [4]; + + group.lights = lights; + + const result = await hue.groups.updateGroupAttributes(group) + , updatedGroup = await hue.groups.get(group) + ; + + expect(result).to.be.true; + expect(updatedGroup).to.have.property('id').to.equal(group.id); + expect(updatedGroup).to.have.property('lights').to.have.members(util.toStringArray(lights)); + }); + + it('should change the class', async () => { + group.class = 'Other'; + const result = await hue.groups.updateGroupAttributes(group) + , updatedGroup = await hue.groups.get(group) + ; + + expect(result).to.be.true; + expect(updatedGroup).to.have.property('id').to.equal(group.id); + expect(updatedGroup).to.have.property('class').to.equal('Other'); + }); }); }); describe('#setGroupState()', () => { - let groupId; + let group; + + beforeEach('create LightGroup for setState', async () => { + const groupToCreate = model.createLightGroup(); + groupToCreate.name = 'setGroupState Tests'; + groupToCreate.lights = 43; - beforeEach('createGroup group for setState', async () => { - const result = await hue.groups.createGroup('groupStateTest', GROUP_LIGHTS); - groupId = result.id; + group = await hue.groups.createGroup(groupToCreate); }); afterEach('delete test group for setState', async () => { - if (groupId) { - await hue.groups.deleteGroup(groupId); + if (group) { + await hue.groups.deleteGroup(group); } }); - it('should set on state to true', async () => { const lightState = {on: true} - , result = await hue.groups.setGroupState(groupId, lightState) - , groupStatus = await hue.groups.get(groupId) + , result = await hue.groups.setGroupState(group, lightState) + , groupStatus = await hue.groups.get(group) ; expect(result).to.be.true; @@ -323,15 +641,17 @@ describe('Hue API #groups', () => { }); it('should set on state to false using GroupState', async () => { - const lightState = new GroupState().off() - , result = await hue.groups.setGroupState(groupId, lightState) - , groupStatus = await hue.groups.get(groupId) + const lightState = new model.lightStates.GroupLightState().off() + , result = await hue.groups.setGroupState(group, lightState) + , groupStatus = await hue.groups.get(group) ; expect(result).to.be.true; expect(groupStatus).to.have.property('action'); expect(groupStatus.action).to.have.property('on').to.be.false; }); + + //TODO could expand tests to rooms, zone and entertainment, but if it works for one should work for all }); }); \ No newline at end of file diff --git a/lib/api/Lights.js b/lib/api/Lights.js index 3bb802e..ab30130 100644 --- a/lib/api/Lights.js +++ b/lib/api/Lights.js @@ -1,28 +1,63 @@ 'use strict'; -const lightsApi = require('./http/endpoints/lights') +const Bottleneck = require('bottleneck') + , lightsApi = require('./http/endpoints/lights') , ApiDefinition = require('./http/ApiDefinition.js') , ApiError = require('../ApiError') + , util = require('../util') + , model = require('../model') ; module.exports = class Lights extends ApiDefinition { constructor(hueApi) { super(hueApi); + + // As per Bridge documentation guidance, limit the number of calls to the light state changes to 10 per second max + this._lightStateLimiter = new Bottleneck({maxConcurrent: 1, minTime: 60}); } + /** + * Gets all the Lights from the Bridge + * @returns {Promise>} + */ getAll() { return this.execute(lightsApi.getAllLights); } - getLightById(id) { + /** + * Get a specific Light from the Bridge. + * @param id {number | Light} The id or Light instance to get from the Bridge. + * @returns {Promise} + */ + getLight(id) { + const lightId = id.id || id; + return this.getAll().then(lights => { - return lights.find(light => { - return light.id === id; - }); + const found = lights.filter(light => light.id === lightId); + + if (found.length === 0) { + throw new ApiError(`Light ${lightId} not found`); + } + return found[0] }); } + /** + * @deprecated since 4.0. Use getLight(id) instead. + * @param id {number} The ide of the light to get. + * @returns {Promise} + */ + getLightById(id) { + util.deprecatedFunction('5.x', 'lights.getLightById(id)', 'Use lights.getLight(id) instead.'); + return this.getLight(id); + } + + /** + * Retrieves a Light from the Bridge by name. + * @param name {string} The name of the light to get. + * @returns {Promise>} + */ getLightByName(name) { return this.getAll().then(lights => { return lights.find(light => { @@ -31,37 +66,104 @@ module.exports = class Lights extends ApiDefinition { }); } + /** + * Discovers the "new" lights detected by the Bridge. + * @returns {Promise>} + */ getNew() { return this.execute(lightsApi.getNewLights); } + /** + * Starts a search for "new"/undiscovered Lights by the bridge. This can take up to 30 seconds to complete. + * @returns {Promise} + */ searchForNew() { return this.execute(lightsApi.searchForNewLights); } + /** + * Obtains the current Attributes and State settings for the specified Light. + * @param id {number | Light} The id or Light instance to get the attributes and state for. + * @returns {Promise} + */ getLightAttributesAndState(id) { return this.execute(lightsApi.getLightAttributesAndState, {id: id}); } + /** + * Obtains the current State settings for the specified Light. + * @param id {number | Light} The id or Light instance to get the current state for. + * @returns {Promise} + */ getLightState(id) { return this.getLightAttributesAndState(id).then(result => { return result.state; }); } + /** + * Sets the current state for the Light to desired settings. + * @param id {number | Light} The id or Light instance to set the state on. + * @param state {Object | LightState} The LightState to set on the light. + * @returns {PromiseLike | Promise} + */ setLightState(id, state) { - return this.hueApi.getLightDefinition(id) + let lightId = id; + if (model.isLightInstance(id)) { + lightId = id.id; + } + + return this.hueApi.getLightDefinition(lightId) .then(device => { if (!device) { - throw new ApiError(`Light with id:${id} was not found on this bridge`); + throw new ApiError(`Light with id:${lightId} was not found on this bridge`); } - return this.execute(lightsApi.setLightState, {id: id, state: state, device: device}); + return this._setLightState(id, state, device); }); } + /** + * Renames a Light on the Bridge to the specified name in the Light instance. + * @param light {Light} The Light to rename with the new name set. + * @returns {Promise} + */ + renameLight(light) { + if (model.isLightInstance(light)) { + return this.execute(lightsApi.setLightAttributes, {id: light, light: light}); + } else { + throw new ApiError('Light parameter is not an instance of a light'); + } + } + + /** + * @deprecated since 4.x, use renameLight(light) instead + * @param id {int} The Light to rename. + * @param name {string} The new name. + * @returns {Promise} + */ rename(id, name) { - return this.execute(lightsApi.setLightAttributes, {id: id, name: name}); + if (arguments.length === 1) { + util.deprecatedFunction('5.x', 'lights.rename(id, name)', 'Use lights.renameLight(light) instead.'); + return this.renameLight(id); + } else { + util.deprecatedFunction('5.x', 'lights.rename(id, name)', 'Use lights.renameLight(light) instead.'); + return this.execute(lightsApi.setLightAttributes, {id: id, name: name}); + } } + /** + * Deletes a Light from the Hue Bridge + * @param id { number | Light} The id or Light instance to be deleted + * @returns {Promise} + */ deleteLight(id) { return this.execute(lightsApi.deleteLight, {id: id}); } + + _setLightState(id, state, device) { + const self = this; + + return this._lightStateLimiter.schedule(() => { + return self.execute(lightsApi.setLightState, {id: id, state: state, device: device}); + }); + } }; \ No newline at end of file diff --git a/lib/api/Lights.test.js b/lib/api/Lights.test.js index 17dd359..76ecdb7 100644 --- a/lib/api/Lights.test.js +++ b/lib/api/Lights.test.js @@ -1,25 +1,28 @@ 'use strict'; const expect = require('chai').expect - , v3Api = require('../v3').api - , discovery = require('../v3').discovery - , LightState = require('../v3').lightStates.LightState + , v3 = require('../v3') + , discovery = v3.discovery + , model = require('../model') + , LightState = model.lightStates.LightState , testValues = require('../../test/support/testValues.js') ; -describe('Hue API #lights', () => { +describe('Hue API #lights', function() { let hue; - before(() => { - return discovery.nupnpSearch() - .then(searchResults => { - const localApi = v3Api.createLocal(searchResults[0].ipaddress); - return localApi.connect(testValues.username) - .then(api => { - hue = api; - }); - }); + this.timeout(10000); + + before(async () => { + const searchResults = await discovery.nupnpSearch(); + + if (! searchResults || searchResults.length === 0) { + throw new Error('Failed to find a bridge in nupnp search'); + } + + const localApi = v3.api.createLocal(searchResults[0].ipaddress); + hue = await localApi.connect(testValues.username); }); describe('#getAll()', () => { @@ -30,13 +33,26 @@ describe('Hue API #lights', () => { let light = lights[0]; expect(light).to.have.property('id').to.be.greaterThan(0); - //TODO this is a pointless test now that we use objects to model the data - expect(light.getHuePayload()).to.have - .keys('id', 'name', 'modelid', 'type', 'swversion', 'swupdate', 'uniqueid', 'manufacturername', - 'state', 'capabilities', 'config', 'productname'); + + expect(model.isLightInstance(light)).to.be.true; }); }); + describe('#getLight()', () => { + + it('should get light with id === 2', async () => { + const result = await hue.lights.getLight(2); + expect(result).to.have.property('id').to.equal(2); + }); + + it('should get light with id as a Light object', async () => { + const light = await hue.lights.getLight(2); + expect(light).to.have.property('id').to.equal(2); + + const result = await hue.lights.getLight(light); + expect(result).to.have.property('id').to.equal(2); + }); + }); describe('#getLightById()', () => { @@ -44,6 +60,14 @@ describe('Hue API #lights', () => { const result = await hue.lights.getLightById(2); expect(result).to.have.property('id').to.equal(2); }); + + it('should get light with id as a Light object', async () => { + const light = await hue.lights.getLightById(2); + expect(light).to.have.property('id').to.equal(2); + + const result = await hue.lights.getLightById(light); + expect(result).to.have.property('id').to.equal(2); + }); }); @@ -79,45 +103,94 @@ describe('Hue API #lights', () => { describe('#getLightAttributesAndState()', () => { - it('should return a light state for id=2', async () => { - const result = await hue.lights.getLightAttributesAndState(2); - - expect(result).to.have.property('id', 2); - - expect(result).to.have.property('state'); - expect(result.state).to.have.property('on'); - expect(result.state).to.have.property('bri'); - expect(result.state).to.have.property('hue'); - expect(result.state).to.have.property('sat'); - expect(result.state).to.have.property('effect'); - expect(result.state).to.have.property('xy'); - expect(result.state).to.have.property('alert'); - expect(result.state).to.have.property('colormode'); - expect(result.state).to.have.property('mode'); - expect(result.state).to.have.property('reachable'); - - expect(result).to.have.property('swupdate'); - expect(result.swupdate).to.have.property('state'); - expect(result.swupdate).to.have.property('lastinstall'); - - expect(result).to.have.property('type'); - expect(result).to.have.property('name'); - expect(result).to.have.property('modelid'); - expect(result).to.have.property('manufacturername'); - expect(result).to.have.property('productname'); - - expect(result).to.have.property('capabilities'); - expect(result.capabilities).to.have.property('certified'); - expect(result.capabilities).to.have.property('control'); - expect(result.capabilities).to.have.property('streaming'); - - expect(result).to.have.property('config'); - expect(result.config).to.have.property('archetype'); - expect(result.config).to.have.property('function'); - expect(result.config).to.have.property('direction'); - - expect(result).to.have.property('uniqueid'); - expect(result).to.have.property('swversion'); + describe('using id', () => { + + it('should return a light state for id=2', async () => { + const result = await hue.lights.getLightAttributesAndState(2); + + expect(result).to.have.property('id', 2); + + expect(result).to.have.property('state'); + expect(result.state).to.have.property('on'); + expect(result.state).to.have.property('bri'); + expect(result.state).to.have.property('hue'); + expect(result.state).to.have.property('sat'); + expect(result.state).to.have.property('effect'); + expect(result.state).to.have.property('xy'); + expect(result.state).to.have.property('alert'); + expect(result.state).to.have.property('colormode'); + expect(result.state).to.have.property('mode'); + expect(result.state).to.have.property('reachable'); + + expect(result).to.have.property('swupdate'); + expect(result.swupdate).to.have.property('state'); + expect(result.swupdate).to.have.property('lastinstall'); + + expect(result).to.have.property('type'); + expect(result).to.have.property('name'); + expect(result).to.have.property('modelid'); + expect(result).to.have.property('manufacturername'); + expect(result).to.have.property('productname'); + + expect(result).to.have.property('capabilities'); + expect(result.capabilities).to.have.property('certified'); + expect(result.capabilities).to.have.property('control'); + expect(result.capabilities).to.have.property('streaming'); + + expect(result).to.have.property('config'); + expect(result.config).to.have.property('archetype'); + expect(result.config).to.have.property('function'); + expect(result.config).to.have.property('direction'); + + expect(result).to.have.property('uniqueid'); + expect(result).to.have.property('swversion'); + }); + }); + + describe('using Light instance', () => { + + it('should return a light state', async () => { + const light = await hue.lights.getLight(2) + , result = await hue.lights.getLightAttributesAndState(light) + ; + + expect(result).to.have.property('id').to.equal(2); + + expect(result).to.have.property('state'); + expect(result.state).to.have.property('on'); + expect(result.state).to.have.property('bri'); + expect(result.state).to.have.property('hue'); + expect(result.state).to.have.property('sat'); + expect(result.state).to.have.property('effect'); + expect(result.state).to.have.property('xy'); + expect(result.state).to.have.property('alert'); + expect(result.state).to.have.property('colormode'); + expect(result.state).to.have.property('mode'); + expect(result.state).to.have.property('reachable'); + + expect(result).to.have.property('swupdate'); + expect(result.swupdate).to.have.property('state'); + expect(result.swupdate).to.have.property('lastinstall'); + + expect(result).to.have.property('type'); + expect(result).to.have.property('name'); + expect(result).to.have.property('modelid'); + expect(result).to.have.property('manufacturername'); + expect(result).to.have.property('productname'); + + expect(result).to.have.property('capabilities'); + expect(result.capabilities).to.have.property('certified'); + expect(result.capabilities).to.have.property('control'); + expect(result.capabilities).to.have.property('streaming'); + + expect(result).to.have.property('config'); + expect(result.config).to.have.property('archetype'); + expect(result.config).to.have.property('function'); + expect(result.config).to.have.property('direction'); + + expect(result).to.have.property('uniqueid'); + expect(result).to.have.property('swversion'); + }); }); }); @@ -138,58 +211,108 @@ describe('Hue API #lights', () => { expect(result).to.have.property('mode'); expect(result).to.have.property('reachable'); }); + + it('should return a light state for Light object', async () => { + const light = await hue.lights.getLight(2) + , result = await hue.lights.getLightState(light) + ; + + expect(result).to.have.property('on'); + expect(result).to.have.property('bri'); + expect(result).to.have.property('hue'); + expect(result).to.have.property('sat'); + expect(result).to.have.property('effect'); + expect(result).to.have.property('xy'); + expect(result).to.have.property('alert'); + expect(result).to.have.property('colormode'); + expect(result).to.have.property('mode'); + expect(result).to.have.property('reachable'); + }); }); describe('#rename()', () => { - const renameLightId = 2; - - let originalName; + let light + , originalName + ; beforeEach(async () => { - const light = await hue.lights.getLightById(renameLightId); + light = await hue.lights.getLight(2); originalName = light.name; }); - afterEach(async() => { + afterEach('reset light name in bridge', async () => { if (originalName) { - await hue.lights.rename(renameLightId, originalName); + light.name = originalName; + await hue.lights.rename(light); } }); - it('should rename a light', async () => { - const newName = 'Lounge Living Color' - , result = await hue.lights.rename(renameLightId, newName) - , actual = await hue.lights.getLightAttributesAndState(renameLightId); + describe('using id and name parameters', () => { - expect(result).to.be.true; + it('should rename a light using id as integer', async () => { + const newName = 'Lounge Living Color' + , result = await hue.lights.rename(light.id, newName) + , updatedLight = await hue.lights.getLight(light); + + expect(result).to.be.true; + + expect(updatedLight).to.have.property('id').to.equal(light.id); + expect(updatedLight).to.have.property('name').to.equal(newName); + }); + + it('should rename a light using id as a Light instance', async () => { + const newName = 'Lounge Living Color' + , result = await hue.lights.rename(light, newName) + , updateLight = await hue.lights.getLight(light); - expect(actual).to.have.property('id', renameLightId); - expect(actual).to.have.property('name', newName); + expect(result).to.be.true; + + expect(updateLight).to.have.property('id').to.equal(light.id); + expect(updateLight).to.have.property('name').to.equal(newName); + }); + + it('should error is name is too long', async () => { + const newName = `Renamed Light ${Date.now()} ${Date.now()}`; + + try { + await hue.lights.rename(light.id, newName); + expect.fail('Should have failed to rename light'); + } catch (err) { + expect(err.message).to.contain('does not meet maximum length requirement'); + } + }); }); + describe('using light instance', () => { - it('should error is name is too long', async () => { - const newName = `Renamed Light ${Date.now()} ${Date.now()}`; + it('should rename a light', async () => { + const newName = 'New Light Name'; - try { - await hue.lights.rename(renameLightId, newName); - expect.fail('Should have failed to rename light'); - } catch (err) { - expect(err.message).to.contain('does not meet maximum length requirement'); - } + light.name = newName; + + const result = await hue.lights.rename(light) + , updateLight = await hue.lights.getLight(light) + ; + + expect(result).to.be.true; + + expect(updateLight).to.have.property('id').to.equal(light.id); + expect(updateLight).to.have.property('name').to.equal(newName); + }); }); }); describe('#setLightState()', () => { - describe('using raw objects', () => { + describe('using Light instance', () => { it('should set an xy value', async () => { const id = testValues.testLightId - , result = await hue.lights.setLightState(id, {on: true, xy: [0.1948, 0.5478]}) + , light = await hue.lights.getLight(id) + , result = await hue.lights.setLightState(light, {on: true, xy: [0.1948, 0.5478]}) , finalLightState = await hue.lights.getLightState(id) ; @@ -200,305 +323,323 @@ describe('Hue API #lights', () => { expect(finalLightState.xy[0]).to.be.within(0.194, 0.195); expect(finalLightState.xy[1]).to.be.closeTo(0.547, 0.548); }); + }); + describe('using light id', () => { - it('should set an xy and bri value', async () => { - const id = testValues.testLightId - , state = { - on: true, - bri: 254, - colormode: 'xy', - xy: [0.153, 0.048] - } - , result = await hue.lights.setLightState(id, state) - , finalLightState = await hue.lights.getLightState(id) - ; + describe('using raw objects', () => { - expect(result).to.be.true; - expect(finalLightState).to.have.property('on').to.be.true; + it('should set an xy value', async () => { + const id = testValues.testLightId + , result = await hue.lights.setLightState(id, {on: true, xy: [0.1948, 0.5478]}) + , finalLightState = await hue.lights.getLightState(id) + ; - expect(finalLightState).to.have.property('xy'); - expect(finalLightState.xy[0]).to.be.within(0.15, 0.155); - expect(finalLightState.xy[1]).to.be.closeTo(0.045, 0.05); - }); - }); + expect(result).to.be.true; + expect(finalLightState).to.have.property('on').to.be.true; - describe('using lightState object', () => { + expect(finalLightState).to.have.property('xy'); + expect(finalLightState.xy[0]).to.be.within(0.194, 0.195); + expect(finalLightState.xy[1]).to.be.closeTo(0.547, 0.548); + }); - it('should set alert to "lselect"', async () => { - const id = testValues.testLightId - , state = new LightState().alert('lselect') - , result = await hue.lights.setLightState(id, state) - , finalLightState = await hue.lights.getLightState(id) - ; - expect(result).to.be.true; - expect(finalLightState).to.have.property('alert').to.equal('lselect'); + it('should set an xy and bri value', async () => { + const id = testValues.testLightId + , state = { + on: true, + bri: 254, + colormode: 'xy', + xy: [0.153, 0.048] + } + , result = await hue.lights.setLightState(id, state) + , finalLightState = await hue.lights.getLightState(id) + ; + + expect(result).to.be.true; + expect(finalLightState).to.have.property('on').to.be.true; + + expect(finalLightState).to.have.property('xy'); + expect(finalLightState.xy[0]).to.be.within(0.15, 0.155); + expect(finalLightState.xy[1]).to.be.closeTo(0.045, 0.05); + }); }); + describe('using lightState object', () => { - describe('#on', () => { + it('should set alert to "lselect"', async () => { + const id = testValues.testLightId + , state = new LightState().alert('lselect') + , result = await hue.lights.setLightState(id, state) + , finalLightState = await hue.lights.getLightState(id) + ; - function testOn(on) { - return async () => { - const id = testValues.testLightId - , state = new LightState().on(on) - , result = await hue.lights.setLightState(id, state) - , finalLightState = await hue.lights.getLightState(id) - ; + expect(result).to.be.true; + expect(finalLightState).to.have.property('alert').to.equal('lselect'); + }); - expect(result).to.be.true; - expect(finalLightState).to.have.property('on').to.equal(on); - }; - } - it('should set on to true', testOn(true)); + describe('#on', () => { - it('should set on to false', testOn(false)); - }); + function testOn(on) { + return async () => { + const id = testValues.testLightId + , state = new LightState().on(on) + , result = await hue.lights.setLightState(id, state) + , finalLightState = await hue.lights.getLightState(id) + ; + expect(result).to.be.true; + expect(finalLightState).to.have.property('on').to.equal(on); + }; + } - describe('#bri', () => { + it('should set on to true', testOn(true)); - function testBri(briVal) { - return async () => { - const id = testValues.testLightId - , state = new LightState().on().bri(briVal) - , result = await hue.lights.setLightState(id, state) - , finalLightState = await hue.lights.getLightState(id) - ; + it('should set on to false', testOn(false)); + }); - expect(result).to.be.true; - expect(finalLightState).to.have.property('on').to.be.true; - expect(finalLightState).to.have.property('bri').to.equal(briVal); - }; - } - it('should set bri to 1', testBri(1)); + describe('#bri', () => { - it('should set bri to 254', testBri(254)); + function testBri(briVal) { + return async () => { + const id = testValues.testLightId + , state = new LightState().on().bri(briVal) + , result = await hue.lights.setLightState(id, state) + , finalLightState = await hue.lights.getLightState(id) + ; - it('should set bri to 100', testBri(100)); - }); + expect(result).to.be.true; + expect(finalLightState).to.have.property('on').to.be.true; + expect(finalLightState).to.have.property('bri').to.equal(briVal); + }; + } + it('should set bri to 1', testBri(1)); - describe('#hue', () => { + it('should set bri to 254', testBri(254)); - function testHue(val) { - return async () => { - const id = testValues.testLightId - , state = new LightState().on().hue(val) - , result = await hue.lights.setLightState(id, state) - , finalLightState = await hue.lights.getLightState(id) - ; + it('should set bri to 100', testBri(100)); + }); - expect(result).to.be.true; - expect(finalLightState).to.have.property('on').to.be.true; - expect(finalLightState).to.have.property('hue').to.equal(val); - }; - } - it('should set hue to 1', testHue(1)); + describe('#hue', () => { - it('should set hue to 254', testHue(254)); + function testHue(val) { + return async () => { + const id = testValues.testLightId + , state = new LightState().on().hue(val) + , result = await hue.lights.setLightState(id, state) + , finalLightState = await hue.lights.getLightState(id) + ; - it('should set hue to 100', testHue(100)); - }); + expect(result).to.be.true; + expect(finalLightState).to.have.property('on').to.be.true; + expect(finalLightState).to.have.property('hue').to.equal(val); + }; + } + it('should set hue to 1', testHue(1)); - describe('#sat', () => { + it('should set hue to 254', testHue(254)); - function testSat(val) { - return async () => { - const id = testValues.testLightId - , state = new LightState().on().sat(val) - , result = await hue.lights.setLightState(id, state) - , finalLightState = await hue.lights.getLightState(id) - ; + it('should set hue to 100', testHue(100)); + }); - expect(result).to.be.true; - expect(finalLightState).to.have.property('on').to.be.true; - expect(finalLightState).to.have.property('sat').to.equal(val); - }; - } - it('should set sat to 1', testSat(1)); + describe('#sat', () => { - it('should set sat to 254', testSat(254)); + function testSat(val) { + return async () => { + const id = testValues.testLightId + , state = new LightState().on().sat(val) + , result = await hue.lights.setLightState(id, state) + , finalLightState = await hue.lights.getLightState(id) + ; - it('should set sat to 100', testSat(100)); - }); + expect(result).to.be.true; + expect(finalLightState).to.have.property('on').to.be.true; + expect(finalLightState).to.have.property('sat').to.equal(val); + }; + } + it('should set sat to 1', testSat(1)); - describe('#xy', () => { + it('should set sat to 254', testSat(254)); - function testXY(xVal, yVal) { - return async () => { - const id = testValues.testLightId - , state = new LightState().on().xy(xVal, yVal) - , result = await hue.lights.setLightState(id, state) - , finalLightState = await hue.lights.getLightState(id) - ; + it('should set sat to 100', testSat(100)); + }); - expect(result).to.be.true; - expect(finalLightState).to.have.property('on').to.be.true; - expect(finalLightState).to.have.property('xy').to.contain(xVal, yVal); - }; - } - it('should set xy to 1,1', testXY(1, 1)); + describe('#xy', () => { - it('should set xy to 0,1', testXY(0, 1)); + function testXY(xVal, yVal) { + return async () => { + const id = testValues.testLightId + , state = new LightState().on().xy(xVal, yVal) + , result = await hue.lights.setLightState(id, state) + , finalLightState = await hue.lights.getLightState(id) + ; - it('should set xy to 0,0', testXY(0, 0)); + expect(result).to.be.true; + expect(finalLightState).to.have.property('on').to.be.true; + expect(finalLightState).to.have.property('xy').to.contain(xVal, yVal); + }; + } - it('should set xy to 1,0', testXY(1, 0)); + it('should set xy to 1,1', testXY(1, 1)); - it('should set xy to 0.5,0.5', testXY(0.5, 0.5)); + it('should set xy to 0,1', testXY(0, 1)); - it('should set xy to 0.178,0.99', testXY(0.178, 0.99)); - }); + it('should set xy to 0,0', testXY(0, 0)); + it('should set xy to 1,0', testXY(1, 0)); - describe('#ct', () => { + it('should set xy to 0.5,0.5', testXY(0.5, 0.5)); - function testCt(val) { - return async () => { - const id = testValues.testLightId - , state = new LightState().on().ct(val) - , result = await hue.lights.setLightState(id, state) - , finalLightState = await hue.lights.getLightState(id) - ; + it('should set xy to 0.178,0.99', testXY(0.178, 0.99)); + }); - expect(result).to.be.true; - expect(finalLightState).to.have.property('on').to.be.true; - expect(finalLightState).to.have.property('ct').to.equal(val); - }; - } - it('should set ct to 153', testCt(153)); + describe('#ct', () => { - it('should set ct to 500', testCt(500)); + function testCt(val) { + return async () => { + const id = testValues.testLightId + , state = new LightState().on().ct(val) + , result = await hue.lights.setLightState(id, state) + , finalLightState = await hue.lights.getLightState(id) + ; - it('should set ct to 200', testCt(200)); + expect(result).to.be.true; + expect(finalLightState).to.have.property('on').to.be.true; + expect(finalLightState).to.have.property('ct').to.equal(val); + }; + } - it('should set ct to 499', testCt(499)); + it('should set ct to 153', testCt(153)); - //TODO do failure conditions - }); + it('should set ct to 500', testCt(500)); + it('should set ct to 200', testCt(200)); - describe('#alert', () => { + it('should set ct to 499', testCt(499)); - function testAlert(val) { - return async () => { - const id = testValues.testLightId - , state = new LightState().on().alert(val) - , result = await hue.lights.setLightState(id, state) - , finalLightState = await hue.lights.getLightState(id) - ; + //TODO do failure conditions + }); - expect(result).to.be.true; - expect(finalLightState).to.have.property('on').to.be.true; - expect(finalLightState).to.have.property('alert').to.equal(val); - }; - } - it('should set alert to none', testAlert('none')); + describe('#alert', () => { - it('should set alert to select', testAlert('select')); + function testAlert(val) { + return async () => { + const id = testValues.testLightId + , state = new LightState().on().alert(val) + , result = await hue.lights.setLightState(id, state) + , finalLightState = await hue.lights.getLightState(id) + ; - it('should set alert to lselect', testAlert('lselect')); - }); + expect(result).to.be.true; + expect(finalLightState).to.have.property('on').to.be.true; + expect(finalLightState).to.have.property('alert').to.equal(val); + }; + } + it('should set alert to none', testAlert('none')); - describe('#effect', () => { + it('should set alert to select', testAlert('select')); - function testEffect(val) { - return async () => { - const id = testValues.testLightId - , state = new LightState().on().effect(val) - , result = await hue.lights.setLightState(id, state) - , finalLightState = await hue.lights.getLightState(id) - ; + it('should set alert to lselect', testAlert('lselect')); + }); - expect(result).to.be.true; - expect(finalLightState).to.have.property('on').to.be.true; - expect(finalLightState).to.have.property('effect').to.equal(val); - }; - } - it('should set alert to none', testEffect('none')); + describe('#effect', () => { - it('should set alert to colorloop', testEffect('colorloop')); - }); + function testEffect(val) { + return async () => { + const id = testValues.testLightId + , state = new LightState().on().effect(val) + , result = await hue.lights.setLightState(id, state) + , finalLightState = await hue.lights.getLightState(id) + ; + expect(result).to.be.true; + expect(finalLightState).to.have.property('on').to.be.true; + expect(finalLightState).to.have.property('effect').to.equal(val); + }; + } - describe('#transitiontime', () => { + it('should set alert to none', testEffect('none')); - function testTransitiontime(val) { - return async () => { - const id = testValues.testLightId - , state = new LightState().on().transitiontime(val) - , result = await hue.lights.setLightState(id, state) - , finalLightState = await hue.lights.getLightState(id) - ; + it('should set alert to colorloop', testEffect('colorloop')); + }); - expect(result).to.be.true; - expect(finalLightState).to.have.property('on').to.be.true; - // It is not possible to query the transition time value azs it is no longer returned in the state values - // from the Hue API. - }; - } - afterEach(async () => { - // Turn off the light so that the next test call will do something. - const id = testValues.testLightId; - await hue.lights.setLightState(id, new LightState().off()); - }); + describe('#transitiontime', () => { - it('should set to 0', testTransitiontime(0)); + function testTransitiontime(val) { + return async () => { + const id = testValues.testLightId + , state = new LightState().on().transitiontime(val) + , result = await hue.lights.setLightState(id, state) + , finalLightState = await hue.lights.getLightState(id) + ; - it('should set to 4', testTransitiontime(4)); + expect(result).to.be.true; + expect(finalLightState).to.have.property('on').to.be.true; + // It is not possible to query the transition time value azs it is no longer returned in the state values + // from the Hue API. + }; + } - it('should set to 10', testTransitiontime(10)); + afterEach(async () => { + // Turn off the light so that the next test call will do something. + const id = testValues.testLightId; + await hue.lights.setLightState(id, new LightState().off()); + }); - it('should set to 1000', testTransitiontime(1000)); - }); + it('should set to 0', testTransitiontime(0)); + it('should set to 4', testTransitiontime(4)); - //TODO inc seem to be more difficult to test - describe('#bri_inc', () => { + it('should set to 10', testTransitiontime(10)); - function testBriInc(initialBri, incVal, expectedBri) { - return async () => { - const id = testValues.testLightId - , initialState = new LightState().on().bri(initialBri) - , incState = new LightState().transitiontime(0).bri_inc(incVal) - ; + it('should set to 1000', testTransitiontime(1000)); + }); - const initialResult = await hue.lights.setLightState(id, initialState); - expect(initialResult).to.be.true; - const result = await hue.lights.setLightState(id, incState); - expect(result).to.be.true; + //TODO inc seem to be more difficult to test + describe('#bri_inc', () => { + function testBriInc(initialBri, incVal, expectedBri) { + return async () => { + const id = testValues.testLightId + , initialState = new LightState().on().bri(initialBri) + , incState = new LightState().transitiontime(0).bri_inc(incVal) + ; - const finalLightState = await hue.lights.getLightState(id); - expect(finalLightState).to.have.property('on').to.be.true; - expect(finalLightState).to.have.property('bri').to.equal(expectedBri); - }; - } + const initialResult = await hue.lights.setLightState(id, initialState); + expect(initialResult).to.be.true; - it('should respond to +1', testBriInc(1, 1, 2)); + const result = await hue.lights.setLightState(id, incState); + expect(result).to.be.true; - it('should respond to +200', testBriInc(1, 200, 201)); - }); - }); + const finalLightState = await hue.lights.getLightState(id); + expect(finalLightState).to.have.property('on').to.be.true; + expect(finalLightState).to.have.property('bri').to.equal(expectedBri); + }; + } + it('should respond to +1', testBriInc(1, 1, 2)); + + it('should respond to +200', testBriInc(1, 200, 201)); + }); + }); - //TODO complete all the property tests for a light state + //TODO complete all the property tests for a light state + }); }); }); \ No newline at end of file diff --git a/lib/api/ResourceLinks.js b/lib/api/ResourceLinks.js index 10f47fb..2f7e940 100644 --- a/lib/api/ResourceLinks.js +++ b/lib/api/ResourceLinks.js @@ -3,6 +3,7 @@ const resourceLinksApi = require('./http/endpoints/resourcelinks') , ResourceLink = require('../model/ResourceLink') , ApiDefinition = require('./http/ApiDefinition.js') + , util = require('../util') ; @@ -12,23 +13,50 @@ module.exports = class ResourceLinks extends ApiDefinition { super(hueApi); } + /** + * @returns {Promise} + */ getAll() { return this.execute(resourceLinksApi.getAll); } - get(id) { + /** + * @param id {int | ResourceLink} + * @returns {Promise} + */ + getResourceLink(id) { return this.execute(resourceLinksApi.getResourceLink, {id: id}); + } + /** + * @param name {string} + * @returns {Promise} + */ + getResourceLinkByName(name) { + return this.getAll() + .then(resourceLinks => { + return resourceLinks.filter(resourceLink => resourceLink.name === name); + }); + } + + /** + * @param resourceLink {ResourceLink} + * @returns {Promise} + */ createResourceLink(resourceLink) { const self = this; return self.execute(resourceLinksApi.createResourceLink, {resourceLink: resourceLink}) .then(result => { - return self.get(result.id); + return self.getResourceLink(result.id); }); } + /** + * @param id {int | ResourceLink} + * @returns {Promise} + */ deleteResourceLink(id) { let resourceLinkId = id; if (id instanceof ResourceLink) { @@ -37,9 +65,11 @@ module.exports = class ResourceLinks extends ApiDefinition { return this.execute(resourceLinksApi.deleteResourceLink, {id: resourceLinkId}); } + /** + * @param resourceLink {ResourceLink} + * @returns {Promise} + */ updateResourceLink(resourceLink) { return this.execute(resourceLinksApi.updateResourceLink, {id: resourceLink.id, resourceLink: resourceLink}); } - - //TODO consider adding getByName() }; \ No newline at end of file diff --git a/lib/api/ResourceLinks.test.js b/lib/api/ResourceLinks.test.js index 707166c..8fee030 100644 --- a/lib/api/ResourceLinks.test.js +++ b/lib/api/ResourceLinks.test.js @@ -39,15 +39,27 @@ describe('Hue API #resourceLinks', () => { describe('#get()', () => { - it('should get a resource link that exists', async () => { + it('should get a resource link that exists using an id', async () => { const allResourceLinks = await hue.resourceLinks.getAll() , targetResourceLink = allResourceLinks[allResourceLinks.length - 1] - , resourceLinkId = targetResourceLink.id ; - const resourceLink = await hue.resourceLinks.get(resourceLinkId); + const resourceLink = await hue.resourceLinks.getResourceLink(targetResourceLink.id); expect(model.isResourceLinkInstance(resourceLink)).to.be.true; - expect(resourceLink).to.have.property('id').to.equal(resourceLinkId); + expect(resourceLink).to.have.property('id').to.equal(targetResourceLink.id); + + //TODO need to do a deep equals on contents against targetResourceLink + expect(resourceLink).to.have.property('name').to.equal(targetResourceLink.name); + }); + + it('should get a resource link that exists using a ResourceLink', async () => { + const allResourceLinks = await hue.resourceLinks.getAll() + , targetResourceLink = allResourceLinks[allResourceLinks.length - 1] + ; + + const resourceLink = await hue.resourceLinks.getResourceLink(targetResourceLink); + expect(model.isResourceLinkInstance(resourceLink)).to.be.true; + expect(resourceLink).to.have.property('id').to.equal(targetResourceLink.id); //TODO need to do a deep equals on contents against targetResourceLink expect(resourceLink).to.have.property('name').to.equal(targetResourceLink.name); @@ -163,7 +175,7 @@ describe('Hue API #resourceLinks', () => { expect(updated).to.have.property('classid').to.be.true; expect(updated).to.have.property('links').to.be.true; - const resourceLink = await hue.resourceLinks.get(existingResourceLink.id); + const resourceLink = await hue.resourceLinks.getResourceLink(existingResourceLink.id); expect(resourceLink).to.have.property('name').to.equal(newName); }); }); diff --git a/lib/api/Rules.js b/lib/api/Rules.js index ebde8c2..f727db9 100644 --- a/lib/api/Rules.js +++ b/lib/api/Rules.js @@ -3,6 +3,7 @@ const rulesApi = require('./http/endpoints/rules') , Rule = require('../model/rules/Rule') , ApiDefinition = require('./http/ApiDefinition.js') + , util = require('../util') ; module.exports = class Sensors extends ApiDefinition { @@ -11,14 +12,29 @@ module.exports = class Sensors extends ApiDefinition { super(hueApi); } + /** + * @returns {Promise} + */ getAll() { return this.execute(rulesApi.getAll); } - get(id) { + /** + * @param id {int | Rule} + * @returns {Promise} + */ + getRule(id) { return this.execute(rulesApi.getRule, {id: id}); } + /** + * @deprecated since 4.x, use getRule(id) instead + */ + get(id) { + util.deprecatedFunction('5.x', 'rules.get(id)', 'Use rules.getRule(id) instead.'); + return this.getRule(id); + } + // getOrphanedRules() { // return this._filterRules(rule => {return rule.status === 'resourcedeleted'}); // } @@ -27,14 +43,33 @@ module.exports = class Sensors extends ApiDefinition { // return this._filterRules(rule => {return rule.status === 'disabled'}); // } + /** + * @param name {string} + * @returns {Promise} + */ + getRuleByName(name) { + return this.getAll() + .then(rules => { + return rules.filter(rule => rule.name === name); + }); + } + + /** + * @param rule {Rule} + * @returns {Promise} + */ createRule(rule) { const self = this; return self.execute(rulesApi.createRule, {rule: rule}) .then(data => { - return self.get(data.id); + return self.getRule(data.id); }); } + /** + * @param id {int | Rule} + * @returns {Promise} + */ deleteRule(id) { if (id instanceof Rule) { return this.execute(rulesApi.deleteRule, {id: id.id}); @@ -43,6 +78,11 @@ module.exports = class Sensors extends ApiDefinition { } } + /** + * + * @param rule {Rule} + * @returns {Promise<*>} + */ updateRule(rule) { return this.execute(rulesApi.updateRule, {id: rule.id, rule: rule}); } diff --git a/lib/api/Rules.test.js b/lib/api/Rules.test.js index 697345c..748d4a4 100644 --- a/lib/api/Rules.test.js +++ b/lib/api/Rules.test.js @@ -4,20 +4,18 @@ const expect = require('chai').expect , v3Api = require('../v3').api , discovery = require('../v3').discovery , model = require('../v3').model - - // , Rule = require('../model/rules/Rule') - , LightState = require('../v3').lightStates.LightState - // , conditionOperators = require('../v3').rules.conditions.operators + , LightState = model.lightStates.LightState + , GroupState = model.lightStates.GroupLightState , conditionOperators = model.ruleConditionOperators - - // , rules = require('../v3').rules , testValues = require('../../test/support/testValues.js') ; describe('Hue API #rules', () => { let hue - , testOpenCloseSensor + , testSensor + , targetLight + , targetGroup ; before(() => { @@ -44,18 +42,27 @@ describe('Hue API #rules', () => { return hue.sensors.get(result.id); }) .then(sensor => { - testOpenCloseSensor = sensor; + testSensor = sensor; + + return Promise.all([ + hue.lights.getAll(), + hue.groups.getAll() + ]); + }) + .then(results => { + targetLight = results[0][1]; + targetGroup = results[1][1]; }); }); }); after(async () => { - if (hue && testOpenCloseSensor) { + if (hue && testSensor) { try { - await hue.sensors.deleteSensor(testOpenCloseSensor.id); + await hue.sensors.deleteSensor(testSensor.id); } catch (err) { - console.error(`Failed to remove CLIP Sensor, ${testOpenCloseSensor.toString()}, error: ${err}`); + console.error(`Failed to remove CLIP Sensor, ${testSensor.toString()}, error: ${err}`); } } }); @@ -82,15 +89,30 @@ describe('Hue API #rules', () => { describe('#get()', () => { - it('should get an existing rule', async () => { - const all = await hue.rules.getAll() - , target = all[0] - ; + describe('using id value', () => { - const rule = await hue.rules.get(target.id); + it('should get an existing rule', async () => { + const allRules = await hue.rules.getAll() + , target = allRules[0] + ; - expect(rule).to.have.property('id').to.equal(target.id); - //TODO more values need to be checked + const rule = await hue.rules.get(target.id); + + expect(rule).to.have.property('id').to.equal(target.id); + }); + }); + + + describe('using Rule object', () => { + + it('should get an existing rule', async () => { + const allRules = await hue.rules.getAll() + , target = allRules[1] + ; + + const rule = await hue.rules.get(target); + expect(rule).to.have.property('id').to.equal(target.id); + }); }); }); @@ -114,8 +136,8 @@ describe('Hue API #rules', () => { rule.name = 'Simple Test Rule'; rule.recycle = true; - rule.addCondition(model.ruleConditions.sensor(testOpenCloseSensor).when('open').changed()); - rule.addAction(model.ruleActions.light(testValues.hueLightId).withState(new LightState().on())); + rule.addCondition(model.ruleConditions.sensor(testSensor).when('open').changed()); + rule.addAction(model.actions.light(targetLight).withState(new LightState().on())); const result = await hue.rules.createRule(rule); expect(result).to.have.property('id'); @@ -126,38 +148,62 @@ describe('Hue API #rules', () => { describe('#deleteRule()', () => { - let ruleId = null; + let rule = null; beforeEach(async () => { - const rule = model.createRule(); - rule.name = 'Test Rule to be deleted'; - rule.recycle = true; - rule.addCondition(model.ruleConditions.sensor(testOpenCloseSensor).when('open').equals(true)); - rule.addAction(model.ruleActions.light(testValues.hueLightId).withState(new LightState().on())); + const newRule = model.createRule(); + newRule.name = 'Test Rule to be deleted'; + newRule.recycle = true; + newRule.addCondition(model.ruleConditions.sensor(testSensor).when('open').equals(true)); + newRule.addAction(model.actions.light(targetLight).withState(new LightState().on())); - const result = await hue.rules.createRule(rule); - ruleId = result.id; + rule = await hue.rules.createRule(newRule); }); afterEach(async () => { - if (ruleId) { - const existing = await hue.rules.get(ruleId); + if (rule) { + let existing = null; + + try { + existing = await hue.rules.get(rule); + } catch (err) { + // Not found is fine + expect(err.getHueErrorType()).to.equal(3); + } + if (existing) { try { - await hue.rules.deleteRule(ruleId); + await hue.rules.deleteRule(rule); } catch (err) { - console.error(`Failed to delete rule: ${ruleId}`); + console.error(`Failed to delete rule: ${rule.id}`); } } } }); - it('should delete a rule', async () => { - const result = await hue.rules.deleteRule(ruleId); + it('should delete a rule using id', async () => { + const result = await hue.rules.deleteRule(rule.id); expect(result).to.have.be.true; - ruleId = null; }); + + it('should delete a rule using Rule object', async () => { + const result = await hue.rules.deleteRule(rule); + expect(result).to.have.be.true; + }); + + it('should error if a rule is not found', async () => { + const allRules = await hue.rules.getAll(); + const invalidRuleId = getNextRuleId(allRules); + + try { + await hue.rules.deleteRule(invalidRuleId); + expect.fail('should not get here') + } catch (err) { + expect(err.getHueErrorType()).to.equal(3); + expect(err.message).to.contain('not available'); + } + }) }); @@ -191,8 +237,8 @@ describe('Hue API #rules', () => { const newRule = model.createRule(); newRule.name = 'Test Rule to be deleted'; newRule.recycle = true; - newRule.addCondition(model.ruleConditions.sensor(testOpenCloseSensor).when('open').equals(true)); - newRule.addAction(model.ruleActions.light(testValues.hueLightId).withState(new LightState().on())); + newRule.addCondition(model.ruleConditions.sensor(testSensor).when('open').equals(true)); + newRule.addAction(model.actions.light(targetLight).withState(new LightState().on())); const result = await hue.rules.createRule(newRule); ruleId = result.id; @@ -227,7 +273,7 @@ describe('Hue API #rules', () => { it('should update conditions', async () => { rule.resetConditions(); - rule.addCondition(model.ruleConditions.sensor(testOpenCloseSensor).when('open').equals(false)); + rule.addCondition(model.ruleConditions.sensor(testSensor).when('open').equals(false)); const result = await hue.rules.updateRule(rule); expect(result).to.have.property('name').to.be.true; @@ -243,29 +289,52 @@ describe('Hue API #rules', () => { }); - it('should add an action', async () => { - rule.resetActions(); - rule.addAction(model.ruleActions.light(0).withState(new LightState().on(false))); + describe('adding actions', () => { - const result = await hue.rules.updateRule(rule); - expect(result).to.have.property('name').to.be.true; - expect(result).to.have.property('actions').to.be.true; - expect(result).to.have.property('conditions').to.be.true; + it('should add a LightStateAction', async () => { + rule.resetActions(); + rule.addAction(model.actions.light(targetLight).withState(new LightState().on(false))); - const updatedRule = await hue.rules.get(ruleId); - expect(updatedRule.actions).to.have.length(1); + const result = await hue.rules.updateRule(rule); + expect(result).to.have.property('name').to.be.true; + expect(result).to.have.property('actions').to.be.true; + expect(result).to.have.property('conditions').to.be.true; + + const updatedRule = await hue.rules.get(ruleId); + expect(updatedRule.actions).to.have.length(1); - const action = updatedRule.actions[0]; - expect(action).to.have.property('address').to.equal('/lights/0/state'); - expect(action).to.have.property('method').to.equal('PUT'); - expect(action).to.have.property('body').to.deep.equals({on: false}); + const action = updatedRule.actions[0]; + expect(action).to.have.property('address').to.equal(`/lights/${targetLight.id}/state`); + expect(action).to.have.property('method').to.equal('PUT'); + expect(action).to.have.property('body').to.deep.equals({on: false}); + }); + + it('should add a GroupStateAction', async () => { + rule.resetActions(); + rule.addAction(model.actions.group(targetGroup).withState(new GroupState().on(false))); + + const result = await hue.rules.updateRule(rule); + expect(result).to.have.property('name').to.be.true; + expect(result).to.have.property('actions').to.be.true; + expect(result).to.have.property('conditions').to.be.true; + + const updatedRule = await hue.rules.get(ruleId); + expect(updatedRule.actions).to.have.length(1); + + const action = updatedRule.actions[0]; + expect(action).to.have.property('address').to.equal(`/groups/${targetGroup.id}/action`); + expect(action).to.have.property('method').to.equal('PUT'); + expect(action).to.have.property('body').to.deep.equals({on: false}); + }); + + //TODO expand to cover all other actions }); it('should produce error if no conditions set', async () => { try { rule.resetConditions(); - hue.rules.updateRule(rule); + await hue.rules.updateRule(rule); expect.fail('Should have produced an error'); } catch (err) { expect(err).to.have.property('message').to.contain('at least one condition'); @@ -275,11 +344,29 @@ describe('Hue API #rules', () => { it('should produce error if no actions set', async () => { try { rule.resetActions(); - hue.rules.updateRule(rule); + await hue.rules.updateRule(rule); expect.fail('Should have produced an error'); } catch (err) { expect(err).to.have.property('message').to.contain('at least one action'); } }); }); -}); \ No newline at end of file +}); + + +function getNextRuleId(allRules) { + const ids = allRules.map(rule => rule.id); + + let id = 1 + , nextId = null + ; + while (!nextId) { + id++; + + if (ids.indexOf(`${id}`) === -1) { + nextId = id; + } + } + + return nextId; +} \ No newline at end of file diff --git a/lib/api/Scenes.js b/lib/api/Scenes.js index e2d8cca..1852c31 100644 --- a/lib/api/Scenes.js +++ b/lib/api/Scenes.js @@ -3,6 +3,8 @@ const scenesApi = require('./http/endpoints/scenes') , ApiDefinition = require('./http/ApiDefinition') , GroupLightState = require('../model/lightstate/GroupState') + , model = require('../model') + , util = require('../util') ; module.exports = class Scenes extends ApiDefinition { @@ -11,46 +13,110 @@ module.exports = class Scenes extends ApiDefinition { super(hueApi); } + /** + * @returns {Promise} + */ getAll() { return this.execute(scenesApi.getAll); } + /** + * @deprecated since 4.x use getScene(id) instead. + */ + get(id) { + util.deprecatedFunction('5.x', 'scenes.get(id)', 'Use scenes.getScene(id) instead.'); + return this.getScene(id); + } + + /** + * @param id {string | Scene} + * @returns {Promise} + */ + getScene(id) { + return this.execute(scenesApi.getScene, {id: id}); + } + + /** + * @deprecated since 4.x use getSceneByName(name) instead. + */ getByName(name) { + util.deprecatedFunction('5.x', 'scenes.getByName(name)', 'Use scenes.getSceneByName(name) instead.'); + return this.getSceneByName(name); + } + + /** + * Obtains the scenes that have the specified name from the bridge. + * @param name {string} + * @returns {Promise} + */ + getSceneByName(name) { return this.getAll().then(allScenes => { return allScenes.filter(scene => scene.name === name); }); } + /** + * @param scene {Scene} + * @returns {Promise} + */ createScene(scene) { const self = this; return this.execute(scenesApi.createScene, {scene: scene}) .then(data => { - return self.get(data.id); + return self.getScene(data.id); }); } - get(id) { - return this.execute(scenesApi.getScene, {id: id}); - } - + /** + * @deprecated since 4.x use updateScene(scene) instead. + */ update(id, scene) { + util.deprecatedFunction('5.x', 'scenes.update(id, scene)', 'Use scenes.updateScene(scene) instead.'); return this.execute(scenesApi.updateScene, {id: id, scene: scene}); } + /** + * @param scene {Scene} + * @returns {Promise>} + */ + updateScene(scene) { + return this.execute(scenesApi.updateScene, {id: scene, scene: scene}); + } + + /** + * Updates the light state for a specific light in the scene + * @param id {string | Scene} + * @param lightId {int | Light} + * @param sceneLightState {SceneLightState} + * @returns {Promise} + */ updateLightState(id, lightId, sceneLightState) { return this.execute(scenesApi.updateSceneLightState, {id: id, lightStateId: lightId, lightState: sceneLightState}); } + /** + * @param id {string | Scene} + * @returns {Promise} + */ deleteScene(id) { return this.execute(scenesApi.deleteScene, {id: id}); } + /** + * @param id {string | Scene} + * @returns {Promsie} + */ activateScene(id) { // Scene activation is done as an intersection of setting a group light state to a scene id, the intersection of the // scene light ids and that of the group is the lights that are targeted to change. - // + + let sceneId = id; + if (model.isSceneInstance(id)) { + sceneId = id.id; + } + // We target the all lights group here, so that all the lights in the scene are targeted. - return this.hueApi.groups.setGroupState(0, new GroupLightState().scene(id)); + return this.hueApi.groups.setGroupState(0, new GroupLightState().scene(sceneId)); } }; \ No newline at end of file diff --git a/lib/api/Scenes.test.js b/lib/api/Scenes.test.js index 6a1d0b9..8ad5509 100644 --- a/lib/api/Scenes.test.js +++ b/lib/api/Scenes.test.js @@ -93,49 +93,36 @@ describe('Hue API #scenes', () => { }); - // describe('cleanup', () => { - // - // it('should remove', async () => { - // const all = await hue.scenes.getAll() - // , toRemove = [] - // ; - // - // all.forEach(scene => { - // // if (!scene.locked && scene.name === 'node-hue-scene-with-transition') { - // if (!scene.locked) { - // toRemove.push(scene); - // } - // }); - // - // console.log(JSON.stringify(toRemove, null, 2)); - // - // // const promises = []; - // // toRemove.forEach(scene => { - // // promises.push(hue.scenes.deleteScene(scene.id).then(result => { - // // console.log(`Deleted: ${scene.id}: ${result}`); - // // })); - // // }); - // // - // // await Promise.all(promises); - // }); - // }); - - describe('#deleteScene()', () => { - let id; + let scene; beforeEach(async () => { - const scene = model.createLightScene(); - scene.name = 'test-delete'; - scene.lights = [2]; + const newScene = model.createLightScene(); + newScene.name = 'test-delete'; + newScene.lights = [2]; + + scene = await hue.scenes.createScene(newScene); + }); + + afterEach(async () => { + if (scene) { + try { + const existing = await hue.scenes.get(scene); + await hue.scenes.deleteScene(existing); + } catch (err) { + // Do nothing + } + } + }); - const result = await hue.scenes.createScene(scene); - id = result.id; + it('should remove a scene using id', async () => { + const result = await hue.scenes.deleteScene(scene.id); + expect(result).to.be.true; }); - it('should remove a scene', async () => { - const result = await hue.scenes.deleteScene(id); + it('should remove a scene using Scene object', async () => { + const result = await hue.scenes.deleteScene(scene); expect(result).to.be.true; }); }); @@ -162,7 +149,7 @@ describe('Hue API #scenes', () => { }); - describe('#updateScene()', () => { + describe('#update()', () => { it('should update an existing scene name', async () => { const updatedName = 'testing-update-name'; @@ -177,6 +164,21 @@ describe('Hue API #scenes', () => { }); }); + describe('#updateScene()', () => { + + it('should update an existing scene name', async () => { + const updatedName = 'testing-update-name'; + scene.name = updatedName; + + const result = await hue.scenes.updateScene(scene); + expect(result).to.have.property('name').to.be.true; + + const updatedScene = await hue.scenes.get(scene.id); + expect(updatedScene).to.have.property('id').to.equal(scene.id); + expect(updatedScene).to.have.property('name').to.equal(updatedName); + }); + }); + describe('#updateLightState()', () => { @@ -211,10 +213,15 @@ describe('Hue API #scenes', () => { }); - it('should activate an existing scene', async () => { + it('should activate an existing scene using id', async () => { const result = await hue.scenes.activateScene(scene.id); expect(result).to.be.true; }); + it('should activate an existing scene using scene', async () => { + const result = await hue.scenes.activateScene(scene); + expect(result).to.be.true; + }); + }); }); \ No newline at end of file diff --git a/lib/api/Schedules.js b/lib/api/Schedules.js index a74f56d..dc6e2f1 100644 --- a/lib/api/Schedules.js +++ b/lib/api/Schedules.js @@ -2,6 +2,7 @@ const schedulesApi = require('./http/endpoints/schedules') , ApiDefinition = require('./http/ApiDefinition') + , util = require('../util') ; module.exports = class Schedules extends ApiDefinition { @@ -10,25 +11,84 @@ module.exports = class Schedules extends ApiDefinition { super(hueApi); } + /** + * Gets all the Schedules from the bridge. + * @returns {Promise>} A Promise that will resolve to an Array of Schedules. + */ getAll() { return this.execute(schedulesApi.getAll); } - createSchedule(schedule) { - //TODO convert to schedule if possible here - return this.execute(schedulesApi.createSchedule, {schedule: schedule}); - //TODO return the actual Schedule model object using a get + /** + * @deprecated Use getSchedule(id) instead + * @param id {int | Schedule} + * @returns {Promise} + */ + get(id) { + util.deprecatedFunction('5.x', 'schedules.get(id)', 'Use schedules.getSchedule(id) instead.'); + return this.getSchedule(id); } - get(id) { + /** + * Gets a specific Schedule from the Bridge. + * @param id {int | Schedule} The id or Schedule instance to retrieve from the bridge. + * @returns {Promise} A Promise that will resolve to the actual schedule instance. + */ + getSchedule(id) { return this.execute(schedulesApi.getScheduleAttributes, {id: id}); } - update(id, schedule) { - //TODO convert to schedule if possible here - return this.execute(schedulesApi.setScheduleAttributes, {id: id, schedule: schedule}); + /** + * + * @param name + * @returns {Promise} + */ + getScheduleByName(name) { + return this.getAll() + .then(schedules => { + return schedules.filter(schedule => schedule.name === name); + }); + } + + /** + * Creates a new Schedule on the bridge. + * @param schedule {Schedule} The instance to create on the bridge. + * @returns {PromiseLike | Promise} The resultant Schedule instance that was created. + */ + createSchedule(schedule) { + const self = this; + + return self.execute(schedulesApi.createSchedule, {schedule: schedule}) + .then(result => { + return self.getSchedule(result.id); + }); } + /** + * Updates a Schedule. + * @param schedule {Schedule} The schedule with updated attributes to be saved to the Bridge. + * @returns {Promise} A promise that will resolve to an Object of the keys that were the attrubutes updated + * and a Boolean value that indicates if it was updated. + */ + updateSchedule(schedule) { + return this.execute(schedulesApi.setScheduleAttributes, {id: schedule, schedule: schedule}); + } + + // /** + // * @deprecated Use udpateSchedule(schedule) instead. + // * @param id + // * @param schedule + // * @returns {Promise} + // */ + // update(id, schedule) { + // return this.execute(schedulesApi.setScheduleAttributes, {id: id, schedule: schedule}); + // } + + /** + * Deletes an existing Schedule. + * @param id {int | Schedule} The id or Schedule instance to delete. + * @returns {Promise} A Promise that will resolve to a boolean indicating success. + */ deleteSchedule(id) { return this.execute(schedulesApi.deleteSchedule, {id: id}); } diff --git a/lib/api/Schedules.test.js b/lib/api/Schedules.test.js new file mode 100644 index 0000000..a33a531 --- /dev/null +++ b/lib/api/Schedules.test.js @@ -0,0 +1,390 @@ +'use strict'; + +const expect = require('chai').expect + , v3Api = require('../v3').api + , discovery = require('../v3').discovery + , model = require('../model') + , testValues = require('../../test/support/testValues.js') + , ApiError = require('../ApiError') +; + +describe('Hue API #schedule', () => { + + let hue; + + before(() => { + return discovery.nupnpSearch() + .then(searchResults => { + const localApi = v3Api.createLocal(searchResults[0].ipaddress); + return localApi.connect(testValues.username) + .then(api => { + hue = api; + }); + }); + }); + + + describe('#getAll()', () => { + + it('should find some', async () => { + const results = await hue.schedules.getAll(); + + expect(results).to.be.instanceOf(Array); + expect(results).to.have.length.to.be.at.least(1); + + const schedule = results[0]; + expect(model.isScheduleInstance(schedule)).to.be.true; + }); + }); + + + describe('#get()', () => { + + let targetSchedule; + + before(async () => { + const schedules = await hue.schedules.getAll(); + targetSchedule = schedules[0]; + }); + + describe('using id', () => { + + it('should get a specific schedule', async () => { + const schedule = await hue.schedules.get(targetSchedule.id); + + expect(model.isScheduleInstance(schedule)).to.be.true; + expect(schedule).to.have.property('id').to.equal(targetSchedule.id); + expect(schedule).to.have.property('description').to.equal(targetSchedule.description); + expect(schedule).to.have.property('localtime').to.equal(targetSchedule.localtime); + expect(schedule).to.have.property('status').to.equal(targetSchedule.status); + expect(schedule).to.have.property('recycle').to.equal(targetSchedule.recycle); + }); + + it('should fail for invalid schedule id', async () => { + try { + await hue.schedules.get('65535'); + expect.fail('should not get here'); + } catch (err) { + expect(err).to.be.instanceof(ApiError); + expect(err.getHueErrorType()).to.equal(3); + expect(err.message).to.contain('not available'); + } + }); + + }); + + + describe('using Schedule Object', () => { + + it('should get a specific schedule', async () => { + const schedule = await hue.schedules.get(targetSchedule); + + expect(model.isScheduleInstance(schedule)).to.be.true; + expect(schedule).to.have.property('id').to.equal(targetSchedule.id); + expect(schedule).to.have.property('description').to.equal(targetSchedule.description); + expect(schedule).to.have.property('localtime').to.equal(targetSchedule.localtime); + expect(schedule).to.have.property('status').to.equal(targetSchedule.status); + expect(schedule).to.have.property('recycle').to.equal(targetSchedule.recycle); + }); + }); + }); + + + describe('#createSchedule()', function () { + + // We need a longer wait time here as we have to pause for the schedules to trigger and remove themselves from the bridge + this.timeout(30 * 1000); + + let created; + + beforeEach(() => { + created = null; + }); + + afterEach(async () => { + if (created) { + try { + await hue.schedules.deleteSchedule(created); + } catch (err) { + console.log(`Failed to delete created schedule: ${created.id}, ${err.message}`); + } + } + }); + + + it('should create a schedule', async () => { + const schedule = model.createSchedule(); + schedule.name = 'Test Schedule Recurring'; + schedule.description = 'A node-hue-api test schedule that can be removed'; + schedule.localtime = model.timePatterns.createRecurringTime('W124/T12:00:00'); + schedule.recycle = true; + schedule.command = model.actions.light(0).withState({on: true}); + + const result = await hue.schedules.createSchedule(schedule); + created = result; + + expect(result).to.have.property('name').to.equal(schedule.name); + expect(result).to.have.property('description').to.equal(schedule.description); + + expect(result).to.have.property('command'); + expect(result.command).to.have.property('method').to.equal('PUT'); + expect(result.command).to.have.property('address').to.contain('/lights/0/state'); + expect(result.command).to.have.property('body').to.deep.equal({on: true}); + }); + + //TODO should try all the different time patterns for schedules + + + it('should create a schedule that will autodelete', async () => { + const schedule = model.createSchedule(); + + schedule.name = 'Test Schedule AutoDelete'; + schedule.description = 'A node-hue-api test schedule should autodelete itself'; + schedule.localtime = model.timePatterns.createTimer().seconds(2); + schedule.recycle = true; + schedule.autodelete = true; + schedule.command = model.actions.light(40).withState(new model.lightStates.LightState().alertShort()); + + const result = await hue.schedules.createSchedule(schedule); + expect(result).to.have.property('name').to.equal(schedule.name); + expect(result).to.have.property('autodelete').to.be.true; + + // Verify it has auto deleted + await waitFor(8 * 1000); + try { + await hue.schedules.getSchedule(result); + expect.fail('should have auto deleted from schedules'); + } catch (err) { + console.error(err); + expect(err.getHueErrorType()).to.equal(3); + } + }); + + + it('should create a schedule with a repeat timer that repeats 2 times', async () => { + const schedule = model.createSchedule(); + + schedule.name = 'Reoccurring Schedule from Timer'; + schedule.description = 'A node-hue-api test schedule should autodelete itself'; + schedule.localtime = model.timePatterns.createRecurringTimer().seconds(5).reoccurs(2); + schedule.recycle = true; + schedule.command = model.actions.light(40).withState(new model.lightStates.LightState().alertShort()); + + const result = await hue.schedules.createSchedule(schedule); + + await waitFor(15 * 1000); + try { + await hue.schedules.get(result); + expect.fail('should have auto deleted from schedules'); + } catch (err) { + expect(err.getHueErrorType()).to.equal(3); + } + }); + + + it('should create a schedule with a repeats forever', async () => { + const schedule = model.createSchedule(); + + schedule.name = 'Reoccurring Schedule from Timer'; + schedule.description = 'A node-hue-api test schedule should autodelete itself'; + schedule.localtime = model.timePatterns.createRecurringTimer().seconds(3); + schedule.recycle = true; + schedule.command = model.actions.light(40).withState(new model.lightStates.LightState().alertShort()); + + const result = await hue.schedules.createSchedule(schedule); + created = result; + + // Let the schedule run at least three times + await waitFor(12 * 1000); + + // Stop it as it is annoying + const runningSchedule = await hue.schedules.get(result); + runningSchedule.status = 'disabled'; + await hue.schedules.updateSchedule(runningSchedule); + }); + + + }); + + describe('#updateSchedule()', () => { + + let schedule; + + beforeEach(async () => { + const createSchedule = model.createSchedule(); + createSchedule.name = 'Test Schedule For Updates'; + createSchedule.description = 'A node-hue-api test schedule that can be removed'; + createSchedule.localtime = model.timePatterns.createAbsoluteTime(new Date(Date.now() + (1000 * 60 * 60))); + createSchedule.recycle = true; + createSchedule.command = model.actions.light(0).withState({on: true}); + + schedule = await hue.schedules.createSchedule(createSchedule); + }); + + afterEach(async () => { + if (schedule) { + try { + await hue.schedules.deleteSchedule(schedule); + } catch (err) { + console.log(`Failed to delete created schedule: ${schedule.id}, ${err.message}`); + } + } + }); + + + it('should update the name', async () => { + const newName = 'Updated Schedule Name'; + schedule.name = newName; + + const result = await hue.schedules.updateSchedule(schedule) + , updatedSchedule = await hue.schedules.get(schedule) + ; + + expect(result).to.have.property('name').to.be.true; + expect(updatedSchedule).to.have.property('name').to.equal(newName); + }); + + + it('should update the description', async () => { + const original = schedule.getHuePayload() + , newDescription = original.description + Date.now(); + + schedule.description = newDescription; + + const result = await hue.schedules.updateSchedule(schedule) + , updatedSchedule = await hue.schedules.get(schedule) + ; + + expect(result).to.have.property('description').to.be.true; + expect(updatedSchedule).to.have.property('description').to.equal(newDescription); + }); + + + it('should update the command', async () => { + const newCommand = model.actions.light(1).withState(new model.lightStates.LightState().off()); + schedule.command = newCommand; + + const result = await hue.schedules.updateSchedule(schedule) + , updatedSchedule = await hue.schedules.get(schedule) + ; + + expect(result).to.have.property('command').to.be.true; + expect(updatedSchedule).to.have.property('command').to.have.property('address').to.contain('/lights/1'); + expect(updatedSchedule).to.have.property('command').to.have.property('body').to.deep.equal({on: false}); + }); + + it('should update the localtime', async () => { + const newTime = model.timePatterns.createAbsoluteTime(new Date(Date.now() + (10 * 1000 * 60 * 60))); + schedule.localtime = newTime; + + const result = await hue.schedules.updateSchedule(schedule) + , updatedSchedule = await hue.schedules.get(schedule) + ; + + expect(result).to.have.property('localtime').to.be.true; + expect(updatedSchedule).to.have.property('localtime').to.equal(newTime.toString()); + }); + + it('should update the status', async () => { + expect(schedule.status).to.equal('enabled'); + schedule.status = 'disabled'; + + const result = await hue.schedules.updateSchedule(schedule) + , updatedSchedule = await hue.schedules.get(schedule) + ; + + expect(result).to.have.property('status').to.be.true; + expect(updatedSchedule).to.have.property('status').to.equal('disabled'); + }); + + it('should update autodelete', async () => { + expect(schedule).to.have.property('autodelete').to.be.true; + schedule.autodelete = false; + + const result = await hue.schedules.updateSchedule(schedule) + , updatedSchedule = await hue.schedules.get(schedule) + ; + + expect(result).to.have.property('autodelete').to.be.true; + expect(updatedSchedule).to.have.property('autodelete').to.be.false; + }); + }); + + + describe('#deleteSchedule()', () => { + + let schedule; + + beforeEach(async () => { + const createSchedule = model.createSchedule(); + createSchedule.name = 'Test Schedule For Deletes'; + createSchedule.description = 'A node-hue-api test schedule that can be removed'; + createSchedule.localtime = model.timePatterns.createAbsoluteTime(new Date(Date.now() + (1000 * 60 * 60))); + createSchedule.recycle = true; + createSchedule.command = model.actions.light(0).withState({on: false}); + + schedule = await hue.schedules.createSchedule(createSchedule); + }); + + afterEach(async () => { + if (schedule) { + try { + const exists = await hue.schedules.get(schedule); + await hue.schedules.deleteSchedule(exists); + } catch (err) { + if (err.getHueErrorType() !== 3) { + console.log(`Failed to delete created schedule: ${schedule.id}, ${err.message}`); + } + } + } + }); + + + it('should delete an existing schedule', async () => { + const result = await hue.schedules.deleteSchedule(schedule); + expect(result).to.be.true; + }); + + it('should error when deleting a schedule that does not exist', async () => { + const allSchedules = await hue.schedules.getAll() + , nextId = getNextScheduleId(allSchedules) + ; + + try { + await hue.schedules.deleteSchedule(nextId); + expect.fail('Should not get here'); + } catch (err) { + expect(err.message).to.contain('not available'); + expect(err.getHueErrorType()).to.equal(3); + } + }); + }); +}); + + +//TODO make part of utils +function waitFor(milliseconds) { + return new Promise(resolve => { + setTimeout(() => { + resolve(true) + }, milliseconds); + }) +} + +// Common with Rules tests +function getNextScheduleId(allSchedules) { + const ids = allSchedules.map(schedule => schedule.id); + + let id = 1 + , nextId = null + ; + while (!nextId) { + id++; + + if (ids.indexOf(id) === -1) { + nextId = id; + } + } + + return nextId; +} \ No newline at end of file diff --git a/lib/api/Sensors.js b/lib/api/Sensors.js index f26f74c..a9b9e4c 100644 --- a/lib/api/Sensors.js +++ b/lib/api/Sensors.js @@ -1,8 +1,8 @@ 'use strict'; const sensorsApi = require('./http/endpoints/sensors') - , Sensor = require('../model/sensors/Sensor.js') , ApiDefinition = require('./http/ApiDefinition.js') + , util = require('../util') ; @@ -12,26 +12,72 @@ module.exports = class Sensors extends ApiDefinition { super(hueApi); } + /** + * Gets all the sesnors from the bridge + * @returns {Promise} + */ getAll() { return this.execute(sensorsApi.getAllSensors); } + /** + * @deprecated use getSensor(id) instead + * @param id {string | Sensor} + * @returns {Promise} + */ get(id) { + util.deprecatedFunction('5.x', 'sensors..get(id)', 'Use sensors.getSensor(id) instead.'); + return this.getSensor(id); + } + + /** + * @param id {string | Sensor} + * @returns {Promise} + */ + getSensor(id) { return this.execute(sensorsApi.getSensor, {id: id}); } + /** + * Starts a search for new ZigBee sensors + * @returns {Promise} + */ searchForNew() { return this.execute(sensorsApi.findNewSensors); } + /** + * Obtains the new sesnors that were found from the previous search for new sensors + * @returns {Promise}. + */ getNew() { return this.execute(sensorsApi.getNewSensors); } + /** + * Will update the name attribute of the Sensor on the Bridge. + * @param sensor { Sensor } The Sensor with the update to the name applied + * @returns {Promise} + */ + renameSensor(sensor) { + return this.execute(sensorsApi.updateSensor, {id: sensor, name: sensor.name}); + } + + /** + * @deprecated use renameSensor(sensor) instead + * @param id {String | Sensor} The id or the Sensor instance to update + * @returns {Promise} + */ updateName(id, name) { + util.deprecatedFunction('5.x', 'sensors.updateName(id, name)', 'Use sensors.rename(sensor) instead.'); return this.execute(sensorsApi.updateSensor, {id: id, name: name}); } + /** + * Creates a new Sensor (CLIP based) + * @param sensor {Sensor} The CLIP Sensor that you wish to create. + * @returns {Promise} + */ createSensor(sensor) { const self = this; @@ -41,18 +87,30 @@ module.exports = class Sensors extends ApiDefinition { }); } + /** + * Deletes a sensor from the Bridge + * @param id {string | Sensor} The id or Sensor instance to remove from the bridge + * @returns {Promise} + */ deleteSensor(id) { - let sensorId = id; - if (id instanceof Sensor) { - sensorId = id.id; - } - return this.execute(sensorsApi.deleteSensor, {id: sensorId}); + return this.execute(sensorsApi.deleteSensor, {id: id}); } + /** + * Will update the configuration attributes of the Sensor in the bridge. + * @param sensor {Sensor} + * @returns {Promise} + */ updateSensorConfig(sensor) { return this.execute(sensorsApi.changeSensorConfig, {id: sensor.id, sensor: sensor}); } + /** + * Will update the state attributes of the Sensor in the bridge. + * @param sensor {Sensor} + * @param limitToStateNames {String[]} optional list of state attributes to limit the update to (should not be needed in practice, was added to get around a bug). + * @returns {Promise} + */ updateSensorState(sensor, limitToStateNames) { return this.execute(sensorsApi.changeSensorState, {id: sensor.id, sensor: sensor, filterStateNames: limitToStateNames}); } diff --git a/lib/api/Sensors.test.js b/lib/api/Sensors.test.js index 2caea9d..6b32031 100644 --- a/lib/api/Sensors.test.js +++ b/lib/api/Sensors.test.js @@ -149,6 +149,34 @@ describe('Hue API #sensors', () => { }); + describe('#rename()', () => { + + let sensor; + + beforeEach('', async () => { + const newSensor = createClipOpenCloseSensor('updateNameTest'); + sensor = await hue.sensors.createSensor(newSensor); + }); + + afterEach('', async () => { + if (sensor) { + await hue.sensors.deleteSensor(sensor); + } + }); + + it('should update the name on an existing sensor', async () => { + sensor.name = `newName-${Date.now()}`; + + const result = await hue.sensors.renameSensor(sensor) + , updatedSensor = await hue.sensors.get(sensor) + ; + + expect(result).to.be.true; + expect(updatedSensor).to.have.property('name').to.equal(sensor.name); + }); + }); + + describe('#updateSensorConfig', () => { let sensorId; diff --git a/lib/api/Users.js b/lib/api/Users.js index 88dd0df..35d9540 100644 --- a/lib/api/Users.js +++ b/lib/api/Users.js @@ -2,6 +2,7 @@ const configurationApi = require('./http/endpoints/configuration') , ApiDefinition = require('./http/ApiDefinition.js') + , util = require('../util') ; @@ -15,7 +16,7 @@ module.exports = class Users extends ApiDefinition { return getAllUsersAsArray(this); } - get(username) { + getUser(username) { return getAllUsers(this) .then(users => { let result = null; @@ -28,7 +29,16 @@ module.exports = class Users extends ApiDefinition { }); } - getByName(appName, deviceName) { + /** + * @deprecated Use getUserByName(username) instead + * @param username {string} + */ + get(username) { + util.deprecatedFunction('5.x', 'users.get(username)', 'Use users.getUser(username) instead.'); + return this.getUser(username); + } + + getUserByName(appName, deviceName) { let nameToMatch; if (arguments.length === 0) { @@ -45,6 +55,23 @@ module.exports = class Users extends ApiDefinition { }); } + /** + * @deprecated use getUserByName(appName, deviceName) instead. + * @param appName {string} + * @param deviceName {string} + * @returns {Promise} + */ + getByName(appName, deviceName) { + util.deprecatedFunction('5.x', 'scenes.get(id)', 'Use scenes.getScene(id) instead.'); + return this.getUserByName(appName, deviceName); + } + + /** + * + * @param appName {string} + * @param deviceName {string} + * @returns {Promise>} + */ createUser(appName, deviceName) { return this.execute(configurationApi.createUser, {appName: appName, deviceName: deviceName, generateKey: true}); } @@ -52,10 +79,6 @@ module.exports = class Users extends ApiDefinition { deleteUser(username) { return this.execute(configurationApi.deleteUser, {element: username}); } - - // pressLinkButton() { - // return this.execute(configurationApi.updateConfiguration, {linkbutton: true}); - // } }; diff --git a/lib/api/Users.test.js b/lib/api/Users.test.js index b443081..99bbde8 100644 --- a/lib/api/Users.test.js +++ b/lib/api/Users.test.js @@ -117,7 +117,7 @@ describe('Hue API #users', () => { it('should get a list of user accounts for valid name', async () => { const username = 'Echo' - , users = await authenticatedHue.users.getByName(username) + , users = await authenticatedHue.users.getUserByName(username) ; expect(users).to.be.instanceof(Array); diff --git a/lib/api/http/endpoints/capabilities.js b/lib/api/http/endpoints/capabilities.js index 1e4c0c1..f15fdea 100644 --- a/lib/api/http/endpoints/capabilities.js +++ b/lib/api/http/endpoints/capabilities.js @@ -1,6 +1,7 @@ 'use strict'; const ApiEndpoint = require('./endpoint') + , model = require('../../../model') ; function getAllCapabilities() { @@ -8,9 +9,16 @@ function getAllCapabilities() { .get() .acceptJson() .uri('//capabilities') - .pureJson(); + .pureJson() + .postProcess(buildCapabilities) + ; } module.exports = { getAll: getAllCapabilities() -}; \ No newline at end of file +}; + +function buildCapabilities(data, requestParameters) { + // const id = requestParameters.baseUrl || null; + return model.createFromBridge('capabilities', null, data); +} \ No newline at end of file diff --git a/lib/api/http/endpoints/configuration.js b/lib/api/http/endpoints/configuration.js index 733d571..05b0bdc 100644 --- a/lib/api/http/endpoints/configuration.js +++ b/lib/api/http/endpoints/configuration.js @@ -1,7 +1,7 @@ 'use strict'; const ApiEndpoint = require('./endpoint') - , UsernamePlaceholder = require('../placeholders/UsernamePlaceholder') + , UsernamePlaceholder = require('../../../placeholders/UsernamePlaceholder') , util = require('../../../util') , ApiError = require('../../../ApiError') ; diff --git a/lib/api/http/endpoints/endpoint.js b/lib/api/http/endpoints/endpoint.js index 311c542..c8ab390 100644 --- a/lib/api/http/endpoints/endpoint.js +++ b/lib/api/http/endpoints/endpoint.js @@ -1,9 +1,9 @@ 'use strict'; -const UsernamePlaceholder = require('../placeholders/UsernamePlaceholder') +const UsernamePlaceholder = require('../../../placeholders/UsernamePlaceholder') ; -const debug = /node-hue-api/.test(process.env['NODE_DEBUG']); +const DEBUG = /node-hue-api/.test(process.env['NODE_DEBUG']); class ApiEndpoint { @@ -109,8 +109,18 @@ class ApiEndpoint { } } - if (debug) { - console.log(JSON.stringify(data));//TODO redact auth passwords + if (DEBUG) { + if (data.placeholders) { + //TODO redact the username from logs, although it would still appear in the URL... + console.log('URL Placeholders:'); + data.placeholders.forEach(placeholder => { + console.log(` ${placeholder.toString()}`); + }); + } + + if (data.headers) { + console.log(`Headers: ${JSON.stringify(data.headers)}`); + } } if (data.statusCode) { diff --git a/lib/api/http/endpoints/groups.js b/lib/api/http/endpoints/groups.js index 93becb9..55c45f9 100644 --- a/lib/api/http/endpoints/groups.js +++ b/lib/api/http/endpoints/groups.js @@ -3,11 +3,13 @@ const ApiEndpoint = require('./endpoint') , ApiError = require('../../../ApiError') , util = require('../../../util') - , GroupIdPlaceholder = require('../placeholders/GroupIdPlaceholder') + , GroupIdPlaceholder = require('../../../placeholders/GroupIdPlaceholder') , model = require('../../../model') , GroupState = require('../../../model/lightstate/GroupState') ; +const GROUP_ID_PLACEHOLDER = new GroupIdPlaceholder(); + module.exports = { getAllGroups: new ApiEndpoint() @@ -28,7 +30,7 @@ module.exports = { getGroupAttributes: new ApiEndpoint() .get() .uri('//groups/') - .placeholder(new GroupIdPlaceholder()) + .placeholder(GROUP_ID_PLACEHOLDER) .acceptJson() .pureJson() .postProcess(buildGroup), @@ -36,7 +38,7 @@ module.exports = { setGroupAttributes: new ApiEndpoint() .put() .uri('//groups/') - .placeholder(new GroupIdPlaceholder()) + .placeholder(GROUP_ID_PLACEHOLDER) .acceptJson() .payload(buildGroupAttributeBody) .pureJson() @@ -45,7 +47,7 @@ module.exports = { setGroupState: new ApiEndpoint() .put() .uri('//groups//action') - .placeholder(new GroupIdPlaceholder()) + .placeholder(GROUP_ID_PLACEHOLDER) .payload(buildGroupStateBody) .pureJson() .postProcess(util.wasSuccessful), @@ -53,14 +55,14 @@ module.exports = { deleteGroup: new ApiEndpoint() .delete() .uri('//groups/') - .placeholder(new GroupIdPlaceholder()) + .placeholder(GROUP_ID_PLACEHOLDER) .pureJson() .postProcess(validateGroupDeletion), setStreaming: new ApiEndpoint() .put() .uri('//groups/') - .placeholder(new GroupIdPlaceholder()) + .placeholder(GROUP_ID_PLACEHOLDER) .payload(buildStreamBody) .pureJson() .postProcess(util.wasSuccessful), @@ -71,7 +73,11 @@ function buildGroupsResult(result) { const groups = []; Object.keys(result).forEach(groupId => { - const group = model.createFromBridge('group', groupId, result[groupId]); + const payload = result[groupId] + , type = payload.type.toLowerCase() + , group = model.createFromBridge(type, groupId, payload) + ; + groups.push(group); }); @@ -114,6 +120,10 @@ function buildGroupBody(parameters) { throw new ApiError('A group must be provided'); } + if (!model.isGroupInstance(group)) { + throw new ApiError('group parameter must be an instance of a Group'); + } + const result = { type: 'application/json', body: { @@ -122,14 +132,16 @@ function buildGroupBody(parameters) { } }; - const lights = util.asStringArray(group.lights); - if (lights) { - result.body.lights = lights; + if (group.lights) { + result.body.lights = group.lights; + } else if (group.type === 'Entertainment') { + // Entertainment requires a empty array to be passed in if no lights defined. + result.body.lights = []; } - if (group.type === 'Room' || group.type === 'Zone') { + if (group.class) { result.body.class = group.class; - } else if (group.type === 'LightGroup') { + } else { result.body.recycle = group.recycle; } @@ -138,22 +150,21 @@ function buildGroupBody(parameters) { function buildGroupAttributeBody(parameters) { const body = {} - , groupAttributes = parameters ? parameters.groupAttributes : null; + , group = parameters.group + ; - const group = model.createFromBridge('group', null, groupAttributes) - , payload = group.getHuePayload() - ; + if (!group) { + throw new ApiError('A group is required to update attributes') + } + + const payload = group.getHuePayload(); - ['name', 'class'].forEach(attr => { - if (groupAttributes[attr]) { - body[attr] = payload[attr]; + ['name', 'lights', 'class'].forEach(key => { + if (payload[key]) { + body[key] = payload[key]; } }); - if (payload.lights) { - body.lights = util.asStringArray(payload.lights); //TODO array of at least one element and must be an existing light otherwise error 7 returned - } - return { type: 'application/json', body: body @@ -175,7 +186,10 @@ function buildStreamBody(parameters) { function buildGroup(data, requestParameters) { - return model.createFromBridge('group', requestParameters.id, data); + const type = data.type.toLowerCase() + , id = GROUP_ID_PLACEHOLDER.getValue(requestParameters) + ; + return model.createFromBridge(type, id, data); } diff --git a/lib/api/http/endpoints/lights.js b/lib/api/http/endpoints/lights.js index e3fc4a4..763ab03 100644 --- a/lib/api/http/endpoints/lights.js +++ b/lib/api/http/endpoints/lights.js @@ -1,5 +1,7 @@ +'use strict'; + const ApiEndpoint = require('./endpoint') - , LightIdPlaceholder = require('../placeholders/LightIdPlaceholder') + , LightIdPlaceholder = require('../../../placeholders/LightIdPlaceholder') , LightState = require('../../../model/lightstate/LightState') , ApiError = require('../../../ApiError') , util = require('../../../util') @@ -7,7 +9,7 @@ const ApiEndpoint = require('./endpoint') , rgb = require('../../../rgb') ; -'use strict'; +const LIGHT_ID_PLACEHOLDER = new LightIdPlaceholder(); module.exports = { @@ -36,7 +38,7 @@ module.exports = { getLightAttributesAndState: new ApiEndpoint() .get() .uri('//lights/') - .placeholder(new LightIdPlaceholder()) + .placeholder(LIGHT_ID_PLACEHOLDER) .acceptJson() .pureJson() .postProcess(injectLightId), @@ -45,7 +47,7 @@ module.exports = { setLightAttributes: new ApiEndpoint() .put() .uri('//lights/') - .placeholder(new LightIdPlaceholder()) + .placeholder(LIGHT_ID_PLACEHOLDER) .acceptJson() .pureJson() .payload(buildLightNamePayload) @@ -54,7 +56,7 @@ module.exports = { setLightState: new ApiEndpoint() .put() .uri('//lights//state') - .placeholder(new LightIdPlaceholder()) + .placeholder(LIGHT_ID_PLACEHOLDER) .acceptJson() .pureJson() .payload(buildLightStateBody) @@ -63,7 +65,7 @@ module.exports = { deleteLight: new ApiEndpoint() .delete() .uri('//lights/') - .placeholder(new LightIdPlaceholder()) + .placeholder(LIGHT_ID_PLACEHOLDER) .acceptJson() .pureJson() }; @@ -82,8 +84,16 @@ function buildLightsResult(result) { } function buildLightNamePayload(parameters) { - // Set the name on a Light instance so that it can be validated using parameter constraints there - const light = model.createFromBridge('light', 0, parameters); + // To support deprecation in the API where we take (id, name) and now just a (light) payload, cater for it here and + // remove once lights.rename(id, name) is removed from API + + let light = null; + if (parameters.light) { + light = parameters.light; + } else { + // Set the name on a Light instance so that it can be validated using parameter constraints there + light = model.createFromBridge('light', 0, {name: parameters.name}); + } return { type: 'application/json', @@ -92,7 +102,8 @@ function buildLightNamePayload(parameters) { } function injectLightId(result, requestParameters) { - return Object.assign({id: requestParameters.id}, result); + const id = LIGHT_ID_PLACEHOLDER.getValue(requestParameters); + return Object.assign({id: id}, result); } function buildLightStateBody(parameters) { diff --git a/lib/api/http/endpoints/resourcelinks.js b/lib/api/http/endpoints/resourcelinks.js index 5c2074c..af6d46c 100644 --- a/lib/api/http/endpoints/resourcelinks.js +++ b/lib/api/http/endpoints/resourcelinks.js @@ -1,12 +1,14 @@ 'use strict'; const ApiEndpoint = require('./endpoint') - , ResourceLinkPlaceholder = require('../placeholders/ResourceLinkPlaceholder') + , ResourceLinkPlaceholder = require('../../../placeholders/ResourceLinkPlaceholder') , model = require('../../../model') , ApiError = require('../../../ApiError') , util = require('../../../util') ; +const RESOURCELINK_PLACEHOLDER = new ResourceLinkPlaceholder(); + module.exports = { getAll: new ApiEndpoint() @@ -19,7 +21,7 @@ module.exports = { getResourceLink: new ApiEndpoint() .get() .uri('//resourcelinks/') - .placeholder(new ResourceLinkPlaceholder()) + .placeholder(RESOURCELINK_PLACEHOLDER) .acceptJson() .pureJson() .postProcess(buildResourceLink), @@ -35,7 +37,7 @@ module.exports = { updateResourceLink: new ApiEndpoint() .put() .uri('//resourcelinks/') - .placeholder(new ResourceLinkPlaceholder()) + .placeholder(RESOURCELINK_PLACEHOLDER) .payload(buildResourceLinkUpdatePayload) .acceptJson() .pureJson() @@ -44,7 +46,7 @@ module.exports = { deleteResourceLink: new ApiEndpoint() .delete() .uri('//resourcelinks/') - .placeholder(new ResourceLinkPlaceholder()) + .placeholder(RESOURCELINK_PLACEHOLDER) .acceptJson() .pureJson() .postProcess(util.wasSuccessful) @@ -65,7 +67,8 @@ function buildResourceLinkResults(data) { function buildResourceLink(data, requestParameters) { if (data) { - return model.createFromBridge('resourcelink', requestParameters.id, data); + const id = RESOURCELINK_PLACEHOLDER.getValue(requestParameters); + return model.createFromBridge('resourcelink', id, data); } return null; } diff --git a/lib/api/http/endpoints/rules.js b/lib/api/http/endpoints/rules.js index ae7fc50..ae5b21e 100644 --- a/lib/api/http/endpoints/rules.js +++ b/lib/api/http/endpoints/rules.js @@ -1,12 +1,14 @@ 'use strict'; const ApiEndpoint = require('./endpoint') - , RuleIdPlaceholder = require('../placeholders/RuleIdPlaceholder') + , RuleIdPlaceholder = require('../../../placeholders/RuleIdPlaceholder') , model = require('../../../model') , ApiError = require('../../../ApiError') , util = require('../../../util') ; +const RULE_ID_PLACEHOLDER = new RuleIdPlaceholder(); + module.exports = { getAll: new ApiEndpoint() @@ -20,7 +22,7 @@ module.exports = { .get() .acceptJson() .uri('/rules/') - .placeholder(new RuleIdPlaceholder()) + .placeholder(RULE_ID_PLACEHOLDER) .pureJson() .postProcess(createGetRuleResult), @@ -36,7 +38,7 @@ module.exports = { .put() .acceptJson() .uri('//rules/') - .placeholder(new RuleIdPlaceholder()) + .placeholder(RULE_ID_PLACEHOLDER) .pureJson() .payload(createRuleUpdatePayload) .postProcess(util.extractUpdatedAttributes), @@ -45,7 +47,7 @@ module.exports = { .delete() .acceptJson() .uri('//rules/') - .placeholder(new RuleIdPlaceholder()) + .placeholder(RULE_ID_PLACEHOLDER) .pureJson() .postProcess(validateRuleDeletion), }; @@ -63,7 +65,8 @@ function createAllRulesResult(result) { } function createGetRuleResult(data, requestParameters) { - return model.createFromBridge('rule', requestParameters.id, data); + const id = RULE_ID_PLACEHOLDER.getValue(requestParameters); + return model.createFromBridge('rule', id, data); } function validateRuleDeletion(result) { diff --git a/lib/api/http/endpoints/scenes.js b/lib/api/http/endpoints/scenes.js index a9c900d..69c6d2e 100644 --- a/lib/api/http/endpoints/scenes.js +++ b/lib/api/http/endpoints/scenes.js @@ -1,8 +1,8 @@ 'use strict'; const ApiEndpoint = require('./endpoint') - , SceneIdPlaceholder = require('../placeholders/SceneIdPlaceholder') - , LightIdPlacehoder = require('../placeholders/LightIdPlaceholder') + , SceneIdPlaceholder = require('../../../placeholders/SceneIdPlaceholder') + , LightIdPlacehoder = require('../../../placeholders/LightIdPlaceholder') , model = require('../../../model') , SceneLightState = require('../../../model/lightstate/SceneLightState') , ApiError = require('../../../ApiError') @@ -70,7 +70,6 @@ function buildScenesResult(result) { const data = result[id] , type = data.type.toLowerCase() ; - console.log(JSON.stringify(data, null, 2)); const scene = model.createFromBridge(type, id, data); scenes.push(scene); diff --git a/lib/api/http/endpoints/schedules.js b/lib/api/http/endpoints/schedules.js index 421cecf..9b8419f 100644 --- a/lib/api/http/endpoints/schedules.js +++ b/lib/api/http/endpoints/schedules.js @@ -1,12 +1,13 @@ 'use strict'; const ApiEndpoint = require('./endpoint') - , ScheduleIdPlaceholder = require('../placeholders/ScheduleIdPlaceholder') + , ScheduleIdPlaceholder = require('../../../placeholders/ScheduleIdPlaceholder') , model = require('../../../model') , ApiError = require('../../../ApiError') , util = require('../../../util') ; +const SCHEDULE_ID_PLACEHOLDER = new ScheduleIdPlaceholder(); module.exports = { @@ -17,7 +18,6 @@ module.exports = { .pureJson() .postProcess(buildSchedulesResult), - //TODO createSchedule: new ApiEndpoint() .post() .acceptJson() @@ -26,34 +26,30 @@ module.exports = { .payload(buildSchedulePayload) .postProcess(buildCreateScheduleResult), - //TODO getScheduleAttributes: new ApiEndpoint() .get() .uri('//schedules/') - .placeholder(new ScheduleIdPlaceholder()) + .placeholder(SCHEDULE_ID_PLACEHOLDER) .acceptJson() .pureJson() .postProcess(buildSchedule), - //TODO setScheduleAttributes: new ApiEndpoint() .put() .uri('//schedules/') - .placeholder(new ScheduleIdPlaceholder()) + .placeholder(SCHEDULE_ID_PLACEHOLDER) .acceptJson() - .payload(buildScheduleAttributeBody) + .payload(buildUpdateSchedulePayload) .pureJson() - // .postProcess(utils.wasSuccessful), - .postProcess(extractUpdatedAttributes), + .postProcess(util.extractUpdatedAttributes), - //TODO deleteSchedule: new ApiEndpoint() .delete() .acceptJson() .uri('//schedules/') - .placeholder(new ScheduleIdPlaceholder()) + .placeholder(SCHEDULE_ID_PLACEHOLDER) .pureJson() - .postProcess(validateScheduleDeletion), + .postProcess(util.wasSuccessful), }; @@ -61,25 +57,15 @@ function buildSchedulesResult(result) { let schedules = []; Object.keys(result).forEach(function (id) { - schedules.push(new Schedule(result[id], id)); + schedules.push(model.createFromBridge('schedule', id, result[id])); }); return schedules; } function buildSchedule(data, requestParameters) { - if (requestParameters) { - return new Schedule(data, requestParameters.id); - } else { - return new Schedule(data); - } -} - -function validateScheduleDeletion(result) { - if (!util.wasSuccessful(result)) { - throw new ApiError(util.parseErrors(result).join(', ')); - } - return true; + const id = SCHEDULE_ID_PLACEHOLDER.getValue(requestParameters); + return model.createFromBridge('schedule', id, data); } function buildSchedulePayload(parameters) { @@ -87,28 +73,36 @@ function buildSchedulePayload(parameters) { if (!schedule) { throw new ApiError('Schedule to create must be provided'); - } else if (!model.isSceneInstance(schedule)) { + } else if (!model.isScheduleInstance(schedule)) { throw new ApiError('You must provide a valid instance of a Schedule to be created'); } + const payload = getSchedulePayload(parameters.username, schedule); + return { type: 'application/json', - body: schedule.payload + body: payload }; } -function buildScheduleAttributeBody(parameters) { - const body = {} - , updatedSchedule = parameters ? parameters.schedule : null; +function buildUpdateSchedulePayload(parameters) { + const schedule = parameters.schedule; - if (updatedSchedule) { - if (model.isScheduleInstance(updatedSchedule)) { - Object.assign(body, updatedSchedule.payload); - } else { - Object.assign(body, updatedSchedule);//TODO need to convert to schedule then validate - } + if (!schedule) { + throw new ApiError('Schedule to update must be provided'); + } else if (!model.isScheduleInstance(schedule)) { + throw new ApiError('You must provide a valid instance of a Schedule when updating'); } + const payload = getSchedulePayload(parameters.username, schedule); + // Extract only the values we can update on a schedule + const body = {}; + ['name', 'description', 'command', 'localtime', 'status', 'autodelete'].forEach(key => { + if (payload[key] !== null) { + body[key] = payload[key]; + } + }); + return { type: 'application/json', body: body @@ -122,21 +116,25 @@ function buildCreateScheduleResult(result) { throw new ApiError(`Error creating group: ${hueErrors[0].description}`, hueErrors[0]); } - return {id: Number(result[0].success.id)}; + const id = result[0].success.id; + return { + id: SCHEDULE_ID_PLACEHOLDER.getValue({id: id}) + }; } -function extractUpdatedAttributes(result) { - if (util.wasSuccessful(result)) { - const values = {} - result.forEach(update => { - const success = update.success; - Object.keys(success).forEach(key => { - const attribute = /.*\/(.*)$/.exec(key)[1]; - values[attribute] = true; //success[key]; - }); - }); - return values; - } else { - throw new ApiError('Error in response'); //TODO extract the error +function getSchedulePayload(username, schedule) { + const payload = schedule.getHuePayload(); + + if (model.timePatterns.isRecurring(payload.localtime)) { + // autodelete does not apply to recurring schedules (as specified in the localtime) + delete payload.autodelete; + } + + // Fix the address from the action to start with "/api/{username}" + const address = payload.command.address; + if (! /^\/api\//.test(address)) { + payload.command.address = `/api/${username}${address}`; } + + return payload; } \ No newline at end of file diff --git a/lib/api/http/endpoints/sensors.js b/lib/api/http/endpoints/sensors.js index 737959c..c81072f 100644 --- a/lib/api/http/endpoints/sensors.js +++ b/lib/api/http/endpoints/sensors.js @@ -1,12 +1,14 @@ 'use strict'; const ApiEndpoint = require('./endpoint') - , SensorIdPlaceholder = require('../placeholders/SensorIdPlaceholder') + , SensorIdPlaceholder = require('../../../placeholders/SensorIdPlaceholder') , model = require('../../../model') , ApiError = require('../../../ApiError') , util = require('../../../util') ; +const SENSOR_ID_PLACEHOLDER = new SensorIdPlaceholder(); + module.exports = { getAllSensors: new ApiEndpoint() @@ -41,7 +43,7 @@ module.exports = { getSensor: new ApiEndpoint() .get() .uri('//sensors/') - .placeholder(new SensorIdPlaceholder()) + .placeholder(SENSOR_ID_PLACEHOLDER) .acceptJson() .pureJson() .postProcess(createSensorResponse), @@ -49,7 +51,7 @@ module.exports = { updateSensor: new ApiEndpoint() .put() .uri('//sensors/') - .placeholder(new SensorIdPlaceholder()) + .placeholder(SENSOR_ID_PLACEHOLDER) .payload(createUpdateSensorPayload) .acceptJson() .pureJson() @@ -58,7 +60,7 @@ module.exports = { deleteSensor: new ApiEndpoint() .delete() .uri('//sensors/') - .placeholder(new SensorIdPlaceholder()) + .placeholder(SENSOR_ID_PLACEHOLDER) .acceptJson() .pureJson() .postProcess(util.wasSuccessful), @@ -66,7 +68,7 @@ module.exports = { changeSensorConfig: new ApiEndpoint() .put() .uri('//sensors//config') - .placeholder(new SensorIdPlaceholder()) + .placeholder(SENSOR_ID_PLACEHOLDER) .payload(buildSensorConfigPayload) .acceptJson() .pureJson() @@ -75,7 +77,7 @@ module.exports = { changeSensorState: new ApiEndpoint() .put() .uri('//sensors//state') - .placeholder(new SensorIdPlaceholder()) + .placeholder(SENSOR_ID_PLACEHOLDER) .payload(buildSensorStatePayload) .acceptJson() .pureJson() @@ -118,8 +120,10 @@ function buildAllSensorsResult(data) { } function createSensorResponse(data, requestParameters) { - const type = data.type.toLowerCase(); - return model.createFromBridge(type, requestParameters.id, data); + const id = SENSOR_ID_PLACEHOLDER.getValue(requestParameters) + , type = data.type.toLowerCase() + ; + return model.createFromBridge(type, id, data); } function createNewSensorResponse(data) { diff --git a/lib/api/index.test.js b/lib/api/index.test.js index 162405f..c819adf 100644 --- a/lib/api/index.test.js +++ b/lib/api/index.test.js @@ -6,7 +6,7 @@ const expect = require('chai').expect , testValues = require('../../test/support/testValues.js') ; -describe('Hue API #lights', () => { +describe('Hue API create for local connections', () => { let hueLocalIpAddress; From 1479fc6b8820fe50ee585f059405486c33f49df1 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Sat, 30 Nov 2019 14:50:44 +0000 Subject: [PATCH 23/35] Updating documentation and examples --- Changelog.md | 91 +- README.md | 170 +- docs/capabilities.md | 169 ++ docs/discovery.md | 27 +- docs/group.md | 255 +- docs/groups.md | 135 +- docs/light.md | 256 +- docs/lights.md | 36 +- docs/resourcelinks.md | 30 +- docs/rules.md | 35 +- docs/scenes.md | 46 +- docs/schedule.md | 0 docs/schedules.md | 167 ++ docs/sensors.md | 38 +- docs/timePatterns.md | 549 ++++ docs/v2_api.md | 2284 ----------------- docs/v3_backwards_compatibility.md | 113 - .../v3/capabilities/getAllCapabilities.js | 22 + examples/v3/groups/createEntertainment.js | 38 + examples/v3/groups/createLightGroup.js | 23 +- examples/v3/groups/createRoom.js | 26 +- examples/v3/groups/createZone.js | 26 +- examples/v3/groups/deleteGroup.js | 11 + examples/v3/groups/getGroup.js | 2 +- ...Attributes.js => updateGroupAttributes.js} | 24 +- .../lights/{getLightById.js => getLight.js} | 6 +- .../v3/lights/getLightAttributesAndState.js | 2 +- examples/v3/lights/getLightState.js | 2 +- examples/v3/resourceLinks/getResourceLink.js | 2 +- .../v3/resourceLinks/getResourceLinkByName.js | 32 + examples/v3/rules/getRule.js | 2 +- examples/v3/rules/getRuleByName.js | 26 + examples/v3/scenes/createScene.js | 2 +- examples/v3/scenes/getScene.js | 4 +- examples/v3/scenes/getSceneByName.js | 2 +- examples/v3/scenes/updateScene.js | 18 +- examples/v3/schedules/createSchedule.js | 44 + examples/v3/schedules/getAllSchedules.js | 27 + .../getScheduleById.js} | 19 +- examples/v3/schedules/getScheduleByName.js | 31 + examples/v3/schedules/updateSchedule.js | 69 + examples/v3/sensors/renameSensor.js | 59 + package-lock.json | 5 + package.json | 4 +- 44 files changed, 2155 insertions(+), 2774 deletions(-) create mode 100644 docs/capabilities.md create mode 100644 docs/schedule.md create mode 100644 docs/schedules.md create mode 100644 docs/timePatterns.md delete mode 100644 docs/v2_api.md delete mode 100644 docs/v3_backwards_compatibility.md create mode 100644 examples/v3/capabilities/getAllCapabilities.js create mode 100644 examples/v3/groups/createEntertainment.js rename examples/v3/groups/{updateAttributes.js => updateGroupAttributes.js} (68%) rename examples/v3/lights/{getLightById.js => getLight.js} (83%) create mode 100644 examples/v3/resourceLinks/getResourceLinkByName.js create mode 100644 examples/v3/rules/getRuleByName.js create mode 100644 examples/v3/schedules/createSchedule.js create mode 100644 examples/v3/schedules/getAllSchedules.js rename examples/v3/{sensors/updateSensorName.js => schedules/getScheduleById.js} (52%) create mode 100644 examples/v3/schedules/getScheduleByName.js create mode 100644 examples/v3/schedules/updateSchedule.js create mode 100644 examples/v3/sensors/renameSensor.js diff --git a/Changelog.md b/Changelog.md index bdb3b07..04e3913 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,25 +3,106 @@ ## 4.0.0 - Deprecated v2 API and shim and modules removed from library +- Introduced rate limiting in the Light and Group set States to be compliant with the Hue API documentation best practices. + This only has an impact on this library, so it may be possible if you are running other software on your network + accessing the Bridge, you will still able to overload it. + * The whole API is currently limited to 12 requests per second by default (currently not configurable) + * `lights.setLightState()` is limited to 10 requests per second + * `groups.setState()` is limited to 1 request per second + +- `v3.discovery.nupnp()` Now returns a different payload as it no longer accesses the XML Discovery endpoint to return + the bridge data as this can become unreliable when the bridge is overloaded. See the [documentation](./docs/discovery.md#n-upnpsearch) + for specifics. + - `v3.api` removed the `create` function as it was deprecated, use `createRemote()` fro the remote API, `createLocal()` for the local API or `createInsecureLocal()` for non-hue bridges that do not support https connections - `v3.Scene` has been removed, use the following functions to create a new Scene instance: * `v3.model.createLightScene()` * `v3.model.createGroupScene()` + This change has also allowed for the separation of the attributes and getter/setters locked down properly based on the type of Scene, i.e. Cannot change the lights in a GroupScene (as they are controlled by the Group). - `v3.sensors` has been removed, use `v3.model.createCLIPxxx()` functions instead -- `v3.rules` has been removed to `v3.model` +- `v3.rules` has been moved into `v3.model` * To create a `Rule` use `v3.model.createRule()` * To create a `RuleCondition` use `v3.model.ruleConditions.[group|sensor]` * To create a `RuleAction` use `v3.model.ruleActions.[light|group|sensor|scene]` - `v3.model` added to support exposing the underlying model objects that represent bridge objects. This module will allow you to create all of the necessary objects, e.g. `createGroupScene()` + +- Capabilities API: + * `capabilities.getAll()` now returns a [`Capabilities` object](./docs/capabilities.md#capabilities-object) + +- Groups API: + * The following API functions will accept a Light Object as the `id` parameter as well as an integer value: + * `groups.get(id)` + * `groups.getGroup(id)` + * `deleteGroup(id)` + * `enableStreaming(id)` + * `disableStreaming(id)` + * `groups.createGroup(group)` introduced, it expects a pre-configured Group instance created using the model functions: + * `model.createLightGroup()` + * `model.createEntertainment()` + * `model.createRoom()` + * `model.createZone()` + * `groups.get(id)` has been deprecated, use `groups.getGroup(id)` instead. + * `groups.createGroup(name, lights)` has been deprecated, use `groups.createGroup(group)` instead. + * `groups.createRoom(name, lights, roomClass)` has been deprecated, use `groups.createGroup(group)` instead. + * `groups.createZone(name, lights, roomClass)` has been deprecated, use `groups.createGroup(group)` instead. + * `groups.updateAttributes(id, data)` has been deprecated, Use `groups.updateGroupAttributes(group)` instead. +- Lights API: + * `getLightById(id)` is deprecated use `getLight(id)` instead + * `rename(id, name)` is deprecated, use `renameLight(light)` instead + * The following API functions will accept a Light Object as the `id` parameter as well as an integer value: + * `getLight(id)` + * `getLightById(id)` + * `getLightAttributesAndState(id)` + * `getLightState(id)` + * `setLightState(id, state)` + * `deleteLight(id)` + +- Scenes API: + * `getScene(id)` introduced, can take a scene id or `Scene` instance as the id value + * `get(id)` has been deprecated, use `getScene(id)` instead + * `getByName(name)` has been depricated use `getSceneByName(name)` instead + * `updateScene(scene)` introduced to replace `update(id, scene)` for updating Scenes + * `update(id, scene)` has been deprecated, will be removed in `5.x`, use `updateScene(scene)` instead + * `deleteScene(id)` can accept a scene id or a `Scene` object as the `id` parameter + * `activateScene(id)` can accept a scene id value or a `Scene` object + * `updateLightState(id, lightId, sceneLightState)` can take an id value or `Scene`/`Light` for the `id` and `lightId` values respectively + +- Sensors API: + * `get(id)` has been depreciated use `getSensor(id)` instead + * `getSensor(id)` will accept a `Sensor` Object as the `id` or the integer `id` value as parameter. + * `updateName(id, name)` has been deprecated, will be removed in `5.x`, use `reanmeSensor(sensor)` instead + * `renameSensor(sensor)` has been added to allow updating of the name only for a sensor (makes API consistent with `lights` and `sensors`) + * `getSensorByName(name)` added to get sensors by `name` + +- Rules API: + * The following API functions will accept a Rule Object as the `id` parameter as well as an integer value: + * `get(id)` + * `deleteRule(id)` + * Added `getRuleByName(name)` function to get rules by `name` + * Rule Actions were common to the new `Schedules`, so have been moved from `v3.model.ruleActions` to `v3.model.actions`. + Use of `v3.model.ruleActions` is considered deprecated. + +- Schedules API: + * The schedules API is finally properly implemented, along with all the various Hue Bridge TimePatterns + * `model.timePatterns` provides an interface with creating the various timePatterns, consult the [documentation](./docs/timePatterns.mc) for details + + * The previous `schedules.update(id, schedule)` function has been removed and replaced with `schedules.update(schedule)`. + + _I am fairly sure that the previous version was most likely never used (base on the implmenetation as it would + have likely errored). With this knowledge, it was not deprecated and just removed. If you are impacted by this change, please raise an Issue._ + +- ResourceLinks API: + * New API interacting with `ResourceLinks` via, `api.resourceLinks`, see [documentation](./docs/resourcelinks.md) for more details. + - All creation function calls to the bridge will now return the created model object. This change makes it consistent as some calls would return the object, others would return the id but no other data. @@ -29,8 +110,6 @@ * `api.rules.createRule()` * `api.scenes.createScene()` * `api.sensors.createSensor()` - -- Added support for `ResourceLinks` in the API - Type system from the `LightState` definitions is now used in all Bridge Object Models to define the attributes/properties obtained from the Bridge. @@ -43,17 +122,17 @@ payload. This was requested to aid in server/client side code situations, as the creation of the model objects are not directly exposed in the library by design. Related to issue #132 -- Adding more in-depth tests to greatly increase coverage around types and models - - Creating Sensors (CLIP variety) has changed as the classes for the sensor objects are no longer directly accessible. All `CLIPxxx` sensors need to be built from the `v3.model.createCLIP[xxx]Sensor()` function for the desired type, e.g. `v3.model.createCLIPGenericStatusSensor()` for a `CLIPGenericStatus` sensor. The function call to instantiate the sensors also no longer take an object to set various attributes of the sensor, - you need to call the approriate setter on the class now to se the attribute, e.g. `sensor.manufacturername = 'node-hue-api-sensor';` + you need to call the approriate setter on the class now to set the attribute, e.g. `sensor.manufacturername = 'node-hue-api-sensor';` - TypeScript definitions added to the library +- Adding more in-depth tests to further increase coverage around types and models, and adding more edge case API level tests + ## 3.4.1 - Fixing issue with the lookup for the Hue motion sensor, issue #146 diff --git a/README.md b/README.md index 565e07c..99ad64e 100644 --- a/README.md +++ b/README.md @@ -8,35 +8,44 @@ various other features of the Hue Bridge. This library abstracts away the actual Philips Hue Bridge REST API and provides all of the features of the Philips API and a number of useful functions to control/configure its various features. -The library fully supports `local network` and `remote internet` access to the Hue Bridge. +The library fully supports `local network` and `remote internet` access to the Hue Bridge API and has 100% coverage of the +documented Hue API. ## Contents - [Change Log](#change-log) - [Installation](#installation) -- [v3 API](#v3-api) - new API introduced in 3.x versions of the library - - [Discovering Local Hue Bridges](docs/discovery.md) - - [Remote API Support](docs/remoteApi.md) - - [Users](docs/users.md) - - [Lights](docs/lights.md) - - [Light Object](docs/light.md) - - [LightState Object](docs/lightState.md) - - [Sensors](docs/sensors.md) - - [Sensor Objects](docs/sensor.md) - - [Scenes](docs/scenes.md) - - [Scene Object](docs/scene.md) - - [SceneLightState Object](docs/lightState.md#scenelightstate) - - [Groups](docs/groups.md) - - [Group Object](docs/group.md) - - [GroupLightState Object](docs/lightState.md#grouplightstate) - - [Rules](docs/rules.md) - - [Rule Object](docs/rule.md) - - [RuleCondition Object](docs/ruleCondition.md) - - [RuleAction Object](docs/ruleAction.md) - - [ResourceLinks](/docs/resourcelinks.md) - - [ResourceLink Object](docs/resourceLink.md) - - [Configuration](docs/configuration.md) - - [Remote](docs/remote.md) +- [v3 API](#v3-api) + - [Connections to the Bridge](#connections-to-the-bridge) + - [Rate Limiting](#rate-limiting) + - [Debug Bridge Communications](#debug-bridge-communications) + - [API Documentation](#api-documentation) + - [Discovering Local Hue Bridges](docs/discovery.md) + - [Remote API Support](docs/remoteApi.md) + - [Users](docs/users.md) + - [Lights](docs/lights.md) + - [Light Object](docs/light.md) + - [LightState Object](docs/lightState.md) + - [Sensors](docs/sensors.md) + - [Sensor Objects](docs/sensor.md) + - [Scenes](docs/scenes.md) + - [Scene Object](docs/scene.md) + - [SceneLightState Object](docs/lightState.md#scenelightstate) + - [Groups](docs/groups.md) + - [Group Objects](docs/group.md) + - [GroupLightState Object](docs/lightState.md#grouplightstate) + - [Rules](docs/rules.md) + - [Rule Object](docs/rule.md) + - [RuleCondition Object](docs/ruleCondition.md) + - [RuleAction Object](docs/ruleAction.md) + - [ResourceLinks](/docs/resourcelinks.md) + - [ResourceLink Object](docs/resourceLink.md) + - [Schedules](docs/schedules.md) + - [Schedule Object](docs/schedule.md) + - [Time Patterns](docs/timePatterns.md) + - [Configuration](docs/configuration.md) + - [Capabilities](docs/capabilities.md) + - [Remote](docs/remote.md) - [Examples](#examples) - [Discover and connect to the Hue Bridge for the first time](#discover-and-connect-to-the-hue-bridge-for-the-first-time) - [Set a LightState on a Light](#set-a-light-state-on-a-light) @@ -47,8 +56,8 @@ The library fully supports `local network` and `remote internet` access to the H ## Change Log -For a list of changes, please refer to the change log; -[Changes](Changelog.md) +For a list of changes, and details of the fixes/improvements, bugs resolved, please refer to the change log; +[Change Log](Changelog.md) ## Installation @@ -62,14 +71,102 @@ Node.js using yarn: $ yarn install node-hue-api ``` - ## v3 API -The V3 API is written to support JavaScript native Promises, as such you can use stand Promise chaining with `then()` +The V3 API is written to support JavaScript native Promises, as such you can use standard Promise chaining with `then()` and `catch()` or utilize synchronous `async` and `await` in your own code base. -_Note that there are a number of runnable code samples in the [examples/v3](examples/v3) directory of this repository._ +As of release `4.0.0` in December 2019, the library now has complete coverage for the Hue REST API. + + +### Connections to the Bridge +By default all connections to the Hue Bridge are done over TLS, after the negotiation of the Bridge certificate being +verified to the expected format and subject contents. + +The Bridge certificate is self-signed, so this will cause issues when validating it normally. The library will process +the certificate, validate the issuer and the subject and if happy will then allow the connection over TLS with the Hue +Bridge. + +When using the remote API functionality of the library, the certificate is validated normally as the https://api.meethue.com +site has an externally valid certificate and CA chain. + +_Note: There is an option to connect over `HTTP` using `createInsecureLocal()` as there are some instances of use of the +library against software the pretends to be a Hue Bridge. Using this method to connect will output warnings on the `console` +that you are connecting in an insecure way_. + + +### Rate Limiting +As of version 4.0+ of the library there are Rate limiters being used in three places: + +* For the whole library, API calls are limited to 12 per second +* For `lights.setLightState()`, API calls are limited to 10 per second +* For `groups.setState()`, API calls are limited to 1 per second + +These defaults are not currently configurable, but have been implemented to conform to the best practices defined in the +Hue API documentation. If you are facing issues with this, then raise a support ticket via an Issue. + +_Note: these do NOT (and cannot) take into account all access to the Hue Bridge, so if you have other softare that also +accesses the bridge, it is still possible to overload it with requests._ + + +### Debug Bridge Communications +You can put the library in to debug mode which will print out the placeholder and request details that it is using to +talk to the Hue Bridge. +To do this, you need to define an environment variable of `NODE_DEBUG` and ensure that it is set to a string that +contains `node-hue-api` in it. + +Once the debug mode is active you will see output like the following on the console: + +``` +Bridge Certificate: + subject: {"C":"NL","O":"Philips Hue","CN":"xxxxxxxxx"} + issuer: {"C":"NL","O":"Philips Hue","CN":"xxxxxxxxx"} + valid from: Jan 1 00:00:00 2017 GMT + valid to: Jan 1 00:00:00 2038 GMT + serial number: xxxxxxx + +Performing validation of bridgeId "xxx" against certifcate subject "xxx"; matched? true +URL Placeholders: + username: { type:string, optional:false, defaultValue:null } +Headers: {"Accept":"application/json"} +{ + "method": "get", + "baseURL": "https://192.xxx.xxx.xxx:443/api", + "url": "/xxxxxxxxxxxxxxxxxxxxxxxxxxx" +} +URL Placeholders: + username: { type:string, optional:false, defaultValue:null } +Headers: {"Accept":"application/json","Content-Type":"application/json"} +{ + "method": "post", + "baseURL": "https://192.xxx.xxx.xxx:443/api", + "url": "/xxxxxxxxxxxxxxxxxxxxxxxx/schedules", + "data": { + "name": "Test Schedule Recurring", + "description": "A node-hue-api test schedule that can be removed", + "command": { + "address": "/api/xxxxxxxxxxxxxxxxxxxxxxxxxx/lights/0/state", + "method": "PUT", + "body": { + "on": true + } + }, + "localtime": "W124/T12:00:00", + "status": "enabled", + "recycle": true + } +} +``` + +_Note: You should be careful as to who can gain access to this output as it will contain sensative data including the +MAC Address of the bridge, IP Address and username values._ + +The above warning applies here with respect to schedule when **not** in debug mode, as the schedule endpoints will contain the +username value (that can be used to authenticate against the bridge) in the payloads of the `command`. + + +## API Documentation - [Discovering Local Hue Bridges](docs/discovery.md) - [Remote API Support](docs/remoteApi.md) - [Users](docs/users.md) @@ -81,19 +178,22 @@ _Note that there are a number of runnable code samples in the [examples/v3](exam - [Scene Object](docs/scene.md) - [SceneLightState Object](docs/lightState.md#scenelightstate) - [Groups](docs/groups.md) - - [Group Object](docs/group.md) + - [Group Objects](docs/group.md) - [GroupLightState Object](docs/lightState.md#grouplightstate) - [Rules](docs/rules.md) - [Rule Object](docs/rule.md) - [RuleCondition Object](docs/ruleCondition.md) - [RuleAction Object](docs/ruleAction.md) -- [ResourceLinks](/docs/resourcelinks.md) +- [ResourceLinks](docs/resourcelinks.md) - [ResourceLink Object](docs/resourceLink.md) +- [Schedules](docs/schedules.md) + - [Schedule Object](docs/schedule.md) + - [Time Patterns](docs/timePatterns.md) - [Configuration](docs/configuration.md) +- [Capabilities](docs/capabilities.md) - [Remote](docs/remote.md) - ## Examples The v3 APIs are documented using example code and links to more complex/complete examples for each API calls, consult the documentation links [above](#v3-api). @@ -218,9 +318,11 @@ documentation and examples referenced within. ### Using Hue Remote API -This library has support for interacting with the `Hue Remote API` as well as local network connections. +This library has support for interacting with the `Hue Remote API` as well as local network connections. There are some +limitations on the remote endpoints, but the majority of them will function as they would on a local network. + +It can be rather involved to set up a remote connection, but not too onerous if you desire such a thing. -It is rather involved to set up a remote connection, but not too onerous if you desire such a thing. The complete documentation for doing this is detailed in the [Remote API](docs/remoteApi.md) and associated links. * [Example for connecting remotely for the first time](./examples/v3/remote/accessFromScratch.js) diff --git a/docs/capabilities.md b/docs/capabilities.md new file mode 100644 index 0000000..fb660ad --- /dev/null +++ b/docs/capabilities.md @@ -0,0 +1,169 @@ +# Capabilities API + +The `capabilities` API provides a means of obtaining the capabilities of the Bridge, along with totals as to how +many more things of each type can be stored/created. + + +* [getAll()](#getAll) +* [Capabilities Object](#capabilities-object) + * [lights](#lights) + * [sensors](#sensors) + * [groups](#groups) + * [scenes](#scenes) + * [schedules](#schedules) + * [rules](#rules) + * [resourcelinks](#resourcelinks) + * [streaming](#streaming) + * [timezones](#timezones) + + +## getAll +The `getAll()` function will get the complete capabilities from the Hue Bridge. + +```js +api.capabilities.getAll() + .then(capabilities => { + // Display the full capabilities from the bridge + console.log(capabilities.toStringDetailed()); + }) +; +``` + +The function call will resolve to a `Capabilities` Object. + +A complete code sample is available [here](../examples/v3/capabilities/getAllCapabilities.js). + + +## Capabilities Object + +The Capabilities Object is an object that holds all the capability data obtained from the bridge. +The properties available on this object are detailed below: + +### lights +Obtains the capabilities for the total number of supported lights on the bridge and how many more can be added. + +* `get` + +```js +{ + "available": 23, + "total": 63 +} +``` + +### sensors +Obtains the capabilities for the total number of supported sensors on the bridge and how many more can be added. +It has a further break down on the supported types of sensors, `clip`, `zll` and `zgp`. + +* `get` + +```js +{ + "available": 209, + "total": 250, + "clip": { + "available": 209, + "total": 250 + }, + "zll": { + "available": 62, + "total": 64 + }, + "zgp": { + "available": 62, + "total": 64 + } +} +``` + +### groups +Obtains the capabilities for the total number of supported groups on the bridge and how many more can be added. + +* `get` + +```js +{ + "available": 41, + "total": 64 +} +``` + +### scenes +Obtains the capabilities for the total number of supported scenes on the bridge and how many more can be added. +It also details how many more lightstates are in use and how many more can be created. + +* `get` + +```js +{ + "available": 89, + "total": 200, + "lightstates": { + "available": 5210, + "total": 12600 + } +} +``` + +### schedules +Obtains the capabilities for the total number of supported schedules on the bridge and how many more can be added. + +* `get` + +```js +{ + "available": 97, + "total": 100 +} +``` + +### rules +Obtains the capabilities for the total number of supported rules on the bridge and how many more can be added. +It also contains similar breakdowns for the `conditions` and the `actions`. +* `get` + +```js +{ + "available": 218, + "total": 250, + "conditions": { + "available": 1429, + "total": 1500 + }, + "actions": { + "available": 956, + "total": 1000 + } +``` + +### resourcelinks +Obtains the capabilities for the total number of supported schedules on the bridge and how many more can be added. + +This is also available as `resourceLinks` getter on the Object (which is consistent with naming of ResourceLinks elsewhere in the API), + +* `get` + +```js +{ + "available": 51, + "total": 64 +} +``` + +### streaming +Obtains the capabilities for around the Entertainment streaming functions. + +* `get` + +```js +{ + "available": 1, + "total": 1, + "channels": 10 +} +``` + +### timezones +Obtains a list of the timezones known to the Hue Bridge as an Array of `String`s. + +* `get` \ No newline at end of file diff --git a/docs/discovery.md b/docs/discovery.md index f99d887..7a1123d 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -38,33 +38,14 @@ The results will be an array of discovered bridges with the following structure: ```json [ { - "name": "Philips hue (192.xxx.xxx.xxx)", - "manufacturer": "Royal Philips Electronics", - "ipaddress": "192.xxx.xxx.xxx", - "model": { - "number": "BSB002", - "description": "Philips hue Personal Wireless Lighting", - "name": "Philips hue bridge 2015", - "serial": "0017xxxxxxxx" - }, - "version": { - "major": "1", - "minor": "0" - }, - "icons": [ - { - "mimetype": "image/png", - "height": "48", - "width": "48", - "depth": "24", - "url": "hue_logo_0.png" - } - ] + "name": "Philips hue", + "ipaddress": "192.xxx.xxx.xxx", + "modelid": "BSB002", + "swversion": "1935074050" } ] ``` -Note that the data returned can vary depending upon the version of the bridge software. ## UPnP Search diff --git a/docs/group.md b/docs/group.md index a1c7cc2..4095518 100644 --- a/docs/group.md +++ b/docs/group.md @@ -1,38 +1,63 @@ # Group -A `Group` is a representation of a group in the Hue Bridge. You cannot create a `Group` object directly, these will -be returned from API calls that provide groups as results, like [api.groups.getAll()](./groups.md#getall). - -The properties and functions available for a Group are: - -* [name](#name) -* [id](#id) -* [lights](#lights) -* [type](#type) -* [action](#action) -* [recycle](#recycle) -* [sensors](#sensors) -* [state](#state) -* [class](#class) -* [locations](#locations) -* [stream](#stream) -* [modelid](#modelid) -* [uniqueid](#uniqueid) -* [toString()](#tostring) -* [toStringDetailed()](#tostringdetailed) +A `Group` is a representation of a grouping construct in the Hue Bridge, of which there are a number of specializations +including serrving different purposes: + +* [`LightGroup`](#lightgroup): The default type of Group which collects lights and sensors +* [`Luminaire`](#luminaire): A special type of group representing a Luminaire, the bridge will create these for these devices if you have any +* [`Room`](#room): A Room that collects a number of lights and sensors togehter (a light and sensor can only belong to a single `Room`) +* [`Entertainment`](#entertainment): A new special type of Group used for an Entertainment area for streaming (syncing light changes + to a visualization via a seperate streaming API). Not all lights can be added to an Entertainment Group. +* [`Zone`](#zone): A group that allows you to define a Zone that might be within a room, or extend across a number of rooms. + This allows you to work around the `Room` limitations of lights and sensors only being able to belong to on Room. + +You cannot create these Object directly, but can either retrieve them from the Hue Bridge via the [Groups API](./groups.md) +or create a new instance of them using the `v3.model` functions: + +* `v3.model.createLightGroup()` +* `v3.model.createRoom()` +* `v3.model.createZone()` +* `v3.model.createEntertainment()` + + +* [Common Group Properties and Functions](#common-group-properties-and-functions) + * [id](#id) + * [name](#name) + * [lights](#lights) + * [type](#type) + * [action](#action) + * [recycle](#recycle) + * [sensors](#sensors) + * [state](#state) + * [toString()](#tostring) + * [toStringDetailed()](#tostringdetailed) +* [LightGroup](#lightgroup) +* [Room](#room) + * [class](#class) +* [Zone](#zone) + * [class](#class +* [Entertainment](#entertainment) + * [class](#class) + * [locations](#locations) + * [stream](#stream) + + + +## Common Group Properties and Functions + +The properties and functions available for all `Group`s are: - -## name -The name for the group. +## id +The id for the group. * `get` -* `set` -## id -The id for the group. +## name +The name for the group. * `get` +* `set` ## lights @@ -52,14 +77,9 @@ The type can be one of the following values: * `Lighsource`: Multisource Luminaire group * `LightGroup`: A group of lights that can be controlled together * A light group is deleted by the bridge if you remove all the lights from it -* `Room`: A group of lights that are physically located in the smae place in a house. These behave like a standard group, but have a couple of differences - * A room can be empry and contain no lights - * A light is only allowed in a single room - * A room is not automatically deleted when all the lights are removed -* `Entertainment`: Represents an entertainment set up, which is a group of lights used to define targets for streaming along with defining position of the lights -* `Zone`: A group of lights that can be controlled together - * A zone can be empty and contain no lights - * A light is allowed to be in multiple zones (as opposed to only being able to belong to a single room) +* `Room` +* `Entertainment` +* `Zone` ## action @@ -87,86 +107,135 @@ the light members are currently on. `get` +### toString() +The `toString()` function will obtain a simple `String` representation of the Scene. + + +###toStringDetailed() +The `toStringDetailed()` function will obtain a more detailed representation of the Scene object. + + + +## LightGroup + +The standard LightGroup has all the properties and functions defined in the [common group properties](#common-group-properties-and-functions). + + +## Room +A group of lights that are physically located in the same place in a house. These behave like a standard group, but have a couple of differences: + +* A room can be empty and contain no lights +* A light is only allowed in a single room +* A room is not automatically deleted when all the lights are removed + +The `Room` Group has all the properties and functions as defined in the [common group properties](#common-group-properties-and-functions) +as well as the following: + ## class -The `class` represents the `Room` category as a `String`. +The `class` represents the `Zone` category as a `String`. * `get` +* `set` The values that can be set for this on a `Room` are: -* `Living room` -* `Kitchen` -* `Dining` -* `Bedroom` -* `Kids bedroom` -* `Bathroom` -* `Nursery` -* `Recreation` -* `Office` -* `Gym` -* `Hallway` -* `Toilet` -* `Front door` -* `Garage` -* `Terrace` -* `Garden` -* `Driveway` -* `Carport` -* `Other` +-------------- | -------------- | ---------- +`Living room` | `Kitchen` | `Dining` +`Bedroom` | `Kids bedroom` | `Bathroom` +`Nursery` | `Recreation` | `Office` +`Gym` | `Hallway` | `Toilet` +`Front door` | `Garage` | `Terrace` +`Garden` | `Driveway` | `Carport` +`Other` | | -Since version 1.30 of the Hue Bridge API the following values can also be present: - -* `Home` -* `Downstairs` -* `Upstairs` -* `Top floor` -* `Attic` -* `Guest room` -* `Staircase` -* `Lounge` -* `Man cave` -* `Computer` -* `Studio` -* `Music` -* `TV` -* `Reading` -* `Closet` -* `Storage` -* `Laundry room` -* `Balcony` -* `Porch` -* `Barbecue` -* `Pool` +Since version 1.30 of the Hue Bridge API the following values can also be used: +-------------- | ------------- | ---------- +`Home` | `Downstairs` | `Upstairs` +`Top floor` | `Attic` | `Guest room` +`Staircase` | `Lounge` | `Man cave` +`Computer` | `Studio` | `Music` +`TV` | `Reading` | `Closet` +`Storage` | `Laundry room` | `Balcony` +`Porch` | `Barbecue` | `Pool` -## locations -The locations of the lights in an `Entertainment` type Group which is an Object that maps the `id` of the light to an -`Array` consisting of three integers. -`get` +## Zone +A group of lights that can be controlled together. +* A zone can be empty and contain no lights +* A light is allowed to be in multiple zones (as opposed to only being able to belong to a single room) -## stream -THe stream details for an `Entertainment` type Group. +The `Zone` Group has all the properties and functions as defined in the [common group properties](#common-group-properties-and-functions) +as well as the following: -`get` +## class +The `class` represents the `Zone` category as a `String`. +* `get` +* `set` -## modelid -A unique model id that identifies the hardware model of the luminaire, only present if the group is a Lunminaire device. +The values that can be set for this on a `Room` are: -* `get` +-------------- | -------------- | ---------- +`Living room` | `Kitchen` | `Dining` +`Bedroom` | `Kids bedroom` | `Bathroom` +`Nursery` | `Recreation` | `Office` +`Gym` | `Hallway` | `Toilet` +`Front door` | `Garage` | `Terrace` +`Garden` | `Driveway` | `Carport` +`Other` | | + +Since version 1.30 of the Hue Bridge API the following values can also be used: + +-------------- | ------------- | ---------- +`Home` | `Downstairs` | `Upstairs` +`Top floor` | `Attic` | `Guest room` +`Staircase` | `Lounge` | `Man cave` +`Computer` | `Studio` | `Music` +`TV` | `Reading` | `Closet` +`Storage` | `Laundry room` | `Balcony` +`Porch` | `Barbecue` | `Pool` -## uniqueid -The unique id on `AA:BB:CC:DD` format for Lunimarie groups or `AA:BB:CC:DD-XX` format for Lightsource groups (XX is the lightsource position). + +## Entertainment +Represents an entertainment set up, which is a group of lights used to define targets for streaming along with defining position of the lights. + +The Entertainment Group has all the properties and functions as defined in the [common group properties](#common-group-properties-and-functions) +as well as the following: + +* [class](#class) +* [locations](#locations) +* [stream](#stream) + +## class +The `class` represents the `Entertainment` category as a `String`. * `get` +The values that can be set for this on a are: -### toString() -The `toString()` function will obtain a simple `String` representation of the Scene. +* `TV` +* `Other` -###toStringDetailed() -The `toStringDetailed()` function will obtain a more detailed representation of the Scene object. \ No newline at end of file +## locations +The locations of the lights in an `Entertainment` type Group which is an Object that maps the `id` of the light to an +`Array` consisting of three integers inidicating the position in 3D space from the source. + +`get` + + +## stream +The stream details object for an `Entertainment` type Group. + +`get` + +The stream object consists of the following keys and values: + +* `proxymode`: A string indicating the proxy mode +* `proxynode`: A string which is an address string to a light in the bridge, e.g. `/lights/22` +* `active`: A Boolean indicating whether or not the Entertainment is currently streaming +* `owner`: If the Entertainment is currently streaming, this is the user id of the owner of the stream. + diff --git a/docs/groups.md b/docs/groups.md index adc1547..647cfb4 100644 --- a/docs/groups.md +++ b/docs/groups.md @@ -3,13 +3,11 @@ The `groups` API provides a means of interacting with groups in the Hue Bridge. * [getAll()](#getall) -* [get()](#get) -* [getByName()](#getbyname) -* [Get Groups by type](#get-groups-by-type) +* [getGroup()](#getgroup) +* [getGroupByName()](#getgroupbyname) +* [get by type](#get-by-type) * [createGroup()](#creategroup) -* [createRoom()](#createroom) -* [createZone()](#createzone) -* [updateAttributes()](#updateattributes) +* [updateGroupAttributes()](#updategroupattributes) * [deleteGroup()](#deletegroup) * [getGroupState()](#getgroupstate) * [setGroupState()](#setgroupstate) @@ -24,54 +22,59 @@ The `getAll()` function allows you to get all the groups that the Hue Bridge has api.groups.getAll() .then(allGroups => { // Display the groups from the bridge - console.log(JSON.stringify(allGroups, null, 2)); + allGroups.forEach(group => { + console.log(group.toStringDetailed()); + }); }); ``` -This function call will resolve to an `Array` of `Group` objects. +This function call will resolve to an `Array` of `Group` instance objects (LightGroup, Zone, Room, Entertainment and Luminaire, +see [Group Objects](./group.md) for more details). A complete code sample for this function is available [here](../examples/v3/groups/getAllGroups.js). -## get() -The `get(id)` function will allow to to get the group specified by the `id` value. +## getGroup() +The `getGroup(id)` function will allow to to get the group specified by the `id` value. -* `id`: The integer `id` of the group you wish to retrieve. +* `id`: The integer `id` of the group you wish to retrieve, or a `Group` object that you want a refreshed copy of. ```js -api.groups.get(1) +api.groups.getGroup(1) .then(group => { console.log(group.toStringDetailed()); }) ; ``` -The call will resolve to a `Group` instance for the specified `id`. +The call will resolve to a [`Group Object`](./group.md) instance for the specified `id`. A complete code sample for this function is available [here](../examples/v3/groups/getGroup.js). -## getByName() -The `getByName(name)` function will retrieve the group(s) from the Hue Bridge that have the specified `name`. Group names +## getGroupByName() +The `getGroupByName(name)` function will retrieve the group(s) from the Hue Bridge that have the specified `name`. Group names are not guaranteed to be unique in the Hue Bridge. ```js -api.groups.getByName('myGroup') +api.groups.getGroupByName('myGroup') .then(matchedGroups => { // Display the groups from the bridge - console.log(JSON.stringify(matchedGroups, null, 2)); + matchedGroups.forEach(group => { + console.log(group.toStringDetailed()); + }); }); ``` -The call will resolve to an `Array` of `Group` objects that have the matching `name`. +The call will resolve to an `Array` of `Group Objects`](./group.md) that have a matching `name`. A complete code sample for this function is available [here](../examples/v3/groups/getGroupByName.js). -## Get Groups by Type +## get by type You can retrieve a specific `type` of `Group` from the Hue Bridge using the following functions: @@ -92,85 +95,56 @@ api.groups.getZones() ; ``` -All these functions will resolve to an `Array` of `Group` object that match the `type` that was desired; e.g. a Zone Groups if you call `getZones()`. +All these functions will resolve to an `Array` of matching [Group Objects](./group.md) that matches the `type` that was requested; e.g. a Zones if you call `getZones()`. A complete code samples for these functions is available [here](../examples/v3/groups/getGroupByType.js). ## createGroup() -The `createGroup(name, lights)` function allows you to create a `Group` in the Hue Bridge. This will create a `LightGroup` type group. +The `createGroup(group)` function allows you to create an instance of a specific [`Group Object`](./group.md) in the Hue Bridge. -* `name`: The name of the group to be created -* `lights`: The `Array` of light `id`s that will be associated with the Group. +* `group`: The Group object that has been built that you wish to create, e.g. a LightGroup populated with a name and light ids ```js -api.groups.createGroup('myNewGroup', [1, 2]) +const group = v3.model.createLightGroup(); +group.name = 'my new group'; +group.lights = [2, 3]; + +api.groups.createGroup(group) .then(createdGroup => { - console.log(`Created new group with id:${createdGroup.id}`); + console.log(`Created new group:\n${createdGroup.toStringDetailed()}); }) ; ``` -The call will resolve to an `Group` object that was created. - -A complete code sample for this function is available [here](../examples/v3/groups/createLightGroup.js). - - - -## createRoom() -The `createRoom(name, lights, roomClass)` function allows you to create a `Group` in the Hue Bridge. This will create a `LightGroup` type group. - -* `name`: The name of the group to be created -* `lights`: The `Array` of light `id`s that will be associated with the Room. A Light can only exist in a single room. - You can pass an empty Array of lights and associate the lights later, or not specify this value at all if you are not providing a `roomClass`. -* `roomClass`: The room class, a `String` value for the class of room, see [here](./group.md#class) for available values. - This is optional, if not specified will default to `Other` class. +The call will resolve to a [`Group Object`](./group.md) that was created using the provided details. -```js -api.groups.createGroup('New Gym Room', [1, 2], 'Gym') - .then(createdRoom => { - console.log(`Created new room with id:${createdRoom.id}`); - }) -; -``` - -The call will resolve to an `Group` object that was created. +Complete code samples creating various types of Groups are available: +* [LightGroup](../examples/v3/groups/createLightGroup.js) +* [Room](../examples/v3/groups/createRoom.js) +* [Zone](../examples/v3/groups/createZone.js) +* [Entertainment](../examples/v3/groups/createEntertainment.js) -A complete code sample for this function is available [here](../examples/v3/groups/createRoom.js). +## updateGroupAttributes() +The `updateGroupAttributes(group)` function allows you to update the attributes of an existing group. The attributes that +can be modified are: -## createZone() -The `createRoom(name, lights, roomClass)` function allows you to create a `Group` in the Hue Bridge. This will create a `LightGroup` type group. + * `name` + * `lights` + * `sensors` + * `class`: The class for the Room/Zone or Entertainment Group -* `name`: The name of the group to be created -* `lights`: Optional `Array` of light `id`s that will be associated with the Zone. -* `roomClass`: Optional room class, a `String` value for the class of zone, see [here](./group.md#class) for available values +* `group`: The Group that has been modified with the updates you want to apply to the Bridge Group. ```js -api.groups.createGroup('New Gym Room', [1, 2], 'Gym') - .then(createdRoom => { - console.log(`Created new room with id:${createdRoom.id}`); - }) -; -``` - -The call will resolve to an `Group` object that was created. - -A complete code sample for this function is available [here](../examples/v3/groups/createZone.js). - - +const group; // Obtained from some other call to retrieve this reference to a Group object from the bridge +// Update the name +group.name = 'Updated Group Name'; -## updateAttributes() -The `updateAttributes(id, data)` function allows you to update the attributes of an existing group. The attributes that -can be modified are `name`, `lights` and/or `room class`. - -* `id`: The `id` of the group to be updated -* `data`: An object with optional keys of `name`, `lights` and/or `class`. All of these are optional, but at least one must be specified. - -```js -api.groups.updateAttributes(id, {name: 'my_new_group_name'}) +api.groups.updateGroupAttributes(group) .then(result => { console.log(`Updated attributes: ${result}`) }) @@ -179,14 +153,14 @@ api.groups.updateAttributes(id, {name: 'my_new_group_name'}) The call will resolve to a `Boolean` indicating the success status of the update to the specified attributes. -A complete code sample for this function is available [here](../examples/v3/groups/updateAttributes.js). +A complete code sample for this function is available [here](../examples/v3/groups/updateGroupAttributes.js). ## deleteGroup() The `deleteGroup(id)` function allow you to delete a group from the Hue Bridge. -* `id`: The integer `id` of the group to delete. +* `id`: The integer `id` of the group to delete, or a `Group Object` that you wish to remove ```js api.groups.deleteGroup(id) @@ -205,7 +179,7 @@ A complete code sample for this function is available [here](../examples/v3/grou ## getGroupState() The `getGroupState(id)` function allows you to get the current state that has been applied to the `Group`. -* `id`: The id of the group to get the state for +* `id`: The id of the group to get the state for, or Group Object. ```js api.groups.getGroupState(id) @@ -223,7 +197,7 @@ The call will resolve to an `Object` that contains the current state values for ## setGroupState() The `setGroupState(id, state)` function allows you to set a state on the lights in the specified `Group`. -* `id`: The id of the group to modify the state on +* `id`: The id of the group to modify the state on, or a `Group Object`. * `state`: A `GroupLightState` for the group to be applied to the lights in the group. This can be an Object with explicit key values or a `GroupLightState` Object. @@ -240,7 +214,7 @@ api.groups.setGroupState(groupId, {on: true}) Using a [`GroupLightState`](lightState.md#grouplightstate) Object: ```js -const GroupLightState = require('node-hue-api').v3.lightStates.GroupLightState; +const GroupLightState = require('node-hue-api').v3.model.lightStates.GroupLightState; const groupState = new GroupLightState().on(); api.groups.setGroupState(groupId, groupState) @@ -250,7 +224,6 @@ api.groups.setGroupState(groupId, groupState) ; ``` - The call will resolve to a `Boolean` indicating success state of setting the desired group light state. A complete code sample for this function is available [here](../examples/v3/groups/setGroupLightState.js). diff --git a/docs/light.md b/docs/light.md index d95be77..16a32d6 100644 --- a/docs/light.md +++ b/docs/light.md @@ -1,18 +1,254 @@ # Light -All objects representing lights that are returned from the API are instances or `Light` objects. +All Lights on the Bridge are built into a `Light` instance. -The Light object has a number of specializations which are: +The Hue Bridge has multiple types of lights which are: -* `OnOffLight` -* `DimmableLight` -* `ColorLight` -* `ColorTemperatureLight` -* `ExtendedColorLight` +* `On Off Light` +* `Dimmable Light` +* `Color Light` +* `Color Temperature Light` +* `Extended Color Light` -Each instance of a `Light` will have different properties depending upon their capabilities. The API will return the -correct mapped instance of `Light` base on the payload it receives from the Hue Bridge. +Each instance of a `Light` will have different properties depending upon their capabilities of the underlying type. -*TODO document the properties of the object* + + +* [Light Properties and Functions](#light-properties-and-functions) + * [id](#id) + * [name](#name) + * [type](#type) + * [modelid](#modelid) + * [manufacturername](#manufacturername) + * [uniqueid](#uniqueid) + * [productid](#productid) + * [swversion](#swversion) + * [swupdate](#swupdate) + * [state](#state) + * [capabilities](#capabilites) + * [coloeGamut](#colorgamut) + * [getSupportedStates()](#getsupportedstates) + * [toString()](#tostring) + * [toStringDetailed()](#tostringdetailed) + + + +## Light Properties and Functions + + +## id +The id for the light in the Bridge. + +* `get` + + +## name +The name for the light. + +* `get` +* `set` + +## type +The type of the light. + +* `get` + +The known types of Lights (there may be more, and also variant of the strings): + + * `On Off Light` + * `Dimmable Light` + * `Color Light` + * `Color Temperature Light` + * `Extended Color Light` + + +## modelid +The model id of the light + +* `get` + +## manufacturername +The manufacturer name of the light. + +* `get` + +## uniqueid +The unique id of the light in the Hue Bridge. + +* `get` + +## productid +The product id for the light + +* `get` + +## swversion +The software version number, if applicable for the light. + +* `get` + +## swupdate +The software update object for the light. + +* `get` + +The Object if present for a light (not all support software updates) is of the form: + +For a light that can be software updated: +```json +{ + "state": "noupdates", + "lastinstall": "2019-09-23T22:12:54" +} +``` + +For a light that does not support software updates: +```json +{ + "state": "notupdatable", + "lastinstall": null +} +``` + +## state +The state of the light when it was retrieved from the Hue Bridge. + +* `get` + +This is an Object representation of a LightState, but is left as a raw Object. + + +## capabilites +An Object representing all the capabilities of the Light. The details for the capabilities varies depending upon the +Light product and manufacturer. Older lights may report nothing whereas new Hue lights used for Entertainment Streaming +will report a lot of details in their capabilities. + +* `get` + +An example of an Extended Color Light capabilities: +```json +"capabilities": { + "certified": true, + "control": { + "mindimlevel": 5000, + "maxlumen": 250, + "colorgamuttype": "B", + "colorgamut": [ + [ + 0.675, + 0.322 + ], + [ + 0.409, + 0.518 + ], + [ + 0.167, + 0.04 + ] + ], + "ct": { + "min": 153, + "max": 500 + } + }, + "streaming": { + "renderer": true, + "proxy": true + } +} +``` + +## colorGamut +Obtains the matched Color Gamut for the Light. This can be loaded from the Light capabilities object, or +via a matching against the light modelid. + +Only lights that support color will report a colorGamut (white lights will not have a color gamut). + +* `get` + +The result will either be `null` or an Object consisting of `red`, `green` and `blue` keys set to an Object with `x`, `y` values: + +```js +{ + red: {x: 0.692, y: 0.308}, + green: {x: 0.17, y: 0.7}, + blue: {x: 0.153, y: 0.048} +} +``` + + +## getSupportedStates() +The function `getSupportedStates()` will return an `Array` of `String` values that are the known states that can be set +on the light. + +Typically you would not need to use this, as the `LightState` object would be used to set the LightState on a Light in +the API, but this can be used to help limit the setting that you can build into a LightState in a UI, or +programmatically. + +An example of this for an Extended Color Light is: +```js +[ + "on", + "bri", + "hue", + "sat", + "effect", + "xy", + "ct", + "alert", + "colormode", + "mode", + "reachable", + "transitiontime", + "bri_inc", + "sat_inc", + "hue_inc", + "ct_inc", + "xy_inc" +] +``` + +An example for a Dimmable Light is: +```js +[ + "on", + "bri", + "alert", + "mode", + "reachable", + "transitiontime", + "bri_inc" +] +``` + +## toString() +The `toString()` function will obtain a simple `String` representation of the Light. + +e.g. +```text +Light + id: 10 +``` + +## toStringDetailed() +The `toStringDetailed()` function will obtain a more detailed representation of the Light object. + +e.g. +```text +Light + id: 14 + state: {"on":false,"bri":254,"alert":"select","mode":"homeautomation","reachable":false} + swupdate: {"state":"noupdates","lastinstall":"2018-12-13T20:43:31"} + type: "Dimmable light" + name: "Hallway Entrance" + modelid: "LWB004" + manufacturername: "Philips" + productname: "Hue white lamp" + capabilities: {"certified":true,"control":{"mindimlevel":2000,"maxlumen":750},"streaming":{"renderer":false,"proxy":false}} + config: {"archetype":"sultanbulb","function":"functional","direction":"omnidirectional","startup":{"mode":"powerfail","configured":true}} + uniqueid: "00:17:xx:xx:xx:xx:xx:xx-0b" + swversion: "5.127.1.26420" +``` \ No newline at end of file diff --git a/docs/lights.md b/docs/lights.md index 8053cea..402028b 100644 --- a/docs/lights.md +++ b/docs/lights.md @@ -2,18 +2,26 @@ The `lights` API provides a means of interacting with the lights in Hue Bridge. +* [Light Object](#light-object) * [getAll()](#getAll) -* [getLightById(id)](#getlightbyid) +* [getLight()](#getlight) +* [getLightById(id)](#getlightbyid): Deprecated * [getLightByName(name)](#getlightbyname) * [searchForNew()](#searchfornew) * [getNew()](#getnew) * [getLightAttributesAndState(id)](#getlightattributesandstate) * [getLightState(id)](#getlightstate) * [setLightState(id, state)](#setlightstate) -* [rename(id, name)](#rename) +* [renameLight(light)](#renamelight) * [deleteLight(id)](#deletelight) +## Light Object +Any function calls that will return an instance of a Light, will return a [`Light`](./light.md) instance. +The `Light` object provides useful getters and setters for interacting with the Light. Consult the [documentation](./light.md) +for specifics. + + ## getAll() The `getAll()` function allows you to get all the lights that the Hue Bridge has registered with it. @@ -32,20 +40,26 @@ A complete code sample for this function is available [here](../examples/v3/ligh -## getLightById() -The `getLightById(id)` function allows you to retrieve a specific light by it's ID value. +## getLight() +The `getLight(id)` function allows you to retrieve a specific light. + +* `id`: The `id` number or a `Light` instance to retrieve from the bridge. ```js -api.lights.getLightById(id) +api.lights.getLight(id) .then(light => { // Display the details of the light console.log(light.toStringDetailed()); }); ``` -This function call will resolve to a single `Light` instance. +This function call will return a Promise that will resolve to a single `Light` instance. + +A complete code sample for this function is available [here](../examples/v3/lights/getLight.js). -A complete code sample for this function is available [here](../examples/v3/lights/getLightById.js). + +## getLightById() +This API `getLightById(id)` has bee deprecated, use [`getLight(id)`](#getlight) instead. @@ -62,7 +76,7 @@ api.lights.getLightByName(name) This function call will resolve to a single `Light` instance. -A complete code sample for this function is available [here](../examples/v3/lights/getLightById.js). +A complete code sample for this function is available [here](../examples/v3/lights/getLight.js). @@ -271,12 +285,12 @@ A complete code sample for this function is available [here](../examples/v3/ligh -## rename() +## renameLight() -The `rename(id, name)` function allows you to rename the light identified by the `id` to the specified `name` value. +The `renameLight(id, name)` function allows you to rename the light identified by the `id` to the specified `name` value. ```js -api.lights.rename(id, 'my_new_name') +api.lights.renameLight(id, 'my_new_name') .then(result => { console.log(`Successfully reanmed light? ${result}`); }); diff --git a/docs/resourcelinks.md b/docs/resourcelinks.md index f7fdeb8..4b988dc 100644 --- a/docs/resourcelinks.md +++ b/docs/resourcelinks.md @@ -9,7 +9,8 @@ See [`ResourceLink`s](./resourceLink.md) for more details on the `ResourceLink` * [getAll()](#getall) -* [get(id)](#get) +* [getResourceLink(id)](#getresourcelink) +* [getResourceLinkByName(name)](#getresourcelinkbyname) * [createResourceLink()](#createresourcelink) * [updateResouceLink()](#updateresourcelink) * [deleteResourceLink()](#deleteresourcelink) @@ -34,14 +35,35 @@ A complete code sample for this function is available [here](../examples/v3/reso -## get() -The `get(id)` function allows a specific `ResourceLink` to be retrieved from the Hue Bridge. +## getResourceLink() +The `getResourceLink(id)` function allows a specific `ResourceLink` to be retrieved from the Hue Bridge. * `id`: The `String` id of the `ResourceLink` to retrieve. ```js -api.resourceLinks.get(62738) +api.resourceLinks.getResourceLink(62738) + .then(resourceLink => { + console.log(resourceLink.toStringDetailed()); + }) +; +``` + +This function call will resolve to a `ResourceLink` object for the specified `id`. + +If the `ResourceLink` cannot be found an `ApiError` will be returned with a `getHueErrorType()` value of `3`. + +A complete code sample for this function is available [here](../examples/v3/resourceLinks/getResourceLink.js). + + +## getResourceLinkByName() +The `getResourceLinkByName(name)` function will retrieve all `ResourceLink` instances that match the provided name from the Hue Bridge. + +* `name`: The `String` name of the `ResourceLink` to retrieve. + + +```js +api.resourceLinks.getResourceLink(62738) .then(resourceLink => { console.log(resourceLink.toStringDetailed()); }) diff --git a/docs/rules.md b/docs/rules.md index e8b317e..46805c2 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -1,6 +1,6 @@ # Rules API -The `rules` API provides a measn of interacting with Rules in the Hue Bridge. +The `rules` API provides a manes of interacting with Rules in the Hue Bridge. Rules are complex event triggers that consist of a one or more conditions that must be satisfied, which when they are will trigger one or more actions for devices connected to the bridge. @@ -15,7 +15,8 @@ The Rules API interacts with specific [`Rule`](./rule.md) objects from the Bridg * [getAll()](#getall) -* [(get(id)](#get) +* [getRule(id)](#getrule) +* [getRuleByName(name)](#getrulebyname) * [createRule(rule)](#createrule) * [deleteRule(id)](#deleterule) * [updateRule(rule)](#updaterule) @@ -39,14 +40,14 @@ This function call will resolve to an `Array` of `Rule` objects. A complete code sample for this function is available [here](../examples/v3/rules/getAllRules.js). -## get() -The `get(id)` function will obtain the specified Rule with the given `id`. +## getRule() +The `getRule(id)` function will obtain the specified Rule with the given `id`. -* `id`: The id fo the rule to get from the Hue Bridge. +* `id`: The id for the rule, or a `Rule` instance to get from the Hue Bridge. ```js -api.rules.get(1) - .then(allRules => { +api.rules.getRule(1) + .then(rule => { // Display the Rule console.log(rule.toStringDetailed()); }); @@ -57,6 +58,26 @@ This function will return a `Rule` object for the specified `id`. A complete code sample for this function is available [here](../examples/v3/rules/getRule.js). +## getRuleByName() +The `getRuleByName(name)` function will obtain all the `Rule`s from the bridge that have the specified `name`. + +* `name`: The name of the `Rule`s to get from the Hue Bridge. + +```js +api.rules.getRuleByName('Opened door') + .then(allRules => { + // Display the Rules + allRules.forEach(rule => { + console.log(rule.toStringDetailed()); + }); + }); +``` + +This function will return an `Array` of `Rule` objects for all of the `Rule`s that matched the specided `name`. + +A complete code sample for this function is available [here](../examples/v3/rules/getRuleByName.js). + + ## createRule() The `createRule(rule)` function will create a new `Rule` in the Hue Bridge. diff --git a/docs/scenes.md b/docs/scenes.md index a6ae5d3..fde004a 100644 --- a/docs/scenes.md +++ b/docs/scenes.md @@ -14,12 +14,12 @@ Bridge will be 102. * [getAll()](#getall) -* [get(id)](#get) -* [getByName(name)](#getbyname) +* [getScene(id)](#getscene) +* [getSceneByName(name)](#getscenebyname) * [createScene()](#createscene) -* [update()](#update) +* [updateScene()](#updatescene) * [updateLightState()](#updatelightstate) -* [delete()](#delete) +* [deleteScene()](#deletescene) * [activateScene](#activatescene) @@ -40,21 +40,21 @@ A complete code sample for this function is available [here](../examples/v3/scen -## get() -The `get(id)` function allows a specific scene to be retrieved from the Hue Bridge. +## getScene() +The `getScene(id)` function allows a specific scene to be retrieved from the Hue Bridge. -* `id`: The `String` id of the scene to retrieve. +* `id`: The `String` id of the scene to retrieve or a previous `Scene` instance obtained from the bridge. ```js -api.scenes.get('GfOL56sqKPGmPer') +api.scenes.getScene('GfOL56sqKPGmPer') .then(scene => { console.log(scene.toStringDetailed()); }) ; ``` -This function call will resolve to a `Scene` object for the specifed scene `id`. +This function call will resolve to a `Scene` object for the specified scene `id`. If the Scene cannot be found an `ApiError` will be returned with a `getHueErrorType()` value of `3`. @@ -62,13 +62,13 @@ A complete code sample for this function is available [here](../examples/v3/scen -## getByName() -The `getByName(name)` function will find all the scenes that are stored in the bridge with the specified `name`. +## getSceneByName() +The `getSceneByName(name)` function will find all the scenes that are stored in the bridge with the specified `name`. * `name`: The `String` that represents the name of the `Scene`s that you wish to find. ```js -api.scenes.getByName('Concentrate') +api.scenes.getSceneByName('Concentrate') .then(results => { // Do something with the scenes we found results.forEach(scene => { @@ -87,7 +87,7 @@ A complete code sample for this function is available [here](../examples/v3/scen ## createScene() The `createScene(scene)` function allows for the creation of new `Scene`s in the Hue Bridge. -* `scene`: A `Scene` object that has been configured with the desired settings for rhe scene being created. +* `scene`: A `Scene` object that has been configured with the desired settings for the scene being created. ```js const scene = v3.model.createLightScene(); @@ -110,17 +110,17 @@ A complete code sample for this function is available [here](../examples/v3/scen -## update() -The `update(id, scene)` function allows you to update an existing `Scene` in the Hue Bridge. +## updateScene() +The `update(scene)` function allows you to update an existing `Scene` in the Hue Bridge. -* `id`: A `String` value of the id for the scene to update -* `scene`: A `Scene` object that contains the relevant updated data to apply to the existing scene. +* `scene`: The `Scene` object that contains the relevant updated data to apply to the existing scene. ```js -const scene = new Scene(); +// The scene would have be retrieved from the bridge using some other call. +const scene; scene.name = 'Updated scene name'; -api.scenes.update('GfOL56sqKPGmPer', scene) +api.scenes.updateScene(scene) .then(updated => { console.log(`Updated scene properties: ${JSON.stringify(updated)}`); }) @@ -184,13 +184,13 @@ For example if you passed a `SceneLightState` that updated the `on` and `bri` at A complete code sample for this function is available [here](../examples/v3/scenes/updateSceneLightState.js). -## delete() -The `delete(id)` function will delete the specified scene identified by the `id` from the Hue Bridge. +## deleteScene() +The `deleteScene(id)` function will delete the specified scene identified by the `id` from the Hue Bridge. -* `id`: The `id` of the scene to delete from the Hue Bridge. +* `id`: The `id` of the Scene or a `Scene` instance to delete from the Hue Bridge. ```js -api.scenes.delete('abc170f') +api.scenes.deleteScene('abc170f') .then(result => { console.log(`Deleted scene? ${result}`); }) diff --git a/docs/schedule.md b/docs/schedule.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/schedules.md b/docs/schedules.md new file mode 100644 index 0000000..baee7a9 --- /dev/null +++ b/docs/schedules.md @@ -0,0 +1,167 @@ +# Schedules API + +The `schedules` API provides a means of interacting with the `Schedule`s in the Hue Bridge. + +The Schedules API interacts with [`Schedule`](./schedule.md) objects along with their associated +[Time Patterns](./timePatterns.md). + + +* [getAll()](#getall) +* [getSchedule(id)](#getschedule) +* [getScheduleByName(name)](#getschedulebyname) +* [createSchedule()](#createschedule) +* [updateSchedule()](#updateschedule) +* [deleteSchedule()](#deleteschedule) + + +## getAll() +The `getAll()` function allows you to get all the `Schedule`s that the Hue Bridge has registered with it. + +```js +api.schedules.getAll() + .then(allSchedules => { + // Display the Schedules from the bridge + allSchedules.forEach(schedule => { + console.log(schedule.toStringDetailed()); + }); + }); +``` + +This function call will resolve to an `Array` of `Schedule` objects. + +A complete code sample for this function is available [here](../examples/v3/schedules/getAllSchedules.js). + + + +## getSchedule() +The `getSchedule(id)` function allows a specific `Schedule` to be retrieved from the Hue Bridge. + +* `id`: The `id` of the `Schedule` or a `Schedule` instance that was previously obtained from the bridge. + + +```js +api.schedules.getSchedule(1) + .then(schedule => { + console.log(schedule.toStringDetailed()); + }) +; +``` + +This function call will resolve to a `Schedule` object for the specified schedule `id`. + +If the Scene cannot be found an `ApiError` will be returned with a `getHueErrorType()` value of `3`. + +A complete code sample for this function is available [here](../examples/v3/schedules/getScheduleById.js). + + + +## getScheduleByName() +The `getScheduleByName(name)` function will find all the `Schedule`s that are stored in the bridge with the specified `name`. + +* `name`: The `String` that represents the name of the `Schedules`s that you wish to find. + +```js +api.schedules.getScheduleByName('Wake Up') + .then(results => { + // Do something with the schedules we matched + results.forEach(scene => { + console.log(schedule.toStringDetailed()); + }); + }) +; +``` + +The function will resolve to an `Array` of `Schedule` Objects that were matched to the specified `name`. It none are +matched the `Array` will be empty. + +A complete code sample for this function is available [here](../examples/v3/schedules/getSchedulesByName.js). + + + +## createSchedule() +The `createSchedule(schedule)` function allows for the creation of new `Schedule`s in the Hue Bridge. + +* `schedule`: A `Schedule` object that has been configured with the desired settings for the `Schedule` being created. + +```js +const model = require('node-hue-api').v3.model; + +const schedule = model.createSchedule(); +schedule.name = 'My Schedule'; +schedule.description = 'A test schedule from the node-hue-api examples'; +// trigger the schedule in 1 hour from now +schedule.localtime = model.timePatterns.createTimer().hours(1); +// Turn all the lights off (using light group 0 for all lights) +schedule.command = model.actions.group(0).withState(new model.lightStates.GroupLightState().off()); + +api.schedules.createSchedule(schedule) + .then(createdSchedule => { + console.log(`Successfully created Schedule\n${createdSchedule.toStringDetailed()}`); + }) +; +``` + +The function will return a Promise that will resolve with a corresponding `Schedule` with a populated `id` attribute. + +A complete code sample for this function is available [here](../examples/v3/schedule/createSchedule.js). + + + +## updateSchedule() +The `updateSchedule(schedule)` function will update the schedule in the bridge to match the attributes of the specified +schedule. + +* `schedule`: The schedule with updated attributes to set on the bridge. + +```js +// Obtain a schedule from the Bridge, e.g. get schedule with id = 1 +const mySchedule = await hue.schedules.get(1); + +// Update some attributes +mySchedule.name = 'Updated Name'; + +// Update the schedule in the bridge +hue.schedules.updateSchedule(mySchedule) + .then(updateResult => { + console.log(`Updated Name? ${updateResult.name}`); // Will print "Updated Name? true" + }); +``` + +_Note: Currently there is no checking as to whether or not a value has been modified, so all the updatable attributes are +passed to the bridge. (this is quicker and more efficient than doing a get/put chain of requests)._ + +The function call will return a `Promise` that will resolve to an `Object` with the update information. + +The result Object that will have the form of the keys that were updated along with a boolean flag +indicating if the value was modified. +```js +{ + "name": true, + "description": true, + "command": true, + "localtime": true, + "status": true, + "autodelete": true +} +``` + +A complete code sample for this function is available [here](../examples/v3/schedule/updateSchedule.js). + + + +## deleteSchedule() +The `deleteSchedule(id)` function will delete the specified scene identified by the `id` from the Hue Bridge. + +* `id`: The `id` of the scene to delete from the Hue Bridge. + +```js +api.scenes.delete('abc170f') + .then(result => { + console.log(`Deleted scene? ${result}`); + }) +; +``` + +The call will resolve to a `Boolean` indicating the success status of the deletion. + +A complete code sample for this function is available [here](../examples/v3/scenes/deleteScene.js). \ No newline at end of file diff --git a/docs/sensors.md b/docs/sensors.md index 2c20bdd..d73f436 100644 --- a/docs/sensors.md +++ b/docs/sensors.md @@ -4,14 +4,15 @@ The sensors API allows you to interact with the sensors features of the Hue Brid * [Sensor Objects](#sensors) * [getAll()](#getall) -* [get(id)](#get) +* [getSensor(id)](#getsensor) * [searchForNew()](#searchfornew) * [getNew()](#getnew) -* [updateName()](#updatesensorname) -* [createSensor()](#createsensor) -* [deleteSensor()](#deletesensor) -* [updateSensorConfig()](#updatesensorconfig) -* [updateSensorState()](#updatesensorstate) +* [renameSensor(sensor)](#renamesensor) +* [updateName()](#updatename) +* [createSensor(sensor)](#createsensor) +* [deleteSensor(id)](#deletesensor) +* [updateSensorConfig(sensor)](#updatesensorconfig) +* [updateSensorState(sensor)](#updatesensorstate) ## Sensors @@ -41,12 +42,12 @@ A complete code sample for getting all sensors is available [here](../examples/v -## get() -The `get(id)` function will obtain the sensor identified by the specified `id` value. +## getSensor() +The `getSensor(id)` function will obtain the sensor identified by the specified `id` value. ```js // Get the daylight sensor for the bridge, at id 1 -api.sensors.get(1) +api.sensors.getSensor(1) .then(sensor => { console.log(sensor.toStringDetailed()); }) @@ -99,17 +100,18 @@ The return `Object` has the following properties: A complete code sample is available [here](../examples/v3/sensors/getNewSensors.js). - -## updateName() -The `updateName(id, name)` function will allow you to rename an existing Sensor in the Hue Bridge. +## renameSensor() +The `renameSensor(sensor)` function will allow you to rename an existing Sensor in the Hue Bridge. The parameters are: -* `id`: The id of the sensor that you want to rename -* `name`: The new name for the sensor identified by the `id` value. +* `sensor`: The updated `Sensor` object with the changed name. ```js -api.sensors.updateName(sensorId, newName) +// The sensor would have been previously obtained from the bridge. +sensor.name = 'Updated Sensor Name'; + +api.sensors.renameSensor(sensor) .then(result => { console.log(`Updated Sensor Name? ${result}`) }); @@ -117,9 +119,13 @@ api.sensors.updateName(sensorId, newName) The result from the function call will be a `Boolean` indicating the success status of the renaming action. -A complete code sample is available [here](../examples/v3/sensors/updateSensorName.js). +A complete code sample is available [here](../examples/v3/sensors/renameSensor.js). +## updateName() +The `updateName(id, name)` function will allow you to rename an existing Sensor in the Hue Bridge. +This has been deprecated, use [`reanmeSesnor(sensor)`](#renamesensor) instead. + ## createSensor() The `createSensor(sensor)` function allows you to create software backed `CLIP` sensors. diff --git a/docs/timePatterns.md b/docs/timePatterns.md new file mode 100644 index 0000000..055c6e7 --- /dev/null +++ b/docs/timePatterns.md @@ -0,0 +1,549 @@ +# Time Patterns + +The Hue Bridge supports at least 10 different time patterns used in scheduling. The `v3.model.timePatterns` module +provides the means of building these patterns in a user friendly way. + +* [Supported Time Formats](#supported-time-formats) + * [Absolute Time](#absolute-time) + * [Randomized Time](#randomized-time) + * [Recurring Time](#recurring-time) + * [Recurrring Randomized Time](#recurring-randomized-time) + * [Time Interval](#time-interval) + * [Timer](#timer--expiring-timer) + * [Recurring Timer](#recurring-timer) + * [Randomized Timer](#randomized-timer) + * [Recurring Randomized Timer](#recurring-randomized-timer) +* [model.timePatterns](#modeltimepatterns) + + + +## Supported Time Formats +Each of the following time formats, detailed below are supported in `Schedules` for the Hue Bridge. You can either go +hardcore and define them as `String`s as per their format, or use the various properties of the class to compose your +desired time pattern. + + +### Absolute Time +An absolute time is an exact date time of the format `[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]`. + +#### Creating an Absolute Time +You can create an `AbsoluteTime` instance using: + +* `timePatterns.createAbsoluteTime(value)`: value is optional and is used to initialize the AbsoluteTime if specified. It can be a `String`, `Date` or another `AbsoluteTime` + +When using no parameters in the instantiation call the `AbsoluteTime` will set itself to todays date with a time of `00:00:00`. + +```js +// A Defaulted time using today's date and 00:00:00 for the time +const myTime = timePatterns.createAbsoluteTime(); + +// A time built from a date object +const timeFromDate = timePatterns.createAbsoluteTime(new Date()) + , timeFromDate_2 = timePatterns.createAbsoluteTime(new Date('4 August 1977 00:00:00 GMT')) +; + +// A string that is compatible with the absolute time format +const timeFromString = timePatterns.createAbsoluteTime('2019-11-01T12:30:00') +``` + +The `AbsoluteTime` object has the following functions available to configure it: + +* `year(value)`: Will set the year to the specified value, requires a 4 digit year, e.g. 2019 +* `month(value)`: Will set the month to the specified value in digits, 1 based, so `1` is Janurary, `2` February and so on +* `day(value)`: Will set the day of the month to the specified value in digits, 1 to 31 +* `hours(value)`: Will set the hours of the day, 0 to 23 +* `minutes(value)`: Will set the minutes, 0 to 59 +* `seconds(value)`: Will set the seconds, 0 to 59 +* `toString()`: Will generate the AbsoluteTime in the bridge format of `[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]` + +All the above setters will return the `AbsoluteTime` instance so that you can chain function calls. + +```js +// Create an AbsoluteTime using fluent function +const myTime = timePatterns.createAbsoluteTime().year(2019).month(11).day(24); + +console.log(myTime.toString()); // Outputs 2019-11-24T00:00:00 +``` + + + +### Randomized Time +A randomized time is an absolute time with a specification of an element of randomness appended to the end of it. It +is of the form `[YYYY]:[MM]:[DD]T[hh]:[mm]:[ss]A[hh]:[mm]:[ss]`. + +The amount of randomness is controlled by the settings of the hours/minutes/seconds of the random value. A maximum of +23 hours is all the that Hue Bridge will allow. + +For example you may want to trigger a schedule at midday 12:00:00, but have it do so randomly by 30 minutes around that +time, to give the appearance of a human being at home when you are not. + + +#### Creating a Randomized Time +You can create a `RandomizedTime` instance using: + +* `timePatterns.createRandomizedTime(value)`: value is optional and is used to configure the date/time component + +When specifying no parameters in the instantiation call the `RandomizedTime` will set itself to todays date with a time +of `00:00:00` and effectively no randomness (zero seconds, minutes and hours). + + +```js +// A defaulted randomized time, today's date and T00:00:00A00:00:00 values +const myRandomTime = timePatterns.createRandomizedTime(); + +// A randomized time built from a date object +const randomTimeFromDate = timePatterns.createRandomizedTime(new Date()) + , randomTimeFromDate_2 = timePatterns.createRandomizedTime(new Date('12 December 2019 23:00:00 GMT')) +; + +// A string that is compatible with the randomized time format (10 minutes of random) +const timeFromString = timePatterns.createAbsoluteTime('2019-11-01T12:30:00A00:10:00') +``` + +The `RandomizedTime` object has the following functions available to configure it: + +* `year(value)`: Will set the year to the specified value, requires a 4 digit year, e.g. 2019 +* `month(value)`: Will set the month to the specified value in digits, 1 based, so `1` is Janurary, `2` February and so on +* `day(value)`: Will set the day of the month to the specified value in digits, 1 to 31 +* `hours(value)`: Will set the hours of the day, 0 to 23 +* `minutes(value)`: Will set the minutes, 0 to 59 +* `seconds(value)`: Will set the seconds, 0 to 59 +* `randomHours(value)`: Will set the random hours of the day, 0 to 23 +* `randomMinutes(value)`: Will set the random minutes, 0 to 59 +* `randomSeconds(value)`: Will set the random seconds, 0 to 59 +* `toString()`: Will generate the RandomizedTime in the bridge format of `[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]A[hh]:[mm]:[ss]` + +All the above setters will return the `RandomizedTime` instance so that you can chain function calls. + +```js +const time = timePatterns.createRandomizedTime(); + +time.year(1977).month(12).day(1) + .hours(23).minutes(12).seconds(31) + .randomHours(1).randomMinutes(1).randomSeconds(10) +; + +console.log(time.toString()); // Will print "1977-12-01T23:12:31A01:01:10" +``` + + + +### Recurring Time +A recurring time is a special time that allows you to specify a time of day and which days of the week to match. +It is used to define schedules that would start at the same time of each day that it is configured for. + +It has the form of `W[bbb]/T[hh]:[mm]:[ss]`, where the `bbb` value is a bitmask of the days of the week. +These are defined in the `model.timePatterns.weekdays` Object. + + +#### Creating a RecurringTime +You can create a `RecurringTime` instance using: + +* `timePatterns.createRecurringTime()` + * `()`: no parameters the `RecurringTime` will be defaulted to `ALL` weekdays at a time of `00:00:00` + * `(int)`: a weekdays `integer` parameter, the `RecurringTime` will be set to the weekdays as specified at a time of '00:00:00' + * `(date)`: a Date parameter, the `RecurringTime` will be set to `ALL` weekdays at the time component of the date passed in + * `(string)`: a string parameter will be parsed from the `W[bbb]/T[hh]:[mm]:[ss]`, errors with `ApiError` if not valid + * `(weekdays, date)`: A `RecurringTime` set to the weekdays integer and at a time specified by the date + + +```js +const weekdays = require('node-hue-api').v3.timePatterns.weekdays; + +// A defaulted recurring time, 00:00:00 for all days of the week +const myRecurringTime = timePatterns.createRecurringTime(); + +// A RecurringTime for Monday and Tuesday at 00:00:00 +const recurringTimeFromWeekday = timePatterns.createRecurringTime(weekdays.MONDAY | weekdays.TUESDAY); + +// A RecurringTime for ALL weekdays at the time component of the provided date (using UTC). +const recurringTimeFromDate = timePatterns.createRecurringTime(new Date(Date.now())) + // time component of 12:31:30 + , recurringTimeFromDate = timePatterns.createRecurringTime(new Date('12 December 2019 12:31:30 UTC')) +; + +// A string that is compatible with the randomized time format, All weekdays at 12:30:00 +const timeFromString = timePatterns.createAbsoluteTime('W127/T12:30:00'); + +// A RecurringTime set by weekdays and date (Sunday at 13:01:59) +const timeFromWeekdaysAndDate = timePatterns.createRecurringTime(weekdays.SUNDAY, new Date('2019-12-01T13:01:59')); +``` + +The `RecurringTime` object has the following functions available to configure it: + +* `hours(value)`: Will set the hours of the day, 0 to 23 +* `minutes(value)`: Will set the minutes, 0 to 59 +* `seconds(value)`: Will set the seconds, 0 to 59 +* `weekdays(value)`: Will set the weekdays to the specified value +* `toString()`: Will generate the RecurringTime in the bridge format of `W[bbb]/T[hh]:[mm]:[ss]` + +All the above setters will return the `RecurringTime` instance so that you can chain function calls. + +To access the weekday values that you can used to define the days of the week to trigger on, use the constants from +`timePatterns.weekdays`, which has the following keys: + +* `MONDAY` +* `TUESDAY` +* `WEDNESDAY` +* `THURSDAY` +* `FRIDAY` +* `SATURDAY` +* `SUNDAY` +* `WEEKDAY`: Monday, Tuesday, Wednesday, Thursday and Friday +* `WEEKEND`: Saturday and Sunday +* `ALL`: All of the days + +```js +const time = timePatterns.createRecurringTime(); + +time.hours(23).minutes(12).seconds(31).weekdays(timePatterns.weekdays.MONDAY); + +console.log(time.toString()); // Will print "W064/T23:12:31" +``` + + + + +### Recurring Randomized Time +This is a [`RecurringTime`](#recurring-time), that has an random element append to the end of it. It takes the form of +`W[bbb]/T[hh]:[mm]:[ss]A[hh]:[mm]:[ss]`. + +The amount of randomness is controlled by the settings of the hours/minutes/seconds of the random value. A maximum of +23 hours is all the that Hue Bridge will allow. + + +#### Creating a Recurring Randomized Time +You can create a `RandomizedRecurringTime` instance using: + +* `timePatterns.createRandomizedRecurringTime()` + * `()`: no parameters the `RecurringTime` will be defaulted to `ALL` weekdays at a time of `00:00:00` + * `(int)`: a weekdays `integer` parameter, the `RecurringTime` will be set to the weekdays as specified at a time of '00:00:00' + * `(date)`: a Date parameter, the `RecurringTime` will be set to `ALL` weekdays at the time component of the date passed in + * `(string)`: a string parameter will be parsed from the `W[bbb]/T[hh]:[mm]:[ss]A[hh]:[mm]:[ss]`, errors with `ApiError` if not valid + * `(weekdays, date)`: A `RecurringTime` set to the weekdays integer and at a time specified by the date + +The random aspect of the time is not currently able to be set from the constructor (unless you use the string format) + +```js +const weekdays = require('node-hue-api').v3.timePatterns.weekdays; + +// A defaulted recurring time, 00:00:00 for all days of the week and no randomness +const myRecurringTime = timePatterns.createRecurringTime(); + +// A RecurringTime for Monday and Tuesday at 00:00:00 and no randomness +const recurringTimeFromWeekday = timePatterns.createRecurringTime(weekdays.MONDAY | weekdays.TUESDAY); + +// A RecurringTime for ALL weekdays at the time component of the provided date (using UTC) and no randomness +const recurringTimeFromDate = timePatterns.createRecurringTime(new Date(Date.now())) + // all the days of the week and a time component of 12:31:30 with no randomness + , recurringTimeFromDate = timePatterns.createRecurringTime(new Date('12 December 2019 12:31:30 UTC')) +; + +// A string that is compatible with the randomized time format, All weekdays at 12:30:00 with a randomness of 30 seconds +const timeFromString = timePatterns.createAbsoluteTime('W127/T12:30:00A00:00:30'); + +// A RecurringTime set by weekdays and date (Sunday at 13:01:59) +const timeFromWeekdaysAndDate = timePatterns.createRecurringTime(weekdays.SUNDAY, new Date('2019-12-01T13:01:59')); +``` + +The documentation for the [RecurringTime](#recurring-time) is applicable for understanding the majority of the +properties/functions you can interact with, along with the addition of the following: + +* `randomHours(value)`: Will set the random hours of the day, 0 to 23 +* `randomMinutes(value)`: Will set the random minutes, 0 to 59 +* `randomSeconds(value)`: Will set the random seconds, 0 to 59 +* `toString()`: Will generate the RandomizedRecurringTime in the bridge format of `W[bbb]/T[hh]:[mm]:[ss]A[hh]:[mm]:[ss]` + + +```js +const time = timePatterns.createRecurringRandomizedTime(); + +// Monday at 23:12:31 with a randomness of 10 minutes +time.hours(23).minutes(12).seconds(31) + .weekdays(timePatterns.weekdays.MONDAY) + .randomMinutes(10) +; + +console.log(time.toString()); // Will print "W064/T23:12:31A00:10:00" +``` + + + +### Time Interval +Creates an interval of time, up to a maximum of 23 hours that occurs on one or more weekdays. + +It has the form of `W[bbb]/T[hh]:[mm]:[ss]/T[hh]:[mm]:[ss]`, where the `bbb` value is a bitmask of the days of the week. +These are defined in the `model.timePatterns.weekdays` Object. + + +#### Creating a Time Interval +You can create a `TimeInterval` instance using: + +* `timePatterns.createTimeInterval()` + * `()`: no parameters the `TimeInterval` will be defaulted to `ALL` weekdays with a `from` time of `00:00:00` and a `to` time of '00:00:00' + * `(string)`: a string parameter will be parsed from the `W[bbb]/T[hh]:[mm]:[ss]/T[hh]:[mm]:[ss]`, errors with `ApiError` if not valid + + +```js +// A defaulted time interval, ALL weekdays with a from and to time of 00:00:00 +const myRandomTime = timePatterns.createTimeInterval(); + +// A string that is compatible with the time interval format (ALL weekdays from 12:30:00 to 12:40:00) +const timeFromString = timePatterns.createTimeInterval('W127/T12:30:00/T12:40:00') +``` + + +The `TimeInterval` object has the following functions available to configure it: + +* `from(date)`: Will set the hours, minutes and seconds from the UTC values of the provided `date` for the `from` time +* `fromHours(value)`: Will set the hours of the day, 0 to 23 for the `from` time +* `fromMinutes(value)`: Will set the minutes, 0 to 59 for the `from` time +* `fromSeconds(value)`: Will set the seconds, 0 to 59 for the `from` time +* `to(date)`: Will set the hours, minutes and seconds from the UTC values of the provided `date` for the `to` time +* `toHours(value)`: Will set the hours of the day, 0 to 23 for the `to` time +* `toMinutes(value)`: Will set the minutes, 0 to 59 the `to` time +* `toSeconds(value)`: Will set the seconds, 0 to 59 the `to` time +* `weekdays(value)`: Will set the weekdays to the specified value +* `toString()`: Will generate the TimeInterval in the bridge format of `W[bbb]/T[hh]:[mm]:[ss]/T[hh]:[mm]:[ss]` + + +```js +const time = timePatterns.createTimeInterval(); + +// Monday from 23:12:00 to 23:59:59 +time.weekdays(timePatterns.weekdays.MONDAY) + .fromHours(23).fromMinutes(12) + .toHours(23).toMinutes(12).toSeconds(59) +; + +console.log(time.toString()); // Will print "W064/T23:12:00/T23:59:59" +``` + + +### Timer / Expiring Timer +A timer that will expire in a specified time frame. + +It has the form of `PT[hh]:[mm]:[ss]` and will trigger once the specified time period is up. +For example `PT01:00:00` would trigger in one hour. + +#### Creating a Timer +You can create an `Timer` instance using: + +* `timePatterns.createTimer(value)`: `value` is optional but can be a String in format of `PT[hh]:[mm]:[ss]` or another `Timer` instance + +```js +// A Defaulted Timer "PT00:00:00" +const timer = timePatterns.createTimer(); + +// A Timer for 1 hour +const timerFromString = timePatterns.createTimer('PT01:00:00'); +``` + +The `Timer` can be configured using the following functions: + +* `hours(value)`: Sets the number of hours before the timer triggers, 0 to 23 +* `minutes(value)`: Sets the number of minutes before the timer triggers, 0 to 59 +* `seconds(value)`: Sets the number of seconds before the timer triggers, 0 to 59 +* `toString()`: Will generate the Timer in for the bridge for of `PT[hh]:[mm]:[ss]` + +All the above setters will return the `Timer` instance so that you can chain function calls. + +```js +// Create a timer that will trigger in 60 seconds / 1 minute (PT00:01:00) +const minuteTimer = timePatterns.createTimer().minutes(1); + +// Create a timer that will trigger in 1 hour (PT01:00:00) +const hourTimer = timePatterns.createTimer().hours(1); + + +// Create a timer that will trigger in 1 hour 20 minutes and 30 seconds +const timer = timePatterns.createTimer().hours(1).minutes(20).seconds(30); +``` + + +### Recurring Timer +A RecurringTimer is a timer that will reoccur either a number of times, or continuously, it has the form of +`R[nn]/PT[hh]:[mm]:[ss]` reoccurring `[nn]` times or `R/PT[hh]:[mm]:[ss]`, reoccurring continuously. + + +#### Creating a Recurring Timer +You can create an `RecurringTimer` instance using: + +* `timePatterns.createRecurringTimer(value)`: `value` is optional but can be a String in format of `PT[hh]:[mm]:[ss]` or another `Timer` instance + +```js +// A timer that is defaulted to R/PT00:00:00 +const recurringTimer = timepatterns.createRecurringTimer(); + +// A timer that will repeat every 10 minutes R/PT00:10:00 indefinitely +const timerFromString = timepatterns.createRecurringTimer('R/PT00:10:00') +``` + + +The `RecurringTimer` can be configured using the following functions: + + * `hours(value)`: Sets the number of hours before the timer triggers, 0 to 23 + * `minutes(value)`: Sets the number of minutes before the timer triggers, 0 to 59 + * `seconds(value)`: Sets the number of seconds before the timer triggers, 0 to 59 + * `reoccurs(value)`: Will reoccur for exactly `value` times, 0 to 99, 0 meaning it will continue to repeat forever. + * `toString()`: Will generate the Timer in for the bridge for of `PT[hh]:[mm]:[ss]` + +All the above setters will return the `Timer` instance so that you can chain function calls. + +```js +// Create a RecurringTimer that will trigger every 60 seconds / 1 minute (R/PT00:01:00) +const minuteTimer = timePatterns.createRecurringTimer().minutes(1); + +// Create a timer that will trigger in 1 hour (R03/PT01:00:00) for three times then stop +const hourTimer = timePatterns.createTimer().hours(1).reoccurs(3); + + +// Create a RecurringTimer that will trigger every 1 hour 20 minutes and 30 seconds 99 times and then stop +const timer = timePatterns.createTimer().hours(1).minutes(20).seconds(30).reoccurs(99); +``` + + + +### Randomized Timer +A `RandomizedTimer` is a `Timer` with a random element as to when it will trigger. + +It has the form of `PT[hh]:[mm]:[ss]A[hh]:[mm]:[ss]` and will trigger once around the time specified within the random timeframe. +For example `PT01:00:00A00:00:30` would trigger in one hour wityh 30 seconds of randomness. + +#### Creating a RandomizedTimer +You can create an `RandomizedTimer` instance using: + +* `timePatterns.createRandomizedTimer(value)`: `value` is optional but can be a String in format of `PT[hh]:[mm]:[ss]A[hh]:[mm]:[ss]` + or another `RandomizedTimer` instance + +```js +// A Defaulted Timer "PT00:00:00A00:00:00" +const timer = timePatterns.createRandomizedTimer(); + +// A Timer for 1 hour with 10 minutes of randomness +const timerFromString = timePatterns.createRandomizedTimer('PT01:00:00A00:10:00'); +``` + +The `RandomizedTimer` can be configured using the following functions: + +* `hours(value)`: Sets the number of hours before the timer triggers, 0 to 23 +* `minutes(value)`: Sets the number of minutes before the timer triggers, 0 to 59 +* `seconds(value)`: Sets the number of seconds before the timer triggers, 0 to 59 +* `randomHours(value)`: Will set the random hours of the day, 0 to 23 +* `randomMinutes(value)`: Will set the random minutes, 0 to 59 +* `randomSeconds(value)`: Will set the random seconds, 0 to 59 +* `toString()`: Will generate the Timer in for the bridge for of `PT[hh]:[mm]:[ss]A[hh]:[mm]:[ss]` + +All the above setters will return the `Timer` instance so that you can chain function calls. + +```js +// Create a timer that will trigger in 60 seconds / 1 minute with 5 seconds of randomness (PT00:01:00A00:00:05) +const minuteTimer = timePatterns.createRandomizedTimer().minutes(1).randomSeconds(5); + +// Create a timer that will trigger in 1 hour with randomness of 2 minutes (PT01:00:00A00:02:00) +const hourTimer = timePatterns.createRandomizedTimer().hours(1).randomMinutes(2); + + +// Create a timer that will trigger in 1 hour 20 minutes and 30 seconds with 30 minutes and 10 seconds of randomness +const timer = timePatterns.createRandomizedTimer().hours(1).minutes(20).seconds(30).randomMinutes(30).randomSeconds(10); +``` + + + + +#### Recurring Randomized Timer +A `RecurringRandomizedTimer` is a `Timer` that will trigger at the specified time (allowing for an element of randomness) +for a number of times (or forever, depneding upon the reoccurrance setting). + +It has the form of `R/PT[hh]:[mm]:[ss]A[hh]:[mm]:[ss]` for timers that have no limit on the number of times it reoccurs or +`R[nn]/PT[hh]:[mm]:[ss]A[hh]:[mm]:[ss]` where `[nn]` is the number of times it will reoccur before expiring. + + +### Creating a RecurringRandomizedTimer +You can create an `RecurringRandomizedTimer` instance using: + +* `timePatterns.createRecurringRandomizedTimer(value)`: `value` is optional but can be a String in format of `R[nn]/PT[hh]:[mm]:[ss]A[hh]:[mm]:[ss]` + or `R/PT[hh]:[mm]:[ss]A[hh]:[mm]:[ss]` or another `RecurringRandomizedTimer` instance + +```js +// A Defaulted Timer "R/PT00:00:00A00:00:00" +const timer = timePatterns.createRecurringRandomizedTimer(); + +// A Timer for 1 hour with 10 minutes of randomness +const timerFromString = timePatterns.createRecurringRandomizedTimer('R/PT01:00:00A00:10:00'); +``` + +The `RecurringRandomizedTimer` can be configured using the following functions: + +* `hours(value)`: Sets the number of hours before the timer triggers, 0 to 23 +* `minutes(value)`: Sets the number of minutes before the timer triggers, 0 to 59 +* `seconds(value)`: Sets the number of seconds before the timer triggers, 0 to 59 +* `randomHours(value)`: Will set the random hours of the day, 0 to 23 +* `randomMinutes(value)`: Will set the random minutes, 0 to 59 +* `randomSeconds(value)`: Will set the random seconds, 0 to 59 +* `reoccurs(value)`: Will reoccur for exactly `value` times, 0 to 99, 0 meaning it will continue to repeat forever. +* `toString()`: Will generate the Timer in for the bridge for of `PT[hh]:[mm]:[ss]A[hh]:[mm]:[ss]` + +All the above setters will return the `Timer` instance so that you can chain function calls. + +```js +// Create a timer that will trigger in 60 seconds / 1 minute with 5 seconds of randomness (PT00:01:00A00:00:05) +const minuteTimer = timePatterns.createTimer().minutes(1).randomSeconds(5); + +// Create a timer that will trigger in 1 hour with randomness of 2 minutes (PT01:00:00A00:02:00) +const hourTimer = timePatterns.createTimer().hours(1).randomMinutes(2); + + +// Create a timer that will trigger in 1 hour 20 minutes and 30 seconds with 30 minutes and 10 seconds of randomness +const timer = timePatterns.createTimer().hours(1).minutes(20).seconds(30).randomMinutes(30).randomSeconds(10); +``` + + + +## model.timePatterns + +### weekdays +This provides access to the definitions of the bitmask values the weekdays property of a `RecurringTime` or +`RandomizedRecurringTime`. + +* `MONDAY` +* `TUESDAY` +* `WEDNESDAY` +* `THURSDAY` +* `FRIDAY` +* `SATURDAY` +* `SUNDAY` +* `WEEKDAY`: Monday, Tuesday, Wednesday, Thursday and Friday +* `WEEKEND`: Saturday and Sunday +* `ALL`: All of the days + +When setting the weekdays, you will have to bitwise or them using `|`. + +For example to create a bitmask for Monday, Wednesday and Friday you would use the following: + +```js +const weekdays = require('node-hue-api').v3.model.timePatterns.weekdays; + +// Creating a bitmask for Monday, Wednesday and Friday only +const myWeekdayBitmask = weekdays.MONDAY | weekdays.WEDNESDAY | weekdays.FRIDAY; +``` + + + +### isRecurring() + +### isTimePattern() + +### createAbsoluteTime() + +### createRandomizedTime() + +### createRecurringTime() + +### createRecurringRandomizedTime() + +### createTimeInterval() + +### createTimer() + + diff --git a/docs/v2_api.md b/docs/v2_api.md deleted file mode 100644 index 6f86638..0000000 --- a/docs/v2_api.md +++ /dev/null @@ -1,2284 +0,0 @@ -# v2 API - -The following is the documentation for the old v2 API. This is provided as a reference for anyone still using the -backwards compatibility [shim](../README.md#2x-backwards-compatibility-shim). - - -## Contents - -- [Finding the Lights Attached to the Bridge](#finding-the-lights-attached-to-the-bridge) -- [Interacting with a Hue Light or Living Color Lamp](#interacting-with-a-hue-light-or-living-color-lamp) -- [Using LightState to Build States](#using-lightstate-to-build-states) -- [Turning a Light On/Off using LightState](#turning-a-light-onoff-using-lightstate) -- [Setting Light States using custom JSON Object](#setting-light-states-using-custom-json-object) -- [Getting the Current Status/State for a Light](#getting-the-current-statusstate-for-a-light) -- [Working with Groups](#working-with-groups) -- [Working with Schedules](#working-with-schedules) -- [Working with scenes](#working-with-scenes) - - -### Locating a Philips Hue Bridge -There are two functions available to find the Phillips Hue Bridges on the network ``nupnpSearch()`` and ``upnpSearch()``. -Both of these methods are useful if you do not know the IP Address of the bridge already. - -The official Hue documentation recommends an approach to finding bridges by using both UPnP and N-UPnP in parallel -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 meethue portal), -in which case you can fall back to the slower -``upnpSearch()`` function. - -This function is considerably faster to resolve the bridges < 500ms compared to 5 seconds to perform a full search on my -own network. - -```js -var hue = require("node-hue-api"); - -var displayBridges = function(bridge) { - console.log("Hue Bridges Found: " + JSON.stringify(bridge)); -}; - -// -------------------------- -// Using a promise -hue.nupnpSearch().then(displayBridges).done(); - -// -------------------------- -// Using a callback -hue.nupnpSearch(function(err, result) { - if (err) throw err; - displayBridges(result); -}); -``` - -The results from this call will be of the form; -``` -Hue Bridges Found: [{"id":"001788fffe096103","ipaddress":"192.168.2.129","name":"Philips Hue","mac":"00:00:00:00:00"}] -``` - - -#### upnpSearch or searchForBridges() -This API function utilizes a network scan for the SSDP responses of devices on a network. It is the only method that does not -support callbacks, and is only in the API as a fallback since Phillips provided a quicker discovery method once the API was -officially released. - -```js -var hue = require("node-hue-api"), - timeout = 2000; // 2 seconds - -var displayBridges = function(bridge) { - console.log("Hue Bridges Found: " + JSON.stringify(bridge)); -}; - -hue.upnpSearch(timeout).then(displayBridges).done(); -``` -A timeout can be provided to the function to increase/decrease the amount of time that it waits for responses from the -search request, by default this is set to 5 seconds (the above example sets this to 2 seconds). - -The results from this function call will be of the form; -``` -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 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 -now do this via the API too if you already have an existing user account on the Bridge). - -This library offer two functions to register new devices/users with the Hue Bridge. These are detailed below. - - -### Bridge Configuration -You can obtain a summary of the configuration of the Bridge using the ``config()`` or ``getConfig()`` functions; - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.config().then(displayResult).done(); -// using getConfig() alias -api.getConfig().then(displayResult).done(); - -// -------------------------- -// Using a callback -api.config(function(err, config) { - if (err) throw err; - displayResult(config); -}); -// using getConfig() alias -api.getConfig(function(err, config) { - if (err) throw err; - displayResult(config); -}); -``` - -This will provide results detailing the configuration of the bridge (IP Address, Name, Link Button Status, Defined Users, etc...); -``` -{ - "name": "Philips hue", - "zigbeechannel": 11, - "bridgeid": "xxxxxxx", - "mac": "00:xx:88:xx:f3:xx", - "dhcp": false, - "ipaddress": "192.168.2.245", - "netmask": "255.255.255.0", - "gateway": "192.168.2.1", - "proxyaddress": "none", - "proxyport": 0, - "UTC": "2017-01-04T20:01:21", - "localtime": "2017-01-04T20:01:21", - "timezone": "Europe/London", - "modelid": "BSB002", - "datastoreversion": "59", - "swversion": "01036659", - "apiversion": "1.16.0", - "swupdate": { - "updatestate": 0, - "checkforupdate": false, - "devicetypes": { - "bridge": false, - "lights": [], - "sensors": [] - }, - "url": "", - "text": "", - "notify": false - }, - "linkbutton": false, - "portalservices": true, - "portalconnection": "connected", - "portalstate": { - "signedon": true, - "incoming": true, - "outgoing": true, - "communication": "disconnected" - }, - "factorynew": false, - "replacesbridgeid": "xxxxxxxxxxx", - "backup": { - "status": "idle", - "errorcode": 0 - }, - "whitelist": { - ... - } -} -``` - -If you invoke the ``config()`` or ``connect()`` functions with an invalid user account (i.e. one that is not valid) then -results of the name and software version will be returned from the bridge with no other information; -``` - - "apiversion": "1.16.0" - "bridgeid": "xxxxxxxxxxx" - "datastoreversion": "59" - "factorynew": false - "mac": "00:xx:88:xx:f3:xx" - "modelid": "BSB002" - "name": "Philips hue" - "replacesbridgeid": "xxxxxxxxxxx" - "swversion": "01036659" -} -``` -For this reason, if you want to validate that the user account used to connect to the bridge is correct, you will have to -look for a field that is not present in the above result. ``zigbeechannel``, ``ipaddress`` or ``linkbutton`` would be good -properties to check. - -//TODO Need to document setting config value and timezones - -### Timezones -To obtain the valid timezones for the bridge, you can use the ``getTimezones()`` or ``timezones()`` function. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.getTimezones().then(displayResult).done(); -// or using 'timezones' alias -api.timezones().then(displayResult).done(); - -// -------------------------- -// Using a callback -api.getTimezones(function(err, config) { - if (err) throw err; - displayResult(config); -}); -// or using 'timezones' alias -api.timezones(function(err, config) { - if (err) throw err; - displayResult(config); -}); -``` - -//TODO setting a time zone - - -### 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` and `version` 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 host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.getVersion().then(displayResult).done(); -// or using 'version' alias -api.version().then(displayResult).done(); - -// -------------------------- -// Using a callback -api.getVersion(function(err, config) { - if (err) throw err; - displayResult(config); -}); -// or using 'version' alias -api.version(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 do not have have an existing user account on the Bridge to use to access its protected functions. - -```js -var HueApi = require("node-hue-api").HueApi; - -var host = "192.168.2.129", - userDescription = "device description goes here"; - -var displayUserResult = function(result) { - console.log("Created user: " + JSON.stringify(result)); -}; - -var displayError = function(err) { - console.log(err); -}; - -var hue = new HueApi(); - -// -------------------------- -// Using a promise -hue.registerUser(host, userDescription) - .then(displayUserResult) - .fail(displayError) - .done(); - -// -------------------------- -// Using a callback (with default description and auto generated username) -hue.createUser(host, function(err, user) { - if (err) throw err; - displayUserResult(user); -}); -``` - -The description for the user account is optional, if you do not provide one, then the default of "Node.js API" will be set. - -There is a convenience method, if you have a existing user account when you register a new user, that will programmatically -press the link button for you. See the details for the function ``pressLinkButton()`` for more details. - - -#### Registration Output/Error -When registering a new user you will get the username created, or an error that will likely be due to not pressing the -link button on the Bridge. - -If the link button was NOT pressed on the bridge, then you will get an ``ApiError`` thrown, which will be captured by the displayError function in the above examples. -``` -Api Error: link button not pressed -``` - -If the link button was pressed you should get a response that will provide you with a hash to use as the username for connecting with the Hue Bridge, e.g. -``` -033a6feb77750dc770ec4a4487a9e8db -``` - - -### Bridge Description -You can obtain the UPnP/Discovery description details of the Bridge using the function ``description()`` or -``getDescription()``. The result of this will be the contents of the `/description.xml` converted into a JSON object. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.description().then(displayResult).done(); -// using alias getDescription() -api.getDescription().then(displayResult).done(); - -// -------------------------- -// Using a callback -api.description(function(err, config) { - if (err) throw err; - displayResult(config); -}); -// using alias getDescription() -api.getDescription(function(err, config) { - if (err) throw err; - displayResult(config); -}); -``` - - -### Validating a Connection to a Philips Hue Bridge -To connect to a Philips Hue Bridge and obtain some basic details about it you can use the any -of the following functions; -* ``config()`` or ``getConfig()`` -* ``version()`` or ``getVersion()`` - -The details of the results of these functions are provided above. - - -### Obtaining the Complete State of the Bridge -If you have a valid user account in the Bridge, then you can obtain the complete status of the bridge using -``fullState()`` or ``getFullState()``. -This function is computationally expensive on the bridge and should not be invoked frequently. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.getFullState().then(displayResult).done(); -// or alias fullState() -api.fullState().then(displayResult).done(); - -// -------------------------- -// Using a callback -api.getFullState(function(err, config) { - if (err) throw err; - displayResult(config); -}); -// or alias fullState() -api.fullState(function(err, config) { - if (err) throw err; - displayResult(config); -}); -``` - -This will produce a JSON response similar to the following (large parts have been removed from the result below); -``` -{ - "lights": { - "5": { - "state": { - "on": false, - "bri": 0, - "hue": 6144, - "sat": 254, - "xy": [ - 0.6376, - 0.3563 - ], - "alert": "none", - "effect": "none", - "colormode": "hs", - "reachable": true - }, - "type": "Color light", - "name": "Living Color TV", - "modelid": "LLC007", - "swversion": "4.6.0.8274", - "pointsymbol": { - "1": "none", - "2": "none", - "3": "none", - "4": "none", - "5": "none", - "6": "none", - "7": "none", - "8": "none" - } - } - }, - "groups": { - "1": { - "action": { - "on": false, - "bri": 63, - "hue": 65527, - "sat": 253, - "xy": [ - 0.6736, - 0.3221 - ], - "ct": 500, - "effect": "none", - "colormode": "ct" - }, - "lights": [ - "1", - "2", - "3" - ], - "name": "NodejsApiTest" - } - }, - "config": { - ... - "whitelist": { - "51780342fd7746f2fb4e65c30b91d7": { - "last use date": "2013-05-29T20:29:51", - "create date": "2013-05-29T20:29:51", - "name": "Node.js API" - }, - "08a902b95915cdd9b75547cb50892dc4": { - "last use date": "1987-01-06T22:53:37", - "create date": "2013-04-02T13:39:18", - "name": "Node Hue Api Tests User" - } - }, - "swversion": "01005825" - ... - }, - "schedules": { - "1": { - "name": "Updated Name", - "description": "Like anyone really needs a wake up on Xmas day...", - "command": { - "address": "/api/08a902b95915cdd9b75547cb50892dc4/lights/5/state", - "body": { - "on": true - }, - "method": "PUT" - }, - "time": "2014-01-01T07:00:30", - "created": "1970-01-01T00:00:00" - } - }, - "scenes": {} -``` - -### Obtaining Registered Users/Devices -To obtain the details for all the registered users/devices for a Hue Bridge you can use the ``registeredUsers()`` function. -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129"; -var username = "08a902b95915cdd9b75547cb50892dc4"; -var api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.registeredUsers().then(displayResult).done(); - -// -------------------------- -// Using a callback -api.registeredUsers(function(err, config) { - if (err) throw err; - displayResult(config); -}); -``` -This will produce a JSON response that has a root key of "devices" that has an array of registered devices/users for the Bridge. An example of the result is shown below -``` -{ - "devices": [ - { - "name": "Node API", - "username": "083b2f780c78555d532b78544f135798", - "created": "2013-01-02T19:17:02", - "accessed": "2012-12-24T20:18:55" - }, - { - "name": "iPad", - "username": "279c26146e3318997d69a8a66330b5f5", - "created": "2012-12-24T14:05:25", - "accessed": "2013-01-04T21:37:29" - }, - { - "name": "iPhone", - "username": "fcb0a47cd664f0cbaa34d36def54577d", - "created": "2012-12-24T17:13:54", - "accessed": "2013-01-03T20:50:40" - } - ] -} -```` - -### Deleting a User/Device -To delete a user or device from the Bridge, you will need an existing user account to authenticate as, and then you can call -``deleteUser()`` or ``unregisterUser()`` to remove a user from the Bridge Whitelist; - -```js -var HueApi = require("node-hue-api").HueApi; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4"; - -var displayUserResult = function(result) { - console.log("Deleted user: " + JSON.stringify(result)); -}; - -var displayError = function(err) { - console.log(err); -}; - -var hue = new HueApi(host, username); - -// -------------------------- -// Using a promise -hue.deleteUser("2b997aae306f15a734d8d1c2315d47cb") - .then(displayUserResult) - .fail(displayError) - .done(); - -// -------------------------- -// Using a callback -hue.unregisterUser("1ab7d44219e64c373b4b915e34494443", function(err, user) { - if (err) throw err; - displayUserResult(user); -}); -``` -Which will result in a ``true`` result if the user was removed, or an error if any other result occurs (i.e. the user does not exist) as shown below; -``` -{ - message: 'resource, /config/whitelist/2b997aae306f15a734d8d1c2315d47cb, not available', - type: 3, - address: '/config/whitelist/2b997aae306f15a734d8d1c2315d47cb' -} -``` - - -## Finding the Lights Attached to the Bridge -To find all the lights that are registered with the Hue Bridge, so that you might be able to interact with them, you can use the ``lights()`` function. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.lights() - .then(displayResult) - .done(); - -// -------------------------- -// Using a callback -api.lights(function(err, lights) { - if (err) throw err; - displayResult(lights); -}); -``` - -This will output a JSON object that will provide details of the lights that the Hue Bridge knows about; -``` -{ - "lights": [ - { - "id": "1", - "name": "Lounge Living Color", - "type": "Extended color light", - "modelid": "LCT001", - "manufacturername": "Phillips", - "uniqueid": "00:17:88:01:xx:xx:xx:xx-xx", - "swversion": "66013452", - "state": { - "on": true, - "bri": 202, - "hue": 11315, - "sat": 237, - "effect": "none", - "xy": [ - 0.5534, - 0.4239 - ], - "alert": "none", - "colormode": "xy", - "reachable": true - } - }, - { - "id": "2", - "name": "Right Bedside", - "type": "Extended color light", - "modelid": "LCT001", - "manufacturername": "Phillips", - "uniqueid": "00:17:88:01:xx:xx:xx:xx-xx", - "swversion": "66013452", - "state": { - "on": true, - "bri": 202, - "hue": 11315, - "sat": 237, - "effect": "none", - "xy": [ - 0.5534, - 0.4239 - ], - "alert": "none", - "colormode": "xy", - "reachable": true - } - }, - { - "id": "3", - "name": "Left Bedside", - "type": "Extended color light", - "modelid": "LCT001", - "manufacturername": "Phillips", - "uniqueid": "00:17:88:01:xx:xx:xx:xx-xx", - "swversion": "66013452", - "state": { - "on": true, - "bri": 202, - "hue": 11315, - "sat": 237, - "effect": "none", - "xy": [ - 0.5534, - 0.4239 - ], - "alert": "none", - "colormode": "xy", - "reachable": true - } - } - ] -} -``` -The `id` values are what you will need to use to interact with the light directly and set the states on it (like on/off, color, etc...). - -## Interacting with a Hue Light or Living Color Lamp -The library provides a function, __setLightState()__, that allows you to set the various states on a light connected to the Hue Bridge. -You can either provide a JSON object that contains the values to set the various state values, or you can use the provided __lightState__ object in the library to build the state object ot pass to the function. See below for examples. - -## Using LightState to Build States -The __lightState__ object provides a fluent way to build up a simple or complex light states that you can pass to a light. - -The majority of the various states that you can set on a Hue Light or Living Color lamp are available from this object. - -### LightState Options -The __lightState__ object provides the following methods that can be used to build various states (all of which can be combined); - -The LightState object, provides functions with the same name of the underlying Hue Bridge API properties for lights, -which take values documented in the official Phillips Hue Lights API: - -| Function | Details | -|:-------------|:---------------------| -| `on(value)` | Sets the `on` state, where the value is `true` or `false`| -| `bri(value)` | Sets the brightness, where value from 0 to 255 | -| `hue(value)` | Sets the hue, where value from 0 to 65535 | -| `sat(value)` | Sets the saturation value from 0 to 255 | -| `xy(x, y)` | Sets the xy value where x and y is from 0 to 1 in the Philips Color co-ordinate system | -| `ct(colorTemperature)` | Set the color temperature to a value between 153 and 500 | -| `alert(value)` | Sets the alert state to value `none`, `select` or `lselect`. If no parameter is passed will default to `none`. | -| `effect(effectName)` | Sets the effect on the light(s) where `effectName` is either `none` or `colorloop`. | -| `transitiontime(int)` | Sets a transition time to a multiple of 100 milliseconds, e.g. 4 means 400ms | -| `bri_inc(value)`| Increments/Decrements the brightness by the value specified. Accepts values -254 to 254. | -| `sat_inc(value)`| Increments/Decrements the saturation by the value specified. Accepts values -254 to 254. | -| `hue_inc(value)`| Increments/Decrements the hue by the value specified. Accepts values -65534 to 65534. | -| `ct_inc(value)` | Increments/Decrements the color temperature by the value specified. Accepts values -65534 to 65534. | -| `xy_inc(value)` | Increments/Decrements the xy co-ordinate by the value specified. Accepts values -0.5 to 0.5. | - -There are also a number of convenience functions to provide extra functionality or a more natural language for building -up a desired Light State: - -| Function | Details | -|:---------|:--------| -| `turnOn()` | Turn the lights on | -| `turnOff()` |Turn the lights off | -| `off()` |Turn the lights off | -| `brightness(percentage)` |Set the brightness from 0% to 100% (0% is not off)| -| `incrementBrightness(value)` |Alias for the `bri_inc()` function above | -| `colorTemperature(ct)` |Alias for the `ct()` function above| -| `colourTemperature(ct)` |Alias for the `ct()` function above| -| `colorTemp(ct)`| Alias for the `ct()` function above| -| `colourTemp(ct)` |Alias for the `ct()` function above| -| `incrementColorTemp(value)` |Alias for the `ct_inc()` function above | -| `incrementColorTemperature(value)` |Alias for the `ct_inc()` function above | -| `incrementColourTemp(value)` |Alias for the `ct_inc()` function above | -| `incrementColourTemperature(value)` |Alias for the `ct_inc()` function above | -| `saturation(percentage)`| Set the saturation as a percentage value between 0 and 100| -| `incrementSaturation(value)` |Alias for the `sat_inc()` function above | -| `incrementXY(value)` |Alias for the `xy_inc()` function above | -| `incrementHue(value)` |Alias for the `hue_inc()` function above | -| `shortAlert()` |Flashes the light(s) once| -| `alertShort()` |Flashes the light(s) once| -| `longAlert()` |Flashes the light(s) 10 times| -| `alertLong()` |Flashes the light(s) 10 times| -| `transitionTime(int)` |Sets a transition time to a multiple of 100 milliseconds, e.g. 4 means 400ms | -| `transition(milliseconds)` |Specify a specific transition time| -| `transitiontime_milliseconds(milliseconds)` | Sets a transition time in milliseconds (will be rounded to the closest 100ms | -| `transitionSlow()` |A slow transition of 800ms| -| `transitionFast()` | A fast transition of 200ms| -| `transitionInstant()` |A transition of 0ms| -| `transitionDefault()` |A transition time of the bridge default (400ms)| -| `white(colorTemp, briPercent)` | where colorTemp is a value between 154 (cool) and 500 (warm) and briPercent is 0 to 100| -| `hsl(hue, sat, luminosity)` | Where hue is a value from 0 to 359, sat is a saturation percent value from 0 to 100, and luminosity is from 0 to 100| -| `hsb(hue, sat, brightness)` | Where hue is a value from 0 to 359, sat is a saturation percent value from 0 to 100, and brightness is from 0 to 100| -| `rgb(r, g, b)` | Sets an RGB value from integers 0-255| -| `rgb([r, g, b])` | Sets an RGB value from an array of integer values 0-255| -| `colorLoop()` | Starts a color loop effect (rotates through all available hues at the current saturation level)| -| `colourLoop()` | Starts a color loop effect (rotates through all available hues at the current saturation level)| -| `effectColorLoop()` | Starts a color loop effect (rotates through all available hues at the current saturation level)| -| `effectColourLoop()` | Starts a color loop effect (rotates through all available hues at the current saturation level)| -| `copy()`| Allows you to create an independent copy of the LightState| -| `reset()` | Will completely reset/remove all provided values| - - -### Creating Complex States -The LightState object provides a simple way to build up JSON object to set multiple values on a Hue Light. - -To turn on a light and set it to a warm white color; -```js -var hue = require("node-hue-api"), - HueApi = hue.HueApi, - lightState = hue.lightState; - -var displayResult = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - state; - -// Set light state to 'on' with warm white value of 500 and brightness set to 100% -state = lightState.create().on().white(500, 100); - -// -------------------------- -// Using a promise -api.setLightState(5, state) - .then(displayResult) - .done(); - -// -------------------------- -// Using a callback -api.setLightState(5, state, function(err, lights) { - if (err) throw err; - displayResult(lights); -}); -``` - -The __lightState__ object will ensure that the values passed into the various state functions are correctly bounded to avoid -errors when setting them. For example the color temperature value (which determines the white value) must be between 154 and 500. If you pass in a value outside of this range then the lightState function call will set it to the closest valid value. - -Currently the __lightState__ object will combine together all the various state values that get set by the various function calls. This means that if you do create a combination of conflicting values, like __on__ and __off__ the last one set will be the actual value provided in the corresponding JSON object; - -```js -// This will result in a JSON object for the state that sets the brightness to 100% but turn the light "off" -state = lightState.create().on().brightness(100).off(); -``` - -When using __lightState__ it is currently recommended to create a new state object each time you want to build a new state, otherwise you will get a combination of all the previous settings as well as the new values. - - -## Turning a Light On/Off using LightState - -```js -var hue = require("node-hue-api"), - HueApi = hue.HueApi, - lightState = hue.lightState; - -var displayResult = function(result) { - console.log(result); -}; - -var displayError = function(err) { - console.error(err); -}; - -var host = "192.168.2.129", - username = "033a6feb77750dc770ec4a4487a9e8db", - api = new HueApi(host, username), - state = lightState.create(); - -// -------------------------- -// Using a promise - -// Set the lamp with id '2' to on -api.setLightState(2, state.on()) - .then(displayResult) - .fail(displayError) - .done(); - -// Now turn off the lamp -api.setLightState(2, state.off()) - .then(displayResult) - .fail(displayError) - .done(); - -// -------------------------- -// Using a callback -// Set the lamp with id '2' to on -api.setLightState(2, state.on(), function(err, result) { - if (err) throw err; - displayResult(result); -}); - -// Now turn off the lamp -api.setLightState(2, state.off(), function(err, result) { - if (err) throw err; - displayResult(result); -}); -``` - -If the function call is successful, then you should get a response of ``true``. If the call fails then an ``ApiError`` -will be generated with the failure details. - - -## Setting Light States using custom JSON Object -You can pass in your own JSON object that contain the setting(s) that you wish to pass to the light via the bridge. If -you do this, then a LightState object will be created from the passed in object, so that it can be properly validated -and only valid values are passed to the bridge. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(result); -}; - -var displayError = function(err) { - console.error(err); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); -api.setLightState(2, {"on": true}) // provide a value of false to turn off - .then(displayResult) - .fail(displayError) - .done(); -``` - -If the function call is successful, then you should get a response of true. If the call fails then an ``ApiError`` will be generated with the failure details. - - -## Getting the Current Status/State for a Light -To obtain the current state of a light from the Hue Bridge you can use the `lightStatus()` or `getLightStatus()` function; - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayStatus = function(status) { - console.log(JSON.stringify(status, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// Obtain the Status of Light '5' - -// -------------------------- -// Using a promise -api.lightStatus(5) - .then(displayStatus) - .done(); - -// -------------------------- -// Using a callback -api.lightStatus(5, function(err, result) { - if (err) throw err; - displayStatus(result); -}); -``` - -This will produce a JSON object detailing the status of the lamp; -``` -{ - "state": { - "on": true, - "bri": 254, - "hue": 34515, - "sat": 236, - "xy": [ - 0.3138, - 0.3239 - ], - "ct": 153, - "alert": "none", - "effect": "none", - "colormode": "ct", - "reachable": true - }, - "type": "Extended color light", - "name": "Left Bedside", - "modelid": "LCT001", - "swversion": "65003148", - "pointsymbol": { - "1": "none", - "2": "none", - "3": "none", - "4": "none", - "5": "none", - "6": "none", - "7": "none", - "8": "none" - } -} -``` - -### Obtaining the RGB Value for a Light -There is a function to provide the complete light status along with an approximated RGB value (which is a rough conversion -of the xy state of a light into an RGB value). This is not a perfect conversion but does get close to the current color -of the lamp. - -To obtain the status with the RGB approximation of a lamp use `lightStatusWithRGB()` or `getLightStatusWithRGB()`. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResult = function(result) { - console.log(result); -}; - -var displayError = function(err) { - console.error(err); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api; - -api = new HueApi(host, username); -api.lightStatusWithRGB(1) - .then(displayResult) - .fail(displayError) - .done(); -``` - -```js -{ - state: { - rgb: [ 255, 249, 221 ], - on: true, - bri: 254, - hue: 38265, - sat: 92, - effect: 'none', - xy: [ 0.3362, 0.3604 ], - alert: 'none', - colormode: 'xy', - reachable: true - }, - type: 'Color light', - name: 'Living Color Floor', - modelid: 'LLC007', - manufacturername: 'Philips', - uniqueid: '00:17:88:01:00:1b:21:a3-0b', - swversion: '4.6.0.8274' -} -``` - - -### Searching for New Lights -When you have added new lights to the system, you need to invoke a search to discover these new lights to allow the Bridge -to interact with them. The ``searchForNewLights()`` function will invoke a search for any new lights to be added to the -system. - -When you invoke a scan for any new lights in the system, the previous search results are destroyed. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.searchForNewLights() - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.searchForNewLights(function(err, result) { - if (err) throw err; - displayResults(result); -}); -``` -The result from this call should be ``true`` if a search was successfully triggered. It can take some time for the search -to complete. - -### Obtaining Newly Discovered Lights -Once a search has been completed, then the newly discovered lights can be obtained using the ``newLights()`` call. -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.newLights() - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.newLights(function(err, result) { - if (err) throw err; - displayResults(result); -}); -``` -The results from this call should be the new lights that were found during the previous search, and a ``lastscan`` value -that will be the date that the last scan was performed, which could be ``none`` if a search has never been performed. -``` -{ - "lastscan": "2013-06-15T14:45:23" -} -``` - -### Naming Lights -It is possible to name a light using the ``setLightName()`` function; -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.setLightName(5, "A new Name") - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.setLightName(5, "Living Color TV", function(err, result) { - if (err) throw err; - displayResults(result); -}); -``` -If the call is successful, then ``true`` will be returned by the function call, otherwise a ``ApiError`` will result. - - -## Working with 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; - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// Obtain all the defined groups in the Bridge - -// -------------------------- -// Using a promise -api.groups() - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.groups(function(err, result) { - if (err) throw err; - displayResults(result); -}); -``` - -This will produce an array of values detailing the id and names of the groups; -``` -[ - { - "id": "0", - "name": "Lightset 0", - "type": "LightGroup" - }, - { - "id": "1", - "name": "VRC 1", - "lights": [ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8" - ], - "type": "LightGroup", - "action": { - "on": false, - "bri": 162, - "hue": 13088, - "sat": 213, - "effect": "none", - "xy": [ - 0.5134, - 0.4149 - ], - "ct": 467, - "alert": "none", - "colormode": "xy" - } - } -] -``` -Please note, 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 the other groups in the results returned from a call to `groups()`. - -If you need to get the full details of the __Lightset 0__ groups, then you can obtain that by using the `getGroup()` -function, using an id argument of `0`. - -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; -* `luminaires` Will obtain only the *Luminaire* 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 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; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.getGroup(0) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.getGroup(0, function(err, result) { - if (err) throw err; - displayResults(result); -}); -``` - -Which will return produce a result like; -``` -{ - "id": "0", - "name": "Lightset 0", - "lights": [ - "1", - "2", - "3", - "4", - "5" - ], - "type": "LightGroup", - "lastAction": { - "on": true, - "bri": 128, - "hue": 6144, - "sat": 254, - "xy": [ - 0.6376, - 0.3563 - ], - "ct": 500, - "effect": "none", - "colormode": "ct" - } -} -``` - -### Setting the Light State for a Group -A function ``setGroupLightState()`` exists for interacting with a group of lights to be able to set all the lights to a -particular state. This function is identical to that of the ``setLightState()`` function above, except that it works on -groups instead of a single light. - - -### Create a New Group -To create a new group use the __createGroup(name, lightIds)__ function; - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// Create a new Group on the bridge - -// -------------------------- -// Using a promise -api.createGroup("a new group", [4, 5]) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.createGroup("group name", [1, 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" -} -``` - - -### 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; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// Update the name of the group - -// -------------------------- -// Using a promise -api.updateGroup(1, "new group name") - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.updateGroup(1, "new group name", function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -Changing the lights associated with an existing group; -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -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. - -// -------------------------- -// Using a promise -api.updateGroup(1, [1, 2, 3]) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.updateGroup(1, [1, 2, 3], function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -Changing both the name and the lights for an existing group; -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -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. - -// -------------------------- -// Using a promise -api.updateGroup(1, "group name", [4, 5]) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.updateGroup(1, "group name", [4, 5], function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - - -### 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 -possible to delete groups, but use at your own risk *(you may have to reset the bridge to factory defaults if something -goes wrong)*. - -To delete a group use the ``deleteGroup()`` function; - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// Create a new Group on the bridge - -// -------------------------- -// Using a promise -api.deleteGroup(3) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.deleteGroup(4, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` -This function call will return a ``true`` result in the promise chain if successful, otherwise an error will be thrown. - - -## Working with Schedules - -### Obtaining all the Defined Schedules -To obtain all the defined schedules on the Hue Bridge use the ``schedules()`` function. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.schedules() - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.schedules(function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -The function will return a promise that will provide an array of objects, each containing the complete details fo the schedule; -``` -[ - { - "id": "9067578731131353", - "name": "Alarm", - "description": "Peter Wakeup", - "command": { - "address": "/api/yeM5QamRRFNXfv13/groups/0/action", - "body": { - "scene": "f0f7c51a6-on-7" - }, - "method": "PUT" - }, - "localtime": "W124/T06:10:00", - "time": "W124/T06:10:00", - "created": "2015-11-18T22:19:16", - "status": "disabled" - }, - ... -] -``` - -### Obtaining the details of a Schedule -To obtain the details of a schedule use the ``getSchedule(id)`` function; - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - scheduleId = 1; - -// -------------------------- -// Using a promise -api.getSchedule(scheduleId) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.getSchedule(scheduleId, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -The promise returned by the function will return the details of the schedule in the following format; -``` -{ - "id": "9067578731131353", - "name": "Alarm", - "description": "Peter Wakeup", - "command": { - "address": "/api/yeM5QamRRFNXfv13/groups/0/action", - "body": { - "scene": "f0f7c51a6-on-7" - }, - "method": "PUT" - }, - "localtime": "W124/T06:10:00", - "time": "W124/T06:10:00", - "created": "2015-11-18T22:19:16", - "status": "disabled" -} -``` - -### Creating a Schedule -Creating a schedule requires just two elements, a time at which to trigger the schedule and the command that will be -triggered when the schedule is run. -There are other optional values of a name and a description that can be provided to make the schedule easier to identify. - -There are two functions that can be invoked to create a new schedule (which are identically implemented); -- ``scheduleEvent(event, cb)`` -- ``createSchedule(event, cb)`` - -These functions both take an object the wraps up the scheduled event to be created. There are only two required properties -of the object, ``time`` and ``command``, with option properties ``name`` and ``description``. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - scheduledEvent; - -scheduledEvent = { - "name": "Sample Schedule", - "description": "A sample scheduled event to switch on a light", - "time": "2013-12-24T00:00:00", - "command": { - "address": "/api/08a902b95915cdd9b75547cb50892dc4/lights/5/state", - "method" : "PUT", - "body" : { - "on": true - } - } -}; - -// -------------------------- -// Using a promise -api.scheduleEvent(scheduledEvent) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.createSchedule(scheduledEvent, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -The result returned by the promise when creating a new schedule will be that of the ``id`` for the newly created schedule; -``` -{ - "id": "1" -} -``` - -The ``command`` value must be a Hue Bridge API endpoint for it to correctly function, which means it must start with -``/api//``. For now if using this function, you will have to use the exact API end point as specified in -the Phillips Hue REST API. - -To help with building a schedule and to perform some basic checking to ensure that values are correct/valid there is a -helper module ``scheduleEvent`` which can be used the build a valid schedule object. - - -### Using ScheduleEvent to build a Schedule -The ``scheduleEvent`` module/function is used to build up a schedule that the Hue Bridge can understand. It is not a -requirement when creating schedules, but can eliminate some of the basic errors that can result when creating a schedule. - -To obtain a scheduleEvent instance; -```js -var scheduleEvent = require("node-hue-api").scheduledEvent; - -var mySchedule = scheduleEvent.create(); -``` - -This will give you a schedule object that has the following functions available to build a schedule; -- ``withName(String)`` which will set a name for the schedule (optional) -- ``withDescription(String)`` which will set a description for the schedule (optional) -- ``withCommand(command)`` which will set the command object that the schedule will run -- ``on()``, ``at()``, ``when()`` which all take a string or Date value to specify the time the schedule will run, if -passing a string it must be valid when parsed by ``Date.parse()`` - -The ``command`` object currently has to be specified as the Hue Bridge API documentation states which is of the form; -``` -{ - "address": "/api/08a902b95915cdd9b75547cb50892dc4/lights/5/state", - "method" : "PUT", - "body" : { - "on": true - } -} -``` -The above example command will switch on the light with id ``5`` for the username ``08a902b95915cdd9b75547cb50892dc4``. - -If you use the ``withCommand()`` function then the ``address`` will be undergo basic validation to ensure it is an -endpoint for the Hue Bridge which is a common mistake to make when crafting your own values. - -Once a scheduleEvent has been built it can be passEd directly to the ``createSchedule()``, ``scheduleEvent()`` or -``updateSchedule()`` function calls in the Hue API. - -For example to create a new schedule that will turn on the light with id 5 at 07:00 on the 25th December 2013; -```js -var hue = require("node-hue-api"), - HueApi = hue.HueApi, - scheduleEvent = hue.scheduledEvent; - -var displayResult = function (result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - mySchedule; - -mySchedule = scheduleEvent.create() - .withName("Xmas Day Wake Up") - .withDescription("Like anyone really needs a wake up on Xmas day...") - .withCommand( - { - "address": "/api/08a902b95915cdd9b75547cb50892dc4/lights/5/state", - "method" : "PUT", - "body" : { - "on": true - } - }) - .on("2013-12-25T07:00:00"); - -// -------------------------- -// Using a promise -api.createSchedule(mySchedule) - .then(displayResult) - .done(); - -// -------------------------- -// Using a callback -api.createSchedule(mySchedule, function(err, result) { - if (err) throw err; - displayResult(result); -}); -``` - - -### Updating a Schedule -You can update an existing schedule using the ``updateSchedule()`` function; - -```js -var hue = require("node-hue-api"), - HueApi = hue.HueApi, - scheduleEvent = hue.scheduledEvent; - -var displayResult = function (result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - scheduleId = 1, - updatedValues; - -updatedValues = { - "name": "Updated Name", - "time": "January 1, 2014 07:00:30" -}; - -// -------------------------- -// Using a promise -api.updateSchedule(scheduleId, updatedValues) - .then(displayResult) - .done(); - -// -------------------------- -// Using a callback -api.updateSchedule(scheduleId, updatedValues, function(err, result) { - if (err) throw err; - displayResult(result); -}); -``` - -The result from the promise will be an object with the properties of the schedule that were updated and ``true`` as the -value of each one that was successful. -``` -{ - "name": true, - "time": true -} -``` - - -### Deleting a Schedule -All schedules in the Hue Bridge are removed once they are triggered. To remove an impending schedule use the ``deleteSchedule()`` -function; - -```js -var hue = require("node-hue-api"), - HueApi = hue.HueApi; - -var displayResult = function (result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - scheduleId = 1; - -// -------------------------- -// Using a promise -api.deleteSchedule(scheduleId) - .then(displayResult) - .done(); - -// -------------------------- -// Using a callback -api.deleteSchedule(scheduleId, function(err, result) { - if (err) throw err; - displayResult(result); -}); -``` - -If the deletion was successful, then ``true`` will be returned from the promise, otherwise an ``ApiError`` will be thrown, -as in the case if the schedule does not exist. - - -## Working with scenes -The Hue Bridge can store up to 200 scenes internally. There is currently no way to delete a scene from the API once it -is created, although old unused scenes will get overwritten. - -Additionally, bridge scenes should not be confused with the preset scenes stored in the Android and iOS apps. In the -apps these scenes are stored internally. Once activated though, they may then appear as a bridge scene. - - - -### Obtaining all the Defined scenes -To obtain all the defined bridge scenes on the Hue Bridge use the ``scenes()`` or ``getScenes()`` functions: - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username); - -// -------------------------- -// Using a promise -api.scenes() - .then(displayResults) - .done(); -// Using 'getScenes' alias -api.getScenes() - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.scenes(function(err, result){ - if (err) throw err; - displayResults(result); -}); -// Using 'getScenes' alias -api.getScenes(function(err, result){ - if (err) throw err; - displayResults(result); -``` - -The function will return an Array of scene definitions consisting of ``id``, ``name`` and ``lights``; -``` -[ - { - "name": "Tap scene 1", - "lights": [ - "1", - "2", - "3" - ], - "owner": "none", - "recycle": true, - "locked": true, - "appdata": {}, - "picture": "", - "lastupdated": null, - "version": 1, - "id": "OFF-TAP-1" - }, - { - "name": "Tap scene 3", - "lights": [ - "1", - "2", - "3" - ], - "owner": "none", - "recycle": true, - "locked": true, - "appdata": {}, - "picture": "", - "lastupdated": null, - "version": 1, - "id": "TAP-3" - }, - { - "name": "Tap scene 4", - "lights": [ - "1", - "2", - "3" - ], - "owner": "none", - "recycle": true, - "locked": true, - "appdata": {}, - "picture": "", - "lastupdated": null, - "version": 1, - "id": "TAP-4" - }, - { - "name": "Blue rain on 0", - "lights": [ - "1", - "2", - "3" - ], - "owner": "none", - "recycle": true, - "locked": true, - "appdata": {}, - "picture": "", - "lastupdated": null, - "version": 1, - "id": "74f97e0ba-on-0" - } -] -``` - -### Get a Scene -You can obtain a specific scene using the id of the scene and the ``scene()`` or ``getScene()`` function: - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - sceneId = "OFF-TAP-1" - ; - -// -------------------------- -// Using a promise -api.scene(sceneId) - .then(displayResults) - .done(); -// Using 'getScene' alias -api.getScene(sceneId) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.scene(sceneId, function(err, result){ - if (err) throw err; - displayResults(result); -}); -// Using 'getScene' alias -api.getScene(sceneId, function(err, result){ - if (err) throw err; - displayResults(result); -}; -``` - -The functions will return a result of the scene definition, like the following: -``` -{ - "OFF-TAP-1": { - "name": "Tap scene 1", - "lights": [ - "1", - "2", - "3", - "4" - ], - "owner": "none", - "recycle": true, - "locked": true, - "appdata": {}, - "picture": "", - "lastupdated": null, - "version": 1 - } -``` - -### Creating a Scene -The original function `createScene()` was implemented to support the older `1.2.x` version of the Hue Bridge Scene -creation. In version `2.0.x` of this library, it was modified to support both the old an new versions of scene creation. - -If you call this function using a an `array` of light ids and a `name` it will call the `createBasicScene()` function. -Alternatively if you call this function with a `Scene` object, then the `createAdvancedScene()` function will be invoked. - -See below for the specifics of the parameters and results. - - -### Creating a Basic Scene -There are multiple definitions on scenes, some of which are stored in the Bridge, others are stored inside the iOS and -Android applications. This API can only interact and define scenes that are stored inside the Hue Bridge. - -As of version `1.11` of the Hue Bridge Software, all scenes are now stored inside the bridge. - -When creating a new scene, the current state of the lights that are being included become the state of the lights when -you activate/recall the scene in the future. - -When you create a scene via the API function ``createBasicScene()``, the scene will get an ``id`` that is created by the -bridge. - -```js -var hue = require("node-hue-api") - , HueApi = hue.HueApi - , hueScene = hue.scene - ; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.245" - , username = "08a902b95915cdd9b75547cb50892dc4" - , api = new HueApi(host, username) - , scene = hueScene.create() - ; - -scene.withName("My Scene") - .withLights([1, 2, 3]) - .withTransitionTime(500) - ; - -// -------------------------- -// Using a promise -api.createAdvancedScene(scene) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.createAdvancedScene(scene, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -When a new scene is created, you will get a result back with the id of the created scene of the form; -``` -{ - "id": "jsoaw-58sk2" -} -``` - -The ``name`` value is optional, if one is not specified, then it will be set as the ``id`` that is generated. This is a -feature of the underlying Hue Bridge, so may change in a future firmware update. - - -### Creating an Advanced Scene -With the advent of version `1.11` Hue Bridge software version, the creation of Scenes was officially added and the number - of parameters available when creating a scene changed. - -The scenes are now stored inside the bridge itself, and to support the multiple combination of parameters available you -need to provide the settings in a `Scene` object. - -```js -var HueApi = require("node-hue-api").HueApi; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - sceneName = "My New Scene", - lightIds = [1, 2, 3, 4, 5, 6, 7] - ; - -// -------------------------- -// Using a promise -api.createBasicScene(lightIds, sceneName) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.scene(lightIds, sceneName, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -When a new scene is created, you will get a result back with the id of the created scene of the form; -``` -{ - "id": "jsoaw-58sk2" -} -``` - -### Scene Object -The Scene object allows you to define a number of parameters that can be used to define a scene as of the `1.11.x` Hue -Bridge firmware. - -To obtain a `scene` instance; -```js -var hueScene = require("node-hue-api").scene; - -var myScene = scene.create(); -``` - -This will give you a `Scene` object that has the following functions available to build a scene; -- ``withName(String)`` which will set a name for the scene (optional) -- ``withLights([])`` which will set the array of light ids for the scene -- ``withTransitionTime(ms)`` which will set the transtion time in milliseconds for the lights (optional) -- ``withRecycle(boolean)`` which will set the recycle flag on the scene (optional) -- ``withAppData(Object)`` which will set the application data for the scene e.g. ``{data: "some data", version: "1.0"}`` (optional) -- ``withPicture(String)`` a base64 encoded string which is the picture for the scene (optional) - -An example of building a complex scene; -```js -var hueScene = require("node-hue-api").scene; - -var scene = hueScene.create() - .withName("my scene") - .withLights([1, 2, 3]) - .withTransitionTime(2000) - .withAppData({data: "My own application data value for the scene"}) - ; -``` - - -### Updating/Modifying an Existing Scene -You can update an existing scene by using the ``updateScene()`` or ``modifyScene()`` function. When updating the scene -you can set the ``name``, ``lights`` or store the current light states of the lights in the scene. Any combination of -these parameters are possible. - -```js -var hue = require("node-hue-api") - , HueApi = hue.HueApi - , hueScene = hue.scene - ; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.245" - , username = "08a902b95915cdd9b75547cb50892dc4" - , api = new HueApi(host, username) - , sceneId = "iimpLsd4yVuVnjy" - , sceneUpdates = hueScene.create() - ; - -sceneUpdates.withName("My Scene") - .withLights([1, 2, 3]) - ; - -// -------------------------- -// Using a promise -api.modifyScene(sceneId, sceneUpdates) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.modifyScene(sceneId, sceneUpdates, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -When the scene is modified/updated, you will get a response containing the values modified; -``` -{ - "name": true, - "lights": true -} -``` - -Each value that is modified will report a ``true`` or ``false`` value if the parameter was changed. - - -### Set a Light State for a Light in a Scene -If you need to set a different light state for a light that is part of scene (that is a different state to what it was -in when the original scene was created), then you can use the `setSceneLightState()`, ``updateSceneLightState()`` -or ``modifySceneLightState()`` function. - -This function allows you to specify the desired values for a single light in a scene, if you want to set the state for -multiple bulbs, you will have to set it on each one individually. - -```js -var HueApi = require("node-hue-api").HueApi - , lightState = require("node-hue-api").lightState - ; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.245", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - sceneId = "node-hue-api-2", - lightId = 1, - state = lightState.create().on().hue(2000) - ; - -// -------------------------- -// Using a promise -api.setSceneLightState(sceneId, lightId, state) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.setSceneLightState(sceneId, lightId, state, function(err, result){ - if (err) throw err; - displayResults(result); -}); -``` - -The results from setting light sate values will be the name of each value being set followed by a value of `true` if -the change in the value was successful; - -``` -{ - "on": true, - "hue": true -} -``` - - -### Activating or Recalling a Scene -To recall or activate a scene (synonyms for the same activity) use the ``activateScene()`` or ``recallScene()`` function. - -When a scense is being made active, it is possible to also filter the lights in the scene using a group definition to -limit the lights that will be affected by the scene activation. -This means you could have defined a scene for all your bulbs, but if you apply a group filter that includes only, say -the lounge lights, then the scene will be activated only on the lounge lights. - -If a group filter is not specified (it is an optional parameter) then the API does no filtering on the lights in the -scene when it is activated. - -```js -var HueApi = require("node-hue-api").HueApi - , lightState = require("node-hue-api").lightState - ; - -var displayResults = function(result) { - console.log(JSON.stringify(result, null, 2)); -}; - -var host = "192.168.2.129", - username = "08a902b95915cdd9b75547cb50892dc4", - api = new HueApi(host, username), - sceneId = "node-hue-api-2", - lightId = 1, - state = lightState.create().on().hue(2000) - ; - -// -------------------------- -// Using a promise -api.activateScene(sceneId) - .then(displayResults) - .done(); -// using the "recallScene" alias -api.recallScene(sceneId) - .then(displayResults) - .done(); - -// -------------------------- -// Using a callback -api.activateScene(sceneId, function(err, result) { - if (err) throw err; - displayResults(result); -}); -// using the "recallScene" alias -api.recallScene(sceneId, function(err, result) { - if (err) throw err; - displayResults(result); -}); -``` - -When a Scene is successfully activated/recalled, the result will be `true`. - -### Scenes by Name -There is no sensible way to dealing with scenes by name currently (firmware version 1.5+) as it is possible to define -multiple scenes with the same name (in fact in testing even editing a scene in the iOS app created a new scene on the -bridge). - -The scene `id` is the only reliable and consistent way to interact with scene activation/recalling. \ No newline at end of file diff --git a/docs/v3_backwards_compatibility.md b/docs/v3_backwards_compatibility.md deleted file mode 100644 index 7d8c89f..0000000 --- a/docs/v3_backwards_compatibility.md +++ /dev/null @@ -1,113 +0,0 @@ -# v3.x Breaking Changes to Backwards Compatibility - -With the release of `v3` version of the `node-hue-api`, backwards compatibility with existing code has been attempted -whilst completely rewriting the underlying code to support new features of JavaScript and removal of out of date -dependencies. - -The new API is available under the `v3` object, `require('node-hue-api').v3` whilst the pre-existing top level objects -have been left in place for backwards compatibility (although now with added nag messages to warn about deprecation). - -The older backwards compatibility layer will remain for all `3.x` releases of the library before being removed from the -`4.x` releases. - -Whilst a lot of work went into providing backwards compatibility, there are some change that could not easily be avoided -or would result in a massive amount of effort to shim them to provide 100% backwards compatibility. These changes are -listed below and are based on the old test suite run against the `3.x` release to identify areas where calling code -needed to be updated. - - -# Breaking Changes to existing endpoints - -* [discovery](#discovery) -* [description](#description) -* [groups](#groups) - * [getGroup()](#getGroup) -* [LightState](#LightState) -* [Scenes](#scenes) - * [createScene()](#createScene) - * [updateScene()](#updateScene) -* [Schedules](#schedules) -* [Timer](#timer) - - -## discovery - -The old deprecated functions `searchForBridges()` and `locateBridges()` have been removed. - - -## description - -The functions `description()` and `getDescription()` return a different object as the result. - - -## groups - -All Group functions that return objects for the group will have integrer based `id` attributes instead of being `String`s. - -### getGroup -The `getGroup()` function no longer returns an object with a `lastAction` attribute. - -Attempting to get a group that does not exist, will still error, but the error message will be of a different form. -Also passing invalid group IDs, that are not integers (or can be converted to one) will fail with a new form or error. - - -## LightState - -There is a shim in place to allow for the use of the old style LightState functions. This shim will create a new API -version of a LightState object that the new API functions with. - -The purpose of this is to provide a drop in shim to allow older code to work without having to be modified, but you are -strongly encouraged to utilize the new `LightState` object available using `require('node-hue-api').v3.lightStates.LightState'` -as this removes a number of overloaded function names that were unnecessary from the old API. - -There will be some new errors generated when attempting to set values that fall outside the allowed ranges of the light, -e.g. When setting a hue to be < 0. - - -## Scenes - -The old Scene object was more of a Builder pattern object. This has now been replaced with a builder object that builds -`Scene`s using the new `Scene` object from the updated API. The existing APIs support taking this object so no code -should need to change. - -If you need the actual Scene object that this is creating, then you need to call `getScene()` to get the `Scene` which -can be used as is with the older API functions. - -### createScene -The function `createScene()` used to support the creation of a Scene using an array of light ids and a name. This is no -longer supported due to changes in the underlying Hue API. - -### updateScene -The function `updateScene()` used to support an ability to snapshot the existing scene member state by passing a `null` -scene object to the function invocation. This is no longer supported and you must provided the updated `Scene` object -with the desired states stored in it. - - -## Schedules -The schedules and scheduled events have undergone updates due to changes in the Hue API and adoption of the `v3` objects -underneath. - -The `time` attribute is not deprecated in the Philips Hue REST API, so all calls to `time` should be replaced by `localtime`, -which is the new field that replaced `time`. - -When building a schedule, there is only limited support for the following time objects: - -* `RecurringTime` -* `AbsoluteTime` -* `Timer` - -More will be coming in a later update. If you need support for another time type, then raise an issue and it will be -prioritized. - -You cannot pass in any other forms of time currently, so it is not possible to pass in something like `new Date().getTIme()` -or `Date.now()` anymore (although these were never particularly useful in practice). - -If you want access to the underlying `Schedule` then you will need to call `getSchedule()`, this is not required for the -API functions, it will handle this automatically. - - -## Timer -The reference to `timer` has been removed, as it was part of the initial attempt to add some of the scheduling object -types and was probably not used in practice. - -This is now supported by passing a Timer patterned string into a schedule event. diff --git a/examples/v3/capabilities/getAllCapabilities.js b/examples/v3/capabilities/getAllCapabilities.js new file mode 100644 index 0000000..33a73db --- /dev/null +++ b/examples/v3/capabilities/getAllCapabilities.js @@ -0,0 +1,22 @@ +'use strict'; + +const v3 = require('../../../index').v3; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3; + +// Replace this with your username for accessing the bridge +const USERNAME = require('../../../test/support/testValues').username; + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + return api.capabilities.getAll(); + }) + .then(capabilities => { + // Display the Capabilities Object Details + console.log(capabilities.toStringDetailed()); + }) +; diff --git a/examples/v3/groups/createEntertainment.js b/examples/v3/groups/createEntertainment.js new file mode 100644 index 0000000..2680d44 --- /dev/null +++ b/examples/v3/groups/createEntertainment.js @@ -0,0 +1,38 @@ +'use strict'; + +const v3 = require('../../../index').v3; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3; + +const model = v3.model; + +// Replace this with your Bridge User name +const USERNAME = require('../../../test/support/testValues').username; + + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + const entertainment = model.createEntertainment(); + // The name of the new zone we are creating + entertainment.name = 'Testing Entertainment Creation'; + // The array of light ids that will be in the entertainment group, not all lights can be added, they have to support streaming + entertainment.lights = [44, 43]; + // The class for the entertainment group, this has to be selected from the valid values, consult the documentation for details + entertainment.class = 'TV'; + + return api.groups.createGroup(entertainment) + .then(group => { + console.log(group.toStringDetailed()); + + // Delete the new Entertainment Group + return api.groups.deleteGroup(group); + }); + }) + .catch(err => { + console.error(err); + }) +; \ No newline at end of file diff --git a/examples/v3/groups/createLightGroup.js b/examples/v3/groups/createLightGroup.js index c750587..de4a789 100644 --- a/examples/v3/groups/createLightGroup.js +++ b/examples/v3/groups/createLightGroup.js @@ -4,6 +4,9 @@ const v3 = require('../../../index').v3; // If using this code outside of this library the above should be replaced with // const v3 = require('node-hue-api').v3; +const model = v3.model; + +// Replace this with your Bridge User name const USERNAME = require('../../../test/support/testValues').username; @@ -13,15 +16,21 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { + const newGroup = model.createLightGroup(); // The name of the new group to create - const groupName = 'My New Group' - // The array of light ids that will be in the group - , groupLights = [1] - ; + newGroup.name = 'My New Group'; + // The array of light ids that will be in the group + newGroup.lights = [2]; + + return api.groups.createGroup(newGroup) + .then(group => { + console.log(group.toStringDetailed()); - return api.groups.createGroup(groupName, groupLights); + // Delete the new Group + return api.groups.deleteGroup(group); + }); }) - .then(group => { - console.log(group.toStringDetailed()); + .catch(err => { + console.error(err); }) ; \ No newline at end of file diff --git a/examples/v3/groups/createRoom.js b/examples/v3/groups/createRoom.js index 11238e7..ca732f5 100644 --- a/examples/v3/groups/createRoom.js +++ b/examples/v3/groups/createRoom.js @@ -4,6 +4,9 @@ const v3 = require('../../../index').v3; // If using this code outside of this library the above should be replaced with // const v3 = require('node-hue-api').v3; +const model = v3.model; + +// Replace this with your Bridge User name const USERNAME = require('../../../test/support/testValues').username; @@ -13,20 +16,21 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - + const newRoom = model.createRoom(); // The name of the new room we are creating - const roomName = 'Testing Room Creation' - - // The array of light ids that will be in the group - , roomLights = [] + newRoom.name = 'Testing Room Creation'; + // The class of the room we are creating, these are specified in the Group documentation under class attribute + newRoom.class = 'Gym'; - // The class of the room we are creating, these are specified in the Group documentation under class attribute - , roomClass = 'Gym' - ; + return api.groups.createGroup(newRoom) + .then(room => { + console.log(room.toStringDetailed()); - return api.groups.createRoom(roomName, roomLights, roomClass); + // Delete the new Room + return api.groups.deleteGroup(room); + }); }) - .then(room => { - console.log(room.toStringDetailed()); + .catch(err => { + console.error(err); }) ; \ No newline at end of file diff --git a/examples/v3/groups/createZone.js b/examples/v3/groups/createZone.js index 0f54481..73f9596 100644 --- a/examples/v3/groups/createZone.js +++ b/examples/v3/groups/createZone.js @@ -4,6 +4,9 @@ const v3 = require('../../../index').v3; // If using this code outside of this library the above should be replaced with // const v3 = require('node-hue-api').v3; +const model = v3.model; + +// Replace this with your Bridge User name const USERNAME = require('../../../test/support/testValues').username; @@ -13,16 +16,23 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - + const newZone = model.createZone(); // The name of the new zone we are creating - const zoneName = 'Testing Zone Creation' - // The array of light ids that will be in the zone - , zoneLights = [1, 2, 3, 4, 5] - ; + newZone.name = 'Testing Zone Creation'; + // The array of light ids that will be in the zone + newZone.lights = [2, 3, 4, 5]; + // The class for the zone, this has to be selected from the valid value, consult the documentation for details + newZone.class = 'Toilet'; + + return api.groups.createGroup(newZone) + .then(zone => { + console.log(zone.toStringDetailed()); - return api.groups.createZone(zoneName, zoneLights); + // Delete the new Zone + return api.groups.deleteGroup(zone); + }); }) - .then(zone => { - console.log(zone.toStringDetailed()); + .catch(err => { + console.error(err); }) ; \ No newline at end of file diff --git a/examples/v3/groups/deleteGroup.js b/examples/v3/groups/deleteGroup.js index 2ad5231..0901ea1 100644 --- a/examples/v3/groups/deleteGroup.js +++ b/examples/v3/groups/deleteGroup.js @@ -14,6 +14,17 @@ v3.discovery.nupnpSearch() }) .then(api => { + //TODO remove + // return api.groups.getByName('Custom group for $lights') + // .then(groups => { + // const promises = []; + // + // groups.forEach(group => { + // promises.push(api.groups.deleteGroup(group.id)); + // }); + // return Promise.all(promises); + // }) + // Create a new group that we can then delete return api.groups.createZone('Testing Group Deletion') .then(group => { diff --git a/examples/v3/groups/getGroup.js b/examples/v3/groups/getGroup.js index 21a39c5..ab57eac 100644 --- a/examples/v3/groups/getGroup.js +++ b/examples/v3/groups/getGroup.js @@ -16,7 +16,7 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - return api.groups.get(GROUP_ID); + return api.groups.getGroup(GROUP_ID); }) .then(group => { // Display the details for the group diff --git a/examples/v3/groups/updateAttributes.js b/examples/v3/groups/updateGroupAttributes.js similarity index 68% rename from examples/v3/groups/updateAttributes.js rename to examples/v3/groups/updateGroupAttributes.js index 5ee7de0..5aa42ee 100644 --- a/examples/v3/groups/updateAttributes.js +++ b/examples/v3/groups/updateGroupAttributes.js @@ -1,8 +1,10 @@ 'use strict'; -const v3 = require('../../../index').v3; +const v3 = require('../../../index').v3 + , model = v3.model; // If using this code outside of this library the above should be replaced with -// const v3 = require('node-hue-api').v3; +// const v3 = require('node-hue-api').v3 +// , model = v3.model; const USERNAME = require('../../../test/support/testValues').username; @@ -10,7 +12,7 @@ const USERNAME = require('../../../test/support/testValues').username; const CLEANUP = true; // The Id of the group that we will getOperator -let createGroupId = null; +let createdGroupId = null; v3.discovery.nupnpSearch() @@ -19,27 +21,31 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - // Create a new group that we can modify the attributes on - return api.groups.createZone('Testing Group Update Attributes') + const zone = model.createZone(); + zone.name = 'Testing Group Update Attributes'; + + return api.groups.createGroup(zone) .then(group => { // Display the new group console.log(group.toStringDetailed()); // Store the ID so we can later remove it - createGroupId = group.id; + createdGroupId = group.id; + // Update the name of the group + group.name = 'A new group name'; // Update the name on the group (can also do 'class' for the room class and 'lights' to update the lights associated with the group) - return api.groups.updateAttributes(group.id, {name: 'A new name'}); + return api.groups.updateGroupAttributes(group); }) .then(result => { // Display the result of the console.log(`Update group attributes? ${result}`); }) .then(() => { - if (CLEANUP && createGroupId) { + if (CLEANUP && createdGroupId) { console.log('Cleaning up and removing group that was created'); - return api.groups.deleteGroup(createGroupId); + return api.groups.deleteGroup(createdGroupId); } }); }) diff --git a/examples/v3/lights/getLightById.js b/examples/v3/lights/getLight.js similarity index 83% rename from examples/v3/lights/getLightById.js rename to examples/v3/lights/getLight.js index 4f124f0..e66650e 100644 --- a/examples/v3/lights/getLightById.js +++ b/examples/v3/lights/getLight.js @@ -6,8 +6,8 @@ const v3 = require('../../../index').v3; // Replace this with your username for accessing the bridge const USERNAME = require('../../../test/support/testValues').username - // The name of the light we wish to retrieve by name - , LIGHT_ID = 1 + // The name of the light we wish to retrieve by id + , LIGHT_ID = 10 ; v3.discovery.nupnpSearch() @@ -16,7 +16,7 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - return api.lights.getLightById(LIGHT_ID); + return api.lights.get(LIGHT_ID); }) .then(light => { // Display the details of the light diff --git a/examples/v3/lights/getLightAttributesAndState.js b/examples/v3/lights/getLightAttributesAndState.js index bb5b01f..3441227 100644 --- a/examples/v3/lights/getLightAttributesAndState.js +++ b/examples/v3/lights/getLightAttributesAndState.js @@ -7,7 +7,7 @@ const v3 = require('../../../index').v3; // Replace this with your username for accessing the bridge const USERNAME = require('../../../test/support/testValues').username // The name of the light we wish to retrieve by name - , LIGHT_ID = 1 + , LIGHT_ID = 10 ; v3.discovery.nupnpSearch() diff --git a/examples/v3/lights/getLightState.js b/examples/v3/lights/getLightState.js index 9918298..bd1bf4b 100644 --- a/examples/v3/lights/getLightState.js +++ b/examples/v3/lights/getLightState.js @@ -7,7 +7,7 @@ const v3 = require('../../../index').v3; // Replace this with your username for accessing the bridge const USERNAME = require('../../../test/support/testValues').username // The name of the light we wish to retrieve by name - , LIGHT_ID = 1 + , LIGHT_ID = 10 ; v3.discovery.nupnpSearch() diff --git a/examples/v3/resourceLinks/getResourceLink.js b/examples/v3/resourceLinks/getResourceLink.js index 1bb3895..b0a0f2a 100644 --- a/examples/v3/resourceLinks/getResourceLink.js +++ b/examples/v3/resourceLinks/getResourceLink.js @@ -19,7 +19,7 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - return api.resourceLinks.get(RESOURCE_LINK_ID); + return api.resourceLinks.getResourceLink(RESOURCE_LINK_ID); }) .then(resourceLink => { console.log(`${resourceLink.toStringDetailed()}`); diff --git a/examples/v3/resourceLinks/getResourceLinkByName.js b/examples/v3/resourceLinks/getResourceLinkByName.js new file mode 100644 index 0000000..a48f38a --- /dev/null +++ b/examples/v3/resourceLinks/getResourceLinkByName.js @@ -0,0 +1,32 @@ +'use strict'; + +const v3 = require('../../../index').v3; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3; + +// Replace this with your username for accessing the bridge +const USERNAME = require('../../../test/support/testValues').username; + +// Replace this with your desired name for the ResourceLinks you want to retrieve +const RESOURCE_LINK_NAME = 'Meditation lights'; + +// +// This code will obtain the specified ResourceLink identified by the RESOURCE_LINK_NAME above and display it on the console + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + return api.resourceLinks.getResourceLinkByName(RESOURCE_LINK_NAME); + }) + .then(resourceLinks => { + resourceLinks.forEach(resourceLink => { + console.log(`${resourceLink.toStringDetailed()}`); + }); + }) + .catch(err => { + console.error(`Unexpected Error: ${err.message}`); + }) +; diff --git a/examples/v3/rules/getRule.js b/examples/v3/rules/getRule.js index 7d8b478..1e7bac2 100644 --- a/examples/v3/rules/getRule.js +++ b/examples/v3/rules/getRule.js @@ -15,7 +15,7 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - return api.rules.get(RULE_ID); + return api.rules.getRule(RULE_ID); }) .then(rule => { // Print the details for the Rule diff --git a/examples/v3/rules/getRuleByName.js b/examples/v3/rules/getRuleByName.js new file mode 100644 index 0000000..ff1ba77 --- /dev/null +++ b/examples/v3/rules/getRuleByName.js @@ -0,0 +1,26 @@ +'use strict'; + +const v3 = require('../../../index').v3; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3; + +const USERNAME = require('../../../test/support/testValues').username; + +// Set this to the desired Rule name to retrieve from the bridge +const RULE_NAME = 'Tap 2.1 Default'; + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + return api.rules.getRuleByName(RULE_NAME); + }) + .then(rules => { + // Print the details for the Rules found + rules.forEach(rule => { + console.log(rule.toStringDetailed()); + }); + }) +; \ No newline at end of file diff --git a/examples/v3/scenes/createScene.js b/examples/v3/scenes/createScene.js index 94d5f69..479f150 100644 --- a/examples/v3/scenes/createScene.js +++ b/examples/v3/scenes/createScene.js @@ -36,7 +36,7 @@ v3.discovery.nupnpSearch() console.log(`Created LightScene\n${scene.toStringDetailed()}`); // Now remove the scene we just created - return api.scenes.deleteScene(scene.id); + return api.scenes.deleteScene(scene); }); }) .catch(err => { diff --git a/examples/v3/scenes/getScene.js b/examples/v3/scenes/getScene.js index 0ee9219..b4bf849 100644 --- a/examples/v3/scenes/getScene.js +++ b/examples/v3/scenes/getScene.js @@ -7,7 +7,7 @@ const v3 = require('../../../index').v3; // Replace this with your username for accessing the bridge const USERNAME = require('../../../test/support/testValues').username; -// Set this to the id of the scene +// Set this to the id of the scene to retrieve const SCENE_ID = 'GfOL56sqKPGmPer'; v3.discovery.nupnpSearch() @@ -16,7 +16,7 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - return api.scenes.get(SCENE_ID); + return api.scenes.getScene(SCENE_ID); }) .then(scene => { // Do something useful with the Scene diff --git a/examples/v3/scenes/getSceneByName.js b/examples/v3/scenes/getSceneByName.js index 4099092..4859d98 100644 --- a/examples/v3/scenes/getSceneByName.js +++ b/examples/v3/scenes/getSceneByName.js @@ -16,7 +16,7 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - return api.scenes.getByName(SCENE_NAME); + return api.scenes.getSceneByName(SCENE_NAME); }) .then(scenes => { // Do something useful with the Scenes diff --git a/examples/v3/scenes/updateScene.js b/examples/v3/scenes/updateScene.js index 2bf2f08..443481f 100644 --- a/examples/v3/scenes/updateScene.js +++ b/examples/v3/scenes/updateScene.js @@ -1,11 +1,9 @@ 'use strict'; const v3 = require('../../../index').v3 - , Scene = v3.Scene ; // If using this code outside of this library the above should be replaced with // const v3 = require('node-hue-api').v3 -// , Scene = v3.Scene // ; // Replace this with your username for accessing the bridge @@ -20,14 +18,16 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - // Create a scene with the desired updates - const updatedScene = new Scene(); - // Update the name - updatedScene.name = 'my-cool-scene'; - // Set update the target lights for an existing LightScene - updatedScene.lights = [1, 2]; + return api.scenes.getScene(SCENE_ID) + .then(scene => { + // Update the desired values of the scene - return api.scenes.update(SCENE_ID, updatedScene); + // Update the name + scene.name = 'my-cool-scene'; + // Set update the target lights for an existing LightScene + scene.lights = [2, 3]; + return api.scenes.updateScene(scene); + }) }) .then(result => { console.log(`Updated Scene Attributes? ${JSON.stringify(result)}`); diff --git a/examples/v3/schedules/createSchedule.js b/examples/v3/schedules/createSchedule.js new file mode 100644 index 0000000..3f35c12 --- /dev/null +++ b/examples/v3/schedules/createSchedule.js @@ -0,0 +1,44 @@ +'use strict'; + +const v3 = require('../../../index').v3 + , model = v3.model +; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3 +// , model = v3.model +// ; + +// Replace this with your username for accessing the bridge +const USERNAME = require('../../../test/support/testValues').username; + + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + const schedule = model.createSchedule(); + + // Set the attributes of the Schedule + schedule.name = 'Test Schedule'; + schedule.description = 'A test schedule from the node-hue-api examples'; + schedule.recycle = true; + // trigger the schedule in 1 hour from now + schedule.localtime = model.timePatterns.createTimer().hours(1); + // Turn all the lights off (using light group 0 for all lights) + schedule.command = model.actions.group(0).withState(new model.lightStates.GroupLightState().off()); + + return api.schedules.createSchedule(schedule) + .then(created => { + console.log(`Created schedule in the bridge: ${created.id}`); + console.log(created.toStringDetailed()); + + // Now remove the scene we just created + return api.schedules.deleteSchedule(created); + }); + }) + .catch(err => { + console.error(`Unexpected Error: ${err.message}`); + }) +; diff --git a/examples/v3/schedules/getAllSchedules.js b/examples/v3/schedules/getAllSchedules.js new file mode 100644 index 0000000..ad1a891 --- /dev/null +++ b/examples/v3/schedules/getAllSchedules.js @@ -0,0 +1,27 @@ +'use strict'; + +const v3 = require('../../../index').v3; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3; + +// Replace this with your username for accessing the bridge +const USERNAME = require('../../../test/support/testValues').username; + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + return api.schedules.getAll(); + }) + .then(allSchedules => { + // Display the details of the schedules we got back + allSchedules.forEach(schedule => { + console.log(schedule.toStringDetailed()); + }); + }) + .catch(err => { + console.error(`Unexpected Error: ${err.message}`); + }) +; diff --git a/examples/v3/sensors/updateSensorName.js b/examples/v3/schedules/getScheduleById.js similarity index 52% rename from examples/v3/sensors/updateSensorName.js rename to examples/v3/schedules/getScheduleById.js index b04638c..b0e13ff 100644 --- a/examples/v3/sensors/updateSensorName.js +++ b/examples/v3/schedules/getScheduleById.js @@ -7,9 +7,8 @@ const v3 = require('../../../index').v3; // Replace this with your username for accessing the bridge const USERNAME = require('../../../test/support/testValues').username; -// Replace with the desired sensor ID that you want to rename -const SENSOR_ID = 1000; - +// The Schedule id to get from the bridge +const SCHEDULE_ID = 1; v3.discovery.nupnpSearch() .then(searchResults => { @@ -17,17 +16,13 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - // The Hue Daylight software sensor is identified as id 1 - return api.sensors.updateName(SENSOR_ID, 'updated-sensor-name'); + return api.schedules.get(SCHEDULE_ID); }) - .then(result => { - console.log(`Updated sensor name? ${result}`); + .then(schedule => { + // Display the details of the schedules we got back + console.log(schedule.toStringDetailed()); }) .catch(err => { - if (err.getHueErrorType() === 3) { - console.error(`Failed to locate a sensor with id ${SENSOR_ID}`) - } else { - console.error(`Unexpected Error: ${err.message}`); - } + console.error(`Unexpected error: ${err.message}`); }) ; diff --git a/examples/v3/schedules/getScheduleByName.js b/examples/v3/schedules/getScheduleByName.js new file mode 100644 index 0000000..046f9cd --- /dev/null +++ b/examples/v3/schedules/getScheduleByName.js @@ -0,0 +1,31 @@ +'use strict'; + +const v3 = require('../../../index').v3; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3; + +// Replace this with your username for accessing the bridge +const USERNAME = require('../../../test/support/testValues').username; + +// The Schedule name to retrieve from the bridge +const SCHEDULE_NAME = 'Wake up'; + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + return api.schedules.getByName(SCHEDULE_NAME); + }) + .then(schedules => { + // Display the details of the schedules we got back + console.log(`Schedules from the Bridge that match the name: "${SCHEDULE_NAME}"\n`); + schedules.forEach(schedule => { + console.log(schedule.toStringDetailed()); + }); + }) + .catch(err => { + console.error(`Unexpected error: ${err.message}`); + }) +; diff --git a/examples/v3/schedules/updateSchedule.js b/examples/v3/schedules/updateSchedule.js new file mode 100644 index 0000000..a2a7cef --- /dev/null +++ b/examples/v3/schedules/updateSchedule.js @@ -0,0 +1,69 @@ +'use strict'; + +const v3 = require('../../../index').v3 + , model = v3.model +; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3 +// , model = v3.model +// ; + +// Replace this with your username for accessing the bridge +const USERNAME = require('../../../test/support/testValues').username; + + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + // + // Create a new Schedule that we will then update in a subsequent call (so that we do not mess with any existing + // schedules in the bridge). + // Finally once finished, we will remove the schedule from the bridge + // + + const schedule = model.createSchedule(); + schedule.name = 'Test Schedule'; + schedule.description = 'A test schedule from the node-hue-api examples'; + schedule.recycle = true; + schedule.localtime = model.timePatterns.createTimer().hours(1); + schedule.command = model.actions.group(0).withState(new model.lightStates.GroupLightState().off()); + + return api.schedules.createSchedule(schedule) + .then(created => { + console.log(`\nCreated schedule in the bridge: ${created.id}`); + console.log(created.toStringDetailed()); + return created; + }) + .then(created => { + // Now update the localtime of the schedule to trigger in 2 hours and 30 minutes + created.localtime = model.timePatterns.createTimer().hours(2).minutes(30); + + return api.schedules.updateSchedule(created) + .then(updatedAttributes => { + console.log('\nUpdated Schedule Attributes:'); + console.log(JSON.stringify(updatedAttributes, null, 2)); + + // Get the details of the updated schedule + return api.schedules.get(created); + }); + }) + .then(updatedSchedule => { + // Displaying the details of the updated schedule + + console.log(`\nAttributes of the Updated Schedule:`); + console.log(updatedSchedule.toStringDetailed()); + + return updatedSchedule; + }) + .then(scheduleToRemove => { + // Now remove the scene we just created + return api.schedules.deleteSchedule(scheduleToRemove); + }); + }) + .catch(err => { + console.error(`Unexpected Error: ${err.message}`); + }) +; diff --git a/examples/v3/sensors/renameSensor.js b/examples/v3/sensors/renameSensor.js new file mode 100644 index 0000000..957d443 --- /dev/null +++ b/examples/v3/sensors/renameSensor.js @@ -0,0 +1,59 @@ +'use strict'; + +const v3 = require('../../../index').v3; +// If using this code outside of this library the above should be replaced with +// const v3 = require('node-hue-api').v3; + +const model = v3.model; + +// Replace this with your username for accessing the bridge +const USERNAME = require('../../../test/support/testValues').username; + + +v3.discovery.nupnpSearch() + .then(searchResults => { + const host = searchResults[0].ipaddress; + return v3.api.createLocal(host).connect(USERNAME); + }) + .then(api => { + // Will create a new Sensor, then rename it and finally remove it from the Bridge. + return api.sensors.createSensor(getNewSensor()) + .then(sensor => { + // Display the new Sensor we created + console.log(sensor.toStringDetailed()); + + // Update the name of the sensor + sensor.name = 'Updated Name Value'; + + return api.sensors.rename(sensor) + .then(result => { + console.log(`\nUpdated sensor name? ${result}\n`); + + // Obtain the updated sensor from the bridge + return api.sensors.get(sensor); + }) + .then(sensor => { + // Display the updated sensor (should be just the name that has changed) + console.log(sensor.toStringDetailed()); + + // Now delete it from the bridge + return api.sensors.deleteSensor(sensor); + }); + }); + }) + .catch(err => { + console.error(`Unexpected Error: ${err.message}`); + }) +; + +function getNewSensor() { + const sensor = model.createCLIPOpenCloseSensor(); + + sensor.modelid = 'software'; + sensor.swversion = '1.0'; + sensor.uniqueid = '00:00:00:01'; + sensor.manufacturername = 'node-hue-api'; + sensor.name = 'my-generic-status-sensor'; + + return sensor; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cdd3b0f..892b805 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,6 +111,11 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/package.json b/package.json index 2c75512..2c401f8 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,10 @@ } ], "main": "index.js", + "types": "index.d.ts", "scripts": { "test-model": "mocha --recursive \"lib/model/**/*.test.js\"", - "test-api": "mocha --recursive \"lib/api/**/*.test.js\"", + "test-api": "mocha --timeout 5000 --recursive \"lib/api/**/*.test.js\"", "test-types": "mocha --recursive \"lib/types/*.test.js\"", "clean-ts-definitions": "npx rimraf **/*.d.ts", "generate-ts-definitions": "npm run clean-ts-definitions && npx typescript index.js --allowJs --declaration --emitDeclarationOnly", @@ -27,6 +28,7 @@ }, "dependencies": { "axios": "^0.19.0", + "bottleneck": "^2.19.5", "get-ssl-certificate": "^2.3.3" }, "devDependencies": { From 5c68171e7c8d31b3f891c4f02c2e2989b4c1daa7 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Sun, 15 Dec 2019 13:41:10 +0000 Subject: [PATCH 24/35] - Fixing typescript definitions --- lib/api/Groups.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/api/Groups.js b/lib/api/Groups.js index 7526090..00a0ed4 100644 --- a/lib/api/Groups.js +++ b/lib/api/Groups.js @@ -7,7 +7,15 @@ const Bottleneck = require('bottleneck') , util = require('../util') ; - +/** + * @typedef {import('../model/groups/Entertainment')} Entertainment + * @typedef {import('../model/groups/Group')} Group + * @typedef {import('../model/groups/LightGroup')} LightGroup + * @typedef {import('../model/groups/Lightsource')} Lightsource + * @typedef {import('../model/groups/Luminaire')} Luminaire + * @typedef {import('../model/groups/Room')} Room + * @typedef {import('../model/groups/Zone')} Zone + */ module.exports = class Groups extends ApiDefinition { constructor(hueApi) { From a236764364dc63d516fc392bb77314bee916364d Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Sun, 15 Dec 2019 13:41:43 +0000 Subject: [PATCH 25/35] - Correcting typo in attribute name --- lib/model/groups/Entertainment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model/groups/Entertainment.js b/lib/model/groups/Entertainment.js index 6f2a449..d03d329 100644 --- a/lib/model/groups/Entertainment.js +++ b/lib/model/groups/Entertainment.js @@ -11,7 +11,7 @@ const ATTRIBUTES = [ types.object({ name: 'stream', types: [ - types.string({name: 'proymode'}), + types.string({name: 'proxymode'}), types.string({name: 'proxynode'}), types.boolean({name: 'active'}), types.string({name: 'owner'}), From 1542e1be8bc9f10b9603ee6883dac0935ce554e6 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Sun, 15 Dec 2019 18:01:09 +0000 Subject: [PATCH 26/35] - Adding serialization tests and updating tests to avoid deprecated functions where possible --- examples/v3/sensors/deleteSensor.js | 2 +- examples/v3/sensors/getSensor.js | 2 +- examples/v3/sensors/renameSensor.js | 4 +- lib/api/Groups.test.js | 26 +- lib/api/Lights.test.js | 29 +- lib/api/Rules.test.js | 34 +- lib/api/Scenes.test.js | 48 +- lib/api/Schedules.test.js | 26 +- lib/api/Sensors.js | 4 +- lib/api/Sensors.test.js | 40 +- lib/api/http/endpoints/scenes.js | 14 +- lib/model/Light.js | 1 + lib/model/ResourceLink.js | 40 +- lib/model/Schedule.js | 2 +- lib/model/groups/Group.test.js | 345 ------------ lib/model/index.test.js | 514 +++++++----------- lib/model/sensors/CLIPSensor.js | 4 + lib/model/sensors/Sensor.js | 37 +- lib/model/sensors/ZLLSwitch.js | 2 +- test/data/hue/groups/entertainment.json | 59 ++ test/data/hue/groups/lightgroup.json | 33 ++ test/data/hue/groups/room.json | 29 + test/data/hue/groups/zone.json | 30 + test/data/hue/lights/color_light.json | 57 ++ .../hue/lights/color_temperature_light.json | 48 ++ test/data/hue/lights/dimmable_light.json | 40 ++ .../data/hue/lights/extended_color_light.json | 68 +++ test/data/hue/resourcelinks/rslink.json | 12 + test/data/hue/scenes/groupscene.json | 29 + test/data/hue/scenes/lightscene.json | 26 + test/data/hue/schedules/wakeup.json | 16 + test/data/hue/sensors/clip_generic_flag.json | 17 + test/data/hue/sensors/daylight.json | 17 + test/data/hue/sensors/zgpswitch.json | 62 +++ test/data/hue/sensors/zllswitch.json | 123 +++++ test/data/model/groups/entertainment.js | 32 ++ test/data/model/groups/lightgroup_model.js | 38 ++ test/data/model/groups/room.js | 23 + test/data/model/groups/zone.js | 22 + test/data/model/lights/color_light.js | 30 + .../model/lights/color_temperature_light.js | 34 ++ test/data/model/lights/dimmable_light.js | 24 + .../data/model/lights/extended_color_light.js | 42 ++ test/data/model/resourcelinks/rslink.js | 11 + test/data/model/scenes/groupscene.js | 15 + test/data/model/scenes/lightscene.js | 13 + test/data/model/schedules/schedule.js | 16 + test/data/model/sensors/clip_generic_flag.js | 13 + test/data/model/sensors/daylight.js | 11 + test/data/model/sensors/zgpswitch.js | 25 + test/data/model/sensors/zllswitch.js | 44 ++ 51 files changed, 1449 insertions(+), 784 deletions(-) delete mode 100644 lib/model/groups/Group.test.js create mode 100644 test/data/hue/groups/entertainment.json create mode 100644 test/data/hue/groups/lightgroup.json create mode 100644 test/data/hue/groups/room.json create mode 100644 test/data/hue/groups/zone.json create mode 100644 test/data/hue/lights/color_light.json create mode 100644 test/data/hue/lights/color_temperature_light.json create mode 100644 test/data/hue/lights/dimmable_light.json create mode 100644 test/data/hue/lights/extended_color_light.json create mode 100644 test/data/hue/resourcelinks/rslink.json create mode 100644 test/data/hue/scenes/groupscene.json create mode 100644 test/data/hue/scenes/lightscene.json create mode 100644 test/data/hue/schedules/wakeup.json create mode 100644 test/data/hue/sensors/clip_generic_flag.json create mode 100644 test/data/hue/sensors/daylight.json create mode 100644 test/data/hue/sensors/zgpswitch.json create mode 100644 test/data/hue/sensors/zllswitch.json create mode 100644 test/data/model/groups/entertainment.js create mode 100644 test/data/model/groups/lightgroup_model.js create mode 100644 test/data/model/groups/room.js create mode 100644 test/data/model/groups/zone.js create mode 100644 test/data/model/lights/color_light.js create mode 100644 test/data/model/lights/color_temperature_light.js create mode 100644 test/data/model/lights/dimmable_light.js create mode 100644 test/data/model/lights/extended_color_light.js create mode 100644 test/data/model/resourcelinks/rslink.js create mode 100644 test/data/model/scenes/groupscene.js create mode 100644 test/data/model/scenes/lightscene.js create mode 100644 test/data/model/schedules/schedule.js create mode 100644 test/data/model/sensors/clip_generic_flag.js create mode 100644 test/data/model/sensors/daylight.js create mode 100644 test/data/model/sensors/zgpswitch.js create mode 100644 test/data/model/sensors/zllswitch.js diff --git a/examples/v3/sensors/deleteSensor.js b/examples/v3/sensors/deleteSensor.js index f830322..a7b423f 100644 --- a/examples/v3/sensors/deleteSensor.js +++ b/examples/v3/sensors/deleteSensor.js @@ -16,7 +16,7 @@ v3.discovery.nupnpSearch() return v3.api.createLocal(host).connect(USERNAME); }) .then(api => { - return api.sensors.get(SENSOR_ID_TO_DELETE); + return api.sensors.getSensor(SENSOR_ID_TO_DELETE); }) .then(result => { console.log(`Sensor was successfully deleted? ${result}`); diff --git a/examples/v3/sensors/getSensor.js b/examples/v3/sensors/getSensor.js index 990f11c..a3a2544 100644 --- a/examples/v3/sensors/getSensor.js +++ b/examples/v3/sensors/getSensor.js @@ -14,7 +14,7 @@ v3.discovery.nupnpSearch() }) .then(api => { // The Hue Daylight software sensor is identified as id 1 - return api.sensors.get(1); + return api.sensors.getSensor(1); }) .then(sensor => { // Display the details of the sensors we got back diff --git a/examples/v3/sensors/renameSensor.js b/examples/v3/sensors/renameSensor.js index 957d443..1f1c50e 100644 --- a/examples/v3/sensors/renameSensor.js +++ b/examples/v3/sensors/renameSensor.js @@ -25,12 +25,12 @@ v3.discovery.nupnpSearch() // Update the name of the sensor sensor.name = 'Updated Name Value'; - return api.sensors.rename(sensor) + return api.sensors.renameSensor(sensor) .then(result => { console.log(`\nUpdated sensor name? ${result}\n`); // Obtain the updated sensor from the bridge - return api.sensors.get(sensor); + return api.sensors.getSensor(sensor); }) .then(sensor => { // Display the updated sensor (should be just the name that has changed) diff --git a/lib/api/Groups.test.js b/lib/api/Groups.test.js index 68c64f1..7807c6a 100644 --- a/lib/api/Groups.test.js +++ b/lib/api/Groups.test.js @@ -412,7 +412,7 @@ describe('Hue API #groups', function () { group.name = newName; const result = await hue.groups.updateGroupAttributes(group) - , updatedGroup = await hue.groups.get(group.id) + , updatedGroup = await hue.groups.getGroup(group) ; expect(result).to.be.true; @@ -426,7 +426,7 @@ describe('Hue API #groups', function () { group.lights = newLights; const result = await hue.groups.updateGroupAttributes(group) - , updatedGroup = await hue.groups.get(group) + , updatedGroup = await hue.groups.getGroup(group) ; expect(result).to.be.true; @@ -443,7 +443,7 @@ describe('Hue API #groups', function () { group.lights = newLights; const result = await hue.groups.updateGroupAttributes(group) - , updatedGroup = await hue.groups.get(group) + , updatedGroup = await hue.groups.getGroup(group) ; expect(result).to.be.true; @@ -471,7 +471,7 @@ describe('Hue API #groups', function () { group.name = name; const result = await hue.groups.updateGroupAttributes(group) - , updatedGroup = await hue.groups.get(group) + , updatedGroup = await hue.groups.getGroup(group) ; expect(result).to.be.true; @@ -497,7 +497,7 @@ describe('Hue API #groups', function () { it('should change the class', async () => { group.class = 'Other'; const result = await hue.groups.updateGroupAttributes(group) - , updatedGroup = await hue.groups.get(group) + , updatedGroup = await hue.groups.getGroup(group) ; expect(result).to.be.true; @@ -524,7 +524,7 @@ describe('Hue API #groups', function () { group.name = name; const result = await hue.groups.updateGroupAttributes(group) - , updatedGroup = await hue.groups.get(group) + , updatedGroup = await hue.groups.getGroup(group) ; expect(result).to.be.true; @@ -538,7 +538,7 @@ describe('Hue API #groups', function () { group.lights = lights; const result = await hue.groups.updateGroupAttributes(group) - , updatedGroup = await hue.groups.get(group) + , updatedGroup = await hue.groups.getGroup(group) ; expect(result).to.be.true; @@ -549,7 +549,7 @@ describe('Hue API #groups', function () { it('should change the class', async () => { group.class = 'Nursery'; const result = await hue.groups.updateGroupAttributes(group) - , updatedGroup = await hue.groups.get(group) + , updatedGroup = await hue.groups.getGroup(group) ; expect(result).to.be.true; @@ -575,7 +575,7 @@ describe('Hue API #groups', function () { group.name = name; const result = await hue.groups.updateGroupAttributes(group) - , updatedGroup = await hue.groups.get(group) + , updatedGroup = await hue.groups.getGroup(group) ; expect(result).to.be.true; @@ -589,7 +589,7 @@ describe('Hue API #groups', function () { group.lights = lights; const result = await hue.groups.updateGroupAttributes(group) - , updatedGroup = await hue.groups.get(group) + , updatedGroup = await hue.groups.getGroup(group) ; expect(result).to.be.true; @@ -600,7 +600,7 @@ describe('Hue API #groups', function () { it('should change the class', async () => { group.class = 'Other'; const result = await hue.groups.updateGroupAttributes(group) - , updatedGroup = await hue.groups.get(group) + , updatedGroup = await hue.groups.getGroup(group) ; expect(result).to.be.true; @@ -632,7 +632,7 @@ describe('Hue API #groups', function () { it('should set on state to true', async () => { const lightState = {on: true} , result = await hue.groups.setGroupState(group, lightState) - , groupStatus = await hue.groups.get(group) + , groupStatus = await hue.groups.getGroup(group) ; expect(result).to.be.true; @@ -643,7 +643,7 @@ describe('Hue API #groups', function () { it('should set on state to false using GroupState', async () => { const lightState = new model.lightStates.GroupLightState().off() , result = await hue.groups.setGroupState(group, lightState) - , groupStatus = await hue.groups.get(group) + , groupStatus = await hue.groups.getGroup(group) ; expect(result).to.be.true; diff --git a/lib/api/Lights.test.js b/lib/api/Lights.test.js index 76ecdb7..abf4c07 100644 --- a/lib/api/Lights.test.js +++ b/lib/api/Lights.test.js @@ -8,7 +8,7 @@ const expect = require('chai').expect , testValues = require('../../test/support/testValues.js') ; -describe('Hue API #lights', function() { +describe('Hue API #lights', function () { let hue; @@ -17,7 +17,7 @@ describe('Hue API #lights', function() { before(async () => { const searchResults = await discovery.nupnpSearch(); - if (! searchResults || searchResults.length === 0) { + if (!searchResults || searchResults.length === 0) { throw new Error('Failed to find a bridge in nupnp search'); } @@ -231,7 +231,7 @@ describe('Hue API #lights', function() { }); - describe('#rename()', () => { + describe('rename / renameLight', () => { let light , originalName @@ -245,12 +245,11 @@ describe('Hue API #lights', function() { afterEach('reset light name in bridge', async () => { if (originalName) { light.name = originalName; - await hue.lights.rename(light); + await hue.lights.renameLight(light); } }); - describe('using id and name parameters', () => { - + describe('#rename (deprecated function)', () => { it('should rename a light using id as integer', async () => { const newName = 'Lounge Living Color' , result = await hue.lights.rename(light.id, newName) @@ -285,6 +284,24 @@ describe('Hue API #lights', function() { }); }); + describe('#renameLight()', () => { + + it('should rename a light using id as integer', async () => { + const newName = 'Lounge Living Color'; + light.name = newName; + + const result = await hue.lights.renameLight(light) + , updatedLight = await hue.lights.getLight(light) + ; + + expect(result).to.be.true; + + expect(updatedLight).to.have.property('id').to.equal(light.id); + expect(updatedLight).to.have.property('name').to.equal(newName); + }); + }); + + describe('using light instance', () => { it('should rename a light', async () => { diff --git a/lib/api/Rules.test.js b/lib/api/Rules.test.js index 748d4a4..8c2742c 100644 --- a/lib/api/Rules.test.js +++ b/lib/api/Rules.test.js @@ -38,9 +38,6 @@ describe('Hue API #rules', () => { }; return hue.sensors.createSensor(sensor); }) - .then(result => { - return hue.sensors.get(result.id); - }) .then(sensor => { testSensor = sensor; @@ -87,7 +84,7 @@ describe('Hue API #rules', () => { }); - describe('#get()', () => { + describe('#getRule()', () => { describe('using id value', () => { @@ -96,7 +93,7 @@ describe('Hue API #rules', () => { , target = allRules[0] ; - const rule = await hue.rules.get(target.id); + const rule = await hue.rules.getRule(target.id); expect(rule).to.have.property('id').to.equal(target.id); }); @@ -110,7 +107,7 @@ describe('Hue API #rules', () => { , target = allRules[1] ; - const rule = await hue.rules.get(target); + const rule = await hue.rules.getRule(target); expect(rule).to.have.property('id').to.equal(target.id); }); }); @@ -165,7 +162,7 @@ describe('Hue API #rules', () => { let existing = null; try { - existing = await hue.rules.get(rule); + existing = await hue.rules.getRule(rule); } catch (err) { // Not found is fine expect(err.getHueErrorType()).to.equal(3); @@ -229,9 +226,7 @@ describe('Hue API #rules', () => { describe('#updateRule()', () => { - let ruleId - , rule - ; + let rule; beforeEach(async () => { const newRule = model.createRule(); @@ -240,21 +235,18 @@ describe('Hue API #rules', () => { newRule.addCondition(model.ruleConditions.sensor(testSensor).when('open').equals(true)); newRule.addAction(model.actions.light(targetLight).withState(new LightState().on())); - const result = await hue.rules.createRule(newRule); - ruleId = result.id; - - rule = await hue.rules.get(ruleId); + rule = await hue.rules.createRule(newRule); }); afterEach(async () => { - if (ruleId) { - const existing = await hue.rules.get(ruleId); + if (rule) { + const existing = await hue.rules.getRule(rule); if (existing) { try { - await hue.rules.deleteRule(ruleId); + await hue.rules.deleteRule(rule); } catch (err) { - console.error(`Failed to delete rule: ${ruleId}`); + console.error(`Failed to delete rule: ${rule.id}`); } } } @@ -280,7 +272,7 @@ describe('Hue API #rules', () => { expect(result).to.have.property('actions').to.be.true; expect(result).to.have.property('conditions').to.be.true; - const updatedRule = await hue.rules.get(ruleId); + const updatedRule = await hue.rules.getRule(rule); expect(updatedRule.conditions).to.have.length(1); const condition = updatedRule.conditions[0]; @@ -300,7 +292,7 @@ describe('Hue API #rules', () => { expect(result).to.have.property('actions').to.be.true; expect(result).to.have.property('conditions').to.be.true; - const updatedRule = await hue.rules.get(ruleId); + const updatedRule = await hue.rules.getRule(rule); expect(updatedRule.actions).to.have.length(1); const action = updatedRule.actions[0]; @@ -318,7 +310,7 @@ describe('Hue API #rules', () => { expect(result).to.have.property('actions').to.be.true; expect(result).to.have.property('conditions').to.be.true; - const updatedRule = await hue.rules.get(ruleId); + const updatedRule = await hue.rules.getRule(rule); expect(updatedRule.actions).to.have.length(1); const action = updatedRule.actions[0]; diff --git a/lib/api/Scenes.test.js b/lib/api/Scenes.test.js index 8ad5509..5b56e68 100644 --- a/lib/api/Scenes.test.js +++ b/lib/api/Scenes.test.js @@ -39,7 +39,7 @@ describe('Hue API #scenes', () => { }); - describe('#get()', () => { + describe('#getScene()', () => { let targetScene; @@ -49,7 +49,7 @@ describe('Hue API #scenes', () => { }); it('should get a specific scene', async () => { - const scene = await hue.scenes.get(targetScene.id); + const scene = await hue.scenes.getScene(targetScene); expect(model.isSceneInstance(scene)).to.be.true; expect(scene).to.have.property('id').to.equal(targetScene.id); @@ -58,7 +58,7 @@ describe('Hue API #scenes', () => { it('should fail for invalid scene id', async () => { try { - await hue.scenes.get('1000001'); + await hue.scenes.getScene('1000001'); expect.fail('should not get here'); } catch (err) { expect(err).to.be.instanceof(ApiError); @@ -68,7 +68,7 @@ describe('Hue API #scenes', () => { }); - describe('#getByName()', () => { + describe('getByName / getSceneByName', () => { let validSceneName; @@ -77,18 +77,32 @@ describe('Hue API #scenes', () => { validSceneName = scenes[0].name; }); - it('should get a specific scene', async () => { - const results = await hue.scenes.getByName(validSceneName); + describe('#getByName()', () => { + + it('should get a specific scene', async () => { + const results = await hue.scenes.getByName(validSceneName); - expect(results).to.be.instanceof(Array); - expect(results).to.have.length(1); - expect(results[0]).to.have.property('name').to.equal(validSceneName); + expect(results).to.be.instanceof(Array); + expect(results).to.have.length(1); + expect(results[0]).to.have.property('name').to.equal(validSceneName); + }); }); - it('should fail to find for invalid scene name', async () => { - const result = await hue.scenes.getByName('saldkfnlesfh'); - expect(result).to.be.instanceof(Array); - expect(result).to.have.length(0); + describe('#getSceneByName()', () => { + + it('should get a specific scene', async () => { + const results = await hue.scenes.getSceneByName(validSceneName); + + expect(results).to.be.instanceof(Array); + expect(results).to.have.length(1); + expect(results[0]).to.have.property('name').to.equal(validSceneName); + }); + + it('should fail to find for invalid scene name', async () => { + const result = await hue.scenes.getSceneByName('saldkfnlesfh'); + expect(result).to.be.instanceof(Array); + expect(result).to.have.length(0); + }); }); }); @@ -108,7 +122,7 @@ describe('Hue API #scenes', () => { afterEach(async () => { if (scene) { try { - const existing = await hue.scenes.get(scene); + const existing = await hue.scenes.getScene(scene); await hue.scenes.deleteScene(existing); } catch (err) { // Do nothing @@ -144,7 +158,7 @@ describe('Hue API #scenes', () => { afterEach(async () => { if (scene) { - await hue.scenes.deleteScene(scene.id); + await hue.scenes.deleteScene(scene); } }); @@ -158,7 +172,7 @@ describe('Hue API #scenes', () => { const result = await hue.scenes.update(scene.id, scene); expect(result).to.have.property('name').to.be.true; - const updatedScene = await hue.scenes.get(scene.id); + const updatedScene = await hue.scenes.getScene(scene.id); expect(updatedScene).to.have.property('id').to.equal(scene.id); expect(updatedScene).to.have.property('name').to.equal(updatedName); }); @@ -173,7 +187,7 @@ describe('Hue API #scenes', () => { const result = await hue.scenes.updateScene(scene); expect(result).to.have.property('name').to.be.true; - const updatedScene = await hue.scenes.get(scene.id); + const updatedScene = await hue.scenes.getScene(scene.id); expect(updatedScene).to.have.property('id').to.equal(scene.id); expect(updatedScene).to.have.property('name').to.equal(updatedName); }); diff --git a/lib/api/Schedules.test.js b/lib/api/Schedules.test.js index a33a531..0488d1b 100644 --- a/lib/api/Schedules.test.js +++ b/lib/api/Schedules.test.js @@ -50,7 +50,7 @@ describe('Hue API #schedule', () => { describe('using id', () => { it('should get a specific schedule', async () => { - const schedule = await hue.schedules.get(targetSchedule.id); + const schedule = await hue.schedules.getSchedule(targetSchedule.id); expect(model.isScheduleInstance(schedule)).to.be.true; expect(schedule).to.have.property('id').to.equal(targetSchedule.id); @@ -62,7 +62,7 @@ describe('Hue API #schedule', () => { it('should fail for invalid schedule id', async () => { try { - await hue.schedules.get('65535'); + await hue.schedules.getSchedule('65535'); expect.fail('should not get here'); } catch (err) { expect(err).to.be.instanceof(ApiError); @@ -77,7 +77,7 @@ describe('Hue API #schedule', () => { describe('using Schedule Object', () => { it('should get a specific schedule', async () => { - const schedule = await hue.schedules.get(targetSchedule); + const schedule = await hue.schedules.getSchedule(targetSchedule); expect(model.isScheduleInstance(schedule)).to.be.true; expect(schedule).to.have.property('id').to.equal(targetSchedule.id); @@ -155,7 +155,7 @@ describe('Hue API #schedule', () => { await hue.schedules.getSchedule(result); expect.fail('should have auto deleted from schedules'); } catch (err) { - console.error(err); + // console.error(err); expect(err.getHueErrorType()).to.equal(3); } }); @@ -174,7 +174,7 @@ describe('Hue API #schedule', () => { await waitFor(15 * 1000); try { - await hue.schedules.get(result); + await hue.schedules.getSchedule(result); expect.fail('should have auto deleted from schedules'); } catch (err) { expect(err.getHueErrorType()).to.equal(3); @@ -198,7 +198,7 @@ describe('Hue API #schedule', () => { await waitFor(12 * 1000); // Stop it as it is annoying - const runningSchedule = await hue.schedules.get(result); + const runningSchedule = await hue.schedules.getSchedule(result); runningSchedule.status = 'disabled'; await hue.schedules.updateSchedule(runningSchedule); }); @@ -237,7 +237,7 @@ describe('Hue API #schedule', () => { schedule.name = newName; const result = await hue.schedules.updateSchedule(schedule) - , updatedSchedule = await hue.schedules.get(schedule) + , updatedSchedule = await hue.schedules.getSchedule(schedule) ; expect(result).to.have.property('name').to.be.true; @@ -252,7 +252,7 @@ describe('Hue API #schedule', () => { schedule.description = newDescription; const result = await hue.schedules.updateSchedule(schedule) - , updatedSchedule = await hue.schedules.get(schedule) + , updatedSchedule = await hue.schedules.getSchedule(schedule) ; expect(result).to.have.property('description').to.be.true; @@ -265,7 +265,7 @@ describe('Hue API #schedule', () => { schedule.command = newCommand; const result = await hue.schedules.updateSchedule(schedule) - , updatedSchedule = await hue.schedules.get(schedule) + , updatedSchedule = await hue.schedules.getSchedule(schedule) ; expect(result).to.have.property('command').to.be.true; @@ -278,7 +278,7 @@ describe('Hue API #schedule', () => { schedule.localtime = newTime; const result = await hue.schedules.updateSchedule(schedule) - , updatedSchedule = await hue.schedules.get(schedule) + , updatedSchedule = await hue.schedules.getSchedule(schedule) ; expect(result).to.have.property('localtime').to.be.true; @@ -290,7 +290,7 @@ describe('Hue API #schedule', () => { schedule.status = 'disabled'; const result = await hue.schedules.updateSchedule(schedule) - , updatedSchedule = await hue.schedules.get(schedule) + , updatedSchedule = await hue.schedules.getSchedule(schedule) ; expect(result).to.have.property('status').to.be.true; @@ -302,7 +302,7 @@ describe('Hue API #schedule', () => { schedule.autodelete = false; const result = await hue.schedules.updateSchedule(schedule) - , updatedSchedule = await hue.schedules.get(schedule) + , updatedSchedule = await hue.schedules.getSchedule(schedule) ; expect(result).to.have.property('autodelete').to.be.true; @@ -329,7 +329,7 @@ describe('Hue API #schedule', () => { afterEach(async () => { if (schedule) { try { - const exists = await hue.schedules.get(schedule); + const exists = await hue.schedules.getSchedule(schedule); await hue.schedules.deleteSchedule(exists); } catch (err) { if (err.getHueErrorType() !== 3) { diff --git a/lib/api/Sensors.js b/lib/api/Sensors.js index a9b9e4c..03a88f8 100644 --- a/lib/api/Sensors.js +++ b/lib/api/Sensors.js @@ -26,7 +26,7 @@ module.exports = class Sensors extends ApiDefinition { * @returns {Promise} */ get(id) { - util.deprecatedFunction('5.x', 'sensors..get(id)', 'Use sensors.getSensor(id) instead.'); + util.deprecatedFunction('5.x', 'sensors.get(id)', 'Use sensors.getSensor(id) instead.'); return this.getSensor(id); } @@ -83,7 +83,7 @@ module.exports = class Sensors extends ApiDefinition { return self.execute(sensorsApi.createSensor, {sensor: sensor}) .then(data => { - return self.get(data.id); + return self.getSensor(data.id); }); } diff --git a/lib/api/Sensors.test.js b/lib/api/Sensors.test.js index 6b32031..e0e9f78 100644 --- a/lib/api/Sensors.test.js +++ b/lib/api/Sensors.test.js @@ -60,7 +60,7 @@ describe('Hue API #sensors', () => { describe('#get()', () => { it('should get daylight sensor', async () => { - const result = await hue.sensors.get(1); + const result = await hue.sensors.getSensor(1); expect(result).to.be.instanceOf(Sensor); expect(result).to.have.property('id').to.equal(1); @@ -94,22 +94,22 @@ describe('Hue API #sensors', () => { describe('#createSensor()', () => { - let createdSensorId = -1; + let createdSensor; afterEach('remove the created sensor', async () => { - if (createdSensorId > -1) { - await hue.sensors.deleteSensor(createdSensorId); + if (createdSensor) { + await hue.sensors.deleteSensor(createdSensor); } }); it('should create a new sensor', async () => { const sensor = createClipOpenCloseSensor('testSoftwareSensor') , result = await hue.sensors.createSensor(sensor) - , createdSensor = await hue.sensors.get(result.id) ; + createdSensor = await hue.sensors.getSensor(result.id); + expect(result).to.have.property('id').to.be.greaterThan(0); - createdSensorId = result.id; expect(createdSensor).to.have.property('name').to.equal(sensor.name); expect(createdSensor).to.have.property('modelid').to.equal(sensor.modelid); @@ -120,7 +120,7 @@ describe('Hue API #sensors', () => { }); - describe('#updateName()', () => { + describe('#reanmeSensor()', () => { let sensorId; @@ -136,13 +136,17 @@ describe('Hue API #sensors', () => { }); it('should update the name on an existing sensor', async () => { - const initialSensor = await hue.sensors.get(sensorId) + const initialSensor = await hue.sensors.getSensor(sensorId) , newName = `newName-${Date.now()}` - , result = await hue.sensors.updateName(sensorId, newName) - , sensor = await hue.sensors.get(sensorId) ; - + // Raname the sensor expect(initialSensor.name).to.not.equal(newName); + initialSensor.name = newName; + + const result = await hue.sensors.renameSensor(initialSensor) + , sensor = await hue.sensors.getSensor(sensorId) + ; + expect(result).to.be.true; expect(sensor).to.have.property('name').to.equal(newName); }); @@ -168,7 +172,7 @@ describe('Hue API #sensors', () => { sensor.name = `newName-${Date.now()}`; const result = await hue.sensors.renameSensor(sensor) - , updatedSensor = await hue.sensors.get(sensor) + , updatedSensor = await hue.sensors.getSensor(sensor) ; expect(result).to.be.true; @@ -194,7 +198,7 @@ describe('Hue API #sensors', () => { it('should update the config', async () => { - const sensor = await hue.sensors.get(sensorId); + const sensor = await hue.sensors.getSensor(sensorId); expect(sensor).to.have.property('on'); const initalOnState = sensor.on; @@ -203,7 +207,7 @@ describe('Hue API #sensors', () => { sensor.on = !initalOnState; const result = await hue.sensors.updateSensorConfig(sensor) - , updatedSensor = await hue.sensors.get(sensor.id) + , updatedSensor = await hue.sensors.getSensor(sensor.id) ; expect(result).to.be.true; @@ -229,14 +233,14 @@ describe('Hue API #sensors', () => { it('should update the state', async function () { - const sensor = await hue.sensors.get(sensorId); + const sensor = await hue.sensors.getSensor(sensorId); expect(sensor).to.have.property('open', false); // Update some state values sensor.open = true; const result = await hue.sensors.updateSensorState(sensor) - , updatedSensor = await hue.sensors.get(sensor.id) + , updatedSensor = await hue.sensors.getSensor(sensor.id) ; expect(result).to.have.property('open').to.be.true; expect(updatedSensor).to.have.property('open', true); @@ -245,7 +249,7 @@ describe('Hue API #sensors', () => { it('should update a subset of states when filtered', async () => { - const sensor = await hue.sensors.get(sensorId); + const sensor = await hue.sensors.getSensor(sensorId); expect(sensor).to.have.property('open', false); @@ -254,7 +258,7 @@ describe('Hue API #sensors', () => { // Prevent any state updates to be performed const result = await hue.sensors.updateSensorState(sensor, []) - , updatedSensor = await hue.sensors.get(sensor.id) + , updatedSensor = await hue.sensors.getSensor(sensor.id) ; expect(Object.keys(result)).to.have.length(0); diff --git a/lib/api/http/endpoints/scenes.js b/lib/api/http/endpoints/scenes.js index 69c6d2e..52a478a 100644 --- a/lib/api/http/endpoints/scenes.js +++ b/lib/api/http/endpoints/scenes.js @@ -9,6 +9,8 @@ const ApiEndpoint = require('./endpoint') , util = require('../../../util') ; +const SCENE_ID_PLACEHOLDER = new SceneIdPlaceholder(); + module.exports = { getAll: new ApiEndpoint() @@ -39,7 +41,7 @@ module.exports = { .put() .acceptJson() .uri('//scenes//lightstates/') - .placeholder(new SceneIdPlaceholder()) + .placeholder(SCENE_ID_PLACEHOLDER) .placeholder(new LightIdPlacehoder('lightStateId')) .pureJson() .payload(buildUpdateSceneLightStatePayload) @@ -49,7 +51,7 @@ module.exports = { .get() .acceptJson() .uri('//scenes/') - .placeholder(new SceneIdPlaceholder()) + .placeholder(SCENE_ID_PLACEHOLDER) .pureJson() .postProcess(buildSceneResult), @@ -57,7 +59,7 @@ module.exports = { .delete() .acceptJson() .uri('//scenes/') - .placeholder(new SceneIdPlaceholder()) + .placeholder(SCENE_ID_PLACEHOLDER) .pureJson() .postProcess(validateSceneDeletion), }; @@ -79,8 +81,10 @@ function buildScenesResult(result) { } function buildSceneResult(data, requestParameters) { - const type = data.type.toLowerCase(); - return model.createFromBridge(type, requestParameters.id, data); + const type = data.type.toLowerCase() + , id = SCENE_ID_PLACEHOLDER.getValue(requestParameters) + ; + return model.createFromBridge(type, id, data); } function validateSceneDeletion(result) { diff --git a/lib/model/Light.js b/lib/model/Light.js index e6b355c..632660a 100644 --- a/lib/model/Light.js +++ b/lib/model/Light.js @@ -83,6 +83,7 @@ const ATTRIBUTES = [ ] }), types.string({name: 'swversion'}), + types.string({name: 'swconfigid'}), ]; //TODO add support for making it eassier to set power failure modes config.startup.mode = 'powerfail' diff --git a/lib/model/ResourceLink.js b/lib/model/ResourceLink.js index fd7b42f..4c4f0ae 100644 --- a/lib/model/ResourceLink.js +++ b/lib/model/ResourceLink.js @@ -176,22 +176,32 @@ function processLinks(linkData) { const result = {}; if (linkData) { - linkData.forEach(link => { - const parts = /\/(.*)\/(.*)/.exec(link) - , linkType = parts[1] - , linkId = parts[2] - ; - - const validatedLinkType = validateLinkType(linkType); - - let links = result[validatedLinkType]; - if (!links) { - links = []; + // This is the correct format for the bridge data + if (Array.isArray(linkData)) { + linkData.forEach(link => { + const parts = /\/(.*)\/(.*)/.exec(link) + , linkType = parts[1] + , linkId = parts[2] + ; + + const validatedLinkType = validateLinkType(linkType); + + let links = result[validatedLinkType]; + if (!links) { + links = []; + result[validatedLinkType] = links; + } + + links.push(linkId); + }); + } else { + // We end up here if deserializing our own copy of a resource link + Object.keys(linkData).forEach(key => { + const validatedLinkType = validateLinkType(key); + let links = linkData[key]; result[validatedLinkType] = links; - } - - links.push(linkId); - }); + }); + } } return result; diff --git a/lib/model/Schedule.js b/lib/model/Schedule.js index f1cb6be..8b1373b 100644 --- a/lib/model/Schedule.js +++ b/lib/model/Schedule.js @@ -22,7 +22,7 @@ const ATTRIBUTES = [ types.timePattern({name: 'localtime'}), types.string({name: 'created'}), types.choice({name: 'status', validValues: ['enabled', 'disabled'], defaultValue: 'enabled'}), - types.boolean({name: 'autodelete', defaultValue: true}), + types.boolean({name: 'autodelete'}), types.boolean({name: 'recycle', defaultValue: false}), types.string({name: 'starttime'}), ]; diff --git a/lib/model/groups/Group.test.js b/lib/model/groups/Group.test.js deleted file mode 100644 index 9b3fe38..0000000 --- a/lib/model/groups/Group.test.js +++ /dev/null @@ -1,345 +0,0 @@ -'use strict'; - -const expect = require('chai').expect - , model = require('../index') -; - - -describe('Bridge Model - Group', () => { - - describe('#createFromJson()', () => { - - describe('LightGroup', () => { - - const LIGHTGROUP_PAYLOAD = { - "id": 1, - "name": "VRC 1", - "lights": [ - 2, - 3, - 4, - 5, - 6, - 7, - 8 - ], - "sensors": [], - "type": "LightGroup", - "state": { - "all_on": false, - "any_on": true - }, - "recycle": false, - "action": { - "on": false, - "bri": 61, - "hue": 14988, - "sat": 141, - "effect": "none", - "xy": [ - 0.4575, - 0.4101 - ], - "ct": 366, - "alert": "select", - "colormode": "ct" - }, - "node_hue_api": { - "type": "group", - "version": 1 - } - }; - - it('should process valid payload', () => { - const payload = LIGHTGROUP_PAYLOAD - , group = model.createFromJson(payload) - ; - - expect(model.isGroupInstance(group)).to.be.true; - - expect(group).to.have.property('id').to.equal(payload.id); - expect(group).to.have.property('name').to.equal(payload.name); - expect(group).to.have.property('lights').to.have.members(payload.lights.map(id => Number(id))); - expect(group).to.have.property('sensors').to.be.empty; - expect(group).to.have.property('type').to.equal(payload.type); - expect(group).to.have.property('recycle').to.equal(payload.recycle); - expect(group).to.have.property('action').to.deep.equal(payload.action); - }); - }); - - describe('Zone', () => { - - const ZONE_PAYLOAD = { - "id": 1, - "name": "Testing Zone Creation", - "lights": [2, 3, 4], - "sensors": [], - "type": "Zone", - "state": {"all_on": false, "any_on": true}, - "recycle": false, - "class": "Other", - "action": { - "on": false, - "bri": 254, - "hue": 0, - "sat": 0, - "effect": "none", - "xy": [0.3804, 0.3768], - "ct": 366, - "alert": "select", - "colormode": "ct" - }, - "node_hue_api": {"type": "group", "version": 1} - } - - it('should process valid payload', () => { - const payload = ZONE_PAYLOAD - , group = model.createFromJson(payload) - ; - - expect(model.isGroupInstance(group)).to.be.true; - - expect(group).to.have.property('id').to.equal(payload.id); - expect(group).to.have.property('name').to.equal(payload.name); - expect(group).to.have.property('lights').to.have.members(payload.lights.map(id => Number(id))); - expect(group).to.have.property('sensors').to.be.empty; - expect(group).to.have.property('type').to.equal(payload.type); - expect(group).to.have.property('recycle').to.equal(payload.recycle); - expect(group).to.have.property('class').to.equal(payload.class); - expect(group).to.have.property('action').to.deep.equal(payload.action); - }) - }); - - //TODO - }); - - - describe('#createFromBridge()', () => { - - describe('LightGroup', () => { - - const LIGHTGROUP_PAYLOAD = { - "name": "VRC 1", - "lights": [ - "2", - "3", - "4", - "5", - "6", - "7", - "8" - ], - "sensors": [], - "type": "LightGroup", - "state": { - "all_on": false, - "any_on": true - }, - "recycle": false, - "action": { - "on": false, - "bri": 61, - "hue": 14988, - "sat": 141, - "effect": "none", - "xy": [ - 0.4575, - 0.4101 - ], - "ct": 366, - "alert": "select", - "colormode": "ct" - } - }; - - it('should process valid payload', () => { - const id = 1 - , payload = LIGHTGROUP_PAYLOAD - , group = model.createFromBridge('group', id, payload) - ; - - expect(model.isGroupInstance(group)).to.be.true; - - expect(group).to.have.property('id').to.equal(id); - expect(group).to.have.property('name').to.equal(payload.name); - expect(group).to.have.property('lights').to.have.members(payload.lights.map(id => Number(id))); - expect(group).to.have.property('sensors').to.be.empty; - expect(group).to.have.property('type').to.equal(payload.type); - expect(group).to.have.property('recycle').to.equal(payload.recycle); - expect(group).to.have.property('action').to.deep.equal(payload.action); - //TODO state, all_on and any_on - }); - }); - - - describe('Zone', () => { - - const ZONE_PAYLOAD = { - "name": "Testing Zone Creation", - "lights": [ - "2", - "3", - "4" - ], - "sensors": [], - "type": "Zone", - "state": { - "all_on": false, - "any_on": true - }, - "recycle": false, - "class": "Other", - "action": { - "on": false, - "bri": 254, - "hue": 0, - "sat": 0, - "effect": "none", - "xy": [ - 0.3804, - 0.3768 - ], - "ct": 366, - "alert": "select", - "colormode": "ct" - } - }; - - - it('should process valid payload', () => { - const id = 1 - , payload = ZONE_PAYLOAD - , group = model.createFromBridge('group', id, payload) - ; - - expect(model.isGroupInstance(group)).to.be.true; - expect(group).to.have.property('id').to.equal(id); - expect(group).to.have.property('name').to.equal(payload.name); - expect(group).to.have.property('lights').to.have.members(payload.lights.map(id => Number(id))); - expect(group).to.have.property('sensors').to.be.empty; - expect(group).to.have.property('type').to.equal(payload.type); - expect(group).to.have.property('recycle').to.equal(payload.recycle); - expect(group).to.have.property('class').to.equal(payload.class); - expect(group).to.have.property('action').to.deep.equal(payload.action); - }); - }); - - - describe('Room', () => { - - const ROOM_PAYLAOD = { - "name": "Bedroom Lamps", - "lights": [ - "7", - "8" - ], - "sensors": [], - "type": "Room", - "state": { - "all_on": false, - "any_on": false - }, - "recycle": false, - "class": "Other", - "action": { - "on": false, - "bri": 61, - "hue": 14988, - "sat": 141, - "effect": "none", - "xy": [ - 0.4575, - 0.4101 - ], - "ct": 366, - "alert": "select", - "colormode": "ct" - } - } - }); - - - describe('Entertainment', () => { - - const ENTERTAINMENT_PAYLOAD = { - "name": "Lounge Entertainment", - "lights": [ - "18", - "37", - "38", - "17" - ], - "sensors": [], - "type": "Entertainment", - "state": { - "all_on": true, - "any_on": true - }, - "recycle": false, - "class": "TV", - "stream": { - "proxymode": "manual", - "proxynode": "/lights/22", - "active": false, - "owner": null - }, - "locations": { - "17": [ - -0.65, - -0.84, - 0 - ], - "18": [ - 0.69, - -0.85, - 0 - ], - "37": [ - -0.51, - 0.85, - 0 - ], - "38": [ - 0.44, - 0.84, - 0 - ] - }, - "action": { - "on": true, - "bri": 102, - "hue": 2595, - "sat": 127, - "effect": "none", - "xy": [ - 0.5095, - 0.3624 - ], - "ct": 459, - "alert": "select", - "colormode": "hs" - } - }; - - it('should process valid payload', () => { - const id = 1 - , payload = ENTERTAINMENT_PAYLOAD - , group = model.createFromBridge('group', id, payload) - ; - - expect(model.isGroupInstance(group)).to.be.true; - expect(group).to.have.property('id').to.equal(id); - expect(group).to.have.property('name').to.equal(payload.name); - expect(group).to.have.property('lights').to.have.members(payload.lights.map(id => Number(id))); - expect(group).to.have.property('sensors').to.be.empty; - expect(group).to.have.property('type').to.equal(payload.type); - expect(group).to.have.property('recycle').to.equal(payload.recycle); - expect(group).to.have.property('class').to.equal(payload.class); - expect(group).to.have.property('stream').to.deep.equal(payload.class); - expect(group).to.have.property('locations').to.deep.equal(payload.locations); - expect(group).to.have.property('action').to.deep.equal(payload.action); - console.log(JSON.stringify(group)); - }); - }); - }); -}); \ No newline at end of file diff --git a/lib/model/index.test.js b/lib/model/index.test.js index c8f4e77..88de317 100644 --- a/lib/model/index.test.js +++ b/lib/model/index.test.js @@ -1,335 +1,231 @@ 'use strict'; -const expect = require('chai').expect +const fs = require('fs') + , path = require('path') + , expect = require('chai').expect , model = require('./index') ; +const TEST_DATA_PATH = path.join(__dirname, '../../test/data') + , HUE_DATA_PATH = path.join(TEST_DATA_PATH, 'hue') + , MODEL_DATA_PATH = path.join(TEST_DATA_PATH, 'model') +; -describe('Bridge Model', () => { - - describe('#createFromJson()', () => { - - describe('Light', () => { - - const VALID_LIGHT_PAYLOAD = { - 'id': 1, - 'state': { - 'on': true, - 'bri': 1, - 'hue': 16312, - 'sat': 64, - 'effect': 'none', - 'xy': [ - 0.4523, - 0.4228 - ], - 'alert': 'select', - 'colormode': 'xy', - 'mode': 'homeautomation', - 'reachable': true - }, - 'swupdate': { - 'state': 'notupdatable', - 'lastinstall': null - }, - 'type': 'Color light', - 'name': 'Lounge Living Color', - 'modelid': 'LLC001', - 'manufacturername': 'Philips', - 'productname': 'LivingColors', - 'capabilities': { - 'certified': true, - 'control': { - 'colorgamuttype': 'A', - 'colorgamut': [ - [ - 0.704, - 0.296 - ], - [ - 0.2151, - 0.7106 - ], - [ - 0.138, - 0.08 - ] - ] - }, - 'streaming': { - 'renderer': false, - 'proxy': false - } - }, - 'config': { - 'archetype': 'floorshade', - 'function': 'decorative', - 'direction': 'omnidirectional' - }, - 'uniqueid': '00:aa:11:01:00:09:d0:b1-0b', - 'swversion': '2.0.0.5206', - 'node_hue_api': { - 'type': 'light', - 'version': 1 + +describe('Serialization Tests', () => { + + describe('groups', () => { + + describe('hue bridge payloads', () => { + bridgeSerializationTest({ + parentDirectory: HUE_DATA_PATH, + directoryName: 'groups', + instanceCheckFn: model.isGroupInstance, + typeFn: (payload) => { + return payload.type.toLowerCase() } - }; - - it('should create model object from valid payload', () => { - const light = model.createFromJson(VALID_LIGHT_PAYLOAD); - - expect(light).to.have.property('id').to.equal(VALID_LIGHT_PAYLOAD.id); - expect(light).to.have.property('name').to.equal(VALID_LIGHT_PAYLOAD.name); - expect(light).to.have.property('type').to.equal(VALID_LIGHT_PAYLOAD.type); - expect(light).to.have.property('modelid').to.equal(VALID_LIGHT_PAYLOAD.modelid); - expect(light).to.have.property('manufacturername').to.equal(VALID_LIGHT_PAYLOAD.manufacturername); - expect(light).to.have.property('productname').to.equal(VALID_LIGHT_PAYLOAD.productname); - expect(light).to.have.property('capabilities').to.deep.equal(VALID_LIGHT_PAYLOAD.capabilities); - expect(light).to.have.property('uniqueid').to.equal(VALID_LIGHT_PAYLOAD.uniqueid); - expect(light).to.have.property('swversion').to.equal(VALID_LIGHT_PAYLOAD.swversion); + }); + }); + + + describe('node-hue-api payloads', () => { + modelSerializationTest({ + parentDirectory: MODEL_DATA_PATH, + directoryName: 'groups', + instanceCheckFn: model.isGroupInstance, }); }); }); - describe('#createFromBridge()', () => { + describe('lights', () => { + describe('hue bridge payloads', () => { + bridgeSerializationTest({ + parentDirectory: HUE_DATA_PATH, + directoryName: 'lights', + instanceCheckFn: model.isLightInstance, + typeFn: () => 'light' + }); + }); - describe('Light', () => { + describe('node-hue-api payloads', () => { - const VALID_LIGHT_PAYLOAD = { - 'state': { - 'on': true, - 'bri': 1, - 'hue': 16312, - 'sat': 64, - 'effect': 'none', - 'xy': [ - 0.4523, - 0.4228 - ], - 'alert': 'select', - 'colormode': 'xy', - 'mode': 'homeautomation', - 'reachable': true - }, - 'swupdate': { - 'state': 'notupdatable', - 'lastinstall': null - }, - 'type': 'Color light', - 'name': 'Lounge Living Color', - 'modelid': 'LLC001', - 'manufacturername': 'Philips', - 'productname': 'LivingColors', - 'capabilities': { - 'certified': true, - 'control': { - 'colorgamuttype': 'A', - 'colorgamut': [ - [ - 0.704, - 0.296 - ], - [ - 0.2151, - 0.7106 - ], - [ - 0.138, - 0.08 - ] - ] - }, - 'streaming': { - 'renderer': false, - 'proxy': false - } + modelSerializationTest({ + parentDirectory: MODEL_DATA_PATH, + directoryName: 'lights', + instanceCheckFn: model.isLightInstance, + }); + }); + }); + + + describe('sensors', () => { + + describe('hue bridge payloads', () => { + bridgeSerializationTest({ + parentDirectory: HUE_DATA_PATH, + directoryName: 'sensors', + instanceCheckFn: model.isSensorInstance, + typeFn: (payload) => { + return payload.type.toLowerCase() + } + }); + }); + + describe('node-hue-api payloads', () => { + + modelSerializationTest({ + parentDirectory: MODEL_DATA_PATH, + directoryName: 'sensors', + instanceCheckFn: model.isSensorInstance, + }); + }); + }); + + + describe('schedules', () => { + + describe('hue bridge payloads', () => { + bridgeSerializationTest({ + parentDirectory: HUE_DATA_PATH, + directoryName: 'schedules', + instanceCheckFn: model.isScheduleInstance, + typeFn: () => 'schedule' + }); + }); + + describe('node-hue-api payloads', () => { + + modelSerializationTest({ + parentDirectory: MODEL_DATA_PATH, + directoryName: 'schedules', + instanceCheckFn: model.isScheduleInstance, + }); + }); + }); + + + describe('resourcelinks', () => { + + describe('hue bridge payloads', () => { + bridgeSerializationTest({ + parentDirectory: HUE_DATA_PATH, + directoryName: 'resourcelinks', + instanceCheckFn: model.isResourceLinkInstance, + typeFn: () => 'resourcelink' + }); + }); + + describe('node-hue-api payloads', () => { + + modelSerializationTest({ + parentDirectory: MODEL_DATA_PATH, + directoryName: 'resourcelinks', + instanceCheckFn: model.isResourceLinkInstance, + }); + }); + }); + + + describe('scenes', () => { + + describe('hue bridge payloads', () => { + bridgeSerializationTest({ + parentDirectory: HUE_DATA_PATH, + directoryName: 'scenes', + instanceCheckFn: model.isSceneInstance, + typeFn: (payload) => { + return payload.type.toLowerCase(); }, - 'config': { - 'archetype': 'floorshade', - 'function': 'decorative', - 'direction': 'omnidirectional' + convertIdFn: (id) => { + return `${id}` }, - 'uniqueid': '00:aa:11:01:00:09:d0:b1-0b', - 'swversion': '2.0.0.5206' - }; - - it('should create a light from a valid payload', () => { - const id = 1 - , light = model.createFromBridge('light', id, VALID_LIGHT_PAYLOAD); - - expect(light).to.have.property('id').to.equal(1); - expect(light).to.have.property('name').to.equal(VALID_LIGHT_PAYLOAD.name); - expect(light).to.have.property('type').to.equal(VALID_LIGHT_PAYLOAD.type); - expect(light).to.have.property('modelid').to.equal(VALID_LIGHT_PAYLOAD.modelid); - expect(light).to.have.property('manufacturername').to.equal(VALID_LIGHT_PAYLOAD.manufacturername); - expect(light).to.have.property('productname').to.equal(VALID_LIGHT_PAYLOAD.productname); - expect(light).to.have.property('capabilities').to.deep.equal(VALID_LIGHT_PAYLOAD.capabilities); - expect(light).to.have.property('uniqueid').to.equal(VALID_LIGHT_PAYLOAD.uniqueid); - expect(light).to.have.property('swversion').to.equal(VALID_LIGHT_PAYLOAD.swversion); }); }); + describe('node-hue-api payloads', () => { - describe('Group', () => { - - const VALID_LIGHT_GROUP_PAYLOAD = { - "name": "VRC 1", - "lights": [ - "2", - "3", - "4", - "5", - "6", - "7", - "8" - ], - "sensors": [], - "type": "LightGroup", - "state": { - "all_on": false, - "any_on": true - }, - "recycle": false, - "action": { - "on": false, - "bri": 61, - "hue": 14988, - "sat": 141, - "effect": "none", - "xy": [ - 0.4575, - 0.4101 - ], - "ct": 366, - "alert": "select", - "colormode": "ct" - } - } - , VALID_ZONE_GROUP_PAYLOAD = { - "name": "Testing Zone Creation", - "lights": [ - "2", - "3", - "4" - ], - "sensors": [], - "type": "Zone", - "state": { - "all_on": false, - "any_on": true - }, - "recycle": false, - "class": "Other", - "action": { - "on": false, - "bri": 254, - "hue": 0, - "sat": 0, - "effect": "none", - "xy": [ - 0.3804, - 0.3768 - ], - "ct": 366, - "alert": "select", - "colormode": "ct" - } - } - , VALID_ROOM_GROUP_PAYLOAD = { - "name": "Bedroom Lamps", - "lights": [ - "7", - "8" - ], - "sensors": [], - "type": "Room", - "state": { - "all_on": false, - "any_on": false - }, - "recycle": false, - "class": "Other", - "action": { - "on": false, - "bri": 61, - "hue": 14988, - "sat": 141, - "effect": "none", - "xy": [ - 0.4575, - 0.4101 - ], - "ct": 366, - "alert": "select", - "colormode": "ct" - } - } - , VALID_ENTERTAINMENT_PAYLOAD = { - "name": "Lounge Entertainment", - "lights": [ - "18", - "37", - "38", - "17" - ], - "sensors": [], - "type": "Entertainment", - "state": { - "all_on": true, - "any_on": true - }, - "recycle": false, - "class": "TV", - "stream": { - "proxymode": "manual", - "proxynode": "/lights/22", - "active": false, - "owner": null - }, - "locations": { - "17": [ - -0.65, - -0.84, - 0 - ], - "18": [ - 0.69, - -0.85, - 0 - ], - "37": [ - -0.51, - 0.85, - 0 - ], - "38": [ - 0.44, - 0.84, - 0 - ] - }, - "action": { - "on": true, - "bri": 102, - "hue": 2595, - "sat": 127, - "effect": "none", - "xy": [ - 0.5095, - 0.3624 - ], - "ct": 459, - "alert": "select", - "colormode": "hs" - } - } + modelSerializationTest({ + parentDirectory: MODEL_DATA_PATH, + directoryName: 'scenes', + instanceCheckFn: model.isSceneInstance, + }); + }); + }); +}); + + +function modelSerializationTest(config) { + const directory = config.parentDirectory + , dirName = config.directoryName + , instanceCheckFn = config.instanceCheckFn + ; + + const cwd = path.join(directory, dirName); + + if (!fs.existsSync(cwd)) { + return; + } + + fs.readdirSync(cwd).forEach(file => { + it(`should process model data "${file}"`, () => { + // eslint-disable-next-line global-require + const MODEL_PAYLOAD = require(path.join(cwd, file)) + , modelObject = model.createFromJson(MODEL_PAYLOAD) ; - it('should create a group from a valid payload', () => { + expect(instanceCheckFn(modelObject)).to.be.true; + expect(modelObject.getJsonPayload()).to.deep.equal(MODEL_PAYLOAD); + }); + }); - }); +} + +function bridgeSerializationTest(config) { + + const directory = config.parentDirectory + , dirName = config.directoryName + , instanceCheckFn = config.instanceCheckFn + , typeFn = config.typeFn + , convertIdFn = config.convertIdFn || function (id) { + return id + } + ; - }) + //TODO add validation of above + + const cwd = path.join(directory, dirName); + if (!fs.existsSync(cwd)) { + return; + } + + fs.readdirSync(cwd).forEach(file => { + it(`should process bridge data "${file}"`, () => { + // eslint-disable-next-line global-require + const BRIDGE_PAYLOAD = require(path.join(cwd, file)); + + const id = 2 + , expected = removeNullsAndUndefined(Object.assign({id: convertIdFn(id)}, BRIDGE_PAYLOAD)) + , modelObject = model.createFromBridge(typeFn(BRIDGE_PAYLOAD), id, BRIDGE_PAYLOAD) + ; + + expect(instanceCheckFn(modelObject)).to.equal(true, 'instance check failure'); + expect(modelObject.getHuePayload()).to.deep.equal(expected); + }); }); -}); \ No newline at end of file +} + +function removeNullsAndUndefined(data) { + Object.keys(data).forEach(key => { + if (data[key] === undefined || data[key] === null) { + delete data[key]; + } + + if (data[key] instanceof Object) { + removeNullsAndUndefined(data[key]); + } + }); + + return data; +} \ No newline at end of file diff --git a/lib/model/sensors/CLIPSensor.js b/lib/model/sensors/CLIPSensor.js index d72eec4..172a6ac 100644 --- a/lib/model/sensors/CLIPSensor.js +++ b/lib/model/sensors/CLIPSensor.js @@ -56,4 +56,8 @@ module.exports = class CLIPSensor extends Sensor { set manufacturername(value) { return this.setAttributeValue('manufacturername', value); } + + get recycle() { + return this.getAttributeValue('recycle'); + } }; \ No newline at end of file diff --git a/lib/model/sensors/Sensor.js b/lib/model/sensors/Sensor.js index 9750d60..f389145 100644 --- a/lib/model/sensors/Sensor.js +++ b/lib/model/sensors/Sensor.js @@ -1,40 +1,49 @@ 'use strict'; const BridgeObjectWithId = require('../BridgeObjectWithId') - , parameters = require('../../types') + , types = require('../../types') , util = require('../../util') ; const COMMON_ATTRIBUTES = [ - parameters.int8({name: 'id'}), - parameters.string({name: 'name'}), - parameters.string({name: 'type'}), - parameters.string({name: 'modelid'}), - parameters.string({name: 'manufacturername'}), - parameters.string({name: 'uniqueid'}), - parameters.string({name: 'swversion'}), - parameters.string({name: 'swconfigid'}), //TODO this is not present on many devices - parameters.object({name: 'capabilities'}), + types.int8({name: 'id'}), + types.string({name: 'name'}), + types.string({name: 'type'}), + types.string({name: 'modelid'}), + types.string({name: 'manufacturername'}), + types.string({name: 'uniqueid'}), + types.string({name: 'swversion'}), + types.string({name: 'swconfigid'}), //TODO this is not present on many devices + types.object({name: 'capabilities'}), + + + // TODO this is for zllswitch, need to check other z** sensors and refacotr accordingly + types.string({name: 'productname'}), + types.object({name: 'swupdate'}), + types.string({name: 'diversityid'}), + + //TODO this is for CLIP, need to inject this in the constructor + types.boolean({name: 'recycle'}), ]; const COMMON_STATE_ATTRIBUTES = [ - parameters.string({name: 'lastupdated', defaultValue: 'none'}), + types.string({name: 'lastupdated', defaultValue: 'none'}), ]; const COMMON_CONFIG_ATTRIBUTES = [ - parameters.boolean({name: 'on', defaultValue: true}), + types.boolean({name: 'on', defaultValue: true}), ]; module.exports = class Sensor extends BridgeObjectWithId { //TODO consider removing data from here as we have _populate to do this constructor(configAttributes, stateAttributes, id, data) { - const stateAttribute = parameters.object({ + const stateAttribute = types.object({ name: 'state', types: util.flatten(COMMON_STATE_ATTRIBUTES, stateAttributes) }) - , configAttribute = parameters.object({ + , configAttribute = types.object({ name: 'config', types: util.flatten(COMMON_CONFIG_ATTRIBUTES, configAttributes) }) diff --git a/lib/model/sensors/ZLLSwitch.js b/lib/model/sensors/ZLLSwitch.js index 2af4891..7c03da1 100644 --- a/lib/model/sensors/ZLLSwitch.js +++ b/lib/model/sensors/ZLLSwitch.js @@ -12,7 +12,7 @@ const CONFIG_ATTRIBUTES = [ ]; const STATE_ATTRIBUTES = [ - types.boolean({name: 'buttonevent'}), + types.int16({name: 'buttonevent'}), ]; // Hue Dimmer Switch diff --git a/test/data/hue/groups/entertainment.json b/test/data/hue/groups/entertainment.json new file mode 100644 index 0000000..46aa254 --- /dev/null +++ b/test/data/hue/groups/entertainment.json @@ -0,0 +1,59 @@ +{ + "name": "Lounge Entertainment", + "lights": [ + "18", + "37", + "38", + "17" + ], + "sensors": [], + "type": "Entertainment", + "state": { + "all_on": true, + "any_on": true + }, + "recycle": false, + "class": "TV", + "stream": { + "proxymode": "manual", + "proxynode": "/lights/22", + "active": false, + "owner": null + }, + "locations": { + "17": [ + -0.65, + -0.84, + 0 + ], + "18": [ + 0.69, + -0.85, + 0 + ], + "37": [ + -0.51, + 0.85, + 0 + ], + "38": [ + 0.44, + 0.84, + 0 + ] + }, + "action": { + "on": true, + "bri": 102, + "hue": 2595, + "sat": 127, + "effect": "none", + "xy": [ + 0.5095, + 0.3624 + ], + "ct": 459, + "alert": "select", + "colormode": "hs" + } +} \ No newline at end of file diff --git a/test/data/hue/groups/lightgroup.json b/test/data/hue/groups/lightgroup.json new file mode 100644 index 0000000..9f529bb --- /dev/null +++ b/test/data/hue/groups/lightgroup.json @@ -0,0 +1,33 @@ +{ + "name": "VRC 1", + "lights": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8" + ], + "sensors": [], + "type": "LightGroup", + "state": { + "all_on": false, + "any_on": true + }, + "recycle": false, + "action": { + "on": false, + "bri": 61, + "hue": 14988, + "sat": 141, + "effect": "none", + "xy": [ + 0.4575, + 0.4101 + ], + "ct": 366, + "alert": "select", + "colormode": "ct" + } +} \ No newline at end of file diff --git a/test/data/hue/groups/room.json b/test/data/hue/groups/room.json new file mode 100644 index 0000000..44ee8f3 --- /dev/null +++ b/test/data/hue/groups/room.json @@ -0,0 +1,29 @@ +{ + "name": "Bedroom Lamps", + "lights": [ + "7", + "8" + ], + "sensors": [], + "type": "Room", + "state": { + "all_on": false, + "any_on": false + }, + "recycle": false, + "class": "Other", + "action": { + "on": false, + "bri": 61, + "hue": 14988, + "sat": 141, + "effect": "none", + "xy": [ + 0.4575, + 0.4101 + ], + "ct": 366, + "alert": "select", + "colormode": "ct" + } +} \ No newline at end of file diff --git a/test/data/hue/groups/zone.json b/test/data/hue/groups/zone.json new file mode 100644 index 0000000..9cb06fb --- /dev/null +++ b/test/data/hue/groups/zone.json @@ -0,0 +1,30 @@ +{ + "name": "Testing Zone Creation", + "lights": [ + "2", + "3", + "4" + ], + "sensors": [], + "type": "Zone", + "state": { + "all_on": false, + "any_on": true + }, + "recycle": false, + "class": "Other", + "action": { + "on": false, + "bri": 254, + "hue": 0, + "sat": 0, + "effect": "none", + "xy": [ + 0.3804, + 0.3768 + ], + "ct": 366, + "alert": "select", + "colormode": "ct" + } +} \ No newline at end of file diff --git a/test/data/hue/lights/color_light.json b/test/data/hue/lights/color_light.json new file mode 100644 index 0000000..53b8df1 --- /dev/null +++ b/test/data/hue/lights/color_light.json @@ -0,0 +1,57 @@ +{ + "state": { + "on": false, + "bri": 0, + "hue": 17052, + "sat": 138, + "effect": "none", + "xy": [ + 0.4633, + 0.4507 + ], + "alert": "select", + "colormode": "xy", + "mode": "homeautomation", + "reachable": true + }, + "swupdate": { + "state": "notupdatable", + "lastinstall": null + }, + "type": "Color light", + "name": "Lounge Living Color", + "modelid": "LLC001", + "manufacturername": "Philips", + "productname": "LivingColors", + "capabilities": { + "certified": true, + "control": { + "colorgamuttype": "A", + "colorgamut": [ + [ + 0.704, + 0.296 + ], + [ + 0.2151, + 0.7106 + ], + [ + 0.138, + 0.08 + ] + ] + }, + "streaming": { + "renderer": false, + "proxy": false + } + }, + "config": { + "archetype": "floorshade", + "function": "decorative", + "direction": "omnidirectional" + }, + "uniqueid": "00:17:88:xx:xx:xx:xx:xx-xx", + "swversion": "2.0.0.5206" +} \ No newline at end of file diff --git a/test/data/hue/lights/color_temperature_light.json b/test/data/hue/lights/color_temperature_light.json new file mode 100644 index 0000000..9cbb521 --- /dev/null +++ b/test/data/hue/lights/color_temperature_light.json @@ -0,0 +1,48 @@ +{ + "state": { + "on": true, + "bri": 254, + "ct": 366, + "alert": "none", + "colormode": "ct", + "mode": "homeautomation", + "reachable": false + }, + "swupdate": { + "state": "noupdates", + "lastinstall": "2018-12-20T06:22:29" + }, + "type": "Color temperature light", + "name": "Ensuite Door", + "modelid": "LTW013", + "manufacturername": "Philips", + "productname": "Hue ambiance spot", + "capabilities": { + "certified": true, + "control": { + "mindimlevel": 1000, + "maxlumen": 250, + "ct": { + "min": 153, + "max": 454 + } + }, + "streaming": { + "renderer": false, + "proxy": false + } + }, + "config": { + "archetype": "spotbulb", + "function": "functional", + "direction": "downwards", + "startup": { + "mode": "powerfail", + "configured": true + } + }, + "uniqueid": "00:17:88:01:xx:xx:xx:xx-xx", + "swversion": "1.46.13_r26312", + "swconfigid": "1742FA88", + "productid": "Philips-LTW013-1-GU10CTv1" +} \ No newline at end of file diff --git a/test/data/hue/lights/dimmable_light.json b/test/data/hue/lights/dimmable_light.json new file mode 100644 index 0000000..6513262 --- /dev/null +++ b/test/data/hue/lights/dimmable_light.json @@ -0,0 +1,40 @@ +{ + "state": { + "on": false, + "bri": 128, + "alert": "select", + "mode": "homeautomation", + "reachable": false + }, + "swupdate": { + "state": "noupdates", + "lastinstall": "2018-12-13T20:43:23" + }, + "type": "Dimmable light", + "name": "Hallway Bedroom", + "modelid": "LWB004", + "manufacturername": "Philips", + "productname": "Hue white lamp", + "capabilities": { + "certified": true, + "control": { + "mindimlevel": 2000, + "maxlumen": 750 + }, + "streaming": { + "renderer": false, + "proxy": false + } + }, + "config": { + "archetype": "sultanbulb", + "function": "functional", + "direction": "omnidirectional", + "startup": { + "mode": "powerfail", + "configured": true + } + }, + "uniqueid": "00:17:88:01:xx:xx:xx:xx-xx", + "swversion": "5.127.1.26420" +} \ No newline at end of file diff --git a/test/data/hue/lights/extended_color_light.json b/test/data/hue/lights/extended_color_light.json new file mode 100644 index 0000000..43d3bc5 --- /dev/null +++ b/test/data/hue/lights/extended_color_light.json @@ -0,0 +1,68 @@ +{ + "state": { + "on": false, + "bri": 168, + "hue": 16755, + "sat": 51, + "effect": "none", + "xy": [ + 0.4046, + 0.3915 + ], + "ct": 284, + "alert": "select", + "colormode": "ct", + "mode": "homeautomation", + "reachable": true + }, + "swupdate": { + "state": "noupdates", + "lastinstall": "2018-12-20T19:45:44" + }, + "type": "Extended color light", + "name": "Living Ceiling", + "modelid": "LCT001", + "manufacturername": "Philips", + "productname": "Hue color lamp", + "capabilities": { + "certified": true, + "control": { + "mindimlevel": 5000, + "maxlumen": 600, + "colorgamuttype": "B", + "colorgamut": [ + [ + 0.675, + 0.322 + ], + [ + 0.409, + 0.518 + ], + [ + 0.167, + 0.04 + ] + ], + "ct": { + "min": 153, + "max": 500 + } + }, + "streaming": { + "renderer": true, + "proxy": false + } + }, + "config": { + "archetype": "sultanbulb", + "function": "mixed", + "direction": "omnidirectional", + "startup": { + "mode": "powerfail", + "configured": true + } + }, + "uniqueid": "00:17:88:01:xx:xx:xx:xx-xx", + "swversion": "5.127.1.26581" +} \ No newline at end of file diff --git a/test/data/hue/resourcelinks/rslink.json b/test/data/hue/resourcelinks/rslink.json new file mode 100644 index 0000000..89df4d4 --- /dev/null +++ b/test/data/hue/resourcelinks/rslink.json @@ -0,0 +1,12 @@ +{ + "name": "HueLabs 2.0", + "description": "All installed formulas", + "type": "Link", + "classid": 1, + "owner": "985692e7-xxxx-xxxx-xxxx-30d75c0ab864", + "recycle": false, + "links": [ + "/resourcelinks/48840", + "/resourcelinks/62738" + ] +} \ No newline at end of file diff --git a/test/data/hue/scenes/groupscene.json b/test/data/hue/scenes/groupscene.json new file mode 100644 index 0000000..b4980f0 --- /dev/null +++ b/test/data/hue/scenes/groupscene.json @@ -0,0 +1,29 @@ +{ + "name": "Savana Sunset", + "type": "GroupScene", + "group": "9", + "lights": [ + "2", + "3", + "4", + "5", + "6", + "21", + "37", + "38", + "41", + "44", + "45", + "46" + ], + "owner": "53587a59-92a7-xxxx-xxxx-ab777830b474", + "recycle": false, + "locked": false, + "appdata": { + "version": 1, + "data": "YP0PB_r09_d99" + }, + "picture": "", + "lastupdated": "2019-12-11T21:31:52", + "version": 2 +} \ No newline at end of file diff --git a/test/data/hue/scenes/lightscene.json b/test/data/hue/scenes/lightscene.json new file mode 100644 index 0000000..0121b7e --- /dev/null +++ b/test/data/hue/scenes/lightscene.json @@ -0,0 +1,26 @@ +{ + "name": "Tap scene 1", + "type": "LightScene", + "lights": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15" + ], + "owner": "none", + "recycle": true, + "locked": true, + "picture": "", + "lastupdated": null, + "version": 1 +} \ No newline at end of file diff --git a/test/data/hue/schedules/wakeup.json b/test/data/hue/schedules/wakeup.json new file mode 100644 index 0000000..f92f073 --- /dev/null +++ b/test/data/hue/schedules/wakeup.json @@ -0,0 +1,16 @@ +{ + "name": "Wake up", + "description": "L_04_y0GY0_start wake up", + "command": { + "address": "/api/xxxxxxxxxxxxxxxxxxxxxxxxxxx/sensors/12/state", + "body": { + "flag": true + }, + "method": "PUT" + }, + "localtime": "W124/T06:00:00", + "time": "W124/T06:00:00", + "created": "2018-03-08T22:07:37", + "status": "disabled", + "recycle": true +} \ No newline at end of file diff --git a/test/data/hue/sensors/clip_generic_flag.json b/test/data/hue/sensors/clip_generic_flag.json new file mode 100644 index 0000000..666ae6a --- /dev/null +++ b/test/data/hue/sensors/clip_generic_flag.json @@ -0,0 +1,17 @@ +{ + "state": { + "flag": false, + "lastupdated": "2019-04-09T18:17:00" + }, + "config": { + "on": true, + "reachable": true + }, + "name": "Wake up", + "type": "CLIPGenericFlag", + "modelid": "WAKEUP", + "manufacturername": "jMJW9DmvM1a9m6L180eLKSyv9K4K1V3k", + "swversion": "A_1", + "uniqueid": "L_04_Knte0", + "recycle": false +} \ No newline at end of file diff --git a/test/data/hue/sensors/daylight.json b/test/data/hue/sensors/daylight.json new file mode 100644 index 0000000..3174290 --- /dev/null +++ b/test/data/hue/sensors/daylight.json @@ -0,0 +1,17 @@ +{ + "state": { + "daylight": true, + "lastupdated": "2019-12-15T08:31:00" + }, + "config": { + "on": true, + "configured": true, + "sunriseoffset": 30, + "sunsetoffset": -30 + }, + "name": "Daylight", + "type": "Daylight", + "modelid": "PHDL00", + "manufacturername": "Philips", + "swversion": "1.0" +} \ No newline at end of file diff --git a/test/data/hue/sensors/zgpswitch.json b/test/data/hue/sensors/zgpswitch.json new file mode 100644 index 0000000..4f5186d --- /dev/null +++ b/test/data/hue/sensors/zgpswitch.json @@ -0,0 +1,62 @@ +{ + "state": { + "buttonevent": 34, + "lastupdated": "2019-11-11T16:49:22" + }, + "swupdate": { + "state": "notupdatable", + "lastinstall": null + }, + "config": { + "on": true + }, + "name": "Hue Tap 1", + "type": "ZGPSwitch", + "modelid": "ZGPSWITCH", + "manufacturername": "Philips", + "productname": "Hue tap switch", + "diversityid": "d8cde5d5-0eef-xxxx-xxxx-71ddd2952af4", + "uniqueid": "00:00:00:00:00:xx:xx:xx-xx", + "capabilities": { + "certified": true, + "primary": true, + "inputs": [ + { + "repeatintervals": [], + "events": [ + { + "buttonevent": 34, + "eventtype": "initial_press" + } + ] + }, + { + "repeatintervals": [], + "events": [ + { + "buttonevent": 16, + "eventtype": "initial_press" + } + ] + }, + { + "repeatintervals": [], + "events": [ + { + "buttonevent": 17, + "eventtype": "initial_press" + } + ] + }, + { + "repeatintervals": [], + "events": [ + { + "buttonevent": 18, + "eventtype": "initial_press" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/test/data/hue/sensors/zllswitch.json b/test/data/hue/sensors/zllswitch.json new file mode 100644 index 0000000..1cf6c78 --- /dev/null +++ b/test/data/hue/sensors/zllswitch.json @@ -0,0 +1,123 @@ +{ + "state": { + "buttonevent": 1002, + "lastupdated": "2019-12-14T21:34:45" + }, + "swupdate": { + "state": "batterylow", + "lastinstall": null + }, + "config": { + "alert": "none", + "on": true, + "battery": 79, + "reachable": true, + "pending": [] + }, + "name": "Hue dimmer switch 1", + "type": "ZLLSwitch", + "modelid": "RWL021", + "manufacturername": "Philips", + "productname": "Hue dimmer switch", + "diversityid": "73bbabea-3420-xxxx-xxxx-46bf437e119b", + "swversion": "5.45.1.17846", + "uniqueid": "00:17:88:01:02:xx:xx:xx-xx-fc00", + "capabilities": { + "certified": true, + "primary": true, + "inputs": [ + { + "repeatintervals": [ + 800 + ], + "events": [ + { + "buttonevent": 1000, + "eventtype": "initial_press" + }, + { + "buttonevent": 1001, + "eventtype": "repeat" + }, + { + "buttonevent": 1002, + "eventtype": "short_release" + }, + { + "buttonevent": 1003, + "eventtype": "long_release" + } + ] + }, + { + "repeatintervals": [ + 800 + ], + "events": [ + { + "buttonevent": 2000, + "eventtype": "initial_press" + }, + { + "buttonevent": 2001, + "eventtype": "repeat" + }, + { + "buttonevent": 2002, + "eventtype": "short_release" + }, + { + "buttonevent": 2003, + "eventtype": "long_release" + } + ] + }, + { + "repeatintervals": [ + 800 + ], + "events": [ + { + "buttonevent": 3000, + "eventtype": "initial_press" + }, + { + "buttonevent": 3001, + "eventtype": "repeat" + }, + { + "buttonevent": 3002, + "eventtype": "short_release" + }, + { + "buttonevent": 3003, + "eventtype": "long_release" + } + ] + }, + { + "repeatintervals": [ + 800 + ], + "events": [ + { + "buttonevent": 4000, + "eventtype": "initial_press" + }, + { + "buttonevent": 4001, + "eventtype": "repeat" + }, + { + "buttonevent": 4002, + "eventtype": "short_release" + }, + { + "buttonevent": 4003, + "eventtype": "long_release" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/test/data/model/groups/entertainment.js b/test/data/model/groups/entertainment.js new file mode 100644 index 0000000..58e4103 --- /dev/null +++ b/test/data/model/groups/entertainment.js @@ -0,0 +1,32 @@ +module.exports = { + id: 1, + name: 'Lounge Entertainment', + lights: ['18', '37', '38', '17'], + sensors: [], + type: 'Entertainment', + state: {all_on: true, any_on: true}, + recycle: false, + class: 'TV', + stream: + {proxymode: 'manual', proxynode: '/lights/22', active: false}, + locations: + { + '17': [-0.65, -0.84, 0], + '18': [0.69, -0.85, 0], + '37': [-0.51, 0.85, 0], + '38': [0.44, 0.84, 0] + }, + action: + { + on: true, + bri: 102, + hue: 2595, + sat: 127, + effect: 'none', + xy: [0.5095, 0.3624], + ct: 459, + alert: 'select', + colormode: 'hs' + }, + node_hue_api: {type: 'entertainment', version: 1} +}; \ No newline at end of file diff --git a/test/data/model/groups/lightgroup_model.js b/test/data/model/groups/lightgroup_model.js new file mode 100644 index 0000000..03b438a --- /dev/null +++ b/test/data/model/groups/lightgroup_model.js @@ -0,0 +1,38 @@ +module.exports = { + "id": 1, + "name": "VRC 1", + "lights": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8" + ], + "sensors": [], + "type": "LightGroup", + "state": { + "all_on": false, + "any_on": true + }, + "recycle": false, + "action": { + "on": false, + "bri": 61, + "hue": 14988, + "sat": 141, + "effect": "none", + "xy": [ + 0.4575, + 0.4101 + ], + "ct": 366, + "alert": "select", + "colormode": "ct" + }, + "node_hue_api": { + "type": "lightgroup", + "version": 1 + } +} \ No newline at end of file diff --git a/test/data/model/groups/room.js b/test/data/model/groups/room.js new file mode 100644 index 0000000..bde1625 --- /dev/null +++ b/test/data/model/groups/room.js @@ -0,0 +1,23 @@ +module.exports = { + id: 2, + name: 'Bedroom Lamps', + lights: ['7', '8'], + sensors: [], + type: 'Room', + state: {all_on: false, any_on: false}, + recycle: false, + class: 'Other', + action: + { + on: false, + bri: 61, + hue: 14988, + sat: 141, + effect: 'none', + xy: [0.4575, 0.4101], + ct: 366, + alert: 'select', + colormode: 'ct' + }, + node_hue_api: {type: 'room', version: 1} +}; \ No newline at end of file diff --git a/test/data/model/groups/zone.js b/test/data/model/groups/zone.js new file mode 100644 index 0000000..6e59755 --- /dev/null +++ b/test/data/model/groups/zone.js @@ -0,0 +1,22 @@ +module.exports = { + 'id': 1, + 'name': 'Testing Zone Creation', + 'lights': ['2', '3', '4'], + 'sensors': [], + 'type': 'Zone', + 'state': {'all_on': false, 'any_on': true}, + 'recycle': false, + 'class': 'Other', + 'action': { + 'on': false, + 'bri': 254, + 'hue': 0, + 'sat': 0, + 'effect': 'none', + 'xy': [0.3804, 0.3768], + 'ct': 366, + 'alert': 'select', + 'colormode': 'ct' + }, + 'node_hue_api': {'type': 'zone', 'version': 1} +}; \ No newline at end of file diff --git a/test/data/model/lights/color_light.js b/test/data/model/lights/color_light.js new file mode 100644 index 0000000..bf2accf --- /dev/null +++ b/test/data/model/lights/color_light.js @@ -0,0 +1,30 @@ +module.exports = { + "id": 2, + "state": { + "on": false, + "bri": 0, + "hue": 17052, + "sat": 138, + "effect": "none", + "xy": [0.4633, 0.4507], + "alert": "select", + "colormode": "xy", + "mode": "homeautomation", + "reachable": true + }, + "swupdate": {"state": "notupdatable"}, + "type": "Color light", + "name": "Lounge Living Color", + "modelid": "LLC001", + "manufacturername": "Philips", + "productname": "LivingColors", + "capabilities": { + "certified": true, + "control": {"colorgamuttype": "A", "colorgamut": [[0.704, 0.296], [0.2151, 0.7106], [0.138, 0.08]]}, + "streaming": {"renderer": false, "proxy": false} + }, + "config": {"archetype": "floorshade", "function": "decorative", "direction": "omnidirectional"}, + "uniqueid": "00:17:88:xx:xx:xx:xx:xx-xx", + "swversion": "2.0.0.5206", + "node_hue_api": {"type": "light", "version": 1} +} diff --git a/test/data/model/lights/color_temperature_light.js b/test/data/model/lights/color_temperature_light.js new file mode 100644 index 0000000..581eee5 --- /dev/null +++ b/test/data/model/lights/color_temperature_light.js @@ -0,0 +1,34 @@ +module.exports = { + "id": 2, + "state": { + "on": true, + "bri": 254, + "ct": 366, + "alert": "none", + "colormode": "ct", + "mode": "homeautomation", + "reachable": false + }, + "swupdate": {"state": "noupdates", "lastinstall": "2018-12-20T06:22:29"}, + "type": "Color temperature light", + "name": "Ensuite Door", + "modelid": "LTW013", + "manufacturername": "Philips", + "productname": "Hue ambiance spot", + "capabilities": { + "certified": true, + "control": {"mindimlevel": 1000, "maxlumen": 250, "ct": {"min": 153, "max": 454}}, + "streaming": {"renderer": false, "proxy": false} + }, + "config": { + "archetype": "spotbulb", + "function": "functional", + "direction": "downwards", + "startup": {"mode": "powerfail", "configured": true} + }, + "uniqueid": "00:17:88:01:xx:xx:xx:xx-xx", + "swversion": "1.46.13_r26312", + "swconfigid": "1742FA88", + "productid": "Philips-LTW013-1-GU10CTv1", + "node_hue_api": {"type": "light", "version": 1} +} diff --git a/test/data/model/lights/dimmable_light.js b/test/data/model/lights/dimmable_light.js new file mode 100644 index 0000000..0b90539 --- /dev/null +++ b/test/data/model/lights/dimmable_light.js @@ -0,0 +1,24 @@ +module.exports = { + "id": 200, + "state": {"on": false, "bri": 128, "alert": "select", "mode": "homeautomation", "reachable": false}, + "swupdate": {"state": "noupdates", "lastinstall": "2018-12-13T20:43:23"}, + "type": "Dimmable light", + "name": "Hallway Bedroom", + "modelid": "LWB004", + "manufacturername": "Philips", + "productname": "Hue white lamp", + "capabilities": { + "certified": true, + "control": {"mindimlevel": 2000, "maxlumen": 750}, + "streaming": {"renderer": false, "proxy": false} + }, + "config": { + "archetype": "sultanbulb", + "function": "functional", + "direction": "omnidirectional", + "startup": {"mode": "powerfail", "configured": true} + }, + "uniqueid": "00:17:88:01:xx:xx:xx:xx-xx", + "swversion": "5.127.1.26420", + "node_hue_api": {"type": "light", "version": 1} +} diff --git a/test/data/model/lights/extended_color_light.js b/test/data/model/lights/extended_color_light.js new file mode 100644 index 0000000..27a4c93 --- /dev/null +++ b/test/data/model/lights/extended_color_light.js @@ -0,0 +1,42 @@ +module.exports = { + "id": 2, + "state": { + "on": false, + "bri": 168, + "hue": 16755, + "sat": 51, + "effect": "none", + "xy": [0.4046, 0.3915], + "ct": 284, + "alert": "select", + "colormode": "ct", + "mode": "homeautomation", + "reachable": true + }, + "swupdate": {"state": "noupdates", "lastinstall": "2018-12-20T19:45:44"}, + "type": "Extended color light", + "name": "Living Ceiling", + "modelid": "LCT001", + "manufacturername": "Philips", + "productname": "Hue color lamp", + "capabilities": { + "certified": true, + "control": { + "mindimlevel": 5000, + "maxlumen": 600, + "colorgamuttype": "B", + "colorgamut": [[0.675, 0.322], [0.409, 0.518], [0.167, 0.04]], + "ct": {"min": 153, "max": 500} + }, + "streaming": {"renderer": true, "proxy": false} + }, + "config": { + "archetype": "sultanbulb", + "function": "mixed", + "direction": "omnidirectional", + "startup": {"mode": "powerfail", "configured": true} + }, + "uniqueid": "00:17:88:01:xx:xx:xx:xx-xx", + "swversion": "5.127.1.26581", + "node_hue_api": {"type": "light", "version": 1} +} diff --git a/test/data/model/resourcelinks/rslink.js b/test/data/model/resourcelinks/rslink.js new file mode 100644 index 0000000..e3dc561 --- /dev/null +++ b/test/data/model/resourcelinks/rslink.js @@ -0,0 +1,11 @@ +module.exports = { + "id": 2, + "name": "HueLabs 2.0", + "description": "All installed formulas", + "type": "Link", + "classid": 1, + "owner": "985692e7-xxxx-xxxx-xxxx-30d75c0ab864", + "recycle": false, + "node_hue_api": {"type": "resourcelink", "version": 1}, + "links": {"resourcelinks": ["48840", "62738"]} +} diff --git a/test/data/model/scenes/groupscene.js b/test/data/model/scenes/groupscene.js new file mode 100644 index 0000000..5f34709 --- /dev/null +++ b/test/data/model/scenes/groupscene.js @@ -0,0 +1,15 @@ +module.exports = { + "id": "2", + "type": "GroupScene", + "name": "Savana Sunset", + "group": "9", + "lights": ["2", "3", "4", "5", "6", "21", "37", "38", "41", "44", "45", "46"], + "owner": "53587a59-92a7-xxxx-xxxx-ab777830b474", + "recycle": false, + "locked": false, + "appdata": {"version": 1, "data": "YP0PB_r09_d99"}, + "picture": "", + "lastupdated": "2019-12-11T21:31:52", + "version": 2, + "node_hue_api": {"type": "groupscene", "version": 1} +} diff --git a/test/data/model/scenes/lightscene.js b/test/data/model/scenes/lightscene.js new file mode 100644 index 0000000..cb6d1ab --- /dev/null +++ b/test/data/model/scenes/lightscene.js @@ -0,0 +1,13 @@ +module.exports = { + "id": "2", + "type": "LightScene", + "name": "Tap scene 1", + "lights": ["2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"], + "owner": "none", + "recycle": true, + "locked": true, + "picture": "", + "lastupdated": null, + "version": 1, + "node_hue_api": {"type": "lightscene", "version": 1} +} diff --git a/test/data/model/schedules/schedule.js b/test/data/model/schedules/schedule.js new file mode 100644 index 0000000..3153c30 --- /dev/null +++ b/test/data/model/schedules/schedule.js @@ -0,0 +1,16 @@ +module.exports = { + "id": 2, + "name": "Wake up", + "description": "L_04_y0GY0_start wake up", + "command": { + "address": "/api/xxxxxxxxxxxxxxxxxxxxxxxxxxxxx/sensors/12/state", + "method": "PUT", + "body": {"flag": true} + }, + "localtime": "W124/T06:00:00", + "time": "W124/T06:00:00", + "created": "2018-03-08T22:07:37", + "status": "disabled", + "recycle": true, + "node_hue_api": {"type": "schedule", "version": 1} +} diff --git a/test/data/model/sensors/clip_generic_flag.js b/test/data/model/sensors/clip_generic_flag.js new file mode 100644 index 0000000..dae65ed --- /dev/null +++ b/test/data/model/sensors/clip_generic_flag.js @@ -0,0 +1,13 @@ +module.exports = { + "id": 2, + "type": "CLIPGenericFlag", + "state": {"lastupdated": "2019-04-09T18:17:00", "flag": false}, + "config": {"on": true, "reachable": true}, + "name": "Wake up", + "modelid": "WAKEUP", + "manufacturername": "jMJW9DmvM1a9m6L180eLKSyv9K4K1V3k", + "swversion": "A_1", + "uniqueid": "L_04_Knte0", + "recycle": false, + "node_hue_api": {"type": "clipgenericflag", "version": 1} +} diff --git a/test/data/model/sensors/daylight.js b/test/data/model/sensors/daylight.js new file mode 100644 index 0000000..2d5f198 --- /dev/null +++ b/test/data/model/sensors/daylight.js @@ -0,0 +1,11 @@ +module.exports = { + "id": 2, + "type": "Daylight", + "state": {"lastupdated": "2019-12-15T08:31:00", "daylight": true}, + "config": {"on": true, "configured": true, "sunriseoffset": 30, "sunsetoffset": -30}, + "name": "Daylight", + "modelid": "PHDL00", + "manufacturername": "Philips", + "swversion": "1.0", + "node_hue_api": {"type": "daylight", "version": 1} +} diff --git a/test/data/model/sensors/zgpswitch.js b/test/data/model/sensors/zgpswitch.js new file mode 100644 index 0000000..a98a706 --- /dev/null +++ b/test/data/model/sensors/zgpswitch.js @@ -0,0 +1,25 @@ +module.exports = { + "id": 2, + "type": "ZGPSwitch", + "state": {"lastupdated": "2019-11-11T16:49:22", "buttonevent": 34}, + "swupdate": {"state": "notupdatable"}, + "config": {"on": true}, + "name": "Hue Tap 1", + "modelid": "ZGPSWITCH", + "manufacturername": "Philips", + "productname": "Hue tap switch", + "diversityid": "d8cde5d5-0eef-xxxx-xxxx-71ddd2952af4", + "uniqueid": "00:00:00:00:00:xx:xx:xx-xx", + "capabilities": { + "certified": true, + "primary": true, + "inputs": [{ + "repeatintervals": [], + "events": [{"buttonevent": 34, "eventtype": "initial_press"}] + }, {"repeatintervals": [], "events": [{"buttonevent": 16, "eventtype": "initial_press"}]}, { + "repeatintervals": [], + "events": [{"buttonevent": 17, "eventtype": "initial_press"}] + }, {"repeatintervals": [], "events": [{"buttonevent": 18, "eventtype": "initial_press"}]}] + }, + "node_hue_api": {"type": "zgpswitch", "version": 1} +} diff --git a/test/data/model/sensors/zllswitch.js b/test/data/model/sensors/zllswitch.js new file mode 100644 index 0000000..e0bb3c8 --- /dev/null +++ b/test/data/model/sensors/zllswitch.js @@ -0,0 +1,44 @@ +module.exports = { + "id": 2, + "type": "ZLLSwitch", + "state": {"lastupdated": "2019-12-14T21:34:45", "buttonevent": 1002}, + "swupdate": {"state": "batterylow"}, + "config": {"on": true, "reachable": true, "battery": 79, "alert": "none", "pending": []}, + "name": "Hue dimmer switch 1", + "modelid": "RWL021", + "manufacturername": "Philips", + "productname": "Hue dimmer switch", + "diversityid": "73bbabea-3420-xxxx-xxxx-46bf437e119b", + "swversion": "5.45.1.17846", + "uniqueid": "00:17:88:01:02:xx:xx:xx-xx-fc00", + "capabilities": { + "certified": true, + "primary": true, + "inputs": [{ + "repeatintervals": [800], + "events": [{"buttonevent": 1000, "eventtype": "initial_press"}, { + "buttonevent": 1001, + "eventtype": "repeat" + }, {"buttonevent": 1002, "eventtype": "short_release"}, {"buttonevent": 1003, "eventtype": "long_release"}] + }, { + "repeatintervals": [800], + "events": [{"buttonevent": 2000, "eventtype": "initial_press"}, { + "buttonevent": 2001, + "eventtype": "repeat" + }, {"buttonevent": 2002, "eventtype": "short_release"}, {"buttonevent": 2003, "eventtype": "long_release"}] + }, { + "repeatintervals": [800], + "events": [{"buttonevent": 3000, "eventtype": "initial_press"}, { + "buttonevent": 3001, + "eventtype": "repeat" + }, {"buttonevent": 3002, "eventtype": "short_release"}, {"buttonevent": 3003, "eventtype": "long_release"}] + }, { + "repeatintervals": [800], + "events": [{"buttonevent": 4000, "eventtype": "initial_press"}, { + "buttonevent": 4001, + "eventtype": "repeat" + }, {"buttonevent": 4002, "eventtype": "short_release"}, {"buttonevent": 4003, "eventtype": "long_release"}] + }] + }, + "node_hue_api": {"type": "zllswitch", "version": 1} +} From c553e3b109aa5af6386143f2261dc9c1c1c16183 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:36:57 +0000 Subject: [PATCH 27/35] Updates for generating TypeScript definitions --- Changelog.md | 4 + lib/api/Api.js | 109 ++++++++-- lib/api/Configuration.js | 59 +++++- lib/api/Configuration.test.js | 51 +++-- lib/api/HueApiConfig.js | 78 +++++++- lib/api/Lights.js | 5 +- lib/api/Remote.js | 21 ++ lib/api/ResourceLinks.js | 12 +- lib/api/Scenes.js | 29 +-- lib/api/Schedules.js | 5 + lib/api/Sensors.js | 6 +- lib/api/Users.js | 1 - lib/api/http/LocalBootstrap.js | 29 +++ lib/api/http/OAuthTokens.js | 16 ++ lib/api/http/RemoteApi.js | 98 ++++++++- lib/api/http/RemoteBootstrap.js | 33 ++++ lib/api/http/endpoints/configuration.js | 96 +++------ lib/api/index.js | 19 ++ lib/model/BridgeConfiguration.js | 253 ++++++++++++++++++++++++ lib/model/BridgeObject.js | 46 +++++ lib/model/BridgeObjectWithId.js | 33 ---- lib/model/Light.js | 56 +++++- lib/model/colorGamuts.js | 8 +- lib/model/index.js | 89 +++++---- lib/model/scenes/Scene.js | 61 ++++-- 25 files changed, 994 insertions(+), 223 deletions(-) create mode 100644 lib/model/BridgeConfiguration.js diff --git a/Changelog.md b/Changelog.md index 04e3913..edf1661 100644 --- a/Changelog.md +++ b/Changelog.md @@ -103,6 +103,10 @@ - ResourceLinks API: * New API interacting with `ResourceLinks` via, `api.resourceLinks`, see [documentation](./docs/resourcelinks.md) for more details. +- Configuration API: + * `get()` has been deprecated, use `getConfiguration()` instead + * `update()` has bee depricated, use `updateConfiguration()` instead + - All creation function calls to the bridge will now return the created model object. This change makes it consistent as some calls would return the object, others would return the id but no other data. diff --git a/lib/api/Api.js b/lib/api/Api.js index 1c5a33d..7b2d1e3 100644 --- a/lib/api/Api.js +++ b/lib/api/Api.js @@ -15,28 +15,33 @@ const Capabilities = require('./Capabilities') // , EntertainmentApi = require('./entertainment/EntertainmentApi') , HueApiConfig = require('./HueApiConfig') ; - +/** + * @typedef {import('../model/Light')} Light + * @type {Api} + */ module.exports = class Api { constructor(config, transport, remote) { const self = this; self._config = new HueApiConfig(config, transport, remote); - - self.capabilities = new Capabilities(self); - self.configuration = new Configuration(self); - self.lights = new Lights(self); - self.groups = new Groups(self); - self.sensors = new Sensors(self); - self.schedules = new Schedules(self); - self.scenes = new Scenes(self); - self.users = new Users(self); - self.rules = new Rules(self); - self.resourceLinks = new ResourceLinks(self); + + self._api = { + capabilities: new Capabilities(self), + configuration: new Configuration(self), + lights: new Lights(self), + groups: new Groups(self), + sensors: new Sensors(self), + schedules: new Schedules(self), + scenes: new Scenes(self), + users: new Users(self), + rules: new Rules(self), + resourceLinks: new ResourceLinks(self) + }; // Add the remote API if this is a remote instance of the API if (self._config.isRemote) { - self.remote = new Remote(self); + self._api.remote = new Remote(self); } //TODO initial investigation in to the Streaming API for Entertainment @@ -51,6 +56,68 @@ module.exports = class Api { self.syncWithBridge(); } + /** @returns {Capabilities} */ + get capabilities() { + return this._api.capabilities; + } + + /** @returns {Configuration} */ + get configuration() { + return this._api.configuration; + } + + /** @returns {Lights} */ + get lights() { + return this._api.lights; + } + + /** @returns {Groups} */ + get groups() { + return this._api.groups; + } + + /** @returns {Sensors} */ + get sensors() { + return this._api.sensors; + } + + /** @returns {Schedules} */ + get schedules() { + return this._api.schedules; + } + + /** @returns {Scenes} */ + get scenes() { + return this._api.scenes; + } + + /** @returns {Users} */ + get users() { + return this._api.users; + } + + /** @returns {Rules} */ + get rules() { + return this._api.rules; + } + + /** @returns {ResourceLinks} */ + get resourceLinks() { + return this._api.resourceLinks; + } + + /** + * Obtains the remote API endpoints, this will only be present if you have a remote connection established. + * @returns {Remote|null|undefined} + */ + get remote() { + return this._api.remote; + } + + /** + * Obtains the previously cached state that was obtained from the bridge. + * @returns {Promise|PromiseLike | Promise} + */ getCachedState() { const self = this; @@ -61,14 +128,25 @@ module.exports = class Api { } } + /** + * Checks to see if the API is still syncing with the Hue bridge. + * @returns {boolean} + */ isSyncing() { return this._syncPromise != null; } + /** + * The timestamp of the last sync for the cached state. + * @returns {number} + */ getLastSyncTime() { return this._lastSyncTime; } + /** + * Performs an async synchronization activity with the hue bridge to cache the state of things like lights, etc... + */ syncWithBridge() { const self = this; @@ -89,6 +167,11 @@ module.exports = class Api { } } + /** + * Fetches the light for the specified id from the cached state. + * @param {number|string} id The id of the light to fetch from the cached state. + * @returns {Promise} + */ getLightDefinition(id) { return this.getCachedState().then(() => { return this._state.getLight(id); diff --git a/lib/api/Configuration.js b/lib/api/Configuration.js index a0e21fa..4a43101 100644 --- a/lib/api/Configuration.js +++ b/lib/api/Configuration.js @@ -2,28 +2,81 @@ const configurationApi = require('./http/endpoints/configuration') , ApiDefinition = require('./http/ApiDefinition.js') + , util = require('../util') ; - +/** + * Interacts with the Hue Bridge Configuration. + * + * @typedef {import('../model/BridgeConfiguration)} BridgeConfiguration + * + * @type {Configuration} + */ module.exports = class Configuration extends ApiDefinition { constructor(hueApi) { super(hueApi); } + /** + * Obtains the complete configuration from the Hue Bridge in a raw Object format that is returned from the API. + * This function will return all the config along with all the lights, schedules, groups, scenes, resourcelinks, etc... + * + * @returns {Promise} The raw data returned from the Hue Bridge + */ getAll() { return this.execute(configurationApi.getFullState); } + /** + * Updates a configuration value for the Hue Bridge. + * @param data {Object | BridgeConfiguration} An Object (or BridgeConfiguration) representing the data that is to be + * updated for the bridge configuration. + * @deprecated Use updateConfiguration() instead + * @returns {boolean} + */ update(data) { - return this.execute(configurationApi.updateConfiguration, data); + util.deprecatedFunction('5.x', 'configuration.update(data)', 'Use configuration.updateConfiguration(data) instead'); + return this.updateConfiguration(data); + } + + /** + * Updates a configuration value for the Hue Bridge. + * @param data {Object | BridgeConfiguration} An Object (or BridgeConfiguration) representing the data that is to be + * updated for the bridge configuration. + * @returns {boolean} + */ + updateConfiguration(data) { + return this.execute(configurationApi.updateConfiguration, {config: data}); } + /** + * Obtains the configuration of the Hue Bridge. + * @returns {Object} A object representing the configuration properties of the Hue Bridge. + * @deprecated Use getConfiguration() instead. + */ get() { + util.deprecatedFunction('5.x', 'configuration.get()', 'Use configuration.getConfiguration() instead'); + return this.getConfiguration(); + } + + /** + * Obtains the configuration of the Hue Bridge. + * @returns {Object} A object representing the configuration properties of the Hue Bridge. + * @deprecated Use getConfiguration() instead. + */ + getConfiguration() { return this.execute(configurationApi.getConfiguration); } - //TODO this no longer functions from local + /** + * A virtual press of the link button to perform pairing of software/services. This no longer works on the local + * network connection due to security implications which led to it being disabled by Hue developers. + * + * This will function if you are using the library over the remote API/portal though. + * + * @returns {boolean} + */ pressLinkButton() { return this.execute(configurationApi.updateConfiguration, {linkbutton: true}); } diff --git a/lib/api/Configuration.test.js b/lib/api/Configuration.test.js index 92ae0c6..8b7118d 100644 --- a/lib/api/Configuration.test.js +++ b/lib/api/Configuration.test.js @@ -4,11 +4,10 @@ const expect = require('chai').expect , v3Api = require('../v3').api , discovery = require('../v3').discovery , ApiError = require('../../index').ApiError - , testValues = require('../../test/support/testValues.js') //TODO move these + , model = require('../model') + , testValues = require('../../test/support/testValues.js') ; -//TODO these need updating - describe('Hue API #configuration', () => { let hue; @@ -24,11 +23,6 @@ describe('Hue API #configuration', () => { }); }); - describe('#get()', () => { - - }); - - describe('#getAll()', () => { it('should get all configuration', async () => { @@ -39,42 +33,65 @@ describe('Hue API #configuration', () => { expect(allConfig).to.have.property('rules'); expect(allConfig).to.have.property('sensors'); expect(allConfig).to.have.property('resourcelinks'); - expect(allConfig).to.have.property('config'); + validateConfigStructure(allConfig.config); }); }); - describe('#get()', () => { + describe('#getConfiguration()', () => { it('should get the configuration', async () => { - const config = await hue.configuration.get(); - validateConfigStructure(config); + const config = await hue.configuration.getConfiguration(); + expect(model.isBridgeConfigurationInstance(config)).to.be.true; }); }); - describe('#update()', () => { + describe('#updateConfiguration()', () => { + + afterEach(async () => { + await hue.configuration.updateConfiguration({proxyport: 0}); + }); it('should set the proxy port', async () => { - const result = await hue.configuration.update({'proxyport': 8080}); + const port = 8080 + , result = await hue.configuration.updateConfiguration({'proxyport': port}) + , config = await hue.configuration.getConfiguration() + ; + expect(result).to.be.true; + expect(config).to.have.property('proxyport').to.equal(port); }); - it('should fail to press the link button', async () => { + it('should set the proxy port using BridgeConfiguration', async () => { + const port = 8080 + , updatedConfig = model.createBridgeConfiguration(); + + updatedConfig.proxyport = port; + + const result = await hue.configuration.updateConfiguration(updatedConfig) + , config = await hue.configuration.getConfiguration() + ; + + expect(result).to.be.true; + expect(config).to.have.property('proxyport').to.equal(port); + }); + + it('should fail to press the link button on local network', async () => { try { - await hue.configuration.update({'linkbutton': true}); + await hue.configuration.updateConfiguration({'linkbutton': true}); expect.fail('should not get here'); } catch(err) { expect(err).to.be.instanceof(ApiError); - expect(err.message).to.contain('not modifiable'); } }); }); }); + function validateConfigStructure(config) { expect(config).to.have.property('name'); expect(config).to.have.property('zigbeechannel'); diff --git a/lib/api/HueApiConfig.js b/lib/api/HueApiConfig.js index 4ca5406..af1b823 100644 --- a/lib/api/HueApiConfig.js +++ b/lib/api/HueApiConfig.js @@ -2,6 +2,10 @@ const ApiError = require('../ApiError'); +/** + * @typedef {import('./http/RemoteApi')} RemoteApi + * @type {HueApiConfig} + */ module.exports = class HueApiConfig { constructor(config, transport, remoteApi) { @@ -12,14 +16,27 @@ module.exports = class HueApiConfig { this._isRemote = !! config.remote && !!remoteApi; } + /** + * Is the connection to the hue bridge remote. + * @returns {boolean} + */ get isRemote() { return this._isRemote; } + /** + * Gets the transport implementation that is used to conenct with the Hue Bridge + * @returns {Object} + */ get transport() { return this._transport; } + /** + * Gets the remote API in use that was used to bootstrap the remote connection. + * @returns {RemoteApi}* + * @throws ApiError if the connection is local network. + */ get remote() { if (this.isRemote) { return this._remoteApi; @@ -28,56 +45,97 @@ module.exports = class HueApiConfig { } } + /** + * Gets the current username used to connect/interact with the Hue Bridge. + * @returns {String} The bridge username. + */ get username() { return this._config.username; } + /** + * Gets the client id for the remote OAuth connection. + * @returns {String} The clientId for the remote connection + * @throws ApiError if the connection is not remote. + */ get clientId() { - this.requireRemote(); + this._requireRemote(); return this._config.clientId; } + /** + * Gets the client secret for the remote OAuth connection. + * @returns {String} The client secret for the remote connection. + * @throws ApiError if the connection is not remote. + */ get clientSecret() { - this.requireRemote(); + this._requireRemote(); return this._config.clientSecret; } + /** + * The Base URL for communication with the bridge. + * @returns {String} The base URL for the hue bridge. + */ get baseUrl() { return this._config.baseUrl; } + /** + * Gets the client key for the entertainment API/streaming endpoints + * @returns {String} + * @throws ApiError if the connection is not local network. + */ get clientKey() { - this.requireLocal(); + this._requireLocal(); return this._config.clientkey; } + /** + * Gets the current access token. + * @returns {String} + * @throws ApiError if the connection is not remote. + */ get accessToken() { - this.requireRemote(); + this._requireRemote(); return this._remoteApi.accessToken; } - get accessTokenExpiry() { - this.requireRemote(); + /** + * Gets the expiry timestamp of the access token. + * @returns {number} The timestamp for the expiry or -1 if not known + */ + get accessTokenExpiry() {v + this._requireRemote(); return this._remoteApi.accessTokenExpiry; } + /** + * Gets the current refresh token. + * @returns {String} + * @throws ApiError if the connection is not remote. + */ get refreshToken() { - this.requireRemote(); + this._requireRemote(); return this._remoteApi.refreshToken; } + /** + * Gets the expiry timestamp of the refresh token. + * @returns {number} The timestamp for the expiry or -1 if not known + */ get refreshTokenExpiry() { - this.requireRemote(); + this._requireRemote(); return this._remoteApi.refreshTokenExpiry; } - requireRemote() { + _requireRemote() { if (!this.isRemote) { throw new ApiError('The function in only valid on a remote Hue API instance'); } } - requireLocal() { + _requireLocal() { if (this.isRemote) { throw new ApiError('The function in only valid on a local Hue API instance'); } diff --git a/lib/api/Lights.js b/lib/api/Lights.js index ab30130..01645bf 100644 --- a/lib/api/Lights.js +++ b/lib/api/Lights.js @@ -8,6 +8,9 @@ const Bottleneck = require('bottleneck') , model = require('../model') ; + /** + * @type {Lights} + */ module.exports = class Lights extends ApiDefinition { constructor(hueApi) { @@ -19,7 +22,7 @@ module.exports = class Lights extends ApiDefinition { /** * Gets all the Lights from the Bridge - * @returns {Promise>} + * @returns {Promise>} */ getAll() { return this.execute(lightsApi.getAllLights); diff --git a/lib/api/Remote.js b/lib/api/Remote.js index 6e65b27..bc8f14a 100644 --- a/lib/api/Remote.js +++ b/lib/api/Remote.js @@ -6,10 +6,19 @@ module.exports = class Remote { this._hueApi = hueApi; } + /** + * Exchanges the code for a token on the remote API. + * @param code The code to exchange for a new token. + * @returns {String} The token from the remote API. + */ getToken(code) { return this._getRemoteApi().getToken(code); } + /** + * Will refresh the OAuth tokens on the remote API, exchaning the existing ones for new ones. + * @returns {Object} An object containing the new access and refresh tokens. + */ refreshTokens() { const self = this , remoteApi = self._getRemoteApi() @@ -23,10 +32,20 @@ module.exports = class Remote { }); } + /** + * Creates a new remote user for the Hue Bridge. + * + * @param remoteBridgeId {String} The is of the hue bridge on the remote portal + * @param deviceType {String} The user device type identifier. + */ createRemoteUser(remoteBridgeId, deviceType) { return this._getRemoteApi().createRemoteUsername(remoteBridgeId, deviceType); } + /** + * Obtains the remote access credentials that are in use for the remote connection. + * @returns {{clientId: {String}, clientSecret: {String}, tokens: {}, username: {String}}} + */ getRemoteAccessCredentials() { const config = this._getHueApi()._getConfig(); @@ -54,10 +73,12 @@ module.exports = class Remote { return result; } + /** @private */ _getHueApi() { return this._hueApi; } + /** @private */ _getRemoteApi() { return this._getHueApi()._getRemote(); } diff --git a/lib/api/ResourceLinks.js b/lib/api/ResourceLinks.js index 2f7e940..5b7e4b7 100644 --- a/lib/api/ResourceLinks.js +++ b/lib/api/ResourceLinks.js @@ -3,10 +3,11 @@ const resourceLinksApi = require('./http/endpoints/resourcelinks') , ResourceLink = require('../model/ResourceLink') , ApiDefinition = require('./http/ApiDefinition.js') - , util = require('../util') ; - +/** + * @type {ResourceLinks} + */ module.exports = class ResourceLinks extends ApiDefinition { constructor(hueApi) { @@ -21,12 +22,11 @@ module.exports = class ResourceLinks extends ApiDefinition { } /** - * @param id {int | ResourceLink} + * @param id {string | ResourceLink} The resource link id or resource link to retrieve from the bridge. * @returns {Promise} */ getResourceLink(id) { return this.execute(resourceLinksApi.getResourceLink, {id: id}); - } /** @@ -54,7 +54,7 @@ module.exports = class ResourceLinks extends ApiDefinition { } /** - * @param id {int | ResourceLink} + * @param {string | ResourceLink} id * @returns {Promise} */ deleteResourceLink(id) { @@ -66,7 +66,7 @@ module.exports = class ResourceLinks extends ApiDefinition { } /** - * @param resourceLink {ResourceLink} + * @param {ResourceLink} resourceLink * @returns {Promise} */ updateResourceLink(resourceLink) { diff --git a/lib/api/Scenes.js b/lib/api/Scenes.js index 1852c31..9706db8 100644 --- a/lib/api/Scenes.js +++ b/lib/api/Scenes.js @@ -6,7 +6,12 @@ const scenesApi = require('./http/endpoints/scenes') , model = require('../model') , util = require('../util') ; - +/** + * @typedef {import('../model/scenes/LightScene')} LightScene + * @typedef {import('../model/scenes/GroupScene')} GroupScene + * + * @type {Scenes} + */ module.exports = class Scenes extends ApiDefinition { constructor(hueApi) { @@ -14,7 +19,7 @@ module.exports = class Scenes extends ApiDefinition { } /** - * @returns {Promise} + * @returns {Promise<(LightScene | GroupScene)[]>} */ getAll() { return this.execute(scenesApi.getAll); @@ -29,8 +34,8 @@ module.exports = class Scenes extends ApiDefinition { } /** - * @param id {string | Scene} - * @returns {Promise} + * @param id {string | LightScene | GroupScene} + * @returns {Promise} */ getScene(id) { return this.execute(scenesApi.getScene, {id: id}); @@ -47,7 +52,7 @@ module.exports = class Scenes extends ApiDefinition { /** * Obtains the scenes that have the specified name from the bridge. * @param name {string} - * @returns {Promise} + * @returns {Promise<(LightScene | GroupScene)[]>} */ getSceneByName(name) { return this.getAll().then(allScenes => { @@ -56,8 +61,8 @@ module.exports = class Scenes extends ApiDefinition { } /** - * @param scene {Scene} - * @returns {Promise} + * @param scene {LightScene | GroupScene} + * @returns {Promise<(LightScene | GroupScene)>} */ createScene(scene) { const self = this; @@ -77,8 +82,8 @@ module.exports = class Scenes extends ApiDefinition { } /** - * @param scene {Scene} - * @returns {Promise>} + * @param scene {LightScene | GroupScene} + * @returns {Promise} */ updateScene(scene) { return this.execute(scenesApi.updateScene, {id: scene, scene: scene}); @@ -86,7 +91,7 @@ module.exports = class Scenes extends ApiDefinition { /** * Updates the light state for a specific light in the scene - * @param id {string | Scene} + * @param id {string | LightScene | GroupScene} * @param lightId {int | Light} * @param sceneLightState {SceneLightState} * @returns {Promise} @@ -104,8 +109,8 @@ module.exports = class Scenes extends ApiDefinition { } /** - * @param id {string | Scene} - * @returns {Promsie} + * @param id {string | LightScene | GroupScene} + * @returns {Promise} */ activateScene(id) { // Scene activation is done as an intersection of setting a group light state to a scene id, the intersection of the diff --git a/lib/api/Schedules.js b/lib/api/Schedules.js index dc6e2f1..28344a6 100644 --- a/lib/api/Schedules.js +++ b/lib/api/Schedules.js @@ -5,6 +5,11 @@ const schedulesApi = require('./http/endpoints/schedules') , util = require('../util') ; +/** + * @typedef {import('../model/Schedule')} Schedule + * + * @type {Schedules} + */ module.exports = class Schedules extends ApiDefinition { constructor(hueApi) { diff --git a/lib/api/Sensors.js b/lib/api/Sensors.js index 03a88f8..575741d 100644 --- a/lib/api/Sensors.js +++ b/lib/api/Sensors.js @@ -5,7 +5,11 @@ const sensorsApi = require('./http/endpoints/sensors') , util = require('../util') ; - +/** + * @typedef {import('../model/sensors/Sensor')} Sensor + * + * @type {Sensors} + */ module.exports = class Sensors extends ApiDefinition { constructor(hueApi) { diff --git a/lib/api/Users.js b/lib/api/Users.js index 35d9540..e0cdc4f 100644 --- a/lib/api/Users.js +++ b/lib/api/Users.js @@ -67,7 +67,6 @@ module.exports = class Users extends ApiDefinition { } /** - * * @param appName {string} * @param deviceName {string} * @returns {Promise>} diff --git a/lib/api/http/LocalBootstrap.js b/lib/api/http/LocalBootstrap.js index 6ab3c38..ea7124d 100644 --- a/lib/api/http/LocalBootstrap.js +++ b/lib/api/http/LocalBootstrap.js @@ -11,21 +11,50 @@ const url = require('url') const DEBUG = /node-hue-api/.test(process.env.NODE_DEBUG); +/** + * @typedef {import('./Api)} Api + * @type {LocalBootstrap} + */ module.exports = class LocalBootstrap { + /** + * Create a Local Netowrk Bootstrap for connecting to the Hue Bridge. The connection is ALWAYS over TLS/HTTPS. + * + * @param {String} hostname The hostname or ip address of the hue bridge on the lcoal network. + * @param {number=} port The port number for the connections, defaults to 443 and should not need to be specified in the majority of use cases. + */ constructor(hostname, port) { this._baseUrl = url.format({protocol: 'https', hostname: hostname, port: port || 443}); this._hostname = hostname; } + /** + * Gets the Base URL for the local connection to the bridge. + * @returns {String} + */ get baseUrl() { return this._baseUrl; } + /** + * Gets the hostname being used to connect to the hue bridge (ip address or fully qualified domain name). + * @returns {String} + */ get hostname() { return this._hostname; } + /** + * Connects to the Hue Bridge using the local network. + * + * The connection will perform checks on the Hue Bridge TLS Certificate to verify it is correct before sending any + * sensitive information. + * + * @param username The username to use when connecting, can be null, but will severely limit the endpoints that you can call/access + * @param clientkey The clientkey for the user, used by the entertainment API, can be null + * @param timeout The timeout for requests sent to the Hue Bridge. If not set will default to 20 seconds. + * @returns {Promise} The API for interacting with the hue bridge. + */ connect(username, clientkey, timeout) { const self = this , hostname = self.hostname diff --git a/lib/api/http/OAuthTokens.js b/lib/api/http/OAuthTokens.js index 0f7b46b..d3ff426 100644 --- a/lib/api/http/OAuthTokens.js +++ b/lib/api/http/OAuthTokens.js @@ -30,18 +30,34 @@ module.exports = class OAuthTokens { this._data.refreshToken.expiresAt = expiresAt || -1; } + /** + * Gets the refresh token + * @returns {String} + */ get refreshToken() { return this._data.refreshToken.value; } + /** + * Gets the access token + * @returns {String} + */ get accessToken() { return this._data.accessToken.value; } + /** + * Gets the access token expiry if known + * @returns {number} the timestamp value of the expiry of the access token, or -1 os not known + */ get accessTokenExpiresAt() { return this._data.accessToken.expiresAt; } + /** + * Gets the refresh token expiry if known + * @returns {number} the timestamp value of the expiry of the refresh token, or -1 os not known + */ get refreshTokenExpiresAt() { return this._data.refreshToken.expiresAt; } diff --git a/lib/api/http/RemoteApi.js b/lib/api/http/RemoteApi.js index 4f52669..51eefbc 100644 --- a/lib/api/http/RemoteApi.js +++ b/lib/api/http/RemoteApi.js @@ -10,8 +10,12 @@ const crypto = require('crypto') // This class is a bit different to the other endpoints currently as they operate in a digest challenge for the most // part and also operate off a different base url compared with the local/remote endpoints that make up the rest of the // bridge API commands. - -module.exports = class Remote { +/** + * @typedef {import('./OAuthTokens')} OAuthTokens + * + * @type {RemoteApi} + */ +module.exports = class RemoteApi { constructor(clientId, clientSecret) { this._config = { @@ -23,44 +27,92 @@ module.exports = class Remote { this._tokens = new OAuthTokens(); } + /** + * Get the clientID for the connection. + * @returns {String} The clientID of the remote connection. + */ get clientId() { return this._config.clientId; } + /** + * Gets the clientSecret value for the connection. + * @returns {String} THe client secret for the remote connection. + */ get clientSecret() { return this._config.clientSecret; } + /** + * Gets the base URL for the connection. + * @returns {string} + */ get baseUrl() { return this._config.baseUrl; } + /** + * Gets the Access Token for the remote connection + * @returns {String} The access token. + */ get accessToken() { return this._tokens.accessToken; } + /** + * Gets the expiry timestamp value for the expiry of the access token. + * @returns {number|null|undefined} + */ get accessTokenExpiry() { return this._tokens.accessTokenExpiresAt; } + /** + * Gets the Refresh Token for the remote connection, that can be exchanged for new refreshed tokens + * @returns {String} The refresh token. + */ get refreshToken() { return this._tokens.refreshToken; } + /** + * Gets the expiry timestamp value for the expiry of the refresh token. + * @returns {number|null|undefined} + */ get refreshTokenExpiry() { return this._tokens.refreshTokenExpiresAt; } + /** + * Sets the access token for the remote connection + * @param {String} token The access token. + * @param {number=} expiry The timestamp value of the expiry of the access token, optional + * @returns {RemoteApi} + */ setAccessToken(token, expiry) { this._tokens._setAccessToken(token, expiry); return this; } + /** + * Sets the refresh token for the remote connection + * @param {String} token The refresh token. + * @param {number=} expiry The timestamp value of the expiry of the refresh token, optional + * @returns {RemoteApi} + */ setRefreshToken(token, expiry) { this._tokens._setRefreshToken(token, expiry); return this; } + /** + * Builds the digest response to pass to the remote API for the provided request details. + * @param {String} realm + * @param {String} nonce + * @param {String} method HTTP method for the request + * @param {String} path The path for the request + * @returns {String} The digest hash value for the provided data + */ getDigestResponse(realm, nonce, method, path) { const clientId = this.clientId , clientSecret = this.clientSecret @@ -79,6 +131,14 @@ module.exports = class Remote { return hash; } + /** + * Constructs the digest authorization header value from the provided details. + * @param {String} realm + * @param {String} nonce + * @param {String} method + * @param {String} path + * @returns {string} The value to be used for the "Authorization" Header. + */ getAuthorizationHeaderDigest(realm, nonce, method, path) { const clientId = this.clientId , response = this.getDigestResponse(realm, nonce, method, path) @@ -86,7 +146,14 @@ module.exports = class Remote { return `Digest username="${clientId}", realm="${realm}", nonce="${nonce}", uri="${path}", response="${response}"`; } - // This is really poor for security, only including it if I need to allow users to use it in the future, but push them to digest... + /** + * Constructs the basic authorization header value from the provided details. + * + * This is really poor for security, it is only included to complete the implementation of the APIs, you are strongly + * advised to use the digest authorization instead. + + * @returns {string} The value to be used for the "Authorization" Header. + */ getAuthorizationHeaderBasic() { const clientId = this.clientId , clientSecret = this.clientSecret @@ -95,6 +162,11 @@ module.exports = class Remote { return `Basic ${encoded}`; } + /** + * Exchanges the code for OAuth tokens. + * @param code The authorization code that is provided as part of the OAuth flow. + * @returns {Promise} The OAuth Tokens obtained from the remote portal. + */ getToken(code) { const self = this , requestConfig = { @@ -126,6 +198,18 @@ module.exports = class Remote { }); } + /** + * Refreshes the existing tokens by exchangin the current refresh token for new access and refresh tokens. + * + * After calling this the old tokens will no longer be valid. The new tokens obtained will be injected back into the + * API for future calls. + * + * You should ensure you save the new tokens in place of the previous ones that you used to establish the original + * remote connection. + * + * @param refreshToken The refresh token to exchange for new tokens. + * @returns {Promise} The new refreshed tokens. + */ refreshTokens(refreshToken) { const self = this , requestConfig = { @@ -158,6 +242,12 @@ module.exports = class Remote { }); } + /** + * Creates a new remote user + * @param {number=} remoteBridgeId The id of the hue bridge in the remote portal, usually 0. + * @param {String=} deviceType The user device type identifier (this is shown to the end users on the remote access portal). If not specified will default to 'node-hue-api-remote'. + * @returns {Promise} The new remote username. + */ createRemoteUsername(remoteBridgeId, deviceType) { const self = this , accessToken = self.accessToken @@ -198,6 +288,7 @@ module.exports = class Remote { }); } + /** @private */ _respondWithDigest(err, requestConfig) { // We need this information to build the digest Authorization header and get the nonce that we can use for the // request that will be properly validated and issue us the authorization tokens. @@ -221,6 +312,7 @@ module.exports = class Remote { return axios.request(requestConfig); } + /** @private */ _processTokens(start, data) { this.setAccessToken(data.access_token, start + (data.access_token_expires_in * 1000)); this.setRefreshToken(data.refresh_token, start + (data.refresh_token_expires_in * 1000)); diff --git a/lib/api/http/RemoteBootstrap.js b/lib/api/http/RemoteBootstrap.js index fa54ee1..c6e3c79 100644 --- a/lib/api/http/RemoteBootstrap.js +++ b/lib/api/http/RemoteBootstrap.js @@ -7,6 +7,9 @@ const axios = require('axios') , ApiError = require('../../ApiError') ; +/** + * @type {RemoteBootstrap} + */ module.exports = class RemoteBootstrap { constructor(clientId, clientSecret) { @@ -15,6 +18,13 @@ module.exports = class RemoteBootstrap { this.remoteApi = new RemoteApi(clientId, clientSecret); } + /** + * Obtains the AuthCode URL that can be used to request OAuth tokens for your user/application details + * @param {String} deviceId The device ID of the remote application. + * @param {String} appId The application ID of the remote application. + * @param {String} state A unique state value that will be provided back to you in the reponse payload to prevent against cross-site forgeries. + * @returns {string} The URL that can be used to start the exchange for OAuth tokens. + */ getAuthCodeUrl(deviceId, appId, state) { if (! deviceId) { throw new ApiError('A unique deviceid is required for your application when accessing the Remote API'); @@ -31,6 +41,19 @@ module.exports = class RemoteBootstrap { return `${this.remoteApi.baseUrl}/oauth2/auth?clientid=${this.clientId}&state=${state}&deviceid=${deviceId}&appid=${appId}&response_type=code`; } + /** + * Connects to the Remote API using the provided access code, exchanging it for valid OAuth tokens that can be used + * to connect again in the future. + * + * This function is used to bootstrap the first connection to the remote API for a new application. + * + * @param {String} code The authorization code obtained from the callback made by the remote portal to your application + * @param {String} username The username for the remote application. + * @param {number=} timeout The timeout for the access token request to the remote API, defaults to 12 seconds + * @param {String=} deviceType The device type for the application connection. + * @param {number=}remoteBridgeId The id of the bridge in the remote portal, defaults to 0. + * @returns {Promise} + */ connectWithCode(code, username, timeout, deviceType, remoteBridgeId) { const self = this; @@ -47,6 +70,15 @@ module.exports = class RemoteBootstrap { }); } + /** + * Connects to the Remote API using the provided OAuth tokens that were previously obtained. + * @param {String} accessToken The OAuth access token. + * @param {String} refreshToken The OAuth refresh token. + * @param {String} username The remote username used to connect with hue bridge + * @param {number=} timeout The timeout for the access token request to the remote API, defaults to 12 seconds + * @param {String=} deviceType The device type for the application connection. + * @returns {Promise} + */ connectWithTokens(accessToken, refreshToken, username, timeout, deviceType) { const self = this; @@ -65,6 +97,7 @@ module.exports = class RemoteBootstrap { }); } + /** @private */ _getRemoteApi(username, timeout) { const self = this , baseUrl = `${self.remoteApi.baseUrl}/bridge` diff --git a/lib/api/http/endpoints/configuration.js b/lib/api/http/endpoints/configuration.js index 05b0bdc..753e18e 100644 --- a/lib/api/http/endpoints/configuration.js +++ b/lib/api/http/endpoints/configuration.js @@ -4,61 +4,46 @@ const ApiEndpoint = require('./endpoint') , UsernamePlaceholder = require('../../../placeholders/UsernamePlaceholder') , util = require('../../../util') , ApiError = require('../../../ApiError') + , model = require('../../../model') ; -function createUser() { - return new ApiEndpoint() +module.exports = { + createUser: new ApiEndpoint() .post() .acceptJson() .uri('/') .payload(buildUserPayload) .pureJson() - .postProcess(processCreateUser); -} + .postProcess(processCreateUser), -function getConfiguration() { - return new ApiEndpoint() + getConfiguration: new ApiEndpoint() .get() .acceptJson() .uri('//config') - .pureJson(); -} + .pureJson() + .postProcess(createBridgeConfiguration), -function updateConfiguration() { - return new ApiEndpoint() + updateConfiguration: new ApiEndpoint() .put() .acceptJson() .uri('//config') .payload(buildConfigurationPayload) .pureJson() - .postProcess(processConfigurationUpdate); -} + .postProcess(util.wasSuccessful), -function deleteUser() { - return new ApiEndpoint() + deleteUser: new ApiEndpoint() .delete() .acceptJson() - .uri('//config/whitelist/') - .placeholder(new UsernamePlaceholder('element')) - // .errorHandler(deleteUserErrorHandler) + .uri('//config/whitelist/') + .placeholder(new UsernamePlaceholder('userid')) .pureJson() - .postProcess(processDeleteUser); -} + .postProcess(util.wasSuccessful), -function getFullState() { - return new ApiEndpoint() + getFullState: new ApiEndpoint() .get() .acceptJson() .uri('/') - .pureJson(); -} - -module.exports = { - createUser: createUser(), - getConfiguration: getConfiguration(), - updateConfiguration: updateConfiguration(), - deleteUser: deleteUser(), - getFullState: getFullState() + .pureJson() }; @@ -70,22 +55,12 @@ function processCreateUser(data) { } } -// function deleteUserErrorHandler(err) { -// if (err.getHueErrorType() === 1) { -// throw new ApiError(`Failed to locate user to delete`); -// } else { -// console.error(err); -// } -// } - -function processDeleteUser(data) { - if (util.wasSuccessful(data)) { - return true; - } - return false; +function createBridgeConfiguration(data) { + return model.createFromBridge('configuration', null, data); } function buildUserPayload(data) { + //TODO utilize the type system //TODO perform validation on the strings here // applicationName 0..20 // deviceName 0...19 @@ -105,30 +80,17 @@ function buildUserPayload(data) { } function buildConfigurationPayload(parameters) { - const result = { - type: 'application/json', - body: {} - } - , body = result.body - ; - - if (parameters.linkbutton) { - body.linkbutton = true; - } + const config = parameters.config; - // {name: "proxyport", type: "uint16", optional: true}, - // {name: "name", type: "string", minLength: 4, maxLength: 16, optional: true}, - // {name: "swupdate", type: "object", optional: true}, - // {name: "proxyaddress", type: "string", maxLength: 40, optional: true}, - // {name: "linkbutton", type: "boolean", optional: true}, - // {name: "ipaddress", type: "string", optional: true}, - // {name: "netmask", type: "string", optional: true}, - // {name: "gateway", type: "string", optional: true}, - // {name: "dhcp", type: "boolean", optional: true}, - // {name: "portalservices", type: "boolean", optional: true} - return result; -} + let bridgeConfig; + if (model.isBridgeConfigurationInstance(config)) { + bridgeConfig = config; + } else { + bridgeConfig = createBridgeConfiguration(config); + } -function processConfigurationUpdate(data) { - return util.wasSuccessful(data); + return { + type: 'application/json', + body: bridgeConfig.getHuePayload() + }; } \ No newline at end of file diff --git a/lib/api/index.js b/lib/api/index.js index 85aab90..f167ac2 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -5,14 +5,33 @@ const RemoteBootstrap = require('./http/RemoteBootstrap') , LocalInsecureBootstrap = require('./http/LocalInsecureBootstrap') ; + +/** + * Creates a remote bootstrap to connect with a Hue bridge remotely + * @param {String} clientId The OAuth client id for your application. + * @param {String} clientSecret The OAuth client secret for your application. + * @returns {RemoteBootstrap} + */ module.exports.createRemote = function(clientId, clientSecret) { return new RemoteBootstrap(clientId, clientSecret); }; +/** + * Creates a local network bootstrap to connect with Hue bridge on a local network. + * @param {String} host The IP Address or FQDN of the he bridge you are connecting to. + * @param {number=} port The port number to connect to, optional. + * @returns {LocalBootstrap} + */ module.exports.createLocal = function(host, port) { return new LocalBootstrap(host, port); }; +/** + * Creates a local network bootstrap over an insecure HTTP connection. + * @param {String} host The IP Address or FQDN of the he bridge you are connecting to. + * @param {number=} port The port number to connect to, optional. + * @returns {LocalInsecureBootstrap} + */ module.exports.createInsecureLocal = function(host, port) { return new LocalInsecureBootstrap(host, port); }; \ No newline at end of file diff --git a/lib/model/BridgeConfiguration.js b/lib/model/BridgeConfiguration.js new file mode 100644 index 0000000..2e7c026 --- /dev/null +++ b/lib/model/BridgeConfiguration.js @@ -0,0 +1,253 @@ +'use strict'; + +const BridgeObject = require('./BridgeObject') + , types = require('../types') +; + +const ATTRIBUTES = [ + // Modifiable Attributes + types.uint16({name: 'proxyport'}), + types.string({name: 'proxyaddress', minLength: 0, maxLength: 40}), + types.string({name: 'name', minLength: 4, maxLength: 16}), + types.boolean({name: 'linkbutton'}), // Only works on the portal not in local network + types.string({name: 'ipaddress'}), + types.string({name: 'netmask'}), + types.string({name: 'gateway'}), + types.boolean({name: 'dhcp'}), + types.string({name: 'timezone'}), + types.boolean({name: 'touchlink'}), + types.choice({name: 'zigbeechannel', validValues: [11, 15, 20, 25]}), + types.string({name: 'UTC'}), + + // R/O attributes + types.object({ + name: 'swupdate2', + types: [ + types.boolean({name: 'checkforupdate'}), + types.string({name: 'lastchange'}), // This is an iso time format + types.choice({ + name: 'state', + validValues: [ + 'unknown', + 'noupdates', + 'transferring', + 'anyreadytoinstall', + 'allreadtoinstall', + 'installing', + ] + }), + types.object({ + name: 'autoinstall', + types: [ + types.string({name: 'updatetime'}), + types.boolean({name: 'on'}), + ] + }), + types.object({ + name: 'bridge', + types: [ + types.string({name: 'state'}), + types.string({name: 'lastinstall'}), + ] + }), + ] + }), + types.object({name: 'whitelist'}), + types.object({ + name: 'portalstate', + types: [ + types.boolean({name: 'signedon'}), + types.boolean({name: 'incoming'}), + types.boolean({name: 'outgoing'}), + types.string({name: 'communication'}), + ] + }), + types.object({ + name: 'internetservices', + types: [ + types.choice({name: 'internet', validValues: ['connected', 'disconnected']}), + types.choice({name: 'remoteaccess', validValues: ['connected', 'disconnected']}), + types.choice({name: 'time', validValues: ['connected', 'disconnected']}), + types.choice({name: 'swupdate', validValues: ['connected', 'disconnected']}), + ] + }), + types.object({ + name: 'backup', + types: [ + types.choice({ + name: 'status', + validValues: ['idle', 'startmigration', 'fileready_disabled', 'prepare_restore', 'restoring'] + }), + types.uint8({name: 'errorcode'}), + ] + }), + types.string({name: 'apiversion'}), + types.string({name: 'swversion'}), + types.string({name: 'mac'}), + types.string({name: 'modelid'}), + types.string({name: 'bridgeid'}), + types.boolean({name: 'factorynew'}), + types.string({name: 'replacesbridgeid'}), + types.string({name: 'datastoreversion'}), + types.string({name: 'starterkitid'}), +]; + +/** + * @typedef { import('../types/Type') } Type + * @type {BridgeObjectWithId} + */ +module.exports = class BridgeConfiguration extends BridgeObject { + + constructor() { + super(ATTRIBUTES); + } + + set proxyport(value) { + return this.setAttributeValue('proxyport', value); + } + + set proxyaddress(value) { + return this.setAttributeValue('proxyaddress', value); + } + + set name(value) { + return this.setAttributeValue('name', value); + } + + set linkbutton(value) { + return this.setAttributeValue('linkbutton', value); + } + + set ipaddress(value) { + return this.setAttributeValue('ipaddress', value); + } + + set netmask(value) { + return this.setAttributeValue('netmask', value); + } + + set gateway(value) { + return this.setAttributeValue('gateway', value); + } + + set dhcp(value) { + return this.setAttributeValue('dhcp', value); + } + + set timezone(value) { + return this.setAttributeValue('timezone', value); + } + + set touchlink(value) { + return this.setAttributeValue('touchlink', value); + } + + set zigbeechannel(value) { + return this.setAttributeValue('zigbeechannel', value); + } + + /** + * Sets the time in UTC on the bridge, but only if there is internet connection (as it will use the internet for the time) + * @param value An iso time format + * @returns {BridgeObject} + */ + set UTC(value) { + return this.setAttributeValue('UTC', value); + } + + get proxyport() { + return this.getAttributeValue('proxyport'); + } + + get proxyaddress() { + return this.getAttributeValue('proxyaddress'); + } + + get name() { + return this.getAttributeValue('name'); + } + + get linkbutton() { + return this.getAttributeValue('linkbutton'); + } + + get ipaddress() { + return this.getAttributeValue('ipaddress'); + } + + get netmask() { + return this.getAttributeValue('netmask'); + } + + get gateway() { + return this.getAttributeValue('gateway'); + } + + get dhcp() { + return this.getAttributeValue('dhcp'); + } + + get timezone() { + return this.getAttributeValue('timezone'); + } + + get zigbeechannel() { + return this.getAttributeValue('zigbeechannel'); + } + + get UTC() { + return this.getAttributeValue('UTC'); + } + + get swupdate2() { + return this.getAttributeValue('swupdate2'); + } + + get whitelist() { + return this.getAttributeValue('whitelist'); + } + + get internetservices() { + return this.getAttributeValue('internetservices'); + } + + get backup() { + return this.getAttributeValue('backup'); + } + + get apiversion() { + return this.getAttributeValue('apiversion'); + } + + get swversion() { + return this.getAttributeValue('swversion'); + } + + get mac() { + return this.getAttributeValue('mac'); + } + + get modelid() { + return this.getAttributeValue('modelid'); + } + + get bridgeid() { + return this.getAttributeValue('bridgeid'); + } + + get factorynew() { + return this.getAttributeValue('factorynew'); + } + + get replacesbridgeid() { + return this.getAttributeValue('replacesbridgeid'); + } + + get datastoreversion() { + return this.getAttributeValue('datastoreversion'); + } + + get starterkitid() { + return this.getAttributeValue('starterkitid'); + } +}; \ No newline at end of file diff --git a/lib/model/BridgeObject.js b/lib/model/BridgeObject.js index 7db0ffb..e939437 100644 --- a/lib/model/BridgeObject.js +++ b/lib/model/BridgeObject.js @@ -71,6 +71,52 @@ module.exports = class BridgeObject { return result; } + /** + * Obtains a node-hue-api specific JSON payload of the BridgeObject. This can be used for serialization purposes. + * + * This functionality exists to support use cases where server backends need to send data to a web based client to + * work around CORS or custom backend functionality, whilst preserving and providing reusability of the API objects. + * + * @returns {Object} A node-hue-api specific payload that represents the Bridge Object, this can be reconstructed into + * a valid BridgeObject instance via the model.createFromJson() function. + */ + getJsonPayload() { + const data = this._bridgeData; + + data.node_hue_api = { + type: this.constructor.name.toLowerCase(), + version: 1 + }; + + return data; + } + + /** + * Obtains a Hue API compatible representation of the Bridge Object that can be used over the RESTful API. + * @returns {Object} The payload that is compatible with the Hue RESTful API documentation. + */ + getHuePayload() { + const result = {}; + + Object.keys(this._attributes).forEach(name => { + const value = this.getAttributeValue(name); + if (value !== null && value !== undefined) { + result[name] = value; + } + }); + + return result; + } + + /** + * @returns {any | {}} + * @private + */ + get _bridgeData() { + // Return a copy so that it cannot be modified from outside + return Object.assign({}, this._data); + } + /** * @param data {*} * @returns {BridgeObject} diff --git a/lib/model/BridgeObjectWithId.js b/lib/model/BridgeObjectWithId.js index d07826f..dc6d732 100644 --- a/lib/model/BridgeObjectWithId.js +++ b/lib/model/BridgeObjectWithId.js @@ -30,37 +30,4 @@ module.exports = class BridgeObjectWithId extends BridgeObject { get id() { return this.getAttributeValue('id'); } - - getJsonPayload() { - const data = this._bridgeData; - - data.node_hue_api = { - type: this.constructor.name.toLowerCase(), - version: 1 - }; - - return data; - } - - getHuePayload() { - const result = {}; - - Object.keys(this._attributes).forEach(name => { - const value = this.getAttributeValue(name); - if (value !== null && value !== undefined) { - result[name] = value; - } - }); - - return result; - } - - /** - * @returns {any | {}} - * @private - */ - get _bridgeData() { - // Return a copy so that it cannot be modified from outside - return Object.assign({}, this._data); - } }; diff --git a/lib/model/Light.js b/lib/model/Light.js index 632660a..5203561 100644 --- a/lib/model/Light.js +++ b/lib/model/Light.js @@ -86,66 +86,115 @@ const ATTRIBUTES = [ types.string({name: 'swconfigid'}), ]; -//TODO add support for making it eassier to set power failure modes config.startup.mode = 'powerfail' +//TODO add support for making it easier to set power failure modes config.startup.mode = 'powerfail' +/** + * @type {Light} + */ module.exports = class Light extends BridgeObjectWithId { constructor(id) { super(ATTRIBUTES, id); } + /** @returns {number} */ get id() { return this.getAttributeValue('id'); } + /** + * @returns {string} + */ get name() { return this.getAttributeValue('name'); } + /** + * @param {string} value + * @returns {Light} + */ set name(value) { return this.setAttributeValue('name', value); } + /** + * @returns {String} + */ get type() { return this.getAttributeValue('type'); } + /** + * @returns {String} + */ get modelid() { return this.getAttributeValue('modelid'); } + /** + * @returns {string} + */ get manufacturername() { return this.getAttributeValue('manufacturername'); } + /** + * @returns {string} + */ get uniqueid() { return this.getAttributeValue('uniqueid'); } + /** + * @returns {string} + */ get productid() { return this.getAttributeValue('productid'); } + /** + * @returns {string} + */ get productname() { return this.getAttributeValue('productname'); } + /** + * @returns {string} + */ get swversion() { return this.getAttributeValue('swversion'); } + /** + * @returns {string} + */ get swupdate() { return this.getAttributeValue('swupdate'); } + /** + * @returns {Object} + */ get state() { return this.getAttributeValue('state'); } + /** + * @returns {Object} + */ get capabilities() { return this.getAttributeValue('capabilities'); } + /** + * @typedef {Object} colorGamut + * @property {number[]} red + * @property {number[]} green + * @property {number[]} blue + * + * @returns {colorGamut} + */ get colorGamut() { if (this.mappedColorGamut && this.mappedColorGamut !== '2200K-6500K') { return colorGamuts.getColorGamut(this.mappedColorGamut); @@ -154,6 +203,10 @@ module.exports = class Light extends BridgeObjectWithId { } } + /** + * Gets the supported states that the light will accept + * @returns {string[]} + */ getSupportedStates() { const states = Object.keys(this.state); @@ -170,6 +223,7 @@ module.exports = class Light extends BridgeObjectWithId { return states; } + /** @private */ _populate(data) { if (data) { this.mappedColorGamut = getColorGamut(data); diff --git a/lib/model/colorGamuts.js b/lib/model/colorGamuts.js index ea8dc18..9afc220 100644 --- a/lib/model/colorGamuts.js +++ b/lib/model/colorGamuts.js @@ -24,13 +24,13 @@ module.exports = COLOR_GAMUTS; module.exports.getColorGamut = (values) => { return { - red: convertArrayToColorGaumut(values[0]), - green: convertArrayToColorGaumut(values[1]), - blue: convertArrayToColorGaumut(values[2]) + red: convertArrayToColorGamut(values[0]), + green: convertArrayToColorGamut(values[1]), + blue: convertArrayToColorGamut(values[2]) }; }; -function convertArrayToColorGaumut(arr) { +function convertArrayToColorGamut(arr) { return { x: arr[0], y: arr[1] diff --git a/lib/model/index.js b/lib/model/index.js index 9c2b393..58b5489 100644 --- a/lib/model/index.js +++ b/lib/model/index.js @@ -7,6 +7,8 @@ const ApiError = require('../ApiError') , Capabilities = require('./Capabilities') + , BridgeConfiguration = require('./BridgeConfiguration') + , Light = require('./Light') , Group = require('./groups/Group') @@ -56,6 +58,8 @@ const TYPES_TO_MODEL = { capabilities: Capabilities, + configuration: BridgeConfiguration, + entertainment: Entertainment, lightgroup: LightGroup, lightsource: Lightsource, @@ -95,59 +99,67 @@ module.exports.timePatterns = timePatterns; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Instance Check Functions -module.exports.isLightInstance= function(obj) { +module.exports.isLightInstance = function (obj) { return obj instanceof Light; }; -module.exports.isSceneInstance = function(obj) { +module.exports.isSceneInstance = function (obj) { return obj instanceof Scene; }; -module.exports.isGroupSceneInstance = function(obj) { +module.exports.isGroupSceneInstance = function (obj) { return obj instanceof GroupScene; }; -module.exports.isLightSceneInstance = function(obj) { +module.exports.isLightSceneInstance = function (obj) { return obj instanceof LightScene; }; -module.exports.isRuleInstance = function(obj) { +module.exports.isRuleInstance = function (obj) { return obj instanceof Rule; }; -module.exports.isResourceLinkInstance = function(obj) { +module.exports.isResourceLinkInstance = function (obj) { return obj instanceof ResourceLink; }; -module.exports.isScheduleInstance = function(obj) { +module.exports.isScheduleInstance = function (obj) { return obj instanceof Schedule; }; -module.exports.isSensorInstance = function(obj) { +module.exports.isSensorInstance = function (obj) { return obj instanceof Sensor; }; -module.exports.isGroupInstance = function(obj) { +module.exports.isGroupInstance = function (obj) { return obj instanceof Group; }; +module.exports.isBridgeConfigurationInstance = function (obj) { + return obj instanceof BridgeConfiguration; +}; + + +module.exports.createBridgeConfiguration = function () { + return new BridgeConfiguration(); +} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Groups -module.exports.createEntertainment = function() { +module.exports.createEntertainment = function () { return new Entertainment(); }; -module.exports.createLightGroup = function() { +module.exports.createLightGroup = function () { return new LightGroup(); }; -module.exports.createRoom = function() { +module.exports.createRoom = function () { return new Room(); }; -module.exports.createZone = function() { +module.exports.createZone = function () { return new Zone(); }; @@ -155,48 +167,47 @@ module.exports.createZone = function() { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Sensors -module.exports.createCLIPGenericFlagSensor = function() { +module.exports.createCLIPGenericFlagSensor = function () { return new CLIPGenericFlag(); }; -module.exports.createCLIPGenericStatusSensor = function() { +module.exports.createCLIPGenericStatusSensor = function () { return new CLIPGenericStatus(); }; -module.exports.createCLIPHumiditySensor = function() { +module.exports.createCLIPHumiditySensor = function () { return new CLIPHumidity(); }; -module.exports.createCLIPLightlevelSensor = function() { +module.exports.createCLIPLightlevelSensor = function () { return new CLIPLightlevel(); }; -module.exports.createCLIPOpenCloseSensor = function() { +module.exports.createCLIPOpenCloseSensor = function () { return new CLIPOpenCLose(); }; -module.exports.createCLIPPresenceSensor = function() { +module.exports.createCLIPPresenceSensor = function () { return new CLIPPresence(); }; -module.exports.createCLIPTemperatureSensor = function() { +module.exports.createCLIPTemperatureSensor = function () { return new CLIPTemperature(); }; -module.exports.createCLIPSwitchSensor = function() { +module.exports.createCLIPSwitchSensor = function () { return new CLIPSwitch(); }; - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Scenes -module.exports.createLightScene = function() { +module.exports.createLightScene = function () { return new LightScene(); }; -module.exports.createGroupScene = function() { +module.exports.createGroupScene = function () { return new GroupScene(); }; @@ -204,7 +215,7 @@ module.exports.createGroupScene = function() { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Schedules -module.exports.createSchedule = function() { +module.exports.createSchedule = function () { return new Schedule(); } @@ -213,19 +224,19 @@ module.exports.createSchedule = function() { // Actions module.exports.actions = { - light: function(light) { + light: function (light) { return new LightStateAction(light); }, - group: function(group) { + group: function (group) { return new GroupStateAction(group); }, - sensor: function(sensor) { + sensor: function (sensor) { return new SensorStateAction(sensor); }, - scene: function(scene) { + scene: function (scene) { return new SceneAction(scene); } }; @@ -234,16 +245,16 @@ module.exports.actions = { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Rules -module.exports.createRule = function() { +module.exports.createRule = function () { return new Rule(); }; module.exports.ruleConditions = { - sensor: function(sensor) { + sensor: function (sensor) { return new SensorCondition(sensor); }, - group: function(id) { + group: function (id) { return new GroupCondition(id); }, }; @@ -251,22 +262,22 @@ module.exports.ruleConditions = { module.exports.ruleConditionOperators = conditionOperators; module.exports.ruleActions = { - light: function(light) { + light: function (light) { util.deprecatedFunction('5.x', 'model.ruleActions.light(light)', 'Use model.actions.light(light) instead'); return new LightStateAction(light); }, - group: function(group) { + group: function (group) { util.deprecatedFunction('5.x', 'model.ruleActions.group(group)', 'Use model.actions.group(group) instead'); return new GroupStateAction(group); }, - sensor: function(sensor) { + sensor: function (sensor) { util.deprecatedFunction('5.x', 'model.ruleActions.sensor(sensor)', 'Use model.actions.sensor(sensor) instead'); return new SensorStateAction(sensor); }, - scene: function(scene) { + scene: function (scene) { util.deprecatedFunction('5.x', 'model.ruleActions.scene(scene)', 'Use model.actions.scene(scene) instead'); return new SceneAction(scene); } @@ -276,7 +287,7 @@ module.exports.ruleActions = { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // ResourceLinks -module.exports.createResourceLink = function() { +module.exports.createResourceLink = function () { return new ResourceLink(); }; @@ -284,7 +295,7 @@ module.exports.createResourceLink = function() { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Creation Functions - Generic -module.exports.createFromBridge = function(type, id, payload) { +module.exports.createFromBridge = function (type, id, payload) { const ModelObject = TYPES_TO_MODEL[type]; if (!ModelObject) { @@ -311,7 +322,7 @@ module.exports.createFromJson = function (payload) { , version = payloadDataType.version || 0 ; - if (! type) { + if (!type) { throw new ApiError('Invalid payload, missing type from the Data Type'); } diff --git a/lib/model/scenes/Scene.js b/lib/model/scenes/Scene.js index 66c9e3d..e167652 100644 --- a/lib/model/scenes/Scene.js +++ b/lib/model/scenes/Scene.js @@ -27,67 +27,104 @@ module.exports = class Scene extends BridgeObjectWithId { this.setAttributeValue('type', type); } + /** + * @returns {String} + */ get name() { return this.getAttributeValue('name'); } + /** + * @param {string} value + * @returns {Scene} + */ set name(value) { return this.setAttributeValue('name', value) } - // get lightstates() { - // return this.getAttributeValue('lightstates'); - // } - // - // set lightstates(value) { - // //TODO needs to be updated - // - // // //TODO needs to be an {id: {}, id: {}} type object - // // this._updateRawDataValue('type', null); - // // return this._updateRawDataValue('lightstates', value); - // } - + /** + * @returns {String} + */ get type() { return this.getAttributeValue('type'); } + /** + * + * @returns {String} + */ get owner() { return this.getAttributeValue('owner'); } + /** + * @returns {boolean} + */ get recycle() { return this.getAttributeValue('recycle'); } + /** + * + * @param {boolean} value + * @returns {Scene} + */ set recycle(value) { return this.setAttributeValue('recycle', value); } + /** + * @returns {boolean} + */ get locked() { return this.getAttributeValue('locked'); } + /** + * @typedef AppData + * @property {number} version + * @property {string} data + * + * @returns {AppData} + */ get appdata() { // Complex object of version, data return this.getAttributeValue('appdata'); } + /** + * @param {AppData} value + * @returns {Scene} + */ set appdata(value) { return this.setAttributeValue('appdata', value); } + /** + * @param {string} value + * @returns {Scene} + */ set picture(value) { return this.setAttributeValue('picture', value); } + /** + * @returns {String} + */ get picture() { return this.getAttributeValue('picture'); } + /** + * @returns {String} + */ get lastupdated() { return this.getAttributeValue('lastupdated'); } + /** + * @returns {number} + */ get version() { return this.getAttributeValue('version'); } From 2c2fb0ab938854d16cbf7dc5766b4d997cf39315 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 16 Dec 2019 20:08:24 +0000 Subject: [PATCH 28/35] - Adding test for RGB --- lib/api/Lights.test.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/api/Lights.test.js b/lib/api/Lights.test.js index abf4c07..721117f 100644 --- a/lib/api/Lights.test.js +++ b/lib/api/Lights.test.js @@ -655,6 +655,32 @@ describe('Hue API #lights', function () { }); }); + describe('#rgb', () => { + + function testRGB(red, green, blue, xy) { + return async () => { + const id = testValues.testLightId + , state = new LightState().on().rgb(red, green, blue) + , result = await hue.lights.setLightState(id, state) + , finalLightState = await hue.lights.getLightState(id) + ; + + expect(result).to.be.true; + expect(finalLightState).to.have.property('on').to.be.true; + + expect(finalLightState).to.have.property('colormode').to.equal('xy'); + expect(finalLightState).to.have.property('xy'); + expect(finalLightState.xy[0]).to.be.closeTo(xy[0], 0.001); + expect(finalLightState.xy[1]).to.be.closeTo(xy[1], 0.001); + }; + } + + it('should set rgb to red', testRGB(255, 0, 0, [0.6484, 0.3309])); + + it('should set rgb to green', testRGB(0, 255, 0, [0.3157, 0.5906])); + + it('should set rgb to blue', testRGB(0, 0, 255, [0.153, 0.048])); + }); //TODO complete all the property tests for a light state }); From 7ad01cf29b43a8f8b2619faff29933ddea0c4116 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 16 Dec 2019 20:08:59 +0000 Subject: [PATCH 29/35] - Fixing error in handling of payload data, as it needs to support an object as well as group object --- lib/api/http/endpoints/groups.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/api/http/endpoints/groups.js b/lib/api/http/endpoints/groups.js index 55c45f9..9805700 100644 --- a/lib/api/http/endpoints/groups.js +++ b/lib/api/http/endpoints/groups.js @@ -157,7 +157,12 @@ function buildGroupAttributeBody(parameters) { throw new ApiError('A group is required to update attributes') } - const payload = group.getHuePayload(); + let payload; + if (model.isGroupInstance(group)) { + payload = group.getHuePayload(); + } else { + payload = group; + } ['name', 'lights', 'class'].forEach(key => { if (payload[key]) { From 87ca119f6bb41a3260a8abfb7ade83ff4ce60f41 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 16 Dec 2019 20:09:21 +0000 Subject: [PATCH 30/35] - New object for bridge configuration data --- lib/model/BridgeConfiguration.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/model/BridgeConfiguration.js b/lib/model/BridgeConfiguration.js index 2e7c026..6acdb7e 100644 --- a/lib/model/BridgeConfiguration.js +++ b/lib/model/BridgeConfiguration.js @@ -20,6 +20,7 @@ const ATTRIBUTES = [ types.string({name: 'UTC'}), // R/O attributes + types.string({name: 'localtime'}), types.object({ name: 'swupdate2', types: [ @@ -53,6 +54,8 @@ const ATTRIBUTES = [ ] }), types.object({name: 'whitelist'}), + types.boolean({name: 'portalservices'}), + types.string({name: 'portalconnection'}), types.object({ name: 'portalstate', types: [ @@ -155,6 +158,22 @@ module.exports = class BridgeConfiguration extends BridgeObject { return this.setAttributeValue('UTC', value); } + get portalservices() { + return this.getAttributeValue('portalservices'); + } + + get portalconnection() { + return this.getAttributeValue('portalconnection'); + } + + get portalstate() { + return this.getAttributeValue('portalstate'); + } + + get localtime() { + return this.getAttributeValue('localtime'); + } + get proxyport() { return this.getAttributeValue('proxyport'); } From 3b5dc2773fd1bd452c3e6cf9890688994972ebfb Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 16 Dec 2019 20:09:55 +0000 Subject: [PATCH 31/35] - Fixing issues with color gamuts that made this nott work as intended from previous refactoring --- lib/model/colorGamuts.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/model/colorGamuts.js b/lib/model/colorGamuts.js index 9afc220..e08e670 100644 --- a/lib/model/colorGamuts.js +++ b/lib/model/colorGamuts.js @@ -23,16 +23,5 @@ const COLOR_GAMUTS = { module.exports = COLOR_GAMUTS; module.exports.getColorGamut = (values) => { - return { - red: convertArrayToColorGamut(values[0]), - green: convertArrayToColorGamut(values[1]), - blue: convertArrayToColorGamut(values[2]) - }; -}; - -function convertArrayToColorGamut(arr) { - return { - x: arr[0], - y: arr[1] - }; -} \ No newline at end of file + return COLOR_GAMUTS[values]; +}; \ No newline at end of file From aac7a4fcbec4603757328e1ffc7335cc5bae1c45 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Mon, 16 Dec 2019 20:10:55 +0000 Subject: [PATCH 32/35] 4.0.0-alpha-3 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 892b805..4e5ea13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "node-hue-api", - "version": "4.0.0-alpha-2", + "version": "4.0.0-alpha-3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 2c401f8..5e60b3b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-hue-api", "description": "Philips Hue API Library for Node.js", - "version": "4.0.0-alpha-2", + "version": "4.0.0-alpha-3", "author": "Peter Murray ", "contributors": [ { From c673bda56195967c33635e2ae861cefd624c0b86 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Fri, 20 Dec 2019 09:06:58 +0000 Subject: [PATCH 33/35] Adding v2 compatibility notes --- README.md | 13 +++++++++++++ lib/util.js | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/README.md b/README.md index 99ad64e..0c2ea46 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ documented Hue API. - [Connections to the Bridge](#connections-to-the-bridge) - [Rate Limiting](#rate-limiting) - [Debug Bridge Communications](#debug-bridge-communications) + - [v2 Compatibility](#v2-api-compatibility) - [API Documentation](#api-documentation) - [Discovering Local Hue Bridges](docs/discovery.md) - [Remote API Support](docs/remoteApi.md) @@ -166,6 +167,18 @@ The above warning applies here with respect to schedule when **not** in debug mo username value (that can be used to authenticate against the bridge) in the payloads of the `command`. +## v2 API Compatibility +In the version 4.x releases of this library all backwards compatibility to the much older Q promise and callback +functionality was removed (as was indicated in the 3.x documentation). + +What was provided in the 3.x versions of this library to provide some backward comaptibility has now been moved into +another library [node-hue-api-v2-shim](https://github.com/peter-murray/node-hue-api-v2-shim). + +_The `node-hue-api-v2-shim` is only provided to allow you to continue to use the older v2 API functionality in code you +may have had previously written and there are downsides to using it. You are strongly encouraged to migrate to the v3 +API provided in this library (which is where any new features and improvements will be made going forward)._ + + ## API Documentation - [Discovering Local Hue Bridges](docs/discovery.md) - [Remote API Support](docs/remoteApi.md) diff --git a/lib/util.js b/lib/util.js index 3590cb2..f199614 100644 --- a/lib/util.js +++ b/lib/util.js @@ -4,6 +4,8 @@ const ApiError = require('./ApiError.js') , HueError = require('./HueError') //TODO consider remove the use of this here now ; +const suppressDeprecationWarnings = process.env.NODE_HUE_API_SUPPRESS_DEPRICATION_WARNINGS || false; + module.exports = { parseErrors: parseErrors, wasSuccessful: wasSuccessful, @@ -135,6 +137,10 @@ function mergeArrays() { } function deprecatedFunction(version, func, message) { + if (suppressDeprecationWarnings) { + return; + } + console.log(`**************************************************************************************************`); console.log(`Deprecated Function Usage: ${func}\n`); console.log(` ${message}\n`); From ea4670d44f3b4ae6401859d077ca9c99ec55791d Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Fri, 20 Dec 2019 09:07:36 +0000 Subject: [PATCH 34/35] 4.0.0-beta.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e5ea13..a7a8f49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "node-hue-api", - "version": "4.0.0-alpha-3", + "version": "4.0.0-beta.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5e60b3b..17b4732 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-hue-api", "description": "Philips Hue API Library for Node.js", - "version": "4.0.0-alpha-3", + "version": "4.0.0-beta.1", "author": "Peter Murray ", "contributors": [ { From ac814bbbac26b04441af3eeaf20f327474de61e6 Mon Sep 17 00:00:00 2001 From: Peter Murray <681306+peter-murray@users.noreply.github.com> Date: Fri, 27 Dec 2019 14:42:07 +0000 Subject: [PATCH 35/35] Updting Tests via GitHub Actions --- .github/workflows/nodejs.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index c077663..ff7da6d 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,13 +1,15 @@ -name: Node CI +name: Node Tests on: [push] jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: + max-parallel: 4 matrix: + os: [ubuntu-18.04, macos-latest, windows-latest] node-version: [10.x, 12.x] steps: