From bd4a8385e09f80b4549e9f521f4f6de706d09934 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Sat, 1 Jul 2023 23:48:31 +0100 Subject: [PATCH] [hue] Implement CLIP 2 / API v2 (#13570) * [hue] Implement CLIP 2 / API v2 --------- Signed-off-by: Andrew Fiddian-Green --- CODEOWNERS | 2 +- bundles/org.openhab.binding.hue/README.md | 404 +- bundles/org.openhab.binding.hue/doc/hue2.png | Bin 0 -> 113337 bytes .../org.openhab.binding.hue/doc/readme_v1.md | 358 + .../org.openhab.binding.hue/doc/readme_v2.md | 237 + .../hue/internal/HueBindingConstants.java | 82 +- .../hue/internal/action/DynamicsActions.java | 86 + .../internal/config/Clip2BridgeConfig.java | 30 + .../hue/internal/config/Clip2ThingConfig.java | 25 + .../hue/internal/connection/Clip2Bridge.java | 1140 ++ .../internal/console/HueCommandExtension.java | 178 +- .../discovery/Clip2ThingDiscoveryService.java | 194 + .../HueBridgeMDNSDiscoveryParticipant.java | 78 +- .../discovery/HueBridgeNupnpDiscovery.java | 73 +- .../discovery/HueDeviceDiscoveryService.java | 2 +- .../hue/internal/dto/clip2/ActionEntry.java | 34 + .../hue/internal/dto/clip2/Alerts.java | 52 + .../hue/internal/dto/clip2/BridgeConfig.java | 26 + .../hue/internal/dto/clip2/Button.java | 42 + .../internal/dto/clip2/ColorTemperature.java | 86 + .../hue/internal/dto/clip2/ColorXy.java | 64 + .../hue/internal/dto/clip2/Dimming.java | 67 + .../hue/internal/dto/clip2/Dynamics.java | 34 + .../hue/internal/dto/clip2/Effects.java | 64 + .../binding/hue/internal/dto/clip2/Error.java | 29 + .../binding/hue/internal/dto/clip2/Event.java | 41 + .../hue/internal/dto/clip2/Gamut2.java | 61 + .../hue/internal/dto/clip2/LightLevel.java | 60 + .../hue/internal/dto/clip2/MetaData.java | 44 + .../hue/internal/dto/clip2/MirekSchema.java | 54 + .../hue/internal/dto/clip2/Motion.java | 47 + .../hue/internal/dto/clip2/OnState.java | 44 + .../hue/internal/dto/clip2/PairXy.java | 33 + .../binding/hue/internal/dto/clip2/Power.java | 52 + .../hue/internal/dto/clip2/ProductData.java | 63 + .../hue/internal/dto/clip2/Recall.java | 41 + .../internal/dto/clip2/RelativeRotary.java | 46 + .../hue/internal/dto/clip2/Resource.java | 659 + .../internal/dto/clip2/ResourceReference.java | 71 + .../hue/internal/dto/clip2/Resources.java | 38 + .../hue/internal/dto/clip2/Rotation.java | 64 + .../hue/internal/dto/clip2/RotationEvent.java | 52 + .../hue/internal/dto/clip2/Temperature.java | 49 + .../hue/internal/dto/clip2/TimedEffects.java | 39 + .../internal/dto/clip2/enums/ActionType.java | 38 + .../internal/dto/clip2/enums/Archetype.java | 127 + .../dto/clip2/enums/BatteryStateType.java | 27 + .../dto/clip2/enums/ButtonEventType.java | 29 + .../dto/clip2/enums/DirectionType.java | 26 + .../internal/dto/clip2/enums/EffectType.java | 57 + .../dto/clip2/enums/RecallAction.java | 39 + .../dto/clip2/enums/ResourceType.java | 77 + .../dto/clip2/enums/RotationEventType.java | 26 + .../dto/clip2/enums/ZigbeeStatus.java | 46 + .../internal/dto/clip2/helper/Setters.java | 312 + .../hue/internal/exceptions/ApiException.java | 5 + .../exceptions/AssetNotLoadedException.java | 32 + .../DTOPresentButEmptyException.java | 34 + .../exceptions/HttpUnauthorizedException.java | 32 + .../factory/HueThingHandlerFactory.java | 71 +- .../internal/handler/Clip2BridgeHandler.java | 776 ++ .../Clip2StateDescriptionProvider.java | 44 + .../internal/handler/Clip2ThingHandler.java | 1197 ++ .../hue/internal/handler/HueLightHandler.java | 2 +- .../internal/handler/HueSensorHandler.java | 2 +- .../main/resources/OH-INF/i18n/hue.properties | 90 +- .../resources/OH-INF/thing/Clip2Thing.xml | 157 + .../main/resources/OH-INF/thing/bridge.xml | 33 + .../main/resources/OH-INF/thing/channels.xml | 78 +- .../hue/internal/clip2/Clip2DtoTest.java | 600 + .../src/test/resources/auth_v1.json | 7 + .../src/test/resources/behavior_instance.json | 91 + .../src/test/resources/behavior_script.json | 205 + .../src/test/resources/bridge.json | 18 + .../src/test/resources/bridge_home.json | 106 + .../src/test/resources/button.json | 551 + .../src/test/resources/device.json | 1194 ++ .../src/test/resources/device_power.json | 213 + .../src/test/resources/entertainment.json | 79 + .../entertainment_configuration.json | 109 + .../src/test/resources/event.json | 211 + .../src/test/resources/geofence.json | 4 + .../src/test/resources/geofence_client.json | 10 + .../src/test/resources/geolocation.json | 10 + .../src/test/resources/grouped_light.json | 526 + .../src/test/resources/homekit.json | 15 + .../src/test/resources/light.json | 852 ++ .../src/test/resources/light_level.json | 19 + .../src/test/resources/motion.json | 19 + .../src/test/resources/public_image.json | 7 + .../src/test/resources/relative_rotary.json | 24 + .../src/test/resources/room.json | 175 + .../src/test/resources/scene.json | 10355 ++++++++++++++++ .../src/test/resources/temperature.json | 19 + .../src/test/resources/zgp_connectivity.json | 4 + .../test/resources/zigbee_connectivity.json | 390 + .../src/test/resources/zone.json | 172 + .../HueBridgeNupnpDiscoveryOSGITest.java | 25 +- 98 files changed, 23880 insertions(+), 432 deletions(-) create mode 100644 bundles/org.openhab.binding.hue/doc/hue2.png create mode 100644 bundles/org.openhab.binding.hue/doc/readme_v1.md create mode 100644 bundles/org.openhab.binding.hue/doc/readme_v2.md create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/action/DynamicsActions.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/config/Clip2BridgeConfig.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/config/Clip2ThingConfig.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/Clip2ThingDiscoveryService.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ActionEntry.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Alerts.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/BridgeConfig.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Button.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ColorTemperature.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ColorXy.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Dimming.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Dynamics.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Effects.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Error.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Event.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Gamut2.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/LightLevel.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/MetaData.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/MirekSchema.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Motion.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/OnState.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/PairXy.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Power.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ProductData.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Recall.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/RelativeRotary.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resource.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ResourceReference.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resources.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Rotation.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/RotationEvent.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Temperature.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/TimedEffects.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ActionType.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/Archetype.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/BatteryStateType.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ButtonEventType.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/DirectionType.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/EffectType.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/RecallAction.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ResourceType.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/RotationEventType.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ZigbeeStatus.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/helper/Setters.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/AssetNotLoadedException.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/DTOPresentButEmptyException.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/HttpUnauthorizedException.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2StateDescriptionProvider.java create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2ThingHandler.java create mode 100644 bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/Clip2Thing.xml create mode 100644 bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/auth_v1.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/behavior_instance.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/behavior_script.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/bridge.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/bridge_home.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/button.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/device.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/device_power.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/entertainment.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/entertainment_configuration.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/event.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/geofence.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/geofence_client.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/geolocation.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/grouped_light.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/homekit.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/light.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/light_level.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/motion.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/public_image.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/relative_rotary.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/room.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/scene.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/temperature.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/zgp_connectivity.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/zigbee_connectivity.json create mode 100644 bundles/org.openhab.binding.hue/src/test/resources/zone.json diff --git a/CODEOWNERS b/CODEOWNERS index ae42d31f2..c93080812 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -139,7 +139,7 @@ /bundles/org.openhab.binding.homewizard/ @Daniel-42 /bundles/org.openhab.binding.hpprinter/ @cossey /bundles/org.openhab.binding.http/ @openhab/add-ons-maintainers -/bundles/org.openhab.binding.hue/ @cweitkamp +/bundles/org.openhab.binding.hue/ @cweitkamp @andrewfg /bundles/org.openhab.binding.hydrawise/ @digitaldan /bundles/org.openhab.binding.hyperion/ @tavalin /bundles/org.openhab.binding.iammeter/ @lewei50 diff --git a/bundles/org.openhab.binding.hue/README.md b/bundles/org.openhab.binding.hue/README.md index f702bde36..4335bf503 100644 --- a/bundles/org.openhab.binding.hue/README.md +++ b/bundles/org.openhab.binding.hue/README.md @@ -1,79 +1,39 @@ # Philips Hue Binding This binding integrates the [Philips Hue Lighting system](https://www.meethue.com). -The integration happens through the Hue Bridge, which acts as an IP gateway to the ZigBee devices. +The integration happens through the Hue Bridge, which acts as an IP gateway to the Zigbee devices. -![Philips Hue](doc/hue.jpg) +![Philips Hue](doc/hue.jpg) ![Philips Hue](doc/hue2.png) -## Supported Things +## Introduction The Hue Bridge is required as a "bridge" for accessing any other Hue device. It supports the Zigbee Light Link protocol as well as the upwards compatible Zigbee 3.0 protocol. There are two types of Hue Bridges, generally referred to as v1 (the rounded version) and v2 (the squared version). -Only noticeable difference between the two generation of bridges is the added support for Apple HomeKit in v2. +The difference between the two generations of bridges is that the v2 bridge added support for Apple HomeKit and the CLIP v2 API [see next paragraph](#api-versions). Both bridges are fully supported by this binding. Almost all available Hue devices are supported by this binding. This includes not only the "Friends of Hue", but also products like the LivingWhites adapter. -Additionally, it is possible to use OSRAM Lightify devices as well as other Zigbee Light Link compatible products, including the IKEA TRÅDFRI lights (when updated). -Beside bulbs and luminaires the Hue binding also supports some Zigbee sensors. Currently only Hue specific sensors are tested successfully (Hue Motion Sensor and Hue Dimmer Switch). +Additionally, it is possible to use OSRAM Lightify devices as well as other Zigbee Light Link compatible products, including the IKEA TRÅDFRI lights (when updated). +Beside bulbs and luminaires the Hue binding also supports some Zigbee sensors. +Currently only Hue specific sensors are tested successfully (Hue Motion Sensor and Hue Dimmer Switch). Please note that the devices need to be registered with the Hue Bridge before it is possible for this binding to use them. -The Hue binding supports all seven types of lighting devices defined for Zigbee Light Link ([see page 24, table 2](https://www.nxp.com/docs/en/user-guide/JN-UG-3091.pdf). -These are: +## API Versions -| Device type | Zigbee Device ID | Thing type | -|--------------------------|------------------|------------| -| On/Off Light | 0x0000 | 0000 | -| On/Off Plug-in Unit | 0x0010 | 0010 | -| Dimmable Light | 0x0100 | 0100 | -| Dimmable Plug-in Unit | 0x0110 | 0110 | -| Colour Light | 0x0200 | 0200 | -| Extended Colour Light | 0x0210 | 0210 | -| Colour Temperature Light | 0x0220 | 0220 | - -All different models of Hue, OSRAM, or other bulbs nicely fit into one of these seven types. -This type also determines the capability of a device and with that the possible ways of interacting with it. -The following matrix lists the capabilities (channels) for each type: - -| Thing type | On/Off | Brightness | Color | Color Temperature | -|-------------|:------:|:----------:|:-----:|:-----------------:| -| 0000 | X | | | | -| 0010 | X | | | | -| 0100 | X | X | | | -| 0110 | X | X | | | -| 0200 | X | | X | | -| 0210 | X | | X | X | -| 0220 | X | X | | X | - -Beside bulbs and luminaires the Hue binding supports some Zigbee sensors. -Currently only Hue specific sensors are tested successfully (e.g. Hue Motion Sensor, Hue Dimmer Switch, Hue Tap, CLIP Sensor). -The Hue Motion Sensor registers a `ZLLLightLevel` sensor (0106), a `ZLLPresence` sensor (0107) and a `ZLLTemperature` sensor (0302) in one device. -The Hue CLIP Sensor saves scene states with status or flag for HUE rules. -They are presented by the following Zigbee Device ID and _Thing type_: - -| Device type | Zigbee Device ID | Thing type | -|-----------------------------|------------------|----------------| -| Light Sensor | 0x0106 | 0106 | -| Occupancy Sensor | 0x0107 | 0107 | -| Temperature Sensor | 0x0302 | 0302 | -| Non-Colour Controller | 0x0820 | 0820 | -| Non-Colour Scene Controller | 0x0830 | 0830 | -| CLIP Generic Status Sensor | 0x0840 | 0840 | -| CLIP Generic Flag Sensor | 0x0850 | 0850 | -| Geofence Sensor | | geofencesensor | - -The Hue Dimmer Switch has 4 buttons and registers as a Non-Colour Controller switch, while the Hue Tap (also 4 buttons) registers as a Non-Colour Scene Controller in accordance with the ZLL standard. - -Also, Hue Bridge support CLIP Generic Status Sensor and CLIP Generic Flag Sensor. -These sensors save state for rules and calculate what actions to do. -CLIP Sensor set or get by JSON through IP. - -Finally, the Hue binding also supports the groups of lights and rooms set up on the Hue Bridge. +Bridges are accessed by means of the "CLIP" ('Connected Lighting Interface Protocol') Application Program Interface ('API'). +There are two versions of CLIP - namely CLIP v1 and CLIP v2, which are referred to as API v1 and API v2 in the links below. +Signify has stated that any new features (such as dynamic scenes) will only be available on API v2, and in the long term API v1 will eventually be removed. +The API v2 has more features, e.g. it supports Server Sent Events 'SSE' which means that it is much faster to receive status updates in openHAB. +For this reason it is recommended to use API v2 for new openHAB installations. +But unfortunately the API v2 is not supported by older v1 (round) bridges, nor by newer v2 (square-ish) bridges if their firmware is under v1948086000. ## Discovery The Hue Bridge is discovered through mDNS in the local network. +Potentially two types of Bridge will be discovered - namely an API v1 Bridge and/or an API v2 Bridge. + Auto-discovery is enabled by default. To disable it, you can add the following line to `/services/runtime.cfg`: @@ -82,293 +42,59 @@ discovery.hue:background=false ``` Once it is added as a Thing, its authentication button (in the middle) needs to be pressed in order to authorize the binding to access it. -Once the binding is authorized, it automatically reads all devices and groups that are set up on the Hue Bridge and puts them into the Inbox. +Once the binding is authorized, it automatically reads all devices (and groups) that are set up on the Hue Bridge and puts them into the Inbox. -## Thing Configuration +## Configuration for API v1 and API v2 -The Hue Bridge requires the IP address as a configuration value in order for the binding to know where to access it. -In the thing file, this looks e.g. like +- [Configuration for API v1](doc/readme_v1.md#philips-hue-binding-api-v1) +- [Configuration for API v2](doc/readme_v2.md#philips-hue-binding-api-v2) + +## Migration from API v1 to API v2 + +You can create new API v2 things either via the automatic discovery services, via a `.things` file, or manually in the UI. +If things are created manually in the UI then you will have to enter all configuration parameters by hand. +You can use the [console command](doc/readme_v2.md#console-command-for-finding-resourceids) to discover the `resourceId` of all the things in the bridge. +You might also need to edit the names and types of your items, depending on the individual circumstances below. + +### Migration via Automatic Discovery Services + +When new API v2 things are created via the discovery services, then if a matching legacy API v1 thing exists, the new v2 thing will clone some of the the attributes of the existing API v1 thing. +And also, if a legacy API v1 thing exists and has items linked to its channels, then the new API v2 thing will replicate the links between those items and the respective new API v2 thing's channels. + +### Migration via a `.things` File + +You need to manually edit your bridge and thing definitions as shown below: + +- Bridge definitions change from `hue:bridge:bridgename` to `hue:bridge-api2:bridgename`. +- Bridge configuration parameters change `userName` to `applicationKey`. +- Physical thing definitions change from `hue:0100:thingname` or `hue:0210:thingname` etc. to `hue:device:thingname`. +- Room or zone thing definitions change from `hue:group:thingname` to `hue:room:thingname` resp. `hue:zone:thingname`. +- Thing configuration parameters change from `lightId` or `sensorId` etc. to `resourceId`. + +Notes: + +1. In API v1 different things have different types (`0100`, `0220`, `0830`, etc.) but in API v2 all things have the same type `device`. +1. In API v1 different things are configured by different parameters (`sensorId`, `lightId`, etc.) but in API v2 all things are configured via the same `resourceId` parameter. +1. In API v1 some channel names contain underscore characters (`_`) but in API v2 they have changed to dashes (`-`) e.g `color_temperature` -> `color-temperature`. + +Examples: ```java -Bridge hue:bridge:1 [ ipAddress="192.168.0.64" ] -``` +// old (API v1) .. +Bridge hue:bridge:g24 "Philips Hue Hub" @ "Under Stairs" [ipAddress="192.168.1.234", userName="abcdefghijklmnopqrstuvwxyz0123456789ABCD"] { + Thing 0210 b01 "Living Room Standard Lamp Left" @ "Living Room" [lightId="1"] +} -A user to authenticate against the Hue Bridge is automatically generated. -Please note that the generated user name cannot be written automatically to the `.thing` file, and has to be set manually. -The generated user name can be found, after pressing the authentication button on the bridge, with the following console command: `hue username`. -The user name can be set using the `userName` configuration value, e.g.: - -```java -Bridge hue:bridge:1 [ ipAddress="192.168.0.64", userName="qwertzuiopasdfghjklyxcvbnm1234" ] -``` - -| Parameter | Description | -|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ipAddress | Network address of the Hue Bridge. **Mandatory**. | -| port | Port of the Hue Bridge. Optional, default value is 80 or 443, derived from protocol, otherwise user-defined. | -| protocol | Protocol to connect to the Hue Bridge ("http" or "https"), default value is "https"). | -| useSelfSignedCertificate | Use self-signed certificate for HTTPS connection to Hue Bridge. **Advanced**, default value is `true`. | -| userName | Name of a registered Hue Bridge user, that allows to access the API. **Mandatory** | -| pollingInterval | Seconds between fetching light values from the Hue Bridge. Optional, the default value is 10 (min="1", step="1"). | -| sensorPollingInterval | Milliseconds between fetching sensor-values from the Hue Bridge. A higher value means more delay for the sensor values, but a too low value can cause congestion on the bridge. Optional, the default value is 500. Default value will be considered if the value is lower than 50. Use 0 to disable the polling for sensors. | - -### Devices - -The devices are identified by the number that the Hue Bridge assigns to them (also shown in the Hue App as an identifier). -Thus, all it needs for manual configuration is this single value like - -```java -0210 bulb1 "Lamp 1" @ "Office" [ lightId="1" ] -``` - -or - -```java -0107 motion-sensor "Motion Sensor" @ "Entrance" [ sensorId="4" ] -``` - -You can freely choose the thing identifier (such as motion-sensor), its name (such as "Motion Sensor") and the location (such as "Entrance"). - -The following device types also have an optional configuration value to specify the fade time in milliseconds for the transition to a new state: - -- Dimmable Light -- Dimmable Plug-in Unit -- Colour Light -- Extended Colour Light -- Colour Temperature Light - -| Parameter | Description | -|-----------|-------------------------------------------------------------------------------| -| lightId | Number of the device provided by the Hue Bridge. **Mandatory** | -| fadetime | Fade time in Milliseconds to a new state (min="0", step="100", default="400") | - -### Groups - -The groups are identified by the number that the Hue Bridge assigns to them. -Thus, all it needs for manual configuration is this single value like - -```java -group kitchen-bulbs "Kitchen Lamps" @ "Kitchen" [ groupId="1" ] -``` - -You can freely choose the thing identifier (such as kitchen-bulbs), its name (such as "Kitchen Lamps") and the location (such as "Kitchen"). - -The group type also have an optional configuration value to specify the fade time in milliseconds for the transition to a new state. - -| Parameter | Description | -|-----------|-------------------------------------------------------------------------------| -| groupId | Number of the group provided by the Hue Bridge. **Mandatory** | -| fadetime | Fade time in Milliseconds to a new state (min="0", step="100", default="400") | - -## Channels - -The devices support some of the following channels: - -| Channel Type ID | Item Type | Description | Thing types supporting this channel | -|-----------------------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------| -| switch | Switch | This channel supports switching the device on and off. | 0000, 0010, group | -| color | Color | This channel supports full color control with hue, saturation and brightness values. | 0200, 0210, group | -| brightness | Dimmer | This channel supports adjusting the brightness value. Note that this is not available, if the color channel is supported. | 0100, 0110, 0220, group | -| color_temperature | Dimmer | This channel supports adjusting the color temperature from cold (0%) to warm (100%). | 0210, 0220, group | -| color_temperature_abs | Number:Temperature | This channel supports adjusting the color temperature in Kelvin. **Advanced** | 0210, 0220, group | -| alert | String | This channel supports displaying alerts by flashing the bulb either once or multiple times. Valid values are: NONE, SELECT and LSELECT. | 0000, 0100, 0200, 0210, 0220, group | -| effect | Switch | This channel supports color looping. | 0200, 0210, 0220 | -| dimmer_switch | Number | This channel shows which button was last pressed on the dimmer switch. | 0820 | -| illuminance | Number:Illuminance | This channel shows the current illuminance measured by the sensor. | 0106 | -| light_level | Number | This channel shows the current light level measured by the sensor. **Advanced** | 0106 | -| dark | Switch | This channel indicates whether the light level is below the darkness threshold or not. | 0106 | -| daylight | Switch | This channel indicates whether the light level is below the daylight threshold or not. | 0106 | -| presence | Switch | This channel indicates whether a motion is detected by the sensor or not. | 0107 | -| enabled | Switch | This channel activated or deactivates the sensor | 0107 | -| temperature | Number:Temperature | This channel shows the current temperature measured by the sensor. | 0302 | -| flag | Switch | This channel save flag state for a CLIP sensor. | 0850 | -| status | Number | This channel save status state for a CLIP sensor. | 0840 | -| last_updated | DateTime | This channel the date and time when the sensor was last updated. | 0820, 0830, 0840, 0850, 0106, 0107, 0302 | -| battery_level | Number | This channel shows the battery level. | 0820, 0106, 0107, 0302 | -| battery_low | Switch | This channel indicates whether the battery is low or not. | 0820, 0106, 0107, 0302 | -| scene | String | This channel activates the scene with the given ID String. The ID String of each scene is assigned by the Hue Bridge. | bridge, group | - -To load a hue scene inside a rule for example, the ID of the scene will be required. -You can list all the scene IDs with the following console commands: `hue scenes` and `hue scenes`. - -### Trigger Channels - -The dimmer switch additionally supports a trigger channel. - -| Channel ID | Description | Thing types supporting this channel | -|---------------------|----------------------------------|-------------------------------------| -| dimmer_switch_event | Event for dimmer switch pressed. | 0820 | -| tap_switch_event | Event for tap switch pressed. | 0830 | - -The `dimmer_switch_event` can trigger one of the following events: - -| Button | State | Event | -|---------------------|-----------------|-------| -| Button 1 (ON) | INITIAL_PRESSED | 1000 | -| | HOLD | 1001 | -| | SHORT RELEASED | 1002 | -| | LONG RELEASED | 1003 | -| Button 2 (DIM UP) | INITIAL_PRESSED | 2000 | -| | HOLD | 2001 | -| | SHORT RELEASED | 2002 | -| | LONG RELEASED | 2003 | -| Button 3 (DIM DOWN) | INITIAL_PRESSED | 3000 | -| | HOLD | 3001 | -| | SHORT RELEASED | 3002 | -| | LONG RELEASED | 3003 | -| Button 4 (OFF) | INITIAL_PRESSED | 4000 | -| | HOLD | 4001 | -| | SHORT RELEASED | 4002 | -| | LONG RELEASED | 4003 | - -The `tap_switch_event` can trigger one of the following events: - -| Button | State | Event | -|----------|----------|-------| -| Button 1 | Button 1 | 34 | -| Button 2 | Button 2 | 16 | -| Button 3 | Button 3 | 17 | -| Button 4 | Button 4 | 18 | - -## Rule Actions - -This binding includes a rule action, which allows to change a light channel with a specific fading time from within rules. -There is a separate instance for each light or light group, which can be retrieved e.g. through - -```php -val hueActions = getActions("hue","hue:0210:00178810d0dc:1") -``` - -where the first parameter always has to be `hue` and the second is the full Thing UID of the light that should be used. -Once this action instance is retrieved, you can invoke the `fadingLightCommand(String channel, Command command, DecimalType fadeTime)` method on it: - -```php -hueActions.fadingLightCommand("color", new PercentType(100), new DecimalType(1000)) -``` - -| Parameter | Description | -|-----------|--------------------------------------------------------------------------------------------------| -| channel | The following channels have fade time support: **brightness, color, color_temperature, switch** | -| command | All commands supported by the channel can be used | -| fadeTime | Fade time in milliseconds to a new light value (min="0", step="100") | - -## Full Example - -In this example **bulb1** is a standard Philips Hue bulb (LCT001) which supports `color` and `color_temperature`. -Therefore it is a thing of type **0210**. -**bulb2** is an OSRAM tunable white bulb (PAR16 50 TW) supporting `color_temperature` and so the type is **0220**. -And there is one Hue Motion Sensor (represented by three devices) and a Hue Dimmer Switch **dimmer-switch** with a Rule to trigger an action when a key has been pressed. - -### demo.things: - -```java -Bridge hue:bridge:1 "Hue Bridge" [ ipAddress="192.168.0.64" ] { - 0210 bulb1 "Lamp 1" @ "Kitchen" [ lightId="1" ] - 0220 bulb2 "Lamp 2" @ "Kitchen" [ lightId="2" ] - group kitchen-bulbs "Kitchen Lamps" @ "Kitchen" [ groupId="1" ] - 0106 light-level-sensor "Light-Sensor" @ "Entrance" [ sensorId="3" ] - 0107 motion-sensor "Motion-Sensor" @ "Entrance" [ sensorId="4" ] - 0302 temperature-sensor "Temp-Sensor" @ "Entrance" [ sensorId="5" ] - 0820 dimmer-switch "Dimmer-Switch" @ "Entrance" [ sensorId="6" ] +// new (API v2) ... +Bridge hue:bridge-api2:g24 "Philips Hue Hub (api2)" @ "Home" [ipAddress="192.168.1.234", applicationKey="abcdefghijklmnopqrstuvwxyz0123456789ABCD"] { + // Device things + Thing device 11111111-2222-3333-4444-555555555555 "Living Room Standard Lamp Left" @ "Living Room" [resourceId="11111111-2222-3333-4444-555555555555"] // Hue color lamp + .. + // Room things + Thing room 99999999-8888-7777-6666-555555555555 "Back Bedroom (Room)" [resourceId="99999999-8888-7777-6666-555555555555"] // Room + .. + // Zone things + Thing zone 99999999-8888-7777-6666-555555555555 "Standard Lamps" [resourceId="99999999-8888-7777-6666-555555555555"] // Zone + .. } ``` - -### demo.items: - -```java -// Bulb1 -Switch Light1_Toggle { channel="hue:0210:1:bulb1:color" } -Dimmer Light1_Dimmer { channel="hue:0210:1:bulb1:color" } -Color Light1_Color { channel="hue:0210:1:bulb1:color" } -Dimmer Light1_ColorTemp { channel="hue:0210:1:bulb1:color_temperature" } -String Light1_Alert { channel="hue:0210:1:bulb1:alert" } -Switch Light1_Effect { channel="hue:0210:1:bulb1:effect" } - -// Bulb2 -Switch Light2_Toggle { channel="hue:0220:1:bulb2:brightness" } -Dimmer Light2_Dimmer { channel="hue:0220:1:bulb2:brightness" } -Dimmer Light2_ColorTemp { channel="hue:0220:1:bulb2:color_temperature" } - -// Kitchen -Switch Kitchen_Switch { channel="hue:group:1:kitchen-bulbs:switch" } -Dimmer Kitchen_Dimmer { channel="hue:group:1:kitchen-bulbs:brightness" } -Color Kitchen_Color { channel="hue:group:1:kitchen-bulbs:color" } -Dimmer Kitchen_ColorTemp { channel="hue:group:1:kitchen-bulbs:color_temperature" } - -// Light Level Sensor -Number:Illuminance LightLevelSensorIlluminance { channel="hue:0106:1:light-level-sensor:illuminance" } - -// Motion Sensor -Switch MotionSensorPresence { channel="hue:0107:1:motion-sensor:presence" } -DateTime MotionSensorLastUpdate { channel="hue:0107:1:motion-sensor:last_updated" } -Number MotionSensorBatteryLevel { channel="hue:0107:1:motion-sensor:battery_level" } -Switch MotionSensorLowBattery { channel="hue:0107:1:motion-sensor:battery_low" } - -// Temperature Sensor -Number:Temperature TemperatureSensorTemperature { channel="hue:0302:1:temperature-sensor:temperature" } - -// Scenes -String LightScene { channel="hue:bridge:1:scene"} -``` - -Note: The bridge ID is in this example **1** but can be different in each system. -Also, if you are doing all your configuration through files, you may add the full bridge id to the channel definitions (e.g. `channel="hue:0210:00178810d0dc:bulb1:color`) instead of the short version (e.g. `channel="hue:0210:1:bulb1:color`) to prevent frequent discovery messages in the log file. - -### demo.sitemap: - -```perl -sitemap demo label="Main Menu" -{ - Frame { - // Bulb1 - Switch item= Light1_Toggle - Slider item= Light1_Dimmer - Colorpicker item= Light1_Color - Slider item= Light1_ColorTemp - Switch item= Light1_Alert mappings=[NONE="None", SELECT="Alert", LSELECT="Long Alert"] - Switch item= Light1_Effect - - // Bulb2 - Switch item= Light2_Toggle - Slider item= Light2_Dimmer - Slider item= Light2_ColorTemp - - // Kitchen - Switch item= Kitchen_Switch - Slider item= Kitchen_Dimmer - Colorpicker item= Kitchen_Color - Slider item= Kitchen_ColorTemp - - // Motion Sensor - Switch item=MotionSensorPresence - Text item=MotionSensorLastUpdate - Text item=MotionSensorBatteryLevel - Switch item=MotionSensorLowBattery - - // Light Scenes - Default item=LightScene label="Scene []" - } -} -``` - -### Events - - ```php -rule "example trigger rule" -when - Channel "hue:0820:1:dimmer-switch:dimmer_switch_event" triggered -then - ... -end -``` - -The optional `` represents one of the button events that are generated by the Hue Dimmer Switch. -If ommited the rule gets triggered by any key action and you can determine the event that triggered it with the `receivedEvent` method. -Be aware that the events have a '.0' attached to them, like `2001.0` or `34.0`. -So, testing for specific events looks like this: - -```php -if (receivedEvent == "1000.0")) { - //do stuff -} -``` diff --git a/bundles/org.openhab.binding.hue/doc/hue2.png b/bundles/org.openhab.binding.hue/doc/hue2.png new file mode 100644 index 0000000000000000000000000000000000000000..cda1d7e6def5795f6fb168d2c9b8f6ab30ad118b GIT binary patch literal 113337 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.hue/doc/readme_v1.md b/bundles/org.openhab.binding.hue/doc/readme_v1.md new file mode 100644 index 000000000..6e9bf2fa8 --- /dev/null +++ b/bundles/org.openhab.binding.hue/doc/readme_v1.md @@ -0,0 +1,358 @@ +# Philips Hue Binding Configuration for API v1 + +[Back to Overview](../README.md#philips-hue-binding) + +## Supported Things + +Almost all available Hue devices are supported by this binding. +This includes not only the "Friends of Hue", but also products like the LivingWhites adapter. +Additionally, it is possible to use OSRAM Lightify devices as well as other Zigbee Light Link compatible products, including the IKEA TRÅDFRI lights (when updated). +Beside bulbs and luminaires the Hue binding also supports some Zigbee sensors. Currently only Hue specific sensors are tested successfully (Hue Motion Sensor and Hue Dimmer Switch). +Please note that the devices need to be registered with the Hue Bridge before it is possible for this binding to use them. + +The Hue binding supports all seven types of lighting devices defined for Zigbee Light Link ([see page 24, table 2](https://www.nxp.com/docs/en/user-guide/JN-UG-3091.pdf). +These are: + +| Device type | Zigbee Device ID | Thing type | +|--------------------------|------------------|------------| +| On/Off Light | 0x0000 | 0000 | +| On/Off Plug-in Unit | 0x0010 | 0010 | +| Dimmable Light | 0x0100 | 0100 | +| Dimmable Plug-in Unit | 0x0110 | 0110 | +| Colour Light | 0x0200 | 0200 | +| Extended Colour Light | 0x0210 | 0210 | +| Colour Temperature Light | 0x0220 | 0220 | + +All different models of Hue, OSRAM, or other bulbs nicely fit into one of these seven types. +This type also determines the capability of a device and with that the possible ways of interacting with it. +The following matrix lists the capabilities (channels) for each type: + +| Thing type | On/Off | Brightness | Color | Color Temperature | +|-------------|:------:|:----------:|:-----:|:-----------------:| +| 0000 | X | | | | +| 0010 | X | | | | +| 0100 | X | X | | | +| 0110 | X | X | | | +| 0200 | X | | X | | +| 0210 | X | | X | X | +| 0220 | X | X | | X | + +Beside bulbs and luminaires the Hue binding supports some Zigbee sensors. +Currently only Hue specific sensors are tested successfully (e.g. Hue Motion Sensor, Hue Dimmer Switch, Hue Tap, CLIP Sensor). +The Hue Motion Sensor registers a `ZLLLightLevel` sensor (0106), a `ZLLPresence` sensor (0107) and a `ZLLTemperature` sensor (0302) in one device. +The Hue CLIP Sensor saves scene states with status or flag for HUE rules. +They are presented by the following Zigbee Device ID and _Thing type_: + +| Device type | Zigbee Device ID | Thing type | +|-----------------------------|------------------|----------------| +| Light Sensor | 0x0106 | 0106 | +| Occupancy Sensor | 0x0107 | 0107 | +| Temperature Sensor | 0x0302 | 0302 | +| Non-Colour Controller | 0x0820 | 0820 | +| Non-Colour Scene Controller | 0x0830 | 0830 | +| CLIP Generic Status Sensor | 0x0840 | 0840 | +| CLIP Generic Flag Sensor | 0x0850 | 0850 | +| Geofence Sensor | | geofencesensor | + +The Hue Dimmer Switch has 4 buttons and registers as a Non-Colour Controller switch, while the Hue Tap (also 4 buttons) registers as a Non-Colour Scene Controller in accordance with the ZLL standard. + +Also, Hue Bridge support CLIP Generic Status Sensor and CLIP Generic Flag Sensor. +These sensors save state for rules and calculate what actions to do. +CLIP Sensor set or get by JSON through IP. + +Finally, the Hue binding also supports the groups of lights and rooms set up on the Hue Bridge. + +## Thing Configuration + +The Hue Bridge requires the IP address as a configuration value in order for the binding to know where to access it. +In the thing file, this looks e.g. like + +```java +Bridge hue:bridge:1 [ ipAddress="192.168.0.64" ] +``` + +A user to authenticate against the Hue Bridge is automatically generated. +Please note that the generated user name cannot be written automatically to the `.things` file, and has to be set manually. +The generated user name can be found, after pressing the authentication button on the bridge, with the following console command: `hue username`. +The user name can be set using the `userName` configuration value, e.g.: + +```java +Bridge hue:bridge:1 [ ipAddress="192.168.0.64", userName="qwertzuiopasdfghjklyxcvbnm1234" ] +``` + +| Parameter | Description | +|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ipAddress | Network address of the Hue Bridge. **Mandatory**. | +| port | Port of the Hue Bridge. Optional, default value is 80 or 443, derived from protocol, otherwise user-defined. | +| protocol | Protocol to connect to the Hue Bridge ("http" or "https"), default value is "https"). | +| useSelfSignedCertificate | Use self-signed certificate for HTTPS connection to Hue Bridge. **Advanced**, default value is `true`. | +| userName | Name of a registered Hue Bridge user, that allows to access the API. **Mandatory** | +| pollingInterval | Seconds between fetching light values from the Hue Bridge. Optional, the default value is 10 (min="1", step="1"). | +| sensorPollingInterval | Milliseconds between fetching sensor-values from the Hue Bridge. A higher value means more delay for the sensor values, but a too low value can cause congestion on the bridge. Optional, the default value is 500. Default value will be considered if the value is lower than 50. Use 0 to disable the polling for sensors. | + +### Devices + +The devices are identified by the number that the Hue Bridge assigns to them (also shown in the Hue App as an identifier). +Thus, all it needs for manual configuration is this single value like + +```java +0210 bulb1 "Lamp 1" @ "Office" [ lightId="1" ] +``` + +or + +```java +0107 motion-sensor "Motion Sensor" @ "Entrance" [ sensorId="4" ] +``` + +You can freely choose the thing identifier (such as motion-sensor), its name (such as "Motion Sensor") and the location (such as "Entrance"). + +The following device types also have an optional configuration value to specify the fade time in milliseconds for the transition to a new state: + +- Dimmable Light +- Dimmable Plug-in Unit +- Colour Light +- Extended Colour Light +- Colour Temperature Light + +| Parameter | Description | +|-----------|-------------------------------------------------------------------------------| +| lightId | Number of the device provided by the Hue Bridge. **Mandatory** | +| fadetime | Fade time in Milliseconds to a new state (min="0", step="100", default="400") | + + +### Groups + +The groups are identified by the number that the Hue Bridge assigns to them. +Thus, all it needs for manual configuration is this single value like + +```java +group kitchen-bulbs "Kitchen Lamps" @ "Kitchen" [ groupId="1" ] +``` + +You can freely choose the thing identifier (such as kitchen-bulbs), its name (such as "Kitchen Lamps") and the location (such as "Kitchen"). + +The group type also have an optional configuration value to specify the fade time in milliseconds for the transition to a new state. + +| Parameter | Description | +|-----------|-------------------------------------------------------------------------------| +| groupId | Number of the group provided by the Hue Bridge. **Mandatory** | +| fadetime | Fade time in Milliseconds to a new state (min="0", step="100", default="400") | + + +## Channels + +The devices support some of the following channels: + +| Channel Type ID | Item Type | Description | Thing types supporting this channel | +|-----------------------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------| +| switch | Switch | This channel supports switching the device on and off. | 0000, 0010, group | +| color | Color | This channel supports full color control with hue, saturation and brightness values. | 0200, 0210, group | +| brightness | Dimmer | This channel supports adjusting the brightness value. Note that this is not available, if the color channel is supported. | 0100, 0110, 0220, group | +| color_temperature | Dimmer | This channel supports adjusting the color temperature from cold (0%) to warm (100%). | 0210, 0220, group | +| color_temperature_abs | Number:Temperature | This channel supports adjusting the color temperature in Kelvin. +**Advanced** | 0210, 0220, group | +| alert | String | This channel supports displaying alerts by flashing the bulb either once or multiple times. Valid values are: NONE, SELECT and LSELECT. | 0000, 0100, 0200, 0210, 0220, group | +| effect | Switch | This channel supports color looping. | 0200, 0210, 0220 | +| dimmer_switch | Number | This channel shows which button was last pressed on the dimmer switch. | 0820 | +| illuminance | Number:Illuminance | This channel shows the current illuminance measured by the sensor. | 0106 | +| light_level | Number | This channel shows the current light level measured by the sensor. **Advanced** | 0106 | +| dark | Switch | This channel indicates whether the light level is below the darkness threshold or not. | 0106 | +| daylight | Switch | This channel indicates whether the light level is below the daylight threshold or not. | 0106 | +| presence | Switch | This channel indicates whether a motion is detected by the sensor or not. | 0107 | +| enabled | Switch | This channel activated or deactivates the sensor | 0107 | +| temperature | Number:Temperature | This channel shows the current temperature measured by the sensor. | 0302 | +| flag | Switch | This channel save flag state for a CLIP sensor. | 0850 | +| status | Number | This channel save status state for a CLIP sensor. | 0840 | +| last_updated | DateTime | This channel the date and time when the sensor was last updated. | 0820, 0830, 0840, 0850, 0106, 0107, 0302 | +| battery_level | Number | This channel shows the battery level. | 0820, 0106, 0107, 0302 | +| battery_low | Switch | This channel indicates whether the battery is low or not. | 0820, 0106, 0107, 0302 | +| scene | String | This channel activates the scene with the given ID String. The ID String of each scene is assigned by the Hue Bridge. | bridge, group | + +To load a hue scene inside a rule for example, the ID of the scene will be required. +You can list all the scene IDs with the following console commands: `hue scenes` and `hue scenes`. + +### Trigger Channels + +The dimmer switch additionally supports a trigger channel. + +| Channel ID | Description | Thing types supporting this channel | +|---------------------|----------------------------------|-------------------------------------| +| dimmer_switch_event | Event for dimmer switch pressed. | 0820 | +| tap_switch_event | Event for tap switch pressed. | 0830 | + +The `dimmer_switch_event` can trigger one of the following events: + +| Button | State | Event | +|---------------------|-----------------|-------| +| Button 1 (ON) | INITIAL_PRESSED | 1000 | +| | HOLD | 1001 | +| | SHORT RELEASED | 1002 | +| | LONG RELEASED | 1003 | +| Button 2 (DIM UP) | INITIAL_PRESSED | 2000 | +| | HOLD | 2001 | +| | SHORT RELEASED | 2002 | +| | LONG RELEASED | 2003 | +| Button 3 (DIM DOWN) | INITIAL_PRESSED | 3000 | +| | HOLD | 3001 | +| | SHORT RELEASED | 3002 | +| | LONG RELEASED | 3003 | +| Button 4 (OFF) | INITIAL_PRESSED | 4000 | +| | HOLD | 4001 | +| | SHORT RELEASED | 4002 | +| | LONG RELEASED | 4003 | + +The `tap_switch_event` can trigger one of the following events: + +| Button | State | Event | +|----------|----------|-------| +| Button 1 | Button 1 | 34 | +| Button 2 | Button 2 | 16 | +| Button 3 | Button 3 | 17 | +| Button 4 | Button 4 | 18 | + + +## Rule Actions + +This binding includes a rule action, which allows to change a light channel with a specific fading time from within rules. +There is a separate instance for each light or light group, which can be retrieved e.g. through + +```php +val hueActions = getActions("hue","hue:0210:00178810d0dc:1") +``` + +where the first parameter always has to be `hue` and the second is the full Thing UID of the light that should be used. +Once this action instance is retrieved, you can invoke the `fadingLightCommand(String channel, Command command, DecimalType fadeTime)` method on it: + +```php +hueActions.fadingLightCommand("color", new PercentType(100), new DecimalType(1000)) +``` + +| Parameter | Description | +|-----------|--------------------------------------------------------------------------------------------------| +| channel | The following channels have fade time support: **brightness, color, color_temperature, switch** | +| command | All commands supported by the channel can be used | +| fadeTime | Fade time in milliseconds to a new light value (min="0", step="100") | + +## Full Example + +In this example **bulb1** is a standard Philips Hue bulb (LCT001) which supports `color` and `color_temperature`. +Therefore it is a thing of type **0210**. +**bulb2** is an OSRAM tunable white bulb (PAR16 50 TW) supporting `color_temperature` and so the type is **0220**. +And there is one Hue Motion Sensor (represented by three devices) and a Hue Dimmer Switch **dimmer-switch** with a Rule to trigger an action when a key has been pressed. + +### demo.things: + +```java +Bridge hue:bridge:1 "Hue Bridge" [ ipAddress="192.168.0.64" ] { + 0210 bulb1 "Lamp 1" @ "Kitchen" [ lightId="1" ] + 0220 bulb2 "Lamp 2" @ "Kitchen" [ lightId="2" ] + group kitchen-bulbs "Kitchen Lamps" @ "Kitchen" [ groupId="1" ] + 0106 light-level-sensor "Light-Sensor" @ "Entrance" [ sensorId="3" ] + 0107 motion-sensor "Motion-Sensor" @ "Entrance" [ sensorId="4" ] + 0302 temperature-sensor "Temp-Sensor" @ "Entrance" [ sensorId="5" ] + 0820 dimmer-switch "Dimmer-Switch" @ "Entrance" [ sensorId="6" ] +} +``` + +### demo.items: + +```java +// Bulb1 +Switch Light1_Toggle { channel="hue:0210:1:bulb1:color" } +Dimmer Light1_Dimmer { channel="hue:0210:1:bulb1:color" } +Color Light1_Color { channel="hue:0210:1:bulb1:color" } +Dimmer Light1_ColorTemp { channel="hue:0210:1:bulb1:color_temperature" } +String Light1_Alert { channel="hue:0210:1:bulb1:alert" } +Switch Light1_Effect { channel="hue:0210:1:bulb1:effect" } + +// Bulb2 +Switch Light2_Toggle { channel="hue:0220:1:bulb2:brightness" } +Dimmer Light2_Dimmer { channel="hue:0220:1:bulb2:brightness" } +Dimmer Light2_ColorTemp { channel="hue:0220:1:bulb2:color_temperature" } + +// Kitchen +Switch Kitchen_Switch { channel="hue:group:1:kitchen-bulbs:switch" } +Dimmer Kitchen_Dimmer { channel="hue:group:1:kitchen-bulbs:brightness" } +Color Kitchen_Color { channel="hue:group:1:kitchen-bulbs:color" } +Dimmer Kitchen_ColorTemp { channel="hue:group:1:kitchen-bulbs:color_temperature" } + +// Light Level Sensor +Number:Illuminance LightLevelSensorIlluminance { channel="hue:0106:1:light-level-sensor:illuminance" } + +// Motion Sensor +Switch MotionSensorPresence { channel="hue:0107:1:motion-sensor:presence" } +DateTime MotionSensorLastUpdate { channel="hue:0107:1:motion-sensor:last_updated" } +Number MotionSensorBatteryLevel { channel="hue:0107:1:motion-sensor:battery_level" } +Switch MotionSensorLowBattery { channel="hue:0107:1:motion-sensor:battery_low" } + +// Temperature Sensor +Number:Temperature TemperatureSensorTemperature { channel="hue:0302:1:temperature-sensor:temperature" } + +// Scenes +String LightScene { channel="hue:bridge:1:scene"} +``` + +Note: The bridge ID is in this example **1** but can be different in each system. +Also, if you are doing all your configuration through files, you may add the full bridge id to the channel definitions (e.g. `channel="hue:0210:00178810d0dc:bulb1:color`) instead of the short version (e.g. `channel="hue:0210:1:bulb1:color`) to prevent frequent discovery messages in the log file. + +### demo.sitemap: + +```perl +sitemap demo label="Main Menu" +{ + Frame { + // Bulb1 + Switch item= Light1_Toggle + Slider item= Light1_Dimmer + Colorpicker item= Light1_Color + Slider item= Light1_ColorTemp + Switch item= Light1_Alert mappings=[NONE="None", SELECT="Alert", LSELECT="Long Alert"] + Switch item= Light1_Effect + + // Bulb2 + Switch item= Light2_Toggle + Slider item= Light2_Dimmer + Slider item= Light2_ColorTemp + + // Kitchen + Switch item= Kitchen_Switch + Slider item= Kitchen_Dimmer + Colorpicker item= Kitchen_Color + Slider item= Kitchen_ColorTemp + + // Motion Sensor + Switch item=MotionSensorPresence + Text item=MotionSensorLastUpdate + Text item=MotionSensorBatteryLevel + Switch item=MotionSensorLowBattery + + // Light Scenes + Default item=LightScene label="Scene []" + } +} +``` + +### Events + + ```php +rule "example trigger rule" +when + Channel "hue:0820:1:dimmer-switch:dimmer_switch_event" triggered +then + ... +end +``` + +The optional `` represents one of the button events that are generated by the Hue Dimmer Switch. +If ommited the rule gets triggered by any key action and you can determine the event that triggered it with the `receivedEvent` method. +Be aware that the events have a '.0' attached to them, like `2001.0` or `34.0`. +So, testing for specific events looks like this: + +```php +if (receivedEvent == "1000.0") { + //do stuff +} +``` + +[Back to Overview](../README.md#philips-hue-binding) diff --git a/bundles/org.openhab.binding.hue/doc/readme_v2.md b/bundles/org.openhab.binding.hue/doc/readme_v2.md new file mode 100644 index 000000000..b8929df4f --- /dev/null +++ b/bundles/org.openhab.binding.hue/doc/readme_v2.md @@ -0,0 +1,237 @@ +# Philips Hue Binding Configuration for API v2 + +[Back to Overview](../README.md#philips-hue-binding) + +## Supported Things + +The binding supports `bridge-api2`, `device`, `room`, and `zone` thing types. +The `bridge-api2` thing type represents the Hue Bridge which is the server for all other things. +The `device` thing type represents a piece of physical equipment in the home. +Such `device` things may contain either a *light*, a *button*, or (one or more) *sensors*. +Lights can be of any type from a simple on/off light, through dimmable monochrome lights, to full colour dimmable lights. +Buttons are devices having one or more push buttons. +Sensors can be (for example) light level sensors, temperature sensors, or motion sensors. +The `room` and `zone` thing type represents logical groupings of equipment in the home, either within a specific room, or a logical group of equipment. + +## Thing Configuration + +### Bridge + +The Hue Bridge requires the IP address as a configuration value in order for the binding to know where to access it. +It requires an 'application key' to authenticate against the Hue Bridge. +This may be copied from an API v1 installation, or it may be automatically generated (press button to authenticate). +Please note that the generated application key cannot be written automatically to the `.things` file, and has to be set manually. +The generated application key can be found, after pressing the authentication button on the bridge, with the following console command: `openhab:hue applicationkey`. +The application key can be set using the `applicationKey` configuration value, e.g.: + +```java +Bridge hue:bridge-api2:1 [ ipAddress="192.168.0.64", applicationKey="qwertzuiopasdfghjklyxcvbnm1234" ] +``` + +| Parameter | Description | +|--------------------------|----------------------------------------------------------------------------------------------------| +| ipAddress | Network address of the Hue Bridge. **Mandatory**. | +| applicationKey | A code generated by the bridge that allows to access the API. **Mandatory** | +| checkMinutes | Interval in minutes between retrying the HTTP 2 and SSE connections. Default is 60. **Advanced** | +| useSelfSignedCertificate | Use self-signed certificate for HTTPS connection to Hue Bridge. Default is `true`. **Advanced** | + +### Devices, Rooms, and Zones + +Apart from the Bridge, there are three other types of thing -- namely `device`, `room`, and `zone`. +Device things represent physical hardware devices in the system, whereas `room` and `zone` things represent sets of physical lights, either in a room or a zone. +In addition to regular rooms and zones, there is a 'super' `zone` that allows you to control all of the lights in the system. + +All things are identified by a unique Resource Identifier string that the Hue Bridge assigns to them e.g. `d1ae958e-8908-449a-9897-7f10f9b8d4c2`. +Thus, all it needs for manual configuration is this single value, like: + +```java +device officelamp "Lamp 1" @ "Office" [ resourceId="d1ae958e-8908-449a-9897-7f10f9b8d4c2" ] +.. +zone kitchenLights "Kitchen Down Lights" @ "Kitchen" [ resourceId="7f10f9b8-8908-449a-9897-d4c2d1ae958e" ] +``` + +You can get a list of all devices in the bridge and their respective Resource Ids by entering the following console command: `openhab:hue things` +See [console command](#console-command-for-finding-resourceids) + +The configuration of all things (as described above) is the same regardless of whether it is a device containing a light, a button, or (one or more) sensors, or whether it is a room or zone. + +### Channels for Devices + +Device things support some of the following channels: + +| Channel ID | Item Type | Description | +|-----------------------|--------------------|---------------------------------------------------------------------------------------------------------------------| +| color | Color | Supports full color control with hue, saturation and brightness values, or brightness only, or switching on or off. | +| brightness | Dimmer | Supports control of the brightness value, or switching on or off. | +| color-temperature | Dimmer | Supports control of the color temperature in percent from cold (0%) to warm (100%). | +| color-temperature-abs | Number:Temperature | Supports control of the color temperature via a QuantityType having a temperature unit e.g. Kelvin. (Advanced) | +| switch | Switch | Supports switching the device on and off. | +| dynamics | Number:Time | Sets the duration of dynamic transitions between light states. (Advanced) | +| alert | String | Allows setting an alert on a light e.g. flashing them. (Advanced) | +| effect | String | Allows setting an effect on a light e.g. 'candle' effect. (Advanced) | +| button-last-event | Number | Informs which button was last pressed in the device. (Trigger Channel) | +| rotary-steps | Number | Informs about the number of rotary steps of the last rotary dial movement. (Trigger Channel) | +| motion | Switch | Shows if motion has been detected by the sensor. (Read Only) | +| motion-enabled | Switch | Supports enabling / disabling the motion sensor. (Advanced) | +| light-level | Number:Illuminance | Shows the current light level measured by the sensor. (Read Only) | +| light-level-enabled | Switch | Supports enabling / disabling the light level sensor. (Advanced) | +| temperature | Number:Temperature | Shows the current temperature measured by the sensor. (Read Only) | +| temperature-enabled | Switch | Supports enabling / disabling the temperature sensor. (Advanced) | +| battery-level | Number | Shows the battery level. (Read Only) | +| battery-low | Switch | Indicates whether the battery is low or not. (Read Only) | +| last-updated | DateTime | The date and time when the thing state was last updated. (Read Only) (Advanced) | +| color-xy-only | Color | Allows access to the `color-xy` parameter of the light(s) only. Has no impact on `dimming` or `on-off` parameters. | +| dimming-only | Dimmer | Allows access to the `dimming` parameter of the light(s) only. Has no impact on `color-xy` or `on-off` parameters. | +| on-off-only | Switch | Allows access to the `on-off` parameter of the light(s) only. Has no impact on `color-xy` or `dimming` parameters. | + +The exact list of channels in a given device is determined at run time when the system is started. +Each device reports its own live list of capabilities, and the respective list of channels is created accordingly. + +The channels `color-xy-only`, `dimming-only` and `on-off-only` are *advanced* channels - see [below](###advanced-channels-for-devices-,-rooms-and-zones) for more details. + +The `button-last-event` channel is a trigger channel. +When the button is pressed the channel receives a number as calculated by the following formula: + +```text +value = (button_id * 1000) + event_id; +``` + +In a single button device, the `button_id` is 1, whereas in a multi- button device the `button_id` can be either 1, 2, 3, or 4 depending on which button was pressed. +The `event_id` can have the following values: + +| Event | Value | +|----------------------|-------| +| INITIAL_PRESS | 0 | +| REPEAT | 1 | +| SHORT_RELEASE | 2 | +| LONG_RELEASE | 3 | +| DOUBLE_SHORT_RELEASE | 4 | + +So (for example) the channel value `1002` ((1 * 1000) + 2) means that the second button in the device had a short release event. + +The `rotary-steps` channel is a trigger channel. +When the dial is turned, the channel receives a number corresponding to the number of steps of the last movement of a rotary dial. +A positive number means the dial was rotated clock-wise, whereas a negative number means it was rotated counter-clockwise. + +### Channels for Rooms and Zones + +Room and Zone things allow you to control the lights in a given zone or room. +They support the following channels: + +| Channel ID | Item Type | Description | +|---------------------|--------------------|-----------------------------------------------------------------------------------| +| brightness | Dimmer | Supports adjusting the brightness or switching the lights on and off. | +| switch | Switch | Supports switching the lights on and off. | +| scene1) | String | Setting the string to a valid scene friendly name activates the respective scene. | +| dynamics | Number:Time | The duration of dynamic transitions between light or scene states. | +| alert1) | String | This channel allows setting an alert on the lights e.g. flashing them. | + +1) The scene and alert channels are optional. +If the respective room or zone has no scenes or alerts associated with it, the respective channel will not be shown. + +### The `dynamics` Channel + +Some channels support dynamic transitions between light states. +A dynamic transition is where, instead of the light state changing immediately to its new target value, it changes gradually to the new value over a period of time. + +If a thing supports dynamic transitions, then it will have a `dynamics` channel. +This is a numeric channel where you can set the time delay for the transition in milliseconds. +When you set a value for the `dynamics` channel (e.g. 2000 milliseconds) and then quickly issue another command (e.g. brightness 100%), the second command will be executed gradually over the period of milliseconds given by the `dynamics` channel value. +When the `dynamics` channel value is changed, it triggers a time window of ten seconds during which the value is active. +If the second command is sent within the active time window, it will be executed gradually according to the `dynamics` channel value. +However, if the second command is sent after the active time window has expired, then it will be executed immediately. + +### Advanced Channels for Devices, Rooms and Zones + +Some things support additional advanced channels `color-xy-only`, `dimming-only` and/or `on-off-only`. +For convenience the normal channels often amalgamate multiple elements of the state of a light, room or zone into one single channel. +For example, a full color light has one single `color` channel that can accept HSBType commands for changing the color, PercentType commands for changing the brightness, and OnOffType commands for switching it on or off. +By contrast, the purpose of the advanced channels is to individually access specificstate elements of the respective lights, rooms or zones. + +These advanced channels can be used as "presets". +For example, you may want to preset the `dimming-only` channel to 20% at night, and to 100% in the day time. +Then if somebody turns on the light at night time it will turn on to 20% resp. to 100% in the day time. +You can also use the `color-xy-only` channel to preset (say) a cool color in the morning, and a warm color in the evening. +NOTE: you can also preset color temperature values in advance via the `color-temperature` and `color-temperature-abs` channels described above. + +## Console Command for finding ResourceIds + +The openHAB console has a command named `openhab:hue` that (among other things) lists the `resourceId` of all device things in the bridge. +The console command usage is `openhab:hue things`. +An exampe of such a console command, and its respective output, is shown below. + +```shell +openhab> openhab:hue hue:bridge-api2:g24 things +Bridge hue:bridge-api2:g24 "Philips Hue Bridge" [ipAddress="192.168.1.234", applicationKey="abcdefghijklmnopqrstuvwxyz0123456789ABCD"] { + Thing device 11111111-2222-3333-4444-555555555555 "Standard Lamp L" [resourceId="11111111-2222-3333-4444-555555555555"] // Hue color lamp + Thing device 11111111-2222-3333-4444-666666666666 "Kitchen Wallplate Switch" [resourceId="11111111-2222-3333-4444-666666666666"] // Hue wall switch module +} +``` + +The `openhab:hue things` command produces an output that can be used to directly create a `.things` file, as shown below. + +```shell +openhab> openhab:hue hue:bridge-api2:g24 things > myThingsFile.things +``` + +## Rule Actions + +This binding includes a rule action, which implements dynamic (i.e. gradual) transitions to a new scene or light(s) state. +Each thing has a separate action instance, which can be retrieved as follows. + +```php +val hueActions = getActions("hue","hue:device:g24:11111111-2222-3333-4444-555555555555") +``` + +Where the first parameter must always be `hue` and the second must be the full thing UID. +Once the action instance has been retrieved, you can invoke its `dynamicCommand(String channelId, Command command, Long durationMs)` method as follows. + +```php +hueActions.dynamicCommand("brightness", new PercentType(100), new Long(10000)) + +hueActions.dynamicCommand("scene", new StringType("SceneName"), new Long(20000)) +``` + +| Parameter | Description | +|------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| channelId | The channel ID of the channel to send the command to (one of `brightness`, `color`, `color-temperature`, `color-temp-kelvin`, or `scene`). | +| command | The target command state to transition to. | +| durationMs | The dynamic transition duration in milliseconds. | + +## Full Example + +### demo.things: + +```java +Bridge hue:bridge-api2:g24 "Philips Hue Hub" @ "Home" [ipAddress="192.168.1.234", applicationKey="abcdefghijklmnopqrstuvwxyz0123456789ABCD"] { + Thing device 11111111-2222-3333-4444-555555555555 "Living Room Standard Lamp Left" @ "Living Room" [resourceId="11111111-2222-3333-4444-555555555555"] + Thing device 11111111-2222-3333-4444-666666666666 "Kitchen Wallplate Switch" @ "Kitchen" [resourceId="11111111-2222-3333-4444-666666666666"] + + Thing zone 11111111-2222-3333-4444-666666666666 "Kitchen Lights" @ "Kitchen" [resourceId="11111111-2222-3333-4444-666666666666"] +} +``` + +### demo.items: + +```java +Color Living_Room_Standard_Lamp_Left_Colour "Living Room Standard Lamp Left Colour" {channel="hue:device:g24:11111111-2222-3333-4444-555555555555:color"} +Dimmer Living_Room_Standard_Lamp_Left_Brightness "Living Room Standard Lamp Left Brightness [%.0f %%]" {channel="hue:device:g24:11111111-2222-3333-4444-555555555555:brightness"} +Switch Living_Room_Standard_Lamp_Left_Switch "Living Room Standard Lamp Left Switch" (g_Lights_On_Count) {channel="hue:device:g24:11111111-2222-3333-4444-555555555555:switch"} + +Number Kitchen_Wallplate_Switch_Last_Event "Kitchen Wallplate Switch Last Event" {channel="hue:device:g24:11111111-2222-3333-4444-666666666666:button-last-event"} +Switch Kitchen_Wallplate_Switch_Battery_Low_Alarm "Kitchen Wallplate Switch Battery Low Alarm" {channel="hue:device:g24:11111111-2222-3333-4444-666666666666:battery-low"} +``` + +### demo.sitemap: + +```perl +sitemap demo label="Hue" { + Frame label="Standard Lamp" { + Switch item=Living_Room_Standard_Lamp_Left_Switch + Slider item=Living_Room_Standard_Lamp_Left_Brightness + Colorpicker item=Living_Room_Standard_Lamp_Left_Colour + } +} +``` + +[Back to Overview](../README.md#philips-hue-binding) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java index ebc1d36c1..11ad57e72 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java @@ -12,6 +12,9 @@ */ package org.openhab.binding.hue.internal; +import java.util.Map; +import java.util.Set; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; @@ -53,9 +56,14 @@ public class HueBindingConstants { public static final ThingTypeUID THING_TYPE_GEOFENCE_SENSOR = new ThingTypeUID(BINDING_ID, "geofencesensor"); public static final ThingTypeUID THING_TYPE_TEMPERATURE_SENSOR = new ThingTypeUID(BINDING_ID, "0302"); public static final ThingTypeUID THING_TYPE_LIGHT_LEVEL_SENSOR = new ThingTypeUID(BINDING_ID, "0106"); - public static final ThingTypeUID THING_TYPE_GROUP = new ThingTypeUID(BINDING_ID, "group"); + public static final Set V1_THING_TYPE_UIDS = Set.of(THING_TYPE_COLOR_LIGHT, + THING_TYPE_COLOR_TEMPERATURE_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT, + THING_TYPE_ON_OFF_LIGHT, THING_TYPE_ON_OFF_PLUG, THING_TYPE_DIMMABLE_PLUG, THING_TYPE_DIMMER_SWITCH, + THING_TYPE_TAP_SWITCH, THING_TYPE_PRESENCE_SENSOR, THING_TYPE_TEMPERATURE_SENSOR, + THING_TYPE_LIGHT_LEVEL_SENSOR, THING_TYPE_GROUP); + // List all channels public static final String CHANNEL_COLORTEMPERATURE = "color_temperature"; public static final String CHANNEL_COLORTEMPERATURE_ABS = "color_temperature_abs"; @@ -96,11 +104,25 @@ public class HueBindingConstants { // Thing configuration properties public static final String LIGHT_ID = "lightId"; public static final String SENSOR_ID = "sensorId"; - public static final String PRODUCT_NAME = "productName"; + public static final String PROPERTY_PRODUCT_NAME = "productName"; public static final String UNIQUE_ID = "uniqueId"; public static final String FADETIME = "fadetime"; public static final String GROUP_ID = "groupId"; + // property names for API v2 + public static final String PROPERTY_RESOURCE_ID = "resourceId"; + public static final String PROPERTY_RESOURCE_TYPE = "resourceType"; + public static final String PROPERTY_RESOURCE_NAME = "resourceName"; + public static final String PROPERTY_RESOURCE_ARCHETYPE = "resourceArchetype"; + public static final String PROPERTY_PRODUCT_ARCHETYPE = "productArchetype"; + public static final String PROPERTY_PRODUCT_CERTIFIED = "productCertified"; + public static final String PROPERTY_LEGACY_THING_UID = "legacyThingUID"; + public static final String PROPERTY_OWNER = "owner"; + public static final String PROPERTY_OWNER_TYPE = "ownerType"; + public static final String PROPERTY_DIMMING_RANGE = "dimmingRange"; + public static final String PROPERTY_COLOR_TEMP_RANGE = "colorTemperatureRange"; + public static final String PROPERTY_COLOR_GAMUT = "colorGamut"; + public static final String NORMALIZE_ID_REGEX = "[^a-zA-Z0-9_]"; public static final String DISCOVERY_LABEL_PATTERN = "Philips Hue (%s)"; @@ -111,4 +133,60 @@ public class HueBindingConstants { // Config status messages public static final String IP_ADDRESS_MISSING = "missing-ip-address-configuration"; + + // thing types for API v2 + public static final ThingTypeUID THING_TYPE_BRIDGE_API2 = new ThingTypeUID(BINDING_ID, "bridge-api2"); + public static final ThingTypeUID THING_TYPE_DEVICE = new ThingTypeUID(BINDING_ID, "device"); + public static final ThingTypeUID THING_TYPE_ZONE = new ThingTypeUID(BINDING_ID, "zone"); + public static final ThingTypeUID THING_TYPE_ROOM = new ThingTypeUID(BINDING_ID, "room"); + + // channels for API v2 + public static final String CHANNEL_2_COLOR = CHANNEL_COLOR; + public static final String CHANNEL_2_COLOR_TEMP_PERCENT = "color-temperature"; + public static final String CHANNEL_2_COLOR_TEMP_ABSOLUTE = "color-temperature-abs"; + public static final String CHANNEL_2_BRIGHTNESS = CHANNEL_BRIGHTNESS; + public static final String CHANNEL_2_SWITCH = CHANNEL_SWITCH; + public static final String CHANNEL_2_SCENE = CHANNEL_SCENE; + public static final String CHANNEL_2_DYNAMICS = "dynamics"; + public static final String CHANNEL_2_ALERT = CHANNEL_ALERT; + public static final String CHANNEL_2_EFFECT = CHANNEL_EFFECT; + public static final String CHANNEL_2_BUTTON_LAST_EVENT = "button-last-event"; + public static final String CHANNEL_2_ROTARY_STEPS = "rotary-steps"; + public static final String CHANNEL_2_MOTION = "motion"; + public static final String CHANNEL_2_MOTION_ENABLED = "motion-enabled"; + public static final String CHANNEL_2_LIGHT_LEVEL = "light-level"; + public static final String CHANNEL_2_LIGHT_LEVEL_ENABLED = "light-level-enabled"; + public static final String CHANNEL_2_TEMPERATURE = CHANNEL_TEMPERATURE; + public static final String CHANNEL_2_TEMPERATURE_ENABLED = "temperature-enabled"; + public static final String CHANNEL_2_BATTERY_LEVEL = "battery-level"; + public static final String CHANNEL_2_BATTERY_LOW = "battery-low"; + public static final String CHANNEL_2_LAST_UPDATED = "last-updated"; + public static final String CHANNEL_2_COLOR_XY_ONLY = "color-xy-only"; + public static final String CHANNEL_2_DIMMING_ONLY = "dimming-only"; + public static final String CHANNEL_2_ON_OFF_ONLY = "on-off-only"; + + // channel IDs that (optionally) support dynamics + public static final Set DYNAMIC_CHANNELS = Set.of(CHANNEL_2_BRIGHTNESS, CHANNEL_2_COLOR, + CHANNEL_2_COLOR_TEMP_PERCENT, CHANNEL_2_COLOR_TEMP_ABSOLUTE, CHANNEL_2_SCENE); + + /* + * Map of API v1 channel IDs against API v2 channel IDs where, if the v1 channel exists in the system, then we + * should try to replicate the channel/item links from the v1 channel into the respective v2 channel. + */ + public static final Map REPLICATE_CHANNEL_ID_MAP = Map.ofEntries( + Map.entry(CHANNEL_BRIGHTNESS, CHANNEL_2_BRIGHTNESS), // + Map.entry(CHANNEL_COLOR, CHANNEL_2_COLOR), // + Map.entry(CHANNEL_SWITCH, CHANNEL_2_SWITCH), // + Map.entry(CHANNEL_SCENE, CHANNEL_2_SCENE), // + Map.entry(CHANNEL_COLORTEMPERATURE, CHANNEL_2_COLOR_TEMP_PERCENT), // + Map.entry(CHANNEL_COLORTEMPERATURE_ABS, CHANNEL_2_COLOR_TEMP_ABSOLUTE), // + Map.entry(CHANNEL_DIMMER_SWITCH, CHANNEL_2_BUTTON_LAST_EVENT), // + Map.entry(CHANNEL_LIGHT_LEVEL, CHANNEL_2_LIGHT_LEVEL), // + Map.entry(CHANNEL_PRESENCE, CHANNEL_2_MOTION), // + Map.entry(CHANNEL_TEMPERATURE, CHANNEL_2_TEMPERATURE), // + Map.entry(CHANNEL_BATTERY_LEVEL, CHANNEL_2_BATTERY_LEVEL), // + Map.entry(CHANNEL_BATTERY_LOW, CHANNEL_2_BATTERY_LOW), // + Map.entry(CHANNEL_LAST_UPDATED, CHANNEL_2_LAST_UPDATED)); + + public static final String ALL_LIGHTS_KEY = "discovery.group.all-lights.label"; } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/action/DynamicsActions.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/action/DynamicsActions.java new file mode 100644 index 000000000..ebab300ff --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/action/DynamicsActions.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.action; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.handler.Clip2ThingHandler; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.MetricPrefix; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the {@link ThingActions} interface used for sending 'dynamics' commands to Hue API v2 devices, + * rooms or zones. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@ThingActionsScope(name = "hue") +@NonNullByDefault +public class DynamicsActions implements ThingActions { + + private final Logger logger = LoggerFactory.getLogger(DynamicsActions.class); + + private @Nullable Clip2ThingHandler handler; + + public static void dynamicCommand(ThingActions actions, @Nullable String channelId, @Nullable Command command, + @Nullable Long durationMs) { + ((DynamicsActions) actions).dynamicCommand(channelId, command, durationMs); + } + + @RuleAction(label = "@text/dynamics.action.label", description = "@text/dynamics.action.description") + public void dynamicCommand( + @ActionInput(name = "channelId", label = "@text/dynamics.channel.label", description = "@text/dynamics.channel.description") @Nullable String channelId, + @ActionInput(name = "command", label = "@text/dynamics.command.label", description = "@text/dynamics.command.description") @Nullable Command command, + @ActionInput(name = "durationMs", label = "@text/dynamics.duration.label", description = "@text/dynamics.duration.description") @Nullable Long durationMs) { + // + Clip2ThingHandler handler = this.handler; + if (handler == null) { + logger.warn("ThingHandler is null."); + return; + } + if (channelId == null) { + logger.debug("Channel ID is null."); + return; + } + if (command == null) { + logger.debug("Command is null."); + return; + } + if (durationMs == null || durationMs.longValue() <= 0) { + logger.debug("Duration is null, zero or negative."); + return; + } + handler.handleDynamicsCommand(channelId, command, + new QuantityType<>(durationMs.longValue(), MetricPrefix.MILLI(Units.SECOND))); + logger.debug("Dynamic command '{}' sent to channelId '{}' with duration {}ms.", command, channelId, durationMs); + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return handler; + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + this.handler = (Clip2ThingHandler) handler; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/config/Clip2BridgeConfig.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/config/Clip2BridgeConfig.java new file mode 100644 index 000000000..0db7c6c95 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/config/Clip2BridgeConfig.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Configuration for the Clip2BridgeHandler. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Clip2BridgeConfig { + public static final String APPLICATION_KEY = "applicationKey"; + + public String ipAddress = ""; + public String applicationKey = ""; + public int checkMinutes = 60; + public boolean useSelfSignedCertificate = true; +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/config/Clip2ThingConfig.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/config/Clip2ThingConfig.java new file mode 100644 index 000000000..68d891b9e --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/config/Clip2ThingConfig.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Configuration for CLIP V2 things. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Clip2ThingConfig { + public String resourceId = ""; +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java new file mode 100644 index 000000000..c1b5a6804 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java @@ -0,0 +1,1140 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.connection; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import javax.ws.rs.core.MediaType; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http.MetaData.Response; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.GoAwayFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.PingFrame; +import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Promise.Completable; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.openhab.binding.hue.internal.dto.CreateUserRequest; +import org.openhab.binding.hue.internal.dto.SuccessResponse; +import org.openhab.binding.hue.internal.dto.clip2.BridgeConfig; +import org.openhab.binding.hue.internal.dto.clip2.Event; +import org.openhab.binding.hue.internal.dto.clip2.Resource; +import org.openhab.binding.hue.internal.dto.clip2.ResourceReference; +import org.openhab.binding.hue.internal.dto.clip2.Resources; +import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType; +import org.openhab.binding.hue.internal.exceptions.ApiException; +import org.openhab.binding.hue.internal.exceptions.HttpUnauthorizedException; +import org.openhab.binding.hue.internal.handler.Clip2BridgeHandler; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.io.net.http.HttpUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +/** + * This class handles HTTP and SSE connections to/from a Hue Bridge running CLIP 2. + * + * It uses the following connection mechanisms: + * + *
  • The primary communication uses HTTP 2 streams over a shared permanent HTTP 2 session.
  • + *
  • The 'registerApplicationKey()' method uses HTTP/1.1 over the OH common Jetty client.
  • + *
  • The 'isClip2Supported()' static method uses HTTP/1.1 over the OH common Jetty client via 'HttpUtil'.
  • + * + * @author Andrew Fiddian-Green - Initial Contribution + */ +@NonNullByDefault +public class Clip2Bridge implements Closeable { + + /** + * Base (abstract) adapter for listening to HTTP 2 stream events. + * + * It implements a CompletableFuture by means of which the caller can wait for the response data to come in. And + * which, in the case of fatal errors, gets completed exceptionally. + * + * It handles the following fatal error events by notifying the containing class: + * + *
  • onHeaders() HTTP unauthorized codes
  • + */ + private abstract class BaseStreamListenerAdapter extends Stream.Listener.Adapter { + protected final CompletableFuture completable = new CompletableFuture(); + private String contentType = "UNDEFINED"; + + protected T awaitResult() throws ExecutionException, InterruptedException, TimeoutException { + return completable.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + /** + * Return the HTTP content type. + * + * @return content type e.g. 'application/json' + */ + protected String getContentType() { + return contentType; + } + + protected void handleHttp2Error(Http2Error error) { + Http2Exception e = new Http2Exception(error); + if (Http2Error.UNAUTHORIZED.equals(error)) { + // for external error handling, abstract authorization errors into a separate exception + completable.completeExceptionally(new HttpUnauthorizedException("HTTP 2 request not authorized")); + } else { + completable.completeExceptionally(e); + } + fatalErrorDelayed(this, e); + } + + /** + * Check the reply headers to see whether the request was authorised. + */ + @Override + public void onHeaders(@Nullable Stream stream, @Nullable HeadersFrame frame) { + Objects.requireNonNull(frame); + MetaData metaData = frame.getMetaData(); + if (metaData.isResponse()) { + Response responseMetaData = (Response) metaData; + int httpStatus = responseMetaData.getStatus(); + switch (httpStatus) { + case HttpStatus.UNAUTHORIZED_401: + case HttpStatus.FORBIDDEN_403: + handleHttp2Error(Http2Error.UNAUTHORIZED); + default: + } + contentType = responseMetaData.getFields().get(HttpHeader.CONTENT_TYPE).toLowerCase(); + } + } + } + + /** + * Adapter for listening to regular HTTP 2 GET/PUT request stream events. + * + * It assembles the incoming text data into an HTTP 'content' entity. And when the last data frame arrives, it + * returns the full content by completing the CompletableFuture with that data. + * + * In addition to those handled by the parent, it handles the following fatal error events by notifying the + * containing class: + * + *
  • onIdleTimeout()
  • + *
  • onTimeout()
  • + */ + private class ContentStreamListenerAdapter extends BaseStreamListenerAdapter { + private final DataFrameCollector content = new DataFrameCollector(); + + @Override + public void onData(@Nullable Stream stream, @Nullable DataFrame frame, @Nullable Callback callback) { + Objects.requireNonNull(frame); + Objects.requireNonNull(callback); + synchronized (this) { + content.append(frame.getData()); + if (frame.isEndStream() && !completable.isDone()) { + completable.complete(content.contentAsString().trim()); + content.reset(); + } + } + callback.succeeded(); + } + + @Override + public boolean onIdleTimeout(@Nullable Stream stream, @Nullable Throwable x) { + handleHttp2Error(Http2Error.IDLE); + return true; + } + + @Override + public void onTimeout(@Nullable Stream stream, @Nullable Throwable x) { + handleHttp2Error(Http2Error.TIMEOUT); + } + } + + /** + * Class to collect incoming ByteBuffer data from HTTP 2 Data frames. + */ + private static class DataFrameCollector { + private byte[] buffer = new byte[512]; + private int usedSize = 0; + + public void append(ByteBuffer data) { + int dataCapacity = data.capacity(); + int neededSize = usedSize + dataCapacity; + if (neededSize > buffer.length) { + int newSize = (dataCapacity < 4096) ? neededSize : Math.max(2 * buffer.length, neededSize); + buffer = Arrays.copyOf(buffer, newSize); + } + data.get(buffer, usedSize, dataCapacity); + usedSize += dataCapacity; + } + + public String contentAsString() { + return new String(buffer, 0, usedSize, StandardCharsets.UTF_8); + } + + public Reader contentStreamReader() { + return new InputStreamReader(new ByteArrayInputStream(buffer, 0, usedSize), StandardCharsets.UTF_8); + } + + public void reset() { + usedSize = 0; + } + } + + /** + * Adapter for listening to SSE event stream events. + * + * It receives the incoming text lines. Receipt of the first data line causes the CompletableFuture to complete. It + * then parses subsequent data according to the SSE specification. If the line starts with a 'data:' message, it + * adds the data to the list of strings. And if the line is empty (i.e. the last line of an event), it passes the + * full set of strings to the owner via a call-back method. + * + * The stream must be permanently connected, so it ignores onIdleTimeout() events. + * + * The parent class handles most fatal errors, but since the event stream is supposed to be permanently connected, + * the following events are also considered as fatal: + * + *
  • onClosed()
  • + *
  • onReset()
  • + */ + private class EventStreamListenerAdapter extends BaseStreamListenerAdapter { + private final DataFrameCollector eventData = new DataFrameCollector(); + + @Override + public void onClosed(@Nullable Stream stream) { + handleHttp2Error(Http2Error.CLOSED); + } + + @Override + public void onData(@Nullable Stream stream, @Nullable DataFrame frame, @Nullable Callback callback) { + Objects.requireNonNull(frame); + Objects.requireNonNull(callback); + synchronized (this) { + eventData.append(frame.getData()); + BufferedReader reader = new BufferedReader(eventData.contentStreamReader()); + @SuppressWarnings("null") + List receivedLines = reader.lines().collect(Collectors.toList()); + + // a blank line marks the end of an SSE message + boolean endOfMessage = !receivedLines.isEmpty() + && receivedLines.get(receivedLines.size() - 1).isBlank(); + + if (endOfMessage) { + eventData.reset(); + // receipt of ANY message means the event stream is established + if (!completable.isDone()) { + completable.complete(Boolean.TRUE); + } + // append any 'data' field values to the event message + StringBuilder eventContent = new StringBuilder(); + for (String receivedLine : receivedLines) { + if (receivedLine.startsWith("data:")) { + eventContent.append(receivedLine.substring(5).stripLeading()); + } + } + if (eventContent.length() > 0) { + onEventData(eventContent.toString().trim()); + } + } + } + callback.succeeded(); + } + + @Override + public boolean onIdleTimeout(@Nullable Stream stream, @Nullable Throwable x) { + return false; + } + + @Override + public void onReset(@Nullable Stream stream, @Nullable ResetFrame frame) { + handleHttp2Error(Http2Error.RESET); + } + } + + /** + * Enum of potential fatal HTTP 2 session/stream errors. + */ + private enum Http2Error { + CLOSED, + FAILURE, + TIMEOUT, + RESET, + IDLE, + GO_AWAY, + UNAUTHORIZED; + } + + /** + * Private exception for handling HTTP 2 stream and session errors. + */ + @SuppressWarnings("serial") + private static class Http2Exception extends ApiException { + public final Http2Error error; + + public Http2Exception(Http2Error error) { + this(error, null); + } + + public Http2Exception(Http2Error error, @Nullable Throwable cause) { + super("HTTP 2 stream " + error.toString().toLowerCase(), cause); + this.error = error; + } + } + + /** + * Adapter for listening to HTTP 2 session status events. + * + * The session must be permanently connected, so it ignores onIdleTimeout() events. + * It also handles the following fatal events by notifying the containing class: + * + *
  • onClose()
  • + *
  • onFailure()
  • + *
  • onGoAway()
  • + *
  • onReset()
  • + */ + private class SessionListenerAdapter extends Session.Listener.Adapter { + + @Override + public void onClose(@Nullable Session session, @Nullable GoAwayFrame frame) { + fatalErrorDelayed(this, new Http2Exception(Http2Error.CLOSED)); + } + + @Override + public void onFailure(@Nullable Session session, @Nullable Throwable failure) { + fatalErrorDelayed(this, new Http2Exception(Http2Error.FAILURE)); + } + + @Override + public void onGoAway(@Nullable Session session, @Nullable GoAwayFrame frame) { + fatalErrorDelayed(this, new Http2Exception(Http2Error.GO_AWAY)); + } + + @Override + public boolean onIdleTimeout(@Nullable Session session) { + return false; + } + + @Override + public void onPing(@Nullable Session session, @Nullable PingFrame frame) { + checkAliveOk(); + if (Objects.nonNull(session) && Objects.nonNull(frame) && !frame.isReply()) { + session.ping(new PingFrame(true), Callback.NOOP); + } + } + + @Override + public void onReset(@Nullable Session session, @Nullable ResetFrame frame) { + fatalErrorDelayed(this, new Http2Exception(Http2Error.RESET)); + } + } + + /** + * Enum showing the online state of the session connection. + */ + private static enum State { + /** + * Session closed + */ + CLOSED, + /** + * Session open for HTTP calls only + */ + PASSIVE, + /** + * Session open for HTTP calls and actively receiving SSE events + */ + ACTIVE; + } + + private static final Logger LOGGER = LoggerFactory.getLogger(Clip2Bridge.class); + + private static final String APPLICATION_ID = "org-openhab-binding-hue-clip2"; + private static final String APPLICATION_KEY = "hue-application-key"; + + private static final String EVENT_STREAM_ID = "eventStream"; + private static final String FORMAT_URL_CONFIG = "http://%s/api/0/config"; + private static final String FORMAT_URL_RESOURCE = "https://%s/clip/v2/resource/"; + private static final String FORMAT_URL_REGISTER = "http://%s/api"; + private static final String FORMAT_URL_EVENTS = "https://%s/eventstream/clip/v2"; + + private static final long CLIP2_MINIMUM_VERSION = 1948086000L; + + public static final int TIMEOUT_SECONDS = 10; + private static final int CHECK_ALIVE_SECONDS = 300; + private static final int REQUEST_INTERVAL_MILLISECS = 50; + private static final int MAX_CONCURRENT_STREAMS = 3; + private static final int RESTART_AFTER_SECONDS = 5; + + private static final ResourceReference BRIDGE = new ResourceReference().setType(ResourceType.BRIDGE); + + /** + * Static method to attempt to connect to a Hue Bridge, get its software version, and check if it is high enough to + * support the CLIP 2 API. + * + * @param hostName the bridge IP address. + * @return true if bridge is online and it supports CLIP 2, or false if it is online and does not support CLIP 2. + * @throws IOException if unable to communicate with the bridge. + * @throws NumberFormatException if the bridge firmware version is invalid. + */ + public static boolean isClip2Supported(String hostName) throws IOException { + String response; + Properties headers = new Properties(); + headers.put(HttpHeader.ACCEPT, MediaType.APPLICATION_JSON); + response = HttpUtil.executeUrl("GET", String.format(FORMAT_URL_CONFIG, hostName), headers, null, null, + TIMEOUT_SECONDS * 1000); + BridgeConfig config = new Gson().fromJson(response, BridgeConfig.class); + if (Objects.nonNull(config)) { + String swVersion = config.swversion; + if (Objects.nonNull(swVersion)) { + try { + if (Long.parseLong(swVersion) >= CLIP2_MINIMUM_VERSION) { + return true; + } + } catch (NumberFormatException e) { + LOGGER.debug("isClip2Supported() swVersion '{}' is not a number", swVersion); + } + } + } + return false; + } + + private final HttpClient httpClient; + private final HTTP2Client http2Client; + private final String hostName; + private final String baseUrl; + private final String eventUrl; + private final String registrationUrl; + private final String applicationKey; + private final Clip2BridgeHandler bridgeHandler; + private final Gson jsonParser = new Gson(); + private final Semaphore streamMutex = new Semaphore(MAX_CONCURRENT_STREAMS, true); + + private boolean closing; + private boolean internalRestartScheduled; + private boolean externalRestartScheduled; + private State onlineState = State.CLOSED; + private Optional lastRequestTime = Optional.empty(); + private Instant sessionExpireTime = Instant.MAX; + private @Nullable Session http2Session; + + private @Nullable Future checkAliveTask; + private @Nullable Future internalRestartTask; + private Map> fatalErrorTasks = new ConcurrentHashMap<>(); + + /** + * Constructor. + * + * @param httpClientFactory the OH core HttpClientFactory. + * @param bridgeHandler the bridge handler. + * @param hostName the host name (ip address) of the Hue bridge + * @param applicationKey the application key. + */ + public Clip2Bridge(HttpClientFactory httpClientFactory, Clip2BridgeHandler bridgeHandler, String hostName, + String applicationKey) { + LOGGER.debug("Clip2Bridge()"); + httpClient = httpClientFactory.getCommonHttpClient(); + http2Client = httpClientFactory.createHttp2Client("hue-clip2", httpClient.getSslContextFactory()); + http2Client.setConnectTimeout(Clip2Bridge.TIMEOUT_SECONDS * 1000); + http2Client.setIdleTimeout(-1); + this.bridgeHandler = bridgeHandler; + this.hostName = hostName; + this.applicationKey = applicationKey; + baseUrl = String.format(FORMAT_URL_RESOURCE, hostName); + eventUrl = String.format(FORMAT_URL_EVENTS, hostName); + registrationUrl = String.format(FORMAT_URL_REGISTER, hostName); + } + + /** + * Cancel the given task. + * + * @param cancelTask the task to be cancelled (may be null) + * @param mayInterrupt allows cancel() to interrupt the thread. + */ + private void cancelTask(@Nullable Future cancelTask, boolean mayInterrupt) { + if (Objects.nonNull(cancelTask)) { + cancelTask.cancel(mayInterrupt); + } + } + + /** + * Send a ping to the Hue bridge to check that the session is still alive. + */ + private void checkAlive() { + if (onlineState == State.CLOSED) { + return; + } + LOGGER.debug("checkAlive()"); + Session session = http2Session; + if (Objects.nonNull(session)) { + session.ping(new PingFrame(false), Callback.NOOP); + } + if (Instant.now().isAfter(sessionExpireTime)) { + fatalError(this, new Http2Exception(Http2Error.TIMEOUT)); + } + } + + /** + * Connection is ok, so reschedule the session check alive expire time. Called in response to incoming ping frames + * from the bridge. + */ + protected void checkAliveOk() { + LOGGER.debug("checkAliveOk()"); + sessionExpireTime = Instant.now().plusSeconds(CHECK_ALIVE_SECONDS * 2); + } + + /** + * Close the connection. + */ + @Override + public void close() { + closing = true; + externalRestartScheduled = false; + internalRestartScheduled = false; + close2(); + } + + /** + * Private method to close the connection. + */ + private void close2() { + synchronized (this) { + LOGGER.debug("close2()"); + boolean notifyHandler = onlineState == State.ACTIVE && !internalRestartScheduled + && !externalRestartScheduled && !closing; + onlineState = State.CLOSED; + synchronized (fatalErrorTasks) { + fatalErrorTasks.values().forEach(task -> cancelTask(task, true)); + fatalErrorTasks.clear(); + } + if (!internalRestartScheduled) { + // don't close the task if a restart is current + cancelTask(internalRestartTask, true); + internalRestartTask = null; + } + cancelTask(checkAliveTask, true); + checkAliveTask = null; + closeSession(); + try { + http2Client.stop(); + } catch (Exception e) { + // ignore + } + if (notifyHandler) { + bridgeHandler.onConnectionOffline(); + } + } + } + + /** + * Close the HTTP 2 session if necessary. + */ + private void closeSession() { + LOGGER.debug("closeSession()"); + Session session = http2Session; + if (Objects.nonNull(session)) { + session.close(0, null, Callback.NOOP); + } + http2Session = null; + } + + /** + * Method that is called back in case of fatal stream or session events. Note: under normal operation, the Hue + * Bridge sends a 'soft' GO_AWAY command every nine or ten hours, so we handle such soft errors by attempting to + * silently close and re-open the connection without notifying the handler of an actual 'hard' error. + * + * @param listener the entity that caused this method to be called. + * @param cause the exception that caused the error. + */ + private synchronized void fatalError(Object listener, Http2Exception cause) { + if (externalRestartScheduled || internalRestartScheduled || onlineState == State.CLOSED || closing) { + return; + } + String causeId = listener.getClass().getSimpleName(); + if (listener instanceof ContentStreamListenerAdapter) { + // on GET / PUT requests the caller handles errors and closes the stream; the session is still OK + LOGGER.debug("fatalError() {} {} ignoring", causeId, cause.error); + } else if (cause.error == Http2Error.GO_AWAY) { + LOGGER.debug("fatalError() {} {} scheduling reconnect", causeId, cause.error); + + // schedule task to open again + internalRestartScheduled = true; + cancelTask(internalRestartTask, false); + internalRestartTask = bridgeHandler.getScheduler().schedule( + () -> internalRestart(onlineState == State.ACTIVE), RESTART_AFTER_SECONDS, TimeUnit.SECONDS); + + // force close immediately to be clean when internalRestart() starts + close2(); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("fatalError() {} {} closing", causeId, cause.error, cause); + } else { + LOGGER.warn("Fatal error {} {} => closing session.", causeId, cause.error); + } + close2(); + } + } + + /** + * Method that is called back in case of fatal stream or session events. Schedules fatalError() to be called after a + * delay in order to prevent sequencing issues. + * + * @param listener the entity that caused this method to be called. + * @param cause the exception that caused the error. + */ + protected void fatalErrorDelayed(Object listener, Http2Exception cause) { + synchronized (fatalErrorTasks) { + final int index = fatalErrorTasks.size(); + fatalErrorTasks.put(index, bridgeHandler.getScheduler().schedule(() -> { + fatalError(listener, cause); + fatalErrorTasks.remove(index); + }, 1, TimeUnit.SECONDS)); + } + } + + /** + * HTTP GET a Resources object, for a given resource Reference, from the Hue Bridge. The reference is a class + * comprising a resource type and an id. If the id is a specific resource id then only the one specific resource + * is returned, whereas if it is null then all resources of the given resource type are returned. + * + * It wraps the getResourcesImpl() method in a try/catch block, and transposes any HttpUnAuthorizedException into an + * ApiException. Such transposition should never be required in reality since by the time this method is called, the + * connection will surely already have been authorised. + * + * @param reference the Reference class to get. + * @return a Resource object containing either a list of Resources or a list of Errors. + * @throws ApiException if anything fails. + * @throws InterruptedException + */ + public Resources getResources(ResourceReference reference) throws ApiException, InterruptedException { + sleepDuringRestart(); + if (onlineState == State.CLOSED) { + throw new ApiException("getResources() offline"); + } + return getResourcesImpl(reference); + } + + /** + * Internal method to send an HTTP 2 GET request to the Hue Bridge and process its response. + * + * @param reference the Reference class to get. + * @return a Resource object containing either a list of Resources or a list of Errors. + * @throws HttpUnauthorizedException if the request was refused as not authorised or forbidden. + * @throws ApiException if the communication failed, or an unexpected result occurred. + * @throws InterruptedException + */ + private Resources getResourcesImpl(ResourceReference reference) + throws HttpUnauthorizedException, ApiException, InterruptedException { + Session session = http2Session; + if (Objects.isNull(session) || session.isClosed()) { + throw new ApiException("HTTP 2 session is null or closed"); + } + throttle(); + String url = getUrl(reference); + HeadersFrame headers = prepareHeaders(url, MediaType.APPLICATION_JSON); + LOGGER.trace("GET {} HTTP/2", url); + try { + Completable<@Nullable Stream> streamPromise = new Completable<>(); + ContentStreamListenerAdapter contentStreamListener = new ContentStreamListenerAdapter(); + session.newStream(headers, streamPromise, contentStreamListener); + // wait for stream to be opened + Objects.requireNonNull(streamPromise.get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + // wait for HTTP response contents + String contentJson = contentStreamListener.awaitResult(); + String contentType = contentStreamListener.getContentType(); + LOGGER.trace("HTTP/2 200 OK (Content-Type: {}) << {}", contentType, contentJson); + if (!MediaType.APPLICATION_JSON.equals(contentType)) { + throw new ApiException("Unexpected Content-Type: " + contentType); + } + try { + Resources resources = Objects.requireNonNull(jsonParser.fromJson(contentJson, Resources.class)); + if (LOGGER.isDebugEnabled()) { + resources.getErrors().forEach(error -> LOGGER.debug("Resources error:{}", error)); + } + return resources; + } catch (JsonParseException e) { + throw new ApiException("Parsing error", e); + } + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof HttpUnauthorizedException) { + throw (HttpUnauthorizedException) cause; + } + throw new ApiException("Error sending request", e); + } catch (TimeoutException e) { + throw new ApiException("Error sending request", e); + } finally { + throttleDone(); + } + } + + /** + * Build a full path to a server end point, based on a Reference class instance. If the reference contains only + * a resource type, the method returns the end point url to get all resources of the given resource type. Whereas if + * it also contains an id, the method returns the end point url to get the specific single resource with that type + * and id. + * + * @param reference a Reference class instance. + * @return the complete end point url. + */ + private String getUrl(ResourceReference reference) { + String url = baseUrl + reference.getType().name().toLowerCase(); + String id = reference.getId(); + return Objects.isNull(id) || id.isEmpty() ? url : url + "/" + id; + } + + /** + * Restart the session. + * + * @param active boolean that selects whether to restart in active or passive mode. + */ + private void internalRestart(boolean active) { + try { + openPassive(); + if (active) { + openActive(); + } + internalRestartScheduled = false; + } catch (ApiException e) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("internalRestart() failed", e); + } else { + LOGGER.warn("Scheduled reconnection task failed."); + } + internalRestartScheduled = false; + close2(); + } catch (InterruptedException e) { + } + } + + /** + * The event stream calls this method when it has received text data. It parses the text as JSON into a list of + * Event entries, converts the list of events to a list of resources, and forwards that list to the bridge + * handler. + * + * @param data the incoming (presumed to be JSON) text. + */ + protected void onEventData(String data) { + if (onlineState != State.ACTIVE) { + return; + } + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("onEventData() data:{}", data); + } else { + LOGGER.debug("onEventData() data length:{}", data.length()); + } + JsonElement jsonElement; + try { + jsonElement = JsonParser.parseString(data); + } catch (JsonSyntaxException e) { + LOGGER.debug("onEventData() invalid data '{}'", data, e); + return; + } + if (!(jsonElement instanceof JsonArray)) { + LOGGER.debug("onEventData() data is not a JsonArray {}", data); + return; + } + List events; + try { + events = jsonParser.fromJson(jsonElement, Event.EVENT_LIST_TYPE); + } catch (JsonParseException e) { + LOGGER.debug("onEventData() parsing error json:{}", data, e); + return; + } + if (Objects.isNull(events) || events.isEmpty()) { + LOGGER.debug("onEventData() event list is null or empty"); + return; + } + List resources = new ArrayList<>(); + events.forEach(event -> resources.addAll(event.getData())); + if (resources.isEmpty()) { + LOGGER.debug("onEventData() resource list is empty"); + return; + } + resources.forEach(resource -> resource.markAsSparse()); + bridgeHandler.onResourcesEvent(resources); + } + + /** + * Open the HTTP 2 session and the event stream. + * + * @throws ApiException if there was a communication error. + * @throws InterruptedException + */ + public void open() throws ApiException, InterruptedException { + LOGGER.debug("open()"); + openPassive(); + openActive(); + bridgeHandler.onConnectionOnline(); + } + + /** + * Make the session active, by opening an HTTP 2 SSE event stream (if necessary). + * + * @throws ApiException if an error was encountered. + * @throws InterruptedException + */ + private void openActive() throws ApiException, InterruptedException { + synchronized (this) { + openEventStream(); + onlineState = State.ACTIVE; + } + } + + /** + * Open the check alive task if necessary. + */ + private void openCheckAliveTask() { + Future task = checkAliveTask; + if (Objects.isNull(task) || task.isCancelled() || task.isDone()) { + LOGGER.debug("openCheckAliveTask()"); + cancelTask(checkAliveTask, false); + checkAliveTask = bridgeHandler.getScheduler().scheduleWithFixedDelay(() -> checkAlive(), + CHECK_ALIVE_SECONDS, CHECK_ALIVE_SECONDS, TimeUnit.SECONDS); + } + } + + /** + * Implementation to open an HTTP 2 SSE event stream if necessary. + * + * @throws ApiException if an error was encountered. + * @throws InterruptedException + */ + private void openEventStream() throws ApiException, InterruptedException { + Session session = http2Session; + if (Objects.isNull(session) || session.isClosed()) { + throw new ApiException("HTTP 2 session is null or closed"); + } + if (session.getStreams().stream().anyMatch(stream -> Objects.nonNull(stream.getAttribute(EVENT_STREAM_ID)))) { + return; + } + LOGGER.debug("openEventStream()"); + HeadersFrame headers = prepareHeaders(eventUrl, MediaType.SERVER_SENT_EVENTS); + LOGGER.trace("GET {} HTTP/2", eventUrl); + Stream stream = null; + try { + Completable<@Nullable Stream> streamPromise = new Completable<>(); + EventStreamListenerAdapter eventStreamListener = new EventStreamListenerAdapter(); + session.newStream(headers, streamPromise, eventStreamListener); + // wait for stream to be opened + stream = Objects.requireNonNull(streamPromise.get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + stream.setIdleTimeout(0); + stream.setAttribute(EVENT_STREAM_ID, session); + // wait for "hi" from the bridge + eventStreamListener.awaitResult(); + } catch (ExecutionException | TimeoutException e) { + if (Objects.nonNull(stream)) { + stream.reset(new ResetFrame(stream.getId(), 0), Callback.NOOP); + } + throw new ApiException("Error opening event stream", e); + } + } + + /** + * Private method to open the HTTP 2 session in passive mode. + * + * @throws ApiException if there was a communication error. + * @throws InterruptedException + */ + private void openPassive() throws ApiException, InterruptedException { + synchronized (this) { + LOGGER.debug("openPassive()"); + onlineState = State.CLOSED; + try { + http2Client.start(); + } catch (Exception e) { + throw new ApiException("Error starting HTTP/2 client", e); + } + openSession(); + openCheckAliveTask(); + onlineState = State.PASSIVE; + } + } + + /** + * Open the HTTP 2 session if necessary. + * + * @throws ApiException if it was not possible to create and connect the session. + * @throws InterruptedException + */ + private void openSession() throws ApiException, InterruptedException { + Session session = http2Session; + if (Objects.nonNull(session) && !session.isClosed()) { + return; + } + LOGGER.debug("openSession()"); + InetSocketAddress address = new InetSocketAddress(hostName, 443); + try { + SessionListenerAdapter sessionListener = new SessionListenerAdapter(); + Completable<@Nullable Session> sessionPromise = new Completable<>(); + http2Client.connect(http2Client.getBean(SslContextFactory.class), address, sessionListener, sessionPromise); + // wait for the (SSL) session to be opened + http2Session = Objects.requireNonNull(sessionPromise.get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + checkAliveOk(); // initialise the session timeout window + } catch (ExecutionException | TimeoutException e) { + throw new ApiException("Error opening HTTP 2 session", e); + } + } + + /** + * Helper class to create a HeadersFrame for a standard HTTP GET request. + * + * @param url the server url. + * @param acceptContentType the accepted content type for the response. + * @return the HeadersFrame. + */ + private HeadersFrame prepareHeaders(String url, String acceptContentType) { + return prepareHeaders(url, acceptContentType, "GET", -1, null); + } + + /** + * Helper class to create a HeadersFrame for a more exotic HTTP request. + * + * @param url the server url. + * @param acceptContentType the accepted content type for the response. + * @param method the HTTP request method. + * @param contentLength the length of the content e.g. for a PUT call. + * @param contentType the respective content type. + * @return the HeadersFrame. + */ + private HeadersFrame prepareHeaders(String url, String acceptContentType, String method, long contentLength, + @Nullable String contentType) { + HttpFields fields = new HttpFields(); + fields.put(HttpHeader.ACCEPT, acceptContentType); + if (contentType != null) { + fields.put(HttpHeader.CONTENT_TYPE, contentType); + } + if (contentLength >= 0) { + fields.putLongField(HttpHeader.CONTENT_LENGTH, contentLength); + } + fields.put(APPLICATION_KEY, applicationKey); + return new HeadersFrame(new MetaData.Request(method, new HttpURI(url), HttpVersion.HTTP_2, fields), null, + contentLength <= 0); + } + + /** + * Use an HTTP/2 PUT command to send a resource to the server. + * + * @param resource the resource to put. + * @throws ApiException if something fails. + * @throws InterruptedException + */ + public void putResource(Resource resource) throws ApiException, InterruptedException { + sleepDuringRestart(); + if (onlineState == State.CLOSED) { + return; + } + Session session = http2Session; + if (Objects.isNull(session) || session.isClosed()) { + throw new ApiException("HTTP 2 session is null or closed"); + } + throttle(); + String requestJson = jsonParser.toJson(resource); + ByteBuffer requestBytes = ByteBuffer.wrap(requestJson.getBytes(StandardCharsets.UTF_8)); + String url = getUrl(new ResourceReference().setId(resource.getId()).setType(resource.getType())); + HeadersFrame headers = prepareHeaders(url, MediaType.APPLICATION_JSON, "PUT", requestBytes.capacity(), + MediaType.APPLICATION_JSON); + LOGGER.trace("PUT {} HTTP/2 >> {}", url, requestJson); + try { + Completable<@Nullable Stream> streamPromise = new Completable<>(); + ContentStreamListenerAdapter contentStreamListener = new ContentStreamListenerAdapter(); + session.newStream(headers, streamPromise, contentStreamListener); + // wait for stream to be opened + Stream stream = Objects.requireNonNull(streamPromise.get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + stream.data(new DataFrame(stream.getId(), requestBytes, true), Callback.NOOP); + // wait for HTTP response + String contentJson = contentStreamListener.awaitResult(); + String contentType = contentStreamListener.getContentType(); + LOGGER.trace("HTTP/2 200 OK (Content-Type: {}) << {}", contentType, contentJson); + if (!MediaType.APPLICATION_JSON.equals(contentType)) { + throw new ApiException("Unexpected Content-Type: " + contentType); + } + try { + Resources resources = Objects.requireNonNull(jsonParser.fromJson(contentJson, Resources.class)); + if (LOGGER.isDebugEnabled()) { + resources.getErrors().forEach(error -> LOGGER.debug("putResource() resources error:{}", error)); + } + } catch (JsonParseException e) { + LOGGER.debug("putResource() parsing error json:{}", contentJson, e); + throw new ApiException("Parsing error", e); + } + } catch (ExecutionException | TimeoutException e) { + throw new ApiException("putResource() error sending request", e); + } finally { + throttleDone(); + } + } + + /** + * Try to register the application key with the hub. Use the given application key if one is provided; otherwise the + * hub will create a new one. Note: this requires an HTTP 1.1 client call. + * + * @param oldApplicationKey existing application key if any i.e. may be empty. + * @return the existing or a newly created application key. + * @throws HttpUnauthorizedException if the registration failed. + * @throws ApiException if there was a communications error. + * @throws InterruptedException + */ + public String registerApplicationKey(@Nullable String oldApplicationKey) + throws HttpUnauthorizedException, ApiException, InterruptedException { + LOGGER.debug("registerApplicationKey()"); + String json = jsonParser.toJson((Objects.isNull(oldApplicationKey) || oldApplicationKey.isEmpty()) + ? new CreateUserRequest(APPLICATION_ID) + : new CreateUserRequest(oldApplicationKey, APPLICATION_ID)); + Request httpRequest = httpClient.newRequest(registrationUrl).method(HttpMethod.POST) + .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .content(new StringContentProvider(json), MediaType.APPLICATION_JSON); + ContentResponse contentResponse; + try { + LOGGER.trace("POST {} HTTP/1.1 >> {}", registrationUrl, json); + contentResponse = httpRequest.send(); + } catch (TimeoutException | ExecutionException e) { + throw new ApiException("HTTP processing error", e); + } + int httpStatus = contentResponse.getStatus(); + json = contentResponse.getContentAsString().trim(); + LOGGER.trace("HTTP/1.1 {} {} << {}", httpStatus, contentResponse.getReason(), json); + if (httpStatus != HttpStatus.OK_200) { + throw new ApiException("HTTP bad response"); + } + try { + List entries = jsonParser.fromJson(json, SuccessResponse.GSON_TYPE); + if (Objects.nonNull(entries) && !entries.isEmpty()) { + SuccessResponse response = entries.get(0); + Map responseSuccess = response.success; + if (Objects.nonNull(responseSuccess)) { + String newApplicationKey = (String) responseSuccess.get("username"); + if (Objects.nonNull(newApplicationKey)) { + return newApplicationKey; + } + } + } + } catch (JsonParseException e) { + LOGGER.debug("registerApplicationKey() parsing error json:{}", json, e); + } + throw new HttpUnauthorizedException("Application key registration failed"); + } + + public void setExternalRestartScheduled() { + externalRestartScheduled = true; + internalRestartScheduled = false; + cancelTask(internalRestartTask, false); + internalRestartTask = null; + close2(); + } + + /** + * Sleep the caller during any period when the connection is restarting. + * + * @throws ApiException if anything failed. + * @throws InterruptedException + */ + private void sleepDuringRestart() throws ApiException, InterruptedException { + Future restartTask = this.internalRestartTask; + if (Objects.nonNull(restartTask)) { + try { + restartTask.get(RESTART_AFTER_SECONDS * 2, TimeUnit.SECONDS); + } catch (ExecutionException | TimeoutException e) { + throw new ApiException("sleepDuringRestart() error", e); + } + } + internalRestartScheduled = false; + } + + /** + * Test the Hue Bridge connection state by attempting to connect and trying to execute a basic command that requires + * authentication. + * + * @throws HttpUnauthorizedException if it was possible to connect but not to authenticate. + * @throws ApiException if it was not possible to connect. + * @throws InterruptedException + */ + public void testConnectionState() throws HttpUnauthorizedException, ApiException, InterruptedException { + LOGGER.debug("testConnectionState()"); + try { + openPassive(); + getResourcesImpl(BRIDGE); + } catch (ApiException e) { + close2(); + throw e; + } + } + + /** + * Hue Bridges get confused if they receive too many HTTP requests in a short period of time (e.g. on start up), or + * if too many HTTP sessions are opened at the same time. So this method throttles the requests to a maximum of one + * per REQUEST_INTERVAL_MILLISECS, and ensures that no more than MAX_CONCURRENT_SESSIONS sessions are started. + * + * @throws InterruptedException + */ + private synchronized void throttle() throws InterruptedException { + streamMutex.acquire(); + Instant now = Instant.now(); + if (lastRequestTime.isPresent()) { + long delay = Duration.between(now, lastRequestTime.get()).toMillis() + REQUEST_INTERVAL_MILLISECS; + if (delay > 0) { + Thread.sleep(delay); + } + } + lastRequestTime = Optional.of(now); + } + + /** + * Release the mutex. + */ + private void throttleDone() { + streamMutex.release(); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/console/HueCommandExtension.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/console/HueCommandExtension.java index befe496da..b299cf569 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/console/HueCommandExtension.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/console/HueCommandExtension.java @@ -12,13 +12,27 @@ */ package org.openhab.binding.hue.internal.console; +import static org.openhab.binding.hue.internal.HueBindingConstants.*; + import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.hue.internal.HueBindingConstants; +import org.openhab.binding.hue.internal.dto.clip2.MetaData; +import org.openhab.binding.hue.internal.dto.clip2.Resource; +import org.openhab.binding.hue.internal.dto.clip2.ResourceReference; +import org.openhab.binding.hue.internal.dto.clip2.enums.Archetype; +import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType; +import org.openhab.binding.hue.internal.exceptions.ApiException; +import org.openhab.binding.hue.internal.exceptions.AssetNotLoadedException; +import org.openhab.binding.hue.internal.handler.Clip2BridgeHandler; import org.openhab.binding.hue.internal.handler.HueBridgeHandler; import org.openhab.binding.hue.internal.handler.HueGroupHandler; import org.openhab.core.io.console.Console; @@ -38,19 +52,36 @@ import org.osgi.service.component.annotations.Reference; * The {@link HueCommandExtension} is responsible for handling console commands * * @author Laurent Garnier - Initial contribution + * @author Andrew Fiddian-Green - Added CLIP 2 console commands */ @NonNullByDefault @Component(service = ConsoleCommandExtension.class) public class HueCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter { + private static final String FMT_BRIDGE = "Bridge %s \"Philips Hue Bridge\" [ipAddress=\"%s\", applicationKey=\"%s\"] {"; + private static final String FMT_THING = " Thing %s %s \"%s\" [resourceId=\"%s\"] // %s idV1:%s"; + private static final String FMT_COMMENT = " // %s things"; + private static final String FMT_APPKEY = " - Application key: %s"; + private static final String FMT_SCENE = " %s '%s'"; + private static final String USER_NAME = "username"; private static final String SCENES = "scenes"; + private static final String APPLICATION_KEY = "applicationkey"; + private static final String THINGS = "things"; + private static final StringsCompleter SUBCMD_COMPLETER = new StringsCompleter(List.of(USER_NAME, SCENES), false); + + private static final StringsCompleter SUBCMD_COMPLETER_2 = new StringsCompleter( + List.of(APPLICATION_KEY, THINGS, SCENES), false); + private static final StringsCompleter SCENES_COMPLETER = new StringsCompleter(List.of(SCENES), false); private final ThingRegistry thingRegistry; + public static final Set SUPPORTED_RESOURCES = Set.of(ResourceType.DEVICE, ResourceType.ROOM, + ResourceType.ZONE, ResourceType.BRIDGE_HOME); + @Activate public HueCommandExtension(final @Reference ThingRegistry thingRegistry) { super("hue", "Interact with the Hue binding."); @@ -64,9 +95,12 @@ public class HueCommandExtension extends AbstractConsoleCommandExtension impleme ThingHandler thingHandler = null; HueBridgeHandler bridgeHandler = null; HueGroupHandler groupHandler = null; + Clip2BridgeHandler clip2BridgeHandler = null; if (thing != null) { thingHandler = thing.getHandler(); - if (thingHandler instanceof HueBridgeHandler) { + if (thingHandler instanceof Clip2BridgeHandler) { + clip2BridgeHandler = (Clip2BridgeHandler) thingHandler; + } else if (thingHandler instanceof HueBridgeHandler) { bridgeHandler = (HueBridgeHandler) thingHandler; } else if (thingHandler instanceof HueGroupHandler) { groupHandler = (HueGroupHandler) thingHandler; @@ -74,45 +108,130 @@ public class HueCommandExtension extends AbstractConsoleCommandExtension impleme } if (thing == null) { console.println("Bad thing id '" + args[0] + "'"); - printUsage(console); } else if (thingHandler == null) { console.println("No handler initialized for the thingUID '" + args[0] + "'"); - printUsage(console); - } else if (bridgeHandler == null && groupHandler == null) { + } else if (bridgeHandler == null && groupHandler == null && clip2BridgeHandler == null) { console.println("'" + args[0] + "' is neither a Hue BridgeUID nor a Hue groupThingUID"); - printUsage(console); } else { - switch (args[1]) { - case USER_NAME: - if (bridgeHandler != null) { + if (bridgeHandler != null) { + switch (args[1]) { + case USER_NAME: String userName = bridgeHandler.getUserName(); console.println("Your user name is " + (userName != null ? userName : "undefined")); - } else { - console.println("'" + args[0] + "' is not a Hue BridgeUID"); - printUsage(console); - } - break; - case SCENES: - if (bridgeHandler != null) { + return; + case SCENES: bridgeHandler.listScenesForConsole().forEach(console::println); - } else if (groupHandler != null) { + return; + } + } else if (groupHandler != null) { + switch (args[1]) { + case SCENES: groupHandler.listScenesForConsole().forEach(console::println); - } - break; - default: - printUsage(console); - break; + return; + } + } else if (clip2BridgeHandler != null) { + String applicationKey = clip2BridgeHandler.getApplicationKey(); + String ipAddress = clip2BridgeHandler.getIpAddress(); + String exception = ""; + + switch (args[1]) { + case APPLICATION_KEY: + console.println(String.format(FMT_APPKEY, applicationKey)); + return; + + case SCENES: + console.println(String.format(FMT_BRIDGE, thing.getUID(), ipAddress, applicationKey)); + try { + List scenes = clip2BridgeHandler + .getResources(new ResourceReference().setType(ResourceType.SCENE)) + .getResources(); + if (scenes.isEmpty()) { + console.println("no scenes found"); + } else { + scenes.forEach(scene -> console + .println(String.format(FMT_SCENE, scene.getId(), scene.getName()))); + } + } catch (ApiException | AssetNotLoadedException e) { + exception = String.format("%s: '%s'", e.getClass().getName(), e.getMessage()); + } catch (InterruptedException e) { + } + console.println("}"); + if (!exception.isBlank()) { + console.println(exception); + } + return; + + case THINGS: + console.println(String.format(FMT_BRIDGE, thing.getUID(), ipAddress, applicationKey)); + + for (ResourceType resourceType : SUPPORTED_RESOURCES) { + List resources; + try { + resources = clip2BridgeHandler + .getResources(new ResourceReference().setType(resourceType)).getResources(); + } catch (ApiException | AssetNotLoadedException e) { + exception = String.format("%s: '%s'", e.getClass().getName(), e.getMessage()); + break; + } catch (InterruptedException e) { + break; + } + if (!resources.isEmpty()) { + console.println(String.format(FMT_COMMENT, resourceType.toString())); + Map lines = new TreeMap<>(); + + for (Resource resource : resources) { + MetaData metaData = resource.getMetaData(); + if (Objects.nonNull(metaData) + && (metaData.getArchetype() == Archetype.BRIDGE_V2)) { + // do not list the bridge itself + continue; + } + String resourceId = resource.getId(); + String idv1 = resource.getIdV1(); + String thingLabel = resource.getName(); + String comment = resource.getProductName(); + String thingType = resourceType.name().toLowerCase(); + String thingId = resourceId; + + // special zone 'all lights' + if (resource.getType() == ResourceType.BRIDGE_HOME) { + thingLabel = clip2BridgeHandler.getLocalizedText(ALL_LIGHTS_KEY); + comment = "Zone"; + thingType = comment.toLowerCase(); + } + + Optional legacyThingOptional = clip2BridgeHandler.getLegacyThing(idv1); + if (legacyThingOptional.isPresent()) { + Thing legacyThing = legacyThingOptional.get(); + thingId = legacyThing.getUID().getId(); + String legacyLabel = legacyThing.getLabel(); + thingLabel = Objects.nonNull(legacyLabel) ? legacyLabel : thingLabel; + } + + lines.put(thingLabel, String.format(FMT_THING, thingType, thingId, thingLabel, + resourceId, comment, idv1)); + } + lines.entrySet().forEach(entry -> console.println(entry.getValue())); + } + } + console.println("}"); + if (!exception.isBlank()) { + console.println(exception); + } + return; + } } } - } else { - printUsage(console); } + printUsage(console); } @Override public List getUsages() { return Arrays.asList(new String[] { buildCommandUsage(" " + USER_NAME, "show the user name"), + buildCommandUsage(" " + APPLICATION_KEY, "show the API v2 application key"), buildCommandUsage(" " + SCENES, "list all the scenes with their id"), + buildCommandUsage(" " + THINGS, "list all the API v2 device/room/zone things with their id"), buildCommandUsage(" " + SCENES, "list all the scenes from this group with their id") }); } @@ -125,15 +244,18 @@ public class HueCommandExtension extends AbstractConsoleCommandExtension impleme public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) { if (cursorArgumentIndex <= 0) { return new StringsCompleter(thingRegistry.getAll().stream() - .filter(t -> HueBindingConstants.THING_TYPE_BRIDGE.equals(t.getThingTypeUID()) - || HueBindingConstants.THING_TYPE_GROUP.equals(t.getThingTypeUID())) + .filter(t -> THING_TYPE_BRIDGE.equals(t.getThingTypeUID()) + || THING_TYPE_BRIDGE_API2.equals(t.getThingTypeUID()) + || THING_TYPE_GROUP.equals(t.getThingTypeUID())) .map(t -> t.getUID().getAsString()).collect(Collectors.toList()), true) .complete(args, cursorArgumentIndex, cursorPosition, candidates); } else if (cursorArgumentIndex == 1) { Thing thing = getThing(args[0]); - if (thing != null && HueBindingConstants.THING_TYPE_BRIDGE.equals(thing.getThingTypeUID())) { + if (thing != null && (THING_TYPE_BRIDGE.equals(thing.getThingTypeUID()))) { return SUBCMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates); - } else if (thing != null && HueBindingConstants.THING_TYPE_GROUP.equals(thing.getThingTypeUID())) { + } else if (thing != null && (THING_TYPE_BRIDGE_API2.equals(thing.getThingTypeUID()))) { + return SUBCMD_COMPLETER_2.complete(args, cursorArgumentIndex, cursorPosition, candidates); + } else if (thing != null && THING_TYPE_GROUP.equals(thing.getThingTypeUID())) { return SCENES_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates); } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/Clip2ThingDiscoveryService.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/Clip2ThingDiscoveryService.java new file mode 100644 index 000000000..829476e36 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/Clip2ThingDiscoveryService.java @@ -0,0 +1,194 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.discovery; + +import static org.openhab.binding.hue.internal.HueBindingConstants.*; + +import java.util.Date; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.dto.clip2.MetaData; +import org.openhab.binding.hue.internal.dto.clip2.Resource; +import org.openhab.binding.hue.internal.dto.clip2.ResourceReference; +import org.openhab.binding.hue.internal.dto.clip2.enums.Archetype; +import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType; +import org.openhab.binding.hue.internal.exceptions.ApiException; +import org.openhab.binding.hue.internal.exceptions.AssetNotLoadedException; +import org.openhab.binding.hue.internal.handler.Clip2BridgeHandler; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Discovery service to find resource things on a Hue Bridge that is running CLIP 2. + * + * @author Andrew Fiddian-Green - Initial Contribution + */ +@NonNullByDefault +public class Clip2ThingDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService { + + private final Logger logger = LoggerFactory.getLogger(Clip2ThingDiscoveryService.class); + + private static final int DISCOVERY_TIMEOUT_SECONDS = 20; + private static final int DISCOVERY_INTERVAL_SECONDS = 600; + + /** + * Map of resource types and respective thing types that shall be discovered. + */ + private static final Map DISCOVERY_TYPES = Map.of( // + ResourceType.DEVICE, THING_TYPE_DEVICE, // + ResourceType.ROOM, THING_TYPE_ROOM, // + ResourceType.ZONE, THING_TYPE_ZONE, // + ResourceType.BRIDGE_HOME, THING_TYPE_ZONE); + + private @Nullable Clip2BridgeHandler bridgeHandler; + private @Nullable ScheduledFuture discoveryTask; + + public Clip2ThingDiscoveryService() { + super(Set.of(THING_TYPE_DEVICE, THING_TYPE_ROOM, THING_TYPE_ZONE), DISCOVERY_TIMEOUT_SECONDS, true); + } + + @Override + public void activate() { + Clip2BridgeHandler bridgeHandler = this.bridgeHandler; + if (Objects.nonNull(bridgeHandler)) { + bridgeHandler.registerDiscoveryService(this); + } + super.activate(null); + } + + @Override + public void deactivate() { + super.deactivate(); + Clip2BridgeHandler bridgeHandler = this.bridgeHandler; + if (Objects.nonNull(bridgeHandler)) { + bridgeHandler.unregisterDiscoveryService(); + removeOlderResults(new Date().getTime(), bridgeHandler.getThing().getBridgeUID()); + this.bridgeHandler = null; + } + } + + /** + * If the bridge is online, then query it to get all resource types within it, which are allowed to be instantiated + * as OH things, and announce those respective things by calling the core 'thingDiscovered()' method. + */ + private synchronized void discoverThings() { + Clip2BridgeHandler bridgeHandler = this.bridgeHandler; + if (Objects.nonNull(bridgeHandler) && bridgeHandler.getThing().getStatus() == ThingStatus.ONLINE) { + try { + ThingUID bridgeUID = bridgeHandler.getThing().getUID(); + for (Entry entry : DISCOVERY_TYPES.entrySet()) { + for (Resource resource : bridgeHandler.getResources(new ResourceReference().setType(entry.getKey())) + .getResources()) { + + MetaData metaData = resource.getMetaData(); + if (Objects.nonNull(metaData) && (metaData.getArchetype() == Archetype.BRIDGE_V2)) { + // the bridge device is handled by a bridge thing handler + continue; + } + + String resourceId = resource.getId(); + String idv1 = resource.getIdV1(); + String resourceType = resource.getType().toString(); + String resourceName = resource.getName(); + String thingId = resourceId; + String thingLabel = resourceName; + String legacyThingUID = null; + + // special zone 'all lights' + if (resource.getType() == ResourceType.BRIDGE_HOME) { + thingLabel = bridgeHandler.getLocalizedText(ALL_LIGHTS_KEY); + } + + Optional legacyThingOptional = bridgeHandler.getLegacyThing(idv1); + if (legacyThingOptional.isPresent()) { + Thing legacyThing = legacyThingOptional.get(); + legacyThingUID = legacyThing.getUID().getAsString(); + thingId = legacyThing.getUID().getId(); + String legacyLabel = legacyThing.getLabel(); + thingLabel = Objects.nonNull(legacyLabel) ? legacyLabel : thingLabel; + } + + DiscoveryResultBuilder builder = DiscoveryResultBuilder + .create(new ThingUID(entry.getValue(), bridgeUID, thingId)) // + .withBridge(bridgeUID) // + .withLabel(thingLabel) // + .withProperty(PROPERTY_RESOURCE_ID, resourceId) + .withProperty(PROPERTY_RESOURCE_TYPE, resourceType) + .withProperty(PROPERTY_RESOURCE_NAME, resourceName) + .withRepresentationProperty(PROPERTY_RESOURCE_ID); + + if (Objects.nonNull(legacyThingUID)) { + builder = builder.withProperty(PROPERTY_LEGACY_THING_UID, legacyThingUID); + } + thingDiscovered(builder.build()); + } + } + } catch (ApiException | AssetNotLoadedException e) { + logger.debug("discoverThings() bridge is offline or in a bad state"); + } catch (InterruptedException e) { + } + } + stopScan(); + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return bridgeHandler; + } + + @Override + public void setThingHandler(ThingHandler handler) { + if (handler instanceof Clip2BridgeHandler) { + bridgeHandler = (Clip2BridgeHandler) handler; + } + } + + @Override + protected void startBackgroundDiscovery() { + ScheduledFuture discoveryTask = this.discoveryTask; + if (Objects.isNull(discoveryTask) || discoveryTask.isCancelled()) { + this.discoveryTask = scheduler.scheduleWithFixedDelay(this::discoverThings, 0, DISCOVERY_INTERVAL_SECONDS, + TimeUnit.SECONDS); + } + } + + @Override + protected void startScan() { + scheduler.execute(this::discoverThings); + } + + @Override + protected void stopBackgroundDiscovery() { + ScheduledFuture discoveryTask = this.discoveryTask; + if (Objects.nonNull(discoveryTask)) { + discoveryTask.cancel(true); + this.discoveryTask = null; + } + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java index 1c2e162a8..46c8915a4 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeMDNSDiscoveryParticipant.java @@ -14,37 +14,41 @@ package org.openhab.binding.hue.internal.discovery; import static org.openhab.binding.hue.internal.HueBindingConstants.*; +import java.io.IOException; import java.util.Dictionary; -import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import javax.jmdns.ServiceInfo; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.hue.internal.handler.HueBridgeHandler; +import org.openhab.binding.hue.internal.connection.Clip2Bridge; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant; -import org.openhab.core.config.discovery.mdns.internal.MDNSDiscoveryService; import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The {@link HueBridgeMDNSDiscoveryParticipant} is responsible for discovering new and removed Hue Bridges. It uses the - * central {@link MDNSDiscoveryService}. + * central MDNSDiscoveryService. * * @author Kai Kreuzer - Initial contribution * @author Thomas Höfer - Added representation * @author Christoph Weitkamp - Change discovery protocol to mDNS + * @author Andrew Fiddian-Green - Added support for CLIP 2 bridge discovery */ @Component(configurationPid = "discovery.hue") @NonNullByDefault @@ -55,11 +59,16 @@ public class HueBridgeMDNSDiscoveryParticipant implements MDNSDiscoveryParticipa private static final String MDNS_PROPERTY_MODEL_ID = "modelid"; private final Logger logger = LoggerFactory.getLogger(HueBridgeMDNSDiscoveryParticipant.class); + protected final ThingRegistry thingRegistry; private long removalGracePeriod = 0L; - private boolean isAutoDiscoveryEnabled = true; + @Activate + public HueBridgeMDNSDiscoveryParticipant(final @Reference ThingRegistry thingRegistry) { + this.thingRegistry = thingRegistry; + } + @Activate protected void activate(ComponentContext componentContext) { activateOrModifyService(componentContext); @@ -90,7 +99,7 @@ public class HueBridgeMDNSDiscoveryParticipant implements MDNSDiscoveryParticipa @Override public Set getSupportedThingTypeUIDs() { - return HueBridgeHandler.SUPPORTED_THING_TYPES; + return Set.of(THING_TYPE_BRIDGE, THING_TYPE_BRIDGE_API2); } @Override @@ -102,29 +111,62 @@ public class HueBridgeMDNSDiscoveryParticipant implements MDNSDiscoveryParticipa public @Nullable DiscoveryResult createResult(ServiceInfo service) { if (isAutoDiscoveryEnabled) { ThingUID uid = getThingUID(service); - if (uid != null) { + if (Objects.nonNull(uid)) { String host = service.getHostAddresses()[0]; - String id = service.getPropertyString(MDNS_PROPERTY_BRIDGE_ID); - String friendlyName = String.format(DISCOVERY_LABEL_PATTERN, host); - return DiscoveryResultBuilder.create(uid) // - .withProperties(Map.of( // - HOST, host, // - Thing.PROPERTY_MODEL_ID, service.getPropertyString(MDNS_PROPERTY_MODEL_ID), // - Thing.PROPERTY_SERIAL_NUMBER, id.toLowerCase())) // - .withLabel(friendlyName) // + String serial = service.getPropertyString(MDNS_PROPERTY_BRIDGE_ID); + String label = String.format(DISCOVERY_LABEL_PATTERN, host); + String legacyThingUID = null; + + if (new ThingUID(THING_TYPE_BRIDGE_API2, uid.getId()).equals(uid)) { + Optional legacyThingOptional = getLegacyBridge(host); + if (legacyThingOptional.isPresent()) { + Thing legacyThing = legacyThingOptional.get(); + legacyThingUID = legacyThing.getUID().getAsString(); + String label2 = legacyThing.getLabel(); + label = Objects.nonNull(label2) ? label2 : label; + } + } + + DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(uid) // + .withLabel(label) // + .withProperty(HOST, host) // + .withProperty(Thing.PROPERTY_MODEL_ID, service.getPropertyString(MDNS_PROPERTY_MODEL_ID)) // + .withProperty(Thing.PROPERTY_SERIAL_NUMBER, serial.toLowerCase()) // .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER) // - .withTTL(120L) // - .build(); + .withTTL(120L); + + if (Objects.nonNull(legacyThingUID)) { + builder = builder.withProperty(PROPERTY_LEGACY_THING_UID, legacyThingUID); + } + return builder.build(); } } return null; } + /** + * Get the legacy Hue bridge (if any) on the given IP address. + * + * @param ipAddress the IP address. + * @return Optional of a legacy bridge thing. + */ + private Optional getLegacyBridge(String ipAddress) { + return thingRegistry.getAll().stream().filter(thing -> THING_TYPE_BRIDGE.equals(thing.getThingTypeUID()) + && ipAddress.equals(thing.getConfiguration().get(HOST))).findFirst(); + } + @Override public @Nullable ThingUID getThingUID(ServiceInfo service) { String id = service.getPropertyString(MDNS_PROPERTY_BRIDGE_ID); if (id != null && !id.isBlank()) { - return new ThingUID(THING_TYPE_BRIDGE, id.toLowerCase()); + id = id.toLowerCase(); + try { + return Clip2Bridge.isClip2Supported(service.getHostAddresses()[0]) + ? new ThingUID(THING_TYPE_BRIDGE_API2, id) + : new ThingUID(THING_TYPE_BRIDGE, id); + } catch (IOException e) { + // fall through + } } return null; } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeNupnpDiscovery.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeNupnpDiscovery.java index 7dfec9b66..c4ce4769b 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeNupnpDiscovery.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeNupnpDiscovery.java @@ -16,20 +16,24 @@ import static org.openhab.binding.hue.internal.HueBindingConstants.*; import java.io.IOException; import java.util.List; -import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.hue.internal.handler.HueBridgeHandler; +import org.openhab.binding.hue.internal.connection.Clip2Bridge; import org.openhab.core.config.discovery.AbstractDiscoveryService; -import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.io.net.http.HttpUtil; import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,9 +62,12 @@ public class HueBridgeNupnpDiscovery extends AbstractDiscoveryService { private static final int DISCOVERY_TIMEOUT = 10; private final Logger logger = LoggerFactory.getLogger(HueBridgeNupnpDiscovery.class); + private final ThingRegistry thingRegistry; - public HueBridgeNupnpDiscovery() { - super(HueBridgeHandler.SUPPORTED_THING_TYPES, DISCOVERY_TIMEOUT, false); + @Activate + public HueBridgeNupnpDiscovery(final @Reference ThingRegistry thingRegistry) { + super(Set.of(THING_TYPE_BRIDGE, THING_TYPE_BRIDGE_API2), DISCOVERY_TIMEOUT, false); + this.thingRegistry = thingRegistry; } @Override @@ -77,14 +84,31 @@ public class HueBridgeNupnpDiscovery extends AbstractDiscoveryService { String host = bridge.getInternalIpAddress(); String serialNumber = bridge.getId().toLowerCase(); ThingUID uid = new ThingUID(THING_TYPE_BRIDGE, serialNumber); - DiscoveryResult result = DiscoveryResultBuilder.create(uid) // - .withProperties(Map.of( // - HOST, host, // - Thing.PROPERTY_SERIAL_NUMBER, serialNumber)) // - .withLabel(String.format(DISCOVERY_LABEL_PATTERN, host)) // - .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER) // - .build(); - thingDiscovered(result); + ThingUID legacyUID = null; + String label = String.format(DISCOVERY_LABEL_PATTERN, host); + + if (isClip2Supported(host)) { + legacyUID = uid; + uid = new ThingUID(THING_TYPE_BRIDGE_API2, serialNumber); + Optional legacyThingOptional = getLegacyBridge(host); + if (legacyThingOptional.isPresent()) { + Thing legacyThing = legacyThingOptional.get(); + String label2 = legacyThing.getLabel(); + label = Objects.nonNull(label2) ? label2 : label; + } + } + + DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(uid) // + .withLabel(label) // + .withProperty(HOST, host) // + .withProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber) // + .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER); + + if (Objects.nonNull(legacyUID)) { + builder.withProperty(PROPERTY_LEGACY_THING_UID, legacyUID.getAsString()); + } + + thingDiscovered(builder.build()); } } } @@ -170,4 +194,27 @@ public class HueBridgeNupnpDiscovery extends AbstractDiscoveryService { protected @Nullable String doGetRequest(String url) throws IOException { return HttpUtil.executeUrl("GET", url, REQUEST_TIMEOUT); } + + /** + * Get the legacy Hue bridge (if any) on the given IP address. + * + * @param ipAddress the IP address. + * @return Optional of a legacy bridge thing. + */ + private Optional getLegacyBridge(String ipAddress) { + return thingRegistry.getAll().stream().filter(thing -> THING_TYPE_BRIDGE.equals(thing.getThingTypeUID()) + && ipAddress.equals(thing.getConfiguration().get(HOST))).findFirst(); + } + + /** + * Wrap Clip2Bridge.isClip2Supported() inside this method so that integration tests can can override the method, to + * avoid making live network calls. + */ + protected boolean isClip2Supported(String ipAddress) { + try { + return Clip2Bridge.isClip2Supported(ipAddress); + } catch (IOException e) { + return false; + } + } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueDeviceDiscoveryService.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueDeviceDiscoveryService.java index 56b8fda20..1189813b5 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueDeviceDiscoveryService.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueDeviceDiscoveryService.java @@ -277,7 +277,7 @@ public class HueDeviceDiscoveryService extends AbstractDiscoveryService implemen String name; if ("0".equals(group.getId())) { - name = "@text/discovery.group.all_lights.label"; + name = "@text/discovery.group.all-lights.label"; } else if ("Room".equals(group.getType())) { name = group.getName(); } else { diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ActionEntry.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ActionEntry.java new file mode 100644 index 000000000..835d4918b --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ActionEntry.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * DTO that contains an API Action entry. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class ActionEntry { + private @NonNullByDefault({}) ResourceReference target; + private @NonNullByDefault({}) Resource action; + + public ResourceReference getTarget() { + return target; + } + + public Resource getAction() { + return action; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Alerts.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Alerts.java new file mode 100644 index 000000000..6d8b86bd9 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Alerts.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for 'alert' of a light. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Alerts { + private @Nullable @SerializedName("action_values") List actionValues; + private @Nullable String action; + + public @Nullable ActionType getAction() { + String action = this.action; + return Objects.nonNull(action) ? ActionType.of(action) : null; + } + + public List getActionValues() { + List actionValues = this.actionValues; + if (Objects.nonNull(actionValues)) { + return actionValues.stream().map(ActionType::of).collect(Collectors.toList()); + } + return List.of(); + } + + public Alerts setAction(ActionType actionType) { + this.action = actionType.name().toLowerCase(); + return this; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/BridgeConfig.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/BridgeConfig.java new file mode 100644 index 000000000..66d4250ec --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/BridgeConfig.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * A 'special' DTO for bridge discovery to read the software version from a bridge. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class BridgeConfig { + public @Nullable String swversion; +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Button.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Button.java new file mode 100644 index 000000000..7f8da6cc5 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Button.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.hue.internal.dto.clip2.enums.ButtonEventType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for CLIP 2 button state. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Button { + private @NonNullByDefault({}) @SerializedName("last_event") String lastEvent; + + /** + * @return the last button event as an enum. + * @throws IllegalArgumentException if lastEvent is bad. + */ + public ButtonEventType getLastEvent() throws IllegalArgumentException { + return ButtonEventType.valueOf(lastEvent.toUpperCase()); + } + + public State getLastEventState() { + return new StringType(getLastEvent().name()); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ColorTemperature.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ColorTemperature.java new file mode 100644 index 000000000..4b5fd5a5e --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ColorTemperature.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for colour temperature of a light in CLIP 2. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class ColorTemperature { + private @Nullable Long mirek; + private @Nullable @SerializedName("mirek_schema") MirekSchema mirekSchema; + + /** + * Get the color temperature as a QuantityType value. + * + * @return a QuantityType value + * @throws DTOPresentButEmptyException to indicate that the DTO is present but empty. + */ + public @Nullable QuantityType getAbsolute() throws DTOPresentButEmptyException { + Long mirek = this.mirek; + if (Objects.nonNull(mirek)) { + return QuantityType.valueOf(mirek, Units.MIRED).toInvertibleUnit(Units.KELVIN); + } + throw new DTOPresentButEmptyException("'color_temperature' DTO is present but empty"); + } + + public @Nullable Long getMirek() { + return mirek; + } + + public @Nullable MirekSchema getMirekSchema() { + return mirekSchema; + } + + /** + * Get the color temperature as a percentage based on the MirekSchema. Note: this method is only to be used on + * cached state DTOs which already have a defined mirek schema. + * + * @return the percentage of the mirekSchema range. + * @throws DTOPresentButEmptyException to indicate that the DTO is present but empty. + */ + public @Nullable Double getPercent() throws DTOPresentButEmptyException { + Long mirek = this.mirek; + if (Objects.nonNull(mirek)) { + MirekSchema mirekSchema = this.mirekSchema; + mirekSchema = Objects.nonNull(mirekSchema) ? mirekSchema : MirekSchema.DEFAULT_SCHEMA; + double min = mirekSchema.getMirekMinimum(); + double max = mirekSchema.getMirekMaximum(); + double percent = 100f * (mirek.doubleValue() - min) / (max - min); + return Math.max(0, Math.min(100, percent)); + } + throw new DTOPresentButEmptyException("'mirek_schema' DTO is present but empty"); + } + + public ColorTemperature setMirek(double mirek) { + this.mirek = Math.round(mirek); + return this; + } + + public ColorTemperature setMirekSchema(@Nullable MirekSchema mirekSchema) { + this.mirekSchema = mirekSchema; + return this; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ColorXy.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ColorXy.java new file mode 100644 index 000000000..9b909c51b --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ColorXy.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException; +import org.openhab.core.util.ColorUtil.Gamut; + +/** + * DTO for colour X/Y of a light. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class ColorXy { + private @Nullable PairXy xy; + private @Nullable Gamut2 gamut; + + public @Nullable Gamut getGamut() { + Gamut2 gamut = this.gamut; + return Objects.nonNull(gamut) ? gamut.getGamut() : null; + } + + public @Nullable Gamut2 getGamut2() { + return this.gamut; + } + + /** + * @throws DTOPresentButEmptyException to indicate that the DTO is present but empty. + */ + public double[] getXY() throws DTOPresentButEmptyException { + PairXy pairXy = this.xy; + if (Objects.nonNull(pairXy)) { + return pairXy.getXY(); + } + throw new DTOPresentButEmptyException("'color' DTO is present but empty"); + } + + public ColorXy setGamut(@Nullable Gamut gamut) { + this.gamut = Objects.nonNull(gamut) ? new Gamut2().setGamut(gamut) : null; + return this; + } + + public ColorXy setXY(double[] xyValues) { + PairXy pairXy = this.xy; + pairXy = Objects.nonNull(pairXy) ? pairXy : new PairXy(); + pairXy.setXY(xyValues); + this.xy = pairXy; + return this; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Dimming.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Dimming.java new file mode 100644 index 000000000..c51c80c51 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Dimming.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for dimming brightness of a light. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Dimming { + private @Nullable Double brightness; + private @Nullable @SerializedName("min_dim_level") Double minimumDimmingLevel; + + public static final double DEFAULT_MINIMUM_DIMMIMG_LEVEL = 0.5f; + + /** + * @throws DTOPresentButEmptyException to indicate that the DTO is present but empty. + */ + public double getBrightness() throws DTOPresentButEmptyException { + Double brightness = this.brightness; + if (Objects.nonNull(brightness)) { + return brightness; + } + throw new DTOPresentButEmptyException("'dimming' DTO is present but empty"); + } + + public @Nullable Double getMinimumDimmingLevel() { + return minimumDimmingLevel; + } + + public Dimming setBrightness(double brightness) { + this.brightness = brightness; + return this; + } + + public Dimming setMinimumDimmingLevel(Double minimumDimmingLevel) { + this.minimumDimmingLevel = minimumDimmingLevel; + return this; + } + + public @Nullable String toPropertyValue() { + Double minimumDimmingLevel = this.minimumDimmingLevel; + if (Objects.nonNull(minimumDimmingLevel)) { + return String.format("%.1f %% .. 100 %%", minimumDimmingLevel.doubleValue()); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Dynamics.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Dynamics.java new file mode 100644 index 000000000..fd898e673 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Dynamics.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * DTO for dynamics of transitions between light states. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Dynamics { + private @Nullable @SuppressWarnings("unused") Long duration; + private @Nullable @SuppressWarnings("unused") Double speed; + + public Dynamics setDuration(Duration duration) { + this.duration = duration.toMillis(); + return this; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Effects.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Effects.java new file mode 100644 index 000000000..d73020a5e --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Effects.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for 'effects' of a light. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Effects { + private @Nullable @SerializedName("effect_values") List effectValues; + private @Nullable String effect; + private @Nullable @SerializedName("status_values") List statusValues; + private @Nullable String status; + + public boolean allows(EffectType effect) { + List statusValues = this.statusValues; + return Objects.nonNull(statusValues) ? statusValues.contains(effect.name().toLowerCase()) : false; + } + + public EffectType getEffect() { + String effect = this.effect; + return Objects.nonNull(effect) ? EffectType.of(effect) : EffectType.NO_EFFECT; + } + + public EffectType getStatus() { + return Objects.nonNull(status) ? EffectType.of(status) : EffectType.NO_EFFECT; + } + + public List getStatusValues() { + List statusValues = this.statusValues; + return Objects.nonNull(statusValues) ? statusValues : List.of(); + } + + public Effects setEffect(EffectType effectType) { + effect = effectType.name().toLowerCase(); + return this; + } + + public Effects setStatusValues(List statusValues) { + this.statusValues = statusValues; + return this; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Error.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Error.java new file mode 100644 index 000000000..244376829 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Error.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * DTO for CLIP 2 communication errors. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Error { + private @NonNullByDefault({}) String description; + + public String getDescription() { + return description; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Event.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Event.java new file mode 100644 index 000000000..04d397df9 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Event.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.reflect.TypeToken; + +/** + * DTO for CLIP 2 event stream objects. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Event { + public static final Type EVENT_LIST_TYPE = new TypeToken>() { + }.getType(); + + private @Nullable List data = new ArrayList<>(); + + public List getData() { + List data = this.data; + return Objects.nonNull(data) ? data : List.of(); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Gamut2.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Gamut2.java new file mode 100644 index 000000000..b218e57fb --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Gamut2.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.util.ColorUtil.Gamut; + +/** + * DTO for colour gamut of a light. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Gamut2 { + private @Nullable PairXy red; + private @Nullable PairXy green; + private @Nullable PairXy blue; + + public @Nullable Gamut getGamut() { + PairXy red = this.red; + PairXy green = this.green; + PairXy blue = this.blue; + if (Objects.nonNull(red) && Objects.nonNull(green) && Objects.nonNull(blue)) { + return new Gamut(red.getXY(), green.getXY(), blue.getXY()); + } + return null; + } + + public Gamut2 setGamut(Gamut gamut) { + red = new PairXy().setXY(gamut.r()); + green = new PairXy().setXY(gamut.g()); + blue = new PairXy().setXY(gamut.b()); + return this; + } + + public @Nullable String toPropertyValue() { + PairXy red = this.red; + PairXy green = this.green; + PairXy blue = this.blue; + if (Objects.nonNull(red) && Objects.nonNull(green) && Objects.nonNull(blue)) { + double[] r = red.getXY(); + double[] g = green.getXY(); + double[] b = blue.getXY(); + return String.format("(%.3f,%.3f) (%.3f,%.3f) (%.3f,%.3f)", r[0], r[1], g[0], g[1], b[0], b[1]); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/LightLevel.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/LightLevel.java new file mode 100644 index 000000000..6d327bb33 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/LightLevel.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for CLIP 2 light level sensor. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class LightLevel { + private @SerializedName("light_level") int lightLevel; + private @SerializedName("light_level_valid") boolean lightLevelValid; + + public int getLightLevel() { + return lightLevel; + } + + public boolean isLightLevelValid() { + return lightLevelValid; + } + + /** + * Raw sensor light level is '10000 * log10(lux) + 1' so apply the inverse formula to convert to Lux. + * + * @return a QuantityType with light level in Lux, or UNDEF. + */ + public State getLightLevelState() { + if (lightLevelValid) { + double rawLightLevel = lightLevel; + if (rawLightLevel > 1f) { + return new QuantityType<>(Math.pow(10f, (rawLightLevel - 1f) / 10000f), Units.LUX); + } + } + return UnDefType.UNDEF; + } + + public State isLightLevelValidState() { + return OnOffType.from(lightLevelValid); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/MetaData.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/MetaData.java new file mode 100644 index 000000000..b41661946 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/MetaData.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.dto.clip2.enums.Archetype; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for CLIP 2 product metadata. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class MetaData { + private @Nullable String archetype; + private @Nullable String name; + private @Nullable @SerializedName("control_id") Integer controlId; + + public Archetype getArchetype() { + return Archetype.of(archetype); + } + + public @Nullable String getName() { + return name; + } + + public int getControlId() { + Integer controlId = this.controlId; + return controlId != null ? controlId.intValue() : 0; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/MirekSchema.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/MirekSchema.java new file mode 100644 index 000000000..3a3dee205 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/MirekSchema.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for CLIP 2 mirek schema. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class MirekSchema { + private static final int MIN = 153; + private static final int MAX = 500; + + public static final MirekSchema DEFAULT_SCHEMA = new MirekSchema(); + + private @SerializedName("mirek_minimum") int mirekMinimum = MIN; + private @SerializedName("mirek_maximum") int mirekMaximum = MAX; + + public int getMirekMaximum() { + return mirekMaximum; + } + + public int getMirekMinimum() { + return mirekMinimum; + } + + private String toKelvin(int mirek) { + QuantityType kelvin = QuantityType.valueOf(mirek, Units.MIRED).toInvertibleUnit(Units.KELVIN); + return Objects.nonNull(kelvin) ? String.format("%.0f K", kelvin.doubleValue()) : ""; + } + + public String toPropertyValue() { + return String.format("%s .. %s", toKelvin(mirekMinimum), toKelvin(mirekMaximum)); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Motion.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Motion.java new file mode 100644 index 000000000..a669af028 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Motion.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for CLIP 2 motion sensor. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Motion { + private boolean motion; + private @SerializedName("motion_valid") boolean motionValid; + + public boolean isMotion() { + return motion; + } + + public boolean isMotionValid() { + return motionValid; + } + + public State getMotionState() { + return motionValid ? OnOffType.from(motion) : UnDefType.UNDEF; + } + + public State getMotionValidState() { + return OnOffType.from(motionValid); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/OnState.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/OnState.java new file mode 100644 index 000000000..7ab06791a --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/OnState.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException; + +/** + * DTO for 'on' state of a light. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class OnState { + private @Nullable Boolean on; + + /** + * @throws DTOPresentButEmptyException to indicate that the DTO is present but empty. + */ + public boolean isOn() throws DTOPresentButEmptyException { + Boolean on = this.on; + if (Objects.nonNull(on)) { + return on; + } + throw new DTOPresentButEmptyException("'on' DTO is present but empty"); + } + + public void setOn(boolean on) { + this.on = on; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/PairXy.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/PairXy.java new file mode 100644 index 000000000..fa608129e --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/PairXy.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +/** + * DTO that contains an x and y pair of doubles. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +public class PairXy { + private double x; + private double y; + + public double[] getXY() { + return new double[] { x, y }; + } + + public PairXy setXY(double[] xy) { + x = xy.length > 0 ? xy[0] : 0f; + y = xy.length > 1 ? xy[1] : 0f; + return this; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Power.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Power.java new file mode 100644 index 000000000..3becfcdd7 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Power.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.hue.internal.dto.clip2.enums.BatteryStateType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.types.State; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for CLIP 2 power state. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Power { + private @NonNullByDefault({}) @SerializedName("battery_state") String batteryState; + private @SerializedName("battery_level") int batteryLevel; + + public BatteryStateType getBatteryState() { + try { + return BatteryStateType.valueOf(batteryState.toUpperCase()); + } catch (IllegalArgumentException e) { + return BatteryStateType.CRITICAL; + } + } + + public int getBatteryLevel() { + return batteryLevel; + } + + public State getBatteryLowState() { + return OnOffType.from(getBatteryState() != BatteryStateType.NORMAL); + } + + public State getBatteryLevelState() { + return new DecimalType(getBatteryLevel()); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ProductData.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ProductData.java new file mode 100644 index 000000000..357d272af --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ProductData.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.dto.clip2.enums.Archetype; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for CLIP 2 product data. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class ProductData { + private @NonNullByDefault({}) @SerializedName("model_id") String modelId; + private @NonNullByDefault({}) @SerializedName("manufacturer_name") String manufacturerName; + private @NonNullByDefault({}) @SerializedName("product_name") String productName; + private @NonNullByDefault({}) @SerializedName("product_archetype") String productArchetype; + private @NonNullByDefault({}) Boolean certified; + private @NonNullByDefault({}) @SerializedName("software_version") String softwareVersion; + private @Nullable @SerializedName("hardware_platform_type") String hardwarePlatformType; + + public String getModelId() { + return modelId; + } + + public String getManufacturerName() { + return manufacturerName; + } + + public String getProductName() { + return productName; + } + + public Archetype getProductArchetype() { + return Archetype.of(productArchetype); + } + + public Boolean getCertified() { + return certified != null ? certified : false; + } + + public String getSoftwareVersion() { + return softwareVersion != null ? softwareVersion : ""; + } + + public @Nullable String getHardwarePlatformType() { + return hardwarePlatformType; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Recall.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Recall.java new file mode 100644 index 000000000..226ae8a65 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Recall.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.dto.clip2.enums.RecallAction; + +/** + * DTO for scene recall. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Recall { + private @Nullable @SuppressWarnings("unused") String action; + private @Nullable @SuppressWarnings("unused") String status; + private @Nullable @SuppressWarnings("unused") Long duration; + + public Recall setAction(RecallAction action) { + this.action = action.name().toLowerCase(); + return this; + } + + public Recall setDuration(Duration duration) { + this.duration = duration.toMillis(); + return this; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/RelativeRotary.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/RelativeRotary.java new file mode 100644 index 000000000..d6e6c62c3 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/RelativeRotary.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for CLIP 2 tap switch rotary dial state. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class RelativeRotary { + private @Nullable @SerializedName("last_event") RotationEvent lastEvent; + + public State getActionState() { + RotationEvent lastEvent = getLastEvent(); + return Objects.nonNull(lastEvent) ? lastEvent.getActionState() : UnDefType.NULL; + } + + public @Nullable RotationEvent getLastEvent() { + return lastEvent; + } + + public State getStepsState() { + RotationEvent lastEvent = getLastEvent(); + return Objects.nonNull(lastEvent) ? lastEvent.getStepsState() : UnDefType.NULL; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resource.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resource.java new file mode 100644 index 000000000..20dd1e7be --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resource.java @@ -0,0 +1,659 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType; +import org.openhab.binding.hue.internal.dto.clip2.enums.RecallAction; +import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType; +import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus; +import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.openhab.core.util.ColorUtil; +import org.openhab.core.util.ColorUtil.Gamut; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; + +/** + * Complete Resource information DTO for CLIP 2. + * + * Note: all fields are @Nullable because some cases do not (must not) use them. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Resource { + + public static final double PERCENT_DELTA = 30f; + public static final MathContext PERCENT_MATH_CONTEXT = new MathContext(4, RoundingMode.HALF_UP); + + /** + * The SSE event mechanism sends resources in a sparse (skeleton) format that only includes state fields whose + * values have changed. A sparse resource does not contain the full state of the resource. And the absence of any + * field from such a resource does not indicate that the field value is UNDEF, but rather that the value is the same + * as what it was previously set to by the last non-sparse resource. + */ + private transient boolean hasSparseData; + + private @Nullable String type; + private @Nullable String id; + private @Nullable @SerializedName("bridge_id") String bridgeId; + private @Nullable @SerializedName("id_v1") String idV1; + private @Nullable ResourceReference owner; + private @Nullable MetaData metadata; + private @Nullable @SerializedName("product_data") ProductData productData; + private @Nullable List services; + private @Nullable OnState on; + private @Nullable Dimming dimming; + private @Nullable @SerializedName("color_temperature") ColorTemperature colorTemperature; + private @Nullable ColorXy color; + private @Nullable Alerts alert; + private @Nullable Effects effects; + private @Nullable @SerializedName("timed_effects") TimedEffects timedEffects; + private @Nullable ResourceReference group; + private @Nullable List actions; + private @Nullable Recall recall; + private @Nullable Boolean enabled; + private @Nullable LightLevel light; + private @Nullable Button button; + private @Nullable Temperature temperature; + private @Nullable Motion motion; + private @Nullable @SerializedName("power_state") Power powerState; + private @Nullable @SerializedName("relative_rotary") RelativeRotary relativeRotary; + private @Nullable List children; + private @Nullable JsonElement status; + private @Nullable @SuppressWarnings("unused") Dynamics dynamics; + + /** + * Constructor + * + * @param resourceType + */ + public Resource(@Nullable ResourceType resourceType) { + if (Objects.nonNull(resourceType)) { + setType(resourceType); + } + } + + public @Nullable List getActions() { + return actions; + } + + public @Nullable Alerts getAlerts() { + return alert; + } + + public State getAlertState() { + Alerts alerts = this.alert; + if (Objects.nonNull(alerts)) { + if (!alerts.getActionValues().isEmpty()) { + ActionType alertType = alerts.getAction(); + if (Objects.nonNull(alertType)) { + return new StringType(alertType.name()); + } + return new StringType(ActionType.NO_ACTION.name()); + } + } + return UnDefType.NULL; + } + + public String getArchetype() { + MetaData metaData = getMetaData(); + if (Objects.nonNull(metaData)) { + return metaData.getArchetype().toString(); + } + return getType().toString(); + } + + public State getBatteryLevelState() { + Power powerState = this.powerState; + return Objects.nonNull(powerState) ? powerState.getBatteryLevelState() : UnDefType.NULL; + } + + public State getBatteryLowState() { + Power powerState = this.powerState; + return Objects.nonNull(powerState) ? powerState.getBatteryLowState() : UnDefType.NULL; + } + + public @Nullable String getBridgeId() { + String bridgeId = this.bridgeId; + return Objects.isNull(bridgeId) || bridgeId.isBlank() ? null : bridgeId; + } + + /** + * Get the brightness as a PercentType. If off the brightness is 0, otherwise use dimming value. + * + * @return a PercentType with the dimming state, or UNDEF, or NULL + */ + public State getBrightnessState() { + Dimming dimming = this.dimming; + if (Objects.nonNull(dimming)) { + try { + // if off the brightness is 0, otherwise it is dimming value + OnState on = this.on; + double brightness = Objects.nonNull(on) && !on.isOn() ? 0f + : Math.max(0f, Math.min(100f, dimming.getBrightness())); + return new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT)); + } catch (DTOPresentButEmptyException e) { + return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing + } + } + return UnDefType.NULL; + } + + public @Nullable Button getButton() { + return button; + } + + /** + * Get the state corresponding to a button's last event value multiplied by the controlId found for it in the given + * controlIds map. States are decimal values formatted like '1002' where the first digit is the button's controlId + * and the last digit is the ordinal value of the button's last event. + * + * @param controlIds the map of control ids to be referenced. + * @return the state. + */ + public State getButtonEventState(Map controlIds) { + Button button = this.button; + if (Objects.nonNull(button)) { + try { + return new DecimalType( + (controlIds.getOrDefault(getId(), 0).intValue() * 1000) + button.getLastEvent().ordinal()); + } catch (IllegalArgumentException e) { + // fall through + } + } + return UnDefType.NULL; + } + + public State getButtonLastEventState() { + Button button = this.button; + return Objects.nonNull(button) ? button.getLastEventState() : UnDefType.NULL; + } + + public List getChildren() { + List children = this.children; + return Objects.nonNull(children) ? children : List.of(); + } + + /** + * Get the color as an HSBType. This returns an HSB that is based on an amalgamation of the color xy, dimming, and + * on/off JSON elements. It takes its 'H' & 'S' parts from the 'ColorXy' JSON element, and its 'B' part from the + * on/off resp. dimming JSON elements. If off the B part is 0, otherwise it is the dimming element value. Note: this + * method is only to be used on cached state DTOs which already have a defined color gamut. + * + * @return an HSBType containing the current color and brightness level, or UNDEF or NULL. + */ + public State getColorState() { + ColorXy color = this.color; + if (Objects.nonNull(color)) { + try { + Gamut gamut = color.getGamut(); + gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT; + HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut); + OnState on = this.on; + Dimming dimming = this.dimming; + double brightness = Objects.nonNull(on) && !on.isOn() ? 0 + : Objects.nonNull(dimming) ? Math.max(0, Math.min(100, dimming.getBrightness())) : 50; + return new HSBType(hsb.getHue(), hsb.getSaturation(), + new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT))); + } catch (DTOPresentButEmptyException e) { + return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing + } + } + return UnDefType.NULL; + } + + public @Nullable ColorTemperature getColorTemperature() { + return colorTemperature; + } + + public State getColorTemperatureAbsoluteState() { + ColorTemperature colorTemp = colorTemperature; + if (Objects.nonNull(colorTemp)) { + try { + QuantityType colorTemperature = colorTemp.getAbsolute(); + if (Objects.nonNull(colorTemperature)) { + return colorTemperature; + } + } catch (DTOPresentButEmptyException e) { + return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing + } + } + return UnDefType.NULL; + } + + /** + * Get the colour temperature in percent. Note: this method is only to be used on cached state DTOs which already + * have a defined mirek schema. + * + * @return a PercentType with the colour temperature percentage. + */ + public State getColorTemperaturePercentState() { + ColorTemperature colorTemperature = this.colorTemperature; + if (Objects.nonNull(colorTemperature)) { + try { + Double percent = colorTemperature.getPercent(); + if (Objects.nonNull(percent)) { + return new PercentType(new BigDecimal(percent, PERCENT_MATH_CONTEXT)); + } + } catch (DTOPresentButEmptyException e) { + return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing + } + } + return UnDefType.NULL; + } + + public @Nullable ColorXy getColorXy() { + return color; + } + + /** + * Return an HSB where the HS part is derived from the color xy JSON element (only), so the B part is 100% + * + * @return an HSBType. + */ + public State getColorXyState() { + ColorXy color = this.color; + if (Objects.nonNull(color)) { + try { + Gamut gamut = color.getGamut(); + gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT; + HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut); + return new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED); + } catch (DTOPresentButEmptyException e) { + return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing + } + } + return UnDefType.NULL; + } + + public int getControlId() { + MetaData metadata = this.metadata; + return Objects.nonNull(metadata) ? metadata.getControlId() : 0; + } + + public @Nullable Dimming getDimming() { + return dimming; + } + + /** + * Return a PercentType which is derived from the dimming JSON element (only). + * + * @return a PercentType. + */ + public State getDimmingState() { + Dimming dimming = this.dimming; + if (Objects.nonNull(dimming)) { + try { + double dimmingValue = Math.max(0f, Math.min(100f, dimming.getBrightness())); + return new PercentType(new BigDecimal(dimmingValue, PERCENT_MATH_CONTEXT)); + } catch (DTOPresentButEmptyException e) { + return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing + } + } + return UnDefType.NULL; + } + + public @Nullable Effects getEffects() { + return effects; + } + + public State getEffectState() { + Effects effects = this.effects; + return Objects.nonNull(effects) ? new StringType(effects.getStatus().name()) : UnDefType.NULL; + } + + public @Nullable Boolean getEnabled() { + return enabled; + } + + public State getEnabledState() { + Boolean enabled = this.enabled; + return Objects.nonNull(enabled) ? OnOffType.from(enabled.booleanValue()) : UnDefType.NULL; + } + + public @Nullable Gamut getGamut() { + ColorXy color = this.color; + return Objects.nonNull(color) ? color.getGamut() : null; + } + + public @Nullable ResourceReference getGroup() { + return group; + } + + public String getId() { + String id = this.id; + return Objects.nonNull(id) ? id : ""; + } + + public String getIdV1() { + String idV1 = this.idV1; + return Objects.nonNull(idV1) ? idV1 : ""; + } + + public @Nullable LightLevel getLightLevel() { + return light; + } + + public State getLightLevelState() { + LightLevel light = this.light; + return Objects.nonNull(light) ? light.getLightLevelState() : UnDefType.NULL; + } + + public @Nullable MetaData getMetaData() { + return metadata; + } + + public @Nullable Double getMinimumDimmingLevel() { + Dimming dimming = this.dimming; + return Objects.nonNull(dimming) ? dimming.getMinimumDimmingLevel() : null; + } + + public @Nullable MirekSchema getMirekSchema() { + ColorTemperature colorTemp = this.colorTemperature; + return Objects.nonNull(colorTemp) ? colorTemp.getMirekSchema() : null; + } + + public @Nullable Motion getMotion() { + return motion; + } + + public State getMotionState() { + Motion motion = this.motion; + return Objects.nonNull(motion) ? motion.getMotionState() : UnDefType.NULL; + } + + public State getMotionValidState() { + Motion motion = this.motion; + return Objects.nonNull(motion) ? motion.getMotionValidState() : UnDefType.NULL; + } + + public String getName() { + MetaData metaData = getMetaData(); + if (Objects.nonNull(metaData)) { + String name = metaData.getName(); + if (Objects.nonNull(name)) { + return name; + } + } + return getType().toString(); + } + + /** + * Return the state of the On/Off element (only). + */ + public State getOnOffState() { + try { + OnState on = this.on; + return Objects.nonNull(on) ? OnOffType.from(on.isOn()) : UnDefType.NULL; + } catch (DTOPresentButEmptyException e) { + return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing + } + } + + public @Nullable OnState getOnState() { + return on; + } + + public @Nullable ResourceReference getOwner() { + return owner; + } + + public @Nullable Power getPowerState() { + return powerState; + } + + public @Nullable ProductData getProductData() { + return productData; + } + + public String getProductName() { + ProductData productData = getProductData(); + if (Objects.nonNull(productData)) { + return productData.getProductName(); + } + return getType().toString(); + } + + public @Nullable Recall getRecall() { + return recall; + } + + public @Nullable RelativeRotary getRelativeRotary() { + return relativeRotary; + } + + public State getRelativeRotaryActionState() { + RelativeRotary relativeRotary = this.relativeRotary; + return Objects.nonNull(relativeRotary) ? relativeRotary.getActionState() : UnDefType.NULL; + } + + public State getRotaryStepsState() { + RelativeRotary relativeRotary = this.relativeRotary; + return Objects.nonNull(relativeRotary) ? relativeRotary.getStepsState() : UnDefType.NULL; + } + + /** + * Check if the scene resource contains a 'status.active' element. If such an element is present, returns a Boolean + * Optional whose value depends on the value of that element, or an empty Optional if it is not. + * + * @return true, false, or empty. + */ + public Optional getSceneActive() { + if (ResourceType.SCENE == getType()) { + JsonElement status = this.status; + if (Objects.nonNull(status) && status.isJsonObject()) { + JsonElement active = ((JsonObject) status).get("active"); + if (Objects.nonNull(active) && active.isJsonPrimitive()) { + return Optional.of(!"inactive".equalsIgnoreCase(active.getAsString())); + } + } + } + return Optional.empty(); + } + + /** + * If the getSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result is + * present and 'true' (i.e. the scene is active) return the scene name. Or finally (the optional result is present + * and 'false') return 'UnDefType.UNDEF'. + * + * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'. + */ + public State getSceneState() { + Optional active = getSceneActive(); + return active.isEmpty() ? UnDefType.NULL : active.get() ? new StringType(getName()) : UnDefType.UNDEF; + } + + public List getServiceReferences() { + List services = this.services; + return Objects.nonNull(services) ? services : List.of(); + } + + public JsonObject getStatus() { + JsonElement status = this.status; + if (Objects.nonNull(status) && status.isJsonObject()) { + return status.getAsJsonObject(); + } + return new JsonObject(); + } + + public @Nullable Temperature getTemperature() { + return temperature; + } + + public State getTemperatureState() { + Temperature temperature = this.temperature; + return Objects.nonNull(temperature) ? temperature.getTemperatureState() : UnDefType.NULL; + } + + public State getTemperatureValidState() { + Temperature temperature = this.temperature; + return Objects.nonNull(temperature) ? temperature.getTemperatureValidState() : UnDefType.NULL; + } + + public @Nullable Effects getTimedEffects() { + return timedEffects; + } + + public ResourceType getType() { + return ResourceType.of(type); + } + + public State getZigbeeState() { + ZigbeeStatus zigbeeStatus = getZigbeeStatus(); + return Objects.nonNull(zigbeeStatus) ? new StringType(zigbeeStatus.toString()) : UnDefType.NULL; + } + + public @Nullable ZigbeeStatus getZigbeeStatus() { + JsonElement status = this.status; + if (Objects.nonNull(status) && status.isJsonPrimitive()) { + return ZigbeeStatus.of(status.getAsString()); + } + return null; + } + + public boolean hasFullState() { + return !hasSparseData; + } + + /** + * Mark that the resource has sparse data. + * + * @return this instance. + */ + public Resource markAsSparse() { + hasSparseData = true; + return this; + } + + public Resource setAlerts(Alerts alert) { + this.alert = alert; + return this; + } + + public Resource setColorTemperature(ColorTemperature colorTemperature) { + this.colorTemperature = colorTemperature; + return this; + } + + public Resource setColorXy(ColorXy color) { + this.color = color; + return this; + } + + public Resource setDimming(Dimming dimming) { + this.dimming = dimming; + return this; + } + + public Resource setDynamicsDuration(Duration duration) { + dynamics = new Dynamics().setDuration(duration); + return this; + } + + public Resource setEffects(Effects effect) { + this.effects = effect; + return this; + } + + public Resource setEnabled(Command command) { + if (command instanceof OnOffType) { + this.enabled = ((OnOffType) command) == OnOffType.ON; + } + return this; + } + + public Resource setId(String id) { + this.id = id; + return this; + } + + public Resource setMetadata(MetaData metadata) { + this.metadata = metadata; + return this; + } + + public Resource setMirekSchema(@Nullable MirekSchema schema) { + ColorTemperature colorTemperature = this.colorTemperature; + if (Objects.nonNull(colorTemperature)) { + colorTemperature.setMirekSchema(schema); + } + return this; + } + + /** + * Set the on/off JSON element (only). + * + * @param command an OnOffTypee command value. + * @return this resource instance. + */ + public Resource setOnOff(Command command) { + if (command instanceof OnOffType) { + OnOffType onOff = (OnOffType) command; + OnState on = this.on; + on = Objects.nonNull(on) ? on : new OnState(); + on.setOn(OnOffType.ON.equals(onOff)); + this.on = on; + } + return this; + } + + public void setOnState(OnState on) { + this.on = on; + } + + public Resource setRecallAction(RecallAction recallAction) { + Recall recall = this.recall; + this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction); + return this; + } + + public Resource setRecallDuration(Duration recallDuration) { + Recall recall = this.recall; + this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setDuration(recallDuration); + return this; + } + + public Resource setType(ResourceType resourceType) { + this.type = resourceType.name().toLowerCase(); + return this; + } + + @Override + public String toString() { + String id = this.id; + return String.format("id:%s, type:%s", Objects.nonNull(id) ? id : "?" + " ".repeat(35), + getType().name().toLowerCase()); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ResourceReference.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ResourceReference.java new file mode 100644 index 000000000..bc44ea190 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/ResourceReference.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType; + +/** + * DTO that contains an API reference element. + * + * The V2 API is set up in such a way that all resources of the same type are grouped together under the + * /resource/ endpoint, but all those resources commonly reference each other, which is done in a + * standardized way by indicating the resource type (rtype) and resource id (rid). + * + * A typical usage is in a single physical device that hosts multiple services. An existing example is the Philips Hue + * Motion sensor which has a motion, light_level, and temperature service, but theoretically any combination can be + * supported such as an integrated device with two independently controllable light points and a motion sensor. + * + * This means that the information of the device itself can be found under the /device resource endpoint, but it then + * contains a services array which references for example the light and motion resources, for which the details can be + * found under the /light and /motion resource endpoints respectively. Other services the device might have, such as a + * Zigbee radio (zigbee_connectivy) or battery (device_power) are modeled in the same way. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class ResourceReference { + private @Nullable String rid; + private @NonNullByDefault({}) String rtype; + + @Override + public boolean equals(@Nullable Object obj) { + String rid = this.rid; + return (obj instanceof ResourceReference) && (rid != null) && rid.equals(((ResourceReference) obj).rid); + } + + public @Nullable String getId() { + return rid; + } + + public ResourceType getType() { + return ResourceType.of(rtype); + } + + public ResourceReference setId(String id) { + rid = id; + return this; + } + + public ResourceReference setType(ResourceType resourceType) { + rtype = resourceType.name().toLowerCase(); + return this; + } + + @Override + public String toString() { + String id = rid; + return String.format("id:%s, type:%s", id != null ? id : "*" + " ".repeat(35), getType().name().toLowerCase()); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resources.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resources.java new file mode 100644 index 000000000..a20618893 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resources.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * DTO for CLIP 2 to retrieve a list of generic resources from the bridge. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Resources { + private List errors = new ArrayList<>(); + private List data = new ArrayList<>(); + + public List getErrors() { + return errors.stream().map(Error::getDescription).collect(Collectors.toList()); + } + + public List getResources() { + return data; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Rotation.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Rotation.java new file mode 100644 index 000000000..e6e6f18d4 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Rotation.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.dto.clip2.enums.DirectionType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * DTO for rotation element of a tap dial switch. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Rotation { + private @Nullable String direction; + private @Nullable Integer duration; + private @Nullable Integer steps; + + public @Nullable DirectionType getDirection() { + String direction = this.direction; + return Objects.nonNull(direction) ? DirectionType.valueOf(direction.toUpperCase()) : null; + } + + public int getDuration() { + Integer duration = this.duration; + return Objects.nonNull(duration) ? duration.intValue() : 0; + } + + public int getSteps() { + Integer steps = this.steps; + return Objects.nonNull(steps) ? steps.intValue() : 0; + } + + /** + * Get the state corresponding to a relative rotary dial's last steps value. Clockwise rotations are positive, and + * counter clockwise rotations negative. + * + * @return the state or UNDEF. + */ + public State getStepsState() { + DirectionType direction = getDirection(); + Integer steps = this.steps; + if (Objects.nonNull(direction) && Objects.nonNull(steps)) { + return new DecimalType(DirectionType.CLOCK_WISE.equals(direction) ? steps.intValue() : -steps.intValue()); + } + return UnDefType.NULL; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/RotationEvent.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/RotationEvent.java new file mode 100644 index 000000000..1712c7987 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/RotationEvent.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.dto.clip2.enums.RotationEventType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * DTO for rotation event of a dial switch. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class RotationEvent { + private @Nullable String action; + private @Nullable Rotation rotation; + + public @Nullable RotationEventType getAction() { + String action = this.action; + return Objects.nonNull(action) ? RotationEventType.valueOf(action.toUpperCase()) : null; + } + + public State getActionState() { + RotationEventType action = getAction(); + return Objects.nonNull(action) ? new StringType(action.name()) : UnDefType.NULL; + } + + public @Nullable Rotation getRotation() { + return rotation; + } + + public State getStepsState() { + Rotation rotation = this.rotation; + return Objects.nonNull(rotation) ? rotation.getStepsState() : UnDefType.NULL; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Temperature.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Temperature.java new file mode 100644 index 000000000..8f25c2b08 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Temperature.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +import com.google.gson.annotations.SerializedName; + +/** + * DTO for CLIP 2 temperature sensor. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Temperature { + private float temperature; + private @SerializedName("temperature_valid") boolean temperatureValid; + + public float getTemperature() { + return temperature; + } + + public boolean isTemperatureValid() { + return temperatureValid; + } + + public State getTemperatureState() { + return temperatureValid ? new QuantityType<>(temperature, SIUnits.CELSIUS) : UnDefType.UNDEF; + } + + public State getTemperatureValidState() { + return OnOffType.from(temperatureValid); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/TimedEffects.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/TimedEffects.java new file mode 100644 index 000000000..2591d9f1e --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/TimedEffects.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.time.Duration; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * DTO for 'timed_effects' of a light. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class TimedEffects extends Effects { + private @Nullable Long duration; + + public @Nullable Duration getDuration() { + Long duration = this.duration; + return Objects.nonNull(duration) ? Duration.ofMillis(duration) : Duration.ZERO; + } + + public TimedEffects setDuration(Duration duration) { + this.duration = duration.toMillis(); + return this; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ActionType.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ActionType.java new file mode 100644 index 000000000..93484e6d2 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ActionType.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Enum for 'alert' actions. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public enum ActionType { + BREATHE, + NO_ACTION; + + public static ActionType of(@Nullable String value) { + if (value != null) { + try { + return valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + // fall through + } + } + return NO_ACTION; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/Archetype.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/Archetype.java new file mode 100644 index 000000000..30c0b1c6e --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/Archetype.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Enum for product archetypes. + * + * @see API v2 + * documentation + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public enum Archetype { + // device archetypes + BRIDGE_V2, + UNKNOWN_ARCHETYPE, + CLASSIC_BULB, + SULTAN_BULB, + FLOOD_BULB, + SPOT_BULB, + CANDLE_BULB, + LUSTER_BULB, + PENDANT_ROUND, + PENDANT_LONG, + CEILING_ROUND, + CEILING_SQUARE, + FLOOR_SHADE, + FLOOR_LANTERN, + TABLE_SHADE, + RECESSED_CEILING, + RECESSED_FLOOR, + SINGLE_SPOT, + DOUBLE_SPOT, + TABLE_WASH, + WALL_LANTERN, + WALL_SHADE, + FLEXIBLE_LAMP, + GROUND_SPOT, + WALL_SPOT, + PLUG, + HUE_GO, + HUE_LIGHTSTRIP, + HUE_IRIS, + HUE_BLOOM, + BOLLARD, + WALL_WASHER, + HUE_PLAY, + VINTAGE_BULB, + CHRISTMAS_TREE, + HUE_CENTRIS, + HUE_LIGHTSTRIP_TV, + HUE_TUBE, + HUE_SIGNE, + STRING_LIGHT, + // room archetypes + LIVING_ROOM, + KITCHEN, + DINING, + BEDROOM, + KIDS_BEDROOM, + BATHROOM, + NURSERY, + RECREATION, + OFFICE, + GYM, + HALLWAY, + TOILET, + FRONT_DOOR, + GARAGE, + TERRACE, + GARDEN, + DRIVEWAY, + CARPORT, + 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, + OTHER; + + public static Archetype of(@Nullable String value) { + if (value != null) { + try { + return valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + // fall through + } + } + return UNKNOWN_ARCHETYPE; + } + + @Override + public String toString() { + String s = this.name().replace("_", " "); + return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/BatteryStateType.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/BatteryStateType.java new file mode 100644 index 000000000..4241ac7c6 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/BatteryStateType.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Enum for types battery state. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public enum BatteryStateType { + NORMAL, + LOW, + CRITICAL; +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ButtonEventType.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ButtonEventType.java new file mode 100644 index 000000000..df82e70b5 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ButtonEventType.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Enum for types button press. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public enum ButtonEventType { + INITIAL_PRESS, + REPEAT, + SHORT_RELEASE, + LONG_RELEASE, + DOUBLE_SHORT_RELEASE; +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/DirectionType.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/DirectionType.java new file mode 100644 index 000000000..8df48e502 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/DirectionType.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Enum for tap dial rotation directions. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public enum DirectionType { + CLOCK_WISE, + COUNTER_CLOCK_WISE; +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/EffectType.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/EffectType.java new file mode 100644 index 000000000..8c6d429a0 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/EffectType.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2.enums; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Enum for 'effect' types. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public enum EffectType { + // fixed Effects + SPARKLE, + FIRE, + CANDLE, + // timed Effects + SUNRISE, + // applies to both + NO_EFFECT; + + private static final Set FIXED = Set.of(SPARKLE, FIRE, CANDLE); + private static final Set TIMED = Set.of(SUNRISE); + + public static EffectType of(@Nullable String value) { + if (value != null) { + try { + return valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + // fall through + } + } + return NO_EFFECT; + } + + public boolean isFixed() { + return FIXED.contains(this); + } + + public boolean isTimed() { + return TIMED.contains(this); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/RecallAction.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/RecallAction.java new file mode 100644 index 000000000..70fdf4b3b --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/RecallAction.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Enum for scene recall actions. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public enum RecallAction { + ACTIVE, + DYNAMIC_PALETTE, + STATIC; + + public static RecallAction of(@Nullable String value) { + if (value != null) { + try { + return valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + // fall through + } + } + return ACTIVE; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ResourceType.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ResourceType.java new file mode 100644 index 000000000..e66b2afe2 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ResourceType.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2.enums; + +import java.util.EnumSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Enum for resource types. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public enum ResourceType { + AUTH_V1, + BEHAVIOR_INSTANCE, + BEHAVIOR_SCRIPT, + BRIDGE, + BRIDGE_HOME, + BUTTON, + DEVICE, + DEVICE_POWER, + ENTERTAINMENT, + ENTERTAINMENT_CONFIGURATION, + GEOFENCE, + GEOFENCE_CLIENT, + GEOLOCATION, + GROUPED_LIGHT, + HOMEKIT, + LIGHT, + LIGHT_LEVEL, + MOTION, + PUBLIC_IMAGE, + ROOM, + RELATIVE_ROTARY, + SCENE, + TEMPERATURE, + ZGP_CONNECTIVITY, + ZIGBEE_CONNECTIVITY, + ZONE, + UPDATE, + ADD, + DELETE, + ERROR; + + public static final Set SSE_TYPES = EnumSet.of(UPDATE, ADD, DELETE, ERROR); + + public static ResourceType of(@Nullable String value) { + if (value != null) { + try { + return valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + // fall through + } + } + return ERROR; + } + + @Override + public String toString() { + String s = this.name().replace("_", " "); + return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/RotationEventType.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/RotationEventType.java new file mode 100644 index 000000000..13eecc751 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/RotationEventType.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Enum for types of rotary dial events. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public enum RotationEventType { + START, + REPEAT; +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ZigbeeStatus.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ZigbeeStatus.java new file mode 100644 index 000000000..f9c33d1fe --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/enums/ZigbeeStatus.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Enum for possible Zigbee states. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public enum ZigbeeStatus { + CONNECTED, + DISCONNECTED, + CONNECTIVITY_ISSUE, + UNIDIRECTIONAL_INCOMING; + + public static ZigbeeStatus of(@Nullable String value) { + if (value != null) { + try { + return valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + // fall through + } + } + return DISCONNECTED; + } + + @Override + public String toString() { + String s = this.name().replace("_", " "); + return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/helper/Setters.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/helper/Setters.java new file mode 100644 index 000000000..8fef7cff8 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/helper/Setters.java @@ -0,0 +1,312 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2.helper; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Objects; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.dto.clip2.Alerts; +import org.openhab.binding.hue.internal.dto.clip2.ColorTemperature; +import org.openhab.binding.hue.internal.dto.clip2.ColorXy; +import org.openhab.binding.hue.internal.dto.clip2.Dimming; +import org.openhab.binding.hue.internal.dto.clip2.Effects; +import org.openhab.binding.hue.internal.dto.clip2.MetaData; +import org.openhab.binding.hue.internal.dto.clip2.MirekSchema; +import org.openhab.binding.hue.internal.dto.clip2.OnState; +import org.openhab.binding.hue.internal.dto.clip2.Resource; +import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType; +import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.types.Command; +import org.openhab.core.util.ColorUtil; +import org.openhab.core.util.ColorUtil.Gamut; + +/** + * Advanced setter methods for fields in the Resource class for special cases where setting the new value in the target + * resource depends on logic using the values of other fields in a another source Resource. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Setters { + + /** + * Setter for Alert field: + * Use the given command value to set the target resource DTO value based on the attributes of the source resource + * (if any). + * + * @param target the target resource. + * @param command the new state command should be a StringType. + * @param source another resource containing the allowed alert action values. + * + * @return the target resource. + */ + public static Resource setAlert(Resource target, Command command, @Nullable Resource source) { + if ((command instanceof StringType) && Objects.nonNull(source)) { + Alerts otherAlert = source.getAlerts(); + if (Objects.nonNull(otherAlert)) { + ActionType actionType = ActionType.of(((StringType) command).toString()); + if (otherAlert.getActionValues().contains(actionType)) { + target.setAlerts(new Alerts().setAction(actionType)); + } + } + } + return target; + } + + /** + * Setter for Color Temperature field: + * Use the given command value to set the target resource DTO value based on the attributes of the source resource + * (if any). + * + * @param target the target resource. + * @param command the new state command should be a QuantityType (but it can also handle DecimalType). + * @param source another resource containing the MirekSchema. + * + * @return the target resource. + */ + public static Resource setColorTemperatureAbsolute(Resource target, Command command, @Nullable Resource source) { + QuantityType mirek; + if (command instanceof QuantityType) { + QuantityType quantity = (QuantityType) command; + Unit unit = quantity.getUnit(); + if (Units.KELVIN.equals(unit)) { + mirek = quantity.toInvertibleUnit(Units.MIRED); + } else if (Units.MIRED.equals(unit)) { + mirek = quantity; + } else { + QuantityType kelvin = quantity.toInvertibleUnit(Units.KELVIN); + mirek = Objects.nonNull(kelvin) ? kelvin.toInvertibleUnit(Units.MIRED) : null; + } + } else if (command instanceof DecimalType) { + mirek = QuantityType.valueOf(((DecimalType) command).doubleValue(), Units.KELVIN) + .toInvertibleUnit(Units.MIRED); + } else { + mirek = null; + } + if (Objects.nonNull(mirek)) { + MirekSchema schema = target.getMirekSchema(); + schema = Objects.nonNull(schema) ? schema : Objects.nonNull(source) ? source.getMirekSchema() : null; + schema = Objects.nonNull(schema) ? schema : MirekSchema.DEFAULT_SCHEMA; + ColorTemperature colorTemperature = target.getColorTemperature(); + colorTemperature = Objects.nonNull(colorTemperature) ? colorTemperature : new ColorTemperature(); + double min = schema.getMirekMinimum(); + double max = schema.getMirekMaximum(); + double val = Math.max(min, Math.min(max, mirek.doubleValue())); + target.setColorTemperature(colorTemperature.setMirek(val)); + } + return target; + } + + /** + * Setter for Color Temperature field: + * Use the given command value to set the target resource DTO value based on the attributes of the source resource + * (if any). + * + * @param target the target resource. + * @param command the new state command should be a PercentType. + * @param source another resource containing the MirekSchema. + * + * @return the target resource. + */ + public static Resource setColorTemperaturePercent(Resource target, Command command, @Nullable Resource source) { + if (command instanceof PercentType) { + MirekSchema schema = target.getMirekSchema(); + schema = Objects.nonNull(schema) ? schema : Objects.nonNull(source) ? source.getMirekSchema() : null; + schema = Objects.nonNull(schema) ? schema : MirekSchema.DEFAULT_SCHEMA; + ColorTemperature colorTemperature = target.getColorTemperature(); + colorTemperature = Objects.nonNull(colorTemperature) ? colorTemperature : new ColorTemperature(); + double min = schema.getMirekMinimum(); + double max = schema.getMirekMaximum(); + double val = min + ((max - min) * ((PercentType) command).doubleValue() / 100f); + target.setColorTemperature(colorTemperature.setMirek(val)); + } + return target; + } + + /** + * Setter for Color Xy field: + * Use the given command value to set the target resource DTO value based on the attributes of the source resource + * (if any). Use the HS parts of the HSB value to set the value of the 'ColorXy' JSON element, and ignore the 'B' + * part. + * + * @param target the target resource. + * @param command the new state command should be an HSBType with the new color XY value. + * @param source another resource containing the color Gamut. + * + * @return the target resource. + */ + public static Resource setColorXy(Resource target, Command command, @Nullable Resource source) { + if (command instanceof HSBType) { + Gamut gamut = target.getGamut(); + gamut = Objects.nonNull(gamut) ? gamut : Objects.nonNull(source) ? source.getGamut() : null; + gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT; + HSBType hsb = (HSBType) command; + ColorXy color = target.getColorXy(); + target.setColorXy((Objects.nonNull(color) ? color : new ColorXy()).setXY(ColorUtil.hsbToXY(hsb, gamut))); + } + return target; + } + + /** + * Setter for Dimming field: + * Use the given command value to set the target resource DTO value based on the attributes of the source resource + * (if any). + * + * @param target the target resource. + * @param command the new state command should be a PercentType with the new dimming parameter. + * @param source another resource containing the minimum dimming level. + * + * @return the target resource. + */ + public static Resource setDimming(Resource target, Command command, @Nullable Resource source) { + if (command instanceof PercentType) { + Double min = target.getMinimumDimmingLevel(); + min = Objects.nonNull(min) ? min : Objects.nonNull(source) ? source.getMinimumDimmingLevel() : null; + min = Objects.nonNull(min) ? min : Dimming.DEFAULT_MINIMUM_DIMMIMG_LEVEL; + PercentType brightness = (PercentType) command; + if (brightness.doubleValue() < min.doubleValue()) { + brightness = new PercentType(new BigDecimal(min, Resource.PERCENT_MATH_CONTEXT)); + } + Dimming dimming = target.getDimming(); + dimming = Objects.nonNull(dimming) ? dimming : new Dimming(); + dimming.setBrightness(brightness.doubleValue()); + target.setDimming(dimming); + } + return target; + } + + /** + * Setter for Effect field: + * Use the given command value to set the target resource DTO value based on the attributes of the source resource + * (if any). + * + * @param target the target resource. + * @param command the new state command should be a StringType. + * @param source another resource containing the allowed effect action values. + * + * @return the target resource. + */ + public static Resource setEffect(Resource target, Command command, @Nullable Resource source) { + if ((command instanceof StringType) && Objects.nonNull(source)) { + Effects otherEffects = source.getEffects(); + if (Objects.nonNull(otherEffects)) { + EffectType effectType = EffectType.of(((StringType) command).toString()); + if (otherEffects.allows(effectType)) { + target.setEffects(new Effects().setEffect(effectType)); + } + } + } + return target; + } + + /** + * Setter to copy persisted fields from the source Resource into the target Resource. If the field in the target is + * null and the same field in the source is not null, then the value from the source is copied to the target. This + * method allows 'hasSparseData' resources to expand themselves to include necessary fields taken over from a + * previously cached full data resource. + * + * @param target the target resource. + * @param source another resource containing the values to be taken over. + * + * @return the target resource. + */ + public static Resource setResource(Resource target, Resource source) { + // on + OnState targetOnOff = target.getOnState(); + OnState sourceOnOff = source.getOnState(); + if (Objects.isNull(targetOnOff) && Objects.nonNull(sourceOnOff)) { + target.setOnState(sourceOnOff); + } + // dimming + Dimming targetDimming = target.getDimming(); + Dimming sourceDimming = source.getDimming(); + if (Objects.isNull(targetDimming) && Objects.nonNull(sourceDimming)) { + target.setDimming(sourceDimming); + targetDimming = target.getDimming(); + } + // minimum dimming level + Double targetMinDimmingLevel = Objects.nonNull(targetDimming) ? targetDimming.getMinimumDimmingLevel() : null; + Double sourceMinDimmingLevel = Objects.nonNull(sourceDimming) ? sourceDimming.getMinimumDimmingLevel() : null; + if (Objects.isNull(targetMinDimmingLevel) && Objects.nonNull(sourceMinDimmingLevel)) { + targetDimming = Objects.nonNull(targetDimming) ? targetDimming : new Dimming(); + targetDimming.setMinimumDimmingLevel(sourceMinDimmingLevel); + } + // color + ColorXy targetColor = target.getColorXy(); + ColorXy sourceColor = source.getColorXy(); + if (Objects.isNull(targetColor) && Objects.nonNull(sourceColor)) { + target.setColorXy(sourceColor); + targetColor = target.getColorXy(); + } + // color gamut + Gamut targetGamut = Objects.nonNull(targetColor) ? targetColor.getGamut() : null; + Gamut sourceGamut = Objects.nonNull(sourceColor) ? sourceColor.getGamut() : null; + if (Objects.isNull(targetGamut) && Objects.nonNull(sourceGamut)) { + targetColor = Objects.nonNull(targetColor) ? targetColor : new ColorXy(); + targetColor.setGamut(sourceGamut); + } + // color temperature + ColorTemperature targetColorTemp = target.getColorTemperature(); + ColorTemperature sourceColorTemp = source.getColorTemperature(); + if (Objects.isNull(targetColorTemp) && Objects.nonNull(sourceColorTemp)) { + target.setColorTemperature(sourceColorTemp); + targetColorTemp = target.getColorTemperature(); + } + // mirek schema + MirekSchema targetMirekSchema = Objects.nonNull(targetColorTemp) ? targetColorTemp.getMirekSchema() : null; + MirekSchema sourceMirekSchema = Objects.nonNull(sourceColorTemp) ? sourceColorTemp.getMirekSchema() : null; + if (Objects.isNull(targetMirekSchema) && Objects.nonNull(sourceMirekSchema)) { + targetColorTemp = Objects.nonNull(targetColorTemp) ? targetColorTemp : new ColorTemperature(); + targetColorTemp.setMirekSchema(sourceMirekSchema); + } + // metadata + MetaData targetMetaData = target.getMetaData(); + MetaData sourceMetaData = source.getMetaData(); + if (Objects.isNull(targetMetaData) && Objects.nonNull(sourceMetaData)) { + target.setMetadata(sourceMetaData); + } + // alerts + Alerts targetAlerts = target.getAlerts(); + Alerts sourceAlerts = source.getAlerts(); + if (Objects.isNull(targetAlerts) && Objects.nonNull(sourceAlerts)) { + target.setAlerts(sourceAlerts); + } + // effects + Effects targetEffects = target.getEffects(); + Effects sourceEffects = source.getEffects(); + if (Objects.isNull(targetEffects) && Objects.nonNull(sourceEffects)) { + targetEffects = sourceEffects; + target.setEffects(sourceEffects); + targetEffects = target.getEffects(); + } + // effects values + List targetStatusValues = Objects.nonNull(targetEffects) ? targetEffects.getStatusValues() : null; + List sourceStatusValues = Objects.nonNull(sourceEffects) ? sourceEffects.getStatusValues() : null; + if (Objects.isNull(targetStatusValues) && Objects.nonNull(sourceStatusValues)) { + targetEffects = Objects.nonNull(targetEffects) ? targetEffects : new Effects(); + targetEffects.setStatusValues(sourceStatusValues); + } + return target; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/ApiException.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/ApiException.java index 7e3aa0454..cbb3f7fd3 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/ApiException.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/ApiException.java @@ -13,6 +13,7 @@ package org.openhab.binding.hue.internal.exceptions; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; /** * Thrown when the API returns an unknown error. @@ -29,4 +30,8 @@ public class ApiException extends Exception { public ApiException(String message) { super(message); } + + public ApiException(String message, @Nullable Throwable e) { + super(message, e); + } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/AssetNotLoadedException.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/AssetNotLoadedException.java new file mode 100644 index 000000000..e5ddbb7f3 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/AssetNotLoadedException.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Thrown when one of the connection or handler classes has not loaded all its assets. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class AssetNotLoadedException extends Exception { + private static final long serialVersionUID = -1; + + public AssetNotLoadedException() { + } + + public AssetNotLoadedException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/DTOPresentButEmptyException.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/DTOPresentButEmptyException.java new file mode 100644 index 000000000..a758fcdfc --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/DTOPresentButEmptyException.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Thrown when a DTO is present but empty. In some circumstances the API v2 returns an empty DTO ("dtoName":{}) rather + * than null ("dtoName":null). This indicates that the DTO is in principle supported by the containing resource, but + * currently the DTO contains no actual state fields. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class DTOPresentButEmptyException extends Exception { + private static final long serialVersionUID = -1; + + public DTOPresentButEmptyException() { + } + + public DTOPresentButEmptyException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/HttpUnauthorizedException.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/HttpUnauthorizedException.java new file mode 100644 index 000000000..4839eb78d --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/exceptions/HttpUnauthorizedException.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Thrown when an HTTP call to the CLIP 2 bridge returns with an 'unauthorized' status code. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class HttpUnauthorizedException extends ApiException { + private static final long serialVersionUID = -1; + + public HttpUnauthorizedException() { + } + + public HttpUnauthorizedException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/factory/HueThingHandlerFactory.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/factory/HueThingHandlerFactory.java index 28a0424e2..64f064840 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/factory/HueThingHandlerFactory.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/factory/HueThingHandlerFactory.java @@ -20,7 +20,10 @@ import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.hue.internal.HueBindingConstants; +import org.openhab.binding.hue.internal.handler.Clip2BridgeHandler; +import org.openhab.binding.hue.internal.handler.Clip2StateDescriptionProvider; +import org.openhab.binding.hue.internal.handler.Clip2ThingHandler; import org.openhab.binding.hue.internal.handler.HueBridgeHandler; import org.openhab.binding.hue.internal.handler.HueGroupHandler; import org.openhab.binding.hue.internal.handler.HueLightHandler; @@ -38,17 +41,19 @@ import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; /** - * {@link HueThingHandlerFactory} is a factory for {@link HueBridgeHandler}s. + * The factory for all varieties of Hue thing handlers. * * @author Dennis Nobel - Initial contribution of hue binding * @author Kai Kreuzer - added supportsThingType method @@ -56,13 +61,15 @@ import org.osgi.service.component.annotations.Reference; * @author Samuel Leisering - Added support for sensor API * @author Christoph Weitkamp - Added support for sensor API * @author Laurent Garnier - Added support for groups + * @author Andrew Fiddian-Green - Added support for CLIP 2 things */ @NonNullByDefault @Component(service = ThingHandlerFactory.class, configurationPid = "binding.hue") public class HueThingHandlerFactory extends BaseThingHandlerFactory { public static final Set SUPPORTED_THING_TYPES = Stream - .of(HueBridgeHandler.SUPPORTED_THING_TYPES.stream(), HueLightHandler.SUPPORTED_THING_TYPES.stream(), + .of(Clip2BridgeHandler.SUPPORTED_THING_TYPES.stream(), Clip2ThingHandler.SUPPORTED_THING_TYPES.stream(), + HueBridgeHandler.SUPPORTED_THING_TYPES.stream(), HueLightHandler.SUPPORTED_THING_TYPES.stream(), DimmerSwitchHandler.SUPPORTED_THING_TYPES.stream(), TapSwitchHandler.SUPPORTED_THING_TYPES.stream(), PresenceHandler.SUPPORTED_THING_TYPES.stream(), GeofencePresenceHandler.SUPPORTED_THING_TYPES.stream(), @@ -70,25 +77,39 @@ public class HueThingHandlerFactory extends BaseThingHandlerFactory { ClipHandler.SUPPORTED_THING_TYPES.stream(), HueGroupHandler.SUPPORTED_THING_TYPES.stream()) .flatMap(i -> i).collect(Collectors.toUnmodifiableSet()); - private final HttpClient httpClient; + private final HttpClientFactory httpClientFactory; private final HueStateDescriptionProvider stateDescriptionProvider; + private final Clip2StateDescriptionProvider clip2StateDescriptionProvider; private final TranslationProvider i18nProvider; private final LocaleProvider localeProvider; + private final ThingRegistry thingRegistry; + private final ItemChannelLinkRegistry itemChannelLinkRegistry; @Activate public HueThingHandlerFactory(final @Reference HttpClientFactory httpClientFactory, final @Reference HueStateDescriptionProvider stateDescriptionProvider, - final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider) { - this.httpClient = httpClientFactory.getCommonHttpClient(); + final @Reference Clip2StateDescriptionProvider clip2StateDescriptionProvider, + final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider, + final @Reference ThingRegistry thingRegistry, + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry) { + this.httpClientFactory = httpClientFactory; this.stateDescriptionProvider = stateDescriptionProvider; + this.clip2StateDescriptionProvider = clip2StateDescriptionProvider; this.i18nProvider = i18nProvider; this.localeProvider = localeProvider; + this.thingRegistry = thingRegistry; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; } @Override public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration, @Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) { - if (HueBridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { + if (HueBindingConstants.THING_TYPE_BRIDGE_API2.equals(thingTypeUID)) { + return super.createThing(thingTypeUID, configuration, thingUID, null); + } else if (Clip2ThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { + ThingUID clip2ThingUID = getClip2ThingUID(thingTypeUID, thingUID, configuration, bridgeUID); + return super.createThing(thingTypeUID, configuration, clip2ThingUID, bridgeUID); + } else if (HueBridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return super.createThing(thingTypeUID, configuration, thingUID, null); } else if (HueLightHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { ThingUID hueLightUID = getLightUID(thingTypeUID, thingUID, configuration, bridgeUID); @@ -115,6 +136,12 @@ public class HueThingHandlerFactory extends BaseThingHandlerFactory { return SUPPORTED_THING_TYPES.contains(thingTypeUID); } + private ThingUID getClip2ThingUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID, + Configuration configuration, @Nullable ThingUID bridgeUID) { + return thingUID != null ? thingUID + : getThingUID(thingTypeUID, configuration.get(PROPERTY_RESOURCE_ID).toString(), bridgeUID); + } + private ThingUID getLightUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID, Configuration configuration, @Nullable ThingUID bridgeUID) { if (thingUID != null) { @@ -152,26 +179,32 @@ public class HueThingHandlerFactory extends BaseThingHandlerFactory { @Override protected @Nullable ThingHandler createHandler(Thing thing) { - if (HueBridgeHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { - return new HueBridgeHandler((Bridge) thing, httpClient, stateDescriptionProvider, i18nProvider, - localeProvider); - } else if (HueLightHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (HueBindingConstants.THING_TYPE_BRIDGE_API2.equals(thingTypeUID)) { + return new Clip2BridgeHandler((Bridge) thing, httpClientFactory, thingRegistry, localeProvider, + i18nProvider); + } else if (Clip2ThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { + return new Clip2ThingHandler(thing, clip2StateDescriptionProvider, thingRegistry, itemChannelLinkRegistry); + } else if (HueBridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { + return new HueBridgeHandler((Bridge) thing, httpClientFactory.getCommonHttpClient(), + stateDescriptionProvider, i18nProvider, localeProvider); + } else if (HueLightHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return new HueLightHandler(thing, stateDescriptionProvider); - } else if (DimmerSwitchHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + } else if (DimmerSwitchHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return new DimmerSwitchHandler(thing); - } else if (TapSwitchHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + } else if (TapSwitchHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return new TapSwitchHandler(thing); - } else if (PresenceHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + } else if (PresenceHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return new PresenceHandler(thing); - } else if (GeofencePresenceHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + } else if (GeofencePresenceHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return new GeofencePresenceHandler(thing); - } else if (TemperatureHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + } else if (TemperatureHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return new TemperatureHandler(thing); - } else if (LightLevelHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + } else if (LightLevelHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return new LightLevelHandler(thing); - } else if (ClipHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + } else if (ClipHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return new ClipHandler(thing); - } else if (HueGroupHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { + } else if (HueGroupHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return new HueGroupHandler(thing, stateDescriptionProvider); } else { return null; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java new file mode 100644 index 000000000..31b4a4f69 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -0,0 +1,776 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.handler; + +import static org.openhab.binding.hue.internal.HueBindingConstants.*; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.config.Clip2BridgeConfig; +import org.openhab.binding.hue.internal.connection.Clip2Bridge; +import org.openhab.binding.hue.internal.connection.HueTlsTrustManagerProvider; +import org.openhab.binding.hue.internal.discovery.Clip2ThingDiscoveryService; +import org.openhab.binding.hue.internal.dto.clip2.MetaData; +import org.openhab.binding.hue.internal.dto.clip2.ProductData; +import org.openhab.binding.hue.internal.dto.clip2.Resource; +import org.openhab.binding.hue.internal.dto.clip2.ResourceReference; +import org.openhab.binding.hue.internal.dto.clip2.Resources; +import org.openhab.binding.hue.internal.dto.clip2.enums.Archetype; +import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType; +import org.openhab.binding.hue.internal.exceptions.ApiException; +import org.openhab.binding.hue.internal.exceptions.AssetNotLoadedException; +import org.openhab.binding.hue.internal.exceptions.HttpUnauthorizedException; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.io.net.http.TlsTrustManagerProvider; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.thing.binding.builder.BridgeBuilder; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Bridge handler for a CLIP 2 bridge. It communicates with the bridge via CLIP 2 end points, and reads and writes API + * V2 resource objects. It also subscribes to the server's SSE event stream, and receives SSE events from it. + * + * @author Andrew Fiddian-Green - Initial contribution. + */ +@NonNullByDefault +public class Clip2BridgeHandler extends BaseBridgeHandler { + + public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE_API2); + + private static final int FAST_SCHEDULE_MILLI_SECONDS = 500; + private static final int APPLICATION_KEY_MAX_TRIES = 600; // i.e. 300 seconds, 5 minutes + private static final int RECONNECT_DELAY_SECONDS = 10; + private static final int RECONNECT_MAX_TRIES = 5; + + private static final ResourceReference DEVICE = new ResourceReference().setType(ResourceType.DEVICE); + private static final ResourceReference ROOM = new ResourceReference().setType(ResourceType.ROOM); + private static final ResourceReference ZONE = new ResourceReference().setType(ResourceType.ZONE); + private static final ResourceReference BRIDGE = new ResourceReference().setType(ResourceType.BRIDGE); + private static final ResourceReference BRIDGE_HOME = new ResourceReference().setType(ResourceType.BRIDGE_HOME); + private static final ResourceReference SCENE = new ResourceReference().setType(ResourceType.SCENE); + + /** + * List of resource references that need to be mass down loaded. + * NOTE: the SCENE resources must be mass down loaded first! + */ + private static final List MASS_DOWNLOAD_RESOURCE_REFERENCES = List.of(SCENE, DEVICE, ROOM, ZONE); + + private final Logger logger = LoggerFactory.getLogger(Clip2BridgeHandler.class); + + private final HttpClientFactory httpClientFactory; + private final ThingRegistry thingRegistry; + private final Bundle bundle; + private final LocaleProvider localeProvider; + private final TranslationProvider translationProvider; + + private @Nullable Clip2Bridge clip2Bridge; + private @Nullable ServiceRegistration trustManagerRegistration; + private @Nullable Clip2ThingDiscoveryService discoveryService; + + private @Nullable Future checkConnectionTask; + private @Nullable Future updateOnlineStateTask; + private @Nullable ScheduledFuture scheduledUpdateTask; + private Map> resourcesEventTasks = new ConcurrentHashMap<>(); + + private boolean assetsLoaded; + private int applKeyRetriesRemaining; + private int connectRetriesRemaining; + + public Clip2BridgeHandler(Bridge bridge, HttpClientFactory httpClientFactory, ThingRegistry thingRegistry, + LocaleProvider localeProvider, TranslationProvider translationProvider) { + super(bridge); + this.httpClientFactory = httpClientFactory; + this.thingRegistry = thingRegistry; + this.bundle = FrameworkUtil.getBundle(getClass()); + this.localeProvider = localeProvider; + this.translationProvider = translationProvider; + } + + /** + * Cancel the given task. + * + * @param cancelTask the task to be cancelled (may be null) + * @param mayInterrupt allows cancel() to interrupt the thread. + */ + private void cancelTask(@Nullable Future cancelTask, boolean mayInterrupt) { + if (Objects.nonNull(cancelTask)) { + cancelTask.cancel(mayInterrupt); + } + } + + /** + * Check if assets are loaded. + * + * @throws AssetNotLoadedException if assets not loaded. + */ + private void checkAssetsLoaded() throws AssetNotLoadedException { + if (!assetsLoaded) { + throw new AssetNotLoadedException("Assets not loaded"); + } + } + + /** + * Try to connect and set the online status accordingly. If the connection attempt throws an + * HttpUnAuthorizedException then try to register the existing application key, or create a new one, with the hub. + * If the connection attempt throws an ApiException then set the thing status to offline. This method is called on a + * scheduler thread, which reschedules itself repeatedly until the thing is shutdown. + */ + private synchronized void checkConnection() { + logger.debug("checkConnection()"); + + // check connection to the hub + ThingStatusDetail thingStatus; + try { + checkAssetsLoaded(); + getClip2Bridge().testConnectionState(); + thingStatus = ThingStatusDetail.NONE; + } catch (HttpUnauthorizedException e) { + logger.debug("checkConnection() {}", e.getMessage(), e); + thingStatus = ThingStatusDetail.CONFIGURATION_ERROR; + } catch (ApiException e) { + logger.debug("checkConnection() {}", e.getMessage(), e); + thingStatus = ThingStatusDetail.COMMUNICATION_ERROR; + } catch (AssetNotLoadedException e) { + logger.debug("checkConnection() {}", e.getMessage(), e); + thingStatus = ThingStatusDetail.HANDLER_INITIALIZING_ERROR; + } catch (InterruptedException e) { + return; + } + + // update the thing status + boolean retryApplicationKey = false; + boolean retryConnection = false; + switch (thingStatus) { + case CONFIGURATION_ERROR: + if (applKeyRetriesRemaining > 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.api2.conf-error.press-pairing-button"); + try { + registerApplicationKey(); + retryApplicationKey = true; + } catch (HttpUnauthorizedException e) { + retryApplicationKey = true; + } catch (ApiException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication-error"); + } catch (IllegalStateException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.api2.conf-error.read-only"); + } catch (AssetNotLoadedException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.api2.conf-error.assets-not-loaded"); + } catch (InterruptedException e) { + return; + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.api2.conf-error.not-authorized"); + } + break; + + case COMMUNICATION_ERROR: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication-error"); + retryConnection = connectRetriesRemaining > 0; + break; + + case HANDLER_INITIALIZING_ERROR: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.api2.conf-error.assets-not-loaded"); + break; + + case NONE: + default: + updateSelf(); // go online + break; + } + + int milliSeconds; + if (retryApplicationKey) { + // short delay used during attempts to create or validate an application key + milliSeconds = FAST_SCHEDULE_MILLI_SECONDS; + applKeyRetriesRemaining--; + } else { + // default delay, set via configuration parameter, used as heart-beat 'just-in-case' + Clip2BridgeConfig config = getConfigAs(Clip2BridgeConfig.class); + milliSeconds = config.checkMinutes * 60000; + if (retryConnection) { + // exponential back off delay used during attempts to reconnect + int backOffDelay = 60000 * (int) Math.pow(2, RECONNECT_MAX_TRIES - connectRetriesRemaining); + milliSeconds = Math.min(milliSeconds, backOffDelay); + connectRetriesRemaining--; + } + } + + // this method schedules itself to be called again in a loop.. + cancelTask(checkConnectionTask, false); + checkConnectionTask = scheduler.schedule(() -> checkConnection(), milliSeconds, TimeUnit.MILLISECONDS); + } + + /** + * If a child thing has been added, and the bridge is online, update the child's data. + */ + public void childInitialized() { + if (thing.getStatus() == ThingStatus.ONLINE) { + updateThingsScheduled(5000); + } + } + + @Override + public void dispose() { + if (assetsLoaded) { + disposeAssets(); + } + } + + /** + * Dispose the bridge handler's assets. Called from dispose() on a thread, so that dispose() itself can complete + * faster. + */ + private void disposeAssets() { + logger.debug("disposeAssets() {}", this); + synchronized (this) { + assetsLoaded = false; + cancelTask(checkConnectionTask, true); + cancelTask(updateOnlineStateTask, true); + cancelTask(scheduledUpdateTask, true); + checkConnectionTask = null; + updateOnlineStateTask = null; + scheduledUpdateTask = null; + synchronized (resourcesEventTasks) { + resourcesEventTasks.values().forEach(task -> cancelTask(task, true)); + resourcesEventTasks.clear(); + } + ServiceRegistration registration = trustManagerRegistration; + if (Objects.nonNull(registration)) { + registration.unregister(); + trustManagerRegistration = null; + } + Clip2Bridge bridge = clip2Bridge; + if (Objects.nonNull(bridge)) { + bridge.close(); + clip2Bridge = null; + } + Clip2ThingDiscoveryService disco = discoveryService; + if (Objects.nonNull(disco)) { + disco.abortScan(); + } + } + } + + /** + * Return the application key for the console app. + * + * @return the application key. + */ + public String getApplicationKey() { + Clip2BridgeConfig config = getConfigAs(Clip2BridgeConfig.class); + return config.applicationKey; + } + + /** + * Get the Clip2Bridge connection and throw an exception if it is null. + * + * @return the Clip2Bridge. + * @throws AssetNotLoadedException if the Clip2Bridge is null. + */ + private Clip2Bridge getClip2Bridge() throws AssetNotLoadedException { + Clip2Bridge clip2Bridge = this.clip2Bridge; + if (Objects.nonNull(clip2Bridge)) { + return clip2Bridge; + } + throw new AssetNotLoadedException("Clip2Bridge is null"); + } + + /** + * Return the IP address for the console app. + * + * @return the IP address. + */ + public String getIpAddress() { + Clip2BridgeConfig config = getConfigAs(Clip2BridgeConfig.class); + return config.ipAddress; + } + + /** + * Get the v1 legacy Hue bridge (if any) which has the same IP address as this. + * + * @return Optional result containing the legacy bridge (if any found). + */ + public Optional getLegacyBridge() { + String ipAddress = getIpAddress(); + return Objects.nonNull(ipAddress) + ? thingRegistry.getAll().stream() + .filter(thing -> thing.getThingTypeUID().equals(THING_TYPE_BRIDGE) + && ipAddress.equals(thing.getConfiguration().get("ipAddress"))) + .findFirst() + : Optional.empty(); + } + + /** + * Get the v1 legacy Hue thing (if any) which has a Bridge having the same IP address as this, and an ID that + * matches the given parameter. + * + * @param targetIdV1 the idV1 attribute to match. + * @return Optional result containing the legacy thing (if found). + */ + public Optional getLegacyThing(String targetIdV1) { + Optional legacyBridge = getLegacyBridge(); + if (legacyBridge.isEmpty()) { + return Optional.empty(); + } + + String config; + if (targetIdV1.startsWith("/lights/")) { + config = LIGHT_ID; + } else if (targetIdV1.startsWith("/sensors/")) { + config = SENSOR_ID; + } else if (targetIdV1.startsWith("/groups/")) { + config = GROUP_ID; + } else { + return Optional.empty(); + } + + ThingUID legacyBridgeUID = legacyBridge.get().getUID(); + return thingRegistry.getAll().stream() // + .filter(thing -> legacyBridgeUID.equals(thing.getBridgeUID()) + && V1_THING_TYPE_UIDS.contains(thing.getThingTypeUID())) // + .filter(thing -> { + Object id = thing.getConfiguration().get(config); + return (id instanceof String) && targetIdV1.endsWith("/" + (String) id); + }).findFirst(); + } + + /** + * Return a localized text. + * + * @param key the i18n text key. + * @param arguments for parameterized translation. + * @return the localized text. + */ + public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) { + String result = translationProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments); + return Objects.nonNull(result) ? result : key; + } + + /** + * Execute an HTTP GET for a resources reference object from the server. + * + * @param reference containing the resourceType and (optionally) the resourceId of the resource to get. If the + * resourceId is null then all resources of the given type are returned. + * @return the resource, or null if something fails. + * @throws ApiException if a communication error occurred. + * @throws AssetNotLoadedException if one of the assets is not loaded. + * @throws InterruptedException + */ + public Resources getResources(ResourceReference reference) + throws ApiException, AssetNotLoadedException, InterruptedException { + logger.debug("getResources() {}", reference); + checkAssetsLoaded(); + return getClip2Bridge().getResources(reference); + } + + /** + * Getter for the scheduler. + * + * @return the scheduler. + */ + public ScheduledExecutorService getScheduler() { + return scheduler; + } + + @Override + public Collection> getServices() { + return Set.of(Clip2ThingDiscoveryService.class); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (RefreshType.REFRESH.equals(command)) { + return; + } + logger.warn("Bridge thing '{}' has no channels, only REFRESH command supported.", thing.getUID()); + } + + @Override + public void initialize() { + updateThingFromLegacy(); + updateStatus(ThingStatus.UNKNOWN); + applKeyRetriesRemaining = APPLICATION_KEY_MAX_TRIES; + connectRetriesRemaining = RECONNECT_MAX_TRIES; + initializeAssets(); + } + + /** + * Initialize the bridge handler's assets. + */ + private void initializeAssets() { + logger.debug("initializeAssets() {}", this); + synchronized (this) { + Clip2BridgeConfig config = getConfigAs(Clip2BridgeConfig.class); + + String ipAddress = config.ipAddress; + if (ipAddress.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error-no-ip-address"); + return; + } + + try { + if (!Clip2Bridge.isClip2Supported(ipAddress)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.api2.conf-error.clip2-not-supported"); + return; + } + } catch (IOException e) { + logger.trace("initializeAssets() communication error on '{}'", ipAddress, e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.api2.comm-error.exception [\"" + e.getMessage() + "\"]"); + return; + } + + HueTlsTrustManagerProvider trustManagerProvider = new HueTlsTrustManagerProvider(ipAddress + ":443", + config.useSelfSignedCertificate); + + if (Objects.isNull(trustManagerProvider.getPEMTrustManager())) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.api2.conf-error.certificate-load"); + return; + } + + trustManagerRegistration = FrameworkUtil.getBundle(getClass()).getBundleContext() + .registerService(TlsTrustManagerProvider.class.getName(), trustManagerProvider, null); + + String applicationKey = config.applicationKey; + applicationKey = Objects.nonNull(applicationKey) ? applicationKey : ""; + clip2Bridge = new Clip2Bridge(httpClientFactory, this, ipAddress, applicationKey); + + assetsLoaded = true; + } + cancelTask(checkConnectionTask, false); + checkConnectionTask = scheduler.submit(() -> checkConnection()); + } + + /** + * Called when the connection goes offline. Schedule a reconnection. + */ + public void onConnectionOffline() { + if (assetsLoaded) { + try { + getClip2Bridge().setExternalRestartScheduled(); + cancelTask(checkConnectionTask, false); + checkConnectionTask = scheduler.schedule(() -> checkConnection(), RECONNECT_DELAY_SECONDS, + TimeUnit.SECONDS); + } catch (AssetNotLoadedException e) { + // should never occur + } + } + } + + /** + * Called when the connection goes online. Schedule a general state update. + */ + public void onConnectionOnline() { + cancelTask(updateOnlineStateTask, false); + updateOnlineStateTask = scheduler.schedule(() -> updateOnlineState(), 0, TimeUnit.MILLISECONDS); + } + + /** + * Called when an SSE event message comes in with a valid list of resources. For each resource received, inform all + * child thing handlers with the respective resource. + * + * @param resources a list of incoming resource objects. + */ + public void onResourcesEvent(List resources) { + if (assetsLoaded) { + synchronized (resourcesEventTasks) { + int index = resourcesEventTasks.size(); + resourcesEventTasks.put(index, scheduler.submit(() -> { + onResourcesEventTask(resources); + resourcesEventTasks.remove(index); + })); + } + } + } + + private void onResourcesEventTask(List resources) { + logger.debug("onResourcesEventTask() resource count {}", resources.size()); + getThing().getThings().forEach(thing -> { + ThingHandler handler = thing.getHandler(); + if (handler instanceof Clip2ThingHandler) { + resources.forEach(resource -> { + ((Clip2ThingHandler) handler).onResource(resource); + }); + } + }); + } + + /** + * Execute an HTTP PUT to send a Resource object to the server. + * + * @param resource the resource to put. + * @throws ApiException if a communication error occurred. + * @throws AssetNotLoadedException if one of the assets is not loaded. + * @throws InterruptedException + */ + public void putResource(Resource resource) throws ApiException, AssetNotLoadedException, InterruptedException { + logger.debug("putResource() {}", resource); + checkAssetsLoaded(); + getClip2Bridge().putResource(resource); + } + + /** + * Register the application key with the hub. If the current application key is empty it will create a new one. + * + * @throws HttpUnauthorizedException if the communication was OK but the registration failed anyway. + * @throws ApiException if a communication error occurred. + * @throws AssetNotLoadedException if one of the assets is not loaded. + * @throws IllegalStateException if the configuration cannot be changed e.g. read only. + * @throws InterruptedException + */ + private void registerApplicationKey() throws HttpUnauthorizedException, ApiException, AssetNotLoadedException, + IllegalStateException, InterruptedException { + logger.debug("registerApplicationKey()"); + Clip2BridgeConfig config = getConfigAs(Clip2BridgeConfig.class); + String newApplicationKey = getClip2Bridge().registerApplicationKey(config.applicationKey); + Configuration configuration = editConfiguration(); + configuration.put(Clip2BridgeConfig.APPLICATION_KEY, newApplicationKey); + updateConfiguration(configuration); + } + + /** + * Register the discovery service. + * + * @param discoveryService new discoveryService. + */ + public void registerDiscoveryService(Clip2ThingDiscoveryService discoveryService) { + this.discoveryService = discoveryService; + } + + /** + * Unregister the discovery service. + */ + public void unregisterDiscoveryService() { + discoveryService = null; + } + + /** + * Update the bridge's online state and update its dependent things. Called when the connection goes online. + */ + private void updateOnlineState() { + if (assetsLoaded && (thing.getStatus() != ThingStatus.ONLINE)) { + logger.debug("updateOnlineState()"); + connectRetriesRemaining = RECONNECT_MAX_TRIES; + updateStatus(ThingStatus.ONLINE); + updateThingsScheduled(500); + Clip2ThingDiscoveryService discoveryService = this.discoveryService; + if (Objects.nonNull(discoveryService)) { + discoveryService.startScan(null); + } + } + } + + /** + * Update the bridge thing properties. + * + * @throws ApiException if a communication error occurred. + * @throws AssetNotLoadedException if one of the assets is not loaded. + * @throws InterruptedException + */ + private void updateProperties() throws ApiException, AssetNotLoadedException, InterruptedException { + logger.debug("updateProperties()"); + Map properties = new HashMap<>(thing.getProperties()); + + for (Resource device : getClip2Bridge().getResources(BRIDGE).getResources()) { + // set the serial number + String bridgeId = device.getBridgeId(); + if (Objects.nonNull(bridgeId)) { + properties.put(Thing.PROPERTY_SERIAL_NUMBER, bridgeId); + } + break; + } + + for (Resource device : getClip2Bridge().getResources(DEVICE).getResources()) { + MetaData metaData = device.getMetaData(); + if (Objects.nonNull(metaData) && metaData.getArchetype() == Archetype.BRIDGE_V2) { + // set resource properties + properties.put(PROPERTY_RESOURCE_ID, device.getId()); + properties.put(PROPERTY_RESOURCE_TYPE, device.getType().toString()); + + // set metadata properties + String metaDataName = metaData.getName(); + if (Objects.nonNull(metaDataName)) { + properties.put(PROPERTY_RESOURCE_NAME, metaDataName); + } + properties.put(PROPERTY_RESOURCE_ARCHETYPE, metaData.getArchetype().toString()); + + // set product data properties + ProductData productData = device.getProductData(); + if (Objects.nonNull(productData)) { + // set generic thing properties + properties.put(Thing.PROPERTY_MODEL_ID, productData.getModelId()); + properties.put(Thing.PROPERTY_VENDOR, productData.getManufacturerName()); + properties.put(Thing.PROPERTY_FIRMWARE_VERSION, productData.getSoftwareVersion()); + String hardwarePlatformType = productData.getHardwarePlatformType(); + if (Objects.nonNull(hardwarePlatformType)) { + properties.put(Thing.PROPERTY_HARDWARE_VERSION, hardwarePlatformType); + } + + // set hue specific properties + properties.put(PROPERTY_PRODUCT_NAME, productData.getProductName()); + properties.put(PROPERTY_PRODUCT_ARCHETYPE, productData.getProductArchetype().toString()); + properties.put(PROPERTY_PRODUCT_CERTIFIED, productData.getCertified().toString()); + } + break; // we only needed the BRIDGE_V2 resource + } + } + thing.setProperties(properties); + } + + /** + * Update the thing's own state. Called sporadically in case any SSE events may have been lost. + */ + private void updateSelf() { + logger.debug("updateSelf()"); + try { + checkAssetsLoaded(); + updateProperties(); + getClip2Bridge().open(); + } catch (ApiException e) { + logger.trace("updateSelf() {}", e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.api2.comm-error.exception [\"" + e.getMessage() + "\"]"); + onConnectionOffline(); + } catch (AssetNotLoadedException e) { + logger.trace("updateSelf() {}", e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.api2.conf-error.assets-not-loaded"); + } catch (InterruptedException e) { + } + } + + /** + * Check if a PROPERTY_LEGACY_THING_UID value was set by the discovery process, and if so, clone the legacy thing's + * settings into this thing. + */ + private void updateThingFromLegacy() { + if (isInitialized()) { + logger.warn("Cannot update bridge thing '{}' from legacy since handler already initialized.", + thing.getUID()); + return; + } + Map properties = thing.getProperties(); + String legacyThingUID = properties.get(PROPERTY_LEGACY_THING_UID); + if (Objects.nonNull(legacyThingUID)) { + Thing legacyThing = thingRegistry.get(new ThingUID(legacyThingUID)); + if (Objects.nonNull(legacyThing)) { + BridgeBuilder editBuilder = editThing(); + + String location = legacyThing.getLocation(); + if (Objects.nonNull(location) && !location.isBlank()) { + editBuilder = editBuilder.withLocation(location); + } + + Object userName = legacyThing.getConfiguration().get(USER_NAME); + if (userName instanceof String) { + Configuration configuration = thing.getConfiguration(); + configuration.put(Clip2BridgeConfig.APPLICATION_KEY, userName); + editBuilder = editBuilder.withConfiguration(configuration); + } + + Map newProperties = new HashMap<>(properties); + newProperties.remove(PROPERTY_LEGACY_THING_UID); + + updateThing(editBuilder.withProperties(newProperties).build()); + } + } + } + + /** + * Execute the mass download of all relevant resource types, and inform all child thing handlers. + */ + private void updateThingsNow() { + logger.debug("updateThingsNow()"); + try { + Clip2Bridge bridge = getClip2Bridge(); + for (ResourceReference reference : MASS_DOWNLOAD_RESOURCE_REFERENCES) { + ResourceType resourceType = reference.getType(); + List resourceList = bridge.getResources(reference).getResources(); + if (resourceType == ResourceType.ZONE) { + // add special 'All Lights' zone to the zone resource list + resourceList.addAll(bridge.getResources(BRIDGE_HOME).getResources()); + } + getThing().getThings().forEach(thing -> { + ThingHandler handler = thing.getHandler(); + if (handler instanceof Clip2ThingHandler) { + ((Clip2ThingHandler) handler).onResourcesList(resourceType, resourceList); + } + }); + } + } catch (ApiException | AssetNotLoadedException e) { + if (logger.isDebugEnabled()) { + logger.debug("updateThingsNow() unexpected exception", e); + } else { + logger.warn("Unexpected exception '{}' while updating things.", e.getMessage()); + } + } catch (InterruptedException e) { + } + } + + /** + * Schedule a task to call updateThings(). It prevents floods of GET calls when multiple child things are added at + * the same time. + * + * @param delayMilliSeconds the delay before running the next task. + */ + private void updateThingsScheduled(int delayMilliSeconds) { + ScheduledFuture task = this.scheduledUpdateTask; + if (Objects.isNull(task) || task.getDelay(TimeUnit.MILLISECONDS) < 100) { + cancelTask(scheduledUpdateTask, false); + scheduledUpdateTask = scheduler.schedule(() -> updateThingsNow(), delayMilliSeconds, TimeUnit.MILLISECONDS); + } + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2StateDescriptionProvider.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2StateDescriptionProvider.java new file mode 100644 index 000000000..b143ad6c1 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2StateDescriptionProvider.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link Clip2StateDescriptionProvider} provides dynamic state descriptions of scene channels whose list of options + * is determined at runtime. + * + * @author Andrew Fiddian-Green - Initial contribution + * + */ +@NonNullByDefault +@Component(service = { DynamicStateDescriptionProvider.class, Clip2StateDescriptionProvider.class }) +public class Clip2StateDescriptionProvider extends BaseDynamicStateDescriptionProvider { + + @Activate + public Clip2StateDescriptionProvider(final @Reference EventPublisher eventPublisher, + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, + final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + this.eventPublisher = eventPublisher; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2ThingHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2ThingHandler.java new file mode 100644 index 000000000..350e6387c --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2ThingHandler.java @@ -0,0 +1,1197 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.handler; + +import static org.openhab.binding.hue.internal.HueBindingConstants.*; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.action.DynamicsActions; +import org.openhab.binding.hue.internal.config.Clip2ThingConfig; +import org.openhab.binding.hue.internal.dto.clip2.Alerts; +import org.openhab.binding.hue.internal.dto.clip2.ColorXy; +import org.openhab.binding.hue.internal.dto.clip2.Dimming; +import org.openhab.binding.hue.internal.dto.clip2.Effects; +import org.openhab.binding.hue.internal.dto.clip2.Gamut2; +import org.openhab.binding.hue.internal.dto.clip2.MetaData; +import org.openhab.binding.hue.internal.dto.clip2.MirekSchema; +import org.openhab.binding.hue.internal.dto.clip2.ProductData; +import org.openhab.binding.hue.internal.dto.clip2.Resource; +import org.openhab.binding.hue.internal.dto.clip2.ResourceReference; +import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType; +import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType; +import org.openhab.binding.hue.internal.dto.clip2.enums.RecallAction; +import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType; +import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus; +import org.openhab.binding.hue.internal.dto.clip2.helper.Setters; +import org.openhab.binding.hue.internal.exceptions.ApiException; +import org.openhab.binding.hue.internal.exceptions.AssetNotLoadedException; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.MetricPrefix; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.link.ItemChannelLink; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.StateOption; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handler for things based on CLIP 2 'device', 'room', or 'zone resources. + * + * @author Andrew Fiddian-Green - Initial contribution. + */ +@NonNullByDefault +public class Clip2ThingHandler extends BaseThingHandler { + + public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_DEVICE, THING_TYPE_ROOM, + THING_TYPE_ZONE); + + private static final Duration DYNAMICS_ACTIVE_WINDOW = Duration.ofSeconds(10); + + private final Logger logger = LoggerFactory.getLogger(Clip2ThingHandler.class); + + /** + * A map of service Resources whose state contributes to the overall state of this thing. It is a map between the + * resource ID (string) and a Resource object containing the last known state. e.g. a DEVICE thing may support a + * LIGHT service whose Resource contributes to its overall state, or a ROOM or ZONE thing may support a + * GROUPED_LIGHT service whose Resource contributes to the its overall state. + */ + private final Map serviceContributorsCache = new ConcurrentHashMap<>(); + + /** + * A map of Resource IDs which are targets for commands to be sent. It is a map between the type of command + * (ResourcesType) and the resource ID to which the command shall be sent. e.g. a LIGHT 'on' command shall be sent + * to the respective LIGHT resource ID. + */ + private final Map commandResourceIds = new ConcurrentHashMap<>(); + + /** + * Button devices contain one or more physical buttons, each of which is represented by a BUTTON Resource with its + * own unique resource ID, and a respective controlId that indicates which button it is in the device. e.g. a dimmer + * pad has four buttons (controlId's 1..4) each represented by a BUTTON Resource with a unique resource ID. This is + * a map between the resource ID and its respective controlId. + */ + private final Map controlIds = new ConcurrentHashMap<>(); + + /** + * The set of channel IDs that are supported by this thing. e.g. an on/off light may support 'switch' and + * 'zigbeeStatus' channels, whereas a complex light may support 'switch', 'brightness', 'color', 'color temperature' + * and 'zigbeeStatus' channels. + */ + private final Set supportedChannelIdSet = new HashSet<>(); + + /** + * A map of scene IDs and respective scene Resources for the scenes that contribute to and command this thing. It is + * a map between the resource ID (string) and a Resource object containing the scene's last known state. + */ + private final Map sceneContributorsCache = new ConcurrentHashMap<>(); + + /** + * A map of scene names versus Resource IDs for the scenes that contribute to and command this thing. e.g. a command + * for a scene named 'Energize' shall be sent to the respective SCENE resource ID. + */ + private final Map sceneResourceIds = new ConcurrentHashMap<>(); + + /** + * A list of API v1 thing channel UIDs that are linked to items. It is used in the process of replicating the + * Item/Channel links from a legacy v1 thing to this API v2 thing. + */ + private final List legacyLinkedChannelUIDs = new CopyOnWriteArrayList<>(); + + private final ThingRegistry thingRegistry; + private final ItemChannelLinkRegistry itemChannelLinkRegistry; + private final Clip2StateDescriptionProvider stateDescriptionProvider; + + private String resourceId = "?"; + private Resource thisResource; + private Duration dynamicsDuration = Duration.ZERO; + private Instant dynamicsExpireTime = Instant.MIN; + + private boolean disposing; + private boolean hasConnectivityIssue; + private boolean updateSceneContributorsDone; + private boolean updateLightPropertiesDone; + private boolean updatePropertiesDone; + private boolean updateDependenciesDone; + + private @Nullable Future alertResetTask; + private @Nullable Future dynamicsResetTask; + private @Nullable Future updateDependenciesTask; + private @Nullable Future updateServiceContributorsTask; + + public Clip2ThingHandler(Thing thing, Clip2StateDescriptionProvider stateDescriptionProvider, + ThingRegistry thingRegistry, ItemChannelLinkRegistry itemChannelLinkRegistry) { + super(thing); + + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (THING_TYPE_DEVICE.equals(thingTypeUID)) { + thisResource = new Resource(ResourceType.DEVICE); + } else if (THING_TYPE_ROOM.equals(thingTypeUID)) { + thisResource = new Resource(ResourceType.ROOM); + } else if (THING_TYPE_ZONE.equals(thingTypeUID)) { + thisResource = new Resource(ResourceType.ZONE); + } else { + throw new IllegalArgumentException("Wrong thing type " + thingTypeUID.getAsString()); + } + + this.thingRegistry = thingRegistry; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + this.stateDescriptionProvider = stateDescriptionProvider; + } + + /** + * Add a channel ID to the supportedChannelIdSet set. If the channel supports dynamics (timed transitions) then add + * the respective channel as well. + * + * @param channelId the channel ID to add. + */ + private void addSupportedChannel(String channelId) { + if (!disposing && !updateDependenciesDone) { + synchronized (supportedChannelIdSet) { + logger.debug("{} -> addSupportedChannel() '{}' added to supported channel set", resourceId, channelId); + supportedChannelIdSet.add(channelId); + if (DYNAMIC_CHANNELS.contains(channelId)) { + clearDynamicsChannel(); + } + } + } + } + + /** + * Cancel the given task. + * + * @param cancelTask the task to be cancelled (may be null) + * @param mayInterrupt allows cancel() to interrupt the thread. + */ + private void cancelTask(@Nullable Future cancelTask, boolean mayInterrupt) { + if (Objects.nonNull(cancelTask)) { + cancelTask.cancel(mayInterrupt); + } + } + + /** + * Clear the dynamics channel parameters. + */ + private void clearDynamicsChannel() { + dynamicsExpireTime = Instant.MIN; + dynamicsDuration = Duration.ZERO; + updateState(CHANNEL_2_DYNAMICS, new QuantityType<>(0, MetricPrefix.MILLI(Units.SECOND)), true); + } + + @Override + public void dispose() { + logger.debug("{} -> dispose()", resourceId); + disposing = true; + cancelTask(alertResetTask, true); + cancelTask(dynamicsResetTask, true); + cancelTask(updateDependenciesTask, true); + cancelTask(updateServiceContributorsTask, true); + alertResetTask = null; + dynamicsResetTask = null; + updateDependenciesTask = null; + updateServiceContributorsTask = null; + legacyLinkedChannelUIDs.clear(); + sceneContributorsCache.clear(); + sceneResourceIds.clear(); + supportedChannelIdSet.clear(); + commandResourceIds.clear(); + serviceContributorsCache.clear(); + controlIds.clear(); + } + + /** + * Get the bridge handler. + * + * @throws AssetNotLoadedException if the handler does not exist. + */ + private Clip2BridgeHandler getBridgeHandler() throws AssetNotLoadedException { + Bridge bridge = getBridge(); + if (Objects.nonNull(bridge)) { + BridgeHandler handler = bridge.getHandler(); + if (handler instanceof Clip2BridgeHandler) { + return (Clip2BridgeHandler) handler; + } + } + throw new AssetNotLoadedException("Bridge handler missing"); + } + + /** + * Do a double lookup to get the cached resource that matches the given ResourceType. + * + * @param resourceType the type to search for. + * @return the Resource, or null if not found. + */ + private @Nullable Resource getCachedResource(ResourceType resourceType) { + String commandResourceId = commandResourceIds.get(resourceType); + return Objects.nonNull(commandResourceId) ? serviceContributorsCache.get(commandResourceId) : null; + } + + /** + * Return a ResourceReference to this handler's resource. + * + * @return a ResourceReference instance. + */ + public ResourceReference getResourceReference() { + return new ResourceReference().setId(resourceId).setType(thisResource.getType()); + } + + /** + * Register the 'DynamicsAction' service. + */ + @Override + public Collection> getServices() { + return Set.of(DynamicsActions.class); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command commandParam) { + if (RefreshType.REFRESH.equals(commandParam)) { + if ((thing.getStatus() == ThingStatus.ONLINE) && updateDependenciesDone) { + Future task = updateServiceContributorsTask; + if (Objects.isNull(task) || !task.isDone()) { + cancelTask(updateServiceContributorsTask, false); + updateServiceContributorsTask = scheduler.schedule(() -> { + try { + updateServiceContributors(); + } catch (ApiException | AssetNotLoadedException e) { + logger.debug("{} -> handleCommand() error {}", resourceId, e.getMessage(), e); + } catch (InterruptedException e) { + } + }, 3, TimeUnit.SECONDS); + } + } + return; + } + + Channel channel = thing.getChannel(channelUID); + if (channel == null) { + if (logger.isDebugEnabled()) { + logger.debug("{} -> handleCommand() channelUID:{} does not exist", resourceId, channelUID); + + } else { + logger.warn("Command received for channel '{}' which is not in thing '{}'.", channelUID, + thing.getUID()); + } + return; + } + + ResourceType lightResourceType = thisResource.getType() == ResourceType.DEVICE ? ResourceType.LIGHT + : ResourceType.GROUPED_LIGHT; + + Resource putResource = null; + String putResourceId = null; + Command command = commandParam; + String channelId = channelUID.getId(); + Resource cache = getCachedResource(lightResourceType); + + switch (channelId) { + case CHANNEL_2_ALERT: + putResource = Setters.setAlert(new Resource(lightResourceType), command, cache); + cancelTask(alertResetTask, false); + alertResetTask = scheduler.schedule( + () -> updateState(channelUID, new StringType(ActionType.NO_ACTION.name())), 10, + TimeUnit.SECONDS); + break; + + case CHANNEL_2_EFFECT: + putResource = Setters.setEffect(new Resource(lightResourceType), command, cache); + putResource.setOnOff(OnOffType.ON); + break; + + case CHANNEL_2_COLOR_TEMP_PERCENT: + if (command instanceof IncreaseDecreaseType) { + if (Objects.nonNull(cache)) { + State current = cache.getColorTemperaturePercentState(); + if (current instanceof PercentType) { + int sign = IncreaseDecreaseType.INCREASE == command ? 1 : -1; + int percent = ((PercentType) current).intValue() + (sign * (int) Resource.PERCENT_DELTA); + command = new PercentType(Math.min(100, Math.max(0, percent))); + } + } + } else if (command instanceof OnOffType) { + command = OnOffType.OFF == command ? PercentType.ZERO : PercentType.HUNDRED; + } + putResource = Setters.setColorTemperaturePercent(new Resource(lightResourceType), command, cache); + break; + + case CHANNEL_2_COLOR_TEMP_ABSOLUTE: + putResource = Setters.setColorTemperatureAbsolute(new Resource(lightResourceType), command, cache); + break; + + case CHANNEL_2_COLOR: + putResource = new Resource(lightResourceType); + if (command instanceof HSBType) { + HSBType color = ((HSBType) command); + putResource = Setters.setColorXy(putResource, color, cache); + command = color.getBrightness(); + } + // NB fall through for handling of brightness and switch related commands !! + + case CHANNEL_2_BRIGHTNESS: + putResource = Objects.nonNull(putResource) ? putResource : new Resource(lightResourceType); + if (command instanceof IncreaseDecreaseType) { + if (Objects.nonNull(cache)) { + State current = cache.getBrightnessState(); + if (current instanceof PercentType) { + int sign = IncreaseDecreaseType.INCREASE == command ? 1 : -1; + double percent = ((PercentType) current).doubleValue() + (sign * Resource.PERCENT_DELTA); + command = new PercentType(new BigDecimal(Math.min(100f, Math.max(0f, percent)), + Resource.PERCENT_MATH_CONTEXT)); + } + } + } + if (command instanceof PercentType) { + PercentType brightness = (PercentType) command; + putResource = Setters.setDimming(putResource, brightness, cache); + Double minDimLevel = Objects.nonNull(cache) ? cache.getMinimumDimmingLevel() : null; + minDimLevel = Objects.nonNull(minDimLevel) ? minDimLevel : Dimming.DEFAULT_MINIMUM_DIMMIMG_LEVEL; + command = OnOffType.from(brightness.doubleValue() >= minDimLevel); + } + // NB fall through for handling of switch related commands !! + + case CHANNEL_2_SWITCH: + putResource = Objects.nonNull(putResource) ? putResource : new Resource(lightResourceType); + putResource.setOnOff(command); + break; + + case CHANNEL_2_COLOR_XY_ONLY: + putResource = Setters.setColorXy(new Resource(lightResourceType), command, cache); + break; + + case CHANNEL_2_DIMMING_ONLY: + putResource = Setters.setDimming(new Resource(lightResourceType), command, cache); + break; + + case CHANNEL_2_ON_OFF_ONLY: + putResource = new Resource(lightResourceType).setOnOff(command); + break; + + case CHANNEL_2_TEMPERATURE_ENABLED: + putResource = new Resource(ResourceType.TEMPERATURE).setEnabled(command); + break; + + case CHANNEL_2_MOTION_ENABLED: + putResource = new Resource(ResourceType.MOTION).setEnabled(command); + break; + + case CHANNEL_2_LIGHT_LEVEL_ENABLED: + putResource = new Resource(ResourceType.LIGHT_LEVEL).setEnabled(command); + break; + + case CHANNEL_2_SCENE: + if (command instanceof StringType) { + putResourceId = sceneResourceIds.get(((StringType) command).toString()); + if (Objects.nonNull(putResourceId)) { + putResource = new Resource(ResourceType.SCENE).setRecallAction(RecallAction.ACTIVE); + } + } + break; + + case CHANNEL_2_DYNAMICS: + Duration clearAfter = Duration.ZERO; + if (command instanceof QuantityType) { + QuantityType durationMs = ((QuantityType) command).toUnit(MetricPrefix.MILLI(Units.SECOND)); + if (Objects.nonNull(durationMs) && durationMs.longValue() > 0) { + Duration duration = Duration.ofMillis(durationMs.longValue()); + dynamicsDuration = duration; + dynamicsExpireTime = Instant.now().plus(DYNAMICS_ACTIVE_WINDOW); + clearAfter = DYNAMICS_ACTIVE_WINDOW; + logger.debug("{} -> handleCommand() dynamics setting {} valid for {}", resourceId, duration, + clearAfter); + } + } + cancelTask(dynamicsResetTask, false); + dynamicsResetTask = scheduler.schedule(() -> clearDynamicsChannel(), clearAfter.toMillis(), + TimeUnit.MILLISECONDS); + return; + + default: + if (logger.isDebugEnabled()) { + logger.debug("{} -> handleCommand() channelUID:{} unknown", resourceId, channelUID); + } else { + logger.warn("Command received for unknown channel '{}'.", channelUID); + } + return; + } + + if (putResource == null) { + if (logger.isDebugEnabled()) { + logger.debug("{} -> handleCommand() command:{} not supported on channelUID:{}", resourceId, command, + channelUID); + } else { + logger.warn("Command '{}' is not supported on channel '{}'.", command, channelUID); + } + return; + } + + putResourceId = Objects.nonNull(putResourceId) ? putResourceId : commandResourceIds.get(putResource.getType()); + if (putResourceId == null) { + if (logger.isDebugEnabled()) { + logger.debug( + "{} -> handleCommand() channelUID:{}, command:{}, putResourceType:{} => missing resource ID", + resourceId, channelUID, command, putResource.getType()); + } else { + logger.warn("Command '{}' for channel '{}' cannot be processed by thing '{}'.", command, channelUID, + thing.getUID()); + } + return; + } + + if (DYNAMIC_CHANNELS.contains(channelId)) { + if (Instant.now().isBefore(dynamicsExpireTime) && !dynamicsDuration.isZero() + && !dynamicsDuration.isNegative()) { + if (ResourceType.SCENE == putResource.getType()) { + putResource.setRecallDuration(dynamicsDuration); + } else { + putResource.setDynamicsDuration(dynamicsDuration); + } + } + } + + putResource.setId(putResourceId); + logger.debug("{} -> handleCommand() put resource {}", resourceId, putResource); + + try { + getBridgeHandler().putResource(putResource); + } catch (ApiException | AssetNotLoadedException e) { + if (logger.isDebugEnabled()) { + logger.debug("{} -> handleCommand() error {}", resourceId, e.getMessage(), e); + } else { + logger.warn("Command '{}' for thing '{}', channel '{}' failed with error '{}'.", command, + thing.getUID(), channelUID, e.getMessage()); + } + } catch (InterruptedException e) { + } + } + + /** + * Handle a 'dynamics' command for the given channel ID for the given dynamics duration. + * + * @param channelId the ID of the target channel. + * @param command the new target state. + * @param duration the transition duration. + */ + public synchronized void handleDynamicsCommand(String channelId, Command command, QuantityType duration) { + if (DYNAMIC_CHANNELS.contains(channelId)) { + Channel dynamicsChannel = thing.getChannel(CHANNEL_2_DYNAMICS); + Channel targetChannel = thing.getChannel(channelId); + if (Objects.nonNull(dynamicsChannel) && Objects.nonNull(targetChannel)) { + logger.debug("{} - handleDynamicsCommand() channelId:{}, command:{}, duration:{}", resourceId, + channelId, command, duration); + handleCommand(dynamicsChannel.getUID(), duration); + handleCommand(targetChannel.getUID(), command); + return; + } + } + logger.warn("Dynamics command '{}' for thing '{}', channel '{}' and duration'{}' failed.", command, + thing.getUID(), channelId, duration); + } + + @Override + public void initialize() { + Clip2ThingConfig config = getConfigAs(Clip2ThingConfig.class); + + String resourceId = config.resourceId; + if (resourceId.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.api2.conf-error.resource-id-bad"); + return; + } + thisResource.setId(resourceId); + this.resourceId = resourceId; + logger.debug("{} -> initialize()", resourceId); + + updateThingFromLegacy(); + updateStatus(ThingStatus.UNKNOWN); + + dynamicsDuration = Duration.ZERO; + dynamicsExpireTime = Instant.MIN; + + disposing = false; + hasConnectivityIssue = false; + updatePropertiesDone = false; + updateDependenciesDone = false; + updateLightPropertiesDone = false; + updateSceneContributorsDone = false; + + Bridge bridge = getBridge(); + if (Objects.nonNull(bridge)) { + BridgeHandler bridgeHandler = bridge.getHandler(); + if (bridgeHandler instanceof Clip2BridgeHandler) { + ((Clip2BridgeHandler) bridgeHandler).childInitialized(); + } + } + } + + /** + * Update the channel state depending on a new resource sent from the bridge. + * + * @param resource a Resource object containing the new state. + */ + public void onResource(Resource resource) { + if (!disposing) { + boolean resourceConsumed = false; + String incomingResourceId = resource.getId(); + if (resourceId.equals(incomingResourceId)) { + if (resource.hasFullState()) { + thisResource = resource; + if (!updatePropertiesDone) { + updateProperties(resource); + resourceConsumed = updatePropertiesDone; + } + } + if (!updateDependenciesDone) { + resourceConsumed = true; + cancelTask(updateDependenciesTask, false); + updateDependenciesTask = scheduler.submit(() -> updateDependencies()); + } + } else if (ResourceType.SCENE == resource.getType()) { + Resource cachedScene = sceneContributorsCache.get(incomingResourceId); + if (Objects.nonNull(cachedScene)) { + Setters.setResource(resource, cachedScene); + resourceConsumed = updateChannels(resource); + sceneContributorsCache.put(incomingResourceId, resource); + } + } else { + Resource cachedService = serviceContributorsCache.get(incomingResourceId); + if (Objects.nonNull(cachedService)) { + Setters.setResource(resource, cachedService); + resourceConsumed = updateChannels(resource); + serviceContributorsCache.put(incomingResourceId, resource); + if (ResourceType.LIGHT == resource.getType() && !updateLightPropertiesDone) { + updateLightProperties(resource); + } + } + } + if (resourceConsumed) { + logger.debug("{} -> onResource() consumed resource {}", resourceId, resource); + } + } + } + + /** + * Update the thing internal state depending on a full list of resources sent from the bridge. If the resourceType + * is SCENE then call updateScenes(), otherwise if the resource refers to this thing, consume it via onResource() as + * any other resource, or else if the resourceType nevertheless matches the thing type, set the thing state offline. + * + * @param resourceType the type of the resources in the list. + * @param fullResources the full list of resources of the given type. + */ + public void onResourcesList(ResourceType resourceType, List fullResources) { + if (resourceType == ResourceType.SCENE) { + updateSceneContributors(fullResources); + } else { + fullResources.stream().filter(r -> resourceId.equals(r.getId())).findAny() + .ifPresentOrElse(r -> onResource(r), () -> { + if (resourceType == thisResource.getType()) { + logger.debug("{} -> onResourcesList() configuration error: unknown resourceId", resourceId); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.api2.conf-error.resource-id-bad"); + } + }); + } + } + + /** + * Process the incoming Resource to initialize the alert channel. + * + * @param resource a Resource possibly with an Alerts element. + */ + private void updateAlertChannel(Resource resource) { + Alerts alerts = resource.getAlerts(); + if (Objects.nonNull(alerts)) { + List stateOptions = alerts.getActionValues().stream().map(action -> action.name()) + .map(actionId -> new StateOption(actionId, actionId)).collect(Collectors.toList()); + if (!stateOptions.isEmpty()) { + stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_ALERT), stateOptions); + logger.debug("{} -> updateAlerts() found {} associated alerts", resourceId, stateOptions.size()); + } + } + } + + /** + * If this v2 thing has a matching v1 legacy thing in the system, then for each channel in the v1 thing that + * corresponds to an equivalent channel in this v2 thing, and for all items that are linked to the v1 channel, + * create a new channel/item link between that item and the respective v2 channel in this thing. + */ + private void updateChannelItemLinksFromLegacy() { + if (!disposing) { + legacyLinkedChannelUIDs.forEach(legacyLinkedChannelUID -> { + String targetChannelId = REPLICATE_CHANNEL_ID_MAP.get(legacyLinkedChannelUID.getId()); + if (Objects.nonNull(targetChannelId)) { + Channel targetChannel = thing.getChannel(targetChannelId); + if (Objects.nonNull(targetChannel)) { + ChannelUID uid = targetChannel.getUID(); + itemChannelLinkRegistry.getLinkedItems(legacyLinkedChannelUID).forEach(linkedItem -> { + String item = linkedItem.getName(); + if (!itemChannelLinkRegistry.isLinked(item, uid)) { + if (logger.isDebugEnabled()) { + logger.debug( + "{} -> updateChannelItemLinksFromLegacy() item:{} linked to channel:{}", + resourceId, item, uid); + } else { + logger.info("Item '{}' linked to thing '{}' channel '{}'", item, thing.getUID(), + targetChannelId); + } + itemChannelLinkRegistry.add(new ItemChannelLink(item, uid)); + } + }); + } + } + }); + legacyLinkedChannelUIDs.clear(); + } + } + + /** + * Set the active list of channels by removing any that had initially been created by the thing XML declaration, but + * which in fact did not have data returned from the bridge i.e. channels which are not in the supportedChannelIdSet + * + * Also warn if there are channels in the supportedChannelIdSet set which are not in the thing. + * + * Adjusts the channel list so that only the highest level channel is available in the normal channel list. If a + * light supports the color channel, then it's brightness and switch can be commanded via the 'B' part of the HSB + * channel value. And if it supports the brightness channel the switch can be controlled via the brightness. So we + * can remove these lower level channels from the normal channel list. + * + * For more advanced applications, it is necessary to orthogonally command the color xy parameter, dimming + * parameter, and/or on/off parameter independently. So we add corresponding advanced level 'CHANNEL_2_BLAH_ONLY' + * channels for that purpose. Since they are advanced level, normal users should normally not be confused by them, + * yet advanced users can use them nevertheless. + */ + private void updateChannelList() { + if (!disposing) { + synchronized (supportedChannelIdSet) { + logger.debug("{} -> updateChannelList()", resourceId); + + if (supportedChannelIdSet.contains(CHANNEL_2_COLOR)) { + supportedChannelIdSet.add(CHANNEL_2_COLOR_XY_ONLY); + // + supportedChannelIdSet.remove(CHANNEL_2_BRIGHTNESS); + supportedChannelIdSet.add(CHANNEL_2_DIMMING_ONLY); + // + supportedChannelIdSet.remove(CHANNEL_2_SWITCH); + supportedChannelIdSet.add(CHANNEL_2_ON_OFF_ONLY); + } + if (supportedChannelIdSet.contains(CHANNEL_2_BRIGHTNESS)) { + supportedChannelIdSet.add(CHANNEL_2_DIMMING_ONLY); + // + supportedChannelIdSet.remove(CHANNEL_2_SWITCH); + supportedChannelIdSet.add(CHANNEL_2_ON_OFF_ONLY); + } + if (supportedChannelIdSet.contains(CHANNEL_2_SWITCH)) { + supportedChannelIdSet.add(CHANNEL_2_ON_OFF_ONLY); + } + + /* + * This binding creates its dynamic list of channels by a 'subtractive' method i.e. the full set of + * channels is initially created from the thing type xml, and then for any channels where UndfType.NULL + * data is returned, the respective channel is removed from the full list. However in seldom cases + * UndfType.NULL may wrongly be returned, so we should log a warning here just in case. + */ + if (logger.isDebugEnabled()) { + supportedChannelIdSet.stream().filter(channelId -> Objects.isNull(thing.getChannel(channelId))) + .forEach(channelId -> logger.debug( + "{} -> updateChannelList() required channel '{}' missing", resourceId, channelId)); + } else { + supportedChannelIdSet.stream().filter(channelId -> Objects.isNull(thing.getChannel(channelId))) + .forEach(channelId -> logger.warn( + "Thing '{}' is missing required channel '{}'. Please recreate the thing!", + thing.getUID(), channelId)); + } + + // get list of unused channels + List unusedChannels = thing.getChannels().stream() + .filter(channel -> !supportedChannelIdSet.contains(channel.getUID().getId())) + .collect(Collectors.toList()); + + // remove any unused channels + if (!unusedChannels.isEmpty()) { + if (logger.isDebugEnabled()) { + unusedChannels.stream().map(channel -> channel.getUID().getId()) + .forEach(channelId -> logger.debug( + "{} -> updateChannelList() removing unused channel '{}'", resourceId, + channelId)); + } + updateThing(editThing().withoutChannels(unusedChannels).build()); + } + } + } + } + + /** + * Update the state of the existing channels. + * + * @param resource the Resource containing the new channel state. + * @return true if the channel was found and updated. + */ + private boolean updateChannels(Resource resource) { + logger.debug("{} -> updateChannels() from resource {}", resourceId, resource); + boolean fullUpdate = resource.hasFullState(); + switch (resource.getType()) { + case BUTTON: + if (fullUpdate) { + addSupportedChannel(CHANNEL_2_BUTTON_LAST_EVENT); + controlIds.put(resource.getId(), resource.getControlId()); + } + State buttonState = resource.getButtonEventState(controlIds); + updateState(CHANNEL_2_BUTTON_LAST_EVENT, buttonState, fullUpdate); + break; + + case DEVICE_POWER: + updateState(CHANNEL_2_BATTERY_LEVEL, resource.getBatteryLevelState(), fullUpdate); + updateState(CHANNEL_2_BATTERY_LOW, resource.getBatteryLowState(), fullUpdate); + break; + + case LIGHT: + if (fullUpdate) { + updateEffectChannel(resource); + } + updateState(CHANNEL_2_COLOR_TEMP_PERCENT, resource.getColorTemperaturePercentState(), fullUpdate); + updateState(CHANNEL_2_COLOR_TEMP_ABSOLUTE, resource.getColorTemperatureAbsoluteState(), fullUpdate); + updateState(CHANNEL_2_COLOR, resource.getColorState(), fullUpdate); + updateState(CHANNEL_2_COLOR_XY_ONLY, resource.getColorXyState(), fullUpdate); + updateState(CHANNEL_2_EFFECT, resource.getEffectState(), fullUpdate); + // fall through for dimming and on/off related channels + + case GROUPED_LIGHT: + if (fullUpdate) { + updateAlertChannel(resource); + } + updateState(CHANNEL_2_BRIGHTNESS, resource.getBrightnessState(), fullUpdate); + updateState(CHANNEL_2_DIMMING_ONLY, resource.getDimmingState(), fullUpdate); + updateState(CHANNEL_2_SWITCH, resource.getOnOffState(), fullUpdate); + updateState(CHANNEL_2_ON_OFF_ONLY, resource.getOnOffState(), fullUpdate); + updateState(CHANNEL_2_ALERT, resource.getAlertState(), fullUpdate); + break; + + case LIGHT_LEVEL: + updateState(CHANNEL_2_LIGHT_LEVEL, resource.getLightLevelState(), fullUpdate); + updateState(CHANNEL_2_LIGHT_LEVEL_ENABLED, resource.getEnabledState(), fullUpdate); + break; + + case MOTION: + updateState(CHANNEL_2_MOTION, resource.getMotionState(), fullUpdate); + updateState(CHANNEL_2_MOTION_ENABLED, resource.getEnabledState(), fullUpdate); + break; + + case RELATIVE_ROTARY: + if (fullUpdate) { + addSupportedChannel(CHANNEL_2_ROTARY_STEPS); + } + updateState(CHANNEL_2_ROTARY_STEPS, resource.getRotaryStepsState(), fullUpdate); + break; + + case TEMPERATURE: + updateState(CHANNEL_2_TEMPERATURE, resource.getTemperatureState(), fullUpdate); + updateState(CHANNEL_2_TEMPERATURE_ENABLED, resource.getEnabledState(), fullUpdate); + break; + + case ZIGBEE_CONNECTIVITY: + updateConnectivityState(resource); + break; + + case SCENE: + updateState(CHANNEL_2_SCENE, resource.getSceneState(), fullUpdate); + break; + + default: + return false; + } + if (thisResource.getType() == ResourceType.DEVICE) { + updateState(CHANNEL_2_LAST_UPDATED, new DateTimeType(), fullUpdate); + } + return true; + } + + /** + * Check the Zigbee connectivity and set the thing online status accordingly. If the thing is offline then set all + * its channel states to undefined, otherwise execute a refresh command to update channels to the latest current + * state. + * + * @param resource a Resource that potentially contains the Zigbee connectivity state. + */ + private void updateConnectivityState(Resource resource) { + ZigbeeStatus zigbeeStatus = resource.getZigbeeStatus(); + if (Objects.nonNull(zigbeeStatus)) { + logger.debug("{} -> updateConnectivityState() thingStatus:{}, zigbeeStatus:{}", resourceId, + thing.getStatus(), zigbeeStatus); + hasConnectivityIssue = zigbeeStatus != ZigbeeStatus.CONNECTED; + if (hasConnectivityIssue) { + if (thing.getStatusInfo().getStatusDetail() != ThingStatusDetail.COMMUNICATION_ERROR) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, + "@text/offline.api2.comm-error.zigbee-connectivity-issue"); + supportedChannelIdSet.forEach(channelId -> updateState(channelId, UnDefType.UNDEF)); + } + } else if (thing.getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + // issue REFRESH command to update all channels + Channel lastUpdateChannel = thing.getChannel(CHANNEL_2_LAST_UPDATED); + if (Objects.nonNull(lastUpdateChannel)) { + handleCommand(lastUpdateChannel.getUID(), RefreshType.REFRESH); + } + } + } + } + + /** + * Get all resources needed for building the thing state. Build the forward / reverse contributor lookup maps. Set + * up the final list of channels in the thing. + */ + private synchronized void updateDependencies() { + if (!disposing && !updateDependenciesDone) { + logger.debug("{} -> updateDependencies()", resourceId); + try { + if (!updatePropertiesDone) { + logger.debug("{} -> updateDependencies() properties not initialized", resourceId); + return; + } + if (!updateSceneContributorsDone && !updateSceneContributors()) { + logger.debug("{} -> updateDependencies() scenes not initialized", resourceId); + return; + } + updateLookups(); + updateServiceContributors(); + updateChannelList(); + updateChannelItemLinksFromLegacy(); + if (!hasConnectivityIssue) { + updateStatus(ThingStatus.ONLINE); + } + updateDependenciesDone = true; + } catch (ApiException e) { + logger.debug("{} -> updateDependencies() {}", resourceId, e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (AssetNotLoadedException e) { + logger.debug("{} -> updateDependencies() {}", resourceId, e.getMessage(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.api2.conf-error.assets-not-loaded"); + } catch (InterruptedException e) { + } + } + } + + /** + * Process the incoming Resource to initialize the effects channel. + * + * @param resource a Resource possibly with an Effects element. + */ + public void updateEffectChannel(Resource resource) { + Effects effects = resource.getEffects(); + if (Objects.nonNull(effects)) { + List stateOptions = effects.getStatusValues().stream() + .map(effect -> EffectType.of(effect).name()).map(effectId -> new StateOption(effectId, effectId)) + .collect(Collectors.toList()); + if (!stateOptions.isEmpty()) { + stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_EFFECT), + stateOptions); + logger.debug("{} -> updateEffects() found {} effects", resourceId, stateOptions.size()); + } + } + } + + /** + * Update the light properties. + * + * @param resource a Resource object containing the property data. + */ + private synchronized void updateLightProperties(Resource resource) { + if (!disposing && !updateLightPropertiesDone) { + logger.debug("{} -> updateLightProperties()", resourceId); + + Dimming dimming = resource.getDimming(); + thing.setProperty(PROPERTY_DIMMING_RANGE, Objects.nonNull(dimming) ? dimming.toPropertyValue() : null); + + MirekSchema mirekSchema = resource.getMirekSchema(); + thing.setProperty(PROPERTY_COLOR_TEMP_RANGE, + Objects.nonNull(mirekSchema) ? mirekSchema.toPropertyValue() : null); + + ColorXy colorXy = resource.getColorXy(); + Gamut2 gamut = Objects.nonNull(colorXy) ? colorXy.getGamut2() : null; + thing.setProperty(PROPERTY_COLOR_GAMUT, Objects.nonNull(gamut) ? gamut.toPropertyValue() : null); + + updateLightPropertiesDone = true; + } + } + + /** + * Initialize the lookup maps of resources that contribute to the thing state. + */ + private void updateLookups() { + if (!disposing) { + logger.debug("{} -> updateLookups()", resourceId); + // get supported services + List services = thisResource.getServiceReferences(); + + // add supported services to contributorsCache + serviceContributorsCache.clear(); + serviceContributorsCache.putAll(services.stream() + .collect(Collectors.toMap(ResourceReference::getId, r -> new Resource(r.getType())))); + + // add supported services to commandResourceIds + commandResourceIds.clear(); + commandResourceIds.putAll(services.stream() // use a 'mergeFunction' to prevent duplicates + .collect(Collectors.toMap(ResourceReference::getType, ResourceReference::getId, (r1, r2) -> r1))); + } + } + + /** + * Update the primary device properties. + * + * @param resource a Resource object containing the property data. + */ + private synchronized void updateProperties(Resource resource) { + if (!disposing && !updatePropertiesDone) { + logger.debug("{} -> updateProperties()", resourceId); + Map properties = new HashMap<>(thing.getProperties()); + + // resource data + properties.put(PROPERTY_RESOURCE_TYPE, thisResource.getType().toString()); + properties.put(PROPERTY_RESOURCE_NAME, thisResource.getName()); + + // owner information + ResourceReference owner = thisResource.getOwner(); + if (Objects.nonNull(owner)) { + String ownerId = owner.getId(); + if (Objects.nonNull(ownerId)) { + properties.put(PROPERTY_OWNER, ownerId); + } + ResourceType ownerType = owner.getType(); + properties.put(PROPERTY_OWNER_TYPE, ownerType.toString()); + } + + // metadata + MetaData metaData = thisResource.getMetaData(); + if (Objects.nonNull(metaData)) { + properties.put(PROPERTY_RESOURCE_ARCHETYPE, metaData.getArchetype().toString()); + } + + // product data + ProductData productData = thisResource.getProductData(); + if (Objects.nonNull(productData)) { + // standard properties + properties.put(PROPERTY_RESOURCE_ID, resourceId); + properties.put(Thing.PROPERTY_MODEL_ID, productData.getModelId()); + properties.put(Thing.PROPERTY_VENDOR, productData.getManufacturerName()); + properties.put(Thing.PROPERTY_FIRMWARE_VERSION, productData.getSoftwareVersion()); + String hardwarePlatformType = productData.getHardwarePlatformType(); + if (Objects.nonNull(hardwarePlatformType)) { + properties.put(Thing.PROPERTY_HARDWARE_VERSION, hardwarePlatformType); + } + + // hue specific properties + properties.put(PROPERTY_PRODUCT_NAME, productData.getProductName()); + properties.put(PROPERTY_PRODUCT_ARCHETYPE, productData.getProductArchetype().toString()); + properties.put(PROPERTY_PRODUCT_CERTIFIED, productData.getCertified().toString()); + } + + thing.setProperties(properties); + updatePropertiesDone = true; + } + } + + /** + * Execute an HTTP GET command to fetch the resources data for the referenced resource. + * + * @param reference to the required resource. + * @throws ApiException if a communication error occurred. + * @throws AssetNotLoadedException if one of the assets is not loaded. + * @throws InterruptedException + */ + private void updateResource(ResourceReference reference) + throws ApiException, AssetNotLoadedException, InterruptedException { + if (!disposing) { + logger.debug("{} -> updateResource() from resource {}", resourceId, reference); + getBridgeHandler().getResources(reference).getResources().stream() + .forEach(resource -> onResource(resource)); + } + } + + /** + * Fetch the full list of scenes from the bridge, and call updateSceneContributors(List allScenes) + * + * @throws ApiException if a communication error occurred. + * @throws AssetNotLoadedException if one of the assets is not loaded. + * @throws InterruptedException + */ + public boolean updateSceneContributors() throws ApiException, AssetNotLoadedException, InterruptedException { + if (!disposing && !updateSceneContributorsDone) { + ResourceReference scenesReference = new ResourceReference().setType(ResourceType.SCENE); + updateSceneContributors(getBridgeHandler().getResources(scenesReference).getResources()); + } + return updateSceneContributorsDone; + } + + /** + * Process the incoming list of scene resources to find those scenes which contribute to this thing. And if there + * are any, include a scene channel in the supported channel list, and populate its respective state options. + * + * @param allScenes the full list of scene resources. + */ + public synchronized boolean updateSceneContributors(List allScenes) { + if (!disposing && !updateSceneContributorsDone) { + sceneContributorsCache.clear(); + sceneResourceIds.clear(); + + ResourceReference thisReference = getResourceReference(); + List scenes = allScenes.stream().filter(s -> thisReference.equals(s.getGroup())) + .collect(Collectors.toList()); + + if (!scenes.isEmpty()) { + sceneContributorsCache.putAll(scenes.stream().collect(Collectors.toMap(s -> s.getId(), s -> s))); + sceneResourceIds.putAll(scenes.stream().collect(Collectors.toMap(s -> s.getName(), s -> s.getId()))); + + State state = scenes.stream().filter(s -> s.getSceneActive().orElse(false)).map(s -> s.getSceneState()) + .findAny().orElse(UnDefType.UNDEF); + updateState(CHANNEL_2_SCENE, state, true); + + stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_SCENE), scenes + .stream().map(s -> s.getName()).map(n -> new StateOption(n, n)).collect(Collectors.toList())); + + logger.debug("{} -> updateSceneContributors() found {} scenes", resourceId, scenes.size()); + } + updateSceneContributorsDone = true; + } + return updateSceneContributorsDone; + } + + /** + * Execute a series of HTTP GET commands to fetch the resource data for all service resources that contribute to the + * thing state. + * + * @throws ApiException if a communication error occurred. + * @throws AssetNotLoadedException if one of the assets is not loaded. + * @throws InterruptedException + */ + private void updateServiceContributors() throws ApiException, AssetNotLoadedException, InterruptedException { + if (!disposing) { + logger.debug("{} -> updateServiceContributors() called for {} contributors", resourceId, + serviceContributorsCache.size()); + ResourceReference reference = new ResourceReference(); + for (var entry : serviceContributorsCache.entrySet()) { + updateResource(reference.setId(entry.getKey()).setType(entry.getValue().getType())); + } + } + } + + /** + * Update the channel state, and if appropriate add the channel ID to the set of supportedChannelIds. Calls either + * OH core updateState() or triggerChannel() methods depending on the channel kind. + * + * Note: the particular 'UnDefType.UNDEF' value of the state argument is used to specially indicate the undefined + * state, but yet that its channel shall nevertheless continue to be present in the thing. + * + * @param channelID the id of the channel. + * @param state the new state of the channel. + * @param fullUpdate if true always update the channel, otherwise only update if state is not 'UNDEF'. + */ + private void updateState(String channelID, State state, boolean fullUpdate) { + boolean isDefined = state != UnDefType.NULL; + Channel channel = thing.getChannel(channelID); + + if ((fullUpdate || isDefined) && Objects.nonNull(channel)) { + logger.debug("{} -> updateState() '{}' update with '{}' (fullUpdate:{}, isDefined:{})", resourceId, + channelID, state, fullUpdate, isDefined); + + switch (channel.getKind()) { + case STATE: + updateState(channelID, state); + break; + + case TRIGGER: + if (state instanceof DecimalType) { + triggerChannel(channelID, String.valueOf(((DecimalType) state).intValue())); + } + } + } + if (fullUpdate && isDefined) { + addSupportedChannel(channelID); + } + } + + /** + * Check if a PROPERTY_LEGACY_THING_UID value was set by the discovery process, and if so, clone the legacy thing's + * settings into this thing. + */ + private void updateThingFromLegacy() { + if (isInitialized()) { + logger.warn("Cannot update thing '{}' from legacy thing since handler already initialized.", + thing.getUID()); + return; + } + Map properties = thing.getProperties(); + String legacyThingUID = properties.get(PROPERTY_LEGACY_THING_UID); + if (Objects.nonNull(legacyThingUID)) { + Thing legacyThing = thingRegistry.get(new ThingUID(legacyThingUID)); + if (Objects.nonNull(legacyThing)) { + ThingBuilder editBuilder = editThing(); + + String location = legacyThing.getLocation(); + if (Objects.nonNull(location) && !location.isBlank()) { + editBuilder = editBuilder.withLocation(location); + } + + // save list of legacyLinkedChannelUIDs for use after channel list is initialised + legacyLinkedChannelUIDs.clear(); + legacyLinkedChannelUIDs.addAll(legacyThing.getChannels().stream().map(Channel::getUID) + .filter(uid -> REPLICATE_CHANNEL_ID_MAP.containsKey(uid.getId()) + && itemChannelLinkRegistry.isLinked(uid)) + .collect(Collectors.toList())); + + Map newProperties = new HashMap<>(properties); + newProperties.remove(PROPERTY_LEGACY_THING_UID); + + updateThing(editBuilder.withProperties(newProperties).build()); + } + } + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java index 5250e0b29..a574f8f09 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java @@ -180,7 +180,7 @@ public class HueLightHandler extends BaseThingHandler implements HueLightActions } } properties.put(PROPERTY_VENDOR, fullLight.getManufacturerName()); - properties.put(PRODUCT_NAME, fullLight.getProductName()); + properties.put(PROPERTY_PRODUCT_NAME, fullLight.getProductName()); String uniqueID = fullLight.getUniqueID(); if (uniqueID != null) { properties.put(UNIQUE_ID, uniqueID); diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueSensorHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueSensorHandler.java index 59b8bdc4e..bbd9b7f8d 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueSensorHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueSensorHandler.java @@ -118,7 +118,7 @@ public abstract class HueSensorHandler extends BaseThingHandler implements Senso properties.put(PROPERTY_MODEL_ID, modelId); } properties.put(PROPERTY_VENDOR, fullSensor.getManufacturerName()); - properties.put(PRODUCT_NAME, fullSensor.getProductName()); + properties.put(PROPERTY_PRODUCT_NAME, fullSensor.getProductName()); String uniqueID = fullSensor.getUniqueID(); if (uniqueID != null) { properties.put(UNIQUE_ID, uniqueID); diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties index 0467acfac..b43ed7f3b 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties @@ -33,15 +33,54 @@ thing-type.hue.0840.label = CLIP Generic Status Sensor thing-type.hue.0840.description = A generic sensor object for IP sensor use. thing-type.hue.0850.label = CLIP Generic Flag Sensor thing-type.hue.0850.description = A generic sensor object for IP sensor use. +thing-type.hue.bridge-api2.label = Hue API v2 Bridge +thing-type.hue.bridge-api2.description = The Hue Bridge represents a Philips Hue Bridge supporting API v2. thing-type.hue.bridge.label = Hue Bridge thing-type.hue.bridge.description = The Hue Bridge represents the Philips Hue Bridge. +thing-type.hue.device.label = Hue Device +thing-type.hue.device.description = A Hue API v2 device with channels depending on its actual capabilities. +thing-type.hue.device.channel.alert.description = Activate the alert for the light. +thing-type.hue.device.channel.color-xy-only.description = Set the color xy parameter of the light without changing other state parameters. +thing-type.hue.device.channel.dimming-only.description = Set the dimming parameter of the light without changing other state parameters. +thing-type.hue.device.channel.effect.description = Activate the effect for the light. +thing-type.hue.device.channel.light-level.description = Current light level. +thing-type.hue.device.channel.light-level-enabled.description = Light level sensor enabled. +thing-type.hue.device.channel.motion-enabled.description = Motion sensor enabled. +thing-type.hue.device.channel.on-off-only.description = Set the on/off parameter of the light without changing other state parameters. +thing-type.hue.device.channel.temperature.label = Temperature +thing-type.hue.device.channel.temperature.description = Temperature at the sensor location. +thing-type.hue.device.channel.temperature-enabled.description = Temperature sensor enabled. thing-type.hue.geofencesensor.label = Geofence Sensor thing-type.hue.geofencesensor.description = A sensor providing geofence based presence detection. thing-type.hue.group.label = Hue Group thing-type.hue.group.description = A group of lights or a room that could be switched on and off. +thing-type.hue.room.label = Hue Room Light Group +thing-type.hue.room.description = A group of Hue API v2 lights that are located in a single room. +thing-type.hue.room.channel.alert.description = Activate the alert for the group of lights in the room. +thing-type.hue.room.channel.brightness.description = Controls the brightness and switches on/off the group of lights in the room. +thing-type.hue.room.channel.dimming-only.description = Set the dimming parameter of the group of lights in the room without changing other state parameters. +thing-type.hue.room.channel.on-off-only.description = Set the on/off parameter of the group of lights in the room without changing other state parameters. +thing-type.hue.room.channel.scene.description = Activate the scene for the group of lights in the room. +thing-type.hue.room.channel.switch.description = Switch on/off the group of lights in the room. +thing-type.hue.zone.label = Hue Zone Light Group +thing-type.hue.zone.description = A group of Hue API v2 lights that are located in a zone. +thing-type.hue.zone.channel.alert.description = Activate the alert for the group of lights in the zone. +thing-type.hue.zone.channel.brightness.description = Controls the brightness and switches on/off the group of lights in the zone. +thing-type.hue.zone.channel.dimming-only.description = Set the dimming parameter of the group of lights in the zone without changing other state parameters. +thing-type.hue.zone.channel.on-off-only.description = Set the on/off parameter of the group of lights in the zone without changing other state parameters. +thing-type.hue.zone.channel.scene.description = Activate the scene for the group of lights in the zone. +thing-type.hue.zone.channel.switch.description = Switch on/off the group of lights in the zone. # thing types config +thing-type.config.hue.bridge-api2.applicationKey.label = Application Key +thing-type.config.hue.bridge-api2.applicationKey.description = A registered Hue Bridge application key that allows access to the API. +thing-type.config.hue.bridge-api2.checkMinutes.label = Connection Check Interval +thing-type.config.hue.bridge-api2.checkMinutes.description = Minutes between retrying the HTTP 2 and SSE connections. Default is 60. +thing-type.config.hue.bridge-api2.ipAddress.label = Network Address +thing-type.config.hue.bridge-api2.ipAddress.description = Network address of the Hue Bridge. +thing-type.config.hue.bridge-api2.useSelfSignedCertificate.label = Use Self-Signed Certificate +thing-type.config.hue.bridge-api2.useSelfSignedCertificate.description = Use self-signed certificate for HTTPS connection to Hue Bridge. thing-type.config.hue.bridge.ipAddress.label = Network Address thing-type.config.hue.bridge.ipAddress.description = Network address of the Hue Bridge. thing-type.config.hue.bridge.pollingInterval.label = Polling Interval @@ -58,6 +97,8 @@ thing-type.config.hue.bridge.useSelfSignedCertificate.label = Use Self-Signed Ce thing-type.config.hue.bridge.useSelfSignedCertificate.description = Use self-signed certificate for HTTPS connection to Hue Bridge. thing-type.config.hue.bridge.userName.label = Username thing-type.config.hue.bridge.userName.description = Name of a registered Hue Bridge user, that allows to access the API. +thing-type.config.hue.device.resourceId.label = Resource ID +thing-type.config.hue.device.resourceId.description = Unique Resource ID of the device in the Hue bridge thing-type.config.hue.group.groupId.label = Group ID thing-type.config.hue.group.groupId.description = The group identifier identifies one certain Hue group or room. thing-type.config.hue.lightlevelsensor.tholddark.label = Threshold Dark @@ -68,14 +109,24 @@ thing-type.config.hue.presencesensor.sensitivity.label = Sensitivity thing-type.config.hue.presencesensor.sensitivity.description = The current sensitivity of the presence sensor. Cannot exceed maximum sensitivity. thing-type.config.hue.presencesensor.sensitivitymax.label = Maximum Sensitivity thing-type.config.hue.presencesensor.sensitivitymax.description = The maximum sensitivity of the presence sensor. +thing-type.config.hue.room.resourceId.label = Resource ID +thing-type.config.hue.room.resourceId.description = Unique Resource ID of the room in the Hue bridge +thing-type.config.hue.zone.resourceId.label = Resource ID +thing-type.config.hue.zone.resourceId.description = Unique Resource ID of the zone in the Hue bridge # channel types +channel-type.hue.advanced-brightness.label = Dimming Only +channel-type.hue.advanced-color.label = Color XY Only +channel-type.hue.advanced-power.label = On/Off Only +channel-type.hue.alert-v2.label = Alert channel-type.hue.alert.label = Alert channel-type.hue.alert.description = The alert channel allows a temporary change to the bulb’s state. channel-type.hue.alert.state.option.NONE = None channel-type.hue.alert.state.option.SELECT = Alert channel-type.hue.alert.state.option.LSELECT = Long Alert +channel-type.hue.button-last-event.label = Button Last Event +channel-type.hue.button-last-event.description = Numeric code (e.g. 1003) representing the last push button event. channel-type.hue.dark.label = Dark channel-type.hue.dark.description = Light level is below the darkness threshold. channel-type.hue.daylight.label = Daylight @@ -100,19 +151,29 @@ channel-type.hue.dimmer_switch.state.option.4002 = Off (Short Released) channel-type.hue.dimmer_switch.state.option.4003 = Off (Long Released) channel-type.hue.dimmer_switch_event.label = Dimmer Switch Event channel-type.hue.dimmer_switch_event.description = Triggers when a button is pressed on the dimmer switch. +channel-type.hue.dynamics.label = Dynamics +channel-type.hue.dynamics.description = The duration (ms) of dynamic transitions between light or scene states. +channel-type.hue.effect-v2.label = Effect channel-type.hue.effect.label = Color Loop channel-type.hue.effect.description = The effect channel allows putting the bulb in a color looping mode. channel-type.hue.flag.label = Flag channel-type.hue.flag.description = Flag of CLIP sensor. channel-type.hue.illuminance.label = Illuminance channel-type.hue.illuminance.description = Current illuminance. +channel-type.hue.last-updated-v2.label = Last Updated +channel-type.hue.last-updated-v2.description = The date and time when the thing was last updated. +channel-type.hue.last-updated-v2.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS channel-type.hue.last_updated.label = Last Updated channel-type.hue.last_updated.description = The date and time when the sensor was last updated. channel-type.hue.last_updated.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS channel-type.hue.light_level.label = Light Level channel-type.hue.light_level.description = Current light level. +channel-type.hue.rotary-steps.label = Rotary Steps +channel-type.hue.rotary-steps.description = The last 'steps' value (e.g. +/-30) of the rotary dial. +channel-type.hue.scene-v2.label = Scene channel-type.hue.scene.label = Scene channel-type.hue.scene.description = The scene channel allows recalling a scene to all lights that belong to the scene. +channel-type.hue.sensor-enabled.label = Sensor Enabled channel-type.hue.status.label = Status channel-type.hue.status.description = Status of CLIP sensor. channel-type.hue.tap_switch.label = Tap Switch State @@ -168,6 +229,22 @@ offline.light-removed = Hue Bridge reports light as removed. offline.sensor-removed = Hue Bridge reports sensor as removed. offline.group-removed = Hue Bridge reports group as removed. +# api v2 offline configuration error descriptions + +offline.api2.comm-error.zigbee-connectivity-issue = Zigbee connectivity issue. +offline.api2.comm-error.exception = An unexpected exception '{}' occurred. +offline.api2.conf-error.certificate-load = Certificate loading failed. Please check your configuration settings (network address, type of certificate). +offline.api2.conf-error.assets-not-loaded = Bridge/Thing handler assets not loaded. +offline.api2.conf-error.press-pairing-button = Not authenticated. Press pairing button on the Hue Bridge or set a valid application key in configuration. +offline.api2.conf-error.read-only = Configuration update failed. Please update the configuration manually. +offline.api2.conf-error.clip2-not-supported = The Hue Bridge does not support API v2. +offline.api2.conf-error.resource-id-bad = Configuration resourceId is bad. +offline.api2.conf-error.not-authorized = The application key is not authorized. + +# scene channel description + +scene.channel.activate = Activate the scene ''{0}'' + # lightactions actionInputChannelLabel = Channel @@ -181,4 +258,15 @@ actionDesc = Send a light command with a custom fade time. # discovery results -discovery.group.all_lights.label = All lights +discovery.group.all-lights.label = All lights + +# api v2 dynamic actions + +dynamics.action.label = send a dynamic command +dynamics.action.description = Sends a dynamic command to a device, room or zone. +dynamics.channel.label = Target Channel +dynamics.channel.description = The channel ID of the channel to send the command to. +dynamics.command.label = Target Command +dynamics.command.description = The target command state for the light(s) to transition to. +dynamics.duration.label = Duration +dynamics.duration.description = The dynamic transition duration in ms. diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/Clip2Thing.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/Clip2Thing.xml new file mode 100644 index 000000000..d6e768fad --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/Clip2Thing.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + A Hue API v2 device with channels depending on its actual capabilities. + + + + + + + + Activate the alert for the light. + + + Activate the effect for the light. + + + + + + Motion sensor enabled. + + + Current light level. + + + Light level sensor enabled. + + + + Temperature at the sensor location. + + + Temperature sensor enabled. + + + + + + + + Set the color xy parameter of the light without changing other state parameters. + + + Set the dimming parameter of the light without changing other state parameters. + + + Set the on/off parameter of the light without changing other state parameters. + + + + resourceId + + + + + Unique Resource ID of the device in the Hue bridge + + + + + + + + + + + + + A group of Hue API v2 lights that are located in a single room. + + + + Controls the brightness and switches on/off the group of lights in the room. + + + Switch on/off the group of lights in the room. + + + Activate the scene for the group of lights in the room. + + + Activate the alert for the group of lights in the room. + + + + Set the dimming parameter of the group of lights in the room without changing other state parameters. + + + Set the on/off parameter of the group of lights in the room without changing other state + parameters. + + + + resourceId + + + + + Unique Resource ID of the room in the Hue bridge + + + + + + + + + + + + + A group of Hue API v2 lights that are located in a zone. + + + + Controls the brightness and switches on/off the group of lights in the zone. + + + Switch on/off the group of lights in the zone. + + + Activate the scene for the group of lights in the zone. + + + Activate the alert for the group of lights in the zone. + + + + Set the dimming parameter of the group of lights in the zone without changing other state parameters. + + + Set the on/off parameter of the group of lights in the zone without changing other state parameters. + + + + resourceId + + + + + Unique Resource ID of the zone in the Hue bridge + + + + + + diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/bridge.xml index e44c6f356..d2ea0f4be 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/bridge.xml @@ -62,4 +62,37 @@ + + + + The Hue Bridge represents a Philips Hue Bridge supporting API v2. + + serialNumber + + + + network-address + + Network address of the Hue Bridge. + + + password + + A registered Hue Bridge application key that allows access to the API. + + + + Minutes between retrying the HTTP 2 and SSE connections. Default is 60. + 60 + true + + + + Use self-signed certificate for HTTPS connection to Hue Bridge. + true + true + + + + diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml index b6930e534..6e51b1761 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml @@ -22,7 +22,7 @@ - + Number Current light level. @@ -180,4 +180,80 @@ The scene channel allows recalling a scene to all lights that belong to the scene. + + + + trigger + + Numeric code (e.g. 1003) representing the last push button event. + + + + + trigger + + The last 'steps' value (e.g. +/-30) of the rotary dial. + + + + + Switch + + Lock + + + + String + + MediaControl + + + + + DateTime + + The date and time when the thing was last updated. + Time + + + + + String + + + + + + String + + + + + + Number:Time + + The duration (ms) of dynamic transitions between light or scene states. + Time + + + + + Color + + ColorLight + + + + Dimmer + + Light + + + + + Switch + + Switch + + diff --git a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java new file mode 100644 index 000000000..fb32ce574 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java @@ -0,0 +1,600 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.clip2; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.hue.internal.dto.clip2.ActionEntry; +import org.openhab.binding.hue.internal.dto.clip2.Alerts; +import org.openhab.binding.hue.internal.dto.clip2.Button; +import org.openhab.binding.hue.internal.dto.clip2.Dimming; +import org.openhab.binding.hue.internal.dto.clip2.Event; +import org.openhab.binding.hue.internal.dto.clip2.LightLevel; +import org.openhab.binding.hue.internal.dto.clip2.MetaData; +import org.openhab.binding.hue.internal.dto.clip2.MirekSchema; +import org.openhab.binding.hue.internal.dto.clip2.Motion; +import org.openhab.binding.hue.internal.dto.clip2.Power; +import org.openhab.binding.hue.internal.dto.clip2.ProductData; +import org.openhab.binding.hue.internal.dto.clip2.RelativeRotary; +import org.openhab.binding.hue.internal.dto.clip2.Resource; +import org.openhab.binding.hue.internal.dto.clip2.ResourceReference; +import org.openhab.binding.hue.internal.dto.clip2.Resources; +import org.openhab.binding.hue.internal.dto.clip2.Rotation; +import org.openhab.binding.hue.internal.dto.clip2.RotationEvent; +import org.openhab.binding.hue.internal.dto.clip2.Temperature; +import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType; +import org.openhab.binding.hue.internal.dto.clip2.enums.Archetype; +import org.openhab.binding.hue.internal.dto.clip2.enums.BatteryStateType; +import org.openhab.binding.hue.internal.dto.clip2.enums.ButtonEventType; +import org.openhab.binding.hue.internal.dto.clip2.enums.DirectionType; +import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType; +import org.openhab.binding.hue.internal.dto.clip2.enums.RotationEventType; +import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus; +import org.openhab.binding.hue.internal.dto.clip2.helper.Setters; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.openhab.core.util.ColorUtil; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +/** + * JUnit test for CLIP 2 DTOs. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +class Clip2DtoTest { + + private static final Gson GSON = new Gson(); + private static final Double MINIMUM_DIMMING_LEVEL = Double.valueOf(12.34f); + + /** + * Load the test JSON payload string from a file + */ + private String load(String fileName) { + try (FileReader file = new FileReader(String.format("src/test/resources/%s.json", fileName)); + BufferedReader reader = new BufferedReader(file)) { + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line).append("\n"); + } + return builder.toString(); + } catch (IOException e) { + fail(e.getMessage()); + } + return ""; + } + + @Test + void testButton() { + String json = load(ResourceType.BUTTON.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(43, list.size()); + Resource item = list.get(0); + assertEquals(ResourceType.BUTTON, item.getType()); + Button button = item.getButton(); + assertNotNull(button); + assertEquals(ButtonEventType.SHORT_RELEASE, button.getLastEvent()); + } + + @Test + void testDevice() { + String json = load(ResourceType.DEVICE.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(34, list.size()); + boolean itemFound = false; + for (Resource item : list) { + assertEquals(ResourceType.DEVICE, item.getType()); + ProductData productData = item.getProductData(); + assertNotNull(productData); + if (productData.getProductArchetype() == Archetype.BRIDGE_V2) { + itemFound = true; + assertEquals("BSB002", productData.getModelId()); + assertEquals("Signify Netherlands B.V.", productData.getManufacturerName()); + assertEquals("Philips hue", productData.getProductName()); + assertNull(productData.getHardwarePlatformType()); + assertTrue(productData.getCertified()); + assertEquals("1.53.1953188020", productData.getSoftwareVersion()); + break; + } + } + assertTrue(itemFound); + } + + @Test + void testDevicePower() { + String json = load(ResourceType.DEVICE_POWER.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(16, list.size()); + Resource item = list.get(0); + assertEquals(ResourceType.DEVICE_POWER, item.getType()); + Power power = item.getPowerState(); + assertNotNull(power); + assertEquals(60, power.getBatteryLevel()); + assertEquals(BatteryStateType.NORMAL, power.getBatteryState()); + } + + @Test + void testGroupedLight() { + String json = load(ResourceType.GROUPED_LIGHT.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(15, list.size()); + int itemsFound = 0; + for (Resource item : list) { + assertEquals(ResourceType.GROUPED_LIGHT, item.getType()); + Alerts alert; + switch (item.getId()) { + case "db4fd630-3798-40de-b642-c1ef464bf770": + itemsFound++; + assertEquals(OnOffType.OFF, item.getOnOffState()); + assertEquals(PercentType.ZERO, item.getBrightnessState()); + alert = item.getAlerts(); + assertNotNull(alert); + for (ActionType actionValue : alert.getActionValues()) { + assertEquals(ActionType.BREATHE, actionValue); + } + break; + case "9228d710-3c54-4ae4-8c88-bfe57d8fd220": + itemsFound++; + assertEquals(OnOffType.ON, item.getOnOffState()); + assertEquals(PercentType.HUNDRED, item.getBrightnessState()); + alert = item.getAlerts(); + assertNotNull(alert); + for (ActionType actionValue : alert.getActionValues()) { + assertEquals(ActionType.BREATHE, actionValue); + } + break; + default: + } + } + assertEquals(2, itemsFound); + } + + @Test + void testLight() { + String json = load(ResourceType.LIGHT.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(17, list.size()); + int itemFoundCount = 0; + for (Resource item : list) { + assertEquals(ResourceType.LIGHT, item.getType()); + MetaData metaData = item.getMetaData(); + assertNotNull(metaData); + String name = metaData.getName(); + assertNotNull(name); + State state; + if (name.contains("Bay Window Lamp")) { + itemFoundCount++; + assertEquals(ResourceType.LIGHT, item.getType()); + assertEquals(OnOffType.OFF, item.getOnOffState()); + state = item.getBrightnessState(); + assertTrue(state instanceof PercentType); + assertEquals(0, ((PercentType) state).doubleValue(), 0.1); + item.setOnOff(OnOffType.ON); + state = item.getBrightnessState(); + assertTrue(state instanceof PercentType); + assertEquals(93.0, ((PercentType) state).doubleValue(), 0.1); + assertEquals(UnDefType.UNDEF, item.getColorTemperaturePercentState()); + state = item.getColorState(); + assertTrue(state instanceof HSBType); + double[] xy = ColorUtil.hsbToXY((HSBType) state); + assertEquals(0.6367, xy[0], 0.01); // note: rounding errors !! + assertEquals(0.3503, xy[1], 0.01); // note: rounding errors !! + assertEquals(item.getBrightnessState(), ((HSBType) state).getBrightness()); + Alerts alert = item.getAlerts(); + assertNotNull(alert); + for (ActionType actionValue : alert.getActionValues()) { + assertEquals(ActionType.BREATHE, actionValue); + } + } + if (name.contains("Table Lamp A")) { + itemFoundCount++; + assertEquals(ResourceType.LIGHT, item.getType()); + assertEquals(OnOffType.OFF, item.getOnOffState()); + state = item.getBrightnessState(); + assertTrue(state instanceof PercentType); + assertEquals(0, ((PercentType) state).doubleValue(), 0.1); + item.setOnOff(OnOffType.ON); + state = item.getBrightnessState(); + assertTrue(state instanceof PercentType); + assertEquals(56.7, ((PercentType) state).doubleValue(), 0.1); + MirekSchema mirekSchema = item.getMirekSchema(); + assertNotNull(mirekSchema); + assertEquals(153, mirekSchema.getMirekMinimum()); + assertEquals(454, mirekSchema.getMirekMaximum()); + + // test color temperature percent value on light's own scale + state = item.getColorTemperaturePercentState(); + assertTrue(state instanceof PercentType); + assertEquals(96.3, ((PercentType) state).doubleValue(), 0.1); + state = item.getColorTemperatureAbsoluteState(); + assertTrue(state instanceof QuantityType); + assertEquals(2257.3, ((QuantityType) state).doubleValue(), 0.1); + + // test color temperature percent value on the default (full) scale + MirekSchema temp = item.getMirekSchema(); + item.setMirekSchema(MirekSchema.DEFAULT_SCHEMA); + state = item.getColorTemperaturePercentState(); + assertTrue(state instanceof PercentType); + assertEquals(83.6, ((PercentType) state).doubleValue(), 0.1); + state = item.getColorTemperatureAbsoluteState(); + assertTrue(state instanceof QuantityType); + assertEquals(2257.3, ((QuantityType) state).doubleValue(), 0.1); + item.setMirekSchema(temp); + + // change colour temperature percent to zero + Setters.setColorTemperaturePercent(item, PercentType.ZERO, null); + assertEquals(PercentType.ZERO, item.getColorTemperaturePercentState()); + state = item.getColorTemperatureAbsoluteState(); + assertTrue(state instanceof QuantityType); + assertEquals(6535.9, ((QuantityType) state).doubleValue(), 0.1); + + // change colour temperature percent to 100 + Setters.setColorTemperaturePercent(item, PercentType.HUNDRED, null); + assertEquals(PercentType.HUNDRED, item.getColorTemperaturePercentState()); + state = item.getColorTemperatureAbsoluteState(); + assertTrue(state instanceof QuantityType); + assertEquals(2202.6, ((QuantityType) state).doubleValue(), 0.1); + + // change colour temperature kelvin to 4000 K + Setters.setColorTemperatureAbsolute(item, QuantityType.valueOf("4000 K"), null); + state = item.getColorTemperaturePercentState(); + assertTrue(state instanceof PercentType); + assertEquals(32.2, ((PercentType) state).doubleValue(), 0.1); + assertEquals(QuantityType.valueOf("4000 K"), item.getColorTemperatureAbsoluteState()); + + assertEquals(UnDefType.NULL, item.getColorState()); + Alerts alert = item.getAlerts(); + assertNotNull(alert); + for (ActionType actionValue : alert.getActionValues()) { + assertEquals(ActionType.BREATHE, actionValue); + } + } + } + assertEquals(2, itemFoundCount); + } + + @Test + void testLightLevel() { + String json = load(ResourceType.LIGHT_LEVEL.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(1, list.size()); + Resource item = list.get(0); + assertEquals(ResourceType.LIGHT_LEVEL, item.getType()); + Boolean enabled = item.getEnabled(); + assertNotNull(enabled); + assertTrue(enabled); + LightLevel lightLevel = item.getLightLevel(); + assertNotNull(lightLevel); + assertEquals(12725, lightLevel.getLightLevel()); + assertTrue(lightLevel.isLightLevelValid()); + } + + @Test + void testRelativeRotary() { + String json = load(ResourceType.RELATIVE_ROTARY.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(1, list.size()); + Resource item = list.get(0); + assertEquals(ResourceType.RELATIVE_ROTARY, item.getType()); + RelativeRotary relativeRotary = item.getRelativeRotary(); + assertNotNull(relativeRotary); + RotationEvent rotationEvent = relativeRotary.getLastEvent(); + assertNotNull(rotationEvent); + assertEquals(RotationEventType.REPEAT, rotationEvent.getAction()); + Rotation rotation = rotationEvent.getRotation(); + assertNotNull(rotation); + assertEquals(DirectionType.CLOCK_WISE, rotation.getDirection()); + assertEquals(400, rotation.getDuration()); + assertEquals(30, rotation.getSteps()); + assertEquals(new DecimalType(30), relativeRotary.getStepsState()); + assertEquals(new StringType(ButtonEventType.REPEAT.name()), relativeRotary.getActionState()); + } + + @Test + void testResourceMerging() { + // create resource one + Resource one = new Resource(ResourceType.LIGHT).setId("AARDVARK"); + assertNotNull(one); + // preset the minimum dimming level + try { + Dimming dimming = new Dimming().setMinimumDimmingLevel(MINIMUM_DIMMING_LEVEL); + Field dimming2 = one.getClass().getDeclaredField("dimming"); + dimming2.setAccessible(true); + dimming2.set(one, dimming); + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { + fail(); + } + Setters.setColorXy(one, HSBType.RED, null); + Setters.setDimming(one, PercentType.HUNDRED, null); + assertTrue(one.getColorState() instanceof HSBType); + assertEquals(PercentType.HUNDRED, one.getBrightnessState()); + assertTrue(HSBType.RED.closeTo((HSBType) one.getColorState(), 0.01)); + + // switching off should change HSB and Brightness + one.setOnOff(OnOffType.OFF); + assertEquals(0, ((HSBType) one.getColorState()).getBrightness().doubleValue(), 0.01); + assertEquals(PercentType.ZERO, one.getBrightnessState()); + one.setOnOff(OnOffType.ON); + + // setting brightness to zero should change it to the minimum dimming level + Setters.setDimming(one, PercentType.ZERO, null); + assertEquals(MINIMUM_DIMMING_LEVEL, ((HSBType) one.getColorState()).getBrightness().doubleValue(), 0.01); + assertEquals(MINIMUM_DIMMING_LEVEL, ((PercentType) one.getBrightnessState()).doubleValue(), 0.01); + one.setOnOff(OnOffType.ON); + + // null its Dimming field + try { + Field dimming = one.getClass().getDeclaredField("dimming"); + dimming.setAccessible(true); + dimming.set(one, null); + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { + fail(); + } + + // confirm that brightness is no longer valid, and therefore that color has also changed + assertEquals(UnDefType.NULL, one.getBrightnessState()); + assertTrue(one.getColorState() instanceof HSBType); + assertTrue((new HSBType(DecimalType.ZERO, PercentType.HUNDRED, new PercentType(50))) + .closeTo((HSBType) one.getColorState(), 0.01)); + + PercentType testBrightness = new PercentType(42); + + // create resource two + Resource two = new Resource(ResourceType.DEVICE).setId("ALLIGATOR"); + assertNotNull(two); + Setters.setDimming(two, testBrightness, null); + assertEquals(UnDefType.NULL, two.getColorState()); + assertEquals(testBrightness, two.getBrightnessState()); + + // merge two => one + Setters.setResource(one, two); + + // confirm that brightness and color are both once more valid + assertEquals("AARDVARK", one.getId()); + assertEquals(ResourceType.LIGHT, one.getType()); + assertEquals(testBrightness, one.getBrightnessState()); + assertTrue(one.getColorState() instanceof HSBType); + assertTrue((new HSBType(DecimalType.ZERO, PercentType.HUNDRED, testBrightness)) + .closeTo((HSBType) one.getColorState(), 0.01)); + } + + @Test + void testRoomGroup() { + String json = load(ResourceType.ROOM.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(6, list.size()); + Resource item = list.get(0); + assertEquals(ResourceType.ROOM, item.getType()); + List children = item.getChildren(); + assertEquals(2, children.size()); + ResourceReference child = children.get(0); + assertNotNull(child); + assertEquals("0d47bd3d-d82b-4a21-893c-299bff18e22a", child.getId()); + assertEquals(ResourceType.DEVICE, child.getType()); + List services = item.getServiceReferences(); + assertEquals(1, services.size()); + ResourceReference service = services.get(0); + assertNotNull(service); + assertEquals("08947162-67be-4ed5-bfce-f42dade42416", service.getId()); + assertEquals(ResourceType.GROUPED_LIGHT, service.getType()); + } + + @Test + void testScene() { + String json = load(ResourceType.SCENE.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(123, list.size()); + Resource item = list.get(0); + List actions = item.getActions(); + assertNotNull(actions); + assertEquals(3, actions.size()); + ActionEntry actionEntry = actions.get(0); + assertNotNull(actionEntry); + Resource action = actionEntry.getAction(); + assertNotNull(action); + assertEquals(OnOffType.ON, action.getOnOffState()); + } + + @Test + void testSensor2Motion() { + String json = load(ResourceType.MOTION.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(1, list.size()); + Resource item = list.get(0); + assertEquals(ResourceType.MOTION, item.getType()); + Boolean enabled = item.getEnabled(); + assertNotNull(enabled); + assertTrue(enabled); + Motion motion = item.getMotion(); + assertNotNull(motion); + assertTrue(motion.isMotion()); + assertTrue(motion.isMotionValid()); + } + + @Test + void testSetGetPureColors() { + Resource resource = new Resource(ResourceType.LIGHT); + assertNotNull(resource); + + HSBType cyan = new HSBType("180,100,100"); + HSBType yellow = new HSBType("60,100,100"); + HSBType magenta = new HSBType("300,100,100"); + + for (HSBType color : Set.of(HSBType.WHITE, HSBType.RED, HSBType.GREEN, HSBType.BLUE, cyan, yellow, magenta)) { + Setters.setColorXy(resource, color, null); + State state = resource.getColorState(); + assertTrue(state instanceof HSBType); + assertTrue(color.closeTo((HSBType) state, 0.01)); + } + } + + @Test + void testSseLightOrGroupEvent() { + String json = load("event"); + List eventList = GSON.fromJson(json, Event.EVENT_LIST_TYPE); + assertNotNull(eventList); + assertEquals(3, eventList.size()); + Event event = eventList.get(0); + List resources = event.getData(); + assertEquals(9, resources.size()); + for (Resource r : resources) { + ResourceType type = r.getType(); + assertTrue(ResourceType.LIGHT == type || ResourceType.GROUPED_LIGHT == type); + } + } + + @Test + void testSseSceneEvent() { + String json = load("event"); + List eventList = GSON.fromJson(json, Event.EVENT_LIST_TYPE); + assertNotNull(eventList); + assertEquals(3, eventList.size()); + Event event = eventList.get(2); + List resources = event.getData(); + assertEquals(6, resources.size()); + Resource resource = resources.get(1); + assertEquals(ResourceType.SCENE, resource.getType()); + JsonObject status = resource.getStatus(); + assertNotNull(status); + JsonElement active = status.get("active"); + assertNotNull(active); + assertTrue(active.isJsonPrimitive()); + assertEquals("inactive", active.getAsString()); + Optional isActive = resource.getSceneActive(); + assertTrue(isActive.isPresent()); + assertEquals(Boolean.FALSE, isActive.get()); + } + + @Test + void testTemperature() { + String json = load(ResourceType.TEMPERATURE.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(1, list.size()); + Resource item = list.get(0); + assertEquals(ResourceType.TEMPERATURE, item.getType()); + Temperature temperature = item.getTemperature(); + assertNotNull(temperature); + assertEquals(17.2, temperature.getTemperature(), 0.1); + assertTrue(temperature.isTemperatureValid()); + } + + @Test + void testValidJson() { + for (ResourceType res : ResourceType.values()) { + if (!ResourceType.SSE_TYPES.contains(res)) { + try { + String file = res.name().toLowerCase(); + String json = load(file); + JsonElement jsonElement = JsonParser.parseString(json); + assertTrue(jsonElement.isJsonObject()); + } catch (JsonSyntaxException e) { + fail(res.name()); + } + } + } + } + + @Test + void testZigbeeStatus() { + String json = load(ResourceType.ZIGBEE_CONNECTIVITY.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(35, list.size()); + Resource item = list.get(0); + assertEquals(ResourceType.ZIGBEE_CONNECTIVITY, item.getType()); + ZigbeeStatus zigbeeStatus = item.getZigbeeStatus(); + assertNotNull(zigbeeStatus); + assertEquals("Connected", zigbeeStatus.toString()); + } + + @Test + void testZoneGroup() { + String json = load(ResourceType.ZONE.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(7, list.size()); + Resource item = list.get(0); + assertEquals(ResourceType.ZONE, item.getType()); + List children = item.getChildren(); + assertEquals(1, children.size()); + ResourceReference child = children.get(0); + assertNotNull(child); + assertEquals("bcad47a0-3f1f-498c-a8aa-3cf389965219", child.getId()); + assertEquals(ResourceType.LIGHT, child.getType()); + List services = item.getServiceReferences(); + assertEquals(1, services.size()); + ResourceReference service = services.get(0); + assertNotNull(service); + assertEquals("db4fd630-3798-40de-b642-c1ef464bf770", service.getId()); + assertEquals(ResourceType.GROUPED_LIGHT, service.getType()); + } +} diff --git a/bundles/org.openhab.binding.hue/src/test/resources/auth_v1.json b/bundles/org.openhab.binding.hue/src/test/resources/auth_v1.json new file mode 100644 index 000000000..dc0582057 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/auth_v1.json @@ -0,0 +1,7 @@ +{ + "errors": [ + { + "description": "Not Found" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/behavior_instance.json b/bundles/org.openhab.binding.hue/src/test/resources/behavior_instance.json new file mode 100644 index 000000000..2c2c4f4aa --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/behavior_instance.json @@ -0,0 +1,91 @@ +{ + "errors": [], + "data": [ + { + "configuration": { + "what": [ + { + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "recall": { + "rid": "11ac9c82-d031-43a6-a8d5-b6efdee72fe6", + "rtype": "scene" + } + }, + { + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "recall": { + "rid": "8b65c749-3ad8-435e-a7ed-94e3cc99e9d7", + "rtype": "scene" + } + } + ], + "when_constrained": { + "type": "nighttime" + }, + "where": [ + { + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + } + }, + { + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + } + } + ] + }, + "dependees": [ + { + "level": "critical", + "target": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "type": "ResourceDependee" + }, + { + "level": "critical", + "target": { + "rid": "11ac9c82-d031-43a6-a8d5-b6efdee72fe6", + "rtype": "scene" + }, + "type": "ResourceDependee" + }, + { + "level": "critical", + "target": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "type": "ResourceDependee" + }, + { + "level": "critical", + "target": { + "rid": "8b65c749-3ad8-435e-a7ed-94e3cc99e9d7", + "rtype": "scene" + }, + "type": "ResourceDependee" + } + ], + "enabled": true, + "id": "8d0ffbee-e24e-4d3e-b91a-5adc9ef5d49c", + "last_error": "", + "metadata": { + "name": "Coming home" + }, + "script_id": "fd60fcd1-4809-4813-b510-4a18856a595c", + "status": "running", + "type": "behavior_instance" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/behavior_script.json b/bundles/org.openhab.binding.hue/src/test/resources/behavior_script.json new file mode 100644 index 000000000..e1d415b7f --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/behavior_script.json @@ -0,0 +1,205 @@ +{ + "errors": [], + "data": [ + { + "configuration_schema": { + "$ref": "basic_goto_sleep_config.json#" + }, + "description": "Get ready for nice sleep.", + "id": "7e571ac6-f363-42e1-809a-4cbf6523ed72", + "metadata": { + "category": "automation", + "name": "Basic go to sleep routine" + }, + "state_schema": null, + "supported_features": [], + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "basic_wake_up_config.json#" + }, + "description": "Get your body in the mood to wake up by fading on the lights in the morning.", + "id": "ff8957e3-2eb9-4699-a0c8-ad2cb3ede704", + "metadata": { + "category": "automation", + "name": "Basic wake up routine" + }, + "state_schema": null, + "supported_features": [ + "style_sunrise", + "intensity" + ], + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "coming_home_config.json#" + }, + "description": "Automatically turn your lights to choosen light states, when you arrive at home.", + "id": "fd60fcd1-4809-4813-b510-4a18856a595c", + "metadata": { + "category": "automation", + "name": "Coming home" + }, + "state_schema": { + + }, + "supported_features": [], + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "leaving_home_config.json#" + }, + "description": "Automatically turn off your lights when you leave", + "id": "0194752a-2d53-4f92-8209-dfdc52745af3", + "metadata": { + "category": "automation", + "name": "Leaving home" + }, + "state_schema": { + + }, + "supported_features": [], + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "schedule_config.json#" + }, + "description": "Schedule turning on and off lights", + "id": "7238c707-8693-4f19-9095-ccdc1444d228", + "metadata": { + "category": "automation", + "name": "Schedule" + }, + "state_schema": { + + }, + "supported_features": [], + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "timer_config.json#" + }, + "description": "Countdown Timer", + "id": "e73bc72d-96b1-46f8-aa57-729861f80c78", + "metadata": { + "category": "automation", + "name": "Timers" + }, + "state_schema": { + "$ref": "timer_state.json#" + }, + "supported_features": [], + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "pm_config.json#" + }, + "description": "PM Automation", + "id": "db06cabc-c752-4904-9e8f-4ebe98feaa1a", + "max_number_instances": 1, + "metadata": { + "category": "automation", + "name": "PM" + }, + "state_schema": { + "$ref": "pm_state.json#" + }, + "supported_features": [], + "trigger_schema": { + "$ref": "pm_trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "lights_state_after_streaming_config.json#" + }, + "description": "State of lights in the entertainment group after streaming ends", + "id": "7719b841-6b3d-448d-a0e7-601ae9edb6a2", + "metadata": { + "category": "entertainment", + "name": "Light state after streaming" + }, + "state_schema": { + + }, + "supported_features": [], + "trigger_schema": { + + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "natural_light_config.json#" + }, + "description": "Natural light during the day", + "id": "a4260b49-0c69-4926-a29c-417f4a38a352", + "metadata": { + "category": "", + "name": "Natural Light" + }, + "state_schema": { + "$ref": "natural_light_state.json#" + }, + "supported_features": [], + "trigger_schema": { + "$ref": "natural_light_trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "config.json#" + }, + "description": "Tap Switch script", + "id": "f306f634-acdb-4dd6-bdf5-48dd626d667e", + "metadata": { + "category": "accessory", + "name": "Tap Switch" + }, + "state_schema": { + "$ref": "state.json#" + }, + "supported_features": [], + "trigger_schema": { + + }, + "type": "behavior_script", + "version": "0.0.1" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/bridge.json b/bundles/org.openhab.binding.hue/src/test/resources/bridge.json new file mode 100644 index 000000000..b52b6f363 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/bridge.json @@ -0,0 +1,18 @@ +{ + "errors": [], + "data": [ + { + "id": "703765c0-f78a-4aac-9458-f50c0b41e1d8", + "id_v1": "", + "owner": { + "rid": "f4c5c816-925b-4e22-a112-2b44a23f5613", + "rtype": "device" + }, + "bridge_id": "001788fffe2157c7", + "time_zone": { + "time_zone": "Europe/London" + }, + "type": "bridge" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/bridge_home.json b/bundles/org.openhab.binding.hue/src/test/resources/bridge_home.json new file mode 100644 index 000000000..a99a8cb9d --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/bridge_home.json @@ -0,0 +1,106 @@ +{ + "errors": [], + "data": [ + { + "id": "f467cdcc-405f-40ab-8db9-4664aa1c3d63", + "id_v1": "/groups/0", + "children": [ + { + "rid": "dcbc740d-1e4f-48aa-ad02-3e17f1f4eebb", + "rtype": "room" + }, + { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + { + "rid": "8cec1e2f-bcc9-45c9-a0aa-bc9c30c68b64", + "rtype": "room" + }, + { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + { + "rid": "b5fe0539-171c-4733-bf0b-244635a309be", + "rtype": "device" + }, + { + "rid": "a1155885-4bbe-469f-83bb-f964f8e13e82", + "rtype": "device" + }, + { + "rid": "a0509519-3ecb-47d0-9183-25db1e4ea2b2", + "rtype": "device" + }, + { + "rid": "112853f9-c4c4-4d65-ba96-b4c2ab26d94d", + "rtype": "device" + }, + { + "rid": "8c5b05ba-b4f4-47b2-8ba0-fc44363192bc", + "rtype": "device" + }, + { + "rid": "e130feac-3a5c-452e-a97d-5bca470783b3", + "rtype": "device" + }, + { + "rid": "81d9a9d5-228c-45df-828e-0d224929b3d1", + "rtype": "device" + }, + { + "rid": "68ff07d0-6543-4967-9889-e0bc0bc16c31", + "rtype": "device" + }, + { + "rid": "56b560bc-a127-4634-8d80-9946104a4028", + "rtype": "device" + }, + { + "rid": "0e22f8de-eff5-440a-a9ed-06d547d125d7", + "rtype": "device" + }, + { + "rid": "96bec26d-d7c6-4c00-98cd-6f96733296a0", + "rtype": "device" + }, + { + "rid": "f78c5b4b-2f52-4bc3-8097-1ddf97949cc5", + "rtype": "device" + }, + { + "rid": "431026fb-298c-4726-8ce4-47450fea13c4", + "rtype": "device" + }, + { + "rid": "b56191ea-8d18-4257-b5d2-cfc3fc86a1ee", + "rtype": "device" + }, + { + "rid": "e12b3d9c-0b23-4edd-a967-66f5b5cefa92", + "rtype": "device" + }, + { + "rid": "cfecbbd0-e918-42a2-b714-2bad33061d95", + "rtype": "device" + } + ], + "services": [ + { + "rid": "9228d710-3c54-4ae4-8c88-bfe57d8fd220", + "rtype": "grouped_light" + } + ], + "type": "bridge_home" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/button.json b/bundles/org.openhab.binding.hue/src/test/resources/button.json new file mode 100644 index 000000000..6284e2e44 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/button.json @@ -0,0 +1,551 @@ +{ + "errors": [], + "data": [ + { + "id": "d1ae958e-8908-449a-9897-7f10f9b8d4c2", + "id_v1": "/sensors/110", + "owner": { + "rid": "112853f9-c4c4-4d65-ba96-b4c2ab26d94d", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "button": { + "last_event": "short_release" + }, + "type": "button" + }, + { + "id": "a83354b7-bae5-4618-8c8d-079c83e0ca2b", + "id_v1": "/sensors/236", + "owner": { + "rid": "cfecbbd0-e918-42a2-b714-2bad33061d95", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "type": "button" + }, + { + "id": "e20d90ae-b2d8-40ea-9905-66bcde9fd863", + "id_v1": "/sensors/63", + "owner": { + "rid": "0e22f8de-eff5-440a-a9ed-06d547d125d7", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "button": { + "last_event": "short_release" + }, + "type": "button" + }, + { + "id": "4e61a948-22aa-4332-9b97-76245dd9fb87", + "id_v1": "/sensors/6", + "owner": { + "rid": "b56191ea-8d18-4257-b5d2-cfc3fc86a1ee", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "type": "button" + }, + { + "id": "2f78855c-717a-4b77-8513-dd68ca5337b8", + "id_v1": "/sensors/6", + "owner": { + "rid": "b56191ea-8d18-4257-b5d2-cfc3fc86a1ee", + "rtype": "device" + }, + "metadata": { + "control_id": 2 + }, + "type": "button" + }, + { + "id": "7c495cbd-4dd1-4f22-950c-4dd6017b81ec", + "id_v1": "/sensors/6", + "owner": { + "rid": "b56191ea-8d18-4257-b5d2-cfc3fc86a1ee", + "rtype": "device" + }, + "metadata": { + "control_id": 3 + }, + "type": "button" + }, + { + "id": "a15c9be9-14b9-4d98-8d9f-989d6cb54496", + "id_v1": "/sensors/6", + "owner": { + "rid": "b56191ea-8d18-4257-b5d2-cfc3fc86a1ee", + "rtype": "device" + }, + "metadata": { + "control_id": 4 + }, + "button": { + "last_event": "short_release" + }, + "type": "button" + }, + { + "id": "e1e6b2f9-3ace-41ae-b16c-e68f82b5a949", + "id_v1": "/sensors/24", + "owner": { + "rid": "b5fe0539-171c-4733-bf0b-244635a309be", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "type": "button" + }, + { + "id": "717c978e-383d-4900-ab2a-2685983f6174", + "id_v1": "/sensors/24", + "owner": { + "rid": "b5fe0539-171c-4733-bf0b-244635a309be", + "rtype": "device" + }, + "metadata": { + "control_id": 2 + }, + "type": "button" + }, + { + "id": "0456ba22-2f1d-4375-8e33-31465420cd96", + "id_v1": "/sensors/24", + "owner": { + "rid": "b5fe0539-171c-4733-bf0b-244635a309be", + "rtype": "device" + }, + "metadata": { + "control_id": 3 + }, + "type": "button" + }, + { + "id": "92c00fa4-ae65-462c-9130-a28f5bc52bd9", + "id_v1": "/sensors/24", + "owner": { + "rid": "b5fe0539-171c-4733-bf0b-244635a309be", + "rtype": "device" + }, + "metadata": { + "control_id": 4 + }, + "type": "button" + }, + { + "id": "85c18ad0-297d-4fb9-a33a-37f2da70752d", + "id_v1": "/sensors/118", + "owner": { + "rid": "a0509519-3ecb-47d0-9183-25db1e4ea2b2", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "button": { + "last_event": "short_release" + }, + "type": "button" + }, + { + "id": "79af0ff1-a4ed-476d-a381-eabd4d3ee6bd", + "id_v1": "/sensors/135", + "owner": { + "rid": "8c5b05ba-b4f4-47b2-8ba0-fc44363192bc", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "button": { + "last_event": "short_release" + }, + "type": "button" + }, + { + "id": "8a16c730-094a-4aa1-abd8-9cb758468d62", + "id_v1": "/sensors/8", + "owner": { + "rid": "68ff07d0-6543-4967-9889-e0bc0bc16c31", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "type": "button" + }, + { + "id": "3fed0654-68ec-43e1-b3db-f1a3eba076b9", + "id_v1": "/sensors/8", + "owner": { + "rid": "68ff07d0-6543-4967-9889-e0bc0bc16c31", + "rtype": "device" + }, + "metadata": { + "control_id": 2 + }, + "type": "button" + }, + { + "id": "df6e2ba8-94f4-43ce-9bec-72c252616062", + "id_v1": "/sensors/8", + "owner": { + "rid": "68ff07d0-6543-4967-9889-e0bc0bc16c31", + "rtype": "device" + }, + "metadata": { + "control_id": 3 + }, + "type": "button" + }, + { + "id": "a46f615d-6760-479c-ba6d-2837a18902e8", + "id_v1": "/sensors/8", + "owner": { + "rid": "68ff07d0-6543-4967-9889-e0bc0bc16c31", + "rtype": "device" + }, + "metadata": { + "control_id": 4 + }, + "type": "button" + }, + { + "id": "c34bb678-b910-4a06-8019-3e0ce922f17f", + "id_v1": "/sensors/18", + "owner": { + "rid": "96bec26d-d7c6-4c00-98cd-6f96733296a0", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "type": "button" + }, + { + "id": "286b1b18-ff43-40be-a2ff-b1edc24ecb7c", + "id_v1": "/sensors/18", + "owner": { + "rid": "96bec26d-d7c6-4c00-98cd-6f96733296a0", + "rtype": "device" + }, + "metadata": { + "control_id": 2 + }, + "type": "button" + }, + { + "id": "2be1abd3-00a4-4985-b2c9-9bdd4cbe1e4b", + "id_v1": "/sensors/18", + "owner": { + "rid": "96bec26d-d7c6-4c00-98cd-6f96733296a0", + "rtype": "device" + }, + "metadata": { + "control_id": 3 + }, + "type": "button" + }, + { + "id": "dc9f1653-11a8-4794-b706-464cf53e9722", + "id_v1": "/sensors/18", + "owner": { + "rid": "96bec26d-d7c6-4c00-98cd-6f96733296a0", + "rtype": "device" + }, + "metadata": { + "control_id": 4 + }, + "type": "button" + }, + { + "id": "ac4ebfeb-2045-4698-96de-0b6b5421a1d8", + "id_v1": "/sensors/4", + "owner": { + "rid": "81d9a9d5-228c-45df-828e-0d224929b3d1", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "type": "button" + }, + { + "id": "8a5159f1-93de-4c07-9d32-1fe555fde285", + "id_v1": "/sensors/4", + "owner": { + "rid": "81d9a9d5-228c-45df-828e-0d224929b3d1", + "rtype": "device" + }, + "metadata": { + "control_id": 2 + }, + "type": "button" + }, + { + "id": "14debc09-496e-4429-8c22-4975c135bbdf", + "id_v1": "/sensors/4", + "owner": { + "rid": "81d9a9d5-228c-45df-828e-0d224929b3d1", + "rtype": "device" + }, + "metadata": { + "control_id": 3 + }, + "type": "button" + }, + { + "id": "9672e2a9-a3a9-4aec-b31e-f2338114b22b", + "id_v1": "/sensors/4", + "owner": { + "rid": "81d9a9d5-228c-45df-828e-0d224929b3d1", + "rtype": "device" + }, + "metadata": { + "control_id": 4 + }, + "type": "button" + }, + { + "id": "f385a946-c638-4631-b6a6-c490cf809606", + "id_v1": "/sensors/245", + "owner": { + "rid": "56b560bc-a127-4634-8d80-9946104a4028", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "button": { + "last_event": "short_release" + }, + "type": "button" + }, + { + "id": "ba204102-de9f-479e-818a-02ed880caac2", + "id_v1": "/sensors/12", + "owner": { + "rid": "a1155885-4bbe-469f-83bb-f964f8e13e82", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "type": "button" + }, + { + "id": "0032a38b-cf67-478f-934a-520a156a2baf", + "id_v1": "/sensors/12", + "owner": { + "rid": "a1155885-4bbe-469f-83bb-f964f8e13e82", + "rtype": "device" + }, + "metadata": { + "control_id": 2 + }, + "type": "button" + }, + { + "id": "4ce58cbd-5d0f-45ec-920d-ad601fd9cc07", + "id_v1": "/sensors/12", + "owner": { + "rid": "a1155885-4bbe-469f-83bb-f964f8e13e82", + "rtype": "device" + }, + "metadata": { + "control_id": 3 + }, + "type": "button" + }, + { + "id": "813a331a-f26a-4ab2-948c-9b4c398939d0", + "id_v1": "/sensors/12", + "owner": { + "rid": "a1155885-4bbe-469f-83bb-f964f8e13e82", + "rtype": "device" + }, + "metadata": { + "control_id": 4 + }, + "button": { + "last_event": "short_release" + }, + "type": "button" + }, + { + "id": "91ba8839-2bac-4175-9f8c-ed192842d549", + "id_v1": "/sensors/20", + "owner": { + "rid": "e130feac-3a5c-452e-a97d-5bca470783b3", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "type": "button" + }, + { + "id": "f95addfc-2f7c-453f-924d-ba496e07e5f9", + "id_v1": "/sensors/20", + "owner": { + "rid": "e130feac-3a5c-452e-a97d-5bca470783b3", + "rtype": "device" + }, + "metadata": { + "control_id": 2 + }, + "type": "button" + }, + { + "id": "6615f1f1-f3f1-4a05-b8f7-581097458e34", + "id_v1": "/sensors/20", + "owner": { + "rid": "e130feac-3a5c-452e-a97d-5bca470783b3", + "rtype": "device" + }, + "metadata": { + "control_id": 3 + }, + "type": "button" + }, + { + "id": "b0d5a0af-31fd-4189-9150-c551ff9033d7", + "id_v1": "/sensors/20", + "owner": { + "rid": "e130feac-3a5c-452e-a97d-5bca470783b3", + "rtype": "device" + }, + "metadata": { + "control_id": 4 + }, + "button": { + "last_event": "short_release" + }, + "type": "button" + }, + { + "id": "9d3820db-4a20-4925-80f6-c74582ffb871", + "id_v1": "/sensors/26", + "owner": { + "rid": "e12b3d9c-0b23-4edd-a967-66f5b5cefa92", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "type": "button" + }, + { + "id": "a9e1a40b-a13d-4966-b0b1-2b0b5dfe1986", + "id_v1": "/sensors/26", + "owner": { + "rid": "e12b3d9c-0b23-4edd-a967-66f5b5cefa92", + "rtype": "device" + }, + "metadata": { + "control_id": 2 + }, + "type": "button" + }, + { + "id": "8ba063d7-df16-48f5-bb9b-f45849ec1bd3", + "id_v1": "/sensors/26", + "owner": { + "rid": "e12b3d9c-0b23-4edd-a967-66f5b5cefa92", + "rtype": "device" + }, + "metadata": { + "control_id": 3 + }, + "type": "button" + }, + { + "id": "4fc9dcd2-6f71-46c4-b3d0-5d7b496be6f9", + "id_v1": "/sensors/26", + "owner": { + "rid": "e12b3d9c-0b23-4edd-a967-66f5b5cefa92", + "rtype": "device" + }, + "metadata": { + "control_id": 4 + }, + "type": "button" + }, + { + "id": "824ea347-28d1-4e45-b886-1555a160190b", + "id_v1": "/sensors/14", + "owner": { + "rid": "431026fb-298c-4726-8ce4-47450fea13c4", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "button": { + "last_event": "short_release" + }, + "type": "button" + }, + { + "id": "801ede68-21b0-4e6e-98be-eb1a60557ac6", + "id_v1": "/sensors/14", + "owner": { + "rid": "431026fb-298c-4726-8ce4-47450fea13c4", + "rtype": "device" + }, + "metadata": { + "control_id": 2 + }, + "type": "button" + }, + { + "id": "1218d0c3-9a9c-4984-84a5-c12ad085ef2d", + "id_v1": "/sensors/14", + "owner": { + "rid": "431026fb-298c-4726-8ce4-47450fea13c4", + "rtype": "device" + }, + "metadata": { + "control_id": 3 + }, + "type": "button" + }, + { + "id": "eb2c07bc-1d09-4096-a044-dec29b40619a", + "id_v1": "/sensors/14", + "owner": { + "rid": "431026fb-298c-4726-8ce4-47450fea13c4", + "rtype": "device" + }, + "metadata": { + "control_id": 4 + }, + "type": "button" + }, + { + "id": "6bae5b99-349c-4045-8c8f-d4a60562b1d3", + "id_v1": "/sensors/124", + "owner": { + "rid": "f78c5b4b-2f52-4bc3-8097-1ddf97949cc5", + "rtype": "device" + }, + "metadata": { + "control_id": 1 + }, + "button": { + "last_event": "short_release" + }, + "type": "button" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/device.json b/bundles/org.openhab.binding.hue/src/test/resources/device.json new file mode 100644 index 000000000..c2205240a --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/device.json @@ -0,0 +1,1194 @@ +{ + "errors": [], + "data": [ + { + "id": "112853f9-c4c4-4d65-ba96-b4c2ab26d94d", + "id_v1": "/sensors/110", + "product_data": { + "model_id": "RDM001", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue wall switch module", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "1.0.3", + "hardware_platform_type": "100b-11c" + }, + "metadata": { + "name": "Kitchen Wallplate Switch", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "d1ae958e-8908-449a-9897-7f10f9b8d4c2", + "rtype": "button" + }, + { + "rid": "695b7480-42bc-44cf-bd63-1911f98fee34", + "rtype": "device_power" + }, + { + "rid": "3a9c03de-9305-443c-b075-39901e56f424", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "cfecbbd0-e918-42a2-b714-2bad33061d95", + "id_v1": "/sensors/236", + "product_data": { + "model_id": "ROM001", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue Smart button", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "2.47.8", + "hardware_platform_type": "100b-116" + }, + "metadata": { + "name": "Aquarium Button", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "a83354b7-bae5-4618-8c8d-079c83e0ca2b", + "rtype": "button" + }, + { + "rid": "65833fc9-1d08-4d03-b5ae-a03e7eca559c", + "rtype": "device_power" + }, + { + "rid": "13105daf-e295-4c52-bdba-46d03e431782", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "0e22f8de-eff5-440a-a9ed-06d547d125d7", + "id_v1": "/sensors/63", + "product_data": { + "model_id": "ROM001", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue Smart button", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "2.47.8", + "hardware_platform_type": "100b-116" + }, + "metadata": { + "name": "Front Bedroom Smart Button E", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "e20d90ae-b2d8-40ea-9905-66bcde9fd863", + "rtype": "button" + }, + { + "rid": "0fb39897-aa8b-49b4-8215-4e27651eb2f1", + "rtype": "device_power" + }, + { + "rid": "f4b23d32-0812-49fb-b134-0090dcfba2ba", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "01de467b-29a0-48fc-b711-fd9c079bd429", + "id_v1": "/lights/5", + "product_data": { + "model_id": "LWB010", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue white lamp", + "product_archetype": "classic_bulb", + "certified": true, + "software_version": "1.88.1", + "hardware_platform_type": "100b-10c" + }, + "metadata": { + "name": "Wall Lamp", + "archetype": "wall_shade" + }, + "identify": null, + "services": [ + { + "rid": "c42f8220-f232-4400-910c-943547513827", + "rtype": "light" + }, + { + "rid": "14439da7-d362-41c8-b716-04a3839e3be0", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "b56191ea-8d18-4257-b5d2-cfc3fc86a1ee", + "id_v1": "/sensors/6", + "product_data": { + "model_id": "RWL021", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue dimmer switch", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "1.1.28573", + "hardware_platform_type": "100b-109" + }, + "metadata": { + "name": "Living Room Dimmer Pad", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "4e61a948-22aa-4332-9b97-76245dd9fb87", + "rtype": "button" + }, + { + "rid": "2f78855c-717a-4b77-8513-dd68ca5337b8", + "rtype": "button" + }, + { + "rid": "7c495cbd-4dd1-4f22-950c-4dd6017b81ec", + "rtype": "button" + }, + { + "rid": "a15c9be9-14b9-4d98-8d9f-989d6cb54496", + "rtype": "button" + }, + { + "rid": "025bf7ea-5ba4-457f-9fa9-50a9a7886b0f", + "rtype": "device_power" + }, + { + "rid": "9415083c-14fe-4902-a194-376170caf3e3", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "18212397-8c4d-4373-8f59-c047b80994ac", + "id_v1": "/lights/16", + "product_data": { + "model_id": "TRADFRI transformer 30W", + "manufacturer_name": "IKEA of Sweden", + "product_name": "Dimmable light", + "product_archetype": "classic_bulb", + "certified": false, + "software_version": "1.2.245", + "hardware_platform_type": "117c-4101" + }, + "metadata": { + "name": "Worktop (L)", + "archetype": "wall_washer" + }, + "identify": null, + "services": [ + { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + { + "rid": "86837f1d-0bd9-41e6-a236-060eafc33d2f", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "b5fe0539-171c-4733-bf0b-244635a309be", + "id_v1": "/sensors/24", + "product_data": { + "model_id": "RWL021", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue dimmer switch", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "1.1.28573", + "hardware_platform_type": "100b-109" + }, + "metadata": { + "name": "Back Bedroom Dimmer Pad Door", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "e1e6b2f9-3ace-41ae-b16c-e68f82b5a949", + "rtype": "button" + }, + { + "rid": "717c978e-383d-4900-ab2a-2685983f6174", + "rtype": "button" + }, + { + "rid": "0456ba22-2f1d-4375-8e33-31465420cd96", + "rtype": "button" + }, + { + "rid": "92c00fa4-ae65-462c-9130-a28f5bc52bd9", + "rtype": "button" + }, + { + "rid": "42fe582f-b3f9-4ee9-905b-38357d07d3a5", + "rtype": "device_power" + }, + { + "rid": "050e2508-7bdb-428c-a070-cdd002b12822", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "0b4c9bdb-3f46-485b-8337-e5649c03b9e2", + "id_v1": "/lights/20", + "product_data": { + "model_id": "LOM003", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue Smart plug", + "product_archetype": "plug", + "certified": true, + "software_version": "1.93.6", + "hardware_platform_type": "100b-115" + }, + "metadata": { + "name": "Aquarium Light", + "archetype": "recessed_floor" + }, + "identify": { + + }, + "services": [ + { + "rid": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "rtype": "light" + }, + { + "rid": "50fe5b5f-fa28-4d05-97b2-b795bbbc6c21", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "adeb3425-6a6b-49a0-8262-129126de7941", + "id_v1": "/lights/10", + "product_data": { + "model_id": "LTW001", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue ambiance lamp", + "product_archetype": "sultan_bulb", + "certified": true, + "software_version": "67.88.1", + "hardware_platform_type": "100b-104" + }, + "metadata": { + "name": "Table Lamp A", + "archetype": "table_shade" + }, + "identify": { + + }, + "services": [ + { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + { + "rid": "4a40ae7d-3d12-44bf-adbe-bb23f417e41f", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "2cf59d54-8624-445f-9a01-aac19682b954", + "id_v1": "/lights/15", + "product_data": { + "model_id": "TRADFRI transformer 30W", + "manufacturer_name": "IKEA of Sweden", + "product_name": "Dimmable light", + "product_archetype": "classic_bulb", + "certified": false, + "software_version": "1.2.245", + "hardware_platform_type": "117c-4101" + }, + "metadata": { + "name": "Cabinet lights", + "archetype": "wall_washer" + }, + "identify": { + + }, + "services": [ + { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + { + "rid": "57b2f3bf-1fcf-442f-bcc9-ff93d0a266e1", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "a0509519-3ecb-47d0-9183-25db1e4ea2b2", + "id_v1": "/sensors/118", + "product_data": { + "model_id": "RDM001", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue wall switch module", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "1.0.3", + "hardware_platform_type": "100b-11c" + }, + "metadata": { + "name": "Conservatory Wallplate Switch", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "85c18ad0-297d-4fb9-a33a-37f2da70752d", + "rtype": "button" + }, + { + "rid": "15c0d166-8059-45fc-9f32-97e69e714a27", + "rtype": "device_power" + }, + { + "rid": "3eb848e9-fd04-48a6-a4fb-305af3b97ecf", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "8c5b05ba-b4f4-47b2-8ba0-fc44363192bc", + "id_v1": "/sensors/135", + "product_data": { + "model_id": "ROM001", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue Smart button", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "2.47.8", + "hardware_platform_type": "100b-116" + }, + "metadata": { + "name": "Front Bedroom Smart Button A", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "79af0ff1-a4ed-476d-a381-eabd4d3ee6bd", + "rtype": "button" + }, + { + "rid": "995a0ef5-a3a0-41a7-aa25-e3c85539c540", + "rtype": "device_power" + }, + { + "rid": "35a857b1-a376-4f2c-80c8-7542d053fec8", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "68ff07d0-6543-4967-9889-e0bc0bc16c31", + "id_v1": "/sensors/8", + "product_data": { + "model_id": "RWL021", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue dimmer switch", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "1.1.28573", + "hardware_platform_type": "100b-109" + }, + "metadata": { + "name": "Kitchen Bay Dimmer Pad", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "8a16c730-094a-4aa1-abd8-9cb758468d62", + "rtype": "button" + }, + { + "rid": "3fed0654-68ec-43e1-b3db-f1a3eba076b9", + "rtype": "button" + }, + { + "rid": "df6e2ba8-94f4-43ce-9bec-72c252616062", + "rtype": "button" + }, + { + "rid": "a46f615d-6760-479c-ba6d-2837a18902e8", + "rtype": "button" + }, + { + "rid": "c656ed42-7ab1-43fe-a910-039bdd5681bf", + "rtype": "device_power" + }, + { + "rid": "37a1c077-0e0f-435d-a3d9-69110fc3791d", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "d8da96f0-0637-40bc-a89d-65ac47bceb0a", + "id_v1": "/lights/18", + "product_data": { + "model_id": "LTA001", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue ambiance lamp", + "product_archetype": "sultan_bulb", + "certified": true, + "software_version": "1.93.11", + "hardware_platform_type": "100b-112" + }, + "metadata": { + "name": "Table Lamp E", + "archetype": "table_shade" + }, + "identify": { + + }, + "services": [ + { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + { + "rid": "e5e5e388-35ac-4b4a-8e5c-ebeb10d18aee", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "96bec26d-d7c6-4c00-98cd-6f96733296a0", + "id_v1": "/sensors/18", + "product_data": { + "model_id": "RWL021", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue dimmer switch", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "1.1.28573", + "hardware_platform_type": "100b-109" + }, + "metadata": { + "name": "Back Bedroom Dimmer Pad Bed", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "c34bb678-b910-4a06-8019-3e0ce922f17f", + "rtype": "button" + }, + { + "rid": "286b1b18-ff43-40be-a2ff-b1edc24ecb7c", + "rtype": "button" + }, + { + "rid": "2be1abd3-00a4-4985-b2c9-9bdd4cbe1e4b", + "rtype": "button" + }, + { + "rid": "dc9f1653-11a8-4794-b706-464cf53e9722", + "rtype": "button" + }, + { + "rid": "45cf7847-bf65-4e68-831a-772d3c067d46", + "rtype": "device_power" + }, + { + "rid": "141895c2-a4f7-4028-83c9-1e356358fb84", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "7bf2175d-0bb8-48dc-a7b4-775d3af3dfc9", + "id_v1": "/lights/3", + "product_data": { + "model_id": "LCT007", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue color lamp", + "product_archetype": "sultan_bulb", + "certified": true, + "software_version": "67.88.1", + "hardware_platform_type": "100b-104" + }, + "metadata": { + "name": "Bay Window Lamp", + "archetype": "pendant_round" + }, + "identify": { + + }, + "services": [ + { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + { + "rid": "fa249694-315a-43c3-88ca-b3fe8ac9d683", + "rtype": "zigbee_connectivity" + }, + { + "rid": "4658e884-e2a2-425d-ad00-55353afcea4e", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "81d9a9d5-228c-45df-828e-0d224929b3d1", + "id_v1": "/sensors/4", + "product_data": { + "model_id": "RWL021", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue dimmer switch", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "1.1.28573", + "hardware_platform_type": "100b-109" + }, + "metadata": { + "name": "Dining Room Dimmer Pad", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "ac4ebfeb-2045-4698-96de-0b6b5421a1d8", + "rtype": "button" + }, + { + "rid": "8a5159f1-93de-4c07-9d32-1fe555fde285", + "rtype": "button" + }, + { + "rid": "14debc09-496e-4429-8c22-4975c135bbdf", + "rtype": "button" + }, + { + "rid": "9672e2a9-a3a9-4aec-b31e-f2338114b22b", + "rtype": "button" + }, + { + "rid": "2ac49e3d-9437-4b6c-9c19-7570f4381d19", + "rtype": "device_power" + }, + { + "rid": "047a7502-fbb9-49b4-8324-8329502de717", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "37c25501-53e4-4e01-b1bb-2f5ee6e7e258", + "id_v1": "/lights/6", + "product_data": { + "model_id": "LTW013", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue ambiance spot", + "product_archetype": "spot_bulb", + "certified": true, + "software_version": "1.88.1", + "hardware_platform_type": "100b-10c" + }, + "metadata": { + "name": "Downlight 1", + "archetype": "recessed_ceiling" + }, + "identify": { + + }, + "services": [ + { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + { + "rid": "78c09361-22b0-4054-87ee-43754cfa5607", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "78c2c794-7bdd-4a95-a7e8-4ee7b9af28bd", + "id_v1": "/lights/21", + "product_data": { + "model_id": "LOM003", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue Smart plug", + "product_archetype": "plug", + "certified": true, + "software_version": "1.93.6", + "hardware_platform_type": "100b-115" + }, + "metadata": { + "name": "Polar Bear Light", + "archetype": "table_shade" + }, + "identify": { + + }, + "services": [ + { + "rid": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "rtype": "light" + }, + { + "rid": "2f7ae216-0fae-4f91-9fea-418b99149374", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "346a2c5a-b736-497e-aed2-0dd7a7daff52", + "id_v1": "/lights/8", + "product_data": { + "model_id": "LTW013", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue ambiance spot", + "product_archetype": "spot_bulb", + "certified": true, + "software_version": "1.88.1", + "hardware_platform_type": "100b-10c" + }, + "metadata": { + "name": "Downlight 3", + "archetype": "recessed_ceiling" + }, + "identify": { + + }, + "services": [ + { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + { + "rid": "2ca08a39-c00e-4042-97bd-6dfbdb0dd61a", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "9680e1fa-3b1d-4979-94ba-1a0a0e7a47b8", + "id_v1": "/lights/17", + "product_data": { + "model_id": "TRADFRI transformer 10W", + "manufacturer_name": "IKEA of Sweden", + "product_name": "Dimmable light", + "product_archetype": "classic_bulb", + "certified": false, + "software_version": "1.2.245", + "hardware_platform_type": "117c-4101" + }, + "metadata": { + "name": "Worktop (R)", + "archetype": "wall_washer" + }, + "identify": { + + }, + "services": [ + { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + { + "rid": "4b1eb54c-2756-4f20-b8e3-1375cd47da81", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "055f6ba3-0354-4765-b23e-287b505f2cd2", + "id_v1": "/lights/9", + "product_data": { + "model_id": "LTW013", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue ambiance spot", + "product_archetype": "spot_bulb", + "certified": true, + "software_version": "1.88.1", + "hardware_platform_type": "100b-10c" + }, + "metadata": { + "name": "Downlight 4", + "archetype": "recessed_ceiling" + }, + "identify": { + + }, + "services": [ + { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + { + "rid": "f975c0b9-0f57-4f97-ab23-826e370b0a7d", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "56b560bc-a127-4634-8d80-9946104a4028", + "id_v1": "/sensors/245", + "product_data": { + "model_id": "ROM001", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue Smart button", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "2.47.8", + "hardware_platform_type": "100b-116" + }, + "metadata": { + "name": "Polar Bear Button", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "f385a946-c638-4631-b6a6-c490cf809606", + "rtype": "button" + }, + { + "rid": "2ec182bc-d727-478a-8cd5-45d9bcf178ee", + "rtype": "device_power" + }, + { + "rid": "3f9e9e41-5e2b-4cc6-8b36-8876f5d049e8", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "a1155885-4bbe-469f-83bb-f964f8e13e82", + "id_v1": "/sensors/12", + "product_data": { + "model_id": "RWL021", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue dimmer switch", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "1.1.28573", + "hardware_platform_type": "100b-109" + }, + "metadata": { + "name": "Worktops Dimmer Pad Left", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "ba204102-de9f-479e-818a-02ed880caac2", + "rtype": "button" + }, + { + "rid": "0032a38b-cf67-478f-934a-520a156a2baf", + "rtype": "button" + }, + { + "rid": "4ce58cbd-5d0f-45ec-920d-ad601fd9cc07", + "rtype": "button" + }, + { + "rid": "813a331a-f26a-4ab2-948c-9b4c398939d0", + "rtype": "button" + }, + { + "rid": "e4de67d2-130b-42bc-891c-6c61905028bd", + "rtype": "device_power" + }, + { + "rid": "49d03c43-75ef-415b-88d0-264e6c2ea748", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "e130feac-3a5c-452e-a97d-5bca470783b3", + "id_v1": "/sensors/20", + "product_data": { + "model_id": "RWL021", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue dimmer switch", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "1.1.28573", + "hardware_platform_type": "100b-109" + }, + "metadata": { + "name": "Worktops Dimmer Pad Right", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "91ba8839-2bac-4175-9f8c-ed192842d549", + "rtype": "button" + }, + { + "rid": "f95addfc-2f7c-453f-924d-ba496e07e5f9", + "rtype": "button" + }, + { + "rid": "6615f1f1-f3f1-4a05-b8f7-581097458e34", + "rtype": "button" + }, + { + "rid": "b0d5a0af-31fd-4189-9150-c551ff9033d7", + "rtype": "button" + }, + { + "rid": "e77d2083-8728-4382-a631-6a2706bb1a3d", + "rtype": "device_power" + }, + { + "rid": "e2633d2e-fcbc-4a7e-852e-f46d20ca8f21", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "266759fc-aaac-4eea-af5d-f226a146c119", + "id_v1": "/lights/7", + "product_data": { + "model_id": "LTW013", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue ambiance spot", + "product_archetype": "spot_bulb", + "certified": true, + "software_version": "1.88.1", + "hardware_platform_type": "100b-10c" + }, + "metadata": { + "name": "Downlight 2", + "archetype": "recessed_ceiling" + }, + "identify": { + + }, + "services": [ + { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + { + "rid": "e533ace8-78a4-46bd-9491-8821cc9960cd", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "e12b3d9c-0b23-4edd-a967-66f5b5cefa92", + "id_v1": "/sensors/26", + "product_data": { + "model_id": "RWL021", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue dimmer switch", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "1.1.28573", + "hardware_platform_type": "100b-109" + }, + "metadata": { + "name": "Front Bedroom Dimmer Pad Bed", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "9d3820db-4a20-4925-80f6-c74582ffb871", + "rtype": "button" + }, + { + "rid": "a9e1a40b-a13d-4966-b0b1-2b0b5dfe1986", + "rtype": "button" + }, + { + "rid": "8ba063d7-df16-48f5-bb9b-f45849ec1bd3", + "rtype": "button" + }, + { + "rid": "4fc9dcd2-6f71-46c4-b3d0-5d7b496be6f9", + "rtype": "button" + }, + { + "rid": "e692545e-c085-401e-81c5-e865845e8d87", + "rtype": "device_power" + }, + { + "rid": "272f9a1c-b99f-4014-a82c-9d6fed39f19b", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "0d47bd3d-d82b-4a21-893c-299bff18e22a", + "id_v1": "/lights/4", + "product_data": { + "model_id": "LWB010", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue white lamp", + "product_archetype": "classic_bulb", + "certified": true, + "software_version": "1.88.1", + "hardware_platform_type": "100b-10c" + }, + "metadata": { + "name": "Table Lamp", + "archetype": "table_shade" + }, + "identify": { + + }, + "services": [ + { + "rid": "c43e95e6-b268-4e9c-8fc6-cb092e5100d0", + "rtype": "light" + }, + { + "rid": "b3e07aed-51c0-45c4-89dc-643e684a1543", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "e0aafa22-59ab-4467-b603-632d92d2c15b", + "id_v1": "/lights/2", + "product_data": { + "model_id": "LCT007", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue color lamp", + "product_archetype": "sultan_bulb", + "certified": true, + "software_version": "67.88.1", + "hardware_platform_type": "100b-104" + }, + "metadata": { + "name": "Standard Lamp R", + "archetype": "floor_shade" + }, + "identify": { + + }, + "services": [ + { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + { + "rid": "8ade55bf-0e12-4726-be31-8f3645ad519f", + "rtype": "zigbee_connectivity" + }, + { + "rid": "819489fb-2485-4e34-8805-aa19f85c6e5f", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "f4c5c816-925b-4e22-a112-2b44a23f5613", + "id_v1": "", + "product_data": { + "model_id": "BSB002", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Philips hue", + "product_archetype": "bridge_v2", + "certified": true, + "software_version": "1.53.1953188020" + }, + "metadata": { + "name": "Philips hue", + "archetype": "bridge_v2" + }, + "identify": { + + }, + "services": [ + { + "rid": "703765c0-f78a-4aac-9458-f50c0b41e1d8", + "rtype": "bridge" + }, + { + "rid": "ea1571fe-9c39-45a8-b20b-63834fea16bd", + "rtype": "zigbee_connectivity" + }, + { + "rid": "f391a6b1-8266-4039-b19c-3cce3572236f", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "7da8bcc7-6e69-46c4-a2e1-e0c2de8e2270", + "id_v1": "/lights/19", + "product_data": { + "model_id": "LTA001", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue ambiance lamp", + "product_archetype": "sultan_bulb", + "certified": true, + "software_version": "1.93.11", + "hardware_platform_type": "100b-112" + }, + "metadata": { + "name": "Wall Lamp", + "archetype": "wall_shade" + }, + "identify": { + + }, + "services": [ + { + "rid": "c8ec2639-cc81-4316-af05-59d705f4babe", + "rtype": "light" + }, + { + "rid": "f0040018-1c7a-49cc-b640-ba62675eced2", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "e41beec5-0340-4700-9094-8244f9a8ed0d", + "id_v1": "/lights/1", + "product_data": { + "model_id": "LCT007", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue color lamp", + "product_archetype": "sultan_bulb", + "certified": true, + "software_version": "67.88.1", + "hardware_platform_type": "100b-104" + }, + "metadata": { + "name": "Standard Lamp L", + "archetype": "floor_shade" + }, + "identify": { + + }, + "services": [ + { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + { + "rid": "037dd595-b5b7-48e0-be43-e765367aa8f6", + "rtype": "zigbee_connectivity" + }, + { + "rid": "a22e43a7-b804-47dd-b50e-9a8f49efc6df", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "431026fb-298c-4726-8ce4-47450fea13c4", + "id_v1": "/sensors/14", + "product_data": { + "model_id": "RWL021", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue dimmer switch", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "1.1.28573", + "hardware_platform_type": "100b-109" + }, + "metadata": { + "name": "Front Bedroom Dimmer Pad Door", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "824ea347-28d1-4e45-b886-1555a160190b", + "rtype": "button" + }, + { + "rid": "801ede68-21b0-4e6e-98be-eb1a60557ac6", + "rtype": "button" + }, + { + "rid": "1218d0c3-9a9c-4984-84a5-c12ad085ef2d", + "rtype": "button" + }, + { + "rid": "eb2c07bc-1d09-4096-a044-dec29b40619a", + "rtype": "button" + }, + { + "rid": "c5dafdfc-b096-4090-ba24-912440168c87", + "rtype": "device_power" + }, + { + "rid": "3c0c40e5-cf3f-45ae-89f6-b1038cddbb13", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "f78c5b4b-2f52-4bc3-8097-1ddf97949cc5", + "id_v1": "/sensors/124", + "product_data": { + "model_id": "RDM001", + "manufacturer_name": "Signify Netherlands B.V.", + "product_name": "Hue wall switch module", + "product_archetype": "unknown_archetype", + "certified": true, + "software_version": "1.0.3", + "hardware_platform_type": "100b-11c" + }, + "metadata": { + "name": "Hall Alcove Wallplate Switch", + "archetype": "unknown_archetype" + }, + "services": [ + { + "rid": "6bae5b99-349c-4045-8c8f-d4a60562b1d3", + "rtype": "button" + }, + { + "rid": "373b4f7c-4e3b-400b-bf34-831af5bdd0e8", + "rtype": "device_power" + }, + { + "rid": "3a35dfed-10bc-4403-88c8-7963543fb090", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/device_power.json b/bundles/org.openhab.binding.hue/src/test/resources/device_power.json new file mode 100644 index 000000000..654e06799 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/device_power.json @@ -0,0 +1,213 @@ +{ + "errors": [], + "data": [ + { + "id": "695b7480-42bc-44cf-bd63-1911f98fee34", + "id_v1": "/sensors/110", + "owner": { + "rid": "112853f9-c4c4-4d65-ba96-b4c2ab26d94d", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 60 + }, + "type": "device_power" + }, + { + "id": "65833fc9-1d08-4d03-b5ae-a03e7eca559c", + "id_v1": "/sensors/236", + "owner": { + "rid": "cfecbbd0-e918-42a2-b714-2bad33061d95", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 69 + }, + "type": "device_power" + }, + { + "id": "0fb39897-aa8b-49b4-8215-4e27651eb2f1", + "id_v1": "/sensors/63", + "owner": { + "rid": "0e22f8de-eff5-440a-a9ed-06d547d125d7", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 20 + }, + "type": "device_power" + }, + { + "id": "025bf7ea-5ba4-457f-9fa9-50a9a7886b0f", + "id_v1": "/sensors/6", + "owner": { + "rid": "b56191ea-8d18-4257-b5d2-cfc3fc86a1ee", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 73 + }, + "type": "device_power" + }, + { + "id": "42fe582f-b3f9-4ee9-905b-38357d07d3a5", + "id_v1": "/sensors/24", + "owner": { + "rid": "b5fe0539-171c-4733-bf0b-244635a309be", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 57 + }, + "type": "device_power" + }, + { + "id": "15c0d166-8059-45fc-9f32-97e69e714a27", + "id_v1": "/sensors/118", + "owner": { + "rid": "a0509519-3ecb-47d0-9183-25db1e4ea2b2", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 100 + }, + "type": "device_power" + }, + { + "id": "995a0ef5-a3a0-41a7-aa25-e3c85539c540", + "id_v1": "/sensors/135", + "owner": { + "rid": "8c5b05ba-b4f4-47b2-8ba0-fc44363192bc", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 100 + }, + "type": "device_power" + }, + { + "id": "c656ed42-7ab1-43fe-a910-039bdd5681bf", + "id_v1": "/sensors/8", + "owner": { + "rid": "68ff07d0-6543-4967-9889-e0bc0bc16c31", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 77 + }, + "type": "device_power" + }, + { + "id": "45cf7847-bf65-4e68-831a-772d3c067d46", + "id_v1": "/sensors/18", + "owner": { + "rid": "96bec26d-d7c6-4c00-98cd-6f96733296a0", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 49 + }, + "type": "device_power" + }, + { + "id": "2ac49e3d-9437-4b6c-9c19-7570f4381d19", + "id_v1": "/sensors/4", + "owner": { + "rid": "81d9a9d5-228c-45df-828e-0d224929b3d1", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 100 + }, + "type": "device_power" + }, + { + "id": "2ec182bc-d727-478a-8cd5-45d9bcf178ee", + "id_v1": "/sensors/245", + "owner": { + "rid": "56b560bc-a127-4634-8d80-9946104a4028", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 30 + }, + "type": "device_power" + }, + { + "id": "e4de67d2-130b-42bc-891c-6c61905028bd", + "id_v1": "/sensors/12", + "owner": { + "rid": "a1155885-4bbe-469f-83bb-f964f8e13e82", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 71 + }, + "type": "device_power" + }, + { + "id": "e77d2083-8728-4382-a631-6a2706bb1a3d", + "id_v1": "/sensors/20", + "owner": { + "rid": "e130feac-3a5c-452e-a97d-5bca470783b3", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 71 + }, + "type": "device_power" + }, + { + "id": "e692545e-c085-401e-81c5-e865845e8d87", + "id_v1": "/sensors/26", + "owner": { + "rid": "e12b3d9c-0b23-4edd-a967-66f5b5cefa92", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 63 + }, + "type": "device_power" + }, + { + "id": "c5dafdfc-b096-4090-ba24-912440168c87", + "id_v1": "/sensors/14", + "owner": { + "rid": "431026fb-298c-4726-8ce4-47450fea13c4", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 70 + }, + "type": "device_power" + }, + { + "id": "373b4f7c-4e3b-400b-bf34-831af5bdd0e8", + "id_v1": "/sensors/124", + "owner": { + "rid": "f78c5b4b-2f52-4bc3-8097-1ddf97949cc5", + "rtype": "device" + }, + "power_state": { + "battery_state": "normal", + "battery_level": 100 + }, + "type": "device_power" + } + ] +} diff --git a/bundles/org.openhab.binding.hue/src/test/resources/entertainment.json b/bundles/org.openhab.binding.hue/src/test/resources/entertainment.json new file mode 100644 index 000000000..697bca3af --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/entertainment.json @@ -0,0 +1,79 @@ +{ + "errors": [], + "data": [ + { + "id": "4658e884-e2a2-425d-ad00-55353afcea4e", + "id_v1": "/lights/3", + "owner": { + "rid": "7bf2175d-0bb8-48dc-a7b4-775d3af3dfc9", + "rtype": "device" + }, + "renderer": true, + "proxy": true, + "segments": { + "configurable": false, + "max_segments": 1, + "segments": [ + { + "start": 0, + "length": 1 + } + ] + }, + "type": "entertainment" + }, + { + "id": "819489fb-2485-4e34-8805-aa19f85c6e5f", + "id_v1": "/lights/2", + "owner": { + "rid": "e0aafa22-59ab-4467-b603-632d92d2c15b", + "rtype": "device" + }, + "renderer": true, + "proxy": true, + "segments": { + "configurable": false, + "max_segments": 1, + "segments": [ + { + "start": 0, + "length": 1 + } + ] + }, + "type": "entertainment" + }, + { + "id": "f391a6b1-8266-4039-b19c-3cce3572236f", + "id_v1": "", + "owner": { + "rid": "f4c5c816-925b-4e22-a112-2b44a23f5613", + "rtype": "device" + }, + "renderer": false, + "proxy": true, + "type": "entertainment" + }, + { + "id": "a22e43a7-b804-47dd-b50e-9a8f49efc6df", + "id_v1": "/lights/1", + "owner": { + "rid": "e41beec5-0340-4700-9094-8244f9a8ed0d", + "rtype": "device" + }, + "renderer": true, + "proxy": true, + "segments": { + "configurable": false, + "max_segments": 1, + "segments": [ + { + "start": 0, + "length": 1 + } + ] + }, + "type": "entertainment" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/entertainment_configuration.json b/bundles/org.openhab.binding.hue/src/test/resources/entertainment_configuration.json new file mode 100644 index 000000000..dd157346f --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/entertainment_configuration.json @@ -0,0 +1,109 @@ +{ + "errors": [], + "data": [ + { + "configuration_type": "screen", + "id": "7535ee8b-bc2f-4edf-8542-da5d6ab557e9", + "id_v1": "/groups/14", + "locations": { + "service_locations": [ + { + "positions": [ + { + "x": 0.75, + "y": -0.52, + "z": 0.0 + } + ], + "service": { + "rid": "a22e43a7-b804-47dd-b50e-9a8f49efc6df", + "rtype": "entertainment" + }, + "position": { + "x": 0.75, + "y": -0.51999, + "z": 0.0 + } + }, + { + "positions": [ + { + "x": -0.76, + "y": -0.53, + "z": 0.0 + } + ], + "service": { + "rid": "819489fb-2485-4e34-8805-aa19f85c6e5f", + "rtype": "entertainment" + }, + "position": { + "x": -0.75999, + "y": -0.52999, + "z": 0.0 + } + } + ] + }, + "metadata": { + "name": "Entertainment area 1" + }, + "stream_proxy": { + "mode": "auto", + "node": { + "rtype": "entertainment", + "rid": "f391a6b1-8266-4039-b19c-3cce3572236f" + } + }, + "light_services": [ + { + "rtype": "light", + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288" + }, + { + "rtype": "light", + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3" + } + ], + "channels": [ + { + "channel_id": 0, + "position": { + "x": 0.75, + "y": -0.51999, + "z": 0.0 + }, + "members": [ + { + "index": 0, + "service": { + "rtype": "entertainment", + "rid": "a22e43a7-b804-47dd-b50e-9a8f49efc6df" + } + } + ] + }, + { + "channel_id": 1, + "position": { + "x": -0.75999, + "y": -0.52999, + "z": 0.0 + }, + "members": [ + { + "index": 0, + "service": { + "rtype": "entertainment", + "rid": "819489fb-2485-4e34-8805-aa19f85c6e5f" + } + } + ] + } + ], + "type": "entertainment_configuration", + "name": "Entertainment area 1", + "status": "inactive" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/event.json b/bundles/org.openhab.binding.hue/src/test/resources/event.json new file mode 100644 index 000000000..f1118c48d --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/event.json @@ -0,0 +1,211 @@ +[ + { + "creationtime": "2022-10-18T12:02:45Z", + "data": [ + { + "id": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "id_v1": "/lights/18", + "on": { + "on": false + }, + "owner": { + "rid": "d8da96f0-0637-40bc-a89d-65ac47bceb0a", + "rtype": "device" + }, + "type": "light" + }, + { + "id": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "id_v1": "/lights/10", + "on": { + "on": false + }, + "owner": { + "rid": "adeb3425-6a6b-49a0-8262-129126de7941", + "rtype": "device" + }, + "type": "light" + }, + { + "dimming": { + "brightness": 100.0 + }, + "id": "9228d710-3c54-4ae4-8c88-bfe57d8fd220", + "id_v1": "/groups/0", + "owner": { + "rid": "f467cdcc-405f-40ab-8db9-4664aa1c3d63", + "rtype": "bridge_home" + }, + "type": "grouped_light" + }, + { + "id": "6e2fee8d-c25f-4468-8ac8-5ed75c6f3cf1", + "id_v1": "/groups/9", + "on": { + "on": false + }, + "owner": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "type": "grouped_light" + }, + { + "dimming": { + "brightness": 0.0 + }, + "id": "6e2fee8d-c25f-4468-8ac8-5ed75c6f3cf1", + "id_v1": "/groups/9", + "owner": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "type": "grouped_light" + }, + { + "id": "1bfb7090-e2b3-4417-8834-86e1ebb0a50c", + "id_v1": "/groups/10", + "on": { + "on": false + }, + "owner": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "type": "grouped_light" + }, + { + "dimming": { + "brightness": 0.0 + }, + "id": "1bfb7090-e2b3-4417-8834-86e1ebb0a50c", + "id_v1": "/groups/10", + "owner": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "type": "grouped_light" + }, + { + "id": "3913b23e-4ba8-4ebd-965d-e15c7b3213ce", + "id_v1": "/groups/13", + "on": { + "on": false + }, + "owner": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "type": "grouped_light" + }, + { + "dimming": { + "brightness": 0.0 + }, + "id": "3913b23e-4ba8-4ebd-965d-e15c7b3213ce", + "id_v1": "/groups/13", + "owner": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "type": "grouped_light" + } + ], + "id": "dba95b8a-e763-4667-a667-a10cd8ba5b3e", + "type": "update" + }, + { + "creationtime": "2022-10-18T12:02:45Z", + "data": [ + { + "button": { + "last_event": "short_release" + }, + "id": "eb2c07bc-1d09-4096-a044-dec29b40619a", + "id_v1": "/sensors/14", + "owner": { + "rid": "431026fb-298c-4726-8ce4-47450fea13c4", + "rtype": "device" + }, + "type": "button" + } + ], + "id": "5686a018-0779-4f65-b7d1-2943b5a6b066", + "type": "update" + }, + { + "creationtime": "2023-03-28T13:30:51Z", + "data": [ + { + "id": "224d33d8-ceb7-4a8d-a3c9-3c71c9996a59", + "id_v1": "/lights/1", + "on": { + "on": true + }, + "owner": { + "rid": "6485b533-3d67-429c-89ff-e7ba813513f6", + "rtype": "device" + }, + "type": "light" + }, + { + "id": "fc344d9a-4c63-4e38-8082-d23b48b2152c", + "id_v1": "/scenes/zqEj6fQ8nSN8AUO", + "status": { + "active": "inactive" + }, + "type": "scene" + }, + { + "id": "c766b174-536e-4a2d-bc62-e3365c5229ae", + "id_v1": "/groups/0", + "on": { + "on": true + }, + "owner": { + "rid": "bd9983ea-ce23-447e-95a8-cfd02bd037d7", + "rtype": "bridge_home" + }, + "type": "grouped_light" + }, + { + "dimming": { + "brightness": 84.65 + }, + "id": "c766b174-536e-4a2d-bc62-e3365c5229ae", + "id_v1": "/groups/0", + "owner": { + "rid": "bd9983ea-ce23-447e-95a8-cfd02bd037d7", + "rtype": "bridge_home" + }, + "type": "grouped_light" + }, + { + "id": "ba365fdb-f055-410c-9021-561b725cdc22", + "id_v1": "/groups/1", + "on": { + "on": true + }, + "owner": { + "rid": "dc30bab1-66e9-4b8e-9d9d-8725a770c515", + "rtype": "room" + }, + "type": "grouped_light" + }, + { + "dimming": { + "brightness": 84.65 + }, + "id": "ba365fdb-f055-410c-9021-561b725cdc22", + "id_v1": "/groups/1", + "owner": { + "rid": "dc30bab1-66e9-4b8e-9d9d-8725a770c515", + "rtype": "room" + }, + "type": "grouped_light" + } + ], + "id": "441f1e29-f02f-4936-bbfa-c6c08ff1ea12", + "type": "update" + } +] diff --git a/bundles/org.openhab.binding.hue/src/test/resources/geofence.json b/bundles/org.openhab.binding.hue/src/test/resources/geofence.json new file mode 100644 index 000000000..910d19868 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/geofence.json @@ -0,0 +1,4 @@ +{ + "errors": [], + "data": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/geofence_client.json b/bundles/org.openhab.binding.hue/src/test/resources/geofence_client.json new file mode 100644 index 000000000..a0c22bffc --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/geofence_client.json @@ -0,0 +1,10 @@ +{ + "errors": [], + "data": [ + { + "id": "f0291861-4351-8bcd-9cda-1a140dd2bd42", + "name": "samsung SM-G960F", + "type": "geofence_client" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/geolocation.json b/bundles/org.openhab.binding.hue/src/test/resources/geolocation.json new file mode 100644 index 000000000..82cda52f2 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/geolocation.json @@ -0,0 +1,10 @@ +{ + "errors": [], + "data": [ + { + "id": "fe29550d-92b9-4e36-a832-42d041466300", + "is_configured": true, + "type": "geolocation" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/grouped_light.json b/bundles/org.openhab.binding.hue/src/test/resources/grouped_light.json new file mode 100644 index 000000000..36d9be921 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/grouped_light.json @@ -0,0 +1,526 @@ +{ + "errors": [], + "data": [ + { + "id": "db4fd630-3798-40de-b642-c1ef464bf770", + "id_v1": "/groups/6", + "owner": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": null, + "color_temperature": null, + "color_temperature_delta": { + + }, + "color": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "08947162-67be-4ed5-bfce-f42dade42416", + "id_v1": "/groups/3", + "owner": { + "rid": "dcbc740d-1e4f-48aa-ad02-3e17f1f4eebb", + "rtype": "room" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + + }, + "color_temperature_delta": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "0aec7082-c40e-4435-ab01-7b387468f7f9", + "id_v1": "/groups/12", + "owner": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + + }, + "color_temperature_delta": { + + }, + "color": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "a98c217f-ce07-4bc5-990f-24df6eaa043b", + "id_v1": "/groups/7", + "owner": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + + }, + "color_temperature_delta": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "7ef7456a-8c4c-4b85-b433-b8c1bb99249b", + "id_v1": "/groups/15", + "owner": { + "rid": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "rtype": "zone" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + + }, + "color_temperature_delta": { + + }, + "color": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "900f4b11-7ed1-46ae-bcf4-978d0028aac9", + "id_v1": "/groups/4", + "owner": { + "rid": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "rtype": "zone" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + + }, + "color_temperature_delta": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "ef9470ca-7e96-4fe0-84f2-a10783df9af0", + "id_v1": "/groups/2", + "owner": { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + + }, + "color_temperature_delta": { + + }, + "color": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "9228d710-3c54-4ae4-8c88-bfe57d8fd220", + "id_v1": "/groups/0", + "owner": { + "rid": "f467cdcc-405f-40ab-8db9-4664aa1c3d63", + "rtype": "bridge_home" + }, + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + + }, + "color_temperature_delta": { + + }, + "color": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "03b1effe-6521-4579-8734-f0d11ce28ed0", + "id_v1": "/groups/5", + "owner": { + "rid": "8cec1e2f-bcc9-45c9-a0aa-bc9c30c68b64", + "rtype": "room" + }, + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "dimming_delta": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "dd653c2b-9622-45d5-aa57-d0bf9391592b", + "id_v1": "/groups/8", + "owner": { + "rid": "dda859a6-f358-48f5-8d34-e13b04bf6e62", + "rtype": "zone" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "3913b23e-4ba8-4ebd-965d-e15c7b3213ce", + "id_v1": "/groups/13", + "owner": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + + }, + "color_temperature_delta": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "6e2fee8d-c25f-4468-8ac8-5ed75c6f3cf1", + "id_v1": "/groups/9", + "owner": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + + }, + "color_temperature_delta": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "f8cac182-2608-40e3-81e4-f6ac02eba55a", + "id_v1": "/groups/1", + "owner": { + "rid": "bdc282b3-750d-45dd-b6c4-12a2927d8951", + "rtype": "zone" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "1bfb7090-e2b3-4417-8834-86e1ebb0a50c", + "id_v1": "/groups/10", + "owner": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + + }, + "color_temperature_delta": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + }, + { + "id": "4283d61a-15c4-48e2-a7ca-03257093e256", + "id_v1": "/groups/11", + "owner": { + "rid": "2dfe3207-f44f-4dd3-beb6-ec132abb885f", + "rtype": "private_group" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 0.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + + }, + "color_temperature_delta": { + + }, + "color": { + + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "dynamics": { + + }, + "type": "grouped_light" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/homekit.json b/bundles/org.openhab.binding.hue/src/test/resources/homekit.json new file mode 100644 index 000000000..f60f7e30a --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/homekit.json @@ -0,0 +1,15 @@ +{ + "errors": [], + "data": [ + { + "id": "886121d4-6809-4adc-961d-53c25ce05e7d", + "status": "paired", + "status_values": [ + "pairing", + "paired", + "unpaired" + ], + "type": "homekit" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/light.json b/bundles/org.openhab.binding.hue/src/test/resources/light.json new file mode 100644 index 000000000..0db633748 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/light.json @@ -0,0 +1,852 @@ +{ + "errors": [], + "data": [ + { + "id": "c42f8220-f232-4400-910c-943547513827", + "id_v1": "/lights/5", + "owner": { + "rid": "01de467b-29a0-48fc-b711-fd9c079bd429", + "rtype": "device" + }, + "metadata": { + "name": "Wall Lamp", + "archetype": "wall_shade" + }, + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0, + "min_dim_level": 2.0 + }, + "dimming_delta": null, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": null, + "mode": "normal", + "type": "light" + }, + { + "id": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "id_v1": "/lights/16", + "owner": { + "rid": "18212397-8c4d-4373-8f59-c047b80994ac", + "rtype": "device" + }, + "metadata": { + "name": "Worktop (L)", + "archetype": "wall_washer" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 50.0 + }, + "dimming_delta": { + + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "type": "light" + }, + { + "id": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "id_v1": "/lights/20", + "owner": { + "rid": "0b4c9bdb-3f46-485b-8337-e5649c03b9e2", + "rtype": "device" + }, + "metadata": { + "name": "Aquarium Light", + "archetype": "recessed_floor" + }, + "on": { + "on": false + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [] + }, + "mode": "normal", + "type": "light" + }, + { + "id": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "id_v1": "/lights/10", + "owner": { + "rid": "adeb3425-6a6b-49a0-8262-129126de7941", + "rtype": "device" + }, + "metadata": { + "name": "Table Lamp A", + "archetype": "table_shade" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 56.69, + "min_dim_level": 1.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + "mirek": 443, + "mirek_valid": true, + "mirek_schema": { + "mirek_minimum": 153, + "mirek_maximum": 454 + } + }, + "color_temperature_delta": { + + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "type": "light" + }, + { + "id": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "id_v1": "/lights/15", + "owner": { + "rid": "2cf59d54-8624-445f-9a01-aac19682b954", + "rtype": "device" + }, + "metadata": { + "name": "Cabinet lights", + "archetype": "wall_washer" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 72.83 + }, + "dimming_delta": { + + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "type": "light" + }, + { + "id": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "id_v1": "/lights/18", + "owner": { + "rid": "d8da96f0-0637-40bc-a89d-65ac47bceb0a", + "rtype": "device" + }, + "metadata": { + "name": "Table Lamp E", + "archetype": "table_shade" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 56.69, + "min_dim_level": 0.20000000298023225 + }, + "dimming_delta": { + + }, + "color_temperature": { + "mirek": 443, + "mirek_valid": true, + "mirek_schema": { + "mirek_minimum": 153, + "mirek_maximum": 454 + } + }, + "color_temperature_delta": { + + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "effects": { + "status_values": [ + "no_effect", + "candle" + ], + "status": "no_effect", + "effect_values": [ + "no_effect", + "candle" + ] + }, + "type": "light" + }, + { + "id": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "id_v1": "/lights/3", + "owner": { + "rid": "7bf2175d-0bb8-48dc-a7b4-775d3af3dfc9", + "rtype": "device" + }, + "metadata": { + "name": "Bay Window Lamp", + "archetype": "pendant_round" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 93.0, + "min_dim_level": 2.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + "mirek": null, + "mirek_valid": false, + "mirek_schema": { + "mirek_minimum": 153, + "mirek_maximum": 500 + } + }, + "color_temperature_delta": { + + }, + "color": { + "xy": { + "x": 0.6367, + "y": 0.3503 + }, + "gamut": { + "red": { + "x": 0.675, + "y": 0.322 + }, + "green": { + "x": 0.409, + "y": 0.518 + }, + "blue": { + "x": 0.167, + "y": 0.04 + } + }, + "gamut_type": "B" + }, + "dynamics": { + "status": "none", + "status_values": [ + "none", + "dynamic_palette" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "type": "light" + }, + { + "id": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "id_v1": "/lights/6", + "owner": { + "rid": "37c25501-53e4-4e01-b1bb-2f5ee6e7e258", + "rtype": "device" + }, + "metadata": { + "name": "Downlight 1", + "archetype": "recessed_ceiling" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 55.12, + "min_dim_level": 1.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + "mirek": 443, + "mirek_valid": true, + "mirek_schema": { + "mirek_minimum": 153, + "mirek_maximum": 454 + } + }, + "color_temperature_delta": { + + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "type": "light" + }, + { + "id": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "id_v1": "/lights/21", + "owner": { + "rid": "78c2c794-7bdd-4a95-a7e8-4ee7b9af28bd", + "rtype": "device" + }, + "metadata": { + "name": "Polar Bear Light", + "archetype": "table_shade" + }, + "on": { + "on": false + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [] + }, + "mode": "normal", + "type": "light" + }, + { + "id": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "id_v1": "/lights/8", + "owner": { + "rid": "346a2c5a-b736-497e-aed2-0dd7a7daff52", + "rtype": "device" + }, + "metadata": { + "name": "Downlight 3", + "archetype": "recessed_ceiling" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 55.12, + "min_dim_level": 1.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + "mirek": 443, + "mirek_valid": true, + "mirek_schema": { + "mirek_minimum": 153, + "mirek_maximum": 454 + } + }, + "color_temperature_delta": { + + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "type": "light" + }, + { + "id": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "id_v1": "/lights/17", + "owner": { + "rid": "9680e1fa-3b1d-4979-94ba-1a0a0e7a47b8", + "rtype": "device" + }, + "metadata": { + "name": "Worktop (R)", + "archetype": "wall_washer" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 50.0 + }, + "dimming_delta": { + + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "type": "light" + }, + { + "id": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "id_v1": "/lights/9", + "owner": { + "rid": "055f6ba3-0354-4765-b23e-287b505f2cd2", + "rtype": "device" + }, + "metadata": { + "name": "Downlight 4", + "archetype": "recessed_ceiling" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 54.72, + "min_dim_level": 1.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + "mirek": 443, + "mirek_valid": true, + "mirek_schema": { + "mirek_minimum": 153, + "mirek_maximum": 454 + } + }, + "color_temperature_delta": { + + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "type": "light" + }, + { + "id": "3366722b-48b1-4682-a648-607915873c40", + "id_v1": "/lights/7", + "owner": { + "rid": "266759fc-aaac-4eea-af5d-f226a146c119", + "rtype": "device" + }, + "metadata": { + "name": "Downlight 2", + "archetype": "recessed_ceiling" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 54.72, + "min_dim_level": 1.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + "mirek": 443, + "mirek_valid": true, + "mirek_schema": { + "mirek_minimum": 153, + "mirek_maximum": 454 + } + }, + "color_temperature_delta": { + + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "type": "light" + }, + { + "id": "c43e95e6-b268-4e9c-8fc6-cb092e5100d0", + "id_v1": "/lights/4", + "owner": { + "rid": "0d47bd3d-d82b-4a21-893c-299bff18e22a", + "rtype": "device" + }, + "metadata": { + "name": "Table Lamp", + "archetype": "table_shade" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 100.0, + "min_dim_level": 2.0 + }, + "dimming_delta": { + + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "type": "light" + }, + { + "id": "7f077235-0130-4a0a-95f9-107cf83639a3", + "id_v1": "/lights/2", + "owner": { + "rid": "e0aafa22-59ab-4467-b603-632d92d2c15b", + "rtype": "device" + }, + "metadata": { + "name": "Standard Lamp R", + "archetype": "floor_shade" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 56.69, + "min_dim_level": 2.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + "mirek": 443, + "mirek_valid": true, + "mirek_schema": { + "mirek_minimum": 153, + "mirek_maximum": 500 + } + }, + "color_temperature_delta": { + + }, + "color": { + "xy": { + "x": 0.5017, + "y": 0.4152 + }, + "gamut": { + "red": { + "x": 0.675, + "y": 0.322 + }, + "green": { + "x": 0.409, + "y": 0.518 + }, + "blue": { + "x": 0.167, + "y": 0.04 + } + }, + "gamut_type": "B" + }, + "dynamics": { + "status": "none", + "status_values": [ + "none", + "dynamic_palette" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "type": "light" + }, + { + "id": "c8ec2639-cc81-4316-af05-59d705f4babe", + "id_v1": "/lights/19", + "owner": { + "rid": "7da8bcc7-6e69-46c4-a2e1-e0c2de8e2270", + "rtype": "device" + }, + "metadata": { + "name": "Wall Lamp", + "archetype": "wall_shade" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 100.0, + "min_dim_level": 0.20000000298023225 + }, + "dimming_delta": { + + }, + "color_temperature": { + "mirek": 443, + "mirek_valid": true, + "mirek_schema": { + "mirek_minimum": 153, + "mirek_maximum": 454 + } + }, + "color_temperature_delta": { + + }, + "dynamics": { + "status": "none", + "status_values": [ + "none" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "effects": { + "status_values": [ + "no_effect", + "candle" + ], + "status": "no_effect", + "effect_values": [ + "no_effect", + "candle" + ] + }, + "type": "light" + }, + { + "id": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "id_v1": "/lights/1", + "owner": { + "rid": "e41beec5-0340-4700-9094-8244f9a8ed0d", + "rtype": "device" + }, + "metadata": { + "name": "Standard Lamp L", + "archetype": "floor_shade" + }, + "on": { + "on": false + }, + "dimming": { + "brightness": 56.69, + "min_dim_level": 2.0 + }, + "dimming_delta": { + + }, + "color_temperature": { + "mirek": 443, + "mirek_valid": true, + "mirek_schema": { + "mirek_minimum": 153, + "mirek_maximum": 500 + } + }, + "color_temperature_delta": { + + }, + "color": { + "xy": { + "x": 0.5017, + "y": 0.4152 + }, + "gamut": { + "red": { + "x": 0.675, + "y": 0.322 + }, + "green": { + "x": 0.409, + "y": 0.518 + }, + "blue": { + "x": 0.167, + "y": 0.04 + } + }, + "gamut_type": "B" + }, + "dynamics": { + "status": "none", + "status_values": [ + "none", + "dynamic_palette" + ], + "speed": 0.0, + "speed_valid": false + }, + "alert": { + "action_values": [ + "breathe" + ] + }, + "signaling": { + + }, + "mode": "normal", + "type": "light" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/light_level.json b/bundles/org.openhab.binding.hue/src/test/resources/light_level.json new file mode 100644 index 000000000..de8833e06 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/light_level.json @@ -0,0 +1,19 @@ +{ + "errors": [], + "data": [ + { + "id": "18597697-61dc-4090-9bc3-c7f5704f020e", + "id_v1": "/sensors/32", + "owner": { + "rid": "70660557-692d-4d37-8b6b-e3ec63716a72", + "rtype": "device" + }, + "enabled": true, + "light": { + "light_level": 12725, + "light_level_valid": true + }, + "type": "light_level" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/motion.json b/bundles/org.openhab.binding.hue/src/test/resources/motion.json new file mode 100644 index 000000000..b5b7ba658 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/motion.json @@ -0,0 +1,19 @@ +{ + "errors": [], + "data": [ + { + "id": "97244487-dd25-4f4c-b829-8eb5ffb82c29", + "id_v1": "/sensors/30", + "owner": { + "rid": "70660557-692d-4d37-8b6b-e3ec63716a72", + "rtype": "device" + }, + "enabled": true, + "motion": { + "motion": true, + "motion_valid": true + }, + "type": "motion" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/public_image.json b/bundles/org.openhab.binding.hue/src/test/resources/public_image.json new file mode 100644 index 000000000..dc0582057 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/public_image.json @@ -0,0 +1,7 @@ +{ + "errors": [ + { + "description": "Not Found" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/relative_rotary.json b/bundles/org.openhab.binding.hue/src/test/resources/relative_rotary.json new file mode 100644 index 000000000..3945f4e9a --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/relative_rotary.json @@ -0,0 +1,24 @@ +{ + "errors": [], + "data": [ + { + "id": "12345678-7bd4d062-a33f-40e3-a7c0-91f64c39ac1a", + "id_v1": "/sensors/2", + "owner": { + "rid": "497051e4-29c3-419b-babd-d916cffcf3a1", + "rtype": "device" + }, + "relative_rotary": { + "last_event": { + "action": "repeat", + "rotation": { + "direction": "clock_wise", + "duration": 400, + "steps": 30 + } + } + }, + "type": "relative_rotary" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/room.json b/bundles/org.openhab.binding.hue/src/test/resources/room.json new file mode 100644 index 000000000..4a0ab130d --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/room.json @@ -0,0 +1,175 @@ +{ + "errors": [], + "data": [ + { + "id": "dcbc740d-1e4f-48aa-ad02-3e17f1f4eebb", + "id_v1": "/groups/3", + "children": [ + { + "rid": "0d47bd3d-d82b-4a21-893c-299bff18e22a", + "rtype": "device" + }, + { + "rid": "7da8bcc7-6e69-46c4-a2e1-e0c2de8e2270", + "rtype": "device" + } + ], + "services": [ + { + "rid": "08947162-67be-4ed5-bfce-f42dade42416", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Back Bedroom", + "archetype": "bedroom" + }, + "type": "room" + }, + { + "id": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "id_v1": "/groups/12", + "children": [ + { + "rid": "18212397-8c4d-4373-8f59-c047b80994ac", + "rtype": "device" + }, + { + "rid": "7bf2175d-0bb8-48dc-a7b4-775d3af3dfc9", + "rtype": "device" + }, + { + "rid": "9680e1fa-3b1d-4979-94ba-1a0a0e7a47b8", + "rtype": "device" + } + ], + "services": [ + { + "rid": "0aec7082-c40e-4435-ab01-7b387468f7f9", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Kitchen", + "archetype": "kitchen" + }, + "type": "room" + }, + { + "id": "8b529073-36dd-409b-8006-80df304048ea", + "id_v1": "/groups/7", + "children": [ + { + "rid": "2cf59d54-8624-445f-9a01-aac19682b954", + "rtype": "device" + }, + { + "rid": "37c25501-53e4-4e01-b1bb-2f5ee6e7e258", + "rtype": "device" + }, + { + "rid": "346a2c5a-b736-497e-aed2-0dd7a7daff52", + "rtype": "device" + }, + { + "rid": "055f6ba3-0354-4765-b23e-287b505f2cd2", + "rtype": "device" + }, + { + "rid": "266759fc-aaac-4eea-af5d-f226a146c119", + "rtype": "device" + } + ], + "services": [ + { + "rid": "a98c217f-ce07-4bc5-990f-24df6eaa043b", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Dining Room", + "archetype": "dining" + }, + "type": "room" + }, + { + "id": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "id_v1": "/groups/2", + "children": [ + { + "rid": "0b4c9bdb-3f46-485b-8337-e5649c03b9e2", + "rtype": "device" + }, + { + "rid": "78c2c794-7bdd-4a95-a7e8-4ee7b9af28bd", + "rtype": "device" + }, + { + "rid": "e0aafa22-59ab-4467-b603-632d92d2c15b", + "rtype": "device" + }, + { + "rid": "e41beec5-0340-4700-9094-8244f9a8ed0d", + "rtype": "device" + } + ], + "services": [ + { + "rid": "ef9470ca-7e96-4fe0-84f2-a10783df9af0", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Living Room", + "archetype": "living_room" + }, + "type": "room" + }, + { + "id": "8cec1e2f-bcc9-45c9-a0aa-bc9c30c68b64", + "id_v1": "/groups/5", + "children": [ + { + "rid": "01de467b-29a0-48fc-b711-fd9c079bd429", + "rtype": "device" + } + ], + "services": [ + { + "rid": "03b1effe-6521-4579-8734-f0d11ce28ed0", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Top Staircase", + "archetype": "staircase" + }, + "type": "room" + }, + { + "id": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "id_v1": "/groups/13", + "children": [ + { + "rid": "adeb3425-6a6b-49a0-8262-129126de7941", + "rtype": "device" + }, + { + "rid": "d8da96f0-0637-40bc-a89d-65ac47bceb0a", + "rtype": "device" + } + ], + "services": [ + { + "rid": "3913b23e-4ba8-4ebd-965d-e15c7b3213ce", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Front Bedroom", + "archetype": "bedroom" + }, + "type": "room" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/scene.json b/bundles/org.openhab.binding.hue/src/test/resources/scene.json new file mode 100644 index 000000000..a1d267e50 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/scene.json @@ -0,0 +1,10355 @@ +{ + "errors": [], + "data": [ + { + "id": "d7034d54-cadb-4659-acad-14dbec434e9e", + "id_v1": "/scenes/jDfdVi5086X20iu", + "metadata": { + "name": "Energize", + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + } + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "e913d4fd-8c9a-412f-a5d4-b54546274eea", + "id_v1": "/scenes/JOUxgmiKTnSDC6m", + "metadata": { + "name": "Relax", + "image": { + "rid": "a1f7da49-d181-4328-abea-68c9dc4b5416", + "rtype": "public_image" + } + }, + "group": { + "rid": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "4f00d1c0-95fa-492a-880c-1ae4b31ca230", + "id_v1": "/scenes/j7ypDwbFC5JnzHp", + "metadata": { + "name": "Read", + "image": { + "rid": "e101a77f-9984-4f61-aac8-15741983c656", + "rtype": "public_image" + } + }, + "group": { + "rid": "dcbc740d-1e4f-48aa-ad02-3e17f1f4eebb", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "c43e95e6-b268-4e9c-8fc6-cb092e5100d0", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "c8ec2639-cc81-4316-af05-59d705f4babe", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "6bb88fc5-482e-404b-ae68-be22b4ef51ae", + "id_v1": "/scenes/-HTloBncQ2NHAJF", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color": { + "xy": { + "x": 0.561, + "y": 0.4042 + } + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "21bf8267-769a-4200-8fda-4ee60e7ef3b9", + "id_v1": "/scenes/74Im4NZCaGlpyE3", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "bdc282b3-750d-45dd-b6c4-12a2927d8951", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "0407d4bf-4d2f-4f94-8c64-46d79570d3dd", + "id_v1": "/scenes/tluU-mICAfX2J24", + "metadata": { + "name": "Read", + "image": { + "rid": "e101a77f-9984-4f61-aac8-15741983c656", + "rtype": "public_image" + } + }, + "group": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + }, + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "4cd4b150-e1ec-4e8e-88ff-cb9de9740083", + "id_v1": "/scenes/aHFOxJGniFs8TCj", + "metadata": { + "name": "Energize", + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + } + }, + "group": { + "rid": "dcbc740d-1e4f-48aa-ad02-3e17f1f4eebb", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "c43e95e6-b268-4e9c-8fc6-cb092e5100d0", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "c8ec2639-cc81-4316-af05-59d705f4babe", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "19cd4acb-3513-4cbd-8695-8b92d551b12e", + "id_v1": "/scenes/Kupb3CrgpD4KEgV", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "6b654b7c-2b7d-40d3-beca-7b65edfac15f", + "id_v1": "/scenes/yroAzw-5XjXQmrU", + "metadata": { + "name": "Tropical twilight", + "image": { + "rid": "a6a03e6a-fe6e-45bc-b686-878137f3ba91", + "rtype": "public_image" + } + }, + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color_temperature": { + "mirek": 322 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color_temperature": { + "mirek": 322 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color_temperature": { + "mirek": 322 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color_temperature": { + "mirek": 322 + } + } + }, + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.5802, + "y": 0.3952 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.5632, + "y": 0.3841 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.4563, + "y": 0.3607 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.3632, + "y": 0.2877 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.294, + "y": 0.223 + } + }, + "dimming": { + "brightness": 43.7 + } + } + ], + "dimming": [ + { + "brightness": 43.7 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 306 + }, + "dimming": { + "brightness": 43.7 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "cfea0d97-c01f-4a41-87d3-b3f86268db63", + "id_v1": "/scenes/TQ-Tn1gI6EmrvSX", + "metadata": { + "name": "Concentrate", + "image": { + "rid": "b90c8900-a6b7-422c-a5d3-e170187dbf8c", + "rtype": "public_image" + } + }, + "group": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "3eb89ad5-0bcf-476f-9987-a317cb99bd82", + "id_v1": "/scenes/IbZwdrN1P-mJPyb", + "metadata": { + "name": "Relax", + "image": { + "rid": "a1f7da49-d181-4328-abea-68c9dc4b5416", + "rtype": "public_image" + } + }, + "group": { + "rid": "dcbc740d-1e4f-48aa-ad02-3e17f1f4eebb", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "c43e95e6-b268-4e9c-8fc6-cb092e5100d0", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + } + } + }, + { + "target": { + "rid": "c8ec2639-cc81-4316-af05-59d705f4babe", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "eb7cc717-b62f-4c00-ba14-83e5a6dc4833", + "id_v1": "/scenes/vCyrYSG0y3KHSIm", + "metadata": { + "name": "Arctic aurora", + "image": { + "rid": "1e42b2e8-d02e-40d2-9c8d-b1fd8216c686", + "rtype": "public_image" + } + }, + "group": { + "rid": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + }, + "color_temperature": { + "mirek": 153 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + }, + "color_temperature": { + "mirek": 153 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + }, + "color_temperature": { + "mirek": 153 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + }, + "color_temperature": { + "mirek": 153 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.2439, + "y": 0.3791 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1654, + "y": 0.3959 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1829, + "y": 0.3021 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1559, + "y": 0.2699 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.2004, + "y": 0.2469 + } + }, + "dimming": { + "brightness": 30.71 + } + } + ], + "dimming": [ + { + "brightness": 30.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 154 + }, + "dimming": { + "brightness": 30.71 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "172bd373-aa8f-4d4a-bc7d-9837fb8ae0e4", + "id_v1": "/scenes/6iNryUqAYA6gng9", + "metadata": { + "name": "Spring blossom", + "image": { + "rid": "adfa9c3e-e9aa-4b65-b9d3-c5b2c0576715", + "rtype": "public_image" + } + }, + "group": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 82.28 + }, + "color_temperature": { + "mirek": 215 + } + } + }, + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 82.28 + }, + "color_temperature": { + "mirek": 215 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.3517, + "y": 0.3154 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.359, + "y": 0.2925 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.3744, + "y": 0.2692 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.4435, + "y": 0.2537 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.514, + "y": 0.3338 + } + }, + "dimming": { + "brightness": 81.1 + } + } + ], + "dimming": [ + { + "brightness": 81.1 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 198 + }, + "dimming": { + "brightness": 81.1 + } + } + ] + }, + "speed": 0.5595238095238095, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "a8c5fc45-1ac1-4890-be80-13ef53d57b26", + "id_v1": "/scenes/yFhTtOMZRJE6SR9", + "metadata": { + "name": "Savanna sunset", + "image": { + "rid": "4f2ed241-5aea-4c9d-8028-55d2b111e06f", + "rtype": "public_image" + } + }, + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color_temperature": { + "mirek": 385 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color_temperature": { + "mirek": 385 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color_temperature": { + "mirek": 385 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color_temperature": { + "mirek": 385 + } + } + }, + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.6563, + "y": 0.3211 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5862, + "y": 0.3575 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5502, + "y": 0.3655 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4577, + "y": 0.4563 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4162, + "y": 0.4341 + } + }, + "dimming": { + "brightness": 80.71 + } + } + ], + "dimming": [ + { + "brightness": 80.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 373 + }, + "dimming": { + "brightness": 80.71 + } + } + ] + }, + "speed": 0.6190476190476191, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "32e37c7f-3fb7-4b67-bdf4-68482f44549a", + "id_v1": "/scenes/qWO6gZubIYJuDWO", + "metadata": { + "name": "Concentrate", + "image": { + "rid": "b90c8900-a6b7-422c-a5d3-e170187dbf8c", + "rtype": "public_image" + } + }, + "group": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + }, + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "96b8fde7-2f01-4834-b43b-d04d3e45a432", + "id_v1": "/scenes/R77UXvRZ2xl9gBA", + "metadata": { + "name": "25" + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color": { + "xy": { + "x": 0.4596, + "y": 0.4142 + } + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 20.87 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "74382775-3a5c-4868-aacd-52ae64a1d512", + "id_v1": "/scenes/PJEpqBjwEwiN6NT", + "metadata": { + "name": "Spring blossom", + "image": { + "rid": "adfa9c3e-e9aa-4b65-b9d3-c5b2c0576715", + "rtype": "public_image" + } + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + }, + "color": { + "xy": { + "x": 0.3936, + "y": 0.2956 + } + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.3517, + "y": 0.3154 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.359, + "y": 0.2925 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.3744, + "y": 0.2692 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.4435, + "y": 0.2537 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.514, + "y": 0.3338 + } + }, + "dimming": { + "brightness": 81.1 + } + } + ], + "dimming": [ + { + "brightness": 81.1 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 198 + }, + "dimming": { + "brightness": 81.1 + } + } + ] + }, + "speed": 0.5595238095238095, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "b39162e6-0acb-466d-8ba9-6065c9483a8a", + "id_v1": "/scenes/cdOTREPUs2i3AFv", + "metadata": { + "name": "Relax", + "image": { + "rid": "a1f7da49-d181-4328-abea-68c9dc4b5416", + "rtype": "public_image" + } + }, + "group": { + "rid": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "f3eb78de-d550-4f8f-b34f-5a0d2a50f577", + "id_v1": "/scenes/hWUJs6j3h-tN6zW", + "metadata": { + "name": "Relax", + "image": { + "rid": "a1f7da49-d181-4328-abea-68c9dc4b5416", + "rtype": "public_image" + } + }, + "group": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [ + { + "color_temperature": { + "mirek": 447 + }, + "dimming": { + "brightness": 56.69 + } + } + ] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "85c0c3d3-425d-477a-b306-6421ff0d92d7", + "id_v1": "/scenes/WkBbBJGMZylI-Ok", + "metadata": { + "name": "Read", + "image": { + "rid": "e101a77f-9984-4f61-aac8-15741983c656", + "rtype": "public_image" + } + }, + "group": { + "rid": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "ae986691-3c0c-40ee-b673-ff1ef43783ee", + "id_v1": "/scenes/Z0e2COOvLoAwMBN", + "metadata": { + "name": "Bright", + "image": { + "rid": "732ff1d9-76a7-4630-aad0-c8acc499bb0b", + "rtype": "public_image" + } + }, + "group": { + "rid": "8cec1e2f-bcc9-45c9-a0aa-bc9c30c68b64", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "c42f8220-f232-4400-910c-943547513827", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "14b715c9-8346-4d22-8e60-4c3793adff69", + "id_v1": "/scenes/b5FtvnIEJY4cQZG", + "metadata": { + "name": "Concentrate", + "image": { + "rid": "b90c8900-a6b7-422c-a5d3-e170187dbf8c", + "rtype": "public_image" + } + }, + "group": { + "rid": "dcbc740d-1e4f-48aa-ad02-3e17f1f4eebb", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "c43e95e6-b268-4e9c-8fc6-cb092e5100d0", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "c8ec2639-cc81-4316-af05-59d705f4babe", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "b7b7107d-2509-43d7-998c-9a9ebe6604ae", + "id_v1": "/scenes/Z67GqJ9R0DeVgQU", + "metadata": { + "name": "Concentrate", + "image": { + "rid": "b90c8900-a6b7-422c-a5d3-e170187dbf8c", + "rtype": "public_image" + } + }, + "group": { + "rid": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "9e29e989-a885-427e-b0ab-05adb996ef92", + "id_v1": "/scenes/wz9SyQETcVV0zTX", + "metadata": { + "name": "Relax", + "image": { + "rid": "a1f7da49-d181-4328-abea-68c9dc4b5416", + "rtype": "public_image" + } + }, + "group": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "87a262ec-471d-4366-9b5e-b475fd903db0", + "id_v1": "/scenes/GrVKRB2VB5W6hQK", + "metadata": { + "name": "Spring blossom", + "image": { + "rid": "adfa9c3e-e9aa-4b65-b9d3-c5b2c0576715", + "rtype": "public_image" + } + }, + "group": { + "rid": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + }, + "color": { + "xy": { + "x": 0.4345, + "y": 0.2809 + } + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + }, + "color": { + "xy": { + "x": 0.394, + "y": 0.2967 + } + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.3517, + "y": 0.3154 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.359, + "y": 0.2925 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.3744, + "y": 0.2692 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.4435, + "y": 0.2537 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.514, + "y": 0.3338 + } + }, + "dimming": { + "brightness": 81.1 + } + } + ], + "dimming": [ + { + "brightness": 81.1 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 198 + }, + "dimming": { + "brightness": 81.1 + } + } + ] + }, + "speed": 0.5595238095238095, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "579e4ba3-6cf6-4a84-9178-46bbbb7acce0", + "id_v1": "/scenes/JSOcwioJt7Mz1hA", + "metadata": { + "name": "Read", + "image": { + "rid": "e101a77f-9984-4f61-aac8-15741983c656", + "rtype": "public_image" + } + }, + "group": { + "rid": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "910450f9-ce11-4e3d-a855-e2e1cc9e3c53", + "id_v1": "/scenes/snCODeBilKMnyKb", + "metadata": { + "name": "Bright", + "image": { + "rid": "732ff1d9-76a7-4630-aad0-c8acc499bb0b", + "rtype": "public_image" + } + }, + "group": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "a651bf69-b709-4aad-9794-9f1de7a3a314", + "id_v1": "/scenes/-iVnLc4X7dEMWkt", + "metadata": { + "name": "Spring blossom", + "image": { + "rid": "adfa9c3e-e9aa-4b65-b9d3-c5b2c0576715", + "rtype": "public_image" + } + }, + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + }, + "color_temperature": { + "mirek": 215 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + }, + "color_temperature": { + "mirek": 215 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + }, + "color_temperature": { + "mirek": 215 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + }, + "color_temperature": { + "mirek": 215 + } + } + }, + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.3517, + "y": 0.3154 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.359, + "y": 0.2925 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.3744, + "y": 0.2692 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.4435, + "y": 0.2537 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.514, + "y": 0.3338 + } + }, + "dimming": { + "brightness": 81.1 + } + } + ], + "dimming": [ + { + "brightness": 81.1 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 198 + }, + "dimming": { + "brightness": 81.1 + } + } + ] + }, + "speed": 0.5595238095238095, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "86e00f0d-4072-46e9-bf9c-bb41a7b5c798", + "id_v1": "/scenes/3hvPcr7RQgG1TLS", + "metadata": { + "name": "Blood Moon", + "image": { + "rid": "324509da-25d2-4d22-abab-e9091c0efb37", + "rtype": "public_image" + } + }, + "group": { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 19.29 + }, + "color": { + "xy": { + "x": 0.5274, + "y": 0.2947 + } + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 19.29 + }, + "color": { + "xy": { + "x": 0.1752, + "y": 0.0553 + } + } + } + }, + { + "target": { + "rid": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + }, + { + "target": { + "rid": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.1596, + "y": 0.0684 + } + }, + "dimming": { + "brightness": 19.69 + } + }, + { + "color": { + "xy": { + "x": 0.2262, + "y": 0.1898 + } + }, + "dimming": { + "brightness": 19.69 + } + }, + { + "color": { + "xy": { + "x": 0.2826, + "y": 0.1763 + } + }, + "dimming": { + "brightness": 19.69 + } + }, + { + "color": { + "xy": { + "x": 0.3946, + "y": 0.2347 + } + }, + "dimming": { + "brightness": 19.69 + } + }, + { + "color": { + "xy": { + "x": 0.4644, + "y": 0.3458 + } + }, + "dimming": { + "brightness": 19.69 + } + } + ], + "dimming": [ + { + "brightness": 19.69 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 192 + }, + "dimming": { + "brightness": 19.69 + } + } + ] + }, + "speed": 0.626984126984127, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "b486fbab-4a4b-4cf6-9b26-785e8c37be66", + "id_v1": "/scenes/REK9wXlvjo7gaxj", + "metadata": { + "name": "Energize", + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + } + }, + "group": { + "rid": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "e9676a8a-f73b-4b29-a0c3-d8a462bc30fe", + "id_v1": "/scenes/y9YEAoXXqWFDqf5", + "metadata": { + "name": "Savanna sunset", + "image": { + "rid": "4f2ed241-5aea-4c9d-8028-55d2b111e06f", + "rtype": "public_image" + } + }, + "group": { + "rid": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color": { + "xy": { + "x": 0.4639, + "y": 0.4512 + } + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color": { + "xy": { + "x": 0.4639, + "y": 0.4512 + } + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.6563, + "y": 0.3211 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5862, + "y": 0.3575 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5502, + "y": 0.3655 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4577, + "y": 0.4563 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4162, + "y": 0.4341 + } + }, + "dimming": { + "brightness": 80.71 + } + } + ], + "dimming": [ + { + "brightness": 80.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 373 + }, + "dimming": { + "brightness": 80.71 + } + } + ] + }, + "speed": 0.6190476190476191, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "ac76f7cb-3003-445c-b493-ca97edba26fd", + "id_v1": "/scenes/BgE5AxNzExaobqK", + "metadata": { + "name": "Read", + "image": { + "rid": "e101a77f-9984-4f61-aac8-15741983c656", + "rtype": "public_image" + } + }, + "group": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "4779c247-1aab-49cb-8dc0-8baf199bf1d1", + "id_v1": "/scenes/GW8Rpcy64E3rgxk", + "metadata": { + "name": "Arctic aurora", + "image": { + "rid": "1e42b2e8-d02e-40d2-9c8d-b1fd8216c686", + "rtype": "public_image" + } + }, + "group": { + "rid": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + }, + "color": { + "xy": { + "x": 0.3099, + "y": 0.3224 + } + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + }, + "color": { + "xy": { + "x": 0.168, + "y": 0.041 + } + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.2439, + "y": 0.3791 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1654, + "y": 0.3959 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1829, + "y": 0.3021 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1559, + "y": 0.2699 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.2004, + "y": 0.2469 + } + }, + "dimming": { + "brightness": 30.71 + } + } + ], + "dimming": [ + { + "brightness": 30.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 154 + }, + "dimming": { + "brightness": 30.71 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "a932d019-d0bc-4725-894d-b957e721a535", + "id_v1": "/scenes/9CP1XXAKIt2WeQ-", + "metadata": { + "name": "Bright", + "image": { + "rid": "732ff1d9-76a7-4630-aad0-c8acc499bb0b", + "rtype": "public_image" + } + }, + "group": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "3b18195f-7621-4a7a-b7c0-c2d71b6ff0b4", + "id_v1": "/scenes/9ZFMQUAEtlbWFJA", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "d0f9e745-501e-4e6e-98de-8747fadf38a6", + "id_v1": "/scenes/JOx7KswVUYOV1yI", + "metadata": { + "name": "Concentrate", + "image": { + "rid": "b90c8900-a6b7-422c-a5d3-e170187dbf8c", + "rtype": "public_image" + } + }, + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + }, + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "be462a6f-817d-4cfc-bb4e-5dcd21a3678f", + "id_v1": "/scenes/he1cZDGlLN36nIj", + "metadata": { + "name": "Bright", + "image": { + "rid": "732ff1d9-76a7-4630-aad0-c8acc499bb0b", + "rtype": "public_image" + } + }, + "group": { + "rid": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "004b62ef-8254-4f0a-9355-8d4b1cac19a0", + "id_v1": "/scenes/5MC4H676nudrqq2", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color_temperature": { + "mirek": 447 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "e8a19005-f1e2-4fdc-86ae-8bd9137da95d", + "id_v1": "/scenes/POHXckB3LySitjo", + "metadata": { + "name": "Concentrate", + "image": { + "rid": "b90c8900-a6b7-422c-a5d3-e170187dbf8c", + "rtype": "public_image" + } + }, + "group": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "a8a88bc0-e610-4c67-b0b9-d1376f733431", + "id_v1": "/scenes/xrNPkPJ-kyhG0dW", + "metadata": { + "name": "Orange" + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color": { + "xy": { + "x": 0.5605, + "y": 0.4052 + } + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "1032cca9-db78-4635-861a-d7c83ee8b218", + "id_v1": "/scenes/btYzWUVLRd5KMaB", + "metadata": { + "name": "Tropical twilight", + "image": { + "rid": "a6a03e6a-fe6e-45bc-b686-878137f3ba91", + "rtype": "public_image" + } + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color": { + "xy": { + "x": 0.6006, + "y": 0.3758 + } + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.5802, + "y": 0.3952 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.5632, + "y": 0.3841 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.4563, + "y": 0.3607 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.3632, + "y": 0.2877 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.294, + "y": 0.223 + } + }, + "dimming": { + "brightness": 43.7 + } + } + ], + "dimming": [ + { + "brightness": 43.7 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 306 + }, + "dimming": { + "brightness": 43.7 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "b2adc4ca-1c72-4aa6-977a-4b90f70ec6eb", + "id_v1": "/scenes/w4VZEH9lsHLNNyr", + "metadata": { + "name": "Tropical twilight", + "image": { + "rid": "a6a03e6a-fe6e-45bc-b686-878137f3ba91", + "rtype": "public_image" + } + }, + "group": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 73.23 + }, + "color_temperature": { + "mirek": 322 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.5802, + "y": 0.3952 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.5632, + "y": 0.3841 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.4563, + "y": 0.3607 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.3632, + "y": 0.2877 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.294, + "y": 0.223 + } + }, + "dimming": { + "brightness": 43.7 + } + } + ], + "dimming": [ + { + "brightness": 43.7 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 306 + }, + "dimming": { + "brightness": 43.7 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "03bdbe65-92ef-4994-9ddd-68b51886682d", + "id_v1": "/scenes/dNF9SpYiUwMStYM", + "metadata": { + "name": "Energize", + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + } + }, + "group": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "61c925d0-65f8-4e92-9743-767baf98e3a2", + "id_v1": "/scenes/VWlT11NTAPA5oOq", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "8cec1e2f-bcc9-45c9-a0aa-bc9c30c68b64", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "c42f8220-f232-4400-910c-943547513827", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "1e32f0d3-f266-4cbe-94c8-7712bef5ab80", + "id_v1": "/scenes/xHILbiXDomWM6ps", + "metadata": { + "name": "Energize", + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + } + }, + "group": { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + }, + { + "target": { + "rid": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + }, + { + "target": { + "rid": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "08c34a6a-432a-4ff7-864d-bd2f5ae02ef2", + "id_v1": "/scenes/jx961tuvlluVnmP", + "metadata": { + "name": "Tropical twilight", + "image": { + "rid": "a6a03e6a-fe6e-45bc-b686-878137f3ba91", + "rtype": "public_image" + } + }, + "group": { + "rid": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color": { + "xy": { + "x": 0.3591, + "y": 0.2899 + } + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color": { + "xy": { + "x": 0.3591, + "y": 0.2899 + } + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.5802, + "y": 0.3952 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.5632, + "y": 0.3841 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.4563, + "y": 0.3607 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.3632, + "y": 0.2877 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.294, + "y": 0.223 + } + }, + "dimming": { + "brightness": 43.7 + } + } + ], + "dimming": [ + { + "brightness": 43.7 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 306 + }, + "dimming": { + "brightness": 43.7 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "a6bf71d2-607d-4061-b41d-870189394080", + "id_v1": "/scenes/IJVgF8v377r9q5f", + "metadata": { + "name": "Pink" + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color": { + "xy": { + "x": 0.3685, + "y": 0.2595 + } + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "11a794bf-a906-490c-b372-584e0087b8db", + "id_v1": "/scenes/xFPltbGflChILU-", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + }, + { + "target": { + "rid": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "0714ce98-784f-455d-bf9c-e35979abc21d", + "id_v1": "/scenes/4EKZ7vUBMTMyUS3", + "metadata": { + "name": "Read", + "image": { + "rid": "e101a77f-9984-4f61-aac8-15741983c656", + "rtype": "public_image" + } + }, + "group": { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + }, + { + "target": { + "rid": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + }, + { + "target": { + "rid": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "fc923c4c-0614-4563-8c02-ae954b045151", + "id_v1": "/scenes/cGQFOjuM-6DkTD6", + "metadata": { + "name": "Spring blossom", + "image": { + "rid": "adfa9c3e-e9aa-4b65-b9d3-c5b2c0576715", + "rtype": "public_image" + } + }, + "group": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 82.28 + }, + "color_temperature": { + "mirek": 215 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.3517, + "y": 0.3154 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.359, + "y": 0.2925 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.3744, + "y": 0.2692 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.4435, + "y": 0.2537 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.514, + "y": 0.3338 + } + }, + "dimming": { + "brightness": 81.1 + } + } + ], + "dimming": [ + { + "brightness": 81.1 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 198 + }, + "dimming": { + "brightness": 81.1 + } + } + ] + }, + "speed": 0.5595238095238095, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "4f88dafa-c907-4767-b447-87a0144ba02a", + "id_v1": "/scenes/gytZzE8uY5rBK6J", + "metadata": { + "name": "Relax", + "image": { + "rid": "a1f7da49-d181-4328-abea-68c9dc4b5416", + "rtype": "public_image" + } + }, + "group": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [ + { + "color_temperature": { + "mirek": 447 + }, + "dimming": { + "brightness": 56.69 + } + } + ] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "58117220-a2e6-48e5-adcc-6ad0b1774d20", + "id_v1": "/scenes/0vOv579Z3SALB6G", + "metadata": { + "name": "Chinatown", + "image": { + "rid": "63d50cd6-5909-4f7b-8810-137d08f57c54", + "rtype": "public_image" + } + }, + "group": { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 79.53 + }, + "color": { + "xy": { + "x": 0.6152, + "y": 0.3137 + } + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 79.53 + }, + "color": { + "xy": { + "x": 0.674, + "y": 0.322 + } + } + } + }, + { + "target": { + "rid": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + }, + { + "target": { + "rid": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.698, + "y": 0.2984 + } + }, + "dimming": { + "brightness": 79.92 + } + }, + { + "color": { + "xy": { + "x": 0.6731, + "y": 0.3044 + } + }, + "dimming": { + "brightness": 79.92 + } + }, + { + "color": { + "xy": { + "x": 0.6362, + "y": 0.2999 + } + }, + "dimming": { + "brightness": 79.92 + } + }, + { + "color": { + "xy": { + "x": 0.5262, + "y": 0.333 + } + }, + "dimming": { + "brightness": 79.92 + } + }, + { + "color": { + "xy": { + "x": 0.5682, + "y": 0.4183 + } + }, + "dimming": { + "brightness": 79.92 + } + } + ], + "dimming": [ + { + "brightness": 79.92 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 307 + }, + "dimming": { + "brightness": 79.92 + } + } + ] + }, + "speed": 0.6111111111111112, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "9b8dc566-ad3d-46f1-9d19-3d06dbcbc659", + "id_v1": "/scenes/N-KyYPFXZ9T-F2o", + "metadata": { + "name": "Energize", + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + } + }, + "group": { + "rid": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "afff84f7-0d5c-40dd-b76b-e64a48031f10", + "id_v1": "/scenes/kLbiQWOUK7h6hfQ", + "metadata": { + "name": "Energize", + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + } + }, + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + }, + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "c7f71ff1-c11b-428a-94c6-a162f7bfa5fe", + "id_v1": "/scenes/2Vspbhqh8AyilTu", + "metadata": { + "name": "Arctic aurora", + "image": { + "rid": "1e42b2e8-d02e-40d2-9c8d-b1fd8216c686", + "rtype": "public_image" + } + }, + "group": { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color": { + "xy": { + "x": 0.3197, + "y": 0.3419 + } + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color": { + "xy": { + "x": 0.2403, + "y": 0.1843 + } + } + } + }, + { + "target": { + "rid": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + }, + { + "target": { + "rid": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.2439, + "y": 0.3791 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1654, + "y": 0.3959 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1829, + "y": 0.3021 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1559, + "y": 0.2699 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.2004, + "y": 0.2469 + } + }, + "dimming": { + "brightness": 30.71 + } + } + ], + "dimming": [ + { + "brightness": 30.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 154 + }, + "dimming": { + "brightness": 30.71 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "c7ad975f-2701-4323-91b0-823275412e30", + "id_v1": "/scenes/EPfpdYvsekCDsXe", + "metadata": { + "name": "Spring blossom", + "image": { + "rid": "adfa9c3e-e9aa-4b65-b9d3-c5b2c0576715", + "rtype": "public_image" + } + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + }, + "color": { + "xy": { + "x": 0.4633, + "y": 0.274 + } + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 83.46 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 83.46 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.3517, + "y": 0.3154 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.359, + "y": 0.2925 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.3744, + "y": 0.2692 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.4435, + "y": 0.2537 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.514, + "y": 0.3338 + } + }, + "dimming": { + "brightness": 81.1 + } + } + ], + "dimming": [ + { + "brightness": 81.1 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 198 + }, + "dimming": { + "brightness": 81.1 + } + } + ] + }, + "speed": 0.5595238095238095, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "e16e34e7-09c0-4e20-a478-306c3179faf8", + "id_v1": "/scenes/4zBck3smKRsSzhZ", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color": { + "xy": { + "x": 0.561, + "y": 0.4042 + } + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "61f49072-d00e-44e0-b915-f324f8a8ef34", + "id_v1": "/scenes/7KPyxSMxAT-QcFo", + "metadata": { + "name": "Ibiza", + "image": { + "rid": "01e1d138-fc77-44e1-8012-c04fbae878e4", + "rtype": "public_image" + } + }, + "group": { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color": { + "xy": { + "x": 0.5667, + "y": 0.3887 + } + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color": { + "xy": { + "x": 0.5667, + "y": 0.3887 + } + } + } + }, + { + "target": { + "rid": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + }, + { + "target": { + "rid": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.6023, + "y": 0.3259 + } + }, + "dimming": { + "brightness": 48.82 + } + }, + { + "color": { + "xy": { + "x": 0.5714, + "y": 0.3806 + } + }, + "dimming": { + "brightness": 48.82 + } + }, + { + "color": { + "xy": { + "x": 0.5679, + "y": 0.3975 + } + }, + "dimming": { + "brightness": 48.82 + } + }, + { + "color": { + "xy": { + "x": 0.4787, + "y": 0.4495 + } + }, + "dimming": { + "brightness": 48.82 + } + }, + { + "color": { + "xy": { + "x": 0.4376, + "y": 0.4577 + } + }, + "dimming": { + "brightness": 48.82 + } + } + ], + "dimming": [ + { + "brightness": 48.82 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 387 + }, + "dimming": { + "brightness": 48.82 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "cf513e14-69c3-49a2-b6d7-040b76940c0e", + "id_v1": "/scenes/zbpm4NDZI63hcsD", + "metadata": { + "name": "Arctic aurora", + "image": { + "rid": "1e42b2e8-d02e-40d2-9c8d-b1fd8216c686", + "rtype": "public_image" + } + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + }, + "color": { + "xy": { + "x": 0.3102, + "y": 0.323 + } + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 58.66 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 58.66 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.2439, + "y": 0.3791 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1654, + "y": 0.3959 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1829, + "y": 0.3021 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1559, + "y": 0.2699 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.2004, + "y": 0.2469 + } + }, + "dimming": { + "brightness": 30.71 + } + } + ], + "dimming": [ + { + "brightness": 30.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 154 + }, + "dimming": { + "brightness": 30.71 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "b86d73d8-a0f2-4adb-a029-d94f5373b6f1", + "id_v1": "/scenes/cpeQJ7-FcxZ8rwE", + "metadata": { + "name": "Bright", + "image": { + "rid": "732ff1d9-76a7-4630-aad0-c8acc499bb0b", + "rtype": "public_image" + } + }, + "group": { + "rid": "dda859a6-f358-48f5-8d34-e13b04bf6e62", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "5e653b57-c047-4dab-b6bf-d18e4ba10c9a", + "id_v1": "/scenes/Z6ynZHxC6ygRVUs", + "metadata": { + "name": "Concentrate", + "image": { + "rid": "b90c8900-a6b7-422c-a5d3-e170187dbf8c", + "rtype": "public_image" + } + }, + "group": { + "rid": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "205c9083-3b30-4c8a-a3b3-36f54dbdafc1", + "id_v1": "/scenes/TpNvJj8ThRGzJRD", + "metadata": { + "name": "Bright", + "image": { + "rid": "732ff1d9-76a7-4630-aad0-c8acc499bb0b", + "rtype": "public_image" + } + }, + "group": { + "rid": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "754708c8-6247-4247-99aa-b601dd2ff138", + "id_v1": "/scenes/u7dR4dFWm0CZCqp", + "metadata": { + "name": "Energize", + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + } + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "204bdf64-dd8b-4cab-b594-bd77e4d1eed2", + "id_v1": "/scenes/bbGnkmVQ6qgnxWZ", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "dcbc740d-1e4f-48aa-ad02-3e17f1f4eebb", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "c43e95e6-b268-4e9c-8fc6-cb092e5100d0", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "c8ec2639-cc81-4316-af05-59d705f4babe", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "bd26fd22-a73c-4ba0-b4c7-044046ae8ca4", + "id_v1": "/scenes/iwN3rLPBukzpexb", + "metadata": { + "name": "Bright", + "image": { + "rid": "732ff1d9-76a7-4630-aad0-c8acc499bb0b", + "rtype": "public_image" + } + }, + "group": { + "rid": "bdc282b3-750d-45dd-b6c4-12a2927d8951", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "34d5fa77-f1a8-459c-849a-b95ba7b49cd1", + "id_v1": "/scenes/O6NRPvvjNpyPAWF", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "5c9542f7-dd3b-460a-9b34-67d81bc2e46d", + "id_v1": "/scenes/wCyk6fFuzfes3Fi", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "69a618dd-bd34-4077-923e-1264d967019a", + "id_v1": "/scenes/334V-MN1o-BcWXA", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "8cec1e2f-bcc9-45c9-a0aa-bc9c30c68b64", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "c42f8220-f232-4400-910c-943547513827", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "01e37e27-f686-40b2-a242-cb39455f7d49", + "id_v1": "/scenes/JNLQpHEmKPKLlj4", + "metadata": { + "name": "Lilac" + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color": { + "xy": { + "x": 0.2302, + "y": 0.1045 + } + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "11b8029f-4178-42be-bba6-dcde66c9cd6b", + "id_v1": "/scenes/NSLR4yoZ-DwGsJN", + "metadata": { + "name": "Energize", + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + } + }, + "group": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "5ea60a5f-b748-4046-a157-daeb72bac061", + "id_v1": "/scenes/A4MJ5n4yDvWGnkY", + "metadata": { + "name": "Tropical twilight", + "image": { + "rid": "a6a03e6a-fe6e-45bc-b686-878137f3ba91", + "rtype": "public_image" + } + }, + "group": { + "rid": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color_temperature": { + "mirek": 322 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color_temperature": { + "mirek": 322 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color_temperature": { + "mirek": 322 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color_temperature": { + "mirek": 322 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.5802, + "y": 0.3952 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.5632, + "y": 0.3841 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.4563, + "y": 0.3607 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.3632, + "y": 0.2877 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.294, + "y": 0.223 + } + }, + "dimming": { + "brightness": 43.7 + } + } + ], + "dimming": [ + { + "brightness": 43.7 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 306 + }, + "dimming": { + "brightness": 43.7 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "e7c87048-430d-4ec0-be45-a0b7a065036d", + "id_v1": "/scenes/KSyNvHPez7pBdTm", + "metadata": { + "name": "Arctic aurora", + "image": { + "rid": "1e42b2e8-d02e-40d2-9c8d-b1fd8216c686", + "rtype": "public_image" + } + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + }, + "color": { + "xy": { + "x": 0.239, + "y": 0.1817 + } + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.2439, + "y": 0.3791 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1654, + "y": 0.3959 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1829, + "y": 0.3021 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1559, + "y": 0.2699 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.2004, + "y": 0.2469 + } + }, + "dimming": { + "brightness": 30.71 + } + } + ], + "dimming": [ + { + "brightness": 30.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 154 + }, + "dimming": { + "brightness": 30.71 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "d6b60214-b89d-4bcf-82ea-7c5359a03cf5", + "id_v1": "/scenes/0wV1ZF5X3dzzQ3W", + "metadata": { + "name": "Arctic aurora", + "image": { + "rid": "1e42b2e8-d02e-40d2-9c8d-b1fd8216c686", + "rtype": "public_image" + } + }, + "group": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 58.66 + }, + "color_temperature": { + "mirek": 153 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.2439, + "y": 0.3791 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1654, + "y": 0.3959 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1829, + "y": 0.3021 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1559, + "y": 0.2699 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.2004, + "y": 0.2469 + } + }, + "dimming": { + "brightness": 30.71 + } + } + ], + "dimming": [ + { + "brightness": 30.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 154 + }, + "dimming": { + "brightness": 30.71 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "cecc6b2e-e985-4af1-8044-1c02255f95d8", + "id_v1": "/scenes/qqRoAPIkwVHdgbD", + "metadata": { + "name": "Savanna sunset", + "image": { + "rid": "4f2ed241-5aea-4c9d-8028-55d2b111e06f", + "rtype": "public_image" + } + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color": { + "xy": { + "x": 0.6405, + "y": 0.3336 + } + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 92.91 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 92.91 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.6563, + "y": 0.3211 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5862, + "y": 0.3575 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5502, + "y": 0.3655 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4577, + "y": 0.4563 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4162, + "y": 0.4341 + } + }, + "dimming": { + "brightness": 80.71 + } + } + ], + "dimming": [ + { + "brightness": 80.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 373 + }, + "dimming": { + "brightness": 80.71 + } + } + ] + }, + "speed": 0.6190476190476191, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "2080f5c3-2574-49f2-a0d1-670ee646f287", + "id_v1": "/scenes/cSdjCkxVKaa7Hd8", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "317c5030-db6d-4aae-8aac-053d297ab68e", + "id_v1": "/scenes/55rCprgqZfoXSuf", + "metadata": { + "name": "Savanna sunset", + "image": { + "rid": "4f2ed241-5aea-4c9d-8028-55d2b111e06f", + "rtype": "public_image" + } + }, + "group": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 92.91 + }, + "color_temperature": { + "mirek": 387 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.6563, + "y": 0.3211 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5862, + "y": 0.3575 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5502, + "y": 0.3655 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4577, + "y": 0.4563 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4162, + "y": 0.4341 + } + }, + "dimming": { + "brightness": 80.71 + } + } + ], + "dimming": [ + { + "brightness": 80.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 373 + }, + "dimming": { + "brightness": 80.71 + } + } + ] + }, + "speed": 0.6190476190476191, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "fe60b55d-afd2-459f-8113-49a2f43ba1a6", + "id_v1": "/scenes/AZXwPrYdbDJknjY", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "7c5b8fda-abcf-474a-9911-977c86ab772b", + "id_v1": "/scenes/pXBO-PaMtBcJOCJ", + "metadata": { + "name": "Bright", + "image": { + "rid": "732ff1d9-76a7-4630-aad0-c8acc499bb0b", + "rtype": "public_image" + } + }, + "group": { + "rid": "dcbc740d-1e4f-48aa-ad02-3e17f1f4eebb", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "c43e95e6-b268-4e9c-8fc6-cb092e5100d0", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "c8ec2639-cc81-4316-af05-59d705f4babe", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "21a1a402-e584-497b-95f5-30e78769581a", + "id_v1": "/scenes/P-ckmBqpk8IzfCm", + "metadata": { + "name": "Bright", + "image": { + "rid": "732ff1d9-76a7-4630-aad0-c8acc499bb0b", + "rtype": "public_image" + } + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "fd9557b1-bfaf-4898-a7d8-a25a8f592b87", + "id_v1": "/scenes/yiErv0x5QBypAkJ", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "2eae1e67-4345-4f65-821a-3958312482e2", + "id_v1": "/scenes/8xvkaBO161s54YF", + "metadata": { + "name": "Read", + "image": { + "rid": "e101a77f-9984-4f61-aac8-15741983c656", + "rtype": "public_image" + } + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "f9031126-43d7-4264-bc5f-36c5402cdfdd", + "id_v1": "/scenes/9AHQwcdPBrVAVWy", + "metadata": { + "name": "Read", + "image": { + "rid": "e101a77f-9984-4f61-aac8-15741983c656", + "rtype": "public_image" + } + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "56066c28-1980-4141-a091-e7791387db44", + "id_v1": "/scenes/ZuOFvCQd9uSjfyN", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "f359519b-1ab6-47bd-8c9c-36386be75f3d", + "id_v1": "/scenes/B2wQY7FZPVOKMiE", + "metadata": { + "name": "Green" + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color": { + "xy": { + "x": 0.4449, + "y": 0.4875 + } + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "8e3f3596-e32a-4d4d-8119-1144cee0d6c1", + "id_v1": "/scenes/gZRNvqVUEF8zbYX", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color_temperature": { + "mirek": 447 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "a354f0f1-e226-46f2-ab6a-4dd10f00a996", + "id_v1": "/scenes/8mCSXK20LTVPSgy", + "metadata": { + "name": "Relax", + "image": { + "rid": "a1f7da49-d181-4328-abea-68c9dc4b5416", + "rtype": "public_image" + } + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "fe92dabf-e8af-4eff-a99f-7a76ac95e9dc", + "id_v1": "/scenes/lgtP2LF1IIttSN9", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color": { + "xy": { + "x": 0.561, + "y": 0.4042 + } + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color": { + "xy": { + "x": 0.561, + "y": 0.4042 + } + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "57ff4e48-f4aa-438b-a600-8fefc71afc26", + "id_v1": "/scenes/hYrzWTaumQLNoid", + "metadata": { + "name": "Tropical twilight", + "image": { + "rid": "a6a03e6a-fe6e-45bc-b686-878137f3ba91", + "rtype": "public_image" + } + }, + "group": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 73.23 + }, + "color_temperature": { + "mirek": 322 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.5802, + "y": 0.3952 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.5632, + "y": 0.3841 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.4563, + "y": 0.3607 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.3632, + "y": 0.2877 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.294, + "y": 0.223 + } + }, + "dimming": { + "brightness": 43.7 + } + } + ], + "dimming": [ + { + "brightness": 43.7 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 306 + }, + "dimming": { + "brightness": 43.7 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "436fed80-d22c-4769-b39e-1b07435bd7ad", + "id_v1": "/scenes/bONyAiQU0IfhT3Y", + "metadata": { + "name": "Christmas" + }, + "group": { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 75.2 + }, + "color": { + "xy": { + "x": 0.408, + "y": 0.517 + } + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 75.2 + }, + "color": { + "xy": { + "x": 0.533, + "y": 0.2996 + } + } + } + }, + { + "target": { + "rid": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + }, + { + "target": { + "rid": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "411771e8-2c7c-45c5-ac45-08777063283a", + "id_v1": "/scenes/neQSWRQ0iAFup5n", + "metadata": { + "name": "Intermediate" + }, + "group": { + "rid": "bdc282b3-750d-45dd-b6c4-12a2927d8951", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 66.14 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 66.14 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "7097791d-d4a2-49c5-95e3-82566f2ffd4d", + "id_v1": "/scenes/TLJ8kWVcJCrk0oc", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "dda859a6-f358-48f5-8d34-e13b04bf6e62", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "32d8107b-e86f-43a0-a4f1-a1c05dde6b31", + "id_v1": "/scenes/ruDj597bcMm4IRA", + "metadata": { + "name": "Bright", + "image": { + "rid": "732ff1d9-76a7-4630-aad0-c8acc499bb0b", + "rtype": "public_image" + } + }, + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "f4823577-ef28-4c5e-bef0-50aeabfda29f", + "id_v1": "/scenes/AQY4gUUAjMZMHof", + "metadata": { + "name": "Concentrate", + "image": { + "rid": "b90c8900-a6b7-422c-a5d3-e170187dbf8c", + "rtype": "public_image" + } + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "9bd115a5-90d3-4d5d-b39a-3b885142b8b4", + "id_v1": "/scenes/3gg6YPp2p6IkMud", + "metadata": { + "name": "Read", + "image": { + "rid": "e101a77f-9984-4f61-aac8-15741983c656", + "rtype": "public_image" + } + }, + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + }, + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "56038cc7-ca56-4066-9bad-7388c3ac5f0c", + "id_v1": "/scenes/TWhsalV46MFQDSg", + "metadata": { + "name": "Spring blossom", + "image": { + "rid": "adfa9c3e-e9aa-4b65-b9d3-c5b2c0576715", + "rtype": "public_image" + } + }, + "group": { + "rid": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + }, + "color_temperature": { + "mirek": 215 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + }, + "color_temperature": { + "mirek": 215 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + }, + "color_temperature": { + "mirek": 215 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 84.25 + }, + "color_temperature": { + "mirek": 215 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.3517, + "y": 0.3154 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.359, + "y": 0.2925 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.3744, + "y": 0.2692 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.4435, + "y": 0.2537 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.514, + "y": 0.3338 + } + }, + "dimming": { + "brightness": 81.1 + } + } + ], + "dimming": [ + { + "brightness": 81.1 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 198 + }, + "dimming": { + "brightness": 81.1 + } + } + ] + }, + "speed": 0.5595238095238095, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "09aba9d9-8671-443f-910a-fd3709269fb2", + "id_v1": "/scenes/iLPtf3PacLIl-Ge", + "metadata": { + "name": "Concentrate", + "image": { + "rid": "b90c8900-a6b7-422c-a5d3-e170187dbf8c", + "rtype": "public_image" + } + }, + "group": { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + }, + { + "target": { + "rid": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + }, + { + "target": { + "rid": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "fda3b02f-8b2e-4a66-a240-279268cd6957", + "id_v1": "/scenes/f0W0SP-36ET3HpZ", + "metadata": { + "name": "Energize", + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + } + }, + "group": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + }, + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 156 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "b69dbb01-ebca-4125-93f8-30610c2dd967", + "id_v1": "/scenes/hliLF0c-Ij-Kmdp", + "metadata": { + "name": "Arctic aurora", + "image": { + "rid": "1e42b2e8-d02e-40d2-9c8d-b1fd8216c686", + "rtype": "public_image" + } + }, + "group": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 58.66 + }, + "color_temperature": { + "mirek": 153 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.2439, + "y": 0.3791 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1654, + "y": 0.3959 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1829, + "y": 0.3021 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1559, + "y": 0.2699 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.2004, + "y": 0.2469 + } + }, + "dimming": { + "brightness": 30.71 + } + } + ], + "dimming": [ + { + "brightness": 30.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 154 + }, + "dimming": { + "brightness": 30.71 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "c2fc1e3d-db47-41aa-b740-e425728269f2", + "id_v1": "/scenes/2mS4Y86rpr2rJXk", + "metadata": { + "name": "Spring blossom", + "image": { + "rid": "adfa9c3e-e9aa-4b65-b9d3-c5b2c0576715", + "rtype": "public_image" + } + }, + "group": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 82.28 + }, + "color_temperature": { + "mirek": 215 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.3517, + "y": 0.3154 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.359, + "y": 0.2925 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.3744, + "y": 0.2692 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.4435, + "y": 0.2537 + } + }, + "dimming": { + "brightness": 81.1 + } + }, + { + "color": { + "xy": { + "x": 0.514, + "y": 0.3338 + } + }, + "dimming": { + "brightness": 81.1 + } + } + ], + "dimming": [ + { + "brightness": 81.1 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 198 + }, + "dimming": { + "brightness": 81.1 + } + } + ] + }, + "speed": 0.5595238095238095, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "11ac9c82-d031-43a6-a8d5-b6efdee72fe6", + "id_v1": "/scenes/KC78bdx3f0Ny0Zf", + "metadata": { + "name": "Relax", + "image": { + "rid": "a1f7da49-d181-4328-abea-68c9dc4b5416", + "rtype": "public_image" + } + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "6d79e623-684c-40fd-94ba-861e554bb5cd", + "id_v1": "/scenes/ZauTGpxOJ75STTH", + "metadata": { + "name": "Read", + "image": { + "rid": "e101a77f-9984-4f61-aac8-15741983c656", + "rtype": "public_image" + } + }, + "group": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 346 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "4ef2e4bf-51dd-4765-a543-2800d26f7379", + "id_v1": "/scenes/owWGUIpEiEAv1Ke", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color": { + "xy": { + "x": 0.561, + "y": 0.4042 + } + }, + "color_temperature": { + "mirek": 454 + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color": { + "xy": { + "x": 0.561, + "y": 0.4042 + } + }, + "color_temperature": { + "mirek": 454 + } + } + }, + { + "target": { + "rid": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + }, + { + "target": { + "rid": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "53912b9e-b755-4bee-b878-cb94eff5a69b", + "id_v1": "/scenes/JrAmKehf0cRqHsv", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "dda859a6-f358-48f5-8d34-e13b04bf6e62", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "71bc1833-7ce2-4115-9954-797cec09b0fb", + "id_v1": "/scenes/t3ZmyYqFaGt7v-N", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "bdc282b3-750d-45dd-b6c4-12a2927d8951", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "a6751127-bddb-4b9c-8af7-b3fa94262a09", + "id_v1": "/scenes/moIfmXvtnpE8Fzm", + "metadata": { + "name": "Savanna sunset", + "image": { + "rid": "4f2ed241-5aea-4c9d-8028-55d2b111e06f", + "rtype": "public_image" + } + }, + "group": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 92.91 + }, + "color_temperature": { + "mirek": 387 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.6563, + "y": 0.3211 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5862, + "y": 0.3575 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5502, + "y": 0.3655 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4577, + "y": 0.4563 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4162, + "y": 0.4341 + } + }, + "dimming": { + "brightness": 80.71 + } + } + ], + "dimming": [ + { + "brightness": 80.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 373 + }, + "dimming": { + "brightness": 80.71 + } + } + ] + }, + "speed": 0.6190476190476191, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "110a94f1-42df-4284-8ef4-14c297d48732", + "id_v1": "/scenes/FA8hx4n5ucU03YS", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color_temperature": { + "mirek": 447 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "038d2513-294d-428e-a70f-50f234420624", + "id_v1": "/scenes/UdB6F9Jh6QBCNHH", + "metadata": { + "name": "Savanna sunset", + "image": { + "rid": "4f2ed241-5aea-4c9d-8028-55d2b111e06f", + "rtype": "public_image" + } + }, + "group": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 92.91 + }, + "color_temperature": { + "mirek": 387 + } + } + }, + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 92.91 + }, + "color_temperature": { + "mirek": 387 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.6563, + "y": 0.3211 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5862, + "y": 0.3575 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5502, + "y": 0.3655 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4577, + "y": 0.4563 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4162, + "y": 0.4341 + } + }, + "dimming": { + "brightness": 80.71 + } + } + ], + "dimming": [ + { + "brightness": 80.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 373 + }, + "dimming": { + "brightness": 80.71 + } + } + ] + }, + "speed": 0.6190476190476191, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "5abf6055-5f4f-475f-b383-c03c4ff56790", + "id_v1": "/scenes/HGozgCaHKeEpBXp", + "metadata": { + "name": "Bright", + "image": { + "rid": "732ff1d9-76a7-4630-aad0-c8acc499bb0b", + "rtype": "public_image" + } + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "1f5edd4d-a6d5-4f18-8aca-6a225490616b", + "id_v1": "/scenes/28o7V3m7-YS5YB0", + "metadata": { + "name": "Green" + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 45.67 + }, + "color": { + "xy": { + "x": 0.3755, + "y": 0.4525 + } + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "8b65c749-3ad8-435e-a7ed-94e3cc99e9d7", + "id_v1": "/scenes/PHqOyGCggxevIWF", + "metadata": { + "name": "Relax", + "image": { + "rid": "a1f7da49-d181-4328-abea-68c9dc4b5416", + "rtype": "public_image" + } + }, + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "e580c411-c098-41bd-a23b-96656cf24ee8", + "id_v1": "/scenes/OiZRViJI-Bi4D3o", + "metadata": { + "name": "Dimmed", + "image": { + "rid": "8c74b9ba-6e89-4083-a2a7-b10a1e566fed", + "rtype": "public_image" + } + }, + "group": { + "rid": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 30.31 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "fe0f0705-085a-4935-9ade-be88dd1f0586", + "id_v1": "/scenes/aFqX1lOd1Bn2hH5", + "metadata": { + "name": "Tropical twilight", + "image": { + "rid": "a6a03e6a-fe6e-45bc-b686-878137f3ba91", + "rtype": "public_image" + } + }, + "group": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 73.23 + }, + "color_temperature": { + "mirek": 322 + } + } + }, + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 73.23 + }, + "color_temperature": { + "mirek": 322 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.5802, + "y": 0.3952 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.5632, + "y": 0.3841 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.4563, + "y": 0.3607 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.3632, + "y": 0.2877 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.294, + "y": 0.223 + } + }, + "dimming": { + "brightness": 43.7 + } + } + ], + "dimming": [ + { + "brightness": 43.7 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 306 + }, + "dimming": { + "brightness": 43.7 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "637417cd-24d1-47b5-a15c-4bed74e7bbb4", + "id_v1": "/scenes/5g7WYiUHH9A7H6s", + "metadata": { + "name": "Savanna sunset", + "image": { + "rid": "4f2ed241-5aea-4c9d-8028-55d2b111e06f", + "rtype": "public_image" + } + }, + "group": { + "rid": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color_temperature": { + "mirek": 385 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color_temperature": { + "mirek": 385 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color_temperature": { + "mirek": 385 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color_temperature": { + "mirek": 385 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.6563, + "y": 0.3211 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5862, + "y": 0.3575 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5502, + "y": 0.3655 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4577, + "y": 0.4563 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4162, + "y": 0.4341 + } + }, + "dimming": { + "brightness": 80.71 + } + } + ], + "dimming": [ + { + "brightness": 80.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 373 + }, + "dimming": { + "brightness": 80.71 + } + } + ] + }, + "speed": 0.6190476190476191, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "b41ed349-abf7-4a91-a4a2-46c39d677db5", + "id_v1": "/scenes/2CkNfGJ0hheJgPY", + "metadata": { + "name": "Savanna sunset", + "image": { + "rid": "4f2ed241-5aea-4c9d-8028-55d2b111e06f", + "rtype": "public_image" + } + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color": { + "xy": { + "x": 0.6405, + "y": 0.3336 + } + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.6563, + "y": 0.3211 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5862, + "y": 0.3575 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.5502, + "y": 0.3655 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4577, + "y": 0.4563 + } + }, + "dimming": { + "brightness": 80.71 + } + }, + { + "color": { + "xy": { + "x": 0.4162, + "y": 0.4341 + } + }, + "dimming": { + "brightness": 80.71 + } + } + ], + "dimming": [ + { + "brightness": 80.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 373 + }, + "dimming": { + "brightness": 80.71 + } + } + ] + }, + "speed": 0.6190476190476191, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "00877a17-38d1-44c4-884d-b362e3566c25", + "id_v1": "/scenes/OEUmdH71bnd1gHX", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "dcbc740d-1e4f-48aa-ad02-3e17f1f4eebb", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "c43e95e6-b268-4e9c-8fc6-cb092e5100d0", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + } + } + }, + { + "target": { + "rid": "c8ec2639-cc81-4316-af05-59d705f4babe", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "19d9cd0c-fac9-4bb4-845b-4b62886ec422", + "id_v1": "/scenes/2J2lIvm8aqeyVI-", + "metadata": { + "name": "Relax", + "image": { + "rid": "a1f7da49-d181-4328-abea-68c9dc4b5416", + "rtype": "public_image" + } + }, + "group": { + "rid": "c6da8ba8-123e-4d6c-ba58-576f9ac0d98b", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 56.69 + }, + "color_temperature": { + "mirek": 447 + } + } + }, + { + "target": { + "rid": "cd4a1b1d-aac0-4006-a5d3-5dea7a4e4043", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + }, + { + "target": { + "rid": "d2ce3e24-a5d0-499b-9a5a-f53e3ce349df", + "rtype": "light" + }, + "action": { + "on": { + "on": true + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "d8a79343-7961-4d7e-863d-954b24a284e5", + "id_v1": "/scenes/NA8Xwq6TOkehzAu", + "metadata": { + "name": "Concentrate", + "image": { + "rid": "b90c8900-a6b7-422c-a5d3-e170187dbf8c", + "rtype": "public_image" + } + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 233 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "688b0ee7-cec8-49f0-bb83-650e73cf6321", + "id_v1": "/scenes/Nc6wiaKAaT7cHfb", + "metadata": { + "name": "Arctic aurora", + "image": { + "rid": "1e42b2e8-d02e-40d2-9c8d-b1fd8216c686", + "rtype": "public_image" + } + }, + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + }, + "color_temperature": { + "mirek": 153 + } + } + }, + { + "target": { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + }, + "color_temperature": { + "mirek": 153 + } + } + }, + { + "target": { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + }, + "color_temperature": { + "mirek": 153 + } + } + }, + { + "target": { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + }, + "color_temperature": { + "mirek": 153 + } + } + }, + { + "target": { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 53.94 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.2439, + "y": 0.3791 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1654, + "y": 0.3959 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1829, + "y": 0.3021 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1559, + "y": 0.2699 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.2004, + "y": 0.2469 + } + }, + "dimming": { + "brightness": 30.71 + } + } + ], + "dimming": [ + { + "brightness": 30.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 154 + }, + "dimming": { + "brightness": 30.71 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "9515aa26-de9e-4a43-83a4-7a366527ae9d", + "id_v1": "/scenes/QFbY2NFjcSSKHI8", + "metadata": { + "name": "Arctic aurora", + "image": { + "rid": "1e42b2e8-d02e-40d2-9c8d-b1fd8216c686", + "rtype": "public_image" + } + }, + "group": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 58.66 + }, + "color_temperature": { + "mirek": 153 + } + } + }, + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 58.66 + }, + "color_temperature": { + "mirek": 153 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.2439, + "y": 0.3791 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1654, + "y": 0.3959 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1829, + "y": 0.3021 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.1559, + "y": 0.2699 + } + }, + "dimming": { + "brightness": 30.71 + } + }, + { + "color": { + "xy": { + "x": 0.2004, + "y": 0.2469 + } + }, + "dimming": { + "brightness": 30.71 + } + } + ], + "dimming": [ + { + "brightness": 30.71 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 154 + }, + "dimming": { + "brightness": 30.71 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "d2269edc-e7fb-4ac2-913c-264048dafa6e", + "id_v1": "/scenes/9IUgR3QglXUaA3y", + "metadata": { + "name": "Tropical twilight", + "image": { + "rid": "a6a03e6a-fe6e-45bc-b686-878137f3ba91", + "rtype": "public_image" + } + }, + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 48.43 + }, + "color": { + "xy": { + "x": 0.292, + "y": 0.2251 + } + } + } + }, + { + "target": { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 72.44 + } + } + }, + { + "target": { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 72.44 + } + } + } + ], + "palette": { + "color": [ + { + "color": { + "xy": { + "x": 0.5802, + "y": 0.3952 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.5632, + "y": 0.3841 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.4563, + "y": 0.3607 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.3632, + "y": 0.2877 + } + }, + "dimming": { + "brightness": 43.7 + } + }, + { + "color": { + "xy": { + "x": 0.294, + "y": 0.223 + } + }, + "dimming": { + "brightness": 43.7 + } + } + ], + "dimming": [ + { + "brightness": 43.7 + } + ], + "color_temperature": [ + { + "color_temperature": { + "mirek": 306 + }, + "dimming": { + "brightness": 43.7 + } + } + ] + }, + "speed": 0.6388888888888888, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "cf201e30-0ba7-45af-be5b-1dc48acda55c", + "id_v1": "/scenes/MngA-8-skyyxljp", + "metadata": { + "name": "Bright", + "image": { + "rid": "732ff1d9-76a7-4630-aad0-c8acc499bb0b", + "rtype": "public_image" + } + }, + "group": { + "rid": "1758470a-71b3-4d71-b992-cbb3c64d2d03", + "rtype": "room" + }, + "actions": [ + { + "target": { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + }, + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 100.0 + }, + "color_temperature": { + "mirek": 367 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "dc6ba1ad-5626-46f9-a4ec-806b038dca6c", + "id_v1": "/scenes/dLHe82Taw-sSaCl", + "metadata": { + "name": "Nightlight", + "image": { + "rid": "28bbfeff-1a0c-444e-bb4b-0b74b88e0c95", + "rtype": "public_image" + } + }, + "group": { + "rid": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 0.39 + }, + "color_temperature": { + "mirek": 447 + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + }, + { + "id": "3edc81b5-f26d-4e43-a493-b330e9bd8fbc", + "id_v1": "/scenes/hu7LG-5hCiCqHns", + "metadata": { + "name": "Pale blue" + }, + "group": { + "rid": "e3372839-3464-4be0-94a2-949433bc065c", + "rtype": "zone" + }, + "actions": [ + { + "target": { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + }, + "action": { + "on": { + "on": true + }, + "dimming": { + "brightness": 78.35 + }, + "color": { + "xy": { + "x": 0.3132, + "y": 0.3289 + } + } + } + } + ], + "palette": { + "color": [], + "dimming": [], + "color_temperature": [] + }, + "speed": 0.5, + "auto_dynamic": false, + "type": "scene" + } + ] +} diff --git a/bundles/org.openhab.binding.hue/src/test/resources/temperature.json b/bundles/org.openhab.binding.hue/src/test/resources/temperature.json new file mode 100644 index 000000000..5034d6aee --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/temperature.json @@ -0,0 +1,19 @@ +{ + "errors": [], + "data": [ + { + "id": "f5f8010d-8356-4604-88dc-4f73912cbb6a", + "id_v1": "/sensors/33", + "owner": { + "rid": "70660557-692d-4d37-8b6b-e3ec63716a72", + "rtype": "device" + }, + "enabled": true, + "temperature": { + "temperature": 17.26, + "temperature_valid": true + }, + "type": "temperature" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/zgp_connectivity.json b/bundles/org.openhab.binding.hue/src/test/resources/zgp_connectivity.json new file mode 100644 index 000000000..910d19868 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/zgp_connectivity.json @@ -0,0 +1,4 @@ +{ + "errors": [], + "data": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/zigbee_connectivity.json b/bundles/org.openhab.binding.hue/src/test/resources/zigbee_connectivity.json new file mode 100644 index 000000000..af6b8673d --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/zigbee_connectivity.json @@ -0,0 +1,390 @@ +{ + "errors": [], + "data": [ + { + "id": "3a9c03de-9305-443c-b075-39901e56f424", + "id_v1": "/sensors/110", + "owner": { + "rid": "112853f9-c4c4-4d65-ba96-b4c2ab26d94d", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:0b:00:0a:4c", + "type": "zigbee_connectivity" + }, + { + "id": "13105daf-e295-4c52-bdba-46d03e431782", + "id_v1": "/sensors/236", + "owner": { + "rid": "cfecbbd0-e918-42a2-b714-2bad33061d95", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:08:02:42:ff", + "type": "zigbee_connectivity" + }, + { + "id": "f4b23d32-0812-49fb-b134-0090dcfba2ba", + "id_v1": "/sensors/63", + "owner": { + "rid": "0e22f8de-eff5-440a-a9ed-06d547d125d7", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:08:01:27:b3", + "type": "zigbee_connectivity" + }, + { + "id": "14439da7-d362-41c8-b716-04a3839e3be0", + "id_v1": "/lights/5", + "owner": { + "rid": "01de467b-29a0-48fc-b711-fd9c079bd429", + "rtype": "device" + }, + "status": "connectivity_issue", + "mac_address": "00:17:88:01:03:5f:79:06", + "type": "zigbee_connectivity" + }, + { + "id": "9415083c-14fe-4902-a194-376170caf3e3", + "id_v1": "/sensors/6", + "owner": { + "rid": "b56191ea-8d18-4257-b5d2-cfc3fc86a1ee", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:04:e5:67:4f", + "type": "zigbee_connectivity" + }, + { + "id": "86837f1d-0bd9-41e6-a236-060eafc33d2f", + "id_v1": "/lights/16", + "owner": { + "rid": "18212397-8c4d-4373-8f59-c047b80994ac", + "rtype": "device" + }, + "status": "connected", + "mac_address": "d0:cf:5e:ff:fe:dc:da:16", + "type": "zigbee_connectivity" + }, + { + "id": "050e2508-7bdb-428c-a070-cdd002b12822", + "id_v1": "/sensors/24", + "owner": { + "rid": "b5fe0539-171c-4733-bf0b-244635a309be", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:06:35:f5:79", + "type": "zigbee_connectivity" + }, + { + "id": "50fe5b5f-fa28-4d05-97b2-b795bbbc6c21", + "id_v1": "/lights/20", + "owner": { + "rid": "0b4c9bdb-3f46-485b-8337-e5649c03b9e2", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:08:fc:fd:d5", + "type": "zigbee_connectivity" + }, + { + "id": "4a40ae7d-3d12-44bf-adbe-bb23f417e41f", + "id_v1": "/lights/10", + "owner": { + "rid": "adeb3425-6a6b-49a0-8262-129126de7941", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:02:36:f0:2b", + "type": "zigbee_connectivity" + }, + { + "id": "57b2f3bf-1fcf-442f-bcc9-ff93d0a266e1", + "id_v1": "/lights/15", + "owner": { + "rid": "2cf59d54-8624-445f-9a01-aac19682b954", + "rtype": "device" + }, + "status": "connected", + "mac_address": "90:fd:9f:ff:fe:03:07:80", + "type": "zigbee_connectivity" + }, + { + "id": "3eb848e9-fd04-48a6-a4fb-305af3b97ecf", + "id_v1": "/sensors/118", + "owner": { + "rid": "a0509519-3ecb-47d0-9183-25db1e4ea2b2", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:0b:00:0a:51", + "type": "zigbee_connectivity" + }, + { + "id": "35a857b1-a376-4f2c-80c8-7542d053fec8", + "id_v1": "/sensors/135", + "owner": { + "rid": "8c5b05ba-b4f4-47b2-8ba0-fc44363192bc", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:08:03:41:12", + "type": "zigbee_connectivity" + }, + { + "id": "37a1c077-0e0f-435d-a3d9-69110fc3791d", + "id_v1": "/sensors/8", + "owner": { + "rid": "68ff07d0-6543-4967-9889-e0bc0bc16c31", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:04:e7:c0:fd", + "type": "zigbee_connectivity" + }, + { + "id": "e5e5e388-35ac-4b4a-8e5c-ebeb10d18aee", + "id_v1": "/lights/18", + "owner": { + "rid": "d8da96f0-0637-40bc-a89d-65ac47bceb0a", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:06:1f:62:53", + "type": "zigbee_connectivity" + }, + { + "id": "141895c2-a4f7-4028-83c9-1e356358fb84", + "id_v1": "/sensors/18", + "owner": { + "rid": "96bec26d-d7c6-4c00-98cd-6f96733296a0", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:06:e9:98:f1", + "type": "zigbee_connectivity" + }, + { + "id": "fa249694-315a-43c3-88ca-b3fe8ac9d683", + "id_v1": "/lights/3", + "owner": { + "rid": "7bf2175d-0bb8-48dc-a7b4-775d3af3dfc9", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:10:3b:f2:68", + "type": "zigbee_connectivity" + }, + { + "id": "047a7502-fbb9-49b4-8324-8329502de717", + "id_v1": "/sensors/4", + "owner": { + "rid": "81d9a9d5-228c-45df-828e-0d224929b3d1", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:04:e5:67:42", + "type": "zigbee_connectivity" + }, + { + "id": "78c09361-22b0-4054-87ee-43754cfa5607", + "id_v1": "/lights/6", + "owner": { + "rid": "37c25501-53e4-4e01-b1bb-2f5ee6e7e258", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:04:c3:17:68", + "type": "zigbee_connectivity" + }, + { + "id": "2f7ae216-0fae-4f91-9fea-418b99149374", + "id_v1": "/lights/21", + "owner": { + "rid": "78c2c794-7bdd-4a95-a7e8-4ee7b9af28bd", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:08:fb:48:1e", + "type": "zigbee_connectivity" + }, + { + "id": "2ca08a39-c00e-4042-97bd-6dfbdb0dd61a", + "id_v1": "/lights/8", + "owner": { + "rid": "346a2c5a-b736-497e-aed2-0dd7a7daff52", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:04:47:b9:ab", + "type": "zigbee_connectivity" + }, + { + "id": "4b1eb54c-2756-4f20-b8e3-1375cd47da81", + "id_v1": "/lights/17", + "owner": { + "rid": "9680e1fa-3b1d-4979-94ba-1a0a0e7a47b8", + "rtype": "device" + }, + "status": "connected", + "mac_address": "90:fd:9f:ff:fe:76:99:59", + "type": "zigbee_connectivity" + }, + { + "id": "f975c0b9-0f57-4f97-ab23-826e370b0a7d", + "id_v1": "/lights/9", + "owner": { + "rid": "055f6ba3-0354-4765-b23e-287b505f2cd2", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:04:47:ce:dc", + "type": "zigbee_connectivity" + }, + { + "id": "3f9e9e41-5e2b-4cc6-8b36-8876f5d049e8", + "id_v1": "/sensors/245", + "owner": { + "rid": "56b560bc-a127-4634-8d80-9946104a4028", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:08:01:99:25", + "type": "zigbee_connectivity" + }, + { + "id": "49d03c43-75ef-415b-88d0-264e6c2ea748", + "id_v1": "/sensors/12", + "owner": { + "rid": "a1155885-4bbe-469f-83bb-f964f8e13e82", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:04:f1:b2:55", + "type": "zigbee_connectivity" + }, + { + "id": "e2633d2e-fcbc-4a7e-852e-f46d20ca8f21", + "id_v1": "/sensors/20", + "owner": { + "rid": "e130feac-3a5c-452e-a97d-5bca470783b3", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:06:e9:99:5e", + "type": "zigbee_connectivity" + }, + { + "id": "e533ace8-78a4-46bd-9491-8821cc9960cd", + "id_v1": "/lights/7", + "owner": { + "rid": "266759fc-aaac-4eea-af5d-f226a146c119", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:04:c3:0a:77", + "type": "zigbee_connectivity" + }, + { + "id": "272f9a1c-b99f-4014-a82c-9d6fed39f19b", + "id_v1": "/sensors/26", + "owner": { + "rid": "e12b3d9c-0b23-4edd-a967-66f5b5cefa92", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:06:35:f5:27", + "type": "zigbee_connectivity" + }, + { + "id": "b3e07aed-51c0-45c4-89dc-643e684a1543", + "id_v1": "/lights/4", + "owner": { + "rid": "0d47bd3d-d82b-4a21-893c-299bff18e22a", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:03:0b:af:21", + "type": "zigbee_connectivity" + }, + { + "id": "8ade55bf-0e12-4726-be31-8f3645ad519f", + "id_v1": "/lights/2", + "owner": { + "rid": "e0aafa22-59ab-4467-b603-632d92d2c15b", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:10:54:a1:d6", + "type": "zigbee_connectivity" + }, + { + "id": "ea1571fe-9c39-45a8-b20b-63834fea16bd", + "id_v1": "", + "owner": { + "rid": "f4c5c816-925b-4e22-a112-2b44a23f5613", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:01:61:57:c7", + "type": "zigbee_connectivity" + }, + { + "id": "f0040018-1c7a-49cc-b640-ba62675eced2", + "id_v1": "/lights/19", + "owner": { + "rid": "7da8bcc7-6e69-46c4-a2e1-e0c2de8e2270", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:06:1f:57:6c", + "type": "zigbee_connectivity" + }, + { + "id": "037dd595-b5b7-48e0-be43-e765367aa8f6", + "id_v1": "/lights/1", + "owner": { + "rid": "e41beec5-0340-4700-9094-8244f9a8ed0d", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:10:3b:f2:6a", + "type": "zigbee_connectivity" + }, + { + "id": "3c0c40e5-cf3f-45ae-89f6-b1038cddbb13", + "id_v1": "/sensors/14", + "owner": { + "rid": "431026fb-298c-4726-8ce4-47450fea13c4", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:04:f1:b2:0b", + "type": "zigbee_connectivity" + }, + { + "id": "3a35dfed-10bc-4403-88c8-7963543fb090", + "id_v1": "/sensors/124", + "owner": { + "rid": "f78c5b4b-2f52-4bc3-8097-1ddf97949cc5", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:0b:00:0a:57", + "type": "zigbee_connectivity" + }, + { + "id": "59d83ae1-4dff-4716-a343-88ddebe78aad", + "id_v1": "/sensors/30", + "owner": { + "rid": "70660557-692d-4d37-8b6b-e3ec63716a72", + "rtype": "device" + }, + "status": "connected", + "mac_address": "00:17:88:01:0b:cf:5a:5b", + "type": "zigbee_connectivity" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/zone.json b/bundles/org.openhab.binding.hue/src/test/resources/zone.json new file mode 100644 index 000000000..143c9bbe8 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/zone.json @@ -0,0 +1,172 @@ +{ + "errors": [], + "data": [ + { + "id": "e3372839-3464-4be0-94a2-949433bc065c", + "id_v1": "/groups/6", + "children": [ + { + "rid": "bcad47a0-3f1f-498c-a8aa-3cf389965219", + "rtype": "light" + } + ], + "services": [ + { + "rid": "db4fd630-3798-40de-b642-c1ef464bf770", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Bay", + "archetype": "kitchen" + }, + "type": "zone" + }, + { + "id": "42bbbee8-f76d-431d-a87f-a5ca71bb3613", + "id_v1": "/groups/15", + "children": [ + { + "rid": "7f077235-0130-4a0a-95f9-107cf83639a3", + "rtype": "light" + }, + { + "rid": "cb77dbcb-f67b-4e9a-b287-e35a046b4288", + "rtype": "light" + } + ], + "services": [ + { + "rid": "7ef7456a-8c4c-4b85-b433-b8c1bb99249b", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Standard Lamps", + "archetype": "lounge" + }, + "type": "zone" + }, + { + "id": "93d0b50b-1500-4351-a3fa-52d18dd4c8fc", + "id_v1": "/groups/4", + "children": [ + { + "rid": "3bec6abf-3831-4852-8eb9-d2a227d219f2", + "rtype": "light" + }, + { + "rid": "e18446bf-36d1-4cda-aafe-1940bbb8f23b", + "rtype": "light" + }, + { + "rid": "d3bf7a57-b3e6-45f5-833d-00fb5491b67b", + "rtype": "light" + }, + { + "rid": "3366722b-48b1-4682-a648-607915873c40", + "rtype": "light" + } + ], + "services": [ + { + "rid": "900f4b11-7ed1-46ae-bcf4-978d0028aac9", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Downlights", + "archetype": "dining" + }, + "type": "zone" + }, + { + "id": "dda859a6-f358-48f5-8d34-e13b04bf6e62", + "id_v1": "/groups/8", + "children": [ + { + "rid": "7b4038f8-4311-46e6-bdcf-c560eaa65a12", + "rtype": "light" + } + ], + "services": [ + { + "rid": "dd653c2b-9622-45d5-aa57-d0bf9391592b", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Display Cabinet", + "archetype": "dining" + }, + "type": "zone" + }, + { + "id": "cff8919b-7466-4199-a8c5-a5204cd4fcf1", + "id_v1": "/groups/9", + "children": [ + { + "rid": "ba38af49-4206-47fb-b05a-b54887c12b4c", + "rtype": "light" + } + ], + "services": [ + { + "rid": "6e2fee8d-c25f-4468-8ac8-5ed75c6f3cf1", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Andrew", + "archetype": "bedroom" + }, + "type": "zone" + }, + { + "id": "bdc282b3-750d-45dd-b6c4-12a2927d8951", + "id_v1": "/groups/1", + "children": [ + { + "rid": "29e37aaf-abf6-4e5d-8018-37ba8048cfec", + "rtype": "light" + }, + { + "rid": "632baee1-623d-4a31-b1f1-f87e94ac81b7", + "rtype": "light" + } + ], + "services": [ + { + "rid": "f8cac182-2608-40e3-81e4-f6ac02eba55a", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Worktops", + "archetype": "kitchen" + }, + "type": "zone" + }, + { + "id": "57a20e61-7686-48ca-9ad2-6dbf1a2a0364", + "id_v1": "/groups/10", + "children": [ + { + "rid": "09837085-7c06-45e1-92de-a2fa76dbbccb", + "rtype": "light" + } + ], + "services": [ + { + "rid": "1bfb7090-e2b3-4417-8834-86e1ebb0a50c", + "rtype": "grouped_light" + } + ], + "metadata": { + "name": "Elisabeth", + "archetype": "bedroom" + }, + "type": "zone" + } + ] +} \ No newline at end of file diff --git a/itests/org.openhab.binding.hue.tests/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeNupnpDiscoveryOSGITest.java b/itests/org.openhab.binding.hue.tests/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeNupnpDiscoveryOSGITest.java index ec54594a7..706f84414 100644 --- a/itests/org.openhab.binding.hue.tests/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeNupnpDiscoveryOSGITest.java +++ b/itests/org.openhab.binding.hue.tests/src/main/java/org/openhab/binding/hue/internal/discovery/HueBridgeNupnpDiscoveryOSGITest.java @@ -15,7 +15,8 @@ package org.openhab.binding.hue.internal.discovery; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.openhab.binding.hue.internal.HueBindingConstants.THING_TYPE_BRIDGE; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.hue.internal.HueBindingConstants.*; import static org.openhab.core.config.discovery.inbox.InboxPredicates.forThingTypeUID; import java.io.IOException; @@ -25,15 +26,16 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.eclipse.jdt.annotation.NonNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.openhab.binding.hue.internal.HueBindingConstants; import org.openhab.core.config.discovery.DiscoveryListener; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.config.discovery.inbox.Inbox; import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.test.storage.VolatileStorageService; +import org.openhab.core.thing.ThingRegistry; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; @@ -62,7 +64,7 @@ public class HueBridgeNupnpDiscoveryOSGITest extends JavaOSGiTest { private void checkDiscoveryResult(DiscoveryResult result, String expIp, String expSn) { assertThat(result.getBridgeUID(), nullValue()); - assertThat(result.getLabel(), is(String.format(HueBindingConstants.DISCOVERY_LABEL_PATTERN, expIp))); + assertThat(result.getLabel(), is(String.format(DISCOVERY_LABEL_PATTERN, expIp))); assertThat(result.getProperties().get("ipAddress"), is(expIp)); assertThat(result.getProperties().get("serialNumber"), is(expSn)); } @@ -81,6 +83,10 @@ public class HueBridgeNupnpDiscoveryOSGITest extends JavaOSGiTest { // Mock class which only overrides the doGetRequest method in order to make the class testable class ConfigurableBridgeNupnpDiscoveryMock extends HueBridgeNupnpDiscovery { + public ConfigurableBridgeNupnpDiscoveryMock(ThingRegistry thingRegistry) { + super(thingRegistry); + } + @Override protected String doGetRequest(String url) throws IOException { if (url.contains("meethue")) { @@ -92,6 +98,11 @@ public class HueBridgeNupnpDiscoveryOSGITest extends JavaOSGiTest { } throw new IOException(); } + + @Override + protected boolean isClip2Supported(@NonNull String ipAddress) { + return false; + } } @BeforeEach @@ -109,8 +120,8 @@ public class HueBridgeNupnpDiscoveryOSGITest extends JavaOSGiTest { @Test public void bridgeThingTypeIsSupported() { - assertThat(sut.getSupportedThingTypes().size(), is(1)); - assertThat(sut.getSupportedThingTypes().iterator().next(), is(THING_TYPE_BRIDGE)); + assertThat(sut.getSupportedThingTypes().size(), is(2)); + assertThat(sut.getSupportedThingTypes().contains(THING_TYPE_BRIDGE), is(true)); } @Test @@ -121,7 +132,7 @@ public class HueBridgeNupnpDiscoveryOSGITest extends JavaOSGiTest { inbox.remove(oldResult.getThingUID()); } - sut = new ConfigurableBridgeNupnpDiscoveryMock(); + sut = new ConfigurableBridgeNupnpDiscoveryMock(mock(ThingRegistry.class)); registerService(sut, DiscoveryService.class.getName()); discoveryResult = validBridgeDiscoveryResult; final Map results = new HashMap<>(); @@ -170,7 +181,7 @@ public class HueBridgeNupnpDiscoveryOSGITest extends JavaOSGiTest { inbox.remove(oldResult.getThingUID()); } - sut = new ConfigurableBridgeNupnpDiscoveryMock(); + sut = new ConfigurableBridgeNupnpDiscoveryMock(mock(ThingRegistry.class)); registerService(sut, DiscoveryService.class.getName()); final Map results = new HashMap<>(); registerDiscoveryListener(new DiscoveryListener() {