From 78c4962b6a0105f991f52fa392fc4ef63a6e2086 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Thu, 12 Jun 2025 19:21:04 +0200 Subject: [PATCH] [dirigera] Initial contribution (#17719) * feature-squash Signed-off-by: Bernd Weymann --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.dirigera/NOTICE | 13 + .../org.openhab.binding.dirigera/README.md | 734 ++++++ .../doc/follow-sun.png | Bin 0 -> 94068 bytes .../doc/light-presets.png | Bin 0 -> 159960 bytes .../doc/link-candidates.png | Bin 0 -> 91230 bytes .../doc/thing-properties.png | Bin 0 -> 94484 bytes bundles/org.openhab.binding.dirigera/pom.xml | 26 + .../src/main/feature/feature.xml | 9 + .../binding/dirigera/internal/Constants.java | 362 +++ .../internal/DirigeraCommandProvider.java | 41 + .../internal/DirigeraHandlerFactory.java | 174 ++ .../DirigeraStateDescriptionProvider.java | 87 + .../config/BaseDeviceConfiguration.java | 26 + .../config/ColorLightConfiguration.java | 27 + .../config/DirigeraConfiguration.java | 32 + .../console/DirigeraCommandExtension.java | 218 ++ .../discovery/DirigeraDiscoveryService.java | 52 + .../DirigeraMDNSDiscoveryParticipant.java | 108 + .../internal/exception/ApiException.java | 30 + .../internal/exception/GatewayException.java | 30 + .../internal/exception/ModelException.java | 30 + .../internal/handler/BaseHandler.java | 740 ++++++ .../internal/handler/DeviceUpdate.java | 61 + .../internal/handler/DirigeraHandler.java | 1052 +++++++++ .../airpurifier/AirPurifierHandler.java | 162 ++ .../internal/handler/blind/BlindHandler.java | 125 + .../controller/BaseShortcutController.java | 175 ++ .../controller/BlindsControllerHandler.java | 61 + .../DoubleShortcutControllerHandler.java | 85 + .../controller/LightControllerHandler.java | 114 + .../controller/ShortcutControllerHandler.java | 46 + .../controller/SoundControllerHandler.java | 48 + .../internal/handler/light/BaseLight.java | 237 ++ .../handler/light/ColorLightHandler.java | 213 ++ .../handler/light/DimmableLightHandler.java | 118 + .../internal/handler/light/LightCommand.java | 57 + .../handler/light/SwitchLightHandler.java | 42 + .../light/TemperatureLightHandler.java | 164 ++ .../handler/plug/PowerPlugHandler.java | 88 + .../handler/plug/SimplePlugHandler.java | 48 + .../handler/plug/SmartPlugHandler.java | 124 + .../handler/repeater/RepeaterHandler.java | 56 + .../internal/handler/scene/SceneHandler.java | 115 + .../handler/sensor/AirQualityHandler.java | 97 + .../handler/sensor/ContactSensorHandler.java | 84 + .../handler/sensor/LightSensorHandler.java | 76 + .../sensor/MotionLightSensorHandler.java | 101 + .../handler/sensor/MotionSensorHandler.java | 284 +++ .../handler/sensor/WaterSensorHandler.java | 78 + .../handler/speaker/SpeakerHandler.java | 234 ++ .../internal/interfaces/DebugHandler.java | 60 + .../internal/interfaces/DirigeraAPI.java | 113 + .../dirigera/internal/interfaces/Gateway.java | 193 ++ .../dirigera/internal/interfaces/Model.java | 181 ++ .../internal/interfaces/PowerListener.java | 34 + .../dirigera/internal/model/ColorModel.java | 169 ++ .../internal/model/DirigeraModel.java | 563 +++++ .../internal/network/DirigeraAPIImpl.java | 324 +++ .../dirigera/internal/network/Websocket.java | 250 ++ .../src/main/resources/OH-INF/addon/addon.xml | 21 + .../resources/OH-INF/config/base-device.xml | 13 + .../resources/OH-INF/config/color-light.xml | 27 + .../main/resources/OH-INF/config/gateway.xml | 22 + .../resources/OH-INF/config/light-device.xml | 18 + .../resources/OH-INF/i18n/dirigera.properties | 388 +++ .../resources/OH-INF/thing/air-purifier.xml | 55 + .../resources/OH-INF/thing/air-quality.xml | 37 + .../OH-INF/thing/blind-controller.xml | 25 + .../src/main/resources/OH-INF/thing/blind.xml | 27 + .../resources/OH-INF/thing/channel-types.xml | 347 +++ .../resources/OH-INF/thing/color-light.xml | 47 + .../resources/OH-INF/thing/contact-sensor.xml | 29 + .../resources/OH-INF/thing/dimmable-light.xml | 32 + .../OH-INF/thing/double-shortcut.xml | 33 + .../main/resources/OH-INF/thing/gateway.xml | 49 + .../OH-INF/thing/light-controller.xml | 29 + .../resources/OH-INF/thing/light-sensor.xml | 21 + .../OH-INF/thing/motion-light-sensor.xml | 53 + .../resources/OH-INF/thing/motion-sensor.xml | 49 + .../resources/OH-INF/thing/power-plug.xml | 29 + .../main/resources/OH-INF/thing/repeater.xml | 21 + .../src/main/resources/OH-INF/thing/scene.xml | 28 + .../resources/OH-INF/thing/simple-plug.xml | 27 + .../OH-INF/thing/single-shortcut.xml | 29 + .../resources/OH-INF/thing/smart-plug.xml | 53 + .../OH-INF/thing/sound-controller.xml | 25 + .../main/resources/OH-INF/thing/speaker.xml | 53 + .../resources/OH-INF/thing/switch-light.xml | 26 + .../OH-INF/thing/temperature-light.xml | 42 + .../resources/OH-INF/thing/water-sensor.xml | 29 + .../resources/json/gateway/coordinates.json | 8 + .../json/gateway/null-coordinates.json | 5 + .../resources/json/light-presets/bright.json | 32 + .../json/light-presets/slowdown.json | 33 + .../resources/json/light-presets/smooth.json | 32 + .../resources/json/light-presets/warm.json | 32 + .../resources/json/scenes/click-scene.json | 22 + .../json/sensor-config/always-on.json | 7 + .../json/sensor-config/duration-update.json | 8 + .../json/sensor-config/follow-sun.json | 15 + .../json/sensor-config/schedule-on.json | 15 + .../binding/dirigera/internal/FileReader.java | 47 + .../dirigera/internal/TestGeneric.java | 137 ++ .../handler/DirigeraBridgeProvider.java | 122 + .../internal/handler/TestGateway.java | 117 + .../internal/handler/TestWrongHandler.java | 89 + .../handler/airpurifier/TestAirPurifier.java | 171 ++ .../handler/blind/TestBlindHandler.java | 129 + .../controller/TestBlindController.java | 108 + .../TestDoubleShortcutController.java | 147 ++ .../controller/TestLightController.java | 108 + .../controller/TestShortcutController.java | 102 + .../controller/TestSoundController.java | 106 + .../handler/lights/TestColorLight.java | 132 ++ .../handler/lights/TestDimmableLight.java | 125 + .../handler/lights/TestSwitchLight.java | 116 + .../handler/lights/TestTemperatureLight.java | 137 ++ .../handler/repeater/TestRepeater.java | 83 + .../internal/handler/scene/TestScenes.java | 72 + .../handler/sensor/TestAirQualityDevice.java | 113 + .../handler/sensor/TestContactDevice.java | 77 + .../handler/sensor/TestLightSensor.java | 71 + .../handler/sensor/TestMotionLightSensor.java | 85 + .../handler/sensor/TestMotionSensor.java | 87 + .../handler/sensor/TestWaterSensor.java | 96 + .../internal/handler/speaker/TestSpeaker.java | 102 + .../internal/handlerplug/TestPowerPlug.java | 123 + .../internal/handlerplug/TestSimplePlug.java | 121 + .../internal/handlerplug/TestSmartPlug.java | 170 ++ .../dirigera/internal/mock/CallbackMock.java | 173 ++ .../internal/mock/DicoveryServiceMock.java | 66 + .../internal/mock/DirigeraAPISimu.java | 150 ++ .../mock/DirigeraHandlerManipulator.java | 52 + .../internal/mock/HandlerFactoryMock.java | 47 + .../mock/TemperatureLightHandlerMock.java | 44 + .../internal/model/TestColorModel.java | 87 + .../dirigera/internal/model/TestModel.java | 288 +++ .../coverart/sonos-radio-cocktail-hour.avif | Bin 0 -> 3157 bytes .../resources/devices/home-all-devices.json | 2090 +++++++++++++++++ .../test/resources/devices/inspelning.json | 59 + .../test/resources/devices/praktlysing.json | 48 + .../devices/somrig-shortcut-cotroller.json | 91 + .../src/test/resources/devices/starkvind.json | 56 + .../devices/symfonsik-sound-controller.json | 45 + .../devices/tradfir-shortcut-controller.json | 46 + .../devices/tradfri-blinds-controller.json | 48 + .../devices/tradfri-color-lightbulb.json | 61 + .../devices/tradfri-light-driver.json | 49 + .../resources/devices/tradfri-outlet.json | 42 + .../test/resources/devices/tretakt-plug.json | 45 + .../gateway/home-with-coordinates.json | 1615 +++++++++++++ .../gateway/home-without-coordinates.json | 1605 +++++++++++++ .../test/resources/home/home-one-device.json | 174 ++ .../home/home-with-double-shortcut.json | 259 ++ .../src/test/resources/home/home.json | 1615 +++++++++++++ .../websocket/device-added/device-added.json | 51 + .../websocket/device-added/home-after.json | 1659 +++++++++++++ .../websocket/device-added/home-before.json | 1615 +++++++++++++ .../device-removed/device-removed.json | 51 + .../websocket/device-removed/home-after.json | 1615 +++++++++++++ .../websocket/device-removed/home-before.json | 1659 +++++++++++++ .../websocket/scene-created/home-after.json | 1615 +++++++++++++ .../websocket/scene-created/home-before.json | 1543 ++++++++++++ .../scene-created/scene-created.json | 79 + .../websocket/scene-deleted/home-after.json | 1543 ++++++++++++ .../websocket/scene-deleted/home-before.json | 1615 +++++++++++++ .../scene-deleted/scene-deleted.json | 79 + .../scene-pressed/scene-trigger-sequence.json | 98 + bundles/pom.xml | 1 + 171 files changed, 35841 insertions(+) create mode 100644 bundles/org.openhab.binding.dirigera/NOTICE create mode 100644 bundles/org.openhab.binding.dirigera/README.md create mode 100644 bundles/org.openhab.binding.dirigera/doc/follow-sun.png create mode 100644 bundles/org.openhab.binding.dirigera/doc/light-presets.png create mode 100644 bundles/org.openhab.binding.dirigera/doc/link-candidates.png create mode 100644 bundles/org.openhab.binding.dirigera/doc/thing-properties.png create mode 100644 bundles/org.openhab.binding.dirigera/pom.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/Constants.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/DirigeraCommandProvider.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/DirigeraHandlerFactory.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/DirigeraStateDescriptionProvider.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/config/BaseDeviceConfiguration.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/config/ColorLightConfiguration.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/config/DirigeraConfiguration.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/console/DirigeraCommandExtension.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/discovery/DirigeraDiscoveryService.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/discovery/DirigeraMDNSDiscoveryParticipant.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/exception/ApiException.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/exception/GatewayException.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/exception/ModelException.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/BaseHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/DeviceUpdate.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/DirigeraHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/airpurifier/AirPurifierHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/blind/BlindHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/BaseShortcutController.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/BlindsControllerHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/DoubleShortcutControllerHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/LightControllerHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/ShortcutControllerHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/SoundControllerHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/BaseLight.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/ColorLightHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/DimmableLightHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/LightCommand.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/SwitchLightHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/TemperatureLightHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/plug/PowerPlugHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/plug/SimplePlugHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/plug/SmartPlugHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/repeater/RepeaterHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/scene/SceneHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/AirQualityHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/ContactSensorHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/LightSensorHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/MotionLightSensorHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/MotionSensorHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/WaterSensorHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/speaker/SpeakerHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/DebugHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/DirigeraAPI.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/Gateway.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/Model.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/PowerListener.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/model/ColorModel.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/model/DirigeraModel.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/network/DirigeraAPIImpl.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/network/Websocket.java create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/base-device.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/color-light.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/gateway.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/light-device.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/i18n/dirigera.properties create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/air-purifier.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/air-quality.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/blind-controller.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/blind.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/channel-types.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/color-light.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/contact-sensor.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/dimmable-light.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/double-shortcut.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/gateway.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/light-controller.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/light-sensor.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/motion-light-sensor.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/motion-sensor.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/power-plug.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/repeater.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/scene.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/simple-plug.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/single-shortcut.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/smart-plug.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/sound-controller.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/speaker.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/switch-light.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/temperature-light.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/water-sensor.xml create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/json/gateway/coordinates.json create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/json/gateway/null-coordinates.json create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/bright.json create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/slowdown.json create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/smooth.json create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/warm.json create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/json/scenes/click-scene.json create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/always-on.json create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/duration-update.json create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/follow-sun.json create mode 100644 bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/schedule-on.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/FileReader.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/TestGeneric.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/DirigeraBridgeProvider.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/TestGateway.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/TestWrongHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/airpurifier/TestAirPurifier.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/blind/TestBlindHandler.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestBlindController.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestDoubleShortcutController.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestLightController.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestShortcutController.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestSoundController.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestColorLight.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestDimmableLight.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestSwitchLight.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestTemperatureLight.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/repeater/TestRepeater.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/scene/TestScenes.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestAirQualityDevice.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestContactDevice.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestLightSensor.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestMotionLightSensor.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestMotionSensor.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestWaterSensor.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/speaker/TestSpeaker.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handlerplug/TestPowerPlug.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handlerplug/TestSimplePlug.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handlerplug/TestSmartPlug.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/CallbackMock.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/DicoveryServiceMock.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/DirigeraAPISimu.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/DirigeraHandlerManipulator.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/HandlerFactoryMock.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/TemperatureLightHandlerMock.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/model/TestColorModel.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/model/TestModel.java create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/coverart/sonos-radio-cocktail-hour.avif create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/devices/home-all-devices.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/devices/inspelning.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/devices/praktlysing.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/devices/somrig-shortcut-cotroller.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/devices/starkvind.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/devices/symfonsik-sound-controller.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfir-shortcut-controller.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-blinds-controller.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-color-lightbulb.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-light-driver.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-outlet.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/devices/tretakt-plug.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/gateway/home-with-coordinates.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/gateway/home-without-coordinates.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/home/home-one-device.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/home/home-with-double-shortcut.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/home/home.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-added/device-added.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-added/home-after.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-added/home-before.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-removed/device-removed.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-removed/home-after.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-removed/home-before.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-created/home-after.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-created/home-before.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-created/scene-created.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-deleted/home-after.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-deleted/home-before.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-deleted/scene-deleted.json create mode 100644 bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-pressed/scene-trigger-sequence.json diff --git a/CODEOWNERS b/CODEOWNERS index 6cb7f0ce5b4..43ae00e8609 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -84,6 +84,7 @@ /bundles/org.openhab.binding.deutschebahn/ @soenkekueper /bundles/org.openhab.binding.digiplex/ @rmichalak /bundles/org.openhab.binding.digitalstrom/ @openhab/add-ons-maintainers +/bundles/org.openhab.binding.dirigera/ @weymann /bundles/org.openhab.binding.dlinksmarthome/ @MikeJMajor /bundles/org.openhab.binding.dmx/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.dolbycp/ @Cybso diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 91fd0d92962..804f43640be 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -406,6 +406,11 @@ org.openhab.binding.digitalstrom ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.dirigera + ${project.version} + org.openhab.addons.bundles org.openhab.binding.dlinksmarthome diff --git a/bundles/org.openhab.binding.dirigera/NOTICE b/bundles/org.openhab.binding.dirigera/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.dirigera/README.md b/bundles/org.openhab.binding.dirigera/README.md new file mode 100644 index 00000000000..65721a73f0b --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/README.md @@ -0,0 +1,734 @@ +# DIRIGERA Binding + +Binding supporting the DIRIGERA Gateway from IKEA. + +## Supported Things + +The DIRIGERA `bridge` is providing the connection to all devices and scenes. + +Refer to below sections which devices are supported and are covered by `things` connected to the DIRIGERA bridge. + +| ThingTypeUID | Description | Section | Products | +|-----------------------|------------------------------------------------------------|-------------------------------------------|-------------------------------------------| +| `gateway` | IKEA Gateway for smart products | [Gateway](#gateway-channels) | DIRIGERA | +| `air-purifier` | Air cleaning device with particle filter | [Air Purifier](#air-purifier) | STARKVIND | +| `air-quality` | Air measure for temperature, humidity and particles | [Sensors](#air-quality-sensor) | VINDSTYRKA | +| `blind` | Window or door blind | [Blinds](#blinds) | PRAKTLYSING ,KADRILJ ,FRYKTUR, TREDANSEN | +| `blind-controller` | Controller to open and close blinds | [Controller](#blind-controller) | TRÅDFRI | +| `switch-light` | Light with switch ON, OFF capability | [Lights](#switch-lights) | TRÅDFRI | +| `dimmable-light` | Light with brightness support | [Lights](#dimmable-lights) | TRÅDFRI | +| `temperature-light` | Light with color temperature support | [Lights](#temperature-lights) | TRÅDFRI, FLOALT | +| `color-light` | Light with color support | [Lights](#color-lights) | TRÅDFRI, ORMANÅS | +| `light-controller` | Controller to handle light attributes | [Controller](#light-controller) | TRÅDFRI, RODRET,STYRBAAR | +| `motion-sensor` | Sensor detecting motion events | [Sensors](#motion-sensor) | TRÅDFRI | +| `motion-light-sensor` | Sensor detecting motion events and measures light level | [Sensors](#motion-light-sensor) | VALLHORN | +| `single-shortcut` | Shortcut controller with one button | [Controller](#single-shortcut-controller) | TRÅDFRI | +| `double-shortcut` | Shortcut controller with two buttons | [Controller](#double-shortcut-controller) | SOMRIG | +| `simple-plug` | Power plug | [Plugs](#simple-plug) | TRÅDFRI, ÅSKVÄDER | +| `power-plug` | Power plug with status light and child lock | [Plugs](#power-plug) | TRETAKT | +| `smart-plug` | Power plug with electricity measurements | [Plugs](#smart-power-plug) | INSPELNING | +| `speaker` | Speaker with player activities | [Speaker](#speaker) | SYMFONISK | +| `sound-controller` | Controller for speakers | [Controller](#sound-controller) | SYMFONISK, TRÅDFRI | +| `contact-sensor` | Sensor tracking if windows or doors are open | [Sensors](#contact-sensor) | PARASOLL | +| `water-sensor` | Sensor to detect water leaks | [Sensors](#water-sensor) | BADRING | +| `repeater` | Repeater to strengthen signal | [Repeater](#repeater) | TRÅDFRI | +| `scene` | Scene from IKEA Home smart app which can be triggered | [Scenes](#scenes) | - | + +## Discovery + +The discovery will automatically detect your DIRIGERA Gateway via mDNS. +If it cannot be found check your router for IP address. +Manual scan isn't supported. + +After successful creation of DIRIGERA Gateway and pairing process connected devices are automatically added to your INBOX. +You can switch off the automatic detection in [Bridge configuration](#bridge-configuration). + +**Before adding the bridge** read [Pairing section](#gateway-pairing). + +Devices connected to this bridge will be detected automatically unless you don't switch it off in [Bridge Configuration](#bridge-configuration) + +## Gateway Bridge + +### Bridge Configuration + +| Name | Type | Description | Explanation | Default | Required | +|-----------------|---------|------------------------------------------------------------|--------------------------------------------------------------------------------------|---------|----------| +| `ipAddress` | text | DIRIGERA IP Address | Use discovery to obtain this value automatically or enter it manually if known | N/A | yes | +| `id` | text | Unique id of this gateway | Detected automatically after successful pairing | N/A | no | +| `discovery` | boolean | Configure if paired devices shall be detected by discovery | Run continuously in the background and detect new, deleted or changed devices | true | no | + +### Gateway Pairing + +First setup requires pairing the DIRIGERA gateway with openHAB. +You need physical access to the gateway to finish pairing so ensure you can reach it quickly. + +Let's start pairing + +1. Add the bridge found in discovery +2. Pairing started automatically after creation! +3. Press the button on the DIRIGERA rear side +4. Your bridge shall switch to ONLINE + +### Gateway Channels + +| Channel | Type | Read/Write | Description | +|-----------------|-----------|------------|----------------------------------------------| +| `pairing` | Switch | RW | Sets DIRIGERA hub into pairing mode | +| `location` | Location | R(W) | Location in lat.,lon. coordinates | +| `sunrise` | DateTime | R | Date and time of next sunrise | +| `sunset` | DateTime | R | Date and time of next sunset | +| `statistics` | String | R | Several statistics about gateway activities | + +Channel `location` can overwrite GPS position with openHAB location, but it's not possible to delete GPS data. +See [Gateway Limitations](#gateway-limitations) for further information. + +### Follow Sun + + + +[Motion Sensors](#motion-sensor) can be active all the time or follow a schedule. +One schedule is follow the sun which needs to be activated in the IKEA Home smart app in _Hub Settings_. + +## Things + +With [DIRIGERA Gateway Bridge](#gateway-bridge) in place things can be connected as mentioned in the [supported things section](#supported-things). +Things contain generic [configuration](), [properties]() and [channels]() according to their capabilities. + +### Generic Thing Configuration + +Each thing is identified by a unique id which is mandatory to configure. +Discovery will automatically identify the id. + +| Name | Type | Description | Default | Required | +|-------------------|---------|-------------------------------------|---------|----------| +| `id` | text | Unique id of this device / scene | N/A | yes | + +### Generic Thing Properties + +Each thing has properties attached for product information. +It contains information of hardware and firmware version, device model and manufacturer. +Device capabilities are listed in `canReceive` and `canSend`. + + + +### Generic Thing Channels + +#### OTA Channels + +Over-the-Air (OTA) updates are common for many devices. +If device is providing these channels is detected during runtime. + +| Channel | Type | Read/Write | Description | Advanced | +|-----------------|-----------|------------|----------------------------------------------|----------| +| `ota-status` | Number | R | Over-the-air overall status | | +| `ota-state` | Number | R | Over-the-air current state | X | +| `ota-progress` | Number | R | Over-the-air current progress | X | + +`ota-status` shows the _overall status_ if your device is _up to date_ or an _update is available_. +`ota-state` and `ota-progress` shows more detailed information which you may want to follow, that's why they are declared as advanced channels. + +**OTA Mappings** + +Mappings for `ota-status` + +- 0 : Up to date +- 1 : Update available + +Mappings for `ota-state` + +- 0 : Ready to check +- 1 : Check in progress +- 2 : Ready to download +- 3 : Download in progress +- 4 : Update in progress +- 5 : Update failed +- 6 : Ready to update +- 7 : Check failed +- 8 : Download failed +- 9 : Update complete +- 10 : Battery check failed + +#### Links and Candidates + +Devices can be connected directly e.g. sensors or controllers with lights, plugs, blinds or speakers. +It's detected during runtime if a device is capable to support links _and_ if devices are available in your system to support this connection. +The channels are declared advanced and can be used for setup procedure. + +| Channel | Type | Read/Write | Description | Advanced | +|-----------------------|-----------------------|------------|--------------------------------------------------|----------| +| `links` | String | RW | Linked controllers and sensors | X | +| `link-candidates` | String | RW | Candidates which can be linked | X | + + + +Several devices can be linked together like + +- [Light Controller](#light-controller) and [Motion Sensors](#motion-sensor) to [Plugs](#power-plugs) and [Lights](#lights) +- [Blind Controller](#blind-controller) to [Blinds](#blinds) +- [Sound Controller](#sound-controller) to [Speakers](#speaker) + +Established links are shown in channel `links`. +The linked devices can be clicked in the UI and the link will be removed. + +Possible candidates to be linked are shown in channel `link-candidates`. +If a candidate is clicked in the UI the link will be established. + +Candidates and links marked with `(!)` are not present in openHAB environment so no handler is created yet. +In this case it's possible not all links are shown in the UI, but the present ones shall work. + +#### Other Channels + +| Channel | Type | Read/Write | Description | +|-----------------------|-------------------|------------|----------------------------------------------| +| `startup` | Number | RW | Startup behavior after power cutoff | +| `custom-name` | String | RW | Name given from IKEA home smart app | + +`startup` defines how the device shall behave after a power cutoff. +If there's a dedicated hardwired light switch which cuts power towards the bulb it makes sense to switch them on every time the switch is pressed. +But it's also possible to recover the last state. + +Mappings for `startup` + +- 0 : Previous +- 1 : On +- 2 : Off +- 3 : Switch + +Option 3 is offered in IKEA Home smart app to control lights with using your normal light switch _slowly and smooth_. +With this the light shall stay online. +I wasn't able to reproduce this behavior. +Maybe somebody has more success. + + +`custom-name` is declared e.g. in your IKEA Home smart app. +This name is reflected in the discovery and if thing is created this name will be the thing label. +If `custom-name` is changed via openHAB API or a rule the label will not change. + +### Unknown Devices + +Filter your traces regarding 'DIRIGERA MODEL Unsupported Device'. +The trace contains a JSON object at the end which is needed to implement a corresponding handler. + +## Air Purifier + +Air cleaning device with particle filter. + +| Channel | Type | Read/Write | Description | +|-----------------------|-------------------|------------|----------------------------------------------| +| `fan-mode` | Number | RW | Fan on, off, speed or automatic behavior | +| `fan-speed` | Dimmer | RW | Manual regulation of fan speed | +| `fan-runtime` | Number:Time | R | Fan runtime in minutes | +| `filter-elapsed` | Number:Time | R | Filter elapsed time in minutes | +| `filter-remain` | Number:Time | R | Time to filter replacement in minutes | +| `filter-lifetime` | Number:Time | R | Filter lifetime in minutes | +| `filter-alarm` | Switch | R | Filter alarm signal | +| `particulate-matter` | Number:Density | R | Category 2.5 particulate matter | +| `disable-status-light`| Switch | RW | Disable status light on plug | +| `child-lock` | Switch | RW | Child lock for button on plug | + +There are several `Number:Time` which are delivered in minutes as default. +Note you can change the unit when connecting an item e.g. to `d` (days) for readability. +So you can check in a rule if your remaining filter time is going below 7 days instead of calculating minutes. + +### Air Purifier Channel Mappings + +Mappings for `fan-mode` + +- 0 : Auto +- 1 : Low +- 2 : Medium +- 3 : High +- 4 : On +- 5 : Off + +## Blinds + +Window or door blind. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|--------------------------------------------------| +| `blind-state` | Number | RW | State if blind is moving up, down or stopped | +| `blind-level` | Dimmer | RW | Current blind level | +| `battery-level` | Number:Dimensionless | R | Battery charge level in percent | + +#### Blind Channel Mappings + +Mappings for `blind-state` + +- 0 : Stopped +- 1 : Up +- 2 : Down + +## Lights + +Light devices in several variants. +Can be light bulbs, LED stripes, remote driver and more. +Configuration contains + +| Name | Type | Description | Default | Required | +|-------------------|---------|---------------------------------------------------------------------|---------|----------| +| `id` | text | Unique id of this device / scene | N/A | yes | +| `fadeTime` | integer | Required time for fade sequnce to color or brightness | 750 | yes | +| `fadeSequence` | integer | Define sequence if several light parameters are changed at once | 0 | yes | + +`fadeTime` adjust fading time according to your device. +Current behavior shows commands are acknowledged while device is fading but not executed correctly. +So they need to be executed one after another. +Maybe an update of the DIRIGERA gateway will change the current behavior and you can reduce them afterwards. + +`fadeSequence` is only for [Color Lights](#color-lights). +Through `hsb` channel it's possible to adapt color brightness at once. +Again due to fading times they need to be executed in a sequence. +You can choose between options + +- 0: First brightness, then color +- 1: First color, then brightness + +### Lights ON OFF Behavior + +When light is ON each command will change the settings accordingly immediately. +During power OFF the lights will preserve some values until next power ON. + +| Channel | Type | Behavior | +|-----------------------|---------------|---------------------------------------------------------------------------| +| `power` | ON | Switch ON, apply last / stored values | +| `brightness` | ON | Switch ON, apply last / stored values | +| `brightness` | value > 0 | Switch ON, apply this brightness, apply last / stored values | +| `color-temperature` | ON | Switch ON, apply last / stored values | +| `color-temperature` | any | Store value, brightness stays at previous level | +| `color` | ON | Switch ON, apply last / stored values | +| `color` | value > 0 | Switch ON, apply this brightness, apply last / stored values | +| `color` | h,s,b | Store color and brightness for next ON | +| outside | | Switch ON, apply last / stored values | + +## Switch Lights + +Light with switch ON, OFF capability + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|--------------------------------------------------| +| `power` | Switch | RW | Power state of light | + +## Dimmable Lights + +Light with brightness support. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|--------------------------------------------------| +| `power` | Switch | RW | Power state of light | +| `brightness` | Dimmer | RW | Control brightness of light | + +Channel `brightness` can receive + +- ON / OFF +- numbers from 0 to 100 as percent where 0 will switch the light OFF, any other > 0 switches light ON + +## Temperature Lights + +Light with color temperature support. + +| Channel | Type | Read/Write | Description | Advanced | +|---------------------------|-----------------------|------------|------------------------------------------------------|----------| +| `power` | Switch | RW | Power state of light | | +| `brightness` | Dimmer | RW | Control brightness of light | | +| `color-temperature` | Dimmer | RW | Color temperature from cold (0 %) to warm (100 %) | | +| `color-temperature-abs` | Number:Temperature | RW | Color temperature of a bulb in Kelvin | X | + +## Color Lights + +Light with color support. + +| Channel | Type | Read/Write | Description | Advanced | +|---------------------------|-----------------------|------------|------------------------------------------------------|----------| +| `power` | Switch | RW | Power state of light | | +| `brightness` | Dimmer | RW | Brightness of light in percent | | +| `color-temperature` | Dimmer | RW | Color temperature from cold (0 %) to warm (100 %) | | +| `color-temperature-abs` | Number:Temperature | RW | Color temperature of a bulb in Kelvin | | +| `color` | Color | RW | Color of light with hue, saturation and brightness | X | + +Channel `color` can receive + +- ON / OFF +- numbers from 0 to 100 as brightness in percent where 0 will switch the light OFF, any other > 0 switches light ON +- triple values for hue, saturation, brightness + +## Power Plugs + +Power plugs in different variants. + +## Simple Plug + +Simple plug with control of power state and startup behavior. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|----------------------------------------------| +| `power` | Switch | RW | Power state of plug | + +## Power Plug + +Power plug with control of power state, startup behavior, hardware on/off button and status light. +Same channels as [Simple Plug](#simple-plug) plus following channels. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|----------------------------------------------| +| `child-lock` | Switch | RW | Child lock for button on plug | +| `disable-status-light`| Switch | RW | Disable status light on plug | + +## Smart Power Plug + +Smart plug like [Power Plug](#power-plug) plus measuring capability. + +| Channel | Type | Read/Write | Description | +|-----------------------|---------------------------|------------|----------------------------------------------| +| `electric-power` | Number:Power | R | Electric power delivered by plug | +| `energy-total` | Number:Energy | R | Total energy consumption | +| `energy-reset` | Number:Energy | R | Energy consumption since last reset | +| `reset-date` | DateTime | RW | Date and time of last reset | +| `electric-current` | Number:ElectricCurrent | R | Electric current measured by plug | +| `electric-voltage` | Number:ElectricPotential | R | Electric potential of plug | + +Smart plug provides `energy-total` measuring energy consumption over lifetime and `energy-reset` measuring energy consumption from `reset-date` till now. +Channel `reset-date` is writable and will set the date time to the timestamp of command execution. +Past and future timestamps are not possible and will be ignored. + +## Sensors + +Various sensors for detecting events and measuring. + +## Motion Sensor + +Sensor detecting motion events. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|--------------------------------------------------| +| `motion` | Switch | R | Motion detected by the device | +| `active-duration` | Number:Time | RW | Keep connected devices active for this duration | +| `battery-level` | Number:Dimensionless | R | Battery charge level in percent | +| `schedule` | Number | RW | Schedule when the sensor shall be active | +| `schedule-start` | DateTime | RW | Start time of sensor activity | +| `schedule-end` | DateTime | RW | End time of sensor activity | +| `light-preset` | String | RW | Light presets for different times of the day | + +When motion is detected via `motion` channel all connected devices from `links` channel will be active for the time configured in `active-duration`. +Standard duration is seconds if raw number is sent as command. +See [Motion Sensor Rules](#motion-sensor-rules) for further examples. + +Mappings for `schedule` + +- 0 : Always, sensor is always active +- 1 : Follow sun, sensor gets active at sunset and deactivates at sunrise +- 2 : Schedule, custom schedule with manual start and end time + +If option 1, follow sun is selected ensure you gave the permission in the IKEA Home smart app to use your GPS position to calculate times for sunrise and sunset. + +See [Light Controller](#light-controller) for light-preset`. + +## Motion Light Sensor + +Sensor detecting motion events and measures light level. +Same channels as [Motion Sensor](#motion-sensor) with an additional `illuminance` channel. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|----------------------------------------------| +| `illuminance` | Number:Illuminance | R | Illuminance in Lux | + +## Water Sensor + +Sensor to detect water leaks. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|----------------------------------------------| +| `leak` | Switch | R | Water leak detected | +| `battery-level` | Number:Dimensionless | R | Battery charge level in percent | + +## Contact Sensor + +Sensor tracking if windows or doors are open + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|----------------------------------------------| +| `contact` | Contact | R | State if door or window is open or closed | +| `battery-level` | Number:Dimensionless | R | Battery charge level in percent | + +## Air Quality Sensor + +Air measure for temperature, humidity and particles. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|------------------------------------------------------| +| `temperature` | Number:Temperature | R | Air Temperature | +| `humidity` | Number:Dimensionless | R | Air Humidity | +| `particulate-matter` | Number:Density | R | Category 2.5 particulate matter | +| `voc-index` | Number | R | Relative VOC intensity compared to recent history | + +The VOC Index mimics the human nose’s perception of odors with a relative intensity compared to recent history. +The VOC Index is also sensitive to odorless VOCs, but it cannot discriminate between them. +See more information in the [sensor description](https://sensirion.com/media/documents/02232963/6294E043/Info_Note_VOC_Index.pdf). + +## Controller + +Controller for lights, plugs, blinds, shortcuts and speakers. + +## Single Shortcut Controller + +Shortcut controller with one button. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|----------------------------------------------| +| `button1` | trigger | | Trigger of first button | +| `battery-level` | Number:Dimensionless | R | Battery charge level in percent | + +### Button Triggers + +Triggers for `button1` + +- SHORT_PRESSED +- DOUBLE_PRESSED +- LONG_PRESSED + +## Double Shortcut Controller + +Shortcut controller with two buttons. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|----------------------------------------------| +| `button2` | trigger | | Trigger of second button | + +Same as [Single Shortcut Controller](#single-shortcut-controller) with additional `button2` trigger channel. + +## Light Controller + +Controller to handle light attributes. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|----------------------------------------------| +| `battery-level` | Number:Dimensionless | R | Battery charge level in percent | +| `light-preset` | String | RW | Light presets for different times of the day | + + + +Channel `light-preset` provides a JSON array with time an light settings for different times. +If light is switched on by the controller the light attributes for the configured time section is used. +This only works for connected devices shown in channel `links`. + +IKEA provided some presets which can be selected but it's also possible to generate a custom schedule. +They are provided as options as strings + +- Warm +- Slowdown +- Smooth +- Bright + +This feature is from IKEA test center and not officially present in the IKEA Home smart app now. + +## Blind Controller + +Controller to open and close blinds. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|----------------------------------------------| +| `battery-level` | Number:Dimensionless | R | Battery charge level in percent | + +## Sound Controller + +Controller for speakers. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|----------------------------------------------| +| `battery-level` | Number:Dimensionless | R | Battery charge level in percent | + +## Speaker + +Speaker with player activities. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|----------------------------------------------| +| `media-control` | Player | RW | Media control play, pause, next, previous | +| `volume` | Dimmer | RW | Handle volume in percent | +| `mute` | Switch | R(W) | Mute current audio without stop playing | +| `shuffle` | Switch | RW | Control shuffle mode | +| `crossfade` | Switch | RW | Cross fading between tracks | +| `repeat` | Number | RW | Over-the-air overall status | +| `media-title` | String | R | Title of a played media file | +| `image` | RawType | R | Current playing track image | + +Channel `mute` should be writable but this isnn't the case now. +See [Known Limitations](#speaker-limitations). + +## Repeater + +Repeater to strengthen signal. +Sadly there's no further information like _signal strength_ available so only [OTA channels](#ota-channels) and [custom name](#other-channels) is available. + +## Scenes + +Scene from IKEA home smart app which can be triggered. + +| Channel | Type | Read/Write | Description | +|-----------------------|-----------------------|------------|----------------------------------------------| +| `trigger` | Number | RW | Trigger / undo scene execution | +| `last-trigger` | DateTime | R | Date and time when last trigger occurred | + +Scenes are defined in IKEA Home smart app and can be performed via `trigger` channel. +Two commands are defined: + +- 0 : Trigger +- 1 : Undo + +If command 0 (Trigger) is sent scene will be executed. +There's a 30 seconds time slot to send command 1 (Undo). +The countdown is updating `trigger` channel state which can be evaluated if an undo operation is still possible. +State will switch to `Undef` after countdown. + +## Known Limitations + +### Gateway Limitations + +Gateway channel `location` is reflecting the state correctly but isn't writable. +The Model says it `canReceive` command `coordinates` but in fact sending responds `http status 400`. +Channel will stay in this binding hoping a DIRIGERA software update will resolve this issue. + +### Speaker Limitations + +Speaker channel `mute` is reflecting the state correctly but isn't writable. +The Model says it `canReceive` command `isMuted` but in fact sending responds `http status 400`. +If mute is performed on Sonos App the channel is updating correctly, but sending the command fails! +Channel will stay in this binding hoping a DIRIGERA software update will resolve this issue. + +## Development and Testing + +Debugging is essential for such a binding which supports many available products and needs to support future products. +General debug messages will overflow traces and it's hard to find relevant information. +To deal with these challenges commands for [openHAB console](https://www.openhab.org/docs/administration/console.html) are provided. + +``` +Usage: openhab:dirigera token - Get token from DIRIGERA hub +Usage: openhab:dirigera json [ | all] - Print JSON data +Usage: openhab:dirigera debug [ | all] [true | false] - Enable / disable detailed debugging for specific / all devices +``` + +### `token` + +Prints the access token to communicate with DIRIGERA gateway as console output. + +``` +console> openhab:dirigera token +DIRIGERA Hub token: abcdef12345....... +``` + +With token available you can test your devices e.g. via curl commands. + +```java +curl -X PATCH https://$YOUR_IP:8443/v1/devices/$DEVICE -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '[{"attributes":{"colorHue":280,"colorSaturation":1}}]' --insecure +``` + +Replace content in curl command with following variables: + +- $YOUR_IP - IP address of DIRIGERA gateway +- $DEVICE - bulb id you want to control, take it from configuration +- $TOKEN - shortly stop / start DIRIGERA bridge and search for obtained token + +### `json` + +Get capabilities and current status for one `deviceId` or all devices. +Output is shown on console as JSON String. + +``` +console> openhab:dirigera json 3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1 +{"deviceType":"light","isReachable":true,"capabilities":{"canReceive":["customName","isOn","lightLevel","colorTemperature", ...} +``` + +### `debug` + +Enables or disables detailed logging for one `deviceId` or all devices. +Answer is `Done` if command is successfully executed. +If you operate with the device you can see requests and responses in openHAB Log Viewer. +If device cannot be found answer is `Device Id xyz not found `. + +``` +console> openhab:dirigera debug all true +Done +``` + +## Full Example + +### Thing Configuration + +```java +Bridge dirigera:gateway:myhome "My wonderful Home" [ ipAddress="1.2.3.4", discovery=true ] { + Thing temperature-light living-room-bulb "Living Room Table Lamp" [ id="aaaaaaaa-bbbb-xxxx-yyyy-zzzzzzzzzzzz"] + Thing smart-plug dishwasher "Dishwasher" [ id="zzzzzzzz-yyyy-xxxx-aaaa-bbbbbbbbbbbb"] + Thing motion-sensor bedroom-motion "Bedroom Motion" [ id="zzzzzzzz-yyyy-xxxx-aaaa-ffffffffffff"] +} +``` + +### Item Configuration + +```java +Switch Bedroom_Motion_Detection { channel="dirigera:motion-sensor:myhome:bedroom-motion:motion" } +Number:Time Bedroom_Motion_Active_Duration { channel="dirigera:motion-sensor:myhome:bedroom-motion:active-duration" } +Number Bedroom_Motion_Schedule { channel="dirigera:motion-sensor:myhome:bedroom-motion:schedule" } +DateTime Bedroom_Motion_Schedule_Start { channel="dirigera:motion-sensor:myhome:bedroom-motion:schedule-start" } +DateTime Bedroom_Motion_Schedule_End { channel="dirigera:motion-sensor:myhome:bedroom-motion:schedule-end" } +Number:Dimensionless Bedroom_Motion_Battery_Level { channel="dirigera:motion-sensor:myhome:bedroom-motion:battery-level" } + +Switch Table_Lamp_Power_State { channel="dirigera:temperature-light:myhome:living-room-bulb:power" } +Dimmer Table_Lamp_Brightness { channel="dirigera:temperature-light:myhome:living-room-bulb:brightness" } +Dimmer Table_Lamp_Temperature { channel="dirigera:temperature-light:myhome:living-room-bulb:color-temperature" } +Number Table_Lamp_Startup { channel="dirigera:temperature-light:myhome:living-room-bulb:startup" } +Number Table_Lamp_OTA_Status { channel="dirigera:temperature-light:myhome:living-room-bulb:ota-status" } +Number Table_Lamp_OTA_State { channel="dirigera:temperature-light:myhome:living-room-bulb:ota-state" } +Number Table_Lamp_OTA_Progress { channel="dirigera:temperature-light:myhome:living-room-bulb:ota-progress" } + +Switch Dishwasher_Power_State { channel="dirigera:smart-plug:myhome:dishwasher:power" } +Switch Dishwasher_Child_lock { channel="dirigera:smart-plug:myhome:dishwasher:child-lock" } +Switch Dishwasher_Disable_Light { channel="dirigera:smart-plug:myhome:dishwasher:disable-light" } +Number:Power Dishwasher_Power { channel="dirigera:smart-plug:myhome:dishwasher:electric-power" } +Number:Energy Dishwasher_Energy_Total { channel="dirigera:smart-plug:myhome:dishwasher:energy-total" } +Number:Energy Dishwasher_Energy_Reset { channel="dirigera:smart-plug:myhome:dishwasher:energy-reset" } +Number:ElectricCurrent Dishwasher_Ampere { channel="dirigera:smart-plug:myhome:dishwasher:electric-current" } +Number:ElectricPotential Dishwasher_Voltage { channel="dirigera:smart-plug:myhome:dishwasher:electric-potential" } +Number Dishwasher_Startup { channel="dirigera:smart-plug:myhome:dishwasher:startup" } +Number Dishwasher_OTA_Status { channel="dirigera:smart-plug:myhome:dishwasher:ota-status" } +Number Dishwasher_OTA_State { channel="dirigera:smart-plug:myhome:dishwasher:ota-state" } +Number Dishwasher_OTA_Progress { channel="dirigera:smart-plug:myhome:dishwasher:ota-progress" } +``` + +### Rule Examples + +#### Shortcut Controller Rules + +Catch triggers from shortcut controller and trigger a scene. + +```java +rule "Shortcut Button 1 Triggers" +when + Channel 'dirigera:double-shortcut:myhome:my-shortcut-controller:button1' triggered +then + logInfo("DIRIGERA","Button 1 {}",receivedEvent) + myhome-light-scene.sendCommand(0) +end +``` + +#### Motion Sensor Rules + +Change the active duration time + +```java +rule "Sensor configuration" +when + System started +then + logInfo("DIRIGERA","Configuring IKEA sensors") + // active duration = 180 seconds + Bedroom_Motion_Active_Duration.sendCommand(180) + // active duration = 3 minutes aka 180 seconds + Bedroom_Motion_Active_Duration.sendCommand("3 min") +end +``` + + +## Credits + +This work is based on [Leggin](https://github.com/Leggin/dirigera) and [dvdgeisler](https://github.com/dvdgeisler/DirigeraClient). +Without these contributions this binding wouldn't be possible! diff --git a/bundles/org.openhab.binding.dirigera/doc/follow-sun.png b/bundles/org.openhab.binding.dirigera/doc/follow-sun.png new file mode 100644 index 0000000000000000000000000000000000000000..189d5766ed0df14c2e49c9d8d7f8dc33a3983b77 GIT binary patch literal 94068 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

JOcV+ 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

|2WifgFC| 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 VV&wSMoJe*t)c;JW|- literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.dirigera/doc/light-presets.png b/bundles/org.openhab.binding.dirigera/doc/light-presets.png new file mode 100644 index 0000000000000000000000000000000000000000..01252478e1194383c8c9d028b7c8df826fc4ab3f GIT binary patch literal 159960 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.dirigera/pom.xml b/bundles/org.openhab.binding.dirigera/pom.xml new file mode 100644 index 00000000000..d03a2a1f107 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 5.0.0-SNAPSHOT + + + org.openhab.binding.dirigera + + openHAB Add-ons :: Bundles :: Dirigera Binding + + + + org.json + json + 20231013 + compile + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/feature/feature.xml b/bundles/org.openhab.binding.dirigera/src/main/feature/feature.xml new file mode 100644 index 00000000000..5924fcd89fa --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.dirigera/${project.version} + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/Constants.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/Constants.java new file mode 100644 index 00000000000..e39d4ec8600 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/Constants.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link Constants} class defines common constants, which are + * used across the whole binding. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class Constants { + public static final String BINDING_ID = "dirigera"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_GATEWAY = new ThingTypeUID(BINDING_ID, "gateway"); + public static final ThingTypeUID THING_TYPE_COLOR_LIGHT = new ThingTypeUID(BINDING_ID, "color-light"); + public static final ThingTypeUID THING_TYPE_TEMPERATURE_LIGHT = new ThingTypeUID(BINDING_ID, "temperature-light"); + public static final ThingTypeUID THING_TYPE_DIMMABLE_LIGHT = new ThingTypeUID(BINDING_ID, "dimmable-light"); + public static final ThingTypeUID THING_TYPE_SWITCH_LIGHT = new ThingTypeUID(BINDING_ID, "switch-light"); + public static final ThingTypeUID THING_TYPE_MOTION_SENSOR = new ThingTypeUID(BINDING_ID, "motion-sensor"); + public static final ThingTypeUID THING_TYPE_LIGHT_SENSOR = new ThingTypeUID(BINDING_ID, "light-sensor"); + public static final ThingTypeUID THING_TYPE_MOTION_LIGHT_SENSOR = new ThingTypeUID(BINDING_ID, + "motion-light-sensor"); + public static final ThingTypeUID THING_TYPE_CONTACT_SENSOR = new ThingTypeUID(BINDING_ID, "contact-sensor"); + public static final ThingTypeUID THING_TYPE_SIMPLE_PLUG = new ThingTypeUID(BINDING_ID, "simple-plug"); + public static final ThingTypeUID THING_TYPE_POWER_PLUG = new ThingTypeUID(BINDING_ID, "power-plug"); + public static final ThingTypeUID THING_TYPE_SMART_PLUG = new ThingTypeUID(BINDING_ID, "smart-plug"); + public static final ThingTypeUID THING_TYPE_SPEAKER = new ThingTypeUID(BINDING_ID, "speaker"); + public static final ThingTypeUID THING_TYPE_SCENE = new ThingTypeUID(BINDING_ID, "scene"); + public static final ThingTypeUID THING_TYPE_REPEATER = new ThingTypeUID(BINDING_ID, "repeater"); + public static final ThingTypeUID THING_TYPE_LIGHT_CONTROLLER = new ThingTypeUID(BINDING_ID, "light-controller"); + public static final ThingTypeUID THING_TYPE_BLIND_CONTROLLER = new ThingTypeUID(BINDING_ID, "blind-controller"); + public static final ThingTypeUID THING_TYPE_SOUND_CONTROLLER = new ThingTypeUID(BINDING_ID, "sound-controller"); + public static final ThingTypeUID THING_TYPE_SINGLE_SHORTCUT_CONTROLLER = new ThingTypeUID(BINDING_ID, + "single-shortcut"); + public static final ThingTypeUID THING_TYPE_DOUBLE_SHORTCUT_CONTROLLER = new ThingTypeUID(BINDING_ID, + "double-shortcut"); + public static final ThingTypeUID THING_TYPE_AIR_PURIFIER = new ThingTypeUID(BINDING_ID, "air-purifier"); + public static final ThingTypeUID THING_TYPE_AIR_QUALITY = new ThingTypeUID(BINDING_ID, "air-quality"); + public static final ThingTypeUID THING_TYPE_WATER_SENSOR = new ThingTypeUID(BINDING_ID, "water-sensor"); + public static final ThingTypeUID THING_TYPE_BLIND = new ThingTypeUID(BINDING_ID, "blind"); + public static final ThingTypeUID THING_TYPE_UNKNNOWN = new ThingTypeUID(BINDING_ID, "unkown"); + public static final ThingTypeUID THING_TYPE_NOT_FOUND = new ThingTypeUID(BINDING_ID, "not-found"); + public static final ThingTypeUID THING_TYPE_IGNORE = new ThingTypeUID(BINDING_ID, "ignore"); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_GATEWAY, + THING_TYPE_COLOR_LIGHT, THING_TYPE_TEMPERATURE_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_MOTION_SENSOR, + THING_TYPE_CONTACT_SENSOR, THING_TYPE_SIMPLE_PLUG, THING_TYPE_POWER_PLUG, THING_TYPE_SMART_PLUG, + THING_TYPE_SPEAKER, THING_TYPE_SCENE, THING_TYPE_REPEATER, THING_TYPE_LIGHT_CONTROLLER, + THING_TYPE_BLIND_CONTROLLER, THING_TYPE_SOUND_CONTROLLER, THING_TYPE_SINGLE_SHORTCUT_CONTROLLER, + THING_TYPE_DOUBLE_SHORTCUT_CONTROLLER, THING_TYPE_MOTION_LIGHT_SENSOR, THING_TYPE_AIR_QUALITY, + THING_TYPE_AIR_PURIFIER, THING_TYPE_WATER_SENSOR, THING_TYPE_BLIND, THING_TYPE_SWITCH_LIGHT); + + public static final Set IGNORE_THING_TYPES_UIDS = Set.of(THING_TYPE_LIGHT_SENSOR, THING_TYPE_IGNORE); + + public static final List THING_PROPERTIES = List.of("model", "manufacturer", "firmwareVersion", + "hardwareVersion", "serialNumber", "productCode"); + + public static final String WS_URL = "wss://%s:8443/v1"; + public static final String BASE_URL = "https://%s:8443/v1"; + public static final String OAUTH_URL = BASE_URL + "/oauth/authorize"; + public static final String TOKEN_URL = BASE_URL + "/oauth/token"; + public static final String HOME_URL = BASE_URL + "/home"; + public static final String DEVICE_URL = BASE_URL + "/devices/%s"; + public static final String SCENE_URL = BASE_URL + "/scenes/%s"; + public static final String SCENES_URL = BASE_URL + "/scenes"; + + public static final String PROPERTY_IP_ADDRESS = "ipAddress"; + public static final String PROPERTY_DEVICES = "devices"; + public static final String PROPERTY_SCENES = "scenes"; + public static final String PROPERTY_DEVICE_ID = "id"; + public static final String PROPERTY_DEVICE_TYPE = "deviceType"; + public static final String PROPERTY_TYPE = "type"; + public static final String PROPERTY_TOKEN = "token"; + public static final String PROPERTY_ATTRIBUTES = "attributes"; + public static final String PROPERTY_OTA_STATUS = "otaStatus"; + public static final String PROPERTY_OTA_STATE = "otaState"; + public static final String PROPERTY_OTA_PROGRESS = "otaProgress"; + public static final String PROPERTY_BATTERY_PERCENTAGE = "batteryPercentage"; + public static final String PROPERTY_PERMIT_JOIN = "permittingJoin"; + public static final String PROPERTY_STARTUP_BEHAVIOR = "startupOnOff"; + public static final String PROPERTY_POWER_STATE = "isOn"; + public static final String PROPERTY_CUSTOM_NAME = "customName"; + public static final String PROPERTY_REMOTE_LINKS = "remoteLinks"; + + public static final String PROPERTY_EMPTY = ""; + + public static final String ATTRIBUTE_COLOR_MODE = "colorMode"; + + public static final String DEVICE_TYPE_GATEWAY = "gateway"; + public static final String DEVICE_TYPE_SPEAKER = "speaker"; + public static final String DEVICE_TYPE_REPEATER = "repeater"; + public static final String DEVICE_TYPE_AIR_PURIFIER = "airPurifier"; + public static final String DEVICE_TYPE_BLINDS = "blinds"; + public static final String TYPE_USER_SCENE = "userScene"; + public static final String TYPE_CUSTOM_SCENE = "customScene"; + + public static final String DEVICE_TYPE_LIGHT = "light"; + + public static final String DEVICE_TYPE_MOTION_SENSOR = "motionSensor"; + public static final String DEVICE_TYPE_LIGHT_SENSOR = "lightSensor"; + public static final String DEVICE_TYPE_CONTACT_SENSOR = "openCloseSensor"; + public static final String DEVICE_TYPE_ENVIRONMENT_SENSOR = "environmentSensor"; + public static final String DEVICE_TYPE_WATER_SENSOR = "waterSensor"; + public static final String DEVICE_TYPE_OUTLET = "outlet"; + + public static final String DEVICE_TYPE_LIGHT_CONTROLLER = "lightController"; + public static final String DEVICE_TYPE_BLIND_CONTROLLER = "blindsController"; + public static final String DEVICE_TYPE_SOUND_CONTROLLER = "soundController"; + public static final String DEVICE_TYPE_SHORTCUT_CONTROLLER = "shortcutController"; + + // Generic channels + public static final String CHANNEL_CUSTOM_NAME = "custom-name"; + public static final String CHANNEL_LINKS = "links"; + public static final String CHANNEL_LINK_CANDIDATES = "link-candidates"; + public static final String CHANNEL_POWER_STATE = "power"; + public static final String CHANNEL_STARTUP_BEHAVIOR = "startup"; + public static final String CHANNEL_BATTERY_LEVEL = "battery-level"; + public static final String CHANNEL_OTA_STATUS = "ota-status"; + public static final String CHANNEL_OTA_STATE = "ota-state"; + public static final String CHANNEL_OTA_PROGRESS = "ota-progress"; + + // Gateway channels + public static final String CHANNEL_LOCATION = "location"; + public static final String CHANNEL_SUNRISE = "sunrise"; + public static final String CHANNEL_SUNSET = "sunset"; + public static final String CHANNEL_PAIRING = "pairing"; + public static final String CHANNEL_STATISTICS = "statistics"; + + // Light channels + public static final String CHANNEL_LIGHT_BRIGHTNESS = "brightness"; + public static final String CHANNEL_LIGHT_TEMPERATURE = "color-temperature"; + public static final String CHANNEL_LIGHT_TEMPERATURE_ABS = "color-temperature-abs"; + + public static final String CHANNEL_LIGHT_COLOR = "color"; + public static final String CHANNEL_LIGHT_PRESET = "light-preset"; + + // Sensor channels + public static final String CHANNEL_MOTION_DETECTION = "motion"; + public static final String CHANNEL_LEAK_DETECTION = "leak"; + public static final String CHANNEL_ILLUMINANCE = "illuminance"; + public static final String CHANNEL_CONTACT = "contact"; + public static final String CHANNEL_ACTIVE_DURATION = "active-duration"; + public static final String CHANNEL_SCHEDULE = "schedule"; + public static final String CHANNEL_SCHEDULE_START = "schedule-start"; + public static final String CHANNEL_SCHEDULE_END = "schedule-end"; + + // Plug channels + public static final String CHANNEL_POWER = "electric-power"; + public static final String CHANNEL_ENERGY_TOTAL = "energy-total"; + public static final String CHANNEL_ENERGY_RESET = "energy-reset"; + public static final String CHANNEL_ENERGY_RESET_DATE = "reset-date"; + public static final String CHANNEL_CURRENT = "electric-current"; + public static final String CHANNEL_POTENTIAL = "electric-voltage"; + public static final String CHANNEL_CHILD_LOCK = "child-lock"; + public static final String CHANNEL_DISABLE_STATUS_LIGHT = "disable-status-light"; + + // Speaker channels + public static final String CHANNEL_PLAYER = "media-control"; + public static final String CHANNEL_VOLUME = "volume"; + public static final String CHANNEL_MUTE = "mute"; + public static final String CHANNEL_TRACK = "media-title"; + public static final String CHANNEL_PLAY_MODES = "modes"; + public static final String CHANNEL_SHUFFLE = "shuffle"; + public static final String CHANNEL_REPEAT = "repeat"; + public static final String CHANNEL_CROSSFADE = "crossfade"; + public static final String CHANNEL_IMAGE = "image"; + + // Scene channels + public static final String CHANNEL_TRIGGER = "trigger"; + public static final String CHANNEL_LAST_TRIGGER = "last-trigger"; + + // Air quality channels + public static final String CHANNEL_TEMPERATURE = "temperature"; + public static final String CHANNEL_HUMIDITY = "humidity"; + public static final String CHANNEL_PARTICULATE_MATTER = "particulate-matter"; + public static final String CHANNEL_VOC_INDEX = "voc-index"; + + // Air purifier channels + public static final String CHANNEL_PURIFIER_FAN_MODE = "fan-mode"; + public static final String CHANNEL_PURIFIER_FAN_SPEED = "fan-speed"; + public static final String CHANNEL_PURIFIER_FAN_RUNTIME = "fan-runtime"; + public static final String CHANNEL_PURIFIER_FAN_SEQUENCE = "fan-sequence"; + public static final String CHANNEL_PURIFIER_FILTER_ELAPSED = "filter-elapsed"; + public static final String CHANNEL_PURIFIER_FILTER_REMAIN = "filter-remain"; + public static final String CHANNEL_PURIFIER_FILTER_LIFETIME = "filter-lifetime"; + public static final String CHANNEL_PURIFIER_FILTER_ALARM = "filter-alarm"; + + // Blinds channels + public static final String CHANNEL_BLIND_LEVEL = "blind-level"; + public static final String CHANNEL_BLIND_STATE = "blind-state"; + + // Shortcut channels + public static final String CHANNEL_BUTTON_1 = "button1"; + public static final String CHANNEL_BUTTON_2 = "button2"; + + // Websocket update types + public static final String EVENT_TYPE_DEVICE_DISCOVERED = "deviceDiscovered"; + public static final String EVENT_TYPE_DEVICE_ADDED = "deviceAdded"; + public static final String EVENT_TYPE_DEVICE_CHANGE = "deviceStateChanged"; + public static final String EVENT_TYPE_DEVICE_REMOVED = "deviceRemoved"; + + public static final String EVENT_TYPE_SCENE_CREATED = "sceneCreated"; + public static final String EVENT_TYPE_SCENE_UPDATE = "sceneUpdated"; + public static final String EVENT_TYPE_SCENE_DELETED = "sceneDeleted"; + + /** + * Maps connecting device attributes to channel ids + */ + + // Mappings for ota + public static final Map OTA_STATUS_MAP = Map.of("upToDate", 0, "updateAvailable", 1); + public static final Map OTA_STATE_MAP = new HashMap() { + private static final long serialVersionUID = 1L; + { + put("readyToCheck", 0); + put("checkInProgress", 1); + put("readyToDownload", 2); + put("downloadInProgress", 3); + put("updateInProgress", 4); + put("updateFailed", 5); + put("readyToUpdate", 6); + put("checkFailed", 7); + put("downloadFailed", 8); + put("updateComplete", 9); + put("batteryCheckFailed", 10); + } + }; + + // Mappings for startup behavior + public static final Map STARTUP_BEHAVIOR_MAPPING = Map.of("startPrevious", 0, "startOn", 1, + "startOff", 2, "startToggle", 3); + public static final Map STARTUP_BEHAVIOR_REVERSE_MAPPING = reverseStateMapping( + STARTUP_BEHAVIOR_MAPPING); + + /** + * DIRIGERA property to openHAB channel mappings + */ + public static final Map AIR_PURIFIER_MAP = new HashMap() { + private static final long serialVersionUID = 1L; + { + put("fanMode", CHANNEL_PURIFIER_FAN_MODE); + put("motorState", CHANNEL_PURIFIER_FAN_SPEED); + put("motorRuntime", CHANNEL_PURIFIER_FAN_RUNTIME); + put("filterElapsedTime", CHANNEL_PURIFIER_FILTER_ELAPSED); + put("filterAlarmStatus", CHANNEL_PURIFIER_FILTER_ALARM); + put("filterLifetime", CHANNEL_PURIFIER_FILTER_LIFETIME); + put("statusLight", CHANNEL_DISABLE_STATUS_LIGHT); + put("childLock", CHANNEL_CHILD_LOCK); + put("currentPM25", CHANNEL_PARTICULATE_MATTER); + put(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME); + } + }; + public static final Map AIR_QUALITY_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + "currentTemperature", CHANNEL_TEMPERATURE, "currentRH", CHANNEL_HUMIDITY, "currentPM25", + CHANNEL_PARTICULATE_MATTER, "vocIndex", CHANNEL_VOC_INDEX); + + public static final Map BLIND_CONTROLLER_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + "batteryPercentage", CHANNEL_BATTERY_LEVEL); + + public static final Map BLINDS_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + "blindsState", CHANNEL_BLIND_STATE, "batteryPercentage", CHANNEL_BATTERY_LEVEL, "blindsCurrentLevel", + CHANNEL_BLIND_LEVEL); + + public static final Map COLOR_LIGHT_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + PROPERTY_POWER_STATE, CHANNEL_POWER_STATE, "lightLevel", CHANNEL_LIGHT_BRIGHTNESS, "colorHue", + CHANNEL_LIGHT_COLOR, "colorSaturation", CHANNEL_LIGHT_COLOR, "colorTemperature", CHANNEL_LIGHT_TEMPERATURE, + PROPERTY_STARTUP_BEHAVIOR, CHANNEL_STARTUP_BEHAVIOR);; + + public static final Map CONTACT_SENSOR_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + "batteryPercentage", CHANNEL_BATTERY_LEVEL, "isOpen", CHANNEL_CONTACT); + + public static final Map LIGHT_CONTROLLER_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + "batteryPercentage", CHANNEL_BATTERY_LEVEL, "circadianPresets", CHANNEL_LIGHT_PRESET); + + public static final Map LIGHT_SENSOR_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + "illuminance", CHANNEL_ILLUMINANCE); + + public static final Map MOTION_LIGHT_SENSOR_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + "batteryPercentage", CHANNEL_BATTERY_LEVEL, "isDetected", CHANNEL_MOTION_DETECTION, "illuminance", + CHANNEL_ILLUMINANCE, "sensorConfig", CHANNEL_ACTIVE_DURATION, "schedule", CHANNEL_SCHEDULE, + "schedule-start", CHANNEL_SCHEDULE_START, "schedule-end", CHANNEL_SCHEDULE_END, "circadianPresets", + CHANNEL_LIGHT_PRESET); + + public static final Map MOTION_SENSOR_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + "batteryPercentage", CHANNEL_BATTERY_LEVEL, "isDetected", CHANNEL_MOTION_DETECTION, "sensorConfig", + CHANNEL_ACTIVE_DURATION, "schedule", CHANNEL_SCHEDULE, "schedule-start", CHANNEL_SCHEDULE_START, + "schedule-end", CHANNEL_SCHEDULE_END, "circadianPresets", CHANNEL_LIGHT_PRESET); + + public static final Map REPEATER_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME); + + public static final Map SCENE_MAP = Map.of("lastTriggered", CHANNEL_TRIGGER); + + public static final Map SHORTCUT_CONTROLLER_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + "batteryPercentage", CHANNEL_BATTERY_LEVEL); + + public static final Map SMART_PLUG_MAP = new HashMap() { + private static final long serialVersionUID = 1L; + { + put("isOn", CHANNEL_POWER_STATE); + put("currentActivePower", CHANNEL_POWER); + put("currentVoltage", CHANNEL_POTENTIAL); + put("currentAmps", CHANNEL_CURRENT); + put("totalEnergyConsumed", CHANNEL_ENERGY_TOTAL); + put("energyConsumedAtLastReset", CHANNEL_ENERGY_RESET); + put("timeOfLastEnergyReset", CHANNEL_ENERGY_RESET_DATE); + put("statusLight", CHANNEL_DISABLE_STATUS_LIGHT); + put("childLock", CHANNEL_CHILD_LOCK); + put(PROPERTY_STARTUP_BEHAVIOR, CHANNEL_STARTUP_BEHAVIOR); + put(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME); + } + }; + + public static final Map SOUND_CONTROLLER_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + "batteryPercentage", CHANNEL_BATTERY_LEVEL); + + public static final Map SPEAKER_MAP = Map.of("playback", CHANNEL_PLAYER, "volume", CHANNEL_VOLUME, + "isMuted", CHANNEL_MUTE, "playbackAudio", CHANNEL_TRACK, "playbackModes", CHANNEL_PLAY_MODES, + PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME); + + public static final Map TEMPERATURE_LIGHT_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + PROPERTY_POWER_STATE, CHANNEL_POWER_STATE, "lightLevel", CHANNEL_LIGHT_BRIGHTNESS, "colorTemperature", + CHANNEL_LIGHT_TEMPERATURE, PROPERTY_STARTUP_BEHAVIOR, CHANNEL_STARTUP_BEHAVIOR); + + public static final Map WATER_SENSOR_MAP = Map.of(PROPERTY_CUSTOM_NAME, CHANNEL_CUSTOM_NAME, + "batteryPercentage", CHANNEL_BATTERY_LEVEL, "waterLeakDetected", CHANNEL_LEAK_DETECTION); + + public static Map reverseStateMapping(Map mapping) { + Map reverseMap = new HashMap<>(); + for (Map.Entry entry : mapping.entrySet()) { + reverseMap.put(entry.getValue(), entry.getKey()); + } + return reverseMap; + } + + public static Map reverse(Map mapping) { + Map reverseMap = new HashMap<>(); + for (Map.Entry entry : mapping.entrySet()) { + reverseMap.put(entry.getValue(), entry.getKey()); + } + return reverseMap; + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/DirigeraCommandProvider.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/DirigeraCommandProvider.java new file mode 100644 index 00000000000..f505e64404c --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/DirigeraCommandProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.type.DynamicCommandDescriptionProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * Dynamic provider of command options while leaving other state description fields as original. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +@Component(service = { DynamicCommandDescriptionProvider.class, DirigeraCommandProvider.class }) +public class DirigeraCommandProvider extends BaseDynamicCommandDescriptionProvider { + @Activate + public DirigeraCommandProvider(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.dirigera/src/main/java/org/openhab/binding/dirigera/internal/DirigeraHandlerFactory.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/DirigeraHandlerFactory.java new file mode 100644 index 00000000000..f18effa72dc --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/DirigeraHandlerFactory.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.WWWAuthenticationProtocolHandler; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.openhab.binding.dirigera.internal.discovery.DirigeraDiscoveryService; +import org.openhab.binding.dirigera.internal.handler.DirigeraHandler; +import org.openhab.binding.dirigera.internal.handler.airpurifier.AirPurifierHandler; +import org.openhab.binding.dirigera.internal.handler.blind.BlindHandler; +import org.openhab.binding.dirigera.internal.handler.controller.BlindsControllerHandler; +import org.openhab.binding.dirigera.internal.handler.controller.DoubleShortcutControllerHandler; +import org.openhab.binding.dirigera.internal.handler.controller.LightControllerHandler; +import org.openhab.binding.dirigera.internal.handler.controller.ShortcutControllerHandler; +import org.openhab.binding.dirigera.internal.handler.controller.SoundControllerHandler; +import org.openhab.binding.dirigera.internal.handler.light.ColorLightHandler; +import org.openhab.binding.dirigera.internal.handler.light.DimmableLightHandler; +import org.openhab.binding.dirigera.internal.handler.light.SwitchLightHandler; +import org.openhab.binding.dirigera.internal.handler.light.TemperatureLightHandler; +import org.openhab.binding.dirigera.internal.handler.plug.PowerPlugHandler; +import org.openhab.binding.dirigera.internal.handler.plug.SimplePlugHandler; +import org.openhab.binding.dirigera.internal.handler.plug.SmartPlugHandler; +import org.openhab.binding.dirigera.internal.handler.repeater.RepeaterHandler; +import org.openhab.binding.dirigera.internal.handler.scene.SceneHandler; +import org.openhab.binding.dirigera.internal.handler.sensor.AirQualityHandler; +import org.openhab.binding.dirigera.internal.handler.sensor.ContactSensorHandler; +import org.openhab.binding.dirigera.internal.handler.sensor.MotionLightSensorHandler; +import org.openhab.binding.dirigera.internal.handler.sensor.MotionSensorHandler; +import org.openhab.binding.dirigera.internal.handler.sensor.WaterSensorHandler; +import org.openhab.binding.dirigera.internal.handler.speaker.SpeakerHandler; +import org.openhab.core.i18n.LocationProvider; +import org.openhab.core.storage.Storage; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link DirigeraHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.dirigera", service = ThingHandlerFactory.class) +public class DirigeraHandlerFactory extends BaseThingHandlerFactory { + private final Logger logger = LoggerFactory.getLogger(DirigeraHandlerFactory.class); + private final DirigeraStateDescriptionProvider stateProvider; + private final DirigeraDiscoveryService discoveryService; + private final DirigeraCommandProvider commandProvider; + private final LocationProvider locationProvider; + private final Storage bindingStorage; + private final HttpClient insecureClient; + + @Activate + public DirigeraHandlerFactory(@Reference StorageService storageService, + final @Reference DirigeraDiscoveryService discovery, final @Reference LocationProvider locationProvider, + final @Reference DirigeraCommandProvider commandProvider, + final @Reference DirigeraStateDescriptionProvider stateProvider) { + this.locationProvider = locationProvider; + this.commandProvider = commandProvider; + this.discoveryService = discovery; + this.stateProvider = stateProvider; + + this.insecureClient = new HttpClient(new SslContextFactory.Client(true)); + insecureClient.setUserAgentField(null); + try { + this.insecureClient.start(); + // from https://github.com/jetty-project/jetty-reactive-httpclient/issues/33#issuecomment-777771465 + insecureClient.getProtocolHandlers().remove(WWWAuthenticationProtocolHandler.NAME); + } catch (Exception e) { + // catching exception is necessary due to the signature of HttpClient.start() + logger.warn("DIRIGERA FACTORY Failed to start http client: {}", e.getMessage()); + throw new IllegalStateException("Could not create HttpClient", e); + } + bindingStorage = storageService.getStorage(BINDING_ID); + } + + @Deactivate + public void deactivate() { + try { + insecureClient.stop(); + } catch (Exception e) { + logger.warn("Failed to stop http client: {}", e.getMessage()); + } + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (THING_TYPE_GATEWAY.equals(thingTypeUID)) { + return new DirigeraHandler((Bridge) thing, insecureClient, bindingStorage, discoveryService, + locationProvider, commandProvider, bundleContext); + } else if (THING_TYPE_COLOR_LIGHT.equals(thingTypeUID)) { + return new ColorLightHandler(thing, COLOR_LIGHT_MAP, stateProvider); + } else if (THING_TYPE_TEMPERATURE_LIGHT.equals(thingTypeUID)) { + return new TemperatureLightHandler(thing, TEMPERATURE_LIGHT_MAP, stateProvider); + } else if (THING_TYPE_DIMMABLE_LIGHT.equals(thingTypeUID)) { + return new DimmableLightHandler(thing, TEMPERATURE_LIGHT_MAP); + } else if (THING_TYPE_SWITCH_LIGHT.equals(thingTypeUID)) { + return new SwitchLightHandler(thing, TEMPERATURE_LIGHT_MAP); + } else if (THING_TYPE_MOTION_SENSOR.equals(thingTypeUID)) { + return new MotionSensorHandler(thing, MOTION_SENSOR_MAP); + // } else if (THING_TYPE_LIGHT_SENSOR.equals(thingTypeUID)) { + // return new LightSensorHandler(thing, LIGHT_SENSOR_MAP); + } else if (THING_TYPE_MOTION_LIGHT_SENSOR.equals(thingTypeUID)) { + return new MotionLightSensorHandler(thing, MOTION_LIGHT_SENSOR_MAP); + } else if (THING_TYPE_CONTACT_SENSOR.equals(thingTypeUID)) { + return new ContactSensorHandler(thing, CONTACT_SENSOR_MAP); + } else if (THING_TYPE_SIMPLE_PLUG.equals(thingTypeUID)) { + return new SimplePlugHandler(thing, SMART_PLUG_MAP); + } else if (THING_TYPE_POWER_PLUG.equals(thingTypeUID)) { + return new PowerPlugHandler(thing, SMART_PLUG_MAP); + } else if (THING_TYPE_SMART_PLUG.equals(thingTypeUID)) { + return new SmartPlugHandler(thing, SMART_PLUG_MAP); + } else if (THING_TYPE_SPEAKER.equals(thingTypeUID)) { + return new SpeakerHandler(thing, SPEAKER_MAP); + } else if (THING_TYPE_SCENE.equals(thingTypeUID)) { + return new SceneHandler(thing, SCENE_MAP); + } else if (THING_TYPE_REPEATER.equals(thingTypeUID)) { + return new RepeaterHandler(thing, REPEATER_MAP); + } else if (THING_TYPE_LIGHT_CONTROLLER.equals(thingTypeUID)) { + return new LightControllerHandler(thing, LIGHT_CONTROLLER_MAP); + } else if (THING_TYPE_BLIND_CONTROLLER.equals(thingTypeUID)) { + return new BlindsControllerHandler(thing, BLIND_CONTROLLER_MAP); + } else if (THING_TYPE_SOUND_CONTROLLER.equals(thingTypeUID)) { + return new SoundControllerHandler(thing, SOUND_CONTROLLER_MAP); + } else if (THING_TYPE_SINGLE_SHORTCUT_CONTROLLER.equals(thingTypeUID)) { + return new ShortcutControllerHandler(thing, SHORTCUT_CONTROLLER_MAP, bindingStorage); + } else if (THING_TYPE_DOUBLE_SHORTCUT_CONTROLLER.equals(thingTypeUID)) { + return new DoubleShortcutControllerHandler(thing, SHORTCUT_CONTROLLER_MAP, bindingStorage); + } else if (THING_TYPE_AIR_QUALITY.equals(thingTypeUID)) { + return new AirQualityHandler(thing, AIR_QUALITY_MAP); + } else if (THING_TYPE_WATER_SENSOR.equals(thingTypeUID)) { + return new WaterSensorHandler(thing, WATER_SENSOR_MAP); + } else if (THING_TYPE_BLIND.equals(thingTypeUID)) { + return new BlindHandler(thing, BLINDS_MAP); + } else if (THING_TYPE_AIR_PURIFIER.equals(thingTypeUID)) { + return new AirPurifierHandler(thing, AIR_PURIFIER_MAP); + } else { + logger.debug("DIRIGERA FACTORY Request for {} doesn't match {}", thingTypeUID, THING_TYPE_GATEWAY); + return null; + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/DirigeraStateDescriptionProvider.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/DirigeraStateDescriptionProvider.java new file mode 100644 index 00000000000..24b2a4bb8c4 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/DirigeraStateDescriptionProvider.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider; +import org.openhab.core.thing.events.ThingEventFactory; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragment; +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 alert, effect, scene, and colour + * temperature channels whose capabilities are dynamically determined at runtime. + * + * @author Andrew Fiddian-Green - Initial contribution + * + */ +@NonNullByDefault +@Component(service = { DynamicStateDescriptionProvider.class, DirigeraStateDescriptionProvider.class }) +public class DirigeraStateDescriptionProvider extends BaseDynamicStateDescriptionProvider { + private Map stateDescriptionMap = new HashMap<>(); + + @Activate + public DirigeraStateDescriptionProvider(final @Reference EventPublisher eventPublisher, + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, + final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + this.eventPublisher = eventPublisher; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; + } + + @Override + public @Nullable StateDescription getStateDescription(Channel channel, + @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { + StateDescription original = null; + StateDescriptionFragment fragment = stateDescriptionMap.get(channel.getUID()); + if (fragment != null) { + original = fragment.toStateDescription(); + StateDescription modified = super.getStateDescription(channel, original, locale); + if (modified == null) { + modified = original; + } + return modified; + } + return super.getStateDescription(channel, original, locale); + } + + public void setStateDescription(ChannelUID channelUid, StateDescriptionFragment stateDescriptionFragment) { + StateDescription stateDescription = stateDescriptionFragment.toStateDescription(); + if (stateDescription != null) { + StateDescriptionFragment old = stateDescriptionMap.get(channelUid); + stateDescriptionMap.put(channelUid, stateDescriptionFragment); + Set linkedItems = null; + ItemChannelLinkRegistry compareRegistry = itemChannelLinkRegistry; + if (compareRegistry != null) { + linkedItems = compareRegistry.getLinkedItemNames(channelUid); + } + postEvent(ThingEventFactory.createChannelDescriptionChangedEvent(channelUid, + linkedItems != null ? linkedItems : Set.of(), stateDescriptionFragment, old)); + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/config/BaseDeviceConfiguration.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/config/BaseDeviceConfiguration.java new file mode 100644 index 00000000000..e16685e6a88 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/config/BaseDeviceConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link BaseDeviceConfiguration} configuration for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class BaseDeviceConfiguration { + + public String id = ""; +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/config/ColorLightConfiguration.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/config/ColorLightConfiguration.java new file mode 100644 index 00000000000..a708cb65f6a --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/config/ColorLightConfiguration.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link ColorLightConfiguration} configuration for lights with temperature or color attributes + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ColorLightConfiguration extends BaseDeviceConfiguration { + + public int fadeTime = 750; + public int fadeSequence = 0; +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/config/DirigeraConfiguration.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/config/DirigeraConfiguration.java new file mode 100644 index 00000000000..1442cba3abf --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/config/DirigeraConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link DirigeraConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class DirigeraConfiguration extends BaseDeviceConfiguration { + + public String ipAddress = ""; + public boolean discovery = true; + + @Override + public String toString() { + return "IP: " + ipAddress + ", ID: " + id; + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/console/DirigeraCommandExtension.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/console/DirigeraCommandExtension.java new file mode 100644 index 00000000000..d51bf8d83fd --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/console/DirigeraCommandExtension.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.console; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dirigera.internal.Constants; +import org.openhab.binding.dirigera.internal.handler.DirigeraHandler; +import org.openhab.binding.dirigera.internal.interfaces.DebugHandler; +import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.ConsoleCommandCompleter; +import org.openhab.core.io.console.StringsCompleter; +import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; +import org.openhab.core.io.console.extensions.ConsoleCommandExtension; +import org.openhab.core.thing.ThingRegistry; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link DirigeraCommandExtension} is responsible for handling console commands. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +@Component(service = ConsoleCommandExtension.class) +public class DirigeraCommandExtension extends AbstractConsoleCommandExtension { + + private static final String CMD_TOKEN = "token"; + private static final String CMD_JSON = "json"; + private static final String CMD_DEBUG = "debug"; + private static final List COMMANDS = List.of(CMD_TOKEN, CMD_JSON, CMD_DEBUG); + + private final ThingRegistry thingRegistry; + + /** + * Provides a completer for the DIRIGERA console commands. + * + * @param thingRegistry the ThingRegistry to access things and their handlers + */ + private class DirigeraConsoleCommandCompleter implements ConsoleCommandCompleter { + @Override + public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) { + if (cursorArgumentIndex <= 0) { + return new StringsCompleter(List.of(CMD_TOKEN, CMD_JSON, CMD_DEBUG), false).complete(args, + cursorArgumentIndex, cursorPosition, candidates); + } else if (cursorArgumentIndex == 1) { + List options = new ArrayList<>(); + options.add("all"); + options.addAll(getDeviceIds()); + return new StringsCompleter(options, false).complete(args, cursorArgumentIndex, cursorPosition, + candidates); + } else if (cursorArgumentIndex == 2) { + return new StringsCompleter(List.of("true", "false"), false).complete(args, cursorArgumentIndex, + cursorPosition, candidates); + } + return false; + } + } + + /** + * Decodes the console command arguments and checks for validity. + */ + private class DirigeraConsoleCommandDecoder { + boolean valid = false; + String command = ""; + String target = ""; + boolean enable = false; + + DirigeraConsoleCommandDecoder(String[] args) { + // Check parameter count and valid command, return immediately if invalid + if (args.length == 0 || args.length > 3) { + return; + } + command = args[0].toLowerCase(); + if (!COMMANDS.contains(command)) { + return; + } + // Command is valid, check parameters + switch (command) { + case CMD_TOKEN: + // No parameters expected for token command + if (args.length == 1) { + valid = true; + } + break; + case CMD_JSON: + // Take second parameter for device ID or 'all' + if (args.length == 2) { + target = args[1].toLowerCase(); + valid = true; + } + break; + case CMD_DEBUG: + // Three parameters expected for debug command, second as target and third as boolean + if (args.length == 3) { + target = args[1].toLowerCase(); + String booleanCandidate = args[2].toLowerCase(); + if (Boolean.TRUE.toString().toLowerCase().equals(booleanCandidate) + || Boolean.FALSE.toString().toLowerCase().equals(booleanCandidate)) { + enable = Boolean.valueOf(booleanCandidate); + valid = true; + } + } + break; + } + } + } + + @Activate + public DirigeraCommandExtension(final @Reference ThingRegistry thingRegistry) { + super(Constants.BINDING_ID, "Interact with the DIRIGERA binding."); + this.thingRegistry = thingRegistry; + } + + @Override + public void execute(String[] args, Console console) { + DirigeraConsoleCommandDecoder decoder = new DirigeraConsoleCommandDecoder(args); + if (decoder.valid) { + switch (decoder.command) { + case CMD_TOKEN -> printToken(console); + case CMD_JSON -> printJSON(decoder, console); + case CMD_DEBUG -> setDebugParameters(decoder, console); + } + } else { + printUsage(console); + } + } + + private void printToken(Console console) { + for (DirigeraHandler handler : getHubs()) { + console.println(handler.getThing().getLabel() + " token: " + handler.getToken()); + } + } + + private void printJSON(DirigeraConsoleCommandDecoder decodedCommand, Console console) { + String output = null; + if ("all".equals(decodedCommand.target)) { + for (DirigeraHandler handler : getHubs()) { + output = handler.getJSON(); + } + } else { + for (DebugHandler handler : getDevices()) { + if (decodedCommand.target.equals(handler.getDeviceId())) { + output = handler.getJSON(); + } + } + } + if (output != null) { + console.println(output); + } else { + console.println("Device Id " + decodedCommand.target + " not found"); + } + } + + private void setDebugParameters(DirigeraConsoleCommandDecoder decodedCommand, Console console) { + boolean success = false; + if ("all".equals(decodedCommand.target)) { + for (DirigeraHandler handler : getHubs()) { + handler.setDebug(decodedCommand.enable, true); + success = true; + } + } else { + for (DebugHandler handler : getDevices()) { + if (decodedCommand.target.equals(handler.getDeviceId())) { + handler.setDebug(decodedCommand.enable, false); + success = true; + } + } + } + if (success) { + console.println("Done"); + } else { + console.println("Device Id " + decodedCommand.target + " not found"); + } + } + + private List getHubs() { + return thingRegistry.getAll().stream().map(thing -> thing.getHandler()) + .filter(DirigeraHandler.class::isInstance).map(DirigeraHandler.class::cast).toList(); + } + + private List getDevices() { + return thingRegistry.getAll().stream().map(thing -> thing.getHandler()).filter(DebugHandler.class::isInstance) + .map(DebugHandler.class::cast).toList(); + } + + private List getDeviceIds() { + return getDevices().stream().map(debugHandler -> debugHandler.getDeviceId()).toList(); + } + + @Override + public List getUsages() { + return Arrays.asList(buildCommandUsage(CMD_TOKEN, "Get token from DIRIGERA hub"), + buildCommandUsage(CMD_JSON + " [ | all]", "Print JSON data"), + buildCommandUsage(CMD_DEBUG + " [ | all] [true | false] ", + "Enable / disable detailed debugging for specific / all devices")); + } + + @Override + public @Nullable ConsoleCommandCompleter getCompleter() { + return new DirigeraConsoleCommandCompleter(); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/discovery/DirigeraDiscoveryService.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/discovery/DirigeraDiscoveryService.java new file mode 100644 index 00000000000..520dede67b5 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/discovery/DirigeraDiscoveryService.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.discovery; + +import static org.openhab.binding.dirigera.internal.Constants.SUPPORTED_THING_TYPES_UIDS; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryService; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; + +/** + * {@link DirigeraDiscoveryService} notifies about about devices found by + * DIRIGERA hub + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +@Component(service = { DiscoveryService.class, + DirigeraDiscoveryService.class }, configurationPid = "dirigera.device.discovery") +public class DirigeraDiscoveryService extends AbstractDiscoveryService { + + @Activate + public DirigeraDiscoveryService() { + super(SUPPORTED_THING_TYPES_UIDS, 90); + } + + public void deviceDiscovered(DiscoveryResult result) { + thingDiscovered(result); + } + + public void deviceRemoved(DiscoveryResult result) { + thingRemoved(result.getThingUID()); + } + + @Override + protected void startScan() { + // no manual scan supported + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/discovery/DirigeraMDNSDiscoveryParticipant.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/discovery/DirigeraMDNSDiscoveryParticipant.java new file mode 100644 index 00000000000..2b434c29473 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/discovery/DirigeraMDNSDiscoveryParticipant.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.discovery; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.jmdns.ServiceInfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dirigera.internal.Constants; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingTypeUID; +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; + +/** + * {@link DirigeraMDNSDiscoveryParticipant} for mDNS discovery of DIRIGERA gateway + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "dirigera.mdns.discovery") +public class DirigeraMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant { + + private static final String SERVICE_TYPE = "_ihsp._tcp.local."; + private final Logger logger = LoggerFactory.getLogger(DirigeraMDNSDiscoveryParticipant.class); + + protected final ThingRegistry thingRegistry; + + @Activate + public DirigeraMDNSDiscoveryParticipant(final @Reference ThingRegistry thingRegistry) { + this.thingRegistry = thingRegistry; + } + + @Override + public Set getSupportedThingTypeUIDs() { + return Set.of(Constants.THING_TYPE_GATEWAY); + } + + @Override + public String getServiceType() { + return SERVICE_TYPE; + } + + @Override + public @Nullable DiscoveryResult createResult(ServiceInfo si) { + logger.trace("DIRIGERA mDNS createResult for {} with IPs {}", si.getQualifiedNameMap(), si.getURLs()); + Inet4Address[] ipAddresses = si.getInet4Addresses(); + String gatewayName = si.getQualifiedNameMap().get(ServiceInfo.Fields.Instance); + if (gatewayName != null) { + String ipAddress = null; + if (ipAddresses.length == 0) { + // case of mDNS isn't delivering IP address try to resolve it + String domain = si.getQualifiedNameMap().get(ServiceInfo.Fields.Domain); + String gatewayHostName = gatewayName + "." + domain; + try { + InetAddress address = InetAddress.getByName(gatewayHostName); + ipAddress = address.getHostAddress(); + } catch (Exception e) { + logger.warn("DIRIGERA mDNS failed to resolve IP for {} reason {}", gatewayHostName, e.getMessage()); + } + } else if (ipAddresses.length > 0) { + ipAddress = ipAddresses[0].getHostAddress(); + } + if (ipAddress != null) { + Map properties = new HashMap<>(); + properties.put(PROPERTY_IP_ADDRESS, ipAddress); + return DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_GATEWAY, gatewayName)) + .withLabel("DIRIGERA Hub").withRepresentationProperty(PROPERTY_IP_ADDRESS) + .withProperties(properties).build(); + } + } + return null; + } + + @Override + public @Nullable ThingUID getThingUID(ServiceInfo si) { + String gatewayName = si.getQualifiedNameMap().get(ServiceInfo.Fields.Instance); + if (gatewayName != null) { + return new ThingUID(Constants.THING_TYPE_GATEWAY, gatewayName); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/exception/ApiException.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/exception/ApiException.java new file mode 100644 index 00000000000..d6777d28011 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/exception/ApiException.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link ApiException} thrown in case of problems accessing API + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ApiException extends RuntimeException { + + private static final long serialVersionUID = -9075334430125847975L; + + public ApiException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/exception/GatewayException.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/exception/GatewayException.java new file mode 100644 index 00000000000..a5d63b5b7fb --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/exception/GatewayException.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link ApiException} thrown in case of problems accessing DIRIGERA gateway + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class GatewayException extends RuntimeException { + + private static final long serialVersionUID = -9187744844610930469L; + + public GatewayException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/exception/ModelException.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/exception/ModelException.java new file mode 100644 index 00000000000..a24bdb57029 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/exception/ModelException.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link ModelException} thrown in case of problems accessing Model + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ModelException extends RuntimeException { + + private static final long serialVersionUID = -3080953131870014248L; + + public ModelException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/BaseHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/BaseHandler.java new file mode 100644 index 00000000000..75ac50b9de5 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/BaseHandler.java @@ -0,0 +1,740 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.json.JSONArray; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.config.BaseDeviceConfiguration; +import org.openhab.binding.dirigera.internal.exception.GatewayException; +import org.openhab.binding.dirigera.internal.interfaces.DebugHandler; +import org.openhab.binding.dirigera.internal.interfaces.Gateway; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.binding.dirigera.internal.interfaces.PowerListener; +import org.openhab.core.library.CoreItemFactory; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +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.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.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.CommandOption; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link BaseHandler} for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class BaseHandler extends BaseThingHandler implements DebugHandler { + private final Logger logger = LoggerFactory.getLogger(BaseHandler.class); + private List powerListeners = new ArrayList<>(); + private @Nullable Gateway gateway; + + // to be overwritten by child class in order to route the updates to the right instance + protected @Nullable BaseHandler child; + + // maps to route properties to channels and vice versa + protected Map property2ChannelMap; + protected Map channel2PropertyMap; + + // cache to handle each refresh command properly + protected Map channelStateMap; + + /* + * hardlinks initialized with invalid links because the first update shall trigger a link update. If it's declared + * as empty no link update will be triggered. This is necessary for startup phase. + */ + protected List hardLinks = new ArrayList<>(Arrays.asList("undef")); + protected List softLinks = new ArrayList<>(); + protected List linkCandidateTypes = new ArrayList<>(); + + /** + * Lists for canReceive and can Send capabilities + */ + protected List receiveCapabilities = new ArrayList<>(); + protected List sendCapabilities = new ArrayList<>(); + + protected State requestedPowerState = UnDefType.UNDEF; + protected State currentPowerState = UnDefType.UNDEF; + protected BaseDeviceConfiguration config; + protected String customName = ""; + protected String deviceType = ""; + protected boolean disposed = true; + protected boolean online = false; + protected boolean customDebug = false; + + public BaseHandler(Thing thing, Map mapping) { + super(thing); + config = new BaseDeviceConfiguration(); + + // mapping contains, reverse mapping for commands plus state cache + property2ChannelMap = mapping; + channel2PropertyMap = reverse(mapping); + channelStateMap = initializeCache(mapping); + } + + protected void setChildHandler(BaseHandler child) { + this.child = child; + } + + @Override + public void initialize() { + disposed = false; + config = getConfigAs(BaseDeviceConfiguration.class); + + // first get bridge as Gateway + Bridge bridge = getBridge(); + if (bridge != null) { + updateStatus(ThingStatus.UNKNOWN); + BridgeHandler handler = bridge.getHandler(); + if (handler != null) { + if (handler instanceof Gateway gw) { + gateway = gw; + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/dirigera.device.status.wrong-bridge-type"); + return; + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/dirigera.device.missing-bridge-handler"); + return; + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/dirigera.device.status.missing-bridge"); + return; + } + + if (!checkHandler()) { + // if handler doesn't match model status will be set to offline and it will stay until correction + return; + } + + if (!config.id.isBlank()) { + updateProperties(); + BaseHandler proxy = child; + if (proxy != null) { + gateway().registerDevice(proxy, config.id); + } + } + } + + private void updateProperties() { + // fill canSend and canReceive capabilities + Map modelProperties = gateway().model().getPropertiesFor(config.id); + Object canReceiveCapabilities = modelProperties.get(Model.PROPERTY_CAN_RECEIVE); + if (canReceiveCapabilities instanceof JSONArray jsonArray) { + jsonArray.forEach(capability -> { + if (!receiveCapabilities.contains(capability.toString())) { + receiveCapabilities.add(capability.toString()); + } + }); + } + Object canSendCapabilities = modelProperties.get(Model.PROPERTY_CAN_SEND); + if (canSendCapabilities instanceof JSONArray jsonArray) { + jsonArray.forEach(capability -> { + if (!sendCapabilities.contains(capability.toString())) { + sendCapabilities.add(capability.toString()); + } + }); + } + + TreeMap handlerProperties = new TreeMap<>(editProperties()); + modelProperties.forEach((key, value) -> { + handlerProperties.put(key, value.toString()); + }); + updateProperties(handlerProperties); + } + + /** + * Handling of basic commands which are the same for many devices + * - RefreshType for all channels + * - Startup behavior for lights and plugs + * - Power state for lights and plugs + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (customDebug) { + logger.info("DIRIGERA {} handleCommand channel {} command {} {}", thing.getUID(), channelUID.getAsString(), + command.toFullString(), command.getClass()); + } + if (command instanceof RefreshType) { + String channel = channelUID.getIdWithoutGroup(); + State cachedState = channelStateMap.get(channel); + if (cachedState != null) { + super.updateState(channelUID, cachedState); + } + } else { + String targetChannel = channelUID.getIdWithoutGroup(); + String targetProperty = channel2PropertyMap.get(targetChannel); + if (targetProperty != null) { + switch (targetChannel) { + case CHANNEL_STARTUP_BEHAVIOR: + if (command instanceof DecimalType decimal) { + String behaviorCommand = STARTUP_BEHAVIOR_REVERSE_MAPPING.get(decimal.intValue()); + if (behaviorCommand != null) { + JSONObject stqartupAttributes = new JSONObject(); + stqartupAttributes.put(targetProperty, behaviorCommand); + sendAttributes(stqartupAttributes); + } + break; + } + break; + case CHANNEL_POWER_STATE: + if (command instanceof OnOffType onOff) { + if (customDebug) { + logger.info("DIRIGERA BASE_HANDLER {} OnOff command: Current {} / Wanted {}", + thing.getLabel(), currentPowerState, onOff); + } + requestedPowerState = onOff; + if (!currentPowerState.equals(onOff)) { + JSONObject attributes = new JSONObject(); + attributes.put(targetProperty, OnOffType.ON.equals(onOff)); + sendAttributes(attributes); + } else { + requestedPowerState = UnDefType.UNDEF; + if (customDebug) { + logger.info("DIRIGERA BASE_HANDLER Dismiss {} {}", thing.getLabel(), onOff); + } + } + } + break; + case CHANNEL_CUSTOM_NAME: + if (command instanceof StringType string) { + JSONObject attributes = new JSONObject(); + attributes.put(targetProperty, string.toString()); + sendAttributes(attributes); + } + break; + } + } else { + // handle channels which are not defined in device map + switch (targetChannel) { + case CHANNEL_LINKS: + if (customDebug) { + logger.info("DIRIGERA BASE_HANDLER {} remove connection {}", thing.getLabel(), + command.toFullString()); + } + if (command instanceof StringType string) { + linkUpdate(string.toFullString(), false); + } + break; + case CHANNEL_LINK_CANDIDATES: + if (customDebug) { + logger.info("DIRIGERA BASE_HANDLER {} add link {}", thing.getLabel(), + command.toFullString()); + } + if (command instanceof StringType string) { + linkUpdate(string.toFullString(), true); + } + break; + } + } + } + } + + /** + * Wrapper function to respect customDebug flag + * + * @param attributes + * @return status + */ + protected int sendAttributes(JSONObject attributes) { + int status = gateway().api().sendAttributes(config.id, attributes); + if (customDebug) { + logger.info("DIRIGERA BASE_HANDLER {} API call: Status {} payload {}", thing.getUID(), status, attributes); + } + return status; + } + + /** + * Wrapper function to respect customDebug flag + * + * @param attributes + * @return status + */ + protected int sendPatch(JSONObject patch) { + int status = gateway().api().sendPatch(config.id, patch); + if (customDebug) { + logger.info("DIRIGERA BASE_HANDLER {} API call: Status {} payload {}", thing.getUID(), status, patch); + } + return status; + } + + /** + * Handling generic channel updates for many devices. + * If they are not present in child configuration they won't be triggered. + * - Reachable flag for every device to evaluate ONLINE and OFFLINE states + * - Over the air (OTA) updates channels + * - Battery charge level + * - Startup behavior for lights and plugs + * - Power state for lights and plugs + * - custom name + * + * @param update + */ + public void handleUpdate(JSONObject update) { + if (customDebug) { + logger.info("DIRIGERA BASE_HANDLER {} handleUpdate JSON {}", thing.getUID(), update); + } + // check online offline for each device + if (update.has(Model.REACHABLE)) { + if (update.getBoolean(Model.REACHABLE)) { + updateStatus(ThingStatus.ONLINE); + online = true; + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/dirigera.device.status.not-reachable"); + online = false; + } + } + if (update.has(PROPERTY_DEVICE_TYPE) && deviceType.isBlank()) { + deviceType = update.getString(PROPERTY_DEVICE_TYPE); + } + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + // check OTA for each device + if (attributes.has(PROPERTY_OTA_STATUS)) { + createChannelIfNecessary(CHANNEL_OTA_STATUS, "ota-status", CoreItemFactory.NUMBER); + String otaStatusString = attributes.getString(PROPERTY_OTA_STATUS); + Integer otaStatus = OTA_STATUS_MAP.get(otaStatusString); + if (otaStatus != null) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), new DecimalType(otaStatus)); + } else { + logger.warn("DIRIGERA BASE_HANDLER {} Cannot decode ota status {}", thing.getLabel(), + otaStatusString); + } + } + if (attributes.has(PROPERTY_OTA_STATE)) { + createChannelIfNecessary(CHANNEL_OTA_STATE, "ota-state", CoreItemFactory.NUMBER); + String otaStateString = attributes.getString(PROPERTY_OTA_STATE); + Integer otaState = OTA_STATE_MAP.get(otaStateString); + if (otaState != null) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), new DecimalType(otaState)); + // if ota state changes also update properties to keep firmware in thing properties up to date + updateProperties(); + } else { + logger.warn("DIRIGERA BASE_HANDLER {} Cannot decode ota state {}", thing.getLabel(), + otaStateString); + } + } + if (attributes.has(PROPERTY_OTA_PROGRESS)) { + createChannelIfNecessary(CHANNEL_OTA_PROGRESS, "ota-percent", "Number:Dimensionless"); + updateState(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), + QuantityType.valueOf(attributes.getInt(PROPERTY_OTA_PROGRESS), Units.PERCENT)); + } + // battery also common, not for all but sensors and remote controller + if (attributes.has(PROPERTY_BATTERY_PERCENTAGE)) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_BATTERY_LEVEL), + QuantityType.valueOf(attributes.getInt(PROPERTY_BATTERY_PERCENTAGE), Units.PERCENT)); + } + if (attributes.has(PROPERTY_STARTUP_BEHAVIOR)) { + String startupString = attributes.getString(PROPERTY_STARTUP_BEHAVIOR); + Integer startupValue = STARTUP_BEHAVIOR_MAPPING.get(startupString); + if (startupValue != null) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_STARTUP_BEHAVIOR), + new DecimalType(startupValue)); + } else { + logger.warn("DIRIGERA BASE_HANDLER {} Cannot decode startup behavior {}", thing.getLabel(), + startupString); + } + } + if (attributes.has(PROPERTY_POWER_STATE)) { + currentPowerState = OnOffType.from(attributes.getBoolean(PROPERTY_POWER_STATE)); + updateState(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), currentPowerState); + synchronized (powerListeners) { + if (online) { + boolean requested = currentPowerState.equals(requestedPowerState); + powerListeners.forEach(listener -> { + listener.powerChanged((OnOffType) currentPowerState, requested); + }); + requestedPowerState = UnDefType.UNDEF; + } + } + } + if (attributes.has(PROPERTY_CUSTOM_NAME) && customName.isBlank()) { + customName = attributes.getString(PROPERTY_CUSTOM_NAME); + updateState(new ChannelUID(thing.getUID(), CHANNEL_CUSTOM_NAME), StringType.valueOf(customName)); + } + } + if (update.has(PROPERTY_REMOTE_LINKS)) { + JSONArray remoteLinks = update.getJSONArray(PROPERTY_REMOTE_LINKS); + List updateList = new ArrayList<>(); + remoteLinks.forEach(link -> { + updateList.add(link.toString()); + }); + Collections.sort(updateList); + Collections.sort(hardLinks); + if (!hardLinks.equals(updateList)) { + hardLinks = updateList; + // just update internal link list and let the gateway update do all updates regarding soft links + gateway().updateLinks(); + } + } + } + + protected synchronized void createChannelIfNecessary(String channelId, String channelTypeUID, String itemType) { + if (thing.getChannel(channelId) == null) { + if (customDebug) { + logger.info("DIRIGERA BASE_HANDLER {} create Channel {} {} {}", thing.getUID(), channelId, + channelTypeUID, itemType); + } + // https://www.openhab.org/docs/developer/bindings/#updating-the-thing-structure + ThingBuilder thingBuilder = editThing(); + // channel type UID needs to be defined in channel-types.xml + Channel channel = ChannelBuilder.create(new ChannelUID(thing.getUID(), channelId), itemType) + .withType(new ChannelTypeUID(BINDING_ID, channelTypeUID)).build(); + updateThing(thingBuilder.withChannel(channel).build()); + } + } + + protected boolean isPowered() { + return OnOffType.ON.equals(currentPowerState) && online; + } + + /** + * Update cache for refresh, then update state + */ + @Override + protected void updateState(ChannelUID channelUID, State state) { + channelStateMap.put(channelUID.getIdWithoutGroup(), state); + if (!disposed) { + if (customDebug) { + logger.info("DIRIGERA {} updateState {} {}", thing.getUID(), channelUID, state); + } + super.updateState(channelUID, state); + } + } + + @Override + public void dispose() { + disposed = true; + online = false; + BaseHandler proxy = child; + if (proxy != null) { + gateway().unregisterDevice(proxy, config.id); + } + super.dispose(); + } + + @Override + public void handleRemoval() { + BaseHandler proxy = child; + if (proxy != null) { + gateway().deleteDevice(proxy, config.id); + } + super.handleRemoval(); + } + + public Gateway gateway() { + Gateway gw = gateway; + if (gw != null) { + return gw; + } else { + throw new GatewayException(thing.getUID() + " has no Gateway defined"); + } + } + + protected boolean checkHandler() { + // cross check if configured thing type is matching with the model + // if handler is taken from discovery this will do no harm + // but if it's created manually mismatch can happen + ThingTypeUID modelTTUID = gateway().model().identifyDeviceFromModel(config.id); + if (!thing.getThingTypeUID().equals(modelTTUID)) { + // check if id is present in model + if (THING_TYPE_NOT_FOUND.equals(modelTTUID)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, + "@text/dirigera.device.status.id-not-found" + " [\"" + config.id + "\"]"); + } else { + // String message = "Handler " + thing.getThingTypeUID() + " doesn't match with model " + modelTTUID; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/dirigera.device.status.ttuid-mismatch" + " [\"" + thing.getThingTypeUID() + "\",\"" + + modelTTUID + "\"]"); + } + return false; + } + return true; + } + + private Map initializeCache(Map mapping) { + final Map stateMap = new HashMap<>(); + mapping.forEach((key, value) -> { + stateMap.put(key, UnDefType.UNDEF); + }); + return stateMap; + } + + /** + * Evaluates if this device is a controller or sensor + * + * @return boolean + */ + protected boolean isControllerOrSensor() { + return deviceType.toLowerCase().contains("sensor") || deviceType.toLowerCase().contains("controller"); + } + + /** + * Handling of links + */ + + /** + * Update cycle of gateway is done + */ + public void updateLinksStart() { + softLinks.clear(); + } + + /** + * Get real links from device updates. Delivers a copy due to concurrent access. + * + * @return links attached to this device + */ + public List getLinks() { + return new ArrayList(hardLinks); + } + + private void linkUpdate(String linkedDeviceId, boolean add) { + /** + * link has to be set to target device like light or outlet, not to the device which triggers an action like + * lightController or motionSensor + */ + String targetDevice = ""; + String triggerDevice = ""; + List linksToSend = new ArrayList<>(); + if (isControllerOrSensor()) { + // request needs to be sent to target device + targetDevice = linkedDeviceId; + triggerDevice = config.id; + // get current links + JSONObject deviceData = gateway().model().getAllFor(targetDevice, PROPERTY_DEVICES); + if (deviceData.has(PROPERTY_REMOTE_LINKS)) { + JSONArray jsonLinks = deviceData.getJSONArray(PROPERTY_REMOTE_LINKS); + jsonLinks.forEach(link -> { + linksToSend.add(link.toString()); + }); + if (customDebug) { + logger.info("DIRIGERA BASE_HANDLER {} links for {} {}", thing.getLabel(), + gateway().model().getCustonNameFor(targetDevice), linksToSend); + } + // this is sensor branch so add link of sensor + if (add) { + if (!linksToSend.contains(triggerDevice)) { + linksToSend.add(triggerDevice); + } else { + if (customDebug) { + logger.info("DIRIGERA BASE_HANDLER {} already linked {}", thing.getLabel(), + gateway().model().getCustonNameFor(triggerDevice)); + } + } + } else { + if (linksToSend.contains(triggerDevice)) { + linksToSend.remove(triggerDevice); + } else { + if (customDebug) { + logger.info("DIRIGERA BASE_HANDLER {} no link to remove {}", thing.getLabel(), + gateway().model().getCustonNameFor(triggerDevice)); + } + } + } + } else { + if (customDebug) { + logger.info("DIRIGERA BASE_HANDLER {} has no remoteLinks", thing.getLabel()); + } + } + } else { + // send update to this device + targetDevice = config.id; + triggerDevice = linkedDeviceId; + if (add) { + hardLinks.add(triggerDevice); + } else { + hardLinks.remove(triggerDevice); + } + linksToSend.addAll(hardLinks); + } + JSONArray newLinks = new JSONArray(linksToSend); + JSONObject attributes = new JSONObject(); + attributes.put(PROPERTY_REMOTE_LINKS, newLinks); + gateway().api().sendPatch(targetDevice, attributes); + // after api command remoteLinks property will be updated and trigger new linkUpadte + } + + /** + * Adds a soft link towards the device which has the link stored in his attributes + * + * @param device id of the device which contains this link + */ + public void addSoftlink(String id) { + if (!softLinks.contains(id) && !config.id.equals(id)) { + softLinks.add(id); + } + } + + /** + * Update cycle of gateway is done + */ + public void updateLinksDone() { + if (hasLinksOrCandidates()) { + createChannelIfNecessary(CHANNEL_LINKS, CHANNEL_LINKS, CoreItemFactory.STRING); + createChannelIfNecessary(CHANNEL_LINK_CANDIDATES, CHANNEL_LINK_CANDIDATES, CoreItemFactory.STRING); + updateLinks(); + // The candidates needs to be evaluated by child class + // - blindController needs blinds and vice versa + // - soundCotroller needs speakers and vice versa + // - lightController needs light and outlet and vice versa + // So assure "linkCandidateTypes" are overwritten by child class with correct types + updateCandidateLinks(); + } + } + + protected void updateLinks() { + List display = new ArrayList<>(); + List linkCommandOptions = new ArrayList<>(); + List allLinks = new ArrayList<>(); + allLinks.addAll(hardLinks); + allLinks.addAll(softLinks); + Collections.sort(allLinks); + allLinks.forEach(link -> { + String customName = gateway().model().getCustonNameFor(link); + if (!gateway().isKnownDevice(link)) { + // if device isn't present in OH attach this suffix + customName += " (!)"; + } + display.add(customName); + linkCommandOptions.add(new CommandOption(link, customName)); + }); + ChannelUID channelUUID = new ChannelUID(thing.getUID(), CHANNEL_LINKS); + gateway().getCommandProvider().setCommandOptions(channelUUID, linkCommandOptions); + logger.trace("DIRIGERA BASE_HANDLER {} links {}", thing.getLabel(), display); + updateState(channelUUID, StringType.valueOf(display.toString())); + } + + protected void updateCandidateLinks() { + List possibleCandidates = gateway().model().getDevicesForTypes(linkCandidateTypes); + List candidates = new ArrayList<>(); + possibleCandidates.forEach(entry -> { + if (!hardLinks.contains(entry) && !softLinks.contains(entry)) { + candidates.add(entry); + } + }); + + List display = new ArrayList<>(); + List candidateOptions = new ArrayList<>(); + Collections.sort(candidates); + candidates.forEach(candidate -> { + String customName = gateway().model().getCustonNameFor(candidate); + if (!gateway().isKnownDevice(candidate)) { + // if device isn't present in OH attach this suffix + customName += " (!)"; + } + display.add(customName); + candidateOptions.add(new CommandOption(candidate, customName)); + }); + ChannelUID channelUUID = new ChannelUID(thing.getUID(), CHANNEL_LINK_CANDIDATES); + gateway().getCommandProvider().setCommandOptions(channelUUID, candidateOptions); + updateState(channelUUID, StringType.valueOf(display.toString())); + } + + /** + * Check is any outgoing or incoming links or candidates are available + * + * @return true if one of the above conditions is true + */ + private boolean hasLinksOrCandidates() { + return (!hardLinks.isEmpty() || !softLinks.isEmpty() + || !gateway().model().getDevicesForTypes(linkCandidateTypes).isEmpty()); + } + + public void addPowerListener(PowerListener listener) { + synchronized (powerListeners) { + powerListeners.add(listener); + } + } + + public void removePowerListener(PowerListener listener) { + synchronized (powerListeners) { + powerListeners.remove(listener); + } + } + + /** + * Debug commands for console access + */ + + @Override + public String getJSON() { + if (THING_TYPE_SCENE.equals(thing.getThingTypeUID())) { + return gateway().api().readScene(config.id).toString(); + } else { + return gateway().api().readDevice(config.id).toString(); + } + } + + @Override + public String getToken() { + return gateway().getToken(); + } + + @Override + public void setDebug(boolean debug, boolean all) { + if (all) { + ((DebugHandler) gateway()).setDebug(debug, all); + } else { + customDebug = debug; + } + } + + @Override + public String getDeviceId() { + return config.id; + } + + /** + * for unit testing + */ + @Override + public @Nullable ThingHandlerCallback getCallback() { + return super.getCallback(); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/DeviceUpdate.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/DeviceUpdate.java new file mode 100644 index 00000000000..f92e978d0d6 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/DeviceUpdate.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link DeviceUpdate} element handled in device update queue + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class DeviceUpdate { + public enum Action { + ADD, + DISPOSE, + REMOVE, + LINKS; + } + + public @Nullable BaseHandler handler; + public String deviceId; + public Action action; + + public DeviceUpdate(@Nullable BaseHandler handler, String deviceId, Action action) { + this.handler = handler; + this.deviceId = deviceId; + this.action = action; + } + + /** + * Link updates are equal because they are generic, all others false + * + * @param other + * @return + */ + @Override + public boolean equals(@Nullable Object other) { + boolean result = false; + if (other instanceof DeviceUpdate otherDeviceUpdate) { + result = this.action.equals(otherDeviceUpdate.action) && this.deviceId.equals(otherDeviceUpdate.deviceId); + BaseHandler thisProxyHandler = this.handler; + BaseHandler otherProxyHandler = otherDeviceUpdate.handler; + if (result && thisProxyHandler != null && otherProxyHandler != null) { + result = thisProxyHandler.equals(otherProxyHandler); + } + } + return result; + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/DirigeraHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/DirigeraHandler.java new file mode 100644 index 00000000000..5ae0bea082b --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/DirigeraHandler.java @@ -0,0 +1,1052 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +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.HttpHeader; +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.UrlEncoded; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.DirigeraCommandProvider; +import org.openhab.binding.dirigera.internal.config.DirigeraConfiguration; +import org.openhab.binding.dirigera.internal.discovery.DirigeraDiscoveryService; +import org.openhab.binding.dirigera.internal.exception.ApiException; +import org.openhab.binding.dirigera.internal.exception.ModelException; +import org.openhab.binding.dirigera.internal.interfaces.DebugHandler; +import org.openhab.binding.dirigera.internal.interfaces.DirigeraAPI; +import org.openhab.binding.dirigera.internal.interfaces.Gateway; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.binding.dirigera.internal.model.DirigeraModel; +import org.openhab.binding.dirigera.internal.network.DirigeraAPIImpl; +import org.openhab.binding.dirigera.internal.network.Websocket; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.i18n.LocationProvider; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PointType; +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.storage.Storage; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.CommandOption; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.openhab.core.util.StringUtils; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link DirigeraHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class DirigeraHandler extends BaseBridgeHandler implements Gateway, DebugHandler { + + private final Logger logger = LoggerFactory.getLogger(DirigeraHandler.class); + + // Can be overwritten by Unit test for mocking API + protected Map channelStateMap = new HashMap<>(); + protected Class apiProvider = DirigeraAPIImpl.class; + + private final Map deviceTree = new HashMap<>(); + private final DirigeraDiscoveryService discoveryService; + private final DirigeraCommandProvider commandProvider; + private final BundleContext bundleContext; + private final Websocket websocket; + + private Optional api = Optional.empty(); + private Optional model = Optional.empty(); + private Optional> watchdog = Optional.empty(); + private Optional> updater = Optional.empty(); + private Optional> detectionSchedule = Optional.empty(); + + // private ScheduledExecutorService sequentialScheduler; + private List knownDevices = new ArrayList<>(); + private ArrayList websocketQueue = new ArrayList<>(); + private ArrayList deviceModificationQueue = new ArrayList<>(); + private DirigeraConfiguration config; + private Storage storage; + private HttpClient httpClient; + private String token = PROPERTY_EMPTY; + private Instant sunriseInstant = Instant.MAX; + private Instant sunsetInstant = Instant.MIN; + private Instant peakRecognitionTime = Instant.MIN; + private int websocketQueueSizePeak = 0; + private int deviceUpdateQueueSizePeak = 0; + private boolean updateRunning = false; + private boolean customDebug = false; + + public static final DeviceUpdate LINK_UPDATE = new DeviceUpdate(null, "", DeviceUpdate.Action.LINKS); + public static long detectionTimeSeonds = 5; + + public DirigeraHandler(Bridge bridge, HttpClient insecureClient, Storage bindingStorage, + DirigeraDiscoveryService discoveryManager, LocationProvider locationProvider, + DirigeraCommandProvider commandProvider, BundleContext bundleContext) { + super(bridge); + this.discoveryService = discoveryManager; + this.httpClient = insecureClient; + this.storage = bindingStorage; + this.commandProvider = commandProvider; + this.bundleContext = bundleContext; + config = new DirigeraConfiguration(); + websocket = new Websocket(this, insecureClient); + + List locationOptions = new ArrayList<>(); + locationOptions.add(new CommandOption("", "Remove location")); + PointType location = locationProvider.getLocation(); + if (location != null) { + locationOptions.add(new CommandOption(location.toFullString(), "Home location")); + } + commandProvider.setCommandOptions(new ChannelUID(thing.getUID(), CHANNEL_LOCATION), locationOptions); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (customDebug) { + logger.info("DIRIGERA HANDLER command {} : {} {}", channelUID, command.toFullString(), command.getClass()); + } + String channel = channelUID.getIdWithoutGroup(); + if (command instanceof RefreshType) { + State cachedState = channelStateMap.get(channel); + if (cachedState != null) { + super.updateState(channelUID, cachedState); + } + } else if (CHANNEL_PAIRING.equals(channel)) { + JSONObject permissionAttributes = new JSONObject(); + permissionAttributes.put(PROPERTY_PERMIT_JOIN, OnOffType.ON.equals(command)); + api().sendAttributes(config.id, permissionAttributes); + } else if (CHANNEL_LOCATION.equals(channel)) { + PointType coordinatesPoint = null; + if (command instanceof PointType point) { + coordinatesPoint = point; + } else if (command instanceof StringType string) { + if (string.toFullString().isBlank()) { + String nullCoordinates = model().getTemplate(Model.TEMPLATE_NULL_COORDINATES); + JSONObject patchCoordinates = new JSONObject(nullCoordinates); + if (customDebug) { + logger.info("DIRIGERA HANDLER send null coordinates {}", patchCoordinates); + } + api().sendPatch(config.id, patchCoordinates); + } else { + try { + coordinatesPoint = new PointType(string.toFullString()); + } catch (IllegalArgumentException exception) { + logger.warn("DIRIGERA HANDLER wrong home location format {} : {}", string, + exception.getMessage()); + } + } + } + if (coordinatesPoint != null) { + String coordinatesTemplate = model().getTemplate(Model.TEMPLATE_COORDINATES); + String coordinates = String.format(coordinatesTemplate, coordinatesPoint.getLatitude().toFullString(), + coordinatesPoint.getLongitude().toFullString()); + if (customDebug) { + logger.info("DIRIGERA HANDLER send coordinates {}", coordinates); + } + api().sendPatch(config.id, new JSONObject(coordinates)); + } + } + } + + @Override + public void initialize() { + config = getConfigAs(DirigeraConfiguration.class); + if (config.ipAddress.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/dirigera.device.status.missing-ip"); + } else { + // do this asynchronous in case of token and other parameters needs to be + // obtained via Rest API calls + updateStatus(ThingStatus.UNKNOWN); + scheduler.execute(this::doInitialize); + } + } + + private void doInitialize() { + // for discovery known device are stored in storage in order not to report them + // again and again through DiscoveryService + getKnownDevicesFromStorage(); + token = getTokenFromStorage(); + // Step 1 - check if token is stored or pairing is needed + if (token.isBlank()) { + // if token isn't recovered from storage begin pairing process + logger.debug("DIRIGERA HANDLER no token in storage"); + String codeVerifier = generateCodeVerifier(); + String challenge = generateCodeChallenge(codeVerifier); + String code = getCode(challenge); + if (!code.isBlank()) { + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NOT_YET_READY, + "@text/dirigera.gateway.status.pairing-button"); + /** + * Approx 3 minutes possible to push DIRIGERA button + */ + Instant stopAuth = Instant.now().plusSeconds(180); + while (Instant.now().isBefore(stopAuth) && token.isBlank()) { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + Thread.interrupted(); + return; + } + token = getToken(code, codeVerifier); + if (!token.isBlank()) { + logger.debug("DIRIGERA HANDLER token {} received", token); + storeToken(token); + } + } + } + } else { + logger.debug("DIRIGERA HANDLER obtained token {} from storage", token); + } + + // Step 2 - if token is fine start initializing, else set status pairing retry + if (!token.isBlank()) { + // now create api and model + try { + DirigeraAPI apiProviderInstance = (DirigeraAPI) apiProvider + .getConstructor(HttpClient.class, Gateway.class).newInstance(httpClient, this); + if (apiProviderInstance != null) { + api = Optional.of(apiProviderInstance); + } else { + throw (new InstantiationException(apiProvider.descriptorString())); + } + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | NoSuchMethodException | SecurityException e) { + // this will not happen - DirirgeraAPIIMpl tested with mocks in unit tests + logger.error("Error {}", apiProvider.descriptorString()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "@text/dirigera.gateway.status.api-error" + " [\"" + apiProvider.descriptorString() + "\"]"); + return; + } + Model houseModel = new DirigeraModel(this); + model = Optional.of(houseModel); + modelUpdate(); // initialize model + websocket.initialize(); + // checks API access and starts websocket + // status will be set ONLINE / OFFLINE based on the connection result + connectGateway(); + // start watchdog to check gateway connection and start recovery if necessary + watchdog = Optional.of(scheduler.scheduleWithFixedDelay(this::watchdog, 15, 15, TimeUnit.SECONDS)); + // infrequent updates for gateway itself + updater = Optional.of(scheduler.scheduleWithFixedDelay(this::updateGateway, 1, 15, TimeUnit.MINUTES)); + } else { + // status "retry pairing" if token is still blank + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NOT_YET_READY, + "@text/dirigera.gateway.status.pairing-retry"); + } + } + + @Override + public void dispose() { + super.dispose(); + watchdog.ifPresent(watchdogSchedule -> { + watchdogSchedule.cancel(false); + }); + updater.ifPresent(refresher -> { + refresher.cancel(false); + }); + websocket.dispose(); + } + + @Override + public void handleRemoval() { + storage.remove(config.ipAddress); + super.handleRemoval(); + } + + private void updateProperties() { + Map propertiesMap = model().getPropertiesFor(config.id); + TreeMap currentProperties = new TreeMap<>(editProperties()); + propertiesMap.forEach((key, value) -> { + currentProperties.put(key, value.toString()); + }); + updateProperties(currentProperties); + } + + private String getTokenFromStorage() { + JSONObject gatewayStorageJson = getStorageJson(); + if (gatewayStorageJson.has(PROPERTY_TOKEN)) { + String token = gatewayStorageJson.getString(PROPERTY_TOKEN); + if (!token.isBlank()) { + return token; + } + } + return PROPERTY_EMPTY; + } + + private void storeToken(String token) { + if (!config.ipAddress.isBlank()) { + JSONObject tokenStore = new JSONObject(); + tokenStore.put(PROPERTY_TOKEN, token); + storage.put(config.ipAddress, tokenStore.toString()); + } + } + + private void getKnownDevicesFromStorage() { + JSONObject gatewayStorageJson = getStorageJson(); + if (gatewayStorageJson.has(PROPERTY_DEVICES)) { + String knownDeviceString = gatewayStorageJson.getString(PROPERTY_DEVICES); + JSONArray arr = new JSONArray(knownDeviceString); + knownDevices = arr.toList(); + } + } + + private void storeKnownDevices() { + JSONObject gatewayStorageJson = getStorageJson(); + JSONArray toStoreArray = new JSONArray(knownDevices); + gatewayStorageJson.put(PROPERTY_DEVICES, toStoreArray.toString()); + storage.put(config.ipAddress, gatewayStorageJson.toString()); + } + + private JSONObject getStorageJson() { + if (!config.ipAddress.isBlank()) { + String gatewayStorageObject = storage.get(config.ipAddress); + if (gatewayStorageObject != null) { + JSONObject gatewayStorageJson = new JSONObject(gatewayStorageObject.toString()); + return gatewayStorageJson; + } + } + return new JSONObject(); + } + + /** + * Everything to handle pairing process + */ + + private String getCode(String challenge) { + try { + MultiMap<@Nullable String> baseParams = new MultiMap<>(); + baseParams.put("audience", "homesmart.local"); + baseParams.put("response_type", "code"); + baseParams.put("code_challenge", challenge); + baseParams.put("code_challenge_method", "S256"); + + String url = String.format(OAUTH_URL, config.ipAddress); + Request codeRequest = httpClient.newRequest(url).param("audience", "homesmart.local") + .param("response_type", "code").param("code_challenge", challenge) + .param("code_challenge_method", "S256"); + + ContentResponse response = codeRequest.timeout(10, TimeUnit.SECONDS).send(); + int responseStatus = response.getStatus(); + if (responseStatus != 200) { + String reason = response.getReason(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/dirigera.gateway.status.comm-error" + " [\"" + responseStatus + " - " + reason + "\"]"); + return ""; + } + String responseString = response.getContentAsString(); + JSONObject codeResponse = new JSONObject(responseString); + String code = codeResponse.getString("code"); + return code; + } catch (InterruptedException | TimeoutException | ExecutionException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/dirigera.gateway.status.comm-error" + " [\"" + e.getMessage() + "\"]"); + return ""; + } + } + + private String getToken(String code, String codeVerifier) { + try { + MultiMap<@Nullable String> baseParams = new MultiMap<>(); + baseParams.put("code", code); + baseParams.put("name", "openHAB"); + baseParams.put("grant_type", "authorization_code"); + baseParams.put("code_verifier", codeVerifier); + + String url = String.format(TOKEN_URL, config.ipAddress); + String urlEncoded = UrlEncoded.encode(baseParams, StandardCharsets.UTF_8, false); + Request tokenRequest = httpClient.POST(url) + .header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded") + .content(new StringContentProvider("application/x-www-form-urlencoded", urlEncoded, + StandardCharsets.UTF_8)) + .followRedirects(true).timeout(10, TimeUnit.SECONDS); + + ContentResponse response = tokenRequest.send(); + logger.debug("DIRIGERA HANDLER token response {} : {}", response.getStatus(), + response.getContentAsString()); + int responseStatus = response.getStatus(); + if (responseStatus != 200) { + return ""; + } + String responseString = response.getContentAsString(); + JSONObject tokenResponse = new JSONObject(responseString); + String accessToken = tokenResponse.getString("access_token"); + return accessToken; + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("DIRIGERA HANDLER exception fetching token {}", e.getMessage()); + return ""; + } + } + + private String generateCodeChallenge(String codeVerifier) { + try { + MessageDigest digest = MessageDigest.getInstance("sha256"); + byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(hash); + } catch (NoSuchAlgorithmException e) { + logger.warn("DIRIGERA HANDLER error creating code challenge {}", e.getMessage()); + return ""; + } + } + + private String generateCodeVerifier() { + return StringUtils.getRandomAlphanumeric(128); + } + + /** + * Distributing the device update towards the correct function + */ + /** + * Distributing the device update towards the correct function + */ + private void doDeviceUpdate() { + DeviceUpdate deviceUpdate = null; + synchronized (deviceModificationQueue) { + if (deviceUpdateQueueSizePeak < deviceModificationQueue.size()) { + deviceUpdateQueueSizePeak = deviceModificationQueue.size(); + logger.trace("DIRIGERA HANDLER Peak device updates {}", deviceUpdateQueueSizePeak); + peakRecognitionTime = Instant.now(); + } + if (!deviceModificationQueue.isEmpty()) { + deviceUpdate = deviceModificationQueue.remove(0); + /** + * check if other link update is still in queue. If yes dismiss this request and + * wait for latest one + */ + if (deviceUpdate.action.equals(DeviceUpdate.Action.LINKS) + && deviceModificationQueue.contains(deviceUpdate)) { + deviceUpdate = null; + logger.warn("DIRIGERA HANDLER Dismiss link update, there's a later one scheduled"); + } + } + } + if (deviceUpdate != null) { + BaseHandler handler = deviceUpdate.handler; + try { + switch (deviceUpdate.action) { + case ADD: + if (handler != null) { + startUpdate(); + doRegisterDevice(handler, deviceUpdate.deviceId); + } + break; + case DISPOSE: + if (handler != null) { + startUpdate(); + doUnregisterDevice(handler, deviceUpdate.deviceId); + } + break; + case REMOVE: + if (handler != null) { + startUpdate(); + doDeleteDevice(handler, deviceUpdate.deviceId); + } + break; + case LINKS: + startUpdate(); + doUpdateLinks(); + break; + } + } finally { + endUpdate(); + } + } + synchronized (deviceModificationQueue) { + if (deviceModificationQueue.isEmpty() && !Instant.MIN.equals(peakRecognitionTime)) { + logger.trace("DIRIGERA HANDLER Peak to zero time {} ms", + Duration.between(peakRecognitionTime, Instant.now()).toMillis()); + peakRecognitionTime = Instant.MIN; + } + } + } + + private void startUpdate() { + synchronized (this) { + while (updateRunning) { + try { + this.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + updateRunning = true; + } + } + + private void endUpdate() { + synchronized (this) { + updateRunning = false; + this.notifyAll(); + } + } + + @Override + public void registerDevice(BaseHandler deviceHandler, String deviceId) { + synchronized (deviceModificationQueue) { + deviceModificationQueue.add(new DeviceUpdate(deviceHandler, deviceId, DeviceUpdate.Action.ADD)); + } + scheduler.execute(this::doDeviceUpdate); + } + + /** + * register a running device + */ + private void doRegisterDevice(BaseHandler deviceHandler, String deviceId) { + if (!deviceId.isBlank()) { + // if id isn't known yet - store it + if (!knownDevices.contains(deviceId)) { + knownDevices.add(deviceId); + storeKnownDevices(); + } + } + deviceTree.put(deviceId, deviceHandler); + } + + @Override + public void unregisterDevice(BaseHandler deviceHandler, String deviceId) { + synchronized (deviceModificationQueue) { + deviceModificationQueue.add(new DeviceUpdate(deviceHandler, deviceId, DeviceUpdate.Action.DISPOSE)); + } + scheduler.execute(this::doDeviceUpdate); + } + + /** + * unregister device, not running but still available + */ + private void doUnregisterDevice(BaseHandler deviceHandler, String deviceId) { + // unregister from dispose but don't remove it from known devices + deviceTree.remove(deviceId); + } + + @Override + public void deleteDevice(BaseHandler deviceHandler, String deviceId) { + synchronized (deviceModificationQueue) { + deviceModificationQueue.add(new DeviceUpdate(deviceHandler, deviceId, DeviceUpdate.Action.REMOVE)); + } + scheduler.execute(this::doDeviceUpdate); + } + + /** + * Called by all device on handleRe + */ + private void doDeleteDevice(BaseHandler deviceHandler, String deviceId) { + deviceTree.remove(deviceId); + // removal of handler - store known devices + knownDevices.remove(deviceId); + storeKnownDevices(); + // before new detection the handler needs to be removed - now were in removing + // state + // for complex devices several removes are done so don't trigger detection every + // time + detectionSchedule.ifPresentOrElse(previousSchedule -> { + if (!previousSchedule.isDone()) { + previousSchedule.cancel(true); + } + detectionSchedule = Optional + .of(scheduler.schedule(model.get()::detection, detectionTimeSeonds, TimeUnit.SECONDS)); + }, () -> { + detectionSchedule = Optional + .of(scheduler.schedule(model.get()::detection, detectionTimeSeonds, TimeUnit.SECONDS)); + }); + } + + /** + * Interface to Model called if device isn't found anymore + */ + @Override + public void deleteDevice(String deviceId) { + BaseHandler activeHandler = deviceTree.remove(deviceId); + if (activeHandler != null) { + // if a handler is attached the check will fail and update the status to GONE + activeHandler.checkHandler(); + } + // removal of handler - store new known devices + if (knownDevices.contains(deviceId)) { + knownDevices.remove(deviceId); + storeKnownDevices(); + } + } + + @Override + public DirigeraAPI api() throws ApiException { + if (api.isEmpty()) { + throw new ApiException("No API available yet"); + } + return api.get(); + } + + @Override + public Model model() throws ModelException { + if (model.isEmpty()) { + throw new ModelException("No Model available yet"); + } + return model.get(); + } + + @Override + public DirigeraDiscoveryService discovery() { + return discoveryService; + } + + @Override + public BundleContext getBundleContext() { + return bundleContext; + } + + @Override + public boolean isKnownDevice(String id) { + return knownDevices.contains(id); + } + + @Override + public DirigeraCommandProvider getCommandProvider() { + return commandProvider; + } + + @Override + public String getIpAddress() { + return config.ipAddress; + } + + @Override + public boolean discoveryEnabled() { + return config.discovery; + } + + @Override + public void updateLinks() { + synchronized (deviceModificationQueue) { + deviceModificationQueue.add(LINK_UPDATE); + } + scheduler.execute(this::doDeviceUpdate); + } + + private void doUpdateLinks() { + // first clear start update cycle, softlinks are cleared before + synchronized (deviceTree) { + deviceTree.forEach((id, handler) -> { + handler.updateLinksStart(); + }); + // then update all links + deviceTree.forEach((id, handler) -> { + List links = handler.getLinks(); + if (!links.isEmpty()) { + if (customDebug) { + logger.info("DIRIGERA HANDLER links found for {} {}", handler.getThing().getLabel(), + links.size()); + } + } + links.forEach(link -> { + // assure investigated handler is different from target handler + if (!id.equals(link)) { + BaseHandler targetHandler = deviceTree.get(link); + if (targetHandler != null) { + targetHandler.addSoftlink(id); + } else { + if (customDebug) { + logger.info("DIRIGERA HANDLER no targethandler found to link {} to {}", id, link); + } + } + } + }); + }); + // finish update cycle so handler can update states + deviceTree.forEach((id, handler) -> { + handler.updateLinksDone(); + }); + } + } + + private void watchdog() { + // check updater is still active - maybe an exception caused termination + updater.ifPresentOrElse(refresher -> { + if (refresher.isDone()) { + updater = Optional.of(scheduler.scheduleWithFixedDelay(this::updateGateway, 1, 15, TimeUnit.MINUTES)); + } + }, () -> { + updater = Optional.of(scheduler.scheduleWithFixedDelay(this::updateGateway, 1, 15, TimeUnit.MINUTES)); + }); + // check websocket + if (websocket.isRunning()) { + Map pingPongMap = websocket.getPingPongMap(); + if (pingPongMap.size() > 1) { // at least 2 shall be missing before watchdog trigger + logger.debug("DIRIGERA HANDLER Watchdog Ping Pong Panic - {} pings not answered", pingPongMap.size()); + websocket.stop(); + String message = "ping not answered"; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/dirigera.gateway.status.comm-error" + " [\"" + message + "\"]"); + scheduler.execute(this::connectGateway); + } else { + // good case - ping socket and check in next call for answers + websocket.ping(); + } + } else { + String message = "try to recover"; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/dirigera.gateway.status.comm-error" + " [\"" + message + "\"]"); + scheduler.execute(this::connectGateway); + } + } + + /** + * Establish connections + * 1) Check API response + * 2) Start websocket and wait for positive answer + * + * @see websocketConnected + */ + private void connectGateway() { + JSONObject gatewayInfo = api().readDevice(config.id); + // check if API call was successful, otherwise starting websocket doesn't make + // sense + if (!gatewayInfo.has(DirigeraAPI.HTTP_ERROR_FLAG)) { + if (!websocket.isRunning()) { + if (customDebug) { + logger.info("DIRIGERA HANDLER WS restart necessary"); + } + websocket.start(); + // onConnect shall switch to ONLINE! + } // else websocket is running fine + } else { + String message = gatewayInfo.getInt(DirigeraAPI.HTTP_ERROR_STATUS) + " - " + + gatewayInfo.getString(DirigeraAPI.HTTP_ERROR_MESSAGE); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/dirigera.gateway.status.comm-error" + " [\"" + message + "\"]"); + } + } + + private void updateGateway() { + JSONObject gatewayInfo = api().readDevice(config.id); + if (!gatewayInfo.has(DirigeraAPI.HTTP_ERROR_FLAG)) { + handleUpdate(gatewayInfo); + } else { + String message = gatewayInfo.getInt(DirigeraAPI.HTTP_ERROR_STATUS) + " - " + + gatewayInfo.getString(DirigeraAPI.HTTP_ERROR_MESSAGE); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/dirigera.gateway.status.comm-error" + " [\"" + message + "\"]"); + } + } + + private void configureGateway() { + if (config.id.isBlank()) { + List gatewayList = model().getDevicesForTypes(List.of(DEVICE_TYPE_GATEWAY)); + if (gatewayList.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/dirigera.gateway.status.no-gateway"); + } else if (gatewayList.size() > 1) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, + "@text/dirigera.gateway.status.ambiguous-gateway"); + } else { + String id = gatewayList.get(0); + Configuration configUpdate = editConfiguration(); + configUpdate.put(PROPERTY_DEVICE_ID, id); + updateConfiguration(configUpdate); + // get fresh config after update + config = getConfigAs(DirigeraConfiguration.class); + updateProperties(); + } + } + } + + @Override + public void websocketConnected(boolean connected, String reason) { + if (connected) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/dirigera.gateway.status.comm-error" + " [\"" + reason + "\"]"); + } + } + + @Override + public void websocketUpdate(String update) { + synchronized (websocketQueue) { + websocketQueue.add(update); + } + scheduler.execute(this::doUpdate); + } + + private void doUpdate() { + String json = ""; + synchronized (websocketQueue) { + if (websocketQueueSizePeak < websocketQueue.size()) { + websocketQueueSizePeak = websocketQueue.size(); + logger.trace("DIRIGERA HANDLER Websocket update queue size peak {}", websocketQueueSizePeak); + } + if (!websocketQueue.isEmpty()) { + json = websocketQueue.remove(0); + } + } + if (!json.isBlank()) { + JSONObject update; + JSONObject data; + String targetId; + try { + update = new JSONObject(json); + data = update.getJSONObject("data"); + targetId = data.getString("id"); + } catch (JSONException exception) { + logger.debug("DIRIGERA HANDLER cannot decode update {} {}", exception.getMessage(), json); + return; + } + + String type = update.getString(PROPERTY_TYPE); + switch (type) { + case EVENT_TYPE_SCENE_CREATED: + case EVENT_TYPE_SCENE_DELETED: + if (data.has(PROPERTY_TYPE)) { + String dataType = data.getString(PROPERTY_TYPE); + if (dataType.equals(TYPE_CUSTOM_SCENE)) { + // don't handle custom scenes so break + break; + } + } + case EVENT_TYPE_DEVICE_ADDED: + case EVENT_TYPE_DEVICE_REMOVED: + // update model - it will take control on newly added, changed and removed + // devices + if (customDebug) { + logger.info("DIRIGERA HANDLER device added / removed {}", json); + } + modelUpdate(); + break; + case EVENT_TYPE_DEVICE_CHANGE: + case EVENT_TYPE_SCENE_UPDATE: + if (targetId != null) { + if (targetId.equals(config.id)) { + this.handleUpdate(data); + } else { + BaseHandler targetHandler = deviceTree.get(targetId); + if (targetHandler != null) { + targetHandler.handleUpdate(data); + } else { + // special case: if custom name is changed in attributes force model update + // in order to present the updated name in discovery + if (data.has(PROPERTY_ATTRIBUTES)) { + JSONObject attributes = data.getJSONObject(PROPERTY_ATTRIBUTES); + if (attributes.has(Model.CUSTOM_NAME)) { + if (customDebug) { + logger.info("DIRIGERA HANDLER possible name change detected {}", + attributes.getString(Model.CUSTOM_NAME)); + } + modelUpdate(); + } + } + } + } + } + break; + default: + logger.debug("DIRIGERA HANDLER unkown type {} for websocket update {}", type, update); + } + } + } + + /** + * Called in 3 different situations + * 1) initialize + * 2) adding / removing devices or scenes + * 3) renaming devices or scenes + */ + private void modelUpdate() { + Instant modelUpdateStartTime = Instant.now(); + int status = model().update(); + if (status != 200) { + logger.warn("DIRIGERA HANDLER Model update failed {}", status); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/dirigera.gateway.status.comm-error" + " [\"" + status + "\"]"); + return; + } + long durationUpdateTime = Duration.between(modelUpdateStartTime, Instant.now()).toMillis(); + websocket.increase(Websocket.MODEL_UPDATES); + websocket.getStatistics().put(Websocket.MODEL_UPDATE_TIME, durationUpdateTime + " ms"); + websocket.getStatistics().put(Websocket.MODEL_UPDATE_LAST, Instant.now()); + // updateState(new ChannelUID(thing.getUID(), CHANNEL_JSON), + // StringType.valueOf(model().getModelString())); + configureGateway(); + updateGateway(); + } + + private void handleUpdate(JSONObject data) { + // websocket statistics for each update + updateState(new ChannelUID(thing.getUID(), CHANNEL_STATISTICS), + StringType.valueOf(websocket.getStatistics().toString())); + + if (data.has(Model.ATTRIBUTES)) { + JSONObject attributes = data.getJSONObject(Model.ATTRIBUTES); + // check ota for each device + if (attributes.has(PROPERTY_CUSTOM_NAME)) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_CUSTOM_NAME), + StringType.valueOf(attributes.getString(PROPERTY_CUSTOM_NAME))); + } + if (attributes.has(PROPERTY_PERMIT_JOIN)) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_PAIRING), + OnOffType.from(attributes.getBoolean(PROPERTY_PERMIT_JOIN))); + } + if (!attributes.isNull("coordinates")) { + JSONObject coordinates = attributes.getJSONObject("coordinates"); + if (coordinates.has("latitude") && coordinates.has("longitude")) { + PointType homeLocation = new PointType( + coordinates.getDouble("latitude") + "," + coordinates.getDouble("longitude")); + updateState(new ChannelUID(thing.getUID(), CHANNEL_LOCATION), homeLocation); + } else { + updateState(new ChannelUID(thing.getUID(), CHANNEL_LOCATION), UnDefType.UNDEF); + } + } else { + updateState(new ChannelUID(thing.getUID(), CHANNEL_LOCATION), UnDefType.UNDEF); + } + if (attributes.has(PROPERTY_OTA_STATUS)) { + String otaStatusString = attributes.getString(PROPERTY_OTA_STATUS); + Integer otaStatus = OTA_STATUS_MAP.get(otaStatusString); + if (otaStatus != null) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), new DecimalType(otaStatus)); + } else { + logger.warn("DIRIGERA HANDLER {} Cannot decode ota status {}", thing.getLabel(), otaStatusString); + } + } + if (attributes.has(PROPERTY_OTA_STATE)) { + String otaStateString = attributes.getString(PROPERTY_OTA_STATE); + if (OTA_STATE_MAP.containsKey(otaStateString)) { + Integer otaState = OTA_STATE_MAP.get(otaStateString); + if (otaState != null) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), new DecimalType(otaState)); + // if ota state changes also update properties to keep firmware in thing + // properties up to date + updateProperties(); + } else { + logger.debug("DIRIGERA HANDLER {} Cannot decode ota state {}", thing.getLabel(), + otaStateString); + } + } else { + logger.debug("DIRIGERA HANDLER {} Cannot decode ota state {}", thing.getLabel(), otaStateString); + } + } + if (attributes.has(PROPERTY_OTA_PROGRESS)) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), + QuantityType.valueOf(attributes.getInt(PROPERTY_OTA_PROGRESS), Units.PERCENT)); + } + // sunrise & sunset + if (!attributes.isNull("nextSunRise")) { + String sunRiseString = attributes.getString("nextSunRise"); + if (sunRiseString != null) { + sunriseInstant = Instant.parse(sunRiseString); + updateState(new ChannelUID(thing.getUID(), CHANNEL_SUNRISE), new DateTimeType(sunriseInstant)); + } + } else { + updateState(new ChannelUID(thing.getUID(), CHANNEL_SUNRISE), UnDefType.UNDEF); + } + if (!attributes.isNull("nextSunSet")) { + String sunsetString = attributes.getString("nextSunSet"); + if (sunsetString != null) { + sunsetInstant = Instant.parse(attributes.getString("nextSunSet")); + updateState(new ChannelUID(thing.getUID(), CHANNEL_SUNSET), new DateTimeType(sunsetInstant)); + } + } else { + updateState(new ChannelUID(thing.getUID(), CHANNEL_SUNSET), UnDefType.UNDEF); + } + } + } + + @Override + public @Nullable Instant getSunriseDateTime() { + if (sunriseInstant.equals(Instant.MAX)) { + return null; + } + return sunriseInstant; + } + + @Override + public @Nullable Instant getSunsetDateTime() { + if (sunsetInstant.equals(Instant.MIN)) { + return null; + } + return sunsetInstant; + } + + /** + * Update cache for refresh, then update state + */ + + @Override + protected void updateState(ChannelUID channelUID, State state) { + channelStateMap.put(channelUID.getIdWithoutGroup(), state); + super.updateState(channelUID, state); + } + + /** + * Debug commands for console access + */ + + @Override + public String getJSON() { + String json = api().readHome().toString(); + return json; + } + + @Override + public String getToken() { + return token; + } + + @Override + public void setDebug(boolean debug, boolean all) { + customDebug = debug; + if (all) { + deviceTree.forEach((key, handler) -> { + handler.setDebug(debug, false); + }); + } + } + + @Override + public String getDeviceId() { + return config.id; + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/airpurifier/AirPurifierHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/airpurifier/AirPurifierHandler.java new file mode 100644 index 00000000000..5deb70524e7 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/airpurifier/AirPurifierHandler.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.airpurifier; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.Iterator; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.DecimalType; +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.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; + +/** + * The {@link AirPurifierHandler} for handling air cleaning devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class AirPurifierHandler extends BaseHandler { + + /** + * see + * https://github.com/dvdgeisler/DirigeraClient/blob/a760b4419a8b1adf469d14a6ce4e750e52d4d540/dirigera-client-api/src/main/java/de/dvdgeisler/iot/dirigera/client/api/model/device/airpurifier/AirPurifierFanMode.java#L5 + **/ + public static final Map FAN_MODES = Map.of("auto", 0, "low", 1, "medium", 2, "high", 3, "on", 4, + "off", 5); + /** + * see + * https://github.com/Leggin/dirigera/blob/790a3151d8b61151dcd31f2194297dc8d4d89640/src/dirigera/devices/air_purifier.py#L61 + **/ + public static final int FAN_SPEED_MAX = 50; + public static Map fanModeToState = reverseStateMapping(FAN_MODES); + + public AirPurifierHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + String channel = channelUID.getIdWithoutGroup(); + String targetProperty = channel2PropertyMap.get(channel); + if (targetProperty != null) { + switch (channel) { + case CHANNEL_CHILD_LOCK: + case CHANNEL_DISABLE_STATUS_LIGHT: + if (command instanceof OnOffType onOff) { + JSONObject onOffAttributes = new JSONObject(); + onOffAttributes.put(targetProperty, OnOffType.ON.equals(onOff)); + super.sendAttributes(onOffAttributes); + } + break; + case CHANNEL_PURIFIER_FAN_SPEED: + if (command instanceof PercentType percent) { + long speedAbs = Math.round(percent.intValue() * FAN_SPEED_MAX / 100.0); + JSONObject fanSpeedAttributes = new JSONObject(); + fanSpeedAttributes.put(targetProperty, speedAbs); + super.sendAttributes(fanSpeedAttributes); + } + break; + case CHANNEL_PURIFIER_FAN_MODE: + if (command instanceof DecimalType decimal) { + int fanMode = decimal.intValue(); + String fanModeAttribute = fanModeToState.get(fanMode); + if (fanModeAttribute != null) { + JSONObject fanModeAttributes = new JSONObject(); + fanModeAttributes.put(targetProperty, fanModeAttribute); + super.sendAttributes(fanModeAttributes); + } + } + break; + } + } + } + + @Override + public void handleUpdate(JSONObject update) { + super.handleUpdate(update); + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + switch (targetChannel) { + case CHANNEL_PURIFIER_FAN_MODE: + String fanMode = attributes.getString(key); + Integer fanModeNumber = FAN_MODES.get(fanMode); + if (fanModeNumber != null) { + updateState(new ChannelUID(thing.getUID(), targetChannel), + new DecimalType(fanModeNumber)); + } + break; + case CHANNEL_PURIFIER_FAN_SPEED: + float speed = attributes.getFloat(key); + speed = Math.max(Math.min(speed, FAN_SPEED_MAX), 0); + int percent = Math.round(speed * 100 / FAN_SPEED_MAX); + updateState(new ChannelUID(thing.getUID(), targetChannel), new PercentType(percent)); + break; + case CHANNEL_PURIFIER_FAN_RUNTIME: + case CHANNEL_PURIFIER_FILTER_LIFETIME: + updateState(new ChannelUID(thing.getUID(), targetChannel), + QuantityType.valueOf(attributes.getDouble(key), Units.MINUTE)); + break; + case CHANNEL_PURIFIER_FILTER_ELAPSED: + updateState(new ChannelUID(thing.getUID(), targetChannel), + QuantityType.valueOf(attributes.getDouble(key), Units.MINUTE)); + State lifeTimeState = channelStateMap.get(CHANNEL_PURIFIER_FILTER_LIFETIME); + if (lifeTimeState != null && lifeTimeState instanceof QuantityType) { + int elapsed = attributes.getInt(key); + int lifetime = ((QuantityType) lifeTimeState).intValue(); + updateState(new ChannelUID(thing.getUID(), CHANNEL_PURIFIER_FILTER_REMAIN), + QuantityType.valueOf(lifetime - elapsed, Units.MINUTE)); + } + break; + case CHANNEL_PARTICULATE_MATTER: + updateState(new ChannelUID(thing.getUID(), targetChannel), + QuantityType.valueOf(attributes.getDouble(key), Units.MICROGRAM_PER_CUBICMETRE)); + break; + case CHANNEL_PURIFIER_FILTER_ALARM: + case CHANNEL_CHILD_LOCK: + case CHANNEL_DISABLE_STATUS_LIGHT: + updateState(new ChannelUID(thing.getUID(), targetChannel), + OnOffType.from(attributes.getBoolean(key))); + } + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/blind/BlindHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/blind/BlindHandler.java new file mode 100644 index 00000000000..d1500304579 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/blind/BlindHandler.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.blind; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link BlindHandler} for Window / Door blinds + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class BlindHandler extends BaseHandler { + private final Logger logger = LoggerFactory.getLogger(BlindHandler.class); + public static final Map BLIND_STATES = Map.of("stopped", 0, "up", 1, "down", 2); + public static Map blindNumberToState = reverseStateMapping(BLIND_STATES); + + public BlindHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + // links of types which can be established towards this device + linkCandidateTypes = List.of(DEVICE_TYPE_BLIND_CONTROLLER); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + String channel = channelUID.getIdWithoutGroup(); + if (command instanceof RefreshType) { + super.handleCommand(channelUID, command); + } else { + String targetProperty = channel2PropertyMap.get(channel); + if (targetProperty != null) { + switch (channel) { + case CHANNEL_BLIND_STATE: + if (command instanceof DecimalType state) { + String commandAttribute = blindNumberToState.get(state.intValue()); + if (commandAttribute != null) { + JSONObject attributes = new JSONObject(); + attributes.put(targetProperty, commandAttribute); + super.sendAttributes(attributes); + } else { + logger.warn("DIRIGERA BLIND_DEVICE Blind state unknown {}", state.intValue()); + } + } + break; + case CHANNEL_BLIND_LEVEL: + if (command instanceof PercentType percent) { + JSONObject attributes = new JSONObject(); + attributes.put("blindsTargetLevel", percent.intValue()); + super.sendAttributes(attributes); + } + break; + } + } + } + } + + @Override + public void handleUpdate(JSONObject update) { + // handle reachable flag + super.handleUpdate(update); + // now device specific + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + switch (targetChannel) { + case CHANNEL_BLIND_STATE: + String blindState = attributes.getString(key); + Integer stateValue = BLIND_STATES.get(blindState); + if (stateValue != null) { + updateState(new ChannelUID(thing.getUID(), targetChannel), new DecimalType(stateValue)); + } else { + logger.warn("DIRIGERA BLIND_DEVICE Blind state unknown {}", blindState); + } + break; + case CHANNEL_BLIND_LEVEL: + updateState(new ChannelUID(thing.getUID(), targetChannel), + new PercentType(attributes.getInt(key))); + break; + } + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/BaseShortcutController.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/BaseShortcutController.java new file mode 100644 index 00000000000..a6b6dc4763a --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/BaseShortcutController.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.controller; + +import static org.openhab.binding.dirigera.internal.Constants.PROPERTY_DEVICE_ID; + +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONArray; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.core.storage.Storage; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link BaseShortcutController} for triggering scenes + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class BaseShortcutController extends BaseHandler { + private final Logger logger = LoggerFactory.getLogger(BaseShortcutController.class); + + private Storage storage; + public Map sceneMapping = new HashMap<>(); + private Map triggerTimes = new HashMap<>(); + + private static final String SINGLE_PRESS = "singlePress"; + private static final String DOUBLE_PRESS = "doublePress"; + private static final String LONG_PRESS = "longPress"; + private static final List CLICK_PATTERNS = List.of(SINGLE_PRESS, DOUBLE_PRESS, LONG_PRESS); + + public BaseShortcutController(Thing thing, Map mapping, Storage bindingStorage) { + super(thing, mapping); + super.setChildHandler(this); + this.storage = bindingStorage; + } + + public void initializeScenes(String deviceId, String channel) { + // check scenes + CLICK_PATTERNS.forEach(pattern -> { + String patternKey = deviceId + ":" + channel + ":" + pattern; + if (!sceneMapping.containsKey(patternKey)) { + String patternSceneId = storage.get(patternKey); + if (patternSceneId != null) { + sceneMapping.put(patternKey, patternSceneId); + } else { + String uuid = getUID(); + String createdUUID = gateway().api().createScene(uuid, pattern, deviceId); + if (uuid.equals(createdUUID)) { + storage.put(patternKey, createdUUID); + sceneMapping.put(patternKey, createdUUID); + } else { + logger.warn("DIRIGERA BASE_SHORTCUT_CONTROLLER scene create failed for {}", patternKey); + } + } + } + + // after all check if scene is created and register for updates + String sceneId = sceneMapping.get(patternKey); + if (sceneId != null) { + gateway().registerDevice(this, sceneId); + } + }); + } + + @Override + public void dispose() { + sceneMapping.forEach((key, value) -> { + BaseHandler proxy = child; + if (proxy != null) { + gateway().unregisterDevice(proxy, value); + } + }); + super.dispose(); + } + + @Override + public void handleRemoval() { + sceneMapping.forEach((key, value) -> { + // cleanup storage and hub + BaseHandler proxy = child; + if (proxy != null) { + gateway().deleteDevice(proxy, value); + } + gateway().api().deleteScene(value); + storage.remove(key); + }); + super.handleRemoval(); + } + + private String getUID() { + String uuid = UUID.randomUUID().toString(); + while (gateway().model().has(uuid)) { + uuid = UUID.randomUUID().toString(); + } + return uuid; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + } + + @Override + public void handleUpdate(JSONObject update) { + super.handleUpdate(update); + if (update.has(PROPERTY_DEVICE_ID) && update.has("triggers")) { + // first check if trigger happened + String sceneId = update.getString(PROPERTY_DEVICE_ID); + JSONArray triggers = update.getJSONArray("triggers"); + boolean triggered = false; + for (int i = 0; i < triggers.length(); i++) { + JSONObject triggerObject = triggers.getJSONObject(i); + if (triggerObject.has("triggeredAt")) { + String triggerTimeString = triggerObject.getString("triggeredAt"); + Instant triggerTime = Instant.parse(triggerTimeString); + Instant lastTriggered = triggerTimes.get(sceneId); + if (lastTriggered != null) { + if (triggerTime.isAfter(lastTriggered)) { + triggerTimes.put(sceneId, triggerTime); + triggered = true; + } + } else { + triggered = true; + triggerTimes.put(sceneId, triggerTime); + break; + } + } + } + // if triggered deliver + if (triggered) { + sceneMapping.forEach((key, value) -> { + if (sceneId.equals(value)) { + String[] channelPattern = key.split(":"); + String pattern = ""; + switch (channelPattern[2]) { + case SINGLE_PRESS: + pattern = "SHORT_PRESSED"; + break; + case DOUBLE_PRESS: + pattern = "DOUBLE_PRESSED"; + break; + case LONG_PRESS: + pattern = "LONG_PRESSED"; + break; + } + if (!pattern.isBlank()) { + triggerChannel(new ChannelUID(thing.getUID(), channelPattern[1]), pattern); + } + } + }); + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/BlindsControllerHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/BlindsControllerHandler.java new file mode 100644 index 00000000000..16017184432 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/BlindsControllerHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.controller; + +import static org.openhab.binding.dirigera.internal.Constants.DEVICE_TYPE_BLINDS; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * The {@link BlindsControllerHandler} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class BlindsControllerHandler extends BaseHandler { + + public BlindsControllerHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + // links of types which can be established towards this device + linkCandidateTypes = List.of(DEVICE_TYPE_BLINDS); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + } + + @Override + public void handleUpdate(JSONObject update) { + // handle reachable flag, no more special handling + super.handleUpdate(update); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/DoubleShortcutControllerHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/DoubleShortcutControllerHandler.java new file mode 100644 index 00000000000..496ec18588e --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/DoubleShortcutControllerHandler.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.controller; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.core.storage.Storage; +import org.openhab.core.thing.Thing; + +/** + * The {@link DoubleShortcutControllerHandler} for triggering scenes + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class DoubleShortcutControllerHandler extends BaseShortcutController { + public TreeMap relations = new TreeMap<>(); + + public DoubleShortcutControllerHandler(Thing thing, Map mapping, Storage bindingStorage) { + super(thing, mapping, bindingStorage); + super.setChildHandler(this); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + + // now register at gateway all device and scene ids + String relationId = gateway().model().getRelationId(config.id); + relations = gateway().model().getRelations(relationId); + Entry firstEntry = relations.firstEntry(); + String firstDeviceId = firstEntry.getKey(); + super.initializeScenes(firstDeviceId, CHANNEL_BUTTON_1); + gateway().registerDevice(this, firstDeviceId); + values = gateway().api().readDevice(firstDeviceId); + handleUpdate(values); + // double shortcut controller has 2 devices + Entry secondEntry = relations.higherEntry(firstEntry.getKey()); + String secondDeviceId = secondEntry.getKey(); + super.initializeScenes(secondDeviceId, CHANNEL_BUTTON_2); + gateway().registerDevice(this, secondDeviceId); + values = gateway().api().readDevice(secondDeviceId); + handleUpdate(values); + } + } + + @Override + public void dispose() { + // remove device mapping + relations.forEach((key, value) -> { + gateway().unregisterDevice(this, key); + }); + // super removes scene mapping + super.dispose(); + } + + @Override + public void handleRemoval() { + // delete device mapping + relations.forEach((key, value) -> { + gateway().deleteDevice(this, key); + }); + // super deletes scenes from model + super.handleRemoval(); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/LightControllerHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/LightControllerHandler.java new file mode 100644 index 00000000000..5df229ec550 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/LightControllerHandler.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.controller; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONArray; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * The {@link LightControllerHandler} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class LightControllerHandler extends BaseHandler { + + public LightControllerHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + // links of types which can be established towards this device + linkCandidateTypes = List.of(DEVICE_TYPE_LIGHT, DEVICE_TYPE_OUTLET); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + String targetChannel = channelUID.getIdWithoutGroup(); + switch (targetChannel) { + case CHANNEL_LIGHT_PRESET: + if (command instanceof StringType string) { + JSONArray presetValues = new JSONArray(); + // handle the standard presets from IKEA app, custom otherwise without consistency check + switch (string.toFullString()) { + case "Off": + // fine - array stays empty + break; + case "Warm": + presetValues = new JSONArray( + gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_WARM)); + break; + case "Slowdown": + presetValues = new JSONArray( + gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_SLOWDOWN)); + break; + case "Smooth": + presetValues = new JSONArray( + gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_SMOOTH)); + break; + case "Bright": + presetValues = new JSONArray( + gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_BRIGHT)); + break; + default: + presetValues = new JSONArray(string.toFullString()); + } + JSONObject preset = new JSONObject(); + preset.put("circadianPresets", presetValues); + super.sendAttributes(preset); + } + } + } + + @Override + public void handleUpdate(JSONObject update) { + super.handleUpdate(update); + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + switch (key) { + case "circadianPresets": + if (attributes.has("circadianPresets")) { + JSONArray lightPresets = attributes.getJSONArray("circadianPresets"); + updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_PRESET), + StringType.valueOf(lightPresets.toString())); + } + break; + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/ShortcutControllerHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/ShortcutControllerHandler.java new file mode 100644 index 00000000000..beead38a03a --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/ShortcutControllerHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.controller; + +import static org.openhab.binding.dirigera.internal.Constants.CHANNEL_BUTTON_1; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.core.storage.Storage; +import org.openhab.core.thing.Thing; + +/** + * The {@link ShortcutControllerHandler} for triggering scenes + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ShortcutControllerHandler extends BaseShortcutController { + + public ShortcutControllerHandler(Thing thing, Map mapping, Storage bindingStorage) { + super(thing, mapping, bindingStorage); + super.setChildHandler(this); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + super.initializeScenes(config.id, CHANNEL_BUTTON_1); + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/SoundControllerHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/SoundControllerHandler.java new file mode 100644 index 00000000000..58aca664921 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/controller/SoundControllerHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.controller; + +import static org.openhab.binding.dirigera.internal.Constants.DEVICE_TYPE_SPEAKER; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.core.thing.Thing; + +/** + * The {@link SoundControllerHandler} for controlling SYMFONSIK speakers + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SoundControllerHandler extends BaseHandler { + + public SoundControllerHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + // links of types which can be established towards this device + linkCandidateTypes = List.of(DEVICE_TYPE_SPEAKER); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/BaseLight.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/BaseLight.java new file mode 100644 index 00000000000..74561ae8bf9 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/BaseLight.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.light; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.config.ColorLightConfiguration; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.binding.dirigera.internal.interfaces.PowerListener; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link BaseLight} for handling light commands in a controlled way + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class BaseLight extends BaseHandler implements PowerListener { + private final Logger logger = LoggerFactory.getLogger(BaseLight.class); + + protected ColorLightConfiguration lightConfig = new ColorLightConfiguration(); + protected Map lastUserMode = new HashMap<>(); + + private List lightRequestQueue = new ArrayList<>(); + private Instant readyForNextCommand = Instant.now(); + private JSONObject placeHolder = new JSONObject(); + private boolean executingCommand = false; + + public BaseLight(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + // links of types which can be established towards this device + linkCandidateTypes = List.of(DEVICE_TYPE_LIGHT_CONTROLLER, DEVICE_TYPE_MOTION_SENSOR); + } + + @Override + public void initialize() { + super.initialize(); + lightConfig = getConfigAs(ColorLightConfiguration.class); + super.addPowerListener(this); + } + + @Override + public void dispose() { + super.removePowerListener(this); + super.dispose(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + String channel = channelUID.getIdWithoutGroup(); + if (CHANNEL_POWER_STATE.equals(channel) && (command instanceof OnOffType onOff)) { + // route power state into queue instead of direct switch on / off + addOnOffCommand(OnOffType.ON.equals(onOff)); + } else { + super.handleCommand(channelUID, command); + } + } + + protected void addOnOffCommand(boolean on) { + LightCommand command; + if (on) { + command = new LightCommand(placeHolder, LightCommand.Action.ON); + } else { + command = new LightCommand(placeHolder, LightCommand.Action.OFF); + } + synchronized (lightRequestQueue) { + lightRequestQueue.add(command); + if (customDebug) { + logger.info("DIRIGERA BASE_LIGHT {} add command {}", thing.getLabel(), command.toString()); + } + } + scheduler.execute(this::executeCommand); + } + + protected void addCommand(@Nullable LightCommand command) { + if (command == null) { + return; + } + synchronized (lightRequestQueue) { + lightRequestQueue.add(command); + if (customDebug) { + logger.info("DIRIGERA BASE_LIGHT {} add command {}", thing.getLabel(), command.toString()); + } + } + scheduler.execute(this::executeCommand); + } + + /** + * execute commands in the order and delays of the lightRequestQueue + */ + protected void executeCommand() { + LightCommand request = null; + synchronized (lightRequestQueue) { + if (lightRequestQueue.isEmpty()) { + return; + } + + // wait for next time window and previous command is fully executed + while (readyForNextCommand.isAfter(Instant.now()) || executingCommand) { + try { + lightRequestQueue.wait(50); + } catch (InterruptedException e) { + lightRequestQueue.clear(); + Thread.interrupted(); + return; + } + } + + /* + * get command from queue and check if it needs to be executed + * if several requests of the same kind e.g. 5 brightness requests are in only the last one shall be + * executed + */ + if (!lightRequestQueue.isEmpty()) { + request = lightRequestQueue.remove(0); + } else { + lightRequestQueue.notifyAll(); + return; + } + if (lightRequestQueue.contains(request)) { + lightRequestQueue.notifyAll(); + return; + } + // now execute command + executingCommand = true; + } + if (customDebug) { + logger.info("DIRIGERA BASE_LIGHT {} execute {}", thing.getLabel(), request); + } + int addonMillis = 0; + switch (request.action) { + case ON: + super.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), OnOffType.ON); + addonMillis = lightConfig.fadeTime; + break; + case OFF: + super.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), OnOffType.OFF); + break; + case BRIGHTNESS: + case TEMPERATURE: + case COLOR: + super.sendAttributes(request.request); + if (isPowered()) { + addonMillis = lightConfig.fadeTime; + } + break; + } + // after command is sent to API add the time + readyForNextCommand = Instant.now().plus(addonMillis, ChronoUnit.MILLIS); + synchronized (lightRequestQueue) { + executingCommand = false; + lightRequestQueue.notifyAll(); + } + } + + protected void changeProperty(LightCommand.Action action, JSONObject request) { + LightCommand requestedCommand = new LightCommand(request, action); + if (isPowered()) { + addCommand(requestedCommand); + } else { + lastUserMode.put(action, requestedCommand); + switch (action) { + case COLOR: + addCommand(requestedCommand); + lastUserMode.remove(LightCommand.Action.TEMPERATURE); + break; + case TEMPERATURE: + addCommand(requestedCommand); + lastUserMode.remove(LightCommand.Action.COLOR); + break; + case BRIGHTNESS: + case ON: + case OFF: + default: + break; + } + logger.trace("DIRIGERA BASE_LIGHT {} last user mode settings {}", thing.getLabel(), lastUserMode); + } + } + + @Override + public void powerChanged(OnOffType power, boolean requested) { + // apply lum settings according to configuration in the right sequence if power changed to ON + if (OnOffType.ON.equals(power)) { + if (!requested) { + addOnOffCommand(true); + } + if (customDebug) { + logger.info("DIRIGERA BASE_LIGHT {} last user mode restore {}", thing.getLabel(), lastUserMode); + } + LightCommand brightnessCommand = lastUserMode.remove(LightCommand.Action.BRIGHTNESS); + LightCommand colorCommand = lastUserMode.remove(LightCommand.Action.COLOR); + LightCommand temperatureCommand = lastUserMode.remove(LightCommand.Action.TEMPERATURE); + switch (lightConfig.fadeSequence) { + case 0: + addCommand(brightnessCommand); + addCommand(colorCommand); + addCommand(temperatureCommand); + break; + case 1: + addCommand(colorCommand); + addCommand(temperatureCommand); + addCommand(brightnessCommand); + break; + } + } else { + // assure settings are clean for next startup + lastUserMode.clear(); + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/ColorLightHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/ColorLightHandler.java new file mode 100644 index 00000000000..ff739204aa1 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/ColorLightHandler.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.light; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.Iterator; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.DirigeraStateDescriptionProvider; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.binding.dirigera.internal.model.ColorModel; +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.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link ColorLightHandler} for lights with hue, saturation and brightness + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ColorLightHandler extends TemperatureLightHandler { + private final Logger logger = LoggerFactory.getLogger(ColorLightHandler.class); + + private HSBType hsbStateReflection = new HSBType(); // proxy to reflect state to end user + private HSBType hsbDevice = new HSBType(); // strictly holding values which were received via update + private String colorMode = ""; + + public ColorLightHandler(Thing thing, Map mapping, DirigeraStateDescriptionProvider stateProvider) { + super(thing, mapping, stateProvider); + super.setChildHandler(this); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void dispose() { + super.dispose(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + String channel = channelUID.getIdWithoutGroup(); + if (CHANNEL_LIGHT_COLOR.equals(channel)) { + if (command instanceof HSBType hsb) { + // respect sequence + switch (lightConfig.fadeSequence) { + case 0: + brightnessCommand(hsb); + colorCommand(hsb); + break; + case 1: + colorCommand(hsb); + brightnessCommand(hsb); + break; + } + hsbStateReflection = hsb; + updateState(channelUID, hsb); + } else if (command instanceof OnOffType) { + super.addOnOffCommand(OnOffType.ON.equals(command)); + } else if (command instanceof PercentType percent) { + int requestedBrightness = percent.intValue(); + if (requestedBrightness == 0) { + super.addOnOffCommand(false); + } else { + brightnessCommand(new HSBType("0,0," + requestedBrightness)); + super.addOnOffCommand(true); + } + } + } + if (CHANNEL_LIGHT_TEMPERATURE.equals(channel) || CHANNEL_LIGHT_TEMPERATURE_ABS.equals(channel)) { + long kelvin = -1; + HSBType colorTemp = null; + if (command instanceof PercentType percent) { + kelvin = super.getKelvin(percent.intValue()); + colorTemp = ColorModel.kelvin2Hsb(kelvin); + } else if (command instanceof QuantityType number) { + kelvin = number.intValue(); + colorTemp = ColorModel.kelvin2Hsb(kelvin); + } + // there are color lights which cannot handle tempera HSB {}t ,kelvin,colorTempure as stored in capabilities + // in this case calculate color which is fitting to temperature + if (colorTemp != null && !receiveCapabilities.contains(Model.COLOR_TEMPERATURE_CAPABILITY)) { + HSBType colorTempAdaption = new HSBType(colorTemp.getHue(), colorTemp.getSaturation(), + hsbDevice.getBrightness()); + if (customDebug) { + logger.info("DIRIGERA COLOR_LIGHT {} handle temperature as color {}", thing.getLabel(), + colorTempAdaption); + } + colorCommand(colorTempAdaption); + } + } + } + + /** + * Send hue and saturation to light device in case of difference is more than 2% + * + * @param hsb as requested color + * @return true if color request is sent, false otherwise + */ + private void colorCommand(HSBType hsb) { + if (!"color".equals(colorMode) || !ColorModel.closeTo(hsb, hsbDevice, 0.02)) { + JSONObject colorAttributes = new JSONObject(); + colorAttributes.put("colorHue", hsb.getHue().intValue()); + colorAttributes.put("colorSaturation", hsb.getSaturation().intValue() / 100.0); + super.changeProperty(LightCommand.Action.COLOR, colorAttributes); + } + } + + private void brightnessCommand(HSBType hsb) { + int requestedBrightness = hsb.getBrightness().intValue(); + int currentBrightness = hsbDevice.getBrightness().intValue(); + if (Math.abs(requestedBrightness - currentBrightness) > 1) { + if (requestedBrightness > 0) { + JSONObject brightnessattributes = new JSONObject(); + brightnessattributes.put("lightLevel", hsb.getBrightness().intValue()); + super.changeProperty(LightCommand.Action.BRIGHTNESS, brightnessattributes); + } + } + } + + @Override + public void handleUpdate(JSONObject update) { + super.handleUpdate(update); + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + boolean deliverHSB = false; + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + if (ATTRIBUTE_COLOR_MODE.equals(key)) { + colorMode = attributes.getString(key); + } + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + // apply and update to hsbCurrent, only in case !isOn deliver fake brightness HSBs + switch (targetChannel) { + case CHANNEL_LIGHT_COLOR: + switch (key) { + case "colorHue": + double hueValue = attributes.getInt(key); + hsbDevice = new HSBType(new DecimalType(hueValue), hsbDevice.getSaturation(), + hsbDevice.getBrightness()); + hsbStateReflection = new HSBType(new DecimalType(hueValue), + hsbStateReflection.getSaturation(), hsbStateReflection.getBrightness()); + deliverHSB = true; + break; + case "colorSaturation": + int saturationValue = Math.round(attributes.getFloat(key) * 100); + hsbDevice = new HSBType(hsbDevice.getHue(), new PercentType(saturationValue), + hsbDevice.getBrightness()); + hsbStateReflection = new HSBType(hsbStateReflection.getHue(), + new PercentType(saturationValue), hsbStateReflection.getBrightness()); + deliverHSB = true; + break; + case "lightLevel": + int brightnessValue = attributes.getInt(key); + // device needs the right values + hsbDevice = new HSBType(hsbDevice.getHue(), hsbDevice.getSaturation(), + new PercentType(brightnessValue)); + hsbStateReflection = new HSBType(hsbStateReflection.getHue(), + hsbStateReflection.getSaturation(), new PercentType(brightnessValue)); + deliverHSB = true; + break; + } + break; + } + } + } + if (deliverHSB) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_COLOR), hsbStateReflection); + if (!receiveCapabilities.contains(Model.COLOR_TEMPERATURE_CAPABILITY)) { + // if color light doesn't support native light temperature converted values are taken + long kelvin = Math.min(colorTemperatureMin, + Math.max(colorTemperatureMax, ColorModel.hsb2Kelvin(hsbStateReflection))); + updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE), + new PercentType(getPercent(kelvin))); + updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE_ABS), + QuantityType.valueOf(kelvin, Units.KELVIN)); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/DimmableLightHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/DimmableLightHandler.java new file mode 100644 index 00000000000..8bedfb00dee --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/DimmableLightHandler.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.light; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.Iterator; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * {@link DimmableLightHandler} for lights with brightness + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class DimmableLightHandler extends BaseLight { + protected int currentBrightness = 0; + + public DimmableLightHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + String channel = channelUID.getIdWithoutGroup(); + String targetProperty = channel2PropertyMap.get(channel); + if (targetProperty != null) { + switch (channel) { + case CHANNEL_LIGHT_BRIGHTNESS: + if (command instanceof PercentType percent) { + int percentValue = percent.intValue(); + // switch on or off depending on brightness ... + if (percentValue > 0) { + // first change brightness to be stored for power ON ... + if (Math.abs(percentValue - currentBrightness) > 1) { + JSONObject brightnessAttributes = new JSONObject(); + brightnessAttributes.put(targetProperty, percent.intValue()); + super.changeProperty(LightCommand.Action.BRIGHTNESS, brightnessAttributes); + } + // .. then switch power + if (!isPowered()) { + super.addOnOffCommand(true); + } + } else { + super.addOnOffCommand(false); + } + } else if (command instanceof OnOffType onOff) { + super.addOnOffCommand(OnOffType.ON.equals(onOff)); + } + break; + } + } + } + + @Override + public void handleUpdate(JSONObject update) { + super.handleUpdate(update); + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + switch (targetChannel) { + case CHANNEL_LIGHT_BRIGHTNESS: + // set new currentBrightness as received and continue with update depending on power state + currentBrightness = attributes.getInt(key); + case CHANNEL_POWER_STATE: + /** + * Power state changed + * on - report last received brightness + * off - deliver brightness 0 + */ + if (isPowered()) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_BRIGHTNESS), + new PercentType(currentBrightness)); + } else { + updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_BRIGHTNESS), + new PercentType(0)); + } + break; + } + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/LightCommand.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/LightCommand.java new file mode 100644 index 00000000000..e36320684fd --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/LightCommand.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.light; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.json.JSONObject; + +/** + * The {@link LightCommand} is holding all information to execute a new light command + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class LightCommand { + public enum Action { + ON, + BRIGHTNESS, + TEMPERATURE, + COLOR, + OFF + } + + public JSONObject request; + public Action action; + + public LightCommand(JSONObject request, Action action) { + this.request = request; + this.action = action; + } + + /** + * Link updates are equal because they are generic, all others false + * + * @param other + * @return + */ + @Override + public boolean equals(@Nullable Object other) { + return (other instanceof LightCommand command && action.equals(command.action)); + } + + @Override + public String toString() { + return this.action + ": " + this.request.toString(); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/SwitchLightHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/SwitchLightHandler.java new file mode 100644 index 00000000000..7674d56ca17 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/SwitchLightHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.light; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.core.thing.Thing; + +/** + * {@link SwitchLightHandler} for lights which can only be switched on / off + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SwitchLightHandler extends BaseLight { + + public SwitchLightHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/TemperatureLightHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/TemperatureLightHandler.java new file mode 100644 index 00000000000..74a0eda3bf0 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/light/TemperatureLightHandler.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.light; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.math.BigDecimal; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.DirigeraStateDescriptionProvider; +import org.openhab.binding.dirigera.internal.interfaces.Model; +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.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.StateDescriptionFragment; +import org.openhab.core.types.StateDescriptionFragmentBuilder; + +/** + * {@link TemperatureLightHandler} for lights with brightness and color temperature + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class TemperatureLightHandler extends DimmableLightHandler { + private PercentType currentColorTemp = new PercentType(); + + protected final DirigeraStateDescriptionProvider stateProvider; + // default values of "standard IKEA lamps" from JSON + protected int colorTemperatureMin = 4000; + protected int colorTemperatureMax = 2202; + protected int range = colorTemperatureMin - colorTemperatureMax; + + public TemperatureLightHandler(Thing thing, Map mapping, + DirigeraStateDescriptionProvider stateProvider) { + super(thing, mapping); + super.setChildHandler(this); + this.stateProvider = stateProvider; + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + JSONObject attributes = values.getJSONObject(Model.ATTRIBUTES); + // check for settings of color temperature in attributes + TreeMap properties = new TreeMap<>(editProperties()); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + if ("colorTemperatureMin".equals(key)) { + colorTemperatureMin = attributes.getInt(key); + properties.put("colorTemperatureMin", String.valueOf(colorTemperatureMin)); + } else if ("colorTemperatureMax".equals(key)) { + colorTemperatureMax = attributes.getInt(key); + properties.put("colorTemperatureMax", String.valueOf(colorTemperatureMax)); + } + } + StateDescriptionFragment fragment = StateDescriptionFragmentBuilder.create() + .withMinimum(BigDecimal.valueOf(colorTemperatureMax)) + .withMaximum(BigDecimal.valueOf(colorTemperatureMin)).withStep(BigDecimal.valueOf(100)) + .withPattern("%.0f K").withReadOnly(false).build(); + stateProvider.setStateDescription(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE_ABS), fragment); + updateProperties(properties); + range = colorTemperatureMin - colorTemperatureMax; + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + String channel = channelUID.getIdWithoutGroup(); + String targetProperty = channel2PropertyMap.get(channel); + switch (channel) { + case CHANNEL_LIGHT_TEMPERATURE_ABS: + targetProperty = "colorTemperature"; + case CHANNEL_LIGHT_TEMPERATURE: + long kelvinValue = -1; + int percentValue = -1; + if (command instanceof PercentType percent) { + percentValue = percent.intValue(); + kelvinValue = getKelvin(percent.intValue()); + } else if (command instanceof QuantityType number) { + kelvinValue = number.intValue(); + percentValue = getPercent(kelvinValue); + } else if (command instanceof OnOffType onOff) { + super.addOnOffCommand(OnOffType.ON.equals(onOff)); + } + /* + * some color lights which inherit this temperature light don't have the temperature capability. + * As workaround child class ColorLightHandler is handling color temperature + */ + if (receiveCapabilities.contains(Model.COLOR_TEMPERATURE_CAPABILITY) && percentValue != -1 + && kelvinValue != -1) { + JSONObject attributes = new JSONObject(); + attributes.put(targetProperty, kelvinValue); + super.changeProperty(LightCommand.Action.TEMPERATURE, attributes); + if (!isPowered()) { + // fake event for power OFF + updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE), + new PercentType(percentValue)); + updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE_ABS), + QuantityType.valueOf(kelvinValue, Units.KELVIN)); + } + } + } + } + + @Override + public void handleUpdate(JSONObject update) { + super.handleUpdate(update); + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + switch (targetChannel) { + case CHANNEL_LIGHT_TEMPERATURE: + int kelvin = attributes.getInt(key); + // seems some lamps are delivering temperature values out of range + // keep it in range with min/max + kelvin = Math.min(kelvin, colorTemperatureMin); + kelvin = Math.max(kelvin, colorTemperatureMax); + int percent = getPercent(kelvin); + currentColorTemp = new PercentType(percent); + updateState(new ChannelUID(thing.getUID(), targetChannel), currentColorTemp); + updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE_ABS), + QuantityType.valueOf(kelvin, Units.KELVIN)); + break; + } + } + } + } + } + + protected long getKelvin(int percent) { + return Math.round(colorTemperatureMin - (range * percent / 100)); + } + + protected int getPercent(long kelvin) { + return Math.min(100, Math.max(0, Math.round(100 - ((kelvin - colorTemperatureMax) * 100 / range)))); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/plug/PowerPlugHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/plug/PowerPlugHandler.java new file mode 100644 index 00000000000..21f61f0673e --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/plug/PowerPlugHandler.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.plug; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.Iterator; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * The {@link PowerPlugHandler} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class PowerPlugHandler extends SimplePlugHandler { + public PowerPlugHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + } + + @Override + public void initialize() { + super.initialize(); + // update of values is handled in super class + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + String channel = channelUID.getIdWithoutGroup(); + String targetProperty = channel2PropertyMap.get(channel); + if (targetProperty != null) { + switch (channel) { + case CHANNEL_CHILD_LOCK: + case CHANNEL_DISABLE_STATUS_LIGHT: + if (command instanceof OnOffType onOff) { + JSONObject attributes = new JSONObject(); + attributes.put(targetProperty, OnOffType.ON.equals(onOff)); + super.sendAttributes(attributes); + } + break; + } + } + } + + @Override + public void handleUpdate(JSONObject update) { + // handle reachable flag + super.handleUpdate(update); + // now device specific + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + switch (targetChannel) { + case CHANNEL_CHILD_LOCK: + case CHANNEL_DISABLE_STATUS_LIGHT: + updateState(new ChannelUID(thing.getUID(), targetChannel), + OnOffType.from(attributes.getBoolean(key))); + break; + } + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/plug/SimplePlugHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/plug/SimplePlugHandler.java new file mode 100644 index 00000000000..86450702875 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/plug/SimplePlugHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.plug; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.core.thing.Thing; + +/** + * The {@link SimplePlugHandler} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SimplePlugHandler extends BaseHandler { + public SimplePlugHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + // links of types which can be established towards this device + linkCandidateTypes = List.of(DEVICE_TYPE_LIGHT_CONTROLLER, DEVICE_TYPE_MOTION_SENSOR); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + // handling of first update, also for PowerPlug and SmartPlug child classes! + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/plug/SmartPlugHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/plug/SmartPlugHandler.java new file mode 100644 index 00000000000..5751390e379 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/plug/SmartPlugHandler.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.plug; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.time.Instant; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * The {@link SmartPlugHandler} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SmartPlugHandler extends PowerPlugHandler { + private double totalEnergy = -1; + private double resetEnergy = -1; + + public SmartPlugHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + } + + @Override + public void initialize() { + super.initialize(); + // update of values is handled in super class + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + String channel = channelUID.getIdWithoutGroup(); + switch (channel) { + case CHANNEL_ENERGY_RESET_DATE: + if (command instanceof DateTimeType) { + scheduler.schedule(this::energyReset, 250, TimeUnit.MILLISECONDS); + } + } + } + + private void energyReset() { + JSONObject reset = new JSONObject("{\"energyConsumedAtLastReset\": 0}"); + super.sendAttributes(reset); + } + + @Override + public void handleUpdate(JSONObject update) { + // handle reachable flag + super.handleUpdate(update); + // now device specific + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + switch (targetChannel) { + case CHANNEL_POWER: + updateState(new ChannelUID(thing.getUID(), targetChannel), + QuantityType.valueOf(attributes.getDouble(key), Units.WATT)); + break; + case CHANNEL_CURRENT: + updateState(new ChannelUID(thing.getUID(), targetChannel), + QuantityType.valueOf(attributes.getDouble(key), Units.AMPERE)); + break; + case CHANNEL_POTENTIAL: + updateState(new ChannelUID(thing.getUID(), targetChannel), + QuantityType.valueOf(attributes.getDouble(key), Units.VOLT)); + break; + case CHANNEL_ENERGY_TOTAL: + totalEnergy = attributes.getDouble(key); + updateState(new ChannelUID(thing.getUID(), CHANNEL_ENERGY_TOTAL), + QuantityType.valueOf(totalEnergy, Units.KILOWATT_HOUR)); + if (totalEnergy >= 0 && resetEnergy >= 0) { + double diff = totalEnergy - resetEnergy; + updateState(new ChannelUID(thing.getUID(), CHANNEL_ENERGY_RESET), + QuantityType.valueOf(diff, Units.KILOWATT_HOUR)); + } + break; + case CHANNEL_ENERGY_RESET: + resetEnergy = attributes.getDouble(key); + if (totalEnergy >= 0 && resetEnergy >= 0) { + double diff = totalEnergy - resetEnergy; + updateState(new ChannelUID(thing.getUID(), CHANNEL_ENERGY_RESET), + QuantityType.valueOf(diff, Units.KILOWATT_HOUR)); + } + break; + case CHANNEL_ENERGY_RESET_DATE: + String dateTime = attributes.getString(key); + Instant restTime = Instant.parse(dateTime); + updateState(new ChannelUID(thing.getUID(), CHANNEL_ENERGY_RESET_DATE), + new DateTimeType(restTime)); + break; + } + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/repeater/RepeaterHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/repeater/RepeaterHandler.java new file mode 100644 index 00000000000..30038a4ac84 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/repeater/RepeaterHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.repeater; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * The {@link RepeaterHandler} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class RepeaterHandler extends BaseHandler { + + public RepeaterHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + } + + @Override + public void handleUpdate(JSONObject update) { + // handle reachable flag, no more special handling + super.handleUpdate(update); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/scene/SceneHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/scene/SceneHandler.java new file mode 100644 index 00000000000..e0d6a707ca5 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/scene/SceneHandler.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.scene; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.Command; +import org.openhab.core.types.UnDefType; + +/** + * The {@link SceneHandler} for triggering defined scenes + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SceneHandler extends BaseHandler { + private Instant lastTrigger = Instant.MAX; + private int undoDuration = 30; + + public SceneHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + // no link support for Scenes + hardLinks = Arrays.asList(); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readScene(config.id); + handleUpdate(values); + + if (values.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, + "@text/dirigera.scene.status.scene-not-found"); + } else { + updateStatus(ThingStatus.ONLINE); + } + + // check if different undo duration is configured + if (values.has("undoAllowedDuration")) { + undoDuration = values.getInt("undoAllowedDuration"); + } + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + if (CHANNEL_TRIGGER.equals(channelUID.getIdWithoutGroup())) { + if (command instanceof DecimalType decimal) { + int commandNumber = decimal.intValue(); + switch (commandNumber) { + case 0: + gateway().api().triggerScene(config.id, "trigger"); + lastTrigger = Instant.now(); + scheduler.schedule(this::countDown, 1, TimeUnit.SECONDS); + break; + case 1: + gateway().api().triggerScene(config.id, "undo"); + lastTrigger = Instant.MAX; + updateState(new ChannelUID(thing.getUID(), CHANNEL_TRIGGER), UnDefType.UNDEF); + break; + } + } + } + } + + @Override + public void handleUpdate(JSONObject update) { + super.handleUpdate(update); + if (update.has("lastTriggered")) { + Instant lastRiggeredInstant = Instant.parse(update.getString("lastTriggered")); + DateTimeType dtt = new DateTimeType(lastRiggeredInstant); + updateState(new ChannelUID(thing.getUID(), CHANNEL_LAST_TRIGGER), dtt); + } + } + + private void countDown() { + long seconds = Duration.between(lastTrigger, Instant.now()).toSeconds(); + if (seconds >= 0 && seconds <= 30) { + long countDown = undoDuration - seconds; + updateState(new ChannelUID(thing.getUID(), CHANNEL_TRIGGER), new DecimalType(countDown)); + scheduler.schedule(this::countDown, 1, TimeUnit.SECONDS); + } else { + updateState(new ChannelUID(thing.getUID(), CHANNEL_TRIGGER), UnDefType.UNDEF); + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/AirQualityHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/AirQualityHandler.java new file mode 100644 index 00000000000..503c3a1bf22 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/AirQualityHandler.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * The {@link AirQualityHandler} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class AirQualityHandler extends BaseHandler { + + public AirQualityHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + // no link support for Scenes + hardLinks = Arrays.asList(); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + } + + @Override + public void handleUpdate(JSONObject update) { + // handle reachable flag + super.handleUpdate(update); + // now device specific + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + switch (targetChannel) { + case CHANNEL_TEMPERATURE: + double temperature = Math.round(attributes.getDouble(key) * 10) / 10.0; + updateState(new ChannelUID(thing.getUID(), CHANNEL_TEMPERATURE), + QuantityType.valueOf(temperature, SIUnits.CELSIUS)); + break; + case CHANNEL_HUMIDITY: + updateState(new ChannelUID(thing.getUID(), CHANNEL_HUMIDITY), + QuantityType.valueOf(attributes.getDouble(key), Units.PERCENT)); + break; + case CHANNEL_PARTICULATE_MATTER: + updateState(new ChannelUID(thing.getUID(), CHANNEL_PARTICULATE_MATTER), + QuantityType.valueOf(attributes.getDouble(key), Units.MICROGRAM_PER_CUBICMETRE)); + break; + case CHANNEL_VOC_INDEX: + updateState(new ChannelUID(thing.getUID(), CHANNEL_VOC_INDEX), + new DecimalType(attributes.getDouble(key))); + break; + } + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/ContactSensorHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/ContactSensorHandler.java new file mode 100644 index 00000000000..778b4293781 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/ContactSensorHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor; + +import static org.openhab.binding.dirigera.internal.Constants.CHANNEL_CONTACT; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * The {@link ContactSensorHandler} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ContactSensorHandler extends BaseHandler { + + public ContactSensorHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + // no link support for Scenes + hardLinks = Arrays.asList(); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + } + + @Override + public void handleUpdate(JSONObject update) { + // handle reachable flag + super.handleUpdate(update); + // now device specific + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + switch (targetChannel) { + case CHANNEL_CONTACT: + OpenClosedType state = OpenClosedType.CLOSED; + if (attributes.getBoolean(key)) { + state = OpenClosedType.OPEN; + } + updateState(new ChannelUID(thing.getUID(), targetChannel), state); + break; + } + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/LightSensorHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/LightSensorHandler.java new file mode 100644 index 00000000000..eb159e08032 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/LightSensorHandler.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor; + +import static org.openhab.binding.dirigera.internal.Constants.CHANNEL_ILLUMINANCE; + +import java.util.Iterator; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * The {@link LightSensorHandler} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class LightSensorHandler extends BaseHandler { + public LightSensorHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + } + + @Override + public void handleUpdate(JSONObject update) { + // handle reachable flag + super.handleUpdate(update); + // now device specific + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + if (CHANNEL_ILLUMINANCE.equals(targetChannel)) { + updateState(new ChannelUID(thing.getUID(), targetChannel), + QuantityType.valueOf(attributes.getInt(key), Units.LUX)); + } + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/MotionLightSensorHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/MotionLightSensorHandler.java new file mode 100644 index 00000000000..13bb22529cf --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/MotionLightSensorHandler.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; + +/** + * The {@link MotionLightSensorHandler} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class MotionLightSensorHandler extends MotionSensorHandler { + + private TreeMap relations = new TreeMap<>(); + + public MotionLightSensorHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + // assure deviceType is set from main device + if (values.has(PROPERTY_DEVICE_TYPE)) { + deviceType = values.getString(PROPERTY_DEVICE_TYPE); + } + + // get all relations and register + String relationId = gateway().model().getRelationId(config.id); + relations = gateway().model().getRelations(relationId); + // register for updates of twin devices + relations.forEach((key, value) -> { + gateway().registerDevice(this, key); + JSONObject relationValues = gateway().api().readDevice(key); + handleUpdate(relationValues); + }); + } + } + + @Override + public void dispose() { + relations.forEach((key, value) -> { + gateway().unregisterDevice(this, key); + }); + super.dispose(); + } + + @Override + public void handleRemoval() { + relations.forEach((key, value) -> { + gateway().deleteDevice(this, key); + }); + super.handleRemoval(); + } + + @Override + public void handleUpdate(JSONObject update) { + super.handleUpdate(update); + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + if (CHANNEL_ILLUMINANCE.equals(targetChannel)) { + updateState(new ChannelUID(thing.getUID(), targetChannel), + QuantityType.valueOf(attributes.getInt(key), Units.LUX)); + } + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/MotionSensorHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/MotionSensorHandler.java new file mode 100644 index 00000000000..70d3ef76102 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/MotionSensorHandler.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONArray; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +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.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link MotionSensorHandler} + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class MotionSensorHandler extends BaseHandler { + + private final Logger logger = LoggerFactory.getLogger(MotionSensorHandler.class); + private final String timeFormat = "HH:mm"; + private String startTime = "20:00"; + private String endTime = "07:00"; + + public MotionSensorHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + // links of types which can be established towards this device + linkCandidateTypes = List.of(DEVICE_TYPE_LIGHT, DEVICE_TYPE_OUTLET); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + String targetChannel = channelUID.getIdWithoutGroup(); + switch (targetChannel) { + case CHANNEL_ACTIVE_DURATION: + int seconds = -1; + if (command instanceof DecimalType decimal) { + seconds = decimal.intValue(); + } else if (command instanceof QuantityType quantity) { + QuantityType secondsQunatity = quantity.toUnit(Units.SECOND); + if (secondsQunatity != null) { + seconds = secondsQunatity.intValue(); + } + } + if (seconds > 0) { + String updateData = String + .format(gateway().model().getTemplate(Model.TEMPLATE_SENSOR_DURATION_UPDATE), seconds); + sendPatch(new JSONObject(updateData)); + } + break; + case CHANNEL_SCHEDULE: + if (command instanceof DecimalType decimal) { + switch (decimal.intValue()) { + case 0: + gateway().api().sendPatch(config.id, + new JSONObject(gateway().model().getTemplate(Model.TEMPLATE_SENSOR_ALWQAYS_ON))); + break; + case 1: + gateway().api().sendPatch(config.id, + new JSONObject(gateway().model().getTemplate(Model.TEMPLATE_SENSOR_FOLLOW_SUN))); + break; + case 2: + String template = gateway().model().getTemplate(Model.TEMPLATE_SENSOR_SCHEDULE_ON); + gateway().api().sendPatch(config.id, + new JSONObject(String.format(template, startTime, endTime))); + break; + } + } + break; + case CHANNEL_SCHEDULE_START: + String startSchedule = gateway().model().getTemplate(Model.TEMPLATE_SENSOR_SCHEDULE_ON); + if (command instanceof StringType string) { + // take string as it is, no consistency check + startTime = string.toFullString(); + } else if (command instanceof DateTimeType dateTime) { + startTime = dateTime.format(timeFormat, ZoneId.systemDefault()); + } + gateway().api().sendPatch(config.id, new JSONObject(String.format(startSchedule, startTime, endTime))); + break; + case CHANNEL_SCHEDULE_END: + String endSchedule = gateway().model().getTemplate(Model.TEMPLATE_SENSOR_SCHEDULE_ON); + if (command instanceof StringType string) { + endTime = string.toFullString(); + // take string as it is, no consistency check + } else if (command instanceof DateTimeType dateTime) { + endTime = dateTime.format(timeFormat, ZoneId.systemDefault()); + } + gateway().api().sendPatch(config.id, new JSONObject(String.format(endSchedule, startTime, endTime))); + break; + case CHANNEL_LIGHT_PRESET: + if (command instanceof StringType string) { + JSONArray presetValues = new JSONArray(); + // handle the standard presets from IKEA app, custom otherwise without consistency check + switch (string.toFullString()) { + case "Off": + // fine - array stays empty + break; + case "Warm": + presetValues = new JSONArray( + gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_WARM)); + break; + case "Slowdown": + presetValues = new JSONArray( + gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_SLOWDOWN)); + break; + case "Smooth": + presetValues = new JSONArray( + gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_SMOOTH)); + break; + case "Bright": + presetValues = new JSONArray( + gateway().model().getTemplate(Model.TEMPLATE_LIGHT_PRESET_BRIGHT)); + break; + default: + presetValues = new JSONArray(string.toFullString()); + } + JSONObject preset = new JSONObject(); + preset.put("circadianPresets", presetValues); + super.sendAttributes(preset); + } + } + } + + @Override + public void handleUpdate(JSONObject update) { + super.handleUpdate(update); + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + switch (targetChannel) { + case CHANNEL_MOTION_DETECTION: + updateState(new ChannelUID(thing.getUID(), targetChannel), + OnOffType.from(attributes.getBoolean(key))); + break; + case CHANNEL_ACTIVE_DURATION: + if (attributes.has("sensorConfig")) { + JSONObject sensorConfig = attributes.getJSONObject("sensorConfig"); + if (sensorConfig.has("onDuration")) { + int duration = sensorConfig.getInt("onDuration"); + updateState(new ChannelUID(thing.getUID(), targetChannel), + QuantityType.valueOf(duration, Units.SECOND)); + } + } + break; + } + } + // no direct channel mapping - sensor mapping is deeply nested :( + switch (key) { + case "circadianPresets": + if (attributes.has("circadianPresets")) { + JSONArray lightPresets = attributes.getJSONArray("circadianPresets"); + updateState(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_PRESET), + StringType.valueOf(lightPresets.toString())); + } + break; + case "sensorConfig": + if (attributes.has("sensorConfig")) { + JSONObject sensorConfig = attributes.getJSONObject("sensorConfig"); + if (sensorConfig.has("scheduleOn")) { + boolean scheduled = sensorConfig.getBoolean("scheduleOn"); + if (scheduled) { + // examine schedule + if (sensorConfig.has("schedule")) { + JSONObject schedule = sensorConfig.getJSONObject("schedule"); + if (schedule.has("onCondition") && schedule.has("offCondition")) { + JSONObject onCondition = schedule.getJSONObject("onCondition"); + JSONObject offCondition = schedule.getJSONObject("offCondition"); + if (onCondition.has("time")) { + String onTime = onCondition.getString("time"); + String offTime = offCondition.getString("time"); + if ("sunset".equals(onTime)) { + // finally it's identified to follow the sun + updateState(new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE), + new DecimalType(1)); + Instant sunsetDateTime = gateway().getSunsetDateTime(); + if (sunsetDateTime != null) { + updateState( + new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_START), + new DateTimeType(sunsetDateTime)); + } else { + updateState( + new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_START), + UnDefType.UNDEF); + logger.warn( + "MOTION_SENSOR Location not activated in IKEA App - cannot follow sun"); + } + Instant sunriseDateTime = gateway().getSunriseDateTime(); + if (sunriseDateTime != null) { + updateState( + new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_END), + new DateTimeType(sunriseDateTime)); + } else { + updateState( + new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_END), + UnDefType.UNDEF); + logger.warn( + "MOTION_SENSOR Location not activated in IKEA App - cannot follow sun"); + } + } else { + // custom times - even worse parsing + String[] onHourMinute = onTime.split(":"); + String[] offHourMinute = offTime.split(":"); + if (onHourMinute.length == 2 && offHourMinute.length == 2) { + int onHour = Integer.parseInt(onHourMinute[0]); + int onMinute = Integer.parseInt(onHourMinute[1]); + int offHour = Integer.parseInt(offHourMinute[0]); + int offMinute = Integer.parseInt(offHourMinute[1]); + updateState(new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE), + new DecimalType(2)); + ZonedDateTime on = ZonedDateTime.now().withHour(onHour) + .withMinute(onMinute); + ZonedDateTime off = ZonedDateTime.now().withHour(offHour) + .withMinute(offMinute); + updateState( + new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_START), + new DateTimeType(on)); + updateState( + new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_END), + new DateTimeType(off)); + } + } + } + } + } + } else { + // always active + updateState(new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE), new DecimalType(0)); + updateState(new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_START), + UnDefType.UNDEF); + updateState(new ChannelUID(thing.getUID(), CHANNEL_SCHEDULE_END), UnDefType.UNDEF); + } + } + } + break; + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/WaterSensorHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/WaterSensorHandler.java new file mode 100644 index 00000000000..98f8219191f --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/sensor/WaterSensorHandler.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor; + +import static org.openhab.binding.dirigera.internal.Constants.CHANNEL_LEAK_DETECTION; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * The {@link WaterSensorHandler} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class WaterSensorHandler extends BaseHandler { + + public WaterSensorHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + // no link support for Scenes + hardLinks = Arrays.asList(); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + } + + @Override + public void handleUpdate(JSONObject update) { + super.handleUpdate(update); + // now device specific + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + if (CHANNEL_LEAK_DETECTION.equals(targetChannel)) { + updateState(new ChannelUID(thing.getUID(), targetChannel), + OnOffType.from(attributes.getBoolean(key))); + } + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/speaker/SpeakerHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/speaker/SpeakerHandler.java new file mode 100644 index 00000000000..8f7796afd85 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/handler/speaker/SpeakerHandler.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.speaker; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.NextPreviousType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.PlayPauseType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link SpeakerHandler} to control speaker devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class SpeakerHandler extends BaseHandler { + + public SpeakerHandler(Thing thing, Map mapping) { + super(thing, mapping); + super.setChildHandler(this); + // links of types which can be established towards this device + linkCandidateTypes = List.of(DEVICE_TYPE_SOUND_CONTROLLER); + } + + @Override + public void initialize() { + super.initialize(); + if (super.checkHandler()) { + JSONObject values = gateway().api().readDevice(config.id); + handleUpdate(values); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + String channel = channelUID.getIdWithoutGroup(); + if (command instanceof RefreshType) { + super.handleCommand(channelUID, command); + } else { + String targetProperty = channel2PropertyMap.get(channel); + if (targetProperty != null) { + switch (channel) { + case CHANNEL_PLAYER: + if (command instanceof PlayPauseType playPause) { + String playState = (PlayPauseType.PLAY.equals(playPause) ? "playbackPlaying" + : "playbackPaused"); + JSONObject attributes = new JSONObject(); + attributes.put(targetProperty, playState); + super.sendAttributes(attributes); + } else if (command instanceof NextPreviousType nextPrevious) { + String playState = (NextPreviousType.NEXT.equals(nextPrevious) ? "playbackNext" + : "playbackPrevious"); + JSONObject attributes = new JSONObject(); + attributes.put(targetProperty, playState); + super.sendAttributes(attributes); + } + break; + case CHANNEL_VOLUME: + if (command instanceof PercentType percent) { + JSONObject attributes = new JSONObject(); + attributes.put(targetProperty, percent.intValue()); + super.sendAttributes(attributes); + } + break; + case CHANNEL_MUTE: + if (command instanceof OnOffType onOff) { + JSONObject attributes = new JSONObject(); + attributes.put(targetProperty, OnOffType.ON.equals(onOff)); + super.sendAttributes(attributes); + } + break; + } + } else { + // handle channels not in map due to deeper nesting objects + switch (channel) { + case CHANNEL_SHUFFLE: + if (command instanceof OnOffType onOff) { + JSONObject mode = new JSONObject(); + mode.put("shuffle", OnOffType.ON.equals(onOff)); + JSONObject attributes = new JSONObject(); + attributes.put("playbackModes", mode); + super.sendAttributes(attributes); + } + break; + case CHANNEL_CROSSFADE: + if (command instanceof OnOffType onOff) { + JSONObject mode = new JSONObject(); + mode.put("crossfade", OnOffType.ON.equals(onOff)); + JSONObject attributes = new JSONObject(); + attributes.put("playbackModes", mode); + super.sendAttributes(attributes); + } + break; + case CHANNEL_REPEAT: + if (command instanceof DecimalType decimal) { + int repeatModeInt = decimal.intValue(); + String repeatModeStr = ""; + switch (repeatModeInt) { + case 0: + repeatModeStr = "off"; + break; + case 1: + repeatModeStr = "playItem"; + break; + case 2: + repeatModeStr = "playlist"; + break; + } + if (!repeatModeStr.isBlank()) { + JSONObject mode = new JSONObject(); + mode.put("repeat", repeatModeStr); + JSONObject attributes = new JSONObject(); + attributes.put("playbackModes", mode); + super.sendAttributes(attributes); + } + } + break; + } + } + } + } + + @Override + public void handleUpdate(JSONObject update) { + super.handleUpdate(update); + if (update.has(Model.ATTRIBUTES)) { + JSONObject attributes = update.getJSONObject(Model.ATTRIBUTES); + Iterator attributesIterator = attributes.keys(); + while (attributesIterator.hasNext()) { + String key = attributesIterator.next(); + String targetChannel = property2ChannelMap.get(key); + if (targetChannel != null) { + if (CHANNEL_PLAYER.equals(targetChannel)) { + String playerState = attributes.getString(key); + switch (playerState) { + case "playbackPlaying": + updateState(new ChannelUID(thing.getUID(), targetChannel), PlayPauseType.PLAY); + break; + case "playbackIdle": + case "playbackPaused": + updateState(new ChannelUID(thing.getUID(), targetChannel), PlayPauseType.PAUSE); + break; + } + } else if (CHANNEL_VOLUME.equals(targetChannel)) { + updateState(new ChannelUID(thing.getUID(), targetChannel), + new PercentType(attributes.getInt(key))); + } else if (CHANNEL_MUTE.equals(targetChannel)) { + updateState(new ChannelUID(thing.getUID(), targetChannel), + OnOffType.from(attributes.getBoolean(key))); + } else if (CHANNEL_PLAY_MODES.equals(targetChannel)) { + JSONObject playbackModes = attributes.getJSONObject(key); + if (playbackModes.has("crossfade")) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_CROSSFADE), + OnOffType.from(playbackModes.getBoolean("crossfade"))); + } + if (playbackModes.has("shuffle")) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_SHUFFLE), + OnOffType.from(playbackModes.getBoolean("shuffle"))); + } + if (playbackModes.has("repeat")) { + String repeatMode = playbackModes.getString("repeat"); + int playMode = -1; + switch (repeatMode) { + case "off": + playMode = 0; + break; + case "playItem": + playMode = 1; + break; + case "playlist": + playMode = 2; + break; + } + if (playMode != -1) { + updateState(new ChannelUID(thing.getUID(), CHANNEL_REPEAT), new DecimalType(playMode)); + } + } + + } else if (CHANNEL_TRACK.equals(targetChannel)) { + // track is nested into attributes playItem + State track = UnDefType.UNDEF; + State image = UnDefType.UNDEF; + JSONObject audio = attributes.getJSONObject(key); + if (audio.has("playItem")) { + JSONObject playItem = audio.getJSONObject("playItem"); + if (playItem.has("title")) { + track = new StringType(playItem.getString("title")); + } + if (playItem.has("imageURL")) { + String imageURL = playItem.getString("imageURL"); + image = gateway().api().getImage(imageURL); + } + } else if (audio.has("playlist")) { + JSONObject playlist = audio.getJSONObject("playlist"); + if (playlist.has("title")) { + track = new StringType(playlist.getString("title")); + } + } + updateState(new ChannelUID(thing.getUID(), targetChannel), track); + updateState(new ChannelUID(thing.getUID(), CHANNEL_IMAGE), image); + } + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/DebugHandler.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/DebugHandler.java new file mode 100644 index 00000000000..f368f7c46e4 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/DebugHandler.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.interfaces; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.binding.ThingHandler; + +/** + * {@link DebugHandler} interface to control debugging via rule actions + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public interface DebugHandler extends ThingHandler { + + /** + * Returns the token associated with the DIRIGERA gateway. Regardless on which device this action is called the + * token from gateway (bridge) is returned. + * + * @return token as String + */ + String getToken(); + + /** + * Returns the JSON representation at this time for a specific device. If action is called on gateway a snapshot + * from all connected devices is returned. + * + * @return device JSON at this time + */ + String getJSON(); + + /** + * Enables / disables debug for one specific device. If enabled messages are logged on info level regarding + * - commands send via openHAB + * - state updates of openHAB + * - API requests with payload towards gateway + * - push notifications from gateway + * - API responses from gateway + * + * @param debug boolean flag enabling or disabling debug messages + */ + void setDebug(boolean debug, boolean all); + + /** + * Returns the device ID of the device this handler is associated with. + * + * @return device ID as String + */ + String getDeviceId(); +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/DirigeraAPI.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/DirigeraAPI.java new file mode 100644 index 00000000000..1a565239c4a --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/DirigeraAPI.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.interfaces; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.core.types.State; + +/** + * {@link DirigeraAPI} high level interface to communicate with the gateway. These are comfort functions fitting to the + * needs of the handlers. Each function is synchronized so no parallel calls will be established towards gateway. + * Rationale: + * Several times seen that gateway goes into a "quite mode" during monkey testing. It's still accepting commands but no + * more updates were received. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public interface DirigeraAPI { + + /** JSON key for error flag, value shall be boolean */ + static final String HTTP_ERROR_FLAG = "http-error-flag"; + + /** JSON key for error flag, value shall be int */ + static final String HTTP_ERROR_STATUS = "http-error-status"; + + /** JSON key for error message, value shall be String */ + static final String HTTP_ERROR_MESSAGE = "http-error-message"; + + /** + * Read complete home model. + * + * @return JSONObject with data. In case of error the JSONObject is filled with error data + */ + JSONObject readHome(); + + /** + * Read all data for one specific deviceId. + * + * @param deviceId to query + * @return JSONObject with data. In case of error the JSONObject is filled with error data + */ + JSONObject readDevice(String deviceId); + + /** + * Read all data for one specific scene. + * + * @param sceneId to query + * @return JSONObject with data. In case of error the JSONObject is filled with error data + */ + JSONObject readScene(String sceneId); + + /** + * Read all data for one specific scene. + * + * @param sceneId to query + * @param trigger to send + * @return JSONObject with data. In case of error the JSONObject is filled with error data + */ + void triggerScene(String sceneId, String trigger); + + /** + * Send attributes to a device + * + * @param deviceId to update + * @param attributes to send + * @return Integer of http response status + */ + int sendAttributes(String deviceId, JSONObject attributes); + + /** + * Send patch with other data than attributes to a device + * + * @param deviceId to update + * @param data to send + * @return Integer of http response status + */ + int sendPatch(String deviceId, JSONObject data); + + /** + * Creating a scene from scene template for a click pattern of a controller + * + * @param uuid of the scene to be created + * @param clickPattern which shall trigger the scene + * @param controllerId which delivering the clickPattern + * @return String uuid of the created scene + */ + String createScene(String uuid, String clickPattern, String controllerId); + + /** + * Delete scene of given uuid + * + * @param uuid of the scene to be deleted + */ + void deleteScene(String uuid); + + /** + * Get image from an url. + * + * @return RawType in case of successful call, UndefType.UNDEF in case of error + */ + State getImage(String imageURL); +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/Gateway.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/Gateway.java new file mode 100644 index 00000000000..9d94194e3cf --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/Gateway.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.interfaces; + +import java.time.Instant; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dirigera.internal.DirigeraCommandProvider; +import org.openhab.binding.dirigera.internal.discovery.DirigeraDiscoveryService; +import org.openhab.binding.dirigera.internal.exception.ApiException; +import org.openhab.binding.dirigera.internal.exception.ModelException; +import org.openhab.binding.dirigera.internal.handler.BaseHandler; +import org.openhab.core.thing.Thing; +import org.osgi.framework.BundleContext; + +/** + * The {@link Gateway} Gateway interface to access data from other instances. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public interface Gateway { + + /** + * Get the thing attached to this gateway. + * + * @return Thing + */ + Thing getThing(); + + /** + * Get IP address from gateway for API calls and WebSocket connections. + * + * @return ip address as String + */ + String getIpAddress(); + + /** + * Get token associated to this gateway for API calls and WebSocket connections. + * + * @return token as String + */ + String getToken(); + + /** + * Get CommandProvider associated to this binding. For links and link candidates the command options are filled with + * the right link options. + * + * @return DirigeraCommandProvider as DynamicCommandDescriptionProvider + */ + DirigeraCommandProvider getCommandProvider(); + + /** + * Returns the configuration setting if discovery is enabled. + * + * @return boolean discovery flag + */ + boolean discoveryEnabled(); + + /** + * Register a handler with the given deviceId reflecting a device or scene. Shall be called during + * initialization. + * + * This function is handled asynchronous. + * + * @param deviceHandler handler of this binding + * @param deviceId connected device id + */ + void registerDevice(BaseHandler deviceHandler, String deviceId); + + /** + * Unregister a handler associated with the given deviceId reflecting a device or scene. Shall be called + * during dispose. + * + * This function is handled asynchronous. + * + * @param deviceHandler handler of this binding + * @param deviceId connected device id + */ + void unregisterDevice(BaseHandler deviceHandler, String deviceId); + + /** + * Deletes an openHAB handler associated with the given deviceId reflecting a device or scene. Shall be called + * during handleRemoval. + * + * This function is handled asynchronous. + * + * @param deviceHandler handler of this binding + * @param deviceId connected device id + */ + void deleteDevice(BaseHandler deviceHandler, String deviceId); + + /** + * Deletes a device or scene detected by the model. A device can be deleted without openHAB interaction in IKEA Home + * smart app and openHAB needs to be informed about this removal to update ThingStatus accordingly. + * + * @param deviceId device id to be removed + */ + void deleteDevice(String deviceId); + + /** + * Check if device id is known in the gateway namely if a handler is created or not. + * + * @param deviceId connected device id + */ + boolean isKnownDevice(String deviceId); + + /** + * Update websocket connected statues. + * + * @param boolean connected + * @param reason as String + */ + void websocketConnected(boolean connected, String reason); + + /** + * Update from websocket regarding changed data. + * + * This function is handled asynchronous. + * + * @param String content of update + */ + void websocketUpdate(String update); + + /** + * Update links for all devices. Devices which are storing the links (hard link) are responsible to detect changes. + * If change is detected the linked device will be updated with a soft link. + * + * This function is handled asynchronous. + * + * @param String content of update + */ + void updateLinks(); + + /** + * Next sunrise ZonedDateTime. Value is presented if gateway allows access to GPS position. Handler needs to take + * care regarding null values. + * + * @return next sunrise as ZonedDateTime + */ + @Nullable + Instant getSunriseDateTime(); + + /** + * Next sunset ZonedDateTime. Value is presented if gateway allows access to GPS position. Handler needs to take + * care regarding null values. + * + * @return next sunrise as ZonedDateTime + */ + @Nullable + Instant getSunsetDateTime(); + + /** + * Comfort access towards API which is only present after initialization. + * + * @throws ApiMissingException + * @return DirigeraAPI + */ + DirigeraAPI api() throws ApiException; + + /** + * Comfort access towards Model which is only present after initialization. + * + * @throws ModelMissingException + * @return Model + */ + Model model() throws ModelException; + + /** + * Comfort access towards DirigeraDiscoveryManager. + * + * @return DirigeraDiscoveryManager + */ + DirigeraDiscoveryService discovery(); + + /** + * Comfort access towards DirigeraDiscoveryManager. + * + * @return DirigeraDiscoveryManager + */ + BundleContext getBundleContext(); +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/Model.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/Model.java new file mode 100644 index 00000000000..2b44646ce5c --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/Model.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.interfaces; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link Model} is representing the structural data of the gateway. Concrete values e.g. temperature of devices + * shall not be accessed. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public interface Model { + + static final String REACHABLE = "isReachable"; + static final String ATTRIBUTES = "attributes"; + static final String CAPABILITIES = "capabilities"; + static final String PROPERTY_CAN_RECEIVE = "canReceive"; + static final String PROPERTY_CAN_SEND = "canSend"; + static final String SCENES = "scenes"; + static final String CUSTOM_NAME = "customName"; + static final String DEVICE_MODEL = "model"; + static final String DEVICE_TYPE = "deviceType"; + static final String PROPERTY_RELATION_ID = "relationId"; + + static final String COLOR_TEMPERATURE_CAPABILITY = "colorTemperature"; + + static final String TEMPLATE_LIGHT_PRESET_BRIGHT = "/json/light-presets/bright.json"; + static final String TEMPLATE_LIGHT_PRESET_SLOWDOWN = "/json/light-presets/slowdown.json"; + static final String TEMPLATE_LIGHT_PRESET_SMOOTH = "/json/light-presets/smooth.json"; + static final String TEMPLATE_LIGHT_PRESET_WARM = "/json/light-presets/warm.json"; + static final String TEMPLATE_SENSOR_ALWQAYS_ON = "/json/sensor-config/always-on.json"; + static final String TEMPLATE_SENSOR_DURATION_UPDATE = "/json/sensor-config/duration-update.json"; + static final String TEMPLATE_SENSOR_FOLLOW_SUN = "/json/sensor-config/follow-sun.json"; + static final String TEMPLATE_SENSOR_SCHEDULE_ON = "/json/sensor-config/schedule-on.json"; + static final String TEMPLATE_CLICK_SCENE = "/json/scenes/click-scene.json"; + static final String TEMPLATE_COORDINATES = "/json/gateway/coordinates.json"; + static final String TEMPLATE_NULL_COORDINATES = "/json/gateway/null-coordinates.json"; + + /** + * Get structure model as JSON String. + * + * @see json channel + * @return JSON String + */ + String getModelString(); + + /** + * Model update will be performed with API request. Relative expensive operation depending on number of connected + * devices. Call triggers + * - startup + * - add / remove device to DIRIGERA gateway, not openHAB + * - custom name changes for Discovery updates + */ + int update(); + + /** + * Starts a new detection without model update. If handlers are removed they shall appear in discovery again. + */ + void detection(); + + /** + * Get all id's for a specific type. Used to identify link candidates for a specific device. + * - LightController needs lights and plugs and vice versa + * - BlindController needs blinds and vice versa + * - SoundController needs speakers and vice versa + * + * @param types as list of types to query + * @return list of matching device id's + */ + List getDevicesForTypes(List types); + + /** + * Returns a list of all device id's. + * + * @return list of all connected devices + */ + List getAllDeviceIds(); + + /** + * Returns a list with resolved relation id's. There are complex device registering more than one id with different + * type. This binding combines them in one handler. + * - MotionLightHandler + * - DoubleShortcutControllerHandler + * + * @return list of device id's without related devices + */ + List getResolvedDeviceList(); + + /** + * Get all stored information for one device or scene. + * + * @param id to query + * @param type device or scene + * @return data as JSON + */ + JSONObject getAllFor(String id, String type); + + /** + * Gets all relations marked into relationId property + * Rationale: + * VALLHORN Motion Sensor registers 2 devices + * - Motion Sensor + * - Light Sensor + * + * Shortcut Controller with 2 buttons registers 2 controllers + * They shall not be splitted in 2 different things so one Thing shall receive updates for both id's + * + * Use TreeMap to sort device id's so suffix _1 comes before _2 + * + * @param relationId + * @return List of id's with same serial number + */ + TreeMap getRelations(String relationId); + + /** + * Get relationId for a given device id + * + * @param id to check + * @return same id if no relations are found or relationId + */ + String getRelationId(String id); + + /** + * Identify device which is present in model with openHAB ThingTypeUID. + * + * @param id to identify + * @return ThingTypeUID + */ + ThingTypeUID identifyDeviceFromModel(String id); + + /** + * Check if given id is present in devices or scenes. + * + * @param id to check + * @return true if id is found + */ + boolean has(String id); + + /** + * Get the custom name configured in IKEA Smart home app. + * + * @param id to query + * @return name as String + */ + String getCustonNameFor(String id); + + /** + * Properties Map for Discovery + * + * @param id to query + * @return Map with attributes for Thing properties + */ + Map getPropertiesFor(String id); + + /** + * Read a resource file from this bundle. Some presets and commands sent to API shall not be implemented + * in code if they are just needing minor String replacements. + * Root path in project is src/main/resources. Line breaks and white spaces will + * + * @return + */ + String getTemplate(String name); +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/PowerListener.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/PowerListener.java new file mode 100644 index 00000000000..bdcd52325b1 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/interfaces/PowerListener.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.interfaces; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.OnOffType; + +/** + * {@link PowerListener} for notifications of device power events + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public interface PowerListener { + + /** + * Informs if power state of device has changed. + * + * @param power new power state + * @param requested flag showing if new power state was requested by OH user command or from outside (e.g wall + * mounted switch) + */ + void powerChanged(OnOffType power, boolean requested); +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/model/ColorModel.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/model/ColorModel.java new file mode 100644 index 00000000000..5651911f57f --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/model/ColorModel.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.model; + +import java.util.Map.Entry; +import java.util.TreeMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.util.ColorUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ColorModel} converts colors according to DIRIGERA values. openHAB ColorUtil conversion uses XY + * transformations which visually are not matching e.g. for kelvin2HSB values. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class ColorModel { + private static final Logger LOGGER = LoggerFactory.getLogger(ColorModel.class); + + private static final TreeMap MAPPING_RGB_TEMPERETATURE = new TreeMap<>(); + private static final TreeMap MAPPING_TEMPERETATURE_RGB = new TreeMap<>(); + private static final int MAX_HUE = 360; + private static final int MAX_SAT = 100; + + /** + * Simulate color-temperature if color light doesn't have the "canReceive" "colorTemperature" capability + * https://www.npmjs.com/package/color-temperature?activeTab=code + * + * @param kelvin + * @return color temperature as HSBType + */ + private static int[] kelvin2RGB(long kelvin) { + double temperature = kelvin / 100.0; + double red; + double green; + double blue; + /* Calculate red */ + if (temperature <= 66.0) { + red = 255; + } else { + red = temperature - 60.0; + red = 329.698727446 * Math.pow(red, -0.1332047592); + if (red < 0) { + red = 0; + } + if (red > 255) { + red = 255; + } + } + /* Calculate green */ + if (temperature <= 66.0) { + green = temperature; + green = 99.4708025861 * Math.log(green) - 161.1195681661; + if (green < 0) { + green = 0; + } + if (green > 255) { + green = 255; + } + } else { + green = temperature - 60.0; + green = 288.1221695283 * Math.pow(green, -0.0755148492); + if (green < 0) { + green = 0; + } + if (green > 255) { + green = 255; + } + } + /* Calculate blue */ + if (temperature >= 66.0) { + blue = 255; + } else { + if (temperature <= 19.0) { + blue = 0; + } else { + blue = temperature - 10; + blue = 138.5177312231 * Math.log(blue) - 305.0447927307; + if (blue < 0) { + blue = 0; + } + if (blue > 255) { + blue = 255; + } + } + } + return new int[] { (int) Math.round(red), (int) Math.round(green), (int) Math.round(blue) }; + } + + private static void init() { + if (MAPPING_RGB_TEMPERETATURE.isEmpty()) { + for (int i = 1000; i < 10001; i = i + 10) { + int rgbEncoding = encodeRGBValue(kelvin2RGB(i)); + MAPPING_RGB_TEMPERETATURE.put(rgbEncoding, i); + MAPPING_TEMPERETATURE_RGB.put(i, rgbEncoding); + } + } + } + + private static int encodeRGBValue(int[] rgb) { + return rgb[0] * 1000000 + rgb[1] * 1000 + rgb[2]; + } + + private static int[] decodeRGBValue(int encoded) { + int part = encoded; + int red = part / 1000000; + part -= red * 1000000; + int green = part / 1000; + part -= green * 1000; + int blue = part; + return new int[] { red, green, blue }; + } + + public static HSBType kelvin2Hsb(long kelvin) { + init(); + Entry entry = MAPPING_TEMPERETATURE_RGB.ceilingEntry((int) kelvin); + if (entry == null) { + entry = MAPPING_TEMPERETATURE_RGB.floorEntry((int) kelvin); + if (entry == null) { + // this path cannot be entered if tables isn't empty which is prevent by init call + LOGGER.warn("DIRIGERA COLOR_MODEL no rgb mapping found for {}", kelvin); + return new HSBType(); + } + } + int encoded = entry.getValue(); + int[] rgb = decodeRGBValue(encoded); + return ColorUtil.rgbToHsb(rgb); + } + + public static long hsb2Kelvin(HSBType hsb) { + init(); + HSBType compare = new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED); + int rgb[] = ColorUtil.hsbToRgb(compare); + int key = encodeRGBValue(rgb); + Entry entry = MAPPING_RGB_TEMPERETATURE.ceilingEntry(key); + if (entry == null) { + entry = MAPPING_RGB_TEMPERETATURE.floorEntry(key); + if (entry == null) { + // this path cannot be entered if tables isn't empty which is prevent by init call + LOGGER.warn("DIRIGERA COLOR_MODEL no kelvin mapping found for {}", compare); + return -1; + } + } + return entry.getValue(); + } + + public static boolean closeTo(HSBType refHSB, HSBType compareHSB, double percent) { + double hueDistance = Math.abs(refHSB.getHue().doubleValue() - compareHSB.getHue().doubleValue()); + double saturationDistance = Math + .abs(refHSB.getSaturation().doubleValue() - compareHSB.getSaturation().doubleValue()); + return ((hueDistance < (MAX_HUE * percent) || hueDistance > (MAX_HUE - (MAX_HUE * percent))) + && saturationDistance < (MAX_SAT * percent)); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/model/DirigeraModel.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/model/DirigeraModel.java new file mode 100644 index 00000000000..6feeb594f9d --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/model/DirigeraModel.java @@ -0,0 +1,563 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.model; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.TreeMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.json.JSONArray; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.interfaces.DirigeraAPI; +import org.openhab.binding.dirigera.internal.interfaces.Gateway; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.framework.Bundle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link DirigeraModel} is representing the structural data of the devices connected to gateway. Concrete values of + * devices shall not be accessed. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class DirigeraModel implements Model { + private final Logger logger = LoggerFactory.getLogger(DirigeraModel.class); + + private Map resultMap = new HashMap<>(); + private Map templates = new HashMap<>(); + private List devices = new ArrayList<>(); + private JSONObject model = new JSONObject(); + private Gateway gateway; + + public DirigeraModel(Gateway gateway) { + this.gateway = gateway; + } + + @Override + public synchronized String getModelString() { + return model.toString(); + } + + @Override + public synchronized int update() { + Instant startTime = Instant.now(); + JSONObject home = gateway.api().readHome(); + // call finished with error code ... + if (home.has(DirigeraAPI.HTTP_ERROR_FLAG)) { + int status = home.getInt(DirigeraAPI.HTTP_ERROR_STATUS); + logger.warn("DIRIGERA MODEL received model with error code {} - don't take it", status); + return status; + } else if (home.isEmpty()) { + // ... call finished with unchecked exception ... + return 500; + } else { + // ... call finished with success + model = home; + detection(); + } + logger.trace("DIRIGERA MODEL full update {} ms", Duration.between(startTime, Instant.now()).toMillis()); + return 200; + } + + @Override + public synchronized void detection() { + if (gateway.discoveryEnabled()) { + List previousDevices = new ArrayList<>(); + previousDevices.addAll(devices); + + // first get devices + List foundDevices = new ArrayList<>(); + foundDevices.addAll(getResolvedDeviceList()); + foundDevices.addAll(getAllSceneIds()); + devices.clear(); + devices.addAll(foundDevices); + previousDevices.forEach(deviceId -> { + boolean known = gateway.isKnownDevice(deviceId); + boolean removed = !foundDevices.contains(deviceId); + if (removed) { + removedDeviceScene(deviceId); + } else { + if (!known) { + addedDeviceScene(deviceId); + } // don't update known devices + } + }); + foundDevices.removeAll(previousDevices); + foundDevices.forEach(deviceId -> { + boolean known = gateway.isKnownDevice(deviceId); + if (!known) { + addedDeviceScene(deviceId); + } + }); + } + } + + /** + * Returns list with resolved relations + * + * @return + */ + @Override + public synchronized List getResolvedDeviceList() { + List deviceList = new ArrayList<>(); + if (!model.isNull(PROPERTY_DEVICES)) { + JSONArray devices = model.getJSONArray(PROPERTY_DEVICES); + Iterator entries = devices.iterator(); + while (entries.hasNext()) { + JSONObject entry = (JSONObject) entries.next(); + String deviceId = entry.getString(PROPERTY_DEVICE_ID); + String relationId = getRelationId(deviceId); + if (!deviceId.equals(relationId)) { + TreeMap relationMap = getRelations(relationId); + // store for complex devices store result with first found id + relationId = relationMap.firstKey(); + } + if (!deviceList.contains(relationId)) { + deviceList.add(relationId); + } + } + } + return deviceList; + } + + /** + * Returns list with all device id's + * + * @return + */ + @Override + public synchronized List getAllDeviceIds() { + List deviceList = new ArrayList<>(); + if (!model.isNull(PROPERTY_DEVICES)) { + JSONArray devices = model.getJSONArray(PROPERTY_DEVICES); + Iterator entries = devices.iterator(); + while (entries.hasNext()) { + JSONObject entry = (JSONObject) entries.next(); + deviceList.add(entry.getString(PROPERTY_DEVICE_ID)); + } + } + return deviceList; + } + + private List getAllSceneIds() { + List sceneList = new ArrayList<>(); + if (!model.isNull(SCENES)) { + JSONArray scenes = model.getJSONArray(SCENES); + Iterator sceneIterator = scenes.iterator(); + while (sceneIterator.hasNext()) { + JSONObject entry = (JSONObject) sceneIterator.next(); + if (entry.has(PROPERTY_TYPE)) { + if ("userScene".equals(entry.getString(PROPERTY_TYPE))) { + if (entry.has(PROPERTY_DEVICE_ID)) { + String id = entry.getString(PROPERTY_DEVICE_ID); + sceneList.add(id); + } + } + } + } + } + return sceneList; + } + + private void addedDeviceScene(String id) { + DiscoveryResult result = identifiy(id); + if (result != null) { + gateway.discovery().deviceDiscovered(result); + resultMap.put(id, result); + } + } + + private void removedDeviceScene(String id) { + DiscoveryResult deliveredResult = resultMap.remove(id); + if (deliveredResult != null) { + gateway.discovery().deviceRemoved(deliveredResult); + } + // inform gateway to remove device and update handler accordingly + gateway.deleteDevice(id); + } + + @Override + public synchronized List getDevicesForTypes(List types) { + List candidates = new ArrayList<>(); + types.forEach(type -> { + JSONArray addons = getIdsForType(type); + addons.forEach(entry -> { + candidates.add(entry.toString()); + }); + }); + return candidates; + } + + private JSONArray getIdsForType(String type) { + JSONArray returnArray = new JSONArray(); + if (!model.isNull(PROPERTY_DEVICES)) { + JSONArray devices = model.getJSONArray(PROPERTY_DEVICES); + Iterator entries = devices.iterator(); + while (entries.hasNext()) { + JSONObject entry = (JSONObject) entries.next(); + if (!entry.isNull(PROPERTY_DEVICE_TYPE) && !entry.isNull(PROPERTY_DEVICE_ID)) { + if (type.equals(entry.get(PROPERTY_DEVICE_TYPE))) { + returnArray.put(entry.get(PROPERTY_DEVICE_ID)); + } + } + } + } + return returnArray; + } + + private boolean hasAttribute(String id, String attribute) { + JSONObject deviceObject = getAllFor(id, PROPERTY_DEVICES); + if (deviceObject.has(ATTRIBUTES)) { + JSONObject attributes = deviceObject.getJSONObject(ATTRIBUTES); + return attributes.has(attribute); + } + return false; + } + + @Override + public synchronized JSONObject getAllFor(String id, String type) { + JSONObject returnObject = new JSONObject(); + if (model.has(type)) { + JSONArray devices = model.getJSONArray(type); + Iterator entries = devices.iterator(); + while (entries.hasNext()) { + JSONObject entry = (JSONObject) entries.next(); + if (id.equals(entry.get(PROPERTY_DEVICE_ID))) { + return entry; + } + } + } + return returnObject; + } + + @Override + public synchronized String getCustonNameFor(String id) { + JSONObject deviceObject = getAllFor(id, PROPERTY_DEVICES); + if (deviceObject.has(ATTRIBUTES)) { + JSONObject attributes = deviceObject.getJSONObject(ATTRIBUTES); + if (attributes.has(CUSTOM_NAME)) { + String customName = attributes.getString(CUSTOM_NAME); + if (!customName.isBlank()) { + return customName; + } + } + if (attributes.has(DEVICE_MODEL)) { + String deviceModel = attributes.getString(DEVICE_MODEL); + if (!deviceModel.isBlank()) { + return deviceModel; + } + } + if (deviceObject.has(DEVICE_TYPE)) { + return deviceObject.getString(DEVICE_TYPE); + } + // 3 fallback options + } + // not found yet - check scenes + JSONObject sceneObject = getAllFor(id, PROPERTY_SCENES); + if (sceneObject.has("info")) { + JSONObject info = sceneObject.getJSONObject("info"); + if (info.has("name")) { + String name = info.getString("name"); + if (!name.isBlank()) { + return name; + } + } + } + + return id; + } + + @Override + public synchronized Map getPropertiesFor(String id) { + final Map properties = new HashMap<>(); + JSONObject deviceObject = getAllFor(id, PROPERTY_DEVICES); + // get manufacturer, model and version data + if (deviceObject.has(ATTRIBUTES)) { + JSONObject attributes = deviceObject.getJSONObject(ATTRIBUTES); + THING_PROPERTIES.forEach(property -> { + if (attributes.has(property)) { + properties.put(property, attributes.get(property)); + } + }); + } + // put id in as representation property + properties.put(PROPERTY_DEVICE_ID, id); + // add capabilities + if (deviceObject.has(CAPABILITIES)) { + JSONObject capabilities = deviceObject.getJSONObject(CAPABILITIES); + if (capabilities.has(PROPERTY_CAN_RECEIVE)) { + properties.put(PROPERTY_CAN_RECEIVE, capabilities.getJSONArray(PROPERTY_CAN_RECEIVE)); + } + if (capabilities.has(PROPERTY_CAN_SEND)) { + properties.put(PROPERTY_CAN_SEND, capabilities.getJSONArray(PROPERTY_CAN_SEND)); + } + } + + return properties; + } + + @Override + public synchronized TreeMap getRelations(String relationId) { + final TreeMap relationsMap = new TreeMap<>(); + List allDevices = getAllDeviceIds(); + allDevices.forEach(deviceId -> { + JSONObject data = getAllFor(deviceId, PROPERTY_DEVICES); + if (data.has(Model.PROPERTY_RELATION_ID)) { + String relation = data.getString(Model.PROPERTY_RELATION_ID); + if (relationId.equals(relation)) { + String relationDeviceId = data.getString(PROPERTY_DEVICE_ID); + String deviceType = data.getString(PROPERTY_DEVICE_TYPE); + if (relationDeviceId != null && deviceType != null) { + relationsMap.put(relationDeviceId, deviceType); + } + } + } + }); + return relationsMap; + } + + private @Nullable DiscoveryResult identifiy(String id) { + ThingTypeUID ttuid = identifyDeviceFromModel(id); + // don't report gateway, unknown devices and light sensors connected to motion sensors + if (!THING_TYPE_GATEWAY.equals(ttuid) && !THING_TYPE_UNKNNOWN.equals(ttuid) + && !THING_TYPE_LIGHT_SENSOR.equals(ttuid) && !THING_TYPE_IGNORE.equals(ttuid)) { + // check if it's a simple or complex device + String relationId = getRelationId(id); + String firstDeviceId = id; + if (!id.equals(relationId)) { + // complex device + TreeMap relationMap = getRelations(relationId); + // take name from first ordered entry + firstDeviceId = relationMap.firstKey(); + } + // take name and properties from first found id + String customName = getCustonNameFor(firstDeviceId); + Map propertiesMap = getPropertiesFor(firstDeviceId); + return DiscoveryResultBuilder.create(new ThingUID(ttuid, gateway.getThing().getUID(), firstDeviceId)) + .withBridge(gateway.getThing().getUID()).withProperties(propertiesMap) + .withRepresentationProperty(PROPERTY_DEVICE_ID).withLabel(customName).build(); + } + return null; + } + + /** + * Identify device which is present in model + * + * @param id + * @return + */ + @Override + public synchronized ThingTypeUID identifyDeviceFromModel(String id) { + JSONObject entry = getAllFor(id, PROPERTY_DEVICES); + if (entry.isEmpty()) { + entry = getAllFor(id, PROPERTY_SCENES); + } + if (entry.isEmpty()) { + return THING_TYPE_NOT_FOUND; + } else { + return identifyDeviceFromJSON(id, entry); + } + } + + private ThingTypeUID identifyDeviceFromJSON(String id, JSONObject data) { + String typeDeviceType = ""; + if (data.has(Model.PROPERTY_RELATION_ID)) { + return identifiyComplexDevice(data.getString(Model.PROPERTY_RELATION_ID)); + } else if (data.has(PROPERTY_DEVICE_TYPE)) { + String deviceType = data.getString(PROPERTY_DEVICE_TYPE); + typeDeviceType = deviceType; + switch (deviceType) { + case DEVICE_TYPE_GATEWAY: + return THING_TYPE_GATEWAY; + case DEVICE_TYPE_LIGHT: + if (data.has(CAPABILITIES)) { + JSONObject capabilities = data.getJSONObject(CAPABILITIES); + List capabilityList = new ArrayList<>(); + if (capabilities.has(PROPERTY_CAN_RECEIVE)) { + JSONArray receiveProperties = capabilities.getJSONArray(PROPERTY_CAN_RECEIVE); + receiveProperties.forEach(capability -> { + capabilityList.add(capability.toString()); + }); + } + if (capabilityList.contains("colorHue")) { + return THING_TYPE_COLOR_LIGHT; + } else if (capabilityList.contains("colorTemperature")) { + return THING_TYPE_TEMPERATURE_LIGHT; + } else if (capabilityList.contains("lightLevel")) { + return THING_TYPE_DIMMABLE_LIGHT; + } else if (capabilityList.contains("isOn")) { + return THING_TYPE_SWITCH_LIGHT; + } else { + logger.warn("DIRIGERA MODEL cannot identify light {}", data); + } + } else { + logger.warn("DIRIGERA MODEL cannot identify light {}", data); + } + break; + case DEVICE_TYPE_MOTION_SENSOR: + return THING_TYPE_MOTION_SENSOR; + case DEVICE_TYPE_LIGHT_SENSOR: + return THING_TYPE_LIGHT_SENSOR; + case DEVICE_TYPE_CONTACT_SENSOR: + return THING_TYPE_CONTACT_SENSOR; + case DEVICE_TYPE_OUTLET: + if (hasAttribute(id, "currentActivePower")) { + return THING_TYPE_SMART_PLUG; + } else if (hasAttribute(id, "childLock")) { + return THING_TYPE_POWER_PLUG; + } else { + return THING_TYPE_SIMPLE_PLUG; + } + case DEVICE_TYPE_SPEAKER: + return THING_TYPE_SPEAKER; + case DEVICE_TYPE_REPEATER: + return THING_TYPE_REPEATER; + case DEVICE_TYPE_LIGHT_CONTROLLER: + return THING_TYPE_LIGHT_CONTROLLER; + case DEVICE_TYPE_ENVIRONMENT_SENSOR: + return THING_TYPE_AIR_QUALITY; + case DEVICE_TYPE_WATER_SENSOR: + return THING_TYPE_WATER_SENSOR; + case DEVICE_TYPE_AIR_PURIFIER: + return THING_TYPE_AIR_PURIFIER; + case DEVICE_TYPE_BLINDS: + return THING_TYPE_BLIND; + case DEVICE_TYPE_BLIND_CONTROLLER: + return THING_TYPE_BLIND_CONTROLLER; + case DEVICE_TYPE_SOUND_CONTROLLER: + return THING_TYPE_SOUND_CONTROLLER; + case DEVICE_TYPE_SHORTCUT_CONTROLLER: + return THING_TYPE_SINGLE_SHORTCUT_CONTROLLER; + } + } else { + // device type is empty, check for scene + if (!data.isNull(PROPERTY_TYPE)) { + String type = data.getString(PROPERTY_TYPE); + typeDeviceType = type + "/" + typeDeviceType; // just for logging + switch (type) { + case TYPE_USER_SCENE: + return THING_TYPE_SCENE; + case TYPE_CUSTOM_SCENE: + return THING_TYPE_IGNORE; + } + } + } + logger.warn("DIRIGERA MODEL Unsupported device {} with data {} {}", typeDeviceType, data, id); + return THING_TYPE_UNKNNOWN; + } + + private ThingTypeUID identifiyComplexDevice(String relationId) { + Map relationsMap = getRelations(relationId); + if (relationsMap.size() == 2 && relationsMap.containsValue("lightSensor") + && relationsMap.containsValue("motionSensor")) { + return THING_TYPE_MOTION_LIGHT_SENSOR; + } else if (relationsMap.size() == 2 && relationsMap.containsValue("shortcutController")) { + for (Iterator iterator = relationsMap.keySet().iterator(); iterator.hasNext();) { + if (!"shortcutController".equals(relationsMap.get(iterator.next()))) { + return THING_TYPE_UNKNNOWN; + } + } + return THING_TYPE_DOUBLE_SHORTCUT_CONTROLLER; + } else if (relationsMap.size() == 1 && relationsMap.containsValue("gatewy")) { + return THING_TYPE_GATEWAY; + } else { + return THING_TYPE_UNKNNOWN; + } + } + + /** + * Get relationId for a given device id + * + * @param id to check + * @return same id if no relations are found or relationId + */ + @Override + public synchronized String getRelationId(String id) { + JSONObject dataObject = getAllFor(id, PROPERTY_DEVICES); + if (dataObject.has(PROPERTY_RELATION_ID)) { + return dataObject.getString(PROPERTY_RELATION_ID); + } + return id; + } + + /** + * Check if given id is present in devices or scenes + * + * @param id to check + * @return true if id is found + */ + @Override + public synchronized boolean has(String id) { + return getAllDeviceIds().contains(id) || getAllSceneIds().contains(id); + } + + @Override + public String getTemplate(String name) { + String template = templates.get(name); + if (template == null) { + template = getResourceFile(name); + if (!template.isBlank()) { + templates.put(name, template); + } else { + logger.warn("DIRIGERA MODEL empty template for {}", name); + template = "{}"; + } + } + return template; + } + + private String getResourceFile(String fileName) { + try { + Bundle myself = gateway.getBundleContext().getBundle(); + // do this check for unit tests to avoid NullPointerException + if (myself != null) { + URL url = myself.getResource(fileName); + InputStream input = url.openStream(); + // https://www.baeldung.com/java-scanner-usedelimiter + try (Scanner scanner = new Scanner(input).useDelimiter("\\A")) { + String result = scanner.hasNext() ? scanner.next() : ""; + String resultReplaceAll = result.replaceAll("[\\n\\r\\s]", ""); + scanner.close(); + return resultReplaceAll; + } + } else { + // only unit testing + return Files.readString(Paths.get("src/main/resources" + fileName)); + } + } catch (IOException e) { + logger.warn("DIRIGERA MODEL no template found for {}", fileName); + } + return ""; + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/network/DirigeraAPIImpl.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/network/DirigeraAPIImpl.java new file mode 100644 index 00000000000..018822f703c --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/network/DirigeraAPIImpl.java @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.network; + +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.nio.charset.StandardCharsets; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +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.HttpHeader; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.interfaces.DirigeraAPI; +import org.openhab.binding.dirigera.internal.interfaces.Gateway; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.RawType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link DirigeraAPIImpl} provides easy access towards REST API + * + * @author Bernd Weymann - Initial contribution + */ +@WebSocket +@NonNullByDefault +public class DirigeraAPIImpl implements DirigeraAPI { + private final Logger logger = LoggerFactory.getLogger(DirigeraAPIImpl.class); + + private static final String GENREAL_LOCK = "lock"; + private Set activeCallers = new TreeSet<>(); + private HttpClient httpClient; + private Gateway gateway; + + public DirigeraAPIImpl(HttpClient httpClient, Gateway gateway) { + this.httpClient = httpClient; + this.gateway = gateway; + } + + private Request addAuthorizationHeader(Request sourceRequest) { + if (!gateway.getToken().isBlank()) { + return sourceRequest.header(HttpHeader.AUTHORIZATION, "Bearer " + gateway.getToken()); + } else { + logger.warn("DIRIGERA API Cannot operate with token {}", gateway.getToken()); + return sourceRequest; + } + } + + @Override + public JSONObject readHome() { + String url = String.format(HOME_URL, gateway.getIpAddress()); + JSONObject statusObject = new JSONObject(); + startCalling(GENREAL_LOCK); + try { + Request homeRequest = httpClient.newRequest(url); + ContentResponse response = addAuthorizationHeader(homeRequest).timeout(10, TimeUnit.SECONDS).send(); + int responseStatus = response.getStatus(); + if (responseStatus == 200) { + statusObject = new JSONObject(response.getContentAsString()); + } else { + statusObject = getErrorJson(responseStatus, response.getReason()); + } + return statusObject; + } catch (InterruptedException | TimeoutException | ExecutionException | JSONException e) { + logger.warn("DIRIGERA API Exception calling {}", url); + statusObject = getErrorJson(500, e.getMessage()); + return statusObject; + } finally { + endCalling(GENREAL_LOCK); + } + } + + @Override + public JSONObject readDevice(String deviceId) { + String url = String.format(DEVICE_URL, gateway.getIpAddress(), deviceId); + JSONObject statusObject = new JSONObject(); + startCalling(deviceId); + try { + Request homeRequest = httpClient.newRequest(url); + ContentResponse response = addAuthorizationHeader(homeRequest).timeout(10, TimeUnit.SECONDS).send(); + int responseStatus = response.getStatus(); + if (responseStatus == 200) { + statusObject = new JSONObject(response.getContentAsString()); + } else { + statusObject = getErrorJson(responseStatus, response.getReason()); + } + return statusObject; + } catch (InterruptedException | TimeoutException | ExecutionException | JSONException e) { + logger.warn("DIRIGERA API Exception calling {}", url); + statusObject = getErrorJson(500, e.getMessage()); + return statusObject; + } finally { + endCalling(deviceId); + } + } + + @Override + public void triggerScene(String sceneId, String trigger) { + String url = String.format(SCENE_URL, gateway.getIpAddress(), sceneId) + "/" + trigger; + startCalling(sceneId); + try { + Request homeRequest = httpClient.POST(url); + ContentResponse response = addAuthorizationHeader(homeRequest).timeout(10, TimeUnit.SECONDS).send(); + int responseStatus = response.getStatus(); + if (responseStatus != 200 && responseStatus != 202) { + logger.warn("DIRIGERA API Scene trigger failed with {}", responseStatus); + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("DIRIGERA API Exception calling {}", url); + } finally { + endCalling(sceneId); + } + } + + @Override + public int sendAttributes(String id, JSONObject attributes) { + JSONObject data = new JSONObject(); + data.put(Model.ATTRIBUTES, attributes); + return sendPatch(id, data); + } + + @Override + public int sendPatch(String id, JSONObject data) { + String url = String.format(DEVICE_URL, gateway.getIpAddress(), id); + // pack attributes into data json and then into an array + JSONArray dataArray = new JSONArray(); + dataArray.put(data); + StringContentProvider stringProvider = new StringContentProvider("application/json", dataArray.toString(), + StandardCharsets.UTF_8); + Request deviceRequest = httpClient.newRequest(url).method("PATCH") + .header(HttpHeader.CONTENT_TYPE, "application/json").content(stringProvider); + + int responseStatus = 500; + startCalling(id); + try { + ContentResponse response = addAuthorizationHeader(deviceRequest).timeout(10, TimeUnit.SECONDS).send(); + responseStatus = response.getStatus(); + if (responseStatus == 200 || responseStatus == 202) { + logger.debug("DIRIGERA API send finished {} with {} {}", url, dataArray, responseStatus); + } else { + logger.warn("DIRIGERA API send failed {} with {} {}", url, dataArray, responseStatus); + } + return responseStatus; + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("DIRIGERA API send failed {} failed {} {}", url, dataArray, e.getMessage()); + return responseStatus; + } finally { + endCalling(id); + } + } + + @Override + public State getImage(String imageURL) { + State image = UnDefType.UNDEF; + startCalling(GENREAL_LOCK); + try { + ContentResponse response = httpClient.GET(imageURL); + if (response.getStatus() == 200) { + String mimeType = response.getMediaType(); + if (mimeType == null) { + mimeType = RawType.DEFAULT_MIME_TYPE; + } + image = new RawType(response.getContent(), mimeType); + } else { + logger.warn("DIRIGERA API call to {} failed {}", imageURL, response.getStatus()); + } + return image; + } catch (InterruptedException | ExecutionException | TimeoutException e) { + logger.warn("DIRIGERA API call to {} failed {}", imageURL, e.getMessage()); + return image; + } finally { + endCalling(GENREAL_LOCK); + } + } + + @Override + public JSONObject readScene(String sceneId) { + String url = String.format(SCENE_URL, gateway.getIpAddress(), sceneId); + JSONObject statusObject = new JSONObject(); + Request homeRequest = httpClient.newRequest(url); + startCalling(GENREAL_LOCK); + try { + ContentResponse response = addAuthorizationHeader(homeRequest).timeout(10, TimeUnit.SECONDS).send(); + int responseStatus = response.getStatus(); + if (responseStatus == 200) { + statusObject = new JSONObject(response.getContentAsString()); + } else { + statusObject = getErrorJson(responseStatus, response.getReason()); + } + return statusObject; + } catch (InterruptedException | TimeoutException | ExecutionException | JSONException e) { + logger.warn("DIRIGERA API Exception calling {}", url); + statusObject = getErrorJson(-1, e.getMessage()); + return statusObject; + } finally { + endCalling(GENREAL_LOCK); + } + } + + @Override + public String createScene(String uuid, String clickPattern, String controllerId) { + String url = String.format(SCENES_URL, gateway.getIpAddress()); + String sceneTemplate = gateway.model().getTemplate(Model.TEMPLATE_CLICK_SCENE); + String payload = String.format(sceneTemplate, uuid, "openHAB Shortcut Proxy", clickPattern, "0", controllerId); + StringContentProvider stringProvider = new StringContentProvider("application/json", payload, + StandardCharsets.UTF_8); + Request sceneCreateRequest = httpClient.newRequest(url).method("POST") + .header(HttpHeader.CONTENT_TYPE, "application/json").content(stringProvider); + + int responseStatus = 500; + String responseUUID = ""; + int retryCounter = 3; + startCalling(GENREAL_LOCK); + try { + while (retryCounter > 0 && !uuid.equals(responseUUID)) { + try { + ContentResponse response = addAuthorizationHeader(sceneCreateRequest).timeout(10, TimeUnit.SECONDS) + .send(); + responseStatus = response.getStatus(); + if (responseStatus == 200 || responseStatus == 202) { + logger.debug("DIRIGERA API send {} to {} delivered", payload, url); + String responseString = response.getContentAsString(); + JSONObject responseJSON = new JSONObject(responseString); + responseUUID = responseJSON.getString(PROPERTY_DEVICE_ID); + break; + } else { + logger.warn("DIRIGERA API send {} to {} failed with status {}", payload, url, + response.getStatus()); + } + } catch (InterruptedException | TimeoutException | ExecutionException | JSONException e) { + logger.warn("DIRIGERA API call to {} failed {}", url, e.getMessage()); + } + logger.debug("DIRIGERA API createScene failed {} retries remaining", retryCounter); + retryCounter--; + } + return responseUUID; + } finally { + endCalling(GENREAL_LOCK); + } + } + + @Override + public void deleteScene(String uuid) { + String url = String.format(SCENES_URL, gateway.getIpAddress()) + "/" + uuid; + Request sceneDeleteRequest = httpClient.newRequest(url).method("DELETE"); + int responseStatus = 500; + int retryCounter = 3; + startCalling(GENREAL_LOCK); + try { + while (retryCounter > 0 && responseStatus != 200 && responseStatus != 202) { + try { + ContentResponse response = addAuthorizationHeader(sceneDeleteRequest).timeout(10, TimeUnit.SECONDS) + .send(); + responseStatus = response.getStatus(); + if (responseStatus == 200 || responseStatus == 202) { + logger.debug("DIRIGERA API delete {} performed", url); + break; + } else { + logger.warn("DIRIGERA API send {} failed with status {}", url, response.getStatus()); + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("DIRIGERA API call to {} failed {}", url, e.getMessage()); + } + logger.debug("DIRIGERA API deleteScene failed with status {}, {} retries remaining", responseStatus, + retryCounter); + retryCounter--; + } + } finally { + endCalling(GENREAL_LOCK); + } + } + + public JSONObject getErrorJson(int status, @Nullable String message) { + String error = String.format( + "{\"http-error-flag\":true,\"http-error-status\":%s,\"http-error-message\":\"%s\"}", status, message); + return new JSONObject(error); + } + + private void startCalling(String uuid) { + synchronized (this) { + while (activeCallers.contains(uuid)) { + try { + this.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + // abort execution + return; + } + } + activeCallers.add(uuid); + } + } + + private void endCalling(String uuid) { + synchronized (this) { + activeCallers.remove(uuid); + this.notifyAll(); + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/network/Websocket.java b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/network/Websocket.java new file mode 100644 index 00000000000..93c208ea455 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/java/org/openhab/binding/dirigera/internal/network/Websocket.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.network; + +import static org.openhab.binding.dirigera.internal.Constants.WS_URL; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.websocket.api.extensions.Frame; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.interfaces.Gateway; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link Websocket} listens to device changes + * + * @author Bernd Weymann - Initial contribution + */ +@WebSocket +@NonNullByDefault +public class Websocket { + private final Logger logger = LoggerFactory.getLogger(Websocket.class); + private final Map pingPongMap = new HashMap<>(); + + private static final String STARTS = "starts"; + private static final String STOPS = "stops"; + private static final String DISCONNECTS = "disconnetcs"; + private static final String ERRORS = "errors"; + private static final String PINGS = "pings"; + private static final String PING_LATENCY = "pingLatency"; + private static final String PING_LAST = "lastPing"; + private static final String MESSAGES = "messages"; + public static final String MODEL_UPDATES = "modelUpdates"; + public static final String MODEL_UPDATE_TIME = "modelUpdateDuration"; + public static final String MODEL_UPDATE_LAST = "lastModelUpdate"; + + private Optional websocketClient = Optional.empty(); + private Optional session = Optional.empty(); + private JSONObject statistics = new JSONObject(); + private HttpClient httpClient; + private Gateway gateway; + private boolean disposed = false; + + public Websocket(Gateway gateway, HttpClient httpClient) { + this.gateway = gateway; + this.httpClient = httpClient; + } + + public void initialize() { + disposed = false; + } + + public void start() { + if ("unit-test".equals(gateway.getToken())) { + // handle unit tests online + gateway.websocketConnected(true, "unit test"); + return; + } + if (disposed) { + logger.debug("DIRIGERA WS start rejected, disposed {}", disposed); + return; + } + increase(STARTS); + internalStop(); // don't count this internal stopping + try { + pingPongMap.clear(); + WebSocketClient client = new WebSocketClient(httpClient); + client.setMaxIdleTimeout(0); + + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setHeader("Authorization", "Bearer " + gateway.getToken()); + + String websocketURL = String.format(WS_URL, gateway.getIpAddress()); + logger.trace("DIRIGERA WS start {}", websocketURL); + websocketClient = Optional.of(client); + client.start(); + client.connect(this, new URI(websocketURL), request); + } catch (Exception t) { + // catch Exceptions of start stop and declare communication error + logger.warn("DIRIGERA WS handling exception: {}", t.getMessage()); + } + } + + public boolean isRunning() { + return websocketClient.isPresent() && session.isPresent() && session.get().isOpen(); + } + + public void stop() { + increase(STOPS); + internalStop(); + } + + private void internalStop() { + session.ifPresent(session -> { + session.close(); + }); + websocketClient.ifPresent(client -> { + try { + client.stop(); + client.destroy(); + } catch (Exception e) { + logger.warn("DIRIGERA WS exception stopping running client"); + } + }); + websocketClient = Optional.empty(); + this.session = Optional.empty(); + } + + public void dispose() { + internalStop(); + disposed = true; + } + + public void ping() { + session.ifPresentOrElse((session) -> { + try { + // build ping message + String pingId = UUID.randomUUID().toString(); + pingPongMap.put(pingId, Instant.now()); + session.getRemote().sendPing(ByteBuffer.wrap(pingId.getBytes())); + increase(PINGS); + } catch (IOException e) { + logger.warn("DIRIGERA WS ping failed with exception {}", e.getMessage()); + } + }, () -> { + logger.debug("DIRIGERA WS ping found no session - restart websocket"); + }); + } + + /** + * endpoints + */ + + @OnWebSocketMessage + public void onTextMessage(String message) { + increase(MESSAGES); + gateway.websocketUpdate(message); + } + + @OnWebSocketFrame + public void onFrame(Frame frame) { + if (Frame.Type.PONG.equals(frame.getType())) { + ByteBuffer buffer = frame.getPayload(); + byte[] bytes = new byte[frame.getPayloadLength()]; + for (int i = 0; i < frame.getPayloadLength(); i++) { + bytes[i] = buffer.get(i); + } + String paylodString = new String(bytes); + Instant sent = pingPongMap.remove(paylodString); + if (sent != null) { + long durationMS = Duration.between(sent, Instant.now()).toMillis(); + statistics.put(PING_LATENCY, durationMS); + statistics.put(PING_LAST, Instant.now()); + } else { + logger.debug("DIRIGERA WS receiced pong without ping {}", paylodString); + } + } else if (Frame.Type.PING.equals(frame.getType())) { + session.ifPresentOrElse((session) -> { + logger.trace("DIRIGERA onPing "); + ByteBuffer buffer = frame.getPayload(); + try { + session.getRemote().sendPong(buffer); + } catch (IOException e) { + logger.warn("DIRIGERA WS onPing answer exception {}", e.getMessage()); + } + }, () -> { + logger.debug("DIRIGERA WS onPing answer cannot be initiated"); + }); + } + } + + @OnWebSocketConnect + public void onConnect(Session session) { + logger.debug("DIRIGERA WS onConnect"); + this.session = Optional.of(session); + session.setIdleTimeout(-1); + gateway.websocketConnected(true, "connected"); + } + + @OnWebSocketClose + public void onDisconnect(Session session, int statusCode, String reason) { + logger.debug("DIRIGERA WS onDisconnect Status {} Reason {}", statusCode, reason); + this.session = Optional.empty(); + increase(DISCONNECTS); + gateway.websocketConnected(false, reason); + } + + @OnWebSocketError + public void onError(Throwable t) { + String message = t.getMessage(); + logger.warn("DIRIGERA WS onError {}", message); + this.session = Optional.empty(); + if (message == null) { + message = "unknown"; + } + increase(ERRORS); + gateway.websocketConnected(false, message); + } + + /** + * Helper functions + */ + + public JSONObject getStatistics() { + return statistics; + } + + public void increase(String key) { + if (statistics.has(key)) { + int counter = statistics.getInt(key); + statistics.put(key, ++counter); + } else { + statistics.put(key, 1); + } + } + + public Map getPingPongMap() { + return pingPongMap; + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 00000000000..6b4ad97c347 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,21 @@ + + + + binding + DIRIGERA Binding + IKEA Smarthome binding for DIRIGERA Gateway + local + + + mdns + + + mdnsServiceType + _ihsp._tcp.local. + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/base-device.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/base-device.xml new file mode 100644 index 00000000000..8f91b1304c9 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/base-device.xml @@ -0,0 +1,13 @@ + + + + + + + Unique id of this device + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/color-light.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/color-light.xml new file mode 100644 index 00000000000..aa998734366 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/color-light.xml @@ -0,0 +1,27 @@ + + + + + + + Unique id of this device + + + + Required time for fade sequnce to color or brightness + 750 + + + + Define sequence if several light parameters are changed at once + + + + + 0 + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/gateway.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/gateway.xml new file mode 100644 index 00000000000..45eeb15a857 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/gateway.xml @@ -0,0 +1,22 @@ + + + + + + + Gateway IP Address + + + + Unique id of this gateway + + + + Configure if paired devices shall be detected by discovery + true + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/light-device.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/light-device.xml new file mode 100644 index 00000000000..6a55a46561a --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/config/light-device.xml @@ -0,0 +1,18 @@ + + + + + + + Unique id of this device + + + + Required time for fade sequnce to color or brightness + 750 + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/i18n/dirigera.properties b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/i18n/dirigera.properties new file mode 100644 index 00000000000..1a34c2f8edc --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/i18n/dirigera.properties @@ -0,0 +1,388 @@ +# add-on + +addon.dirigera.name = DIRIGERA Binding +addon.dirigera.description = IKEA Smarthome binding for DIRIGERA Gateway + +# thing types + +thing-type.dirigera.air-purifier.label = Air Purifier +thing-type.dirigera.air-purifier.description = Air cleaning device with particle filter +thing-type.dirigera.air-purifier.channel.fan-mode.label = Fan Mode +thing-type.dirigera.air-purifier.channel.fan-mode.description = Fan on, off, speed or automatic behavior +thing-type.dirigera.air-purifier.channel.fan-runtime.label = Fan Runtime +thing-type.dirigera.air-purifier.channel.fan-runtime.description = Fan runtime in minutes +thing-type.dirigera.air-purifier.channel.fan-speed.label = Fan Speed +thing-type.dirigera.air-purifier.channel.fan-speed.description = Manual regulation of fan speed +thing-type.dirigera.air-purifier.channel.filter-alarm.label = Filter Alarm +thing-type.dirigera.air-purifier.channel.filter-alarm.description = Filter alarm signal +thing-type.dirigera.air-purifier.channel.filter-elapsed.label = Filter Elapsed +thing-type.dirigera.air-purifier.channel.filter-elapsed.description = Filter elapsed time in minutes +thing-type.dirigera.air-purifier.channel.filter-lifetime.label = Filter Lifetime +thing-type.dirigera.air-purifier.channel.filter-lifetime.description = Filter lifetime in minutes +thing-type.dirigera.air-purifier.channel.filter-remain.label = Filter Remain +thing-type.dirigera.air-purifier.channel.filter-remain.description = Remaining filter time in minutes +thing-type.dirigera.air-purifier.channel.particulate-matter.label = Particulate Matter +thing-type.dirigera.air-purifier.channel.particulate-matter.description = Category 2.5 particulate matter +thing-type.dirigera.air-quality.label = Air Quality +thing-type.dirigera.air-quality.description = Air measure for temperature, humidity and particles +thing-type.dirigera.air-quality.channel.humidity.label = Humidity +thing-type.dirigera.air-quality.channel.humidity.description = Atmospheric humidity in percent +thing-type.dirigera.air-quality.channel.particulate-matter.label = Particulate Matter +thing-type.dirigera.air-quality.channel.particulate-matter.description = Category 2.5 particulate matter +thing-type.dirigera.air-quality.channel.temperature.label = Temperature +thing-type.dirigera.air-quality.channel.temperature.description = Current indoor temperature +thing-type.dirigera.air-quality.channel.voc-index.label = VOC Index +thing-type.dirigera.air-quality.channel.voc-index.description = Relative VOC intensity compared to recent history +thing-type.dirigera.blind-controller.label = Blinds Controller +thing-type.dirigera.blind-controller.description = Controller to open and close blinds +thing-type.dirigera.blind-controller.channel.battery-level.label = Battery Charge Level +thing-type.dirigera.blind-controller.channel.battery-level.description = Battery charge level in percent +thing-type.dirigera.blind.label = Blind +thing-type.dirigera.blind.description = Window or door blind +thing-type.dirigera.blind.channel.battery-level.label = Battery Charge Level +thing-type.dirigera.blind.channel.battery-level.description = Battery charge level in percent +thing-type.dirigera.color-light.label = Color Light +thing-type.dirigera.color-light.description = Light with color support +thing-type.dirigera.color-light.channel.brightness.label = Brightness +thing-type.dirigera.color-light.channel.brightness.description = Brightness of light in percent +thing-type.dirigera.color-light.channel.color.label = Color +thing-type.dirigera.color-light.channel.color.description = Color of light with hue, saturation and brightness +thing-type.dirigera.color-light.channel.color-temperature.label = Color Temperature +thing-type.dirigera.color-light.channel.color-temperature.description = Color temperature from cold (0 %) to warm (100 %) +thing-type.dirigera.color-light.channel.power.label = Light Powered +thing-type.dirigera.color-light.channel.power.description = Power state of light +thing-type.dirigera.contact-sensor.label = Contact Sensor +thing-type.dirigera.contact-sensor.description = Sensor tracking if windows or doors are open +thing-type.dirigera.contact-sensor.channel.battery-level.label = Battery Charge Level +thing-type.dirigera.contact-sensor.channel.battery-level.description = Battery charge level in percent +thing-type.dirigera.contact-sensor.channel.contact.label = Contact State +thing-type.dirigera.contact-sensor.channel.contact.description = State if door or window is open or closed +thing-type.dirigera.dimmable-light.label = Dimmable Light +thing-type.dirigera.dimmable-light.description = Light with brightness support +thing-type.dirigera.dimmable-light.channel.brightness.label = Brightness +thing-type.dirigera.dimmable-light.channel.brightness.description = Brightness of light in percent +thing-type.dirigera.dimmable-light.channel.power.label = Light Powered +thing-type.dirigera.dimmable-light.channel.power.description = Power state of light +thing-type.dirigera.double-shortcut.label = Two Button Shortcut +thing-type.dirigera.double-shortcut.description = Shortcut controller with two buttons +thing-type.dirigera.double-shortcut.channel.battery-level.label = Battery Charge Level +thing-type.dirigera.double-shortcut.channel.battery-level.description = Battery charge level in percent +thing-type.dirigera.double-shortcut.channel.button1.label = Button 1 Trigger +thing-type.dirigera.double-shortcut.channel.button1.description = Trigger of first button +thing-type.dirigera.double-shortcut.channel.button2.label = Button 2 Trigger +thing-type.dirigera.double-shortcut.channel.button2.description = Trigger of second button +thing-type.dirigera.gateway.label = DIRIGERA Gateway +thing-type.dirigera.gateway.description = IKEA Gateway for smart products +thing-type.dirigera.gateway.channel.location.label = Home Location +thing-type.dirigera.gateway.channel.location.description = Location in latitude, longitude coordinates +thing-type.dirigera.gateway.channel.ota-progress.label = OTA Progress +thing-type.dirigera.gateway.channel.ota-progress.description = Over-the-air update progress +thing-type.dirigera.gateway.channel.ota-state.label = OTA State +thing-type.dirigera.gateway.channel.ota-state.description = Over-the-air current state +thing-type.dirigera.gateway.channel.ota-status.label = OTA Status +thing-type.dirigera.gateway.channel.ota-status.description = Over-the-air overall status +thing-type.dirigera.gateway.channel.pairing.label = Pairing +thing-type.dirigera.gateway.channel.pairing.description = Sets DIRIGERA hub into pairing mode +thing-type.dirigera.gateway.channel.statistics.label = Gateway Statistics +thing-type.dirigera.gateway.channel.statistics.description = Several statistics about gateway activities +thing-type.dirigera.gateway.channel.sunrise.label = Sunrise +thing-type.dirigera.gateway.channel.sunrise.description = Date and time of next sunrise +thing-type.dirigera.gateway.channel.sunset.label = Sunset +thing-type.dirigera.gateway.channel.sunset.description = Date and time of next sunset +thing-type.dirigera.light-controller.label = Light Controller +thing-type.dirigera.light-controller.description = Controller to handle light attributes +thing-type.dirigera.light-controller.channel.battery-level.label = Battery Charge Level +thing-type.dirigera.light-controller.channel.battery-level.description = Battery charge level in percent +thing-type.dirigera.light-controller.channel.light-preset.label = Light Preset +thing-type.dirigera.light-controller.channel.light-preset.description = Light presets for different times of the day +thing-type.dirigera.light-sensor.label = Light Sensor +thing-type.dirigera.light-sensor.description = Sensor measuring illuminance in your room +thing-type.dirigera.motion-light-sensor.label = Motion Light Sensor +thing-type.dirigera.motion-light-sensor.description = Sensor detecting motion events and measures light level +thing-type.dirigera.motion-light-sensor.channel.active-duration.label = Active Duration +thing-type.dirigera.motion-light-sensor.channel.active-duration.description = Keep connected devices active for this duration +thing-type.dirigera.motion-light-sensor.channel.battery-level.label = Battery Charge Level +thing-type.dirigera.motion-light-sensor.channel.battery-level.description = Battery charge level in percent +thing-type.dirigera.motion-light-sensor.channel.illuminance.label = Illuminance +thing-type.dirigera.motion-light-sensor.channel.illuminance.description = Illuminance in Lux +thing-type.dirigera.motion-light-sensor.channel.light-preset.label = Light Preset +thing-type.dirigera.motion-light-sensor.channel.light-preset.description = Light presets for different times of the day +thing-type.dirigera.motion-light-sensor.channel.motion.label = Motion Detected +thing-type.dirigera.motion-light-sensor.channel.motion.description = Motion detected by the device +thing-type.dirigera.motion-light-sensor.channel.schedule.label = Activity Schedule +thing-type.dirigera.motion-light-sensor.channel.schedule.description = Schedule when the sensor shall be active +thing-type.dirigera.motion-light-sensor.channel.schedule-end.label = Activity Schedule End +thing-type.dirigera.motion-light-sensor.channel.schedule-end.description = End time of sensor activity +thing-type.dirigera.motion-light-sensor.channel.schedule-start.label = Activity Schedule Start +thing-type.dirigera.motion-light-sensor.channel.schedule-start.description = Start time of sensor activity +thing-type.dirigera.motion-sensor.label = Motion Sensor +thing-type.dirigera.motion-sensor.description = Sensor detecting motion events +thing-type.dirigera.motion-sensor.channel.active-duration.label = Active Duration +thing-type.dirigera.motion-sensor.channel.active-duration.description = Keep connected devices active for this duration +thing-type.dirigera.motion-sensor.channel.battery-level.label = Battery Charge Level +thing-type.dirigera.motion-sensor.channel.battery-level.description = Battery charge level in percent +thing-type.dirigera.motion-sensor.channel.light-preset.label = Light Preset +thing-type.dirigera.motion-sensor.channel.light-preset.description = Light presets for different times of the day +thing-type.dirigera.motion-sensor.channel.motion.label = Detection Flag +thing-type.dirigera.motion-sensor.channel.motion.description = Flag if detection happened +thing-type.dirigera.motion-sensor.channel.schedule.label = Activity Schedule +thing-type.dirigera.motion-sensor.channel.schedule.description = Schedule when the sensor shall be active +thing-type.dirigera.motion-sensor.channel.schedule-end.label = Activity Schedule End +thing-type.dirigera.motion-sensor.channel.schedule-end.description = End time of sensor activity +thing-type.dirigera.motion-sensor.channel.schedule-start.label = Activity Schedule Start +thing-type.dirigera.motion-sensor.channel.schedule-start.description = Start time of sensor activity +thing-type.dirigera.power-plug.label = Power Plug +thing-type.dirigera.power-plug.description = Power plug with control of power state, startup behavior, hardware on/off button and status light +thing-type.dirigera.power-plug.channel.power.label = Plug Powered +thing-type.dirigera.power-plug.channel.power.description = Power state of plug +thing-type.dirigera.repeater.label = Repeater +thing-type.dirigera.repeater.description = Repeater to strengthen signal +thing-type.dirigera.scene.label = Scene +thing-type.dirigera.scene.description = Scene from IKEA home smart App which can be triggered +thing-type.dirigera.scene.channel.last-trigger.label = Last Trigger +thing-type.dirigera.scene.channel.last-trigger.description = Date and time when last trigger occurred +thing-type.dirigera.scene.channel.trigger.label = Scene Trigger +thing-type.dirigera.scene.channel.trigger.description = Perform / undo scene execution +thing-type.dirigera.simple-plug.label = Simple Plug +thing-type.dirigera.simple-plug.description = Simple plug with control of power state and startup behavior +thing-type.dirigera.simple-plug.channel.power.label = Plug Powered +thing-type.dirigera.simple-plug.channel.power.description = Power state of plug +thing-type.dirigera.single-shortcut.label = Single Button Shortcut +thing-type.dirigera.single-shortcut.description = Shortcut controller with one button +thing-type.dirigera.single-shortcut.channel.battery-level.label = Battery Charge Level +thing-type.dirigera.single-shortcut.channel.battery-level.description = Battery charge level in percent +thing-type.dirigera.single-shortcut.channel.button1.label = Button 1 Trigger +thing-type.dirigera.single-shortcut.channel.button1.description = Trigger of first button +thing-type.dirigera.smart-plug.label = Smart Power Plug +thing-type.dirigera.smart-plug.description = Power plug with electricity measurements +thing-type.dirigera.smart-plug.channel.electric-current.label = Plug Current +thing-type.dirigera.smart-plug.channel.electric-current.description = Electric current measured by plug +thing-type.dirigera.smart-plug.channel.electric-power.label = Electric Power +thing-type.dirigera.smart-plug.channel.electric-power.description = Electric power delivered by plug +thing-type.dirigera.smart-plug.channel.electric-voltage.label = Plug Voltage +thing-type.dirigera.smart-plug.channel.electric-voltage.description = Electric potential of plug +thing-type.dirigera.smart-plug.channel.energy-reset.label = Energy since Reset +thing-type.dirigera.smart-plug.channel.energy-reset.description = Energy consumption since last reset +thing-type.dirigera.smart-plug.channel.energy-total.label = Total Energy +thing-type.dirigera.smart-plug.channel.energy-total.description = Total energy consumption +thing-type.dirigera.smart-plug.channel.power.label = Plug Powered +thing-type.dirigera.smart-plug.channel.power.description = Power state of plug +thing-type.dirigera.smart-plug.channel.reset-date.label = Reset Date Time +thing-type.dirigera.smart-plug.channel.reset-date.description = Date and time of last reset +thing-type.dirigera.sound-controller.label = Sound Controller +thing-type.dirigera.sound-controller.description = Controller for speakers +thing-type.dirigera.sound-controller.channel.battery-level.label = Battery Charge Level +thing-type.dirigera.sound-controller.channel.battery-level.description = Battery charge level in percent +thing-type.dirigera.speaker.label = Speaker +thing-type.dirigera.speaker.description = Speaker with player activities +thing-type.dirigera.speaker.channel.crossfade.label = Cross Fade +thing-type.dirigera.speaker.channel.crossfade.description = Cross fading between tracks +thing-type.dirigera.speaker.channel.image.label = Image +thing-type.dirigera.speaker.channel.image.description = Current playing track image +thing-type.dirigera.speaker.channel.media-control.label = Media Control +thing-type.dirigera.speaker.channel.media-control.description = Media control play, pause, next, previous +thing-type.dirigera.speaker.channel.media-title.label = Media Title +thing-type.dirigera.speaker.channel.media-title.description = Title of a played media file +thing-type.dirigera.speaker.channel.mute.label = Mute Control +thing-type.dirigera.speaker.channel.mute.description = Mute current audio without stop playing +thing-type.dirigera.speaker.channel.repeat.label = Repeat +thing-type.dirigera.speaker.channel.repeat.description = Repeat Mode +thing-type.dirigera.speaker.channel.shuffle.label = Shuffle +thing-type.dirigera.speaker.channel.shuffle.description = Control shuffle mode +thing-type.dirigera.speaker.channel.volume.label = Volume Control +thing-type.dirigera.speaker.channel.volume.description = Control volume in percent +thing-type.dirigera.switch-light.label = Switch Light +thing-type.dirigera.switch-light.description = Light with switch ON, OFF capability +thing-type.dirigera.switch-light.channel.power.label = Light Powered +thing-type.dirigera.switch-light.channel.power.description = Power state of light +thing-type.dirigera.temperature-light.label = Temperature Light +thing-type.dirigera.temperature-light.description = Light with color temperature support +thing-type.dirigera.temperature-light.channel.brightness.label = Light Brightness +thing-type.dirigera.temperature-light.channel.brightness.description = Brightness of light in percent +thing-type.dirigera.temperature-light.channel.color-temperature.label = Color Temperature +thing-type.dirigera.temperature-light.channel.color-temperature.description = Color temperature from cold (0 %) to warm (100 %) +thing-type.dirigera.temperature-light.channel.power.label = Light Powered +thing-type.dirigera.temperature-light.channel.power.description = Power state of light +thing-type.dirigera.water-sensor.label = Water Sensor +thing-type.dirigera.water-sensor.description = Sensor to detect water leaks +thing-type.dirigera.water-sensor.channel.battery-level.label = Battery Charge Level +thing-type.dirigera.water-sensor.channel.battery-level.description = Battery charge level in percent +thing-type.dirigera.water-sensor.channel.leak.label = Leak Detection +thing-type.dirigera.water-sensor.channel.leak.description = Water leak detection + +# thing types config + +thing-type.config.dirigera.base-device.id.label = Device Id +thing-type.config.dirigera.base-device.id.description = Unique id of this device +thing-type.config.dirigera.color-light.fadeSequence.label = Fade Sequence +thing-type.config.dirigera.color-light.fadeSequence.description = Define sequence if several light parameters are changed at once +thing-type.config.dirigera.color-light.fadeSequence.option.0 = First brightness, then color +thing-type.config.dirigera.color-light.fadeSequence.option.1 = First color, then brightness +thing-type.config.dirigera.color-light.fadeTime.label = Fade Time +thing-type.config.dirigera.color-light.fadeTime.description = Required time for fade sequnce to color or brightness +thing-type.config.dirigera.color-light.id.label = Device Id +thing-type.config.dirigera.color-light.id.description = Unique id of this device +thing-type.config.dirigera.gateway.discovery.label = Discovery +thing-type.config.dirigera.gateway.discovery.description = Configure if paired devices shall be detected by discovery +thing-type.config.dirigera.gateway.id.label = Device Id +thing-type.config.dirigera.gateway.id.description = Unique id of this gateway +thing-type.config.dirigera.gateway.ipAddress.label = IP Address +thing-type.config.dirigera.gateway.ipAddress.description = Gateway IP Address +thing-type.config.dirigera.light-device.fadeTime.label = Fade Time +thing-type.config.dirigera.light-device.fadeTime.description = Required time for fade sequnce to color or brightness +thing-type.config.dirigera.light-device.id.label = Device Id +thing-type.config.dirigera.light-device.id.description = Unique id of this device + +# channel types + +channel-type.dirigera.alarm.label = Alarm Switch +channel-type.dirigera.blind-dimmer.label = Blind Level +channel-type.dirigera.blind-dimmer.description = Current blind level +channel-type.dirigera.blind-state.label = Blind State +channel-type.dirigera.blind-state.description = State if blind is moving up, down or stopped +channel-type.dirigera.blind-state.state.option.0 = Stopped +channel-type.dirigera.blind-state.state.option.1 = Up +channel-type.dirigera.blind-state.state.option.2 = Down +channel-type.dirigera.blind-state.command.option.0 = Stopped +channel-type.dirigera.blind-state.command.option.1 = Up +channel-type.dirigera.blind-state.command.option.2 = Down +channel-type.dirigera.child-lock.label = Child Lock +channel-type.dirigera.child-lock.description = Child lock for button on device +channel-type.dirigera.contact.label = Contact +channel-type.dirigera.custom-name.label = Custom Name +channel-type.dirigera.custom-name.description = Name given from IKEA home smart +channel-type.dirigera.datetime-reset.label = Date Time +channel-type.dirigera.datetime-reset.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM +channel-type.dirigera.datetime-reset.command.option.0 = Reset now +channel-type.dirigera.datetime.label = Date Time +channel-type.dirigera.datetime.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM +channel-type.dirigera.dimmer.label = Dimmer +channel-type.dirigera.disable-status-light.label = Disable Status Light +channel-type.dirigera.disable-status-light.description = Disable status light on device +channel-type.dirigera.duration.label = Time +channel-type.dirigera.duration.command.option.1 min = 1 minute +channel-type.dirigera.duration.command.option.3 min = 3 minutes +channel-type.dirigera.duration.command.option.5 min = 5 minutes +channel-type.dirigera.duration.command.option.10 min = 10 minutes +channel-type.dirigera.duration.command.option.15 min = 15 minutes +channel-type.dirigera.duration.command.option.20 min = 20 minutes +channel-type.dirigera.duration.command.option.30 min = 30 minutes +channel-type.dirigera.duration.command.option.40 min = 40 minutes +channel-type.dirigera.duration.command.option.60 min = 60 minutes +channel-type.dirigera.fan-mode.label = Fan Mode +channel-type.dirigera.fan-mode.state.option.0 = Auto +channel-type.dirigera.fan-mode.state.option.1 = Low +channel-type.dirigera.fan-mode.state.option.2 = Medium +channel-type.dirigera.fan-mode.state.option.3 = High +channel-type.dirigera.fan-mode.state.option.4 = On +channel-type.dirigera.fan-mode.state.option.5 = Off +channel-type.dirigera.fan-mode.command.option.0 = Auto +channel-type.dirigera.fan-mode.command.option.1 = Low +channel-type.dirigera.fan-mode.command.option.2 = Medium +channel-type.dirigera.fan-mode.command.option.3 = High +channel-type.dirigera.fan-mode.command.option.4 = On +channel-type.dirigera.fan-mode.command.option.5 = Off +channel-type.dirigera.illuminance.label = Illuminance +channel-type.dirigera.illuminance.description = Illuminance in Lux +channel-type.dirigera.image.label = Image +channel-type.dirigera.light-preset.label = Light Preset +channel-type.dirigera.light-preset.command.option.Off = Off +channel-type.dirigera.light-preset.command.option.Warm = Warm +channel-type.dirigera.light-preset.command.option.Slowdown = Slowdown +channel-type.dirigera.light-preset.command.option.Smooth = Smooth +channel-type.dirigera.light-preset.command.option.Bright = Bright +channel-type.dirigera.link-candidates.label = Link Candidates +channel-type.dirigera.link-candidates.description = Candidates which can be linked +channel-type.dirigera.links.label = Links +channel-type.dirigera.links.description = Linked controllers and sensors +channel-type.dirigera.ota-percent.label = OTA Progress +channel-type.dirigera.ota-percent.description = Over-the-air update progress +channel-type.dirigera.ota-state.label = OTA State +channel-type.dirigera.ota-state.description = Over-the-air current state +channel-type.dirigera.ota-state.state.option.0 = Ready to check +channel-type.dirigera.ota-state.state.option.1 = Check in progress +channel-type.dirigera.ota-state.state.option.2 = Ready to download +channel-type.dirigera.ota-state.state.option.3 = Download in progress +channel-type.dirigera.ota-state.state.option.4 = Update in progress +channel-type.dirigera.ota-state.state.option.5 = Update failed +channel-type.dirigera.ota-state.state.option.6 = Ready to update +channel-type.dirigera.ota-state.state.option.7 = Check failed +channel-type.dirigera.ota-state.state.option.8 = Download failed +channel-type.dirigera.ota-state.state.option.9 = Update complete +channel-type.dirigera.ota-state.state.option.10 = Battery check failed +channel-type.dirigera.ota-status.label = OTA Status +channel-type.dirigera.ota-status.description = Over-the-air overall status +channel-type.dirigera.ota-status.state.option.0 = Up to date +channel-type.dirigera.ota-status.state.option.1 = Update available +channel-type.dirigera.pm25.label = Particulate Matter category 2.5 +channel-type.dirigera.repeat.label = Repeat Options +channel-type.dirigera.repeat.state.option.0 = Off +channel-type.dirigera.repeat.state.option.1 = Title +channel-type.dirigera.repeat.state.option.2 = Playlist +channel-type.dirigera.repeat.command.option.0 = Off +channel-type.dirigera.repeat.command.option.1 = Title +channel-type.dirigera.repeat.command.option.2 = Playlist +channel-type.dirigera.scene-trigger.label = Scene Trigger +channel-type.dirigera.scene-trigger.command.option.0 = Trigger +channel-type.dirigera.scene-trigger.command.option.1 = Undo +channel-type.dirigera.schedule-end-time.label = Schedule Time +channel-type.dirigera.schedule-end-time.state.pattern = %1$tH:%1$tM +channel-type.dirigera.schedule-end-time.command.option.04:00 = 4:00 +channel-type.dirigera.schedule-end-time.command.option.04:30 = 4:30 +channel-type.dirigera.schedule-end-time.command.option.05:00 = 5:00 +channel-type.dirigera.schedule-end-time.command.option.05:30 = 5:30 +channel-type.dirigera.schedule-end-time.command.option.06:00 = 6:00 +channel-type.dirigera.schedule-end-time.command.option.06:30 = 6:30 +channel-type.dirigera.schedule-end-time.command.option.07:00 = 7:00 +channel-type.dirigera.schedule-end-time.command.option.07:30 = 7:30 +channel-type.dirigera.schedule-end-time.command.option.08:00 = 8:00 +channel-type.dirigera.schedule-start-time.label = Schedule Time +channel-type.dirigera.schedule-start-time.state.pattern = %1$tH:%1$tM +channel-type.dirigera.schedule-start-time.command.option.16:00 = 16:00 +channel-type.dirigera.schedule-start-time.command.option.16:30 = 16:30 +channel-type.dirigera.schedule-start-time.command.option.17:00 = 17:00 +channel-type.dirigera.schedule-start-time.command.option.17:30 = 17:30 +channel-type.dirigera.schedule-start-time.command.option.18:00 = 18:00 +channel-type.dirigera.schedule-start-time.command.option.18:30 = 18:30 +channel-type.dirigera.schedule-start-time.command.option.19:00 = 19:00 +channel-type.dirigera.schedule-start-time.command.option.19:30 = 19:30 +channel-type.dirigera.schedule-start-time.command.option.20:00 = 20:00 +channel-type.dirigera.sensor-schedule.label = Sensor Schedule +channel-type.dirigera.sensor-schedule.state.option.0 = Always +channel-type.dirigera.sensor-schedule.state.option.1 = Follow Sun +channel-type.dirigera.sensor-schedule.state.option.2 = Time schedule +channel-type.dirigera.sensor-schedule.command.option.0 = Always +channel-type.dirigera.sensor-schedule.command.option.1 = Follow Sun +channel-type.dirigera.sensor-schedule.command.option.2 = Time schedule +channel-type.dirigera.startup.label = Startup Behavior +channel-type.dirigera.startup.description = Startup behavior after power cutoff +channel-type.dirigera.startup.state.option.0 = Previous +channel-type.dirigera.startup.state.option.1 = On +channel-type.dirigera.startup.state.option.2 = Off +channel-type.dirigera.startup.state.option.3 = Toggle +channel-type.dirigera.startup.command.option.0 = Previous +channel-type.dirigera.startup.command.option.1 = On +channel-type.dirigera.startup.command.option.2 = Off +channel-type.dirigera.startup.command.option.3 = Toggle +channel-type.dirigera.switch-ro.label = On Off Switch +channel-type.dirigera.switch.label = On Off Switch +channel-type.dirigera.text.label = Simple Text +channel-type.dirigera.time.label = Time +channel-type.dirigera.voc.label = VOC Index + +# thing status types + +dirigera.device.status.missing-ip = No IP Address configured +dirigera.device.status.wrong-bridge-handler = BridgeHandler isn't a Gateway +dirigera.device.status.missing-bridge-handler = BridgeHandler is missing +dirigera.device.status.missing-bridge = No Bridge configured +dirigera.device.status.not-reachable = Device not reachable +dirigera.device.status.api-error = API {0} cannot be created +dirigera.device.status.id-not-found = Device id {0} not found +dirigera.device.status.ttuid-mismatch = Handler {0} doesn't match with model {1} +dirigera.gateway.status.pairing-button = Press Button on DIRIGERA Gateway +dirigera.gateway.status.pairing-retry = Pairing failed. Stop and start bridge to initialize new pairing. +dirigera.gateway.status.comm-error = Gateway HTTP Status {0} +dirigera.gateway.status.no-gateway = No Gateway found +dirigera.gateway.status.ambiguous-gateway = More than one Gateway found +dirigera.scene.status.scene-not-found = Scene not found diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/air-purifier.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/air-purifier.xml new file mode 100644 index 00000000000..7aa67979b90 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/air-purifier.xml @@ -0,0 +1,55 @@ + + + + + + + + + + Air cleaning device with particle filter + + + + + Fan on, off, speed or automatic behavior + + + + Manual regulation of fan speed + + + + Fan runtime in minutes + + + + Filter elapsed time in minutes + + + + Remaining filter time in minutes + + + + Filter lifetime in minutes + + + + Filter alarm signal + + + + Category 2.5 particulate matter + + + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/air-quality.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/air-quality.xml new file mode 100644 index 00000000000..c5462215c96 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/air-quality.xml @@ -0,0 +1,37 @@ + + + + + + + + + + Air measure for temperature, humidity and particles + + + + + Current indoor temperature + + + + Atmospheric humidity in percent + + + + Category 2.5 particulate matter + + + + Relative VOC intensity compared to recent history + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/blind-controller.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/blind-controller.xml new file mode 100644 index 00000000000..a89f67bfb20 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/blind-controller.xml @@ -0,0 +1,25 @@ + + + + + + + + + + Controller to open and close blinds + + + + + Battery charge level in percent + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/blind.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/blind.xml new file mode 100644 index 00000000000..e00f8e62957 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/blind.xml @@ -0,0 +1,27 @@ + + + + + + + + + + Window or door blind + + + + + + + Battery charge level in percent + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/channel-types.xml new file mode 100644 index 00000000000..87ff524a948 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/channel-types.xml @@ -0,0 +1,347 @@ + + + + + String + + + + String + + Name given from IKEA home smart + + + String + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + veto + + + Number + + + + + + + + + + + + + + + + veto + + + Number + + Startup behavior after power cutoff + + + + + + + + + + + + + + + + + veto + + + Switch + + Disable status light on device + veto + + + Switch + + Child lock for button on device + veto + + + Switch + + veto + + + Switch + + + + + Switch + + Alarm + + Alarm + Water + + + + + Number:Illuminance + + Illuminance in Lux + + Measurement + Light + + + + + Contact + + Contact + + OpenState + + + + + Dimmer + + veto + + + Image + + + + + DateTime + + + + + + + + + + + + + + + + + + DateTime + + + + + + + + + + + + + + + + veto + + + DateTime + + + + + DateTime + + + + + + + + veto + + + Number + + + + + + + + veto + + + Number:Density + + + + + Number + + + + + Number:Time + + + + Number:Time + + + + + + + + + + + + + + + + + Number + + + + + + + + + + + + + + + + + + + + + + + + Number + + State if blind is moving up, down or stopped + + + + + + + + + + + + + + + + + Dimmer + + Current blind level + Rollershutter + + OpenLevel + + veto + + + Number + + Over-the-air overall status + + + + + + + + + Number + + Over-the-air current state + + + + + + + + + + + + + + + + + + Number:Dimensionless + + Over-the-air update progress + + + + String + + Linked controllers and sensors + veto + + + String + + Candidates which can be linked + veto + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/color-light.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/color-light.xml new file mode 100644 index 00000000000..9da75868178 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/color-light.xml @@ -0,0 +1,47 @@ + + + + + + + + + + Light with color support + + + + + Power state of light + veto + + + + Brightness of light in percent + veto + + + + Color temperature from cold (0 %) to warm (100 %) + veto + + + + Color temperature of a bulb in Kelvin + veto + + + + Color of light with hue, saturation and brightness + veto + + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/contact-sensor.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/contact-sensor.xml new file mode 100644 index 00000000000..bbc30b5566a --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/contact-sensor.xml @@ -0,0 +1,29 @@ + + + + + + + + + + Sensor tracking if windows or doors are open + + + + + State if door or window is open or closed + + + + Battery charge level in percent + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/dimmable-light.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/dimmable-light.xml new file mode 100644 index 00000000000..74cf5422eda --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/dimmable-light.xml @@ -0,0 +1,32 @@ + + + + + + + + + + Light with brightness support + + + + + Power state of light + veto + + + + Brightness of light in percent + veto + + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/double-shortcut.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/double-shortcut.xml new file mode 100644 index 00000000000..e1caade55b6 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/double-shortcut.xml @@ -0,0 +1,33 @@ + + + + + + + + + + Shortcut controller with two buttons + + + + + Trigger of first button + + + + Trigger of second button + + + + Battery charge level in percent + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/gateway.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/gateway.xml new file mode 100644 index 00000000000..19521d7d2f2 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/gateway.xml @@ -0,0 +1,49 @@ + + + + + + IKEA Gateway for smart products + + + + + + Location in latitude, longitude coordinates + + + + Date and time of next sunrise + + + + Date and time of next sunset + + + + Sets DIRIGERA hub into pairing mode + + + + Over-the-air overall status + + + + Over-the-air current state + + + + Over-the-air update progress + + + + Several statistics about gateway activities + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/light-controller.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/light-controller.xml new file mode 100644 index 00000000000..132e7f4e82a --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/light-controller.xml @@ -0,0 +1,29 @@ + + + + + + + + + + Controller to handle light attributes + + + + + Light presets for different times of the day + + + + Battery charge level in percent + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/light-sensor.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/light-sensor.xml new file mode 100644 index 00000000000..6cb06222793 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/light-sensor.xml @@ -0,0 +1,21 @@ + + + + + + + + + + Sensor measuring illuminance in your room + + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/motion-light-sensor.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/motion-light-sensor.xml new file mode 100644 index 00000000000..30d3c9477b3 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/motion-light-sensor.xml @@ -0,0 +1,53 @@ + + + + + + + + + + Sensor detecting motion events and measures light level + + + + + Motion detected by the device + + + + Keep connected devices active for this duration + + + + Illuminance in Lux + + + + Battery charge level in percent + + + + Schedule when the sensor shall be active + + + + Start time of sensor activity + + + + End time of sensor activity + + + + Light presets for different times of the day + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/motion-sensor.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/motion-sensor.xml new file mode 100644 index 00000000000..16c7be0c399 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/motion-sensor.xml @@ -0,0 +1,49 @@ + + + + + + + + + + Sensor detecting motion events + + + + + Flag if detection happened + + + + Keep connected devices active for this duration + + + + Battery charge level in percent + + + + Schedule when the sensor shall be active + + + + Start time of sensor activity + + + + End time of sensor activity + + + + Light presets for different times of the day + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/power-plug.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/power-plug.xml new file mode 100644 index 00000000000..65f4d294109 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/power-plug.xml @@ -0,0 +1,29 @@ + + + + + + + + + + Power plug with control of power state, startup behavior, hardware on/off button and status light + + + + + Power state of plug + veto + + + + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/repeater.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/repeater.xml new file mode 100644 index 00000000000..1899b94ebd3 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/repeater.xml @@ -0,0 +1,21 @@ + + + + + + + + + + Repeater to strengthen signal + + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/scene.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/scene.xml new file mode 100644 index 00000000000..488fec03b31 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/scene.xml @@ -0,0 +1,28 @@ + + + + + + + + + + Scene from IKEA home smart App which can be triggered + + + + + Perform / undo scene execution + + + + Date and time when last trigger occurred + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/simple-plug.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/simple-plug.xml new file mode 100644 index 00000000000..732273e4646 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/simple-plug.xml @@ -0,0 +1,27 @@ + + + + + + + + + + Simple plug with control of power state and startup behavior + + + + + Power state of plug + veto + + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/single-shortcut.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/single-shortcut.xml new file mode 100644 index 00000000000..246d73a90ab --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/single-shortcut.xml @@ -0,0 +1,29 @@ + + + + + + + + + + Shortcut controller with one button + + + + + Trigger of first button + + + + Battery charge level in percent + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/smart-plug.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/smart-plug.xml new file mode 100644 index 00000000000..d91e2262118 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/smart-plug.xml @@ -0,0 +1,53 @@ + + + + + + + + + + Power plug with electricity measurements + + + + + Power state of plug + veto + + + + + + Electric power delivered by plug + + + + Total energy consumption + + + + Energy consumption since last reset + + + + Date and time of last reset + + + + Electric current measured by plug + + + + Electric potential of plug + + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/sound-controller.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/sound-controller.xml new file mode 100644 index 00000000000..9d3ad303633 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/sound-controller.xml @@ -0,0 +1,25 @@ + + + + + + + + + + Controller for speakers + + + + + Battery charge level in percent + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/speaker.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/speaker.xml new file mode 100644 index 00000000000..7d0b7b72703 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/speaker.xml @@ -0,0 +1,53 @@ + + + + + + + + + + Speaker with player activities + + + + + Media control play, pause, next, previous + + + + Control volume in percent + + + + Mute current audio without stop playing + + + + Control shuffle mode + + + + Cross fading between tracks + + + + Repeat Mode + + + + Title of a played media file + + + + Current playing track image + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/switch-light.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/switch-light.xml new file mode 100644 index 00000000000..0944f421206 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/switch-light.xml @@ -0,0 +1,26 @@ + + + + + + + + + + Light with switch ON, OFF capability + + + + + Power state of light + + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/temperature-light.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/temperature-light.xml new file mode 100644 index 00000000000..823bb38f6ad --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/temperature-light.xml @@ -0,0 +1,42 @@ + + + + + + + + + + Light with color temperature support + + + + + Power state of light + veto + + + + Brightness of light in percent + veto + + + + Color temperature from cold (0 %) to warm (100 %) + veto + + + + Color temperature of a bulb in Kelvin + veto + + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/water-sensor.xml b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/water-sensor.xml new file mode 100644 index 00000000000..f994e3daa3e --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/OH-INF/thing/water-sensor.xml @@ -0,0 +1,29 @@ + + + + + + + + + + Sensor to detect water leaks + + + + + Water leak detection + + + + Battery charge level in percent + + + + + + + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/json/gateway/coordinates.json b/bundles/org.openhab.binding.dirigera/src/main/resources/json/gateway/coordinates.json new file mode 100644 index 00000000000..628cecbb728 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/json/gateway/coordinates.json @@ -0,0 +1,8 @@ +{ + "attributes": { + "coordinates": { + "latitude": %s, + "longitude": %s, + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/json/gateway/null-coordinates.json b/bundles/org.openhab.binding.dirigera/src/main/resources/json/gateway/null-coordinates.json new file mode 100644 index 00000000000..c395985b90c --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/json/gateway/null-coordinates.json @@ -0,0 +1,5 @@ +{ + "attributes": { + "coordinates": {} + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/bright.json b/bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/bright.json new file mode 100644 index 00000000000..c931aa92ff1 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/bright.json @@ -0,0 +1,32 @@ +[ + { + "startTime": "07:00", + "lightLevel": 25, + "colorTemperature": 3000 + }, + { + "startTime": "09:00", + "lightLevel": 75, + "colorTemperature": 3900 + }, + { + "startTime": "13:00", + "lightLevel": 100, + "colorTemperature": 4000 + }, + { + "startTime": "17:00", + "lightLevel": 70, + "colorTemperature": 4000 + }, + { + "startTime": "20:00", + "lightLevel": 20, + "colorTemperature": 3900 + }, + { + "startTime": "22:00", + "lightLevel": 1, + "colorTemperature": 3000 + } +] diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/slowdown.json b/bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/slowdown.json new file mode 100644 index 00000000000..5336689b95c --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/slowdown.json @@ -0,0 +1,33 @@ +[ + { + "startTime": "05:00", + "lightLevel": 100, + "colorTemperature": 4000 + }, + { + "startTime": "10:00", + "lightLevel": 100, + "colorTemperature": 3800 + }, + { + "startTime": "17:00", + "lightLevel": 75, + "colorTemperature": 3500 + }, + { + "startTime": "20:00", + "lightLevel": 45, + "colorTemperature": 3000 + }, + { + "startTime": "22:00", + "lightLevel": 20, + "colorTemperature": 2400 + }, + { + "startTime": "23:00", + "lightLevel": 1, + "colorTemperature": 2200 + } +] + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/smooth.json b/bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/smooth.json new file mode 100644 index 00000000000..822eaa11ebc --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/smooth.json @@ -0,0 +1,32 @@ +[ + { + "startTime": "06:00", + "lightLevel": 50, + "colorTemperature": 2400 + }, + { + "startTime": "10:00", + "lightLevel": 100, + "colorTemperature": 4000 + }, + { + "startTime": "16:00", + "lightLevel": 100, + "colorTemperature": 3500 + }, + { + "startTime": "20:00", + "lightLevel": 50, + "colorTemperature": 3000 + }, + { + "startTime": "22:00", + "lightLevel": 20, + "colorTemperature": 2300 + }, + { + "startTime": "23:00", + "lightLevel": 1, + "colorTemperature": 2300 + } +] diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/warm.json b/bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/warm.json new file mode 100644 index 00000000000..7a739564396 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/json/light-presets/warm.json @@ -0,0 +1,32 @@ +[ + { + "startTime": "07:00", + "lightLevel": 25, + "colorTemperature": 3000 + }, + { + "startTime": "09:00", + "lightLevel": 75, + "colorTemperature": 3600 + }, + { + "startTime": "13:00", + "lightLevel": 100, + "colorTemperature": 4000 + }, + { + "startTime": "17:00", + "lightLevel": 70, + "colorTemperature": 3600 + }, + { + "startTime": "20:00", + "lightLevel": 20, + "colorTemperature": 3000 + }, + { + "startTime": "22:00", + "lightLevel": 1, + "colorTemperature": 2200 + } +] diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/json/scenes/click-scene.json b/bundles/org.openhab.binding.dirigera/src/main/resources/json/scenes/click-scene.json new file mode 100644 index 00000000000..210b6491177 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/json/scenes/click-scene.json @@ -0,0 +1,22 @@ +{ + "id": "%s", + "type": "customScene", + "info": { + "name": "%s", + "icon": "scenes_home_filled" + }, + "triggers": [ + { + "type": "controller", + "trigger": { + "controllerType": "shortcutController", + "clickPattern": "%s", + "buttonIndex": %s, + "deviceId": "%s" + } + } + ], + "actions": [], + "commands": [] +} + \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/always-on.json b/bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/always-on.json new file mode 100644 index 00000000000..a20e377156b --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/always-on.json @@ -0,0 +1,7 @@ +{ + "attributes": { + "sensorConfig": { + "scheduleOn": false + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/duration-update.json b/bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/duration-update.json new file mode 100644 index 00000000000..d690d9c1acd --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/duration-update.json @@ -0,0 +1,8 @@ +{ + "attributes": { + "sensorConfig": { + "onDuration": %s + } + } +} + diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/follow-sun.json b/bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/follow-sun.json new file mode 100644 index 00000000000..1a1a5021fbf --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/follow-sun.json @@ -0,0 +1,15 @@ +{ + "attributes": { + "sensorConfig": { + "scheduleOn": true, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/schedule-on.json b/bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/schedule-on.json new file mode 100644 index 00000000000..0d8129ac8d0 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/main/resources/json/sensor-config/schedule-on.json @@ -0,0 +1,15 @@ +{ + "attributes": { + "sensorConfig": { + "scheduleOn": true, + "schedule": { + "onCondition": { + "time": "%s" + }, + "offCondition": { + "time": "%s" + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/FileReader.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/FileReader.java new file mode 100644 index 00000000000..6d2f1cd4976 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/FileReader.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link FileReader} reads from file into String + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class FileReader { + + public static String readFileInString(String filename) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filename), "CP1252"));) { + StringBuffer buf = new StringBuffer(); + String sCurrentLine; + + while ((sCurrentLine = br.readLine()) != null) { + buf.append(sCurrentLine); + } + return buf.toString(); + } catch (IOException e) { + // fail if file cannot be read + fail("File " + filename + " not found"); + } + return "ERR"; + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/TestGeneric.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/TestGeneric.java new file mode 100644 index 00000000000..0be31f8f712 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/TestGeneric.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.light.LightCommand; +import org.openhab.binding.dirigera.internal.interfaces.DirigeraAPI; +import org.openhab.binding.dirigera.internal.interfaces.Gateway; +import org.openhab.binding.dirigera.internal.network.DirigeraAPIImpl; +import org.openhab.core.common.ThreadPoolManager; + +/** + * {@link TestGeneric} some basic tests + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestGeneric { + static String output = "fine!"; + + @Test + void testStringFormatWithNull() { + try { + String error = String.format( + "{\"http-error-flag\":true,\"http-error-status\":%s,\"http-error-message\":\"%s\"}", "5", null); + JSONObject errorJSON = new JSONObject(error); + assertFalse(errorJSON.isNull("http-error-message")); + } catch (Exception e) { + fail(); + } + } + + @Test + void lightCommandQueueTest() { + ArrayList lightRequestQueue = new ArrayList<>(); + JSONObject dummy1 = new JSONObject(); + dummy1.put("dunny1", false); + LightCommand brightness1 = new LightCommand(dummy1, LightCommand.Action.BRIGHTNESS); + lightRequestQueue.add(brightness1); + JSONObject dummy2 = new JSONObject(); + dummy2.put("dunny2", true); + LightCommand brightness2 = new LightCommand(dummy2, LightCommand.Action.BRIGHTNESS); + assertTrue(lightRequestQueue.contains(brightness1)); + assertTrue(lightRequestQueue.contains(brightness2)); + assertTrue(brightness1.equals(brightness2)); + JSONObject dummy3 = null; + assertFalse(brightness1.equals(dummy3)); + LightCommand color = new LightCommand(dummy2, LightCommand.Action.COLOR); + assertFalse(lightRequestQueue.contains(color)); + } + + @Test + void testApiJsonException() { + HttpClient httpMock = mock(HttpClient.class); + Request requestMock = mock(Request.class); + when(httpMock.isRunning()).thenReturn(true); + when(httpMock.getSslContextFactory()).thenReturn(new SslContextFactory.Client(true)); + when(httpMock.newRequest(anyString())).thenReturn(requestMock); + when(requestMock.timeout(10, TimeUnit.SECONDS)).thenReturn(requestMock); + when(requestMock.header(HttpHeader.AUTHORIZATION, "Bearer 1234")).thenReturn(requestMock); + + ContentResponse response = mock(ContentResponse.class); + when(response.getStatus()).thenReturn(200); + // response will force a JSON format exception in API implementation + when(response.getContentAsString()).thenReturn("{\"rubbish\":true,butNoJson:\"false\",]}"); + + try { + when(requestMock.send()).thenReturn(response); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + fail(); + } + + Gateway gateway = mock(Gateway.class); + when(gateway.getToken()).thenReturn("1234"); + DirigeraAPIImpl api = new DirigeraAPIImpl(httpMock, gateway); + JSONObject apiResponse = api.readDevice("abc"); + assertNotNull(apiResponse); + // test completeness and status of error message + assertTrue(apiResponse.has(DirigeraAPI.HTTP_ERROR_FLAG)); + assertTrue(apiResponse.has(DirigeraAPI.HTTP_ERROR_STATUS)); + assertEquals(500, apiResponse.getInt(DirigeraAPI.HTTP_ERROR_STATUS)); + assertTrue(apiResponse.has(DirigeraAPI.HTTP_ERROR_MESSAGE)); + } + + @Test + void testThreadpoolExcpetion() { + ScheduledExecutorService ses = ThreadPoolManager.getScheduledPool("test"); + ScheduledFuture sf = ses.scheduleWithFixedDelay(this::printOutput, 0, 50, TimeUnit.MILLISECONDS); + sleep(); + assertFalse(sf.isDone()); + output = "throw"; + sleep(); + assertTrue(sf.isDone()); + } + + void printOutput() { + if ("throw".equals(output)) { + throw new UnsupportedOperationException("crash"); + } + } + + void sleep() { + try { + Thread.sleep(250); + } catch (InterruptedException e) { + fail(); + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/DirigeraBridgeProvider.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/DirigeraBridgeProvider.java new file mode 100644 index 00000000000..353a1f19603 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/DirigeraBridgeProvider.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.json.JSONArray; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.Constants; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.DicoveryServiceMock; +import org.openhab.binding.dirigera.internal.mock.DirigeraAPISimu; +import org.openhab.binding.dirigera.internal.mock.DirigeraHandlerManipulator; +import org.openhab.binding.dirigera.internal.mock.HandlerFactoryMock; +import org.openhab.core.storage.Storage; +import org.openhab.core.test.storage.VolatileStorageService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.internal.BridgeImpl; +import org.openhab.core.thing.internal.ThingImpl; + +/** + * {@link DirigeraBridgeProvider} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +public class DirigeraBridgeProvider { + public static Bridge prepareSimuBridge() { + return prepareSimuBridge("src/test/resources/home/home.json", false, List.of()); + } + + /** + * Prepare bridge which can be used with DirigraAPISimu Provider + * + * @return Bridge + */ + public static Bridge prepareSimuBridge(String homeFile, boolean discovery, List knownDevicesd) { + String ipAddress = "1.2.3.4"; + HttpClient httpMock = mock(HttpClient.class); + DirigeraAPISimu.fileName = homeFile; + /** + * Prepare persistence + */ + // prepare persistence data + VolatileStorageService storageService = new VolatileStorageService(); + Storage mockStorage = storageService.getStorage(Constants.BINDING_ID); + + JSONObject storageObject = new JSONObject(); + JSONArray knownDevices = new JSONArray(knownDevicesd); + knownDevices.put("594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1"); + storageObject.put(PROPERTY_DEVICES, knownDevices.toString()); + storageObject.put(PROPERTY_TOKEN, "unit-test"); + mockStorage.put(ipAddress, storageObject.toString()); + + // prepare instances + BridgeImpl hubBridge = new BridgeImpl(THING_TYPE_GATEWAY, new ThingUID(BINDING_ID + ":" + "gateway:9876")); + hubBridge.setBridgeUID(new ThingUID(BINDING_ID + ":" + "gateway:9876")); + + /** + * new version with api simulation in background + */ + DirigeraHandlerManipulator hubHandler = new DirigeraHandlerManipulator(hubBridge, httpMock, mockStorage, + new DicoveryServiceMock()); + hubBridge.setHandler(hubHandler); + CallbackMock bridgeCallback = new CallbackMock(); + hubHandler.setCallback(bridgeCallback); + + // set handler to full configured with token, ipAddress and if + Map config = new HashMap<>(); + config.put("ipAddress", ipAddress); + config.put("id", "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1"); + config.put("discovery", discovery); + hubHandler.handleConfigurationUpdate(config); + + hubHandler.initialize(); + bridgeCallback.waitForOnline(); + return hubBridge; + } + + public static ThingHandler createHandler(ThingTypeUID thingTypeUID, Bridge hubBridge, String deviceId) { + VolatileStorageService storageService = new VolatileStorageService(); + HandlerFactoryMock hfm = new HandlerFactoryMock(storageService); + assertTrue(hfm.supportsThingType(thingTypeUID)); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + thing.setLabel("Unit Test Device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + ThingHandler handler = hfm.createHandler(thing); + assertNotNull(handler); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + // set the right id + Map config = new HashMap<>(); + config.put("id", deviceId); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + return handler; + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/TestGateway.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/TestGateway.java new file mode 100644 index 00000000000..66187e5ea7f --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/TestGateway.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.CHANNEL_LOCATION; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.DirigeraAPISimu; +import org.openhab.binding.dirigera.internal.mock.DirigeraHandlerManipulator; +import org.openhab.core.library.types.PointType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * {@link TestGateway} for checking gateway use cases + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestGateway { + private static String deviceId = "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1"; + + private static DirigeraHandler handler = mock(DirigeraHandler.class); + private static CallbackMock callback = mock(CallbackMock.class); + private static Thing thing = mock(Thing.class); + private static String mockFile = "src/test/resources/gateway/home-with-coordinates.json"; + + @Test + void testBridgeCreation() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(mockFile, false, List.of()); + ThingHandler bridgeHandler = hubBridge.getHandler(); + assertTrue(bridgeHandler instanceof DirigeraHandlerManipulator); + handler = (DirigeraHandlerManipulator) bridgeHandler; + thing = handler.getThing(); + ThingHandlerCallback proxyCallback = ((DirigeraHandlerManipulator) handler).getCallback(); + assertNotNull(proxyCallback); + assertTrue(proxyCallback instanceof CallbackMock); + callback = (CallbackMock) proxyCallback; + handler.initialize(); + callback.waitForOnline(); + } + + @Test + void testWithCoordinates() { + mockFile = "src/test/resources/gateway/home-with-coordinates.json"; + testBridgeCreation(); + assertNotNull(handler); + assertNotNull(thing); + assertNotNull(callback); + + State locationPoint = callback.getState("dirigera:gateway:9876:location"); + assertNotNull(locationPoint); + assertTrue(locationPoint instanceof PointType); + assertEquals("9.876,1.234", ((PointType) locationPoint).toFullString(), "Location Point"); + } + + @Test + void testWithoutCoordinates() { + mockFile = "src/test/resources/gateway/home-without-coordinates.json"; + testBridgeCreation(); + assertNotNull(handler); + assertNotNull(thing); + assertNotNull(callback); + + State locationPoint = callback.getState("dirigera:gateway:9876:location"); + assertNotNull(locationPoint); + assertTrue(locationPoint instanceof UnDefType); + } + + @Test + void testCommands() { + testWithCoordinates(); + + // remove location from gateway with empty string + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_LOCATION), StringType.EMPTY); + String patch = DirigeraAPISimu.patchMap.get(deviceId); + assertNotNull(patch); + assertEquals("{\"attributes\":{\"coordinates\":{}}}", patch, "Empty Coordinates"); + DirigeraAPISimu.patchMap.clear(); + + // set new location with valid coordinates + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_LOCATION), StringType.valueOf("9.123,1.987")); + patch = DirigeraAPISimu.patchMap.get(deviceId); + assertNotNull(patch); + assertEquals("{\"attributes\":{\"coordinates\":{\"latitude\":9.123,\"longitude\":1.987}}}", patch, + "Valid Coordinates"); + DirigeraAPISimu.patchMap.clear(); + + // nothing send if value is invalid + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_LOCATION), StringType.valueOf("wrong coding")); + patch = DirigeraAPISimu.patchMap.get(deviceId); + assertNull(patch, "Wrong coordinates"); + DirigeraAPISimu.patchMap.clear(); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/TestWrongHandler.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/TestWrongHandler.java new file mode 100644 index 00000000000..c1cc00aab59 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/TestWrongHandler.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.sensor.ContactSensorHandler; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.internal.ThingImpl; + +/** + * {@link TestWrongHandler} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestWrongHandler { + @Test + void testWrongHandlerForId() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + ThingImpl thing = new ThingImpl(THING_TYPE_CONTACT_SENSOR, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + ContactSensorHandler handler = new ContactSensorHandler(thing, CONTACT_SENSOR_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", "5ac5e131-44a4-4d75-be78-759a095d31fb_1"); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + ThingStatusInfo status = callback.getStatus(); + assertEquals(ThingStatus.OFFLINE, status.getStatus(), "OFFLINE"); + assertEquals(ThingStatusDetail.CONFIGURATION_ERROR, status.getStatusDetail(), "Config Error"); + String description = status.getDescription(); + assertNotNull(description); + assertTrue( + "@text/dirigera.device.status.ttuid-mismatch [\"dirigera:contact-sensor\",\"dirigera:motion-light-sensor\"]" + .equals(description), + "Description"); + } + + @Test + void testMissingId() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + ThingImpl thing = new ThingImpl(THING_TYPE_CONTACT_SENSOR, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + ContactSensorHandler handler = new ContactSensorHandler(thing, CONTACT_SENSOR_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", "5ac5e131-1234-4d75-be78-759a095d31fb_1"); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + ThingStatusInfo status = callback.getStatus(); + assertEquals(ThingStatus.OFFLINE, status.getStatus(), "OFFLINE"); + assertEquals(ThingStatusDetail.GONE, status.getStatusDetail(), "Device disappeared"); + String description = status.getDescription(); + assertNotNull(description); + assertTrue("@text/dirigera.device.status.id-not-found [\"5ac5e131-1234-4d75-be78-759a095d31fb_1\"]" + .equals(description), "Description"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/airpurifier/TestAirPurifier.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/airpurifier/TestAirPurifier.java new file mode 100644 index 00000000000..4189a9af5bb --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/airpurifier/TestAirPurifier.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.airpurifier; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.DirigeraAPISimu; +import org.openhab.core.library.types.DecimalType; +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.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestAirPurifier} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestAirPurifier { + private static String deviceId = "a8319695-0729-428c-9465-aadc0b738995"; + private static ThingTypeUID thingTypeUID = THING_TYPE_AIR_PURIFIER; + + private static AirPurifierHandler handler = mock(AirPurifierHandler.class); + private static CallbackMock callback = mock(CallbackMock.class); + private static Thing thing = mock(Thing.class); + + @Test + void testHandlerCreation() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingHandler factoryHandler = DirigeraBridgeProvider.createHandler(thingTypeUID, hubBridge, deviceId); + assertTrue(factoryHandler instanceof AirPurifierHandler); + handler = (AirPurifierHandler) factoryHandler; + thing = handler.getThing(); + ThingHandlerCallback proxyCallback = handler.getCallback(); + assertNotNull(proxyCallback); + assertTrue(proxyCallback instanceof CallbackMock); + callback = (CallbackMock) proxyCallback; + handler.initialize(); + callback.waitForOnline(); + } + + @Test + void testInitialization() { + testHandlerCreation(); + assertNotNull(handler); + assertNotNull(thing); + assertNotNull(callback); + checkAirPurifierStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_CHILD_LOCK), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_DISABLE_STATUS_LIGHT), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_PURIFIER_FILTER_ALARM), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_PURIFIER_FILTER_ELAPSED), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_PURIFIER_FILTER_LIFETIME), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_PURIFIER_FAN_RUNTIME), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_PURIFIER_FAN_MODE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_PURIFIER_FAN_SPEED), RefreshType.REFRESH); + checkAirPurifierStates(callback); + } + + @Test + void testCommands() { + testHandlerCreation(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_PURIFIER_FAN_MODE), new DecimalType(4)); + String patch = DirigeraAPISimu.patchMap.get(deviceId); + assertNotNull(patch); + assertEquals("{\"attributes\":{\"fanMode\":\"on\"}}", patch, "Fan Mode on"); + + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_PURIFIER_FAN_SPEED), new PercentType(23)); + patch = DirigeraAPISimu.patchMap.get(deviceId); + assertNotNull(patch); + assertEquals("{\"attributes\":{\"motorState\":12}}", patch, "Fan Speed"); + + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_PURIFIER_FAN_SPEED), new PercentType(100)); + patch = DirigeraAPISimu.patchMap.get(deviceId); + assertNotNull(patch); + assertEquals("{\"attributes\":{\"motorState\":50}}", patch, "Fan Speed"); + } + + @Test + void testDump() { + testHandlerCreation(); + assertEquals("unit-test", handler.getToken()); + } + + void checkAirPurifierStates(CallbackMock callback) { + State otaStatus = callback.getState("dirigera:air-purifier:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:air-purifier:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:air-purifier:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + + State disableLightState = callback.getState("dirigera:air-purifier:test-device:disable-status-light"); + assertNotNull(disableLightState); + assertTrue(disableLightState instanceof OnOffType); + assertTrue(OnOffType.ON.equals((disableLightState)), "Status Light Disabled"); + State childlockState = callback.getState("dirigera:air-purifier:test-device:child-lock"); + assertNotNull(childlockState); + assertTrue(childlockState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((childlockState)), "Child Lock enabled"); + + State filterAlarmState = callback.getState("dirigera:air-purifier:test-device:filter-alarm"); + assertNotNull(filterAlarmState); + assertTrue(filterAlarmState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((filterAlarmState)), "Filter Alarm"); + State filterElapsedState = callback.getState("dirigera:air-purifier:test-device:filter-elapsed"); + assertNotNull(filterElapsedState); + assertTrue(filterElapsedState instanceof QuantityType); + assertEquals(Units.MINUTE, ((QuantityType) filterElapsedState).getUnit()); + assertEquals(193540, ((QuantityType) filterElapsedState).intValue()); + State filterLifetimedState = callback.getState("dirigera:air-purifier:test-device:filter-lifetime"); + assertNotNull(filterLifetimedState); + assertTrue(filterLifetimedState instanceof QuantityType); + assertEquals(Units.MINUTE, ((QuantityType) filterLifetimedState).getUnit()); + assertEquals(259200, ((QuantityType) filterLifetimedState).intValue()); + + State motorRuntimeState = callback.getState("dirigera:air-purifier:test-device:fan-runtime"); + assertNotNull(motorRuntimeState); + assertTrue(motorRuntimeState instanceof QuantityType); + assertEquals(Units.MINUTE, ((QuantityType) motorRuntimeState).getUnit()); + assertEquals(472283, ((QuantityType) motorRuntimeState).intValue()); + State fanSpeedState = callback.getState("dirigera:air-purifier:test-device:fan-speed"); + assertNotNull(fanSpeedState); + assertTrue(fanSpeedState instanceof PercentType); + assertEquals(20, ((PercentType) fanSpeedState).intValue()); + State fanModeState = callback.getState("dirigera:air-purifier:test-device:fan-mode"); + assertNotNull(fanModeState); + assertTrue(fanModeState instanceof DecimalType); + assertEquals(0, ((DecimalType) fanModeState).intValue()); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/blind/TestBlindHandler.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/blind/TestBlindHandler.java new file mode 100644 index 00000000000..e5b563158b6 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/blind/TestBlindHandler.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.blind; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.DirigeraAPISimu; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestBlindHandler} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestBlindHandler { + String deviceId = "eadfad54-9d23-4475-92b6-0ee3d6f8b481_1"; + ThingTypeUID thingTypeUID = THING_TYPE_BLIND; + + private static BlindHandler handler = mock(BlindHandler.class); + private static CallbackMock callback = mock(CallbackMock.class); + private static Thing thing = mock(Thing.class); + + @Test + void testHandlerCreation() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingHandler factoryHandler = DirigeraBridgeProvider.createHandler(thingTypeUID, hubBridge, deviceId); + assertTrue(factoryHandler instanceof BlindHandler); + handler = (BlindHandler) factoryHandler; + thing = handler.getThing(); + ThingHandlerCallback proxyCallback = handler.getCallback(); + assertNotNull(proxyCallback); + assertTrue(proxyCallback instanceof CallbackMock); + callback = (CallbackMock) proxyCallback; + handler.initialize(); + callback.waitForOnline(); + } + + @Test + void testInitialization() { + testHandlerCreation(); + assertNotNull(handler); + assertNotNull(thing); + assertNotNull(callback); + checkBlindStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_BATTERY_LEVEL), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_BLIND_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_BLIND_LEVEL), RefreshType.REFRESH); + checkBlindStates(callback); + } + + @Test + void testCommands() { + testHandlerCreation(); + // DirigeraAPISimu api = (DirigeraAPISimu) ((DirigeraHandler) hubBridge.getHandler()).api(); + handler.handleCommand(new ChannelUID(thing.getUID(), "blind-level"), new PercentType(20)); + String patch = DirigeraAPISimu.patchMap.get(deviceId); + assertNotNull(patch); + assertEquals("{\"attributes\":{\"blindsTargetLevel\":20}}", patch, "Target Blind Level"); + } + + void checkBlindStates(CallbackMock callback) { + State decimalState = callback.getState("dirigera:blind:test-device:blind-state"); + assertNotNull(decimalState); + assertTrue(decimalState instanceof DecimalType); + assertEquals(0, ((DecimalType) decimalState).intValue(), "Blind State"); + + State currentLevelState = callback.getState("dirigera:blind:test-device:blind-level"); + assertNotNull(currentLevelState); + assertTrue(currentLevelState instanceof PercentType); + assertEquals(60, ((PercentType) currentLevelState).intValue(), "Blind Level"); + + State batteryState = callback.getState("dirigera:blind:test-device:battery-level"); + assertNotNull(batteryState); + assertTrue(batteryState instanceof QuantityType); + assertTrue(((QuantityType) batteryState).getUnit().equals(Units.PERCENT)); + assertEquals(92, ((QuantityType) batteryState).intValue(), "Battery level"); + + // test ota + State otaStatus = callback.getState("dirigera:blind:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:blind:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:blind:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestBlindController.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestBlindController.java new file mode 100644 index 00000000000..f751078b610 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestBlindController.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.controller; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.HandlerFactoryMock; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestBlindController} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestBlindController { + String deviceId = "9f96eced-7674-4b9f-bbf9-b9575d888638_1"; + ThingTypeUID thingTypeUID = THING_TYPE_BLIND_CONTROLLER; + + @Test + void testHandlerCreation() { + HandlerFactoryMock hfm = new HandlerFactoryMock(mock(StorageService.class)); + assertTrue(hfm.supportsThingType(thingTypeUID)); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + ThingHandler th = hfm.createHandler(thing); + assertNotNull(th); + assertTrue(th instanceof BlindsControllerHandler); + } + + @Test + void testInitialization() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + BlindsControllerHandler handler = new BlindsControllerHandler(thing, BLIND_CONTROLLER_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", deviceId); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_BATTERY_LEVEL), RefreshType.REFRESH); + checkStates(callback); + } + + void checkStates(CallbackMock callback) { + State otaStatus = callback.getState("dirigera:blind-controller:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:blind-controller:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:blind-controller:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + State batteryState = callback.getState("dirigera:blind-controller:test-device:battery-level"); + assertNotNull(batteryState); + assertTrue(batteryState instanceof QuantityType); + assertTrue(((QuantityType) batteryState).getUnit().equals(Units.PERCENT)); + assertEquals(65, ((QuantityType) batteryState).intValue(), "Battery level"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestDoubleShortcutController.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestDoubleShortcutController.java new file mode 100644 index 00000000000..609a84d98e7 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestDoubleShortcutController.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.controller; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.FileReader; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.DirigeraAPISimu; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link TestDoubleShortcutController} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestDoubleShortcutController { + private final Logger logger = LoggerFactory.getLogger(TestDoubleShortcutController.class); + + String deviceId = "854bdf30-86b8-48f5-b070-16ff5ab12be4_1"; + ThingTypeUID thingTypeUID = THING_TYPE_DOUBLE_SHORTCUT_CONTROLLER; + + private static DoubleShortcutControllerHandler handler = mock(DoubleShortcutControllerHandler.class); + private static CallbackMock callback = mock(CallbackMock.class); + private static Thing thing = mock(Thing.class); + + @Test + void testHandlerCreation() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingHandler factoryHandler = DirigeraBridgeProvider.createHandler(thingTypeUID, hubBridge, deviceId); + assertTrue(factoryHandler instanceof DoubleShortcutControllerHandler); + handler = (DoubleShortcutControllerHandler) factoryHandler; + thing = handler.getThing(); + ThingHandlerCallback proxyCallback = handler.getCallback(); + assertNotNull(proxyCallback); + assertTrue(proxyCallback instanceof CallbackMock); + callback = (CallbackMock) proxyCallback; + } + + @Test + void testInitialization() { + testHandlerCreation(); + + handler.initialize(); + callback.waitForOnline(); + checkStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_BATTERY_LEVEL), RefreshType.REFRESH); + checkStates(callback); + } + + void checkStates(CallbackMock callback) { + State otaStatus = callback.getState("dirigera:double-shortcut:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:double-shortcut:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:double-shortcut:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + State batteryState = callback.getState("dirigera:double-shortcut:test-device:battery-level"); + assertNotNull(batteryState); + assertTrue(batteryState instanceof QuantityType); + assertTrue(((QuantityType) batteryState).getUnit().equals(Units.PERCENT)); + assertEquals(89, ((QuantityType) batteryState).intValue(), "Battery level"); + } + + @Test + void testTriggers() { + testInitialization(); + String updateSequence = FileReader + .readFileInString("src/test/resources/websocket/scene-pressed/scene-trigger-sequence.json"); + JSONArray sequence = new JSONArray(updateSequence); + Map sceneMapping = handler.sceneMapping; + // adapt id of scene to match created one + String sceneId = sceneMapping.get(deviceId + ":" + CHANNEL_BUTTON_1 + ":singlePress"); + assertNotNull(sceneId); + JSONObject first = sequence.getJSONObject(0).getJSONObject("data"); + first.put(PROPERTY_DEVICE_ID, sceneId); + handler.handleUpdate(first); + JSONObject second = sequence.getJSONObject(1).getJSONObject("data"); + second.put(PROPERTY_DEVICE_ID, sceneId); + handler.handleUpdate(second); + assertEquals("SHORT_PRESSED", callback.triggerMap.get("dirigera:double-shortcut:test-device:button1"), + "Pressed trigger sent"); + } + + @Test + void testRemoval() { + logger.warn("####### REMOVAL START ############"); + DirigeraAPISimu.scenesAdded.clear(); + DirigeraAPISimu.scenesDeleted.clear(); + testTriggers(); + logger.warn("####### TRIGGER CALLED ############"); + handler.dispose(); + handler.handleRemoval(); + Collections.sort(DirigeraAPISimu.scenesAdded); + Collections.sort(DirigeraAPISimu.scenesDeleted); + assertEquals(6, DirigeraAPISimu.scenesAdded.size(), "Scenes added size"); + assertEquals(6, DirigeraAPISimu.scenesDeleted.size(), "Scenes removed size"); + assertEquals(DirigeraAPISimu.scenesAdded, DirigeraAPISimu.scenesDeleted, "Scenes added equals scnes removed"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestLightController.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestLightController.java new file mode 100644 index 00000000000..73d64f2a9f3 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestLightController.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.controller; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.HandlerFactoryMock; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestLightController} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestLightController { + String deviceId = "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1"; + ThingTypeUID thingTypeUID = THING_TYPE_LIGHT_CONTROLLER; + + @Test + void testHandlerCreation() { + HandlerFactoryMock hfm = new HandlerFactoryMock(mock(StorageService.class)); + assertTrue(hfm.supportsThingType(thingTypeUID)); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + ThingHandler th = hfm.createHandler(thing); + assertNotNull(th); + assertTrue(th instanceof LightControllerHandler); + } + + @Test + void testInitialization() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + LightControllerHandler handler = new LightControllerHandler(thing, LIGHT_CONTROLLER_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", deviceId); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkLightControllerStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_BATTERY_LEVEL), RefreshType.REFRESH); + checkLightControllerStates(callback); + } + + void checkLightControllerStates(CallbackMock callback) { + State otaStatus = callback.getState("dirigera:light-controller:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:light-controller:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:light-controller:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + State batteryState = callback.getState("dirigera:light-controller:test-device:battery-level"); + assertNotNull(batteryState); + assertTrue(batteryState instanceof QuantityType); + assertTrue(((QuantityType) batteryState).getUnit().equals(Units.PERCENT)); + assertEquals(85, ((QuantityType) batteryState).intValue(), "Battery level"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestShortcutController.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestShortcutController.java new file mode 100644 index 00000000000..b2303d4620c --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestShortcutController.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.controller; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestShortcutController} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestShortcutController { + String deviceId = "92dbcea1-3d7e-4d6a-a009-bdf3a1ae6691_1"; + ThingTypeUID thingTypeUID = THING_TYPE_SINGLE_SHORTCUT_CONTROLLER; + + private static ShortcutControllerHandler handler = mock(ShortcutControllerHandler.class); + private static CallbackMock callback = mock(CallbackMock.class); + private static Thing thing = mock(Thing.class); + + @Test + void testHandlerCreation() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingHandler factoryHandler = DirigeraBridgeProvider.createHandler(thingTypeUID, hubBridge, deviceId); + assertTrue(factoryHandler instanceof ShortcutControllerHandler); + handler = (ShortcutControllerHandler) factoryHandler; + thing = handler.getThing(); + ThingHandlerCallback proxyCallback = handler.getCallback(); + assertNotNull(proxyCallback); + assertTrue(proxyCallback instanceof CallbackMock); + callback = (CallbackMock) proxyCallback; + handler.initialize(); + callback.waitForOnline(); + } + + @Test + void testInitialization() { + testHandlerCreation(); + assertNotNull(handler); + assertNotNull(thing); + assertNotNull(callback); + checkStates(callback); + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_BATTERY_LEVEL), RefreshType.REFRESH); + checkStates(callback); + } + + void checkStates(CallbackMock callback) { + State otaStatus = callback.getState("dirigera:single-shortcut:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:single-shortcut:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:single-shortcut:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + State batteryState = callback.getState("dirigera:single-shortcut:test-device:battery-level"); + assertNotNull(batteryState); + assertTrue(batteryState instanceof QuantityType); + assertTrue(((QuantityType) batteryState).getUnit().equals(Units.PERCENT)); + assertEquals(100, ((QuantityType) batteryState).intValue(), "Battery level"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestSoundController.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestSoundController.java new file mode 100644 index 00000000000..3610f7542d0 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/controller/TestSoundController.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.controller; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.HandlerFactoryMock; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestSoundController} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestSoundController { + String deviceId = "cec4c170-7846-4e22-b681-d8a912181cca_1"; + + @Test + void testHandlerCreation() { + HandlerFactoryMock hfm = new HandlerFactoryMock(mock(StorageService.class)); + assertTrue(hfm.supportsThingType(THING_TYPE_SOUND_CONTROLLER)); + ThingImpl thing = new ThingImpl(THING_TYPE_SOUND_CONTROLLER, "test-device"); + ThingHandler th = hfm.createHandler(thing); + assertNotNull(th); + assertTrue(th instanceof SoundControllerHandler); + } + + @Test + void testInitialization() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingImpl thing = new ThingImpl(THING_TYPE_SOUND_CONTROLLER, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + SoundControllerHandler handler = new SoundControllerHandler(thing, SOUND_CONTROLLER_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", deviceId); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_BATTERY_LEVEL), RefreshType.REFRESH); + checkStates(callback); + } + + void checkStates(CallbackMock callback) { + State otaStatus = callback.getState("dirigera:sound-controller:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:sound-controller:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:sound-controller:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + State batteryState = callback.getState("dirigera:sound-controller:test-device:battery-level"); + assertNotNull(batteryState); + assertTrue(batteryState instanceof QuantityType); + assertTrue(((QuantityType) batteryState).getUnit().equals(Units.PERCENT)); + assertEquals(90, ((QuantityType) batteryState).intValue(), "Battery level"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestColorLight.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestColorLight.java new file mode 100644 index 00000000000..ee687bb05ea --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestColorLight.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.lights; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.DirigeraStateDescriptionProvider; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.handler.light.ColorLightHandler; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.HandlerFactoryMock; +import org.openhab.core.events.EventPublisher; +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.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestColorLight} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestColorLight { + String deviceId = "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1"; + ThingTypeUID thingTypeUID = THING_TYPE_COLOR_LIGHT; + + @Test + void testHandlerCreation() { + HandlerFactoryMock hfm = new HandlerFactoryMock(mock(StorageService.class)); + assertTrue(hfm.supportsThingType(thingTypeUID)); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + ThingHandler th = hfm.createHandler(thing); + assertNotNull(th); + assertTrue(th instanceof ColorLightHandler); + } + + @Test + void testInitialization() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + ColorLightHandler handler = new ColorLightHandler(thing, COLOR_LIGHT_MAP, + new DirigeraStateDescriptionProvider(mock(EventPublisher.class), mock(ItemChannelLinkRegistry.class), + mock(ChannelTypeI18nLocalizationService.class))); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", deviceId); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkColorLightStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_COLOR), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_STARTUP_BEHAVIOR), RefreshType.REFRESH); + checkColorLightStates(callback); + } + + void checkColorLightStates(CallbackMock callback) { + State onOffState = callback.getState("dirigera:color-light:test-device:power"); + assertNotNull(onOffState); + assertTrue(onOffState instanceof OnOffType); + assertTrue(OnOffType.ON.equals((onOffState)), "Power State"); + State hsbState = callback.getState("dirigera:color-light:test-device:color"); + assertNotNull(hsbState); + assertTrue(hsbState instanceof HSBType); + assertEquals(119, ((HSBType) hsbState).getHue().intValue(), "Hue"); + assertEquals(70, ((HSBType) hsbState).getSaturation().intValue(), "Saturation"); + // brightness of device is 100 (previous state) but due to power OFF state it's reflected as 0 + assertEquals(0, ((HSBType) hsbState).getBrightness().intValue(), "Brightness"); + // assertEquals(100, ((HSBType) hsbState).getBrightness().intValue(), "Brightness"); + + // test ota + State otaStatus = callback.getState("dirigera:color-light:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:color-light:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:color-light:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + State startupState = callback.getState("dirigera:color-light:test-device:startup"); + assertNotNull(startupState); + assertTrue(startupState instanceof DecimalType); + assertEquals(1, ((DecimalType) startupState).intValue(), "Startup Behavior"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestDimmableLight.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestDimmableLight.java new file mode 100644 index 00000000000..257b68bdbd0 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestDimmableLight.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.lights; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.handler.light.DimmableLightHandler; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.HandlerFactoryMock; +import org.openhab.core.library.types.DecimalType; +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.unit.Units; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link TestDimmableLight} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestDimmableLight { + private final Logger logger = LoggerFactory.getLogger(TestDimmableLight.class); + String deviceId = "eb9a4367-9e23-4d37-9566-401a7ae7caf0_1"; + ThingTypeUID thingTypeUID = THING_TYPE_DIMMABLE_LIGHT; + + @Test + void testHandlerCreation() { + HandlerFactoryMock hfm = new HandlerFactoryMock(mock(StorageService.class)); + assertTrue(hfm.supportsThingType(thingTypeUID)); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + ThingHandler th = hfm.createHandler(thing); + assertNotNull(th); + assertTrue(th instanceof DimmableLightHandler); + } + + @Test + void testInitialization() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + DimmableLightHandler handler = new DimmableLightHandler(thing, TEMPERATURE_LIGHT_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", deviceId); + config.put("fadeTime", 0); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkLightStates(callback); + + callback.clear(); + logger.warn("Refresh"); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_BRIGHTNESS), RefreshType.REFRESH); + logger.warn("Check"); + checkLightStates(callback); + } + + void checkLightStates(CallbackMock callback) { + State onOffState = callback.getState("dirigera:dimmable-light:test-device:power"); + assertNotNull(onOffState); + assertTrue(onOffState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((onOffState)), "Power State"); + + State brightnessState = callback.getState("dirigera:dimmable-light:test-device:brightness"); + assertNotNull(brightnessState); + assertTrue(brightnessState instanceof PercentType); + // device brightness is 100 but due to isOn=false brightness shall reflect 0 + assertEquals(0, ((PercentType) brightnessState).intValue(), "Brightness"); + + // test ota + State otaStatus = callback.getState("dirigera:dimmable-light:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:dimmable-light:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:dimmable-light:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestSwitchLight.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestSwitchLight.java new file mode 100644 index 00000000000..cff77fafd00 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestSwitchLight.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.lights; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.handler.light.SwitchLightHandler; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.DirigeraAPISimu; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestSwitchLight} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestSwitchLight { + String deviceId = "eb9a4367-9e23-4d37-9566-401a7ae7caf0_2"; + ThingTypeUID thingTypeUID = THING_TYPE_SWITCH_LIGHT; + + private static SwitchLightHandler handler = mock(SwitchLightHandler.class); + private static CallbackMock callback = mock(CallbackMock.class); + private static Thing thing = mock(Thing.class); + + @Test + void testHandlerCreation() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingHandler factoryHandler = DirigeraBridgeProvider.createHandler(thingTypeUID, hubBridge, deviceId); + assertTrue(factoryHandler instanceof SwitchLightHandler); + handler = (SwitchLightHandler) factoryHandler; + thing = handler.getThing(); + ThingHandlerCallback proxyCallback = handler.getCallback(); + assertNotNull(proxyCallback); + assertTrue(proxyCallback instanceof CallbackMock); + callback = (CallbackMock) proxyCallback; + handler.initialize(); + callback.waitForOnline(); + } + + @Test + void testInitialization() { + testHandlerCreation(); + assertNotNull(handler); + assertNotNull(thing); + assertNotNull(callback); + chackStatus(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_STARTUP_BEHAVIOR), RefreshType.REFRESH); + chackStatus(callback); + } + + @Test + void testCommands() { + DirigeraAPISimu.patchMap.clear(); + testHandlerCreation(); + // DirigeraAPISimu api = (DirigeraAPISimu) ((DirigeraHandler) hubBridge.getHandler()).api(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), OnOffType.ON); + DirigeraAPISimu.waitForPatch(); + String patch = DirigeraAPISimu.patchMap.get(deviceId); + assertNotNull(patch); + assertEquals("{\"attributes\":{\"isOn\":true}}", patch, "Power on"); + DirigeraAPISimu.patchMap.clear(); + + // simulate feedback + handler.handleUpdate(new JSONObject("{\"attributes\": {\"isOn\": true}}")); + + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), OnOffType.OFF); + DirigeraAPISimu.waitForPatch(); + patch = DirigeraAPISimu.patchMap.get(deviceId); + assertNotNull(patch); + assertEquals("{\"attributes\":{\"isOn\":false}}", patch, "Power off"); + } + + void chackStatus(CallbackMock callback) { + State onOffState = callback.getState("dirigera:switch-light:test-device:power"); + assertNotNull(onOffState); + assertTrue(onOffState instanceof OnOffType); + assertEquals(OnOffType.OFF, onOffState, "Power Status"); + + State startupState = callback.getState("dirigera:switch-light:test-device:startup"); + assertNotNull(startupState); + assertTrue(startupState instanceof DecimalType); + assertEquals(1, ((DecimalType) startupState).intValue(), "Startup State"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestTemperatureLight.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestTemperatureLight.java new file mode 100644 index 00000000000..383efa8d8a3 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/lights/TestTemperatureLight.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.lights; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.DirigeraStateDescriptionProvider; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.handler.light.TemperatureLightHandler; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.HandlerFactoryMock; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.library.types.DecimalType; +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.unit.Units; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestTemperatureLight} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestTemperatureLight { + + String deviceId = "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1"; + ThingTypeUID thingTypeUID = THING_TYPE_TEMPERATURE_LIGHT; + + @Test + void testHandlerCreation() { + HandlerFactoryMock hfm = new HandlerFactoryMock(mock(StorageService.class)); + assertTrue(hfm.supportsThingType(thingTypeUID)); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + ThingHandler th = hfm.createHandler(thing); + assertNotNull(th); + assertTrue(th instanceof TemperatureLightHandler); + } + + @Test + void testInitialization() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + TemperatureLightHandler handler = new TemperatureLightHandler(thing, TEMPERATURE_LIGHT_MAP, + new DirigeraStateDescriptionProvider(mock(EventPublisher.class), mock(ItemChannelLinkRegistry.class), + mock(ChannelTypeI18nLocalizationService.class))); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", deviceId); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkLightStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_BRIGHTNESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_LIGHT_TEMPERATURE_ABS), RefreshType.REFRESH); + checkLightStates(callback); + } + + void checkLightStates(CallbackMock callback) { + State onOffState = callback.getState("dirigera:temperature-light:test-device:power"); + assertNotNull(onOffState); + assertTrue(onOffState instanceof OnOffType); + assertTrue(OnOffType.ON.equals((onOffState)), "On"); + + State temperatureState = callback.getState("dirigera:temperature-light:test-device:color-temperature"); + assertNotNull(temperatureState); + assertTrue(temperatureState instanceof PercentType); + assertEquals(0, ((PercentType) temperatureState).intValue(), "Temperature"); + + State temperatureAbsState = callback.getState("dirigera:temperature-light:test-device:color-temperature-abs"); + assertNotNull(temperatureAbsState); + assertTrue(temperatureAbsState instanceof QuantityType); + assertEquals(4000, ((QuantityType) temperatureAbsState).intValue(), "Temperature Abs"); + + State brightnessState = callback.getState("dirigera:temperature-light:test-device:brightness"); + assertNotNull(brightnessState); + assertTrue(brightnessState instanceof PercentType); + assertEquals(49, ((PercentType) brightnessState).intValue(), "Brightness"); + + // test ota + State otaStatus = callback.getState("dirigera:temperature-light:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:temperature-light:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:temperature-light:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/repeater/TestRepeater.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/repeater/TestRepeater.java new file mode 100644 index 00000000000..988dfe53c91 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/repeater/TestRepeater.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.repeater; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestRepeater} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestRepeater { + + @Test + void testRepeater() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + ThingImpl thing = new ThingImpl(THING_TYPE_REPEATER, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + RepeaterHandler handler = new RepeaterHandler(thing, REPEATER_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", "044b63e7-999d-4caa-8a76-fb8cfd32b381_1"); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkRepeaterStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + checkRepeaterStates(callback); + } + + void checkRepeaterStates(CallbackMock callback) { + State otaStatus = callback.getState("dirigera:repeater:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:repeater:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:repeater:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/scene/TestScenes.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/scene/TestScenes.java new file mode 100644 index 00000000000..c1edc1bc3c6 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/scene/TestScenes.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.scene; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.time.ZoneId; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestSCene} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestScene { + + @Test + void testSceneHandler() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + ThingImpl thing = new ThingImpl(THING_TYPE_SCENE, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + SceneHandler handler = new SceneHandler(thing, SCENE_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58"); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkSceneStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_LAST_TRIGGER), RefreshType.REFRESH); + checkSceneStates(callback); + } + + void checkSceneStates(CallbackMock callback) { + State dateTimeState = callback.getState("dirigera:scene:test-device:last-trigger"); + assertNotNull(dateTimeState); + assertTrue(dateTimeState instanceof DateTimeType); + assertEquals("2024-10-16T02:21:15.977+0200", + ((DateTimeType) dateTimeState).toFullString(ZoneId.of("Europe/Berlin")), "Last trigger"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestAirQualityDevice.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestAirQualityDevice.java new file mode 100644 index 00000000000..d146426a0e6 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestAirQualityDevice.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestAirQualityDevice} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestAirQualityDevice { + + @Test + void testAirQualityDeviceWithSimuBridge() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + testAirQuality(hubBridge); + } + + void testAirQuality(Bridge hubBridge) { + ThingImpl thing = new ThingImpl(THING_TYPE_AIR_QUALITY, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + AirQualityHandler handler = new AirQualityHandler(thing, AIR_QUALITY_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", "f80cac12-65a4-47b4-9f68-a0456a349a43_1"); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkAirQualityStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_TEMPERATURE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_HUMIDITY), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_PARTICULATE_MATTER), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_VOC_INDEX), RefreshType.REFRESH); + checkAirQualityStates(callback); + } + + void checkAirQualityStates(CallbackMock callback) { + State otaStatus = callback.getState("dirigera:air-quality:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:air-quality:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:air-quality:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + + State temperatureState = callback.getState("dirigera:air-quality:test-device:temperature"); + assertNotNull(temperatureState); + assertTrue(temperatureState instanceof QuantityType); + assertTrue(((QuantityType) temperatureState).getUnit().equals(SIUnits.CELSIUS)); + assertEquals(20, ((QuantityType) temperatureState).intValue(), "Temperature"); + + State humidityState = callback.getState("dirigera:air-quality:test-device:humidity"); + assertNotNull(humidityState); + assertTrue(humidityState instanceof QuantityType); + assertTrue(((QuantityType) humidityState).getUnit().equals(Units.PERCENT)); + assertEquals(76, ((QuantityType) humidityState).intValue(), "Hunidity"); + State ppmState = callback.getState("dirigera:air-quality:test-device:particulate-matter"); + assertNotNull(ppmState); + assertTrue(ppmState instanceof QuantityType); + assertTrue(((QuantityType) ppmState).getUnit().equals(Units.MICROGRAM_PER_CUBICMETRE)); + assertEquals(11, ((QuantityType) ppmState).intValue(), "ppm"); + State vocState = callback.getState("dirigera:air-quality:test-device:voc-index"); + assertNotNull(vocState); + assertTrue(vocState instanceof DecimalType); + assertEquals(100, ((DecimalType) vocState).intValue(), "VOC Index"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestContactDevice.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestContactDevice.java new file mode 100644 index 00000000000..357ff17dcfe --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestContactDevice.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestContactDevice} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestContactDevice { + @Test + void testContactDevice() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + ThingImpl thing = new ThingImpl(THING_TYPE_CONTACT_SENSOR, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + ContactSensorHandler handler = new ContactSensorHandler(thing, CONTACT_SENSOR_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1"); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkContactStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_CONTACT), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_BATTERY_LEVEL), RefreshType.REFRESH); + checkContactStates(callback); + } + + void checkContactStates(CallbackMock callback) { + State batteryState = callback.getState("dirigera:contact-sensor:test-device:battery-level"); + assertNotNull(batteryState); + assertTrue(batteryState instanceof QuantityType); + assertTrue(((QuantityType) batteryState).getUnit().equals(Units.PERCENT)); + assertEquals(84, ((QuantityType) batteryState).intValue(), "Battery level"); + State openCloseState = callback.getState("dirigera:contact-sensor:test-device:contact"); + assertNotNull(openCloseState); + assertTrue(openCloseState instanceof OpenClosedType); + assertTrue(OpenClosedType.CLOSED.equals((openCloseState)), "Closed"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestLightSensor.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestLightSensor.java new file mode 100644 index 00000000000..49e2793f590 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestLightSensor.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestLightSensor} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestLightSensor { + + void testLightSensorDevice() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + ThingImpl thing = new ThingImpl(THING_TYPE_LIGHT_SENSOR, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + LightSensorHandler handler = new LightSensorHandler(thing, LIGHT_SENSOR_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3"); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + + State luxState = callback.getState("dirigera:light-sensor:test-device:illuminance"); + assertNotNull(luxState); + assertTrue(luxState instanceof QuantityType); + assertTrue(((QuantityType) luxState).getUnit().equals(Units.LUX)); + assertEquals(1, ((QuantityType) luxState).intValue(), "Lux level"); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_ILLUMINANCE), RefreshType.REFRESH); + luxState = callback.getState("dirigera:light-sensor:test-device:illuminance"); + assertNotNull(luxState); + assertTrue(luxState instanceof QuantityType); + assertTrue(((QuantityType) luxState).getUnit().equals(Units.LUX)); + assertEquals(1, ((QuantityType) luxState).intValue(), "Lux level"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestMotionLightSensor.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestMotionLightSensor.java new file mode 100644 index 00000000000..850d5583e24 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestMotionLightSensor.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +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.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestMotionLightSensor} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestMotionLightSensor { + + @Test + void testMotionLightSensorDevice() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + ThingImpl thing = new ThingImpl(THING_TYPE_MOTION_LIGHT_SENSOR, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + MotionLightSensorHandler handler = new MotionLightSensorHandler(thing, MOTION_LIGHT_SENSOR_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", "5ac5e131-44a4-4d75-be78-759a095d31fb_1"); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkMotionLightStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_ILLUMINANCE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_MOTION_DETECTION), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_BATTERY_LEVEL), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_CUSTOM_NAME), RefreshType.REFRESH); + checkMotionLightStates(callback); + } + + void checkMotionLightStates(CallbackMock callback) { + State batteryState = callback.getState("dirigera:motion-light-sensor:test-device:battery-level"); + assertNotNull(batteryState); + assertTrue(batteryState instanceof QuantityType); + assertTrue(((QuantityType) batteryState).getUnit().equals(Units.PERCENT)); + assertEquals(85, ((QuantityType) batteryState).intValue(), "Battery level"); + State onOffState = callback.getState("dirigera:motion-light-sensor:test-device:motion"); + assertNotNull(onOffState); + assertTrue(onOffState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((onOffState)), "Off"); + State luxState = callback.getState("dirigera:motion-light-sensor:test-device:illuminance"); + assertNotNull(luxState); + assertTrue(luxState instanceof QuantityType); + assertTrue(((QuantityType) luxState).getUnit().equals(Units.LUX)); + assertEquals(1, ((QuantityType) luxState).intValue(), "Lux level"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestMotionSensor.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestMotionSensor.java new file mode 100644 index 00000000000..bf4449f998c --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestMotionSensor.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.core.library.types.DecimalType; +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.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestMotionSensor} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestMotionSensor { + + @Test + void testMotionSensorDevice() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + ThingImpl thing = new ThingImpl(THING_TYPE_MOTION_SENSOR, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + MotionSensorHandler handler = new MotionSensorHandler(thing, MOTION_SENSOR_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", "ee61c57f-8efa-44f4-ba8a-d108ae054138_1"); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkMotionStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_BATTERY_LEVEL), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_MOTION_DETECTION), RefreshType.REFRESH); + checkMotionStates(callback); + + // check commands + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_ACTIVE_DURATION), new DecimalType(10)); + // assertEquals(String.format(MotionSensorHandler.DURATION_UPDATE, 10), + // DirigeraAPISimu.patchMap.get("ee61c57f-8efa-44f4-ba8a-d108ae054138_1"), "10 seconds"); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_ACTIVE_DURATION), QuantityType.valueOf("3 min")); + // assertEquals(String.format(MotionSensorHandler.DURATION_UPDATE, 180), + // DirigeraAPISimu.patchMap.get("ee61c57f-8efa-44f4-ba8a-d108ae054138_1"), "10 seconds"); + } + + void checkMotionStates(CallbackMock callback) { + State batteryState = callback.getState("dirigera:motion-sensor:test-device:battery-level"); + assertNotNull(batteryState); + assertTrue(batteryState instanceof QuantityType); + assertTrue(((QuantityType) batteryState).getUnit().equals(Units.PERCENT)); + assertEquals(20, ((QuantityType) batteryState).intValue(), "Battery level"); + State onOffState = callback.getState("dirigera:motion-sensor:test-device:motion"); + assertNotNull(onOffState); + assertTrue(onOffState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((onOffState)), "Motion detected"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestWaterSensor.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestWaterSensor.java new file mode 100644 index 00000000000..0ba7cc890c3 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/sensor/TestWaterSensor.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.sensor; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.core.library.types.DecimalType; +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.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestWaterSensor} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestWaterSensor { + @Test + void testWaterSensor() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + ThingImpl thing = new ThingImpl(THING_TYPE_WATER_SENSOR, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + WaterSensorHandler handler = new WaterSensorHandler(thing, WATER_SENSOR_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", "9af826ad-a8ad-40bf-8aed-125300bccd20_1"); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkWaterSensorStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_LEAK_DETECTION), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_BATTERY_LEVEL), RefreshType.REFRESH); + checkWaterSensorStates(callback); + } + + void checkWaterSensorStates(CallbackMock callback) { + // test ota & battery + State otaStatus = callback.getState("dirigera:water-sensor:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:water-sensor:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:water-sensor:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + + State onOffState = callback.getState("dirigera:water-sensor:test-device:leak"); + assertNotNull(onOffState); + assertTrue(onOffState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((onOffState)), "Off"); + State batteryState = callback.getState("dirigera:water-sensor:test-device:battery-level"); + assertNotNull(batteryState); + assertTrue(batteryState instanceof QuantityType); + assertTrue(((QuantityType) batteryState).getUnit().equals(Units.PERCENT)); + assertEquals(55, ((QuantityType) batteryState).intValue(), "Battery level"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/speaker/TestSpeaker.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/speaker/TestSpeaker.java new file mode 100644 index 00000000000..49aafcd3869 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handler/speaker/TestSpeaker.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handler.speaker; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.RawType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestSpeaker} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestSpeaker { + @Test + void testSpeakerDevice() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + ThingImpl thing = new ThingImpl(THING_TYPE_SPEAKER, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + SpeakerHandler handler = new SpeakerHandler(thing, SPEAKER_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", "338bb721-35bb-4775-8cd0-ba70fc37ab10_1"); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkSpeakerStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_CROSSFADE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_SHUFFLE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_MUTE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_REPEAT), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_VOLUME), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_TRACK), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_IMAGE), RefreshType.REFRESH); + checkSpeakerStates(callback); + } + + void checkSpeakerStates(CallbackMock callback) { + State crossfadeState = callback.getState("dirigera:speaker:test-device:crossfade"); + assertNotNull(crossfadeState); + assertTrue(crossfadeState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((crossfadeState)), "Crossfade Off"); + State shuffleState = callback.getState("dirigera:speaker:test-device:shuffle"); + assertNotNull(shuffleState); + assertTrue(shuffleState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((shuffleState)), "Shuffle Off"); + State muteState = callback.getState("dirigera:speaker:test-device:mute"); + assertNotNull(muteState); + assertTrue(muteState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((muteState)), "Mute Off"); + State repeatState = callback.getState("dirigera:speaker:test-device:repeat"); + assertNotNull(repeatState); + assertTrue(repeatState instanceof DecimalType); + assertEquals(0, ((DecimalType) repeatState).intValue(), "Repeat setting"); + State volumeState = callback.getState("dirigera:speaker:test-device:volume"); + assertNotNull(volumeState); + assertTrue(volumeState instanceof PercentType); + assertEquals(16, ((PercentType) volumeState).intValue(), "Volume"); + State trackState = callback.getState("dirigera:speaker:test-device:media-title"); + assertNotNull(trackState); + assertTrue(trackState instanceof StringType); + assertTrue(((StringType) trackState).toFullString().startsWith("The Anjunadeep Edition")); + State pictureState = callback.getState("dirigera:speaker:test-device:image"); + assertNotNull(pictureState); + assertTrue(pictureState instanceof RawType); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handlerplug/TestPowerPlug.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handlerplug/TestPowerPlug.java new file mode 100644 index 00000000000..e7db115a206 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handlerplug/TestPowerPlug.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handlerplug; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.handler.plug.PowerPlugHandler; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.HandlerFactoryMock; +import org.openhab.core.library.types.DecimalType; +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.storage.StorageService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestPowerPlug} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestPowerPlug { + String deviceId = "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1"; + ThingTypeUID thingTypeUID = THING_TYPE_POWER_PLUG; + + @Test + void testHandlerCreation() { + HandlerFactoryMock hfm = new HandlerFactoryMock(mock(StorageService.class)); + assertTrue(hfm.supportsThingType(thingTypeUID)); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + ThingHandler th = hfm.createHandler(thing); + assertNotNull(th); + assertTrue(th instanceof PowerPlugHandler); + } + + @Test + void testInitialization() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + PowerPlugHandler handler = new PowerPlugHandler(thing, SMART_PLUG_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", deviceId); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkPowerPlugStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_CHILD_LOCK), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_DISABLE_STATUS_LIGHT), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_STARTUP_BEHAVIOR), RefreshType.REFRESH); + checkPowerPlugStates(callback); + } + + void checkPowerPlugStates(CallbackMock callback) { + State otaStatus = callback.getState("dirigera:power-plug:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:power-plug:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:power-plug:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + + State disableLightState = callback.getState("dirigera:power-plug:test-device:disable-status-light"); + assertNotNull(disableLightState); + assertTrue(disableLightState instanceof OnOffType); + assertTrue(OnOffType.ON.equals((disableLightState)), "Disable Light On"); + State childlockState = callback.getState("dirigera:power-plug:test-device:child-lock"); + assertNotNull(childlockState); + assertTrue(childlockState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((childlockState)), "Child Lock Off"); + State onOffState = callback.getState("dirigera:power-plug:test-device:power"); + assertNotNull(onOffState); + assertTrue(onOffState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((onOffState)), "Power Off"); + State startupState = callback.getState("dirigera:power-plug:test-device:startup"); + assertNotNull(startupState); + assertTrue(startupState instanceof DecimalType); + assertEquals(0, ((DecimalType) startupState).intValue(), "Startup Behavior"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handlerplug/TestSimplePlug.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handlerplug/TestSimplePlug.java new file mode 100644 index 00000000000..5ce5d1b92ba --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handlerplug/TestSimplePlug.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handlerplug; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.handler.plug.SimplePlugHandler; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.HandlerFactoryMock; +import org.openhab.core.library.types.DecimalType; +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.storage.StorageService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestSimplePlug} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestSimplePlug { + String deviceId = "6379a590-dc0a-47b5-b35b-7b46dfefd282_1"; + ThingTypeUID thingTypeUID = THING_TYPE_SIMPLE_PLUG; + + @Test + void testHandlerCreation() { + HandlerFactoryMock hfm = new HandlerFactoryMock(mock(StorageService.class)); + assertTrue(hfm.supportsThingType(thingTypeUID)); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + ThingHandler th = hfm.createHandler(thing); + assertNotNull(th); + assertTrue(th instanceof SimplePlugHandler); + } + + @Test + void testInitialization() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingImpl thing = new ThingImpl(thingTypeUID, "test-device"); + thing.setBridgeUID(hubBridge.getBridgeUID()); + SimplePlugHandler handler = new SimplePlugHandler(thing, SMART_PLUG_MAP); + CallbackMock callback = new CallbackMock(); + callback.setBridge(hubBridge); + handler.setCallback(callback); + + // set the right id + Map config = new HashMap<>(); + config.put("id", deviceId); + handler.handleConfigurationUpdate(config); + + handler.initialize(); + callback.waitForOnline(); + checkPowerPlugStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_CHILD_LOCK), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_DISABLE_STATUS_LIGHT), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_STARTUP_BEHAVIOR), RefreshType.REFRESH); + checkPowerPlugStates(callback); + } + + void checkPowerPlugStates(CallbackMock callback) { + State otaStatus = callback.getState("dirigera:simple-plug:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:simple-plug:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:simple-plug:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + + State disableLightState = callback.getState("dirigera:simple-plug:test-device:disable-light"); + assertNull(disableLightState); + State childlockState = callback.getState("dirigera:simple-plug:test-device:child-lock"); + assertNull(childlockState); + State onOffState = callback.getState("dirigera:simple-plug:test-device:power"); + assertNotNull(onOffState); + assertTrue(onOffState instanceof OnOffType); + assertTrue(OnOffType.ON.equals((onOffState)), "Power On"); + State startupState = callback.getState("dirigera:simple-plug:test-device:startup"); + assertNotNull(startupState); + assertTrue(startupState instanceof DecimalType); + assertEquals(0, ((DecimalType) startupState).intValue(), "Startup Behavior"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handlerplug/TestSmartPlug.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handlerplug/TestSmartPlug.java new file mode 100644 index 00000000000..cd153b02987 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/handlerplug/TestSmartPlug.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.handlerplug; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.handler.plug.SmartPlugHandler; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.DirigeraAPISimu; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +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.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +/** + * {@link TestSmartPlug} Tests device handler creation, initializing and refresh of channels + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestSmartPlug { + String deviceId = "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1"; + ThingTypeUID thingTypeUID = THING_TYPE_SMART_PLUG; + + private static SmartPlugHandler handler = mock(SmartPlugHandler.class); + private static CallbackMock callback = mock(CallbackMock.class); + private static Thing thing = mock(Thing.class); + + @Test + void testHandlerCreation() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/devices/home-all-devices.json", + false, List.of()); + ThingHandler factoryHandler = DirigeraBridgeProvider.createHandler(thingTypeUID, hubBridge, deviceId); + assertTrue(factoryHandler instanceof SmartPlugHandler); + handler = (SmartPlugHandler) factoryHandler; + thing = handler.getThing(); + ThingHandlerCallback proxyCallback = handler.getCallback(); + assertNotNull(proxyCallback); + assertTrue(proxyCallback instanceof CallbackMock); + callback = (CallbackMock) proxyCallback; + handler.initialize(); + callback.waitForOnline(); + } + + @Test + void testInitialization() { + testHandlerCreation(); + assertNotNull(handler); + assertNotNull(thing); + assertNotNull(callback); + checkSmartPlugStates(callback); + + callback.clear(); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATUS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_OTA_PROGRESS), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER_STATE), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_CHILD_LOCK), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_DISABLE_STATUS_LIGHT), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_CURRENT), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POWER), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_POTENTIAL), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_STARTUP_BEHAVIOR), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_ENERGY_TOTAL), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_ENERGY_RESET), RefreshType.REFRESH); + checkSmartPlugStates(callback); + } + + @Test + void testCommands() { + DirigeraAPISimu.patchMap.clear(); + testHandlerCreation(); + assertNotNull(handler); + assertNotNull(thing); + assertNotNull(callback); + handler.handleCommand(new ChannelUID(thing.getUID(), CHANNEL_ENERGY_RESET_DATE), new DateTimeType()); + DirigeraAPISimu.waitForPatch(); + String patch = DirigeraAPISimu.patchMap.get(deviceId); + assertNotNull(patch); + assertEquals("{\"attributes\":{\"energyConsumedAtLastReset\":0}}", patch, "Reset Power"); + DirigeraAPISimu.patchMap.clear(); + } + + void checkSmartPlugStates(CallbackMock callback) { + State otaStatus = callback.getState("dirigera:smart-plug:test-device:ota-status"); + assertNotNull(otaStatus); + assertTrue(otaStatus instanceof DecimalType); + assertEquals(0, ((DecimalType) otaStatus).intValue(), "OTA Status"); + State otaState = callback.getState("dirigera:smart-plug:test-device:ota-state"); + assertNotNull(otaState); + assertTrue(otaState instanceof DecimalType); + assertEquals(0, ((DecimalType) otaState).intValue(), "OTA State"); + State otaProgess = callback.getState("dirigera:smart-plug:test-device:ota-progress"); + assertNotNull(otaProgess); + assertTrue(otaProgess instanceof QuantityType); + assertTrue(((QuantityType) otaProgess).getUnit().equals(Units.PERCENT)); + assertEquals(0, ((QuantityType) otaProgess).intValue(), "OTA Progress"); + + State disableLightState = callback.getState("dirigera:smart-plug:test-device:disable-status-light"); + assertNotNull(disableLightState); + assertTrue(disableLightState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((disableLightState)), "Disable Light Off"); + State childlockState = callback.getState("dirigera:smart-plug:test-device:child-lock"); + assertNotNull(childlockState); + assertTrue(childlockState instanceof OnOffType); + assertTrue(OnOffType.OFF.equals((childlockState)), "Child Lock Off"); + State onOffState = callback.getState("dirigera:smart-plug:test-device:power"); + assertNotNull(onOffState); + assertTrue(onOffState instanceof OnOffType); + assertTrue(OnOffType.ON.equals((onOffState)), "Power On"); + + // Measurement channels + State ampereState = callback.getState("dirigera:smart-plug:test-device:electric-current"); + assertNotNull(ampereState); + assertTrue(ampereState instanceof QuantityType); + assertTrue(((QuantityType) ampereState).getUnit().equals(Units.AMPERE)); + assertEquals(0, ((QuantityType) ampereState).intValue(), "Ampere"); + State voltState = callback.getState("dirigera:smart-plug:test-device:electric-voltage"); + assertNotNull(voltState); + assertTrue(voltState instanceof QuantityType); + assertTrue(((QuantityType) voltState).getUnit().equals(Units.VOLT)); + assertEquals(236, ((QuantityType) voltState).intValue(), "Volt"); + State powerState = callback.getState("dirigera:smart-plug:test-device:electric-power"); + assertNotNull(powerState); + assertTrue(powerState instanceof QuantityType); + assertTrue(((QuantityType) powerState).getUnit().equals(Units.WATT)); + assertEquals(0, ((QuantityType) powerState).intValue(), "Watt"); + State energyTotalState = callback.getState("dirigera:smart-plug:test-device:energy-total"); + assertNotNull(energyTotalState); + assertTrue(energyTotalState instanceof QuantityType); + assertTrue(((QuantityType) energyTotalState).getUnit().equals(Units.KILOWATT_HOUR)); + assertEquals(0, ((QuantityType) energyTotalState).intValue(), "Watt"); + State energyReset = callback.getState("dirigera:smart-plug:test-device:energy-reset"); + assertNotNull(energyReset); + assertTrue(energyReset instanceof QuantityType); + assertTrue(((QuantityType) energyReset).getUnit().equals(Units.KILOWATT_HOUR)); + assertEquals(0, ((QuantityType) energyReset).intValue(), "Watt"); + + State startupState = callback.getState("dirigera:smart-plug:test-device:startup"); + assertNotNull(startupState); + assertTrue(startupState instanceof DecimalType); + assertEquals(0, ((DecimalType) startupState).intValue(), "Startup Behavior"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/CallbackMock.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/CallbackMock.java new file mode 100644 index 00000000000..02587622a66 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/CallbackMock.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.mock; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.core.ConfigDescription; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelGroupUID; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder; +import org.openhab.core.thing.type.ChannelGroupTypeUID; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.TimeSeries; + +/** + * The {@link CallbackMock} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class CallbackMock implements ThingHandlerCallback { + private @Nullable Bridge bridge; + private ThingStatusInfo status = ThingStatusInfoBuilder.create(ThingStatus.OFFLINE).build(); + public Map stateMap = new HashMap<>(); + public Map triggerMap = new HashMap<>(); + + public void clear() { + stateMap.clear(); + } + + public @Nullable State getState(String channel) { + return stateMap.get(channel); + } + + @Override + public void stateUpdated(ChannelUID channelUID, State state) { + stateMap.put(channelUID.getAsString(), state); + } + + @Override + public void postCommand(ChannelUID channelUID, Command command) { + } + + @Override + public void sendTimeSeries(ChannelUID channelUID, TimeSeries timeSeries) { + } + + @Override + public void statusUpdated(Thing thing, ThingStatusInfo thingStatus) { + synchronized (this) { + status = thingStatus; + this.notifyAll(); + } + } + + public ThingStatusInfo getStatus() { + return status; + } + + public void waitForOnline() { + synchronized (this) { + Instant start = Instant.now(); + Instant check = Instant.now(); + while (!ThingStatus.ONLINE.equals(status.getStatus()) && Duration.between(start, check).getSeconds() < 10) { + try { + this.wait(1000); + } catch (InterruptedException e) { + fail("Interruppted waiting for ONLINE"); + } + check = Instant.now(); + } + } + // if method is exited without reaching ONLINE e.g. through timeout fail + if (!ThingStatus.ONLINE.equals(status.getStatus())) { + fail("waitForOnline just reached status " + status); + } + } + + @Override + public void thingUpdated(Thing thing) { + } + + @Override + public void validateConfigurationParameters(Thing thing, Map configurationParameters) { + } + + @Override + public void validateConfigurationParameters(Channel channel, Map configurationParameters) { + } + + @Override + public @Nullable ConfigDescription getConfigDescription(ChannelTypeUID channelTypeUID) { + return null; + } + + @Override + public @Nullable ConfigDescription getConfigDescription(ThingTypeUID thingTypeUID) { + return null; + } + + @Override + public void configurationUpdated(Thing thing) { + } + + @Override + public void migrateThingType(Thing thing, ThingTypeUID thingTypeUID, Configuration configuration) { + } + + @Override + public void channelTriggered(Thing thing, ChannelUID channelUID, String event) { + triggerMap.put(channelUID.getAsString(), event); + } + + @Override + public ChannelBuilder createChannelBuilder(ChannelUID channelUID, ChannelTypeUID channelTypeUID) { + return ChannelBuilder.create(new ChannelUID("handler:test")); + } + + @Override + public ChannelBuilder editChannel(Thing thing, ChannelUID channelUID) { + return ChannelBuilder.create(new ChannelUID("handler:test")); + } + + @Override + public List createChannelBuilders(ChannelGroupUID channelGroupUID, + ChannelGroupTypeUID channelGroupTypeUID) { + return List.of(); + } + + @Override + public boolean isChannelLinked(ChannelUID channelUID) { + return false; + } + + @Override + public @Nullable Bridge getBridge(ThingUID bridgeUID) { + return bridge; + } + + public void setBridge(Bridge bridge) { + this.bridge = bridge; + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/DicoveryServiceMock.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/DicoveryServiceMock.java new file mode 100644 index 00000000000..873b56d0c8b --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/DicoveryServiceMock.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.mock; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.dirigera.internal.discovery.DirigeraDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; + +/** + * The {@link DicoveryServiceMock} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class DicoveryServiceMock extends DirigeraDiscoveryService { + public Map discoveries = new HashMap<>(); + public Map deletes = new HashMap<>(); + + @Override + public void deviceDiscovered(final DiscoveryResult discoveryResult) { + synchronized (this) { + // logger.warn("Discovery thingDiscovered {}", discoveryResult); + String id = discoveryResult.getThingUID().getId(); + discoveries.put(id, discoveryResult); + this.notifyAll(); + } + } + + @Override + public void deviceRemoved(final DiscoveryResult discoveryResult) { + // logger.warn("Discovery thingRemoved {}", discoveryResult); + String id = discoveryResult.getThingUID().getId(); + DiscoveryResult remover = discoveries.remove(id); + // assertNotNull(remover); + if (remover != null) { + deletes.put(id, remover); + } + } + + public void waitForDetection() { + synchronized (this) { + if (discoveries.isEmpty()) { + try { + wait(5000); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/DirigeraAPISimu.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/DirigeraAPISimu.java new file mode 100644 index 00000000000..84124766033 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/DirigeraAPISimu.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.mock; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.openhab.binding.dirigera.internal.Constants.PROPERTY_DEVICE_ID; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.json.JSONArray; +import org.json.JSONObject; +import org.openhab.binding.dirigera.internal.FileReader; +import org.openhab.binding.dirigera.internal.interfaces.DirigeraAPI; +import org.openhab.binding.dirigera.internal.interfaces.Gateway; +import org.openhab.binding.dirigera.internal.interfaces.Model; +import org.openhab.core.library.types.RawType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link DirigeraAPISimu} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class DirigeraAPISimu implements DirigeraAPI { + private static JSONObject model = new JSONObject(); + + public static String fileName = "src/test/resources/home/home.json"; + public static Map patchMap = new HashMap<>(); + public static List scenesAdded = new ArrayList<>(); + public static List scenesDeleted = new ArrayList<>(); + + public DirigeraAPISimu(HttpClient client, Gateway gateway) { + } + + @Override + public JSONObject readHome() { + String modelString = FileReader.readFileInString(fileName); + model = new JSONObject(modelString); + return model; + } + + @Override + public JSONObject readDevice(String deviceId) { + JSONObject returnObject = new JSONObject(); + if (model.has("devices")) { + JSONArray devices = model.getJSONArray("devices"); + Iterator entries = devices.iterator(); + while (entries.hasNext()) { + JSONObject entry = (JSONObject) entries.next(); + if (deviceId.equals(entry.get(PROPERTY_DEVICE_ID))) { + return entry; + } + } + } + return returnObject; + } + + @Override + public void triggerScene(String sceneId, String trigger) { + } + + @Override + public int sendAttributes(String id, JSONObject attributes) { + JSONObject data = new JSONObject(); + data.put(Model.ATTRIBUTES, attributes); + return sendPatch(id, data); + } + + @Override + public int sendPatch(String id, JSONObject attributes) { + synchronized (patchMap) { + patchMap.put(id, attributes.toString()); + patchMap.notifyAll(); + } + return 200; + } + + @Override + public State getImage(String imageURL) { + Path path = Paths.get("src/test/resources/coverart/sonos-radio-cocktail-hour.avif"); + try { + byte[] imageData = Files.readAllBytes(path); + return new RawType(imageData, RawType.DEFAULT_MIME_TYPE); + } catch (IOException e) { + fail("getting image"); + } + return UnDefType.UNDEF; + } + + @Override + public JSONObject readScene(String sceneId) { + JSONObject returnObject = new JSONObject(); + if (model.has("devices")) { + JSONArray devices = model.getJSONArray("scenes"); + Iterator entries = devices.iterator(); + while (entries.hasNext()) { + JSONObject entry = (JSONObject) entries.next(); + if (sceneId.equals(entry.get(PROPERTY_DEVICE_ID))) { + return entry; + } + } + } + return returnObject; + } + + @Override + public String createScene(String uuid, String clickPattern, String controllerId) { + scenesAdded.add(uuid); + return uuid; + } + + @Override + public void deleteScene(String uuid) { + scenesDeleted.add(uuid); + } + + public static void waitForPatch() { + synchronized (patchMap) { + if (patchMap.isEmpty()) { + try { + patchMap.wait(5000); + } catch (InterruptedException e) { + fail(); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/DirigeraHandlerManipulator.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/DirigeraHandlerManipulator.java new file mode 100644 index 00000000000..de932221716 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/DirigeraHandlerManipulator.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.mock; + +import static org.mockito.Mockito.mock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.dirigera.internal.DirigeraCommandProvider; +import org.openhab.binding.dirigera.internal.discovery.DirigeraDiscoveryService; +import org.openhab.binding.dirigera.internal.handler.DirigeraHandler; +import org.openhab.core.i18n.LocationProvider; +import org.openhab.core.storage.Storage; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.osgi.framework.BundleContext; + +/** + * The {@link DirigeraHandlerManipulator} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class DirigeraHandlerManipulator extends DirigeraHandler { + + public DirigeraHandlerManipulator(Bridge bridge, HttpClient insecureClient, Storage bindingStorage, + DirigeraDiscoveryService discoveryService) { + super(bridge, insecureClient, bindingStorage, discoveryService, mock(LocationProvider.class), + mock(DirigeraCommandProvider.class), mock(BundleContext.class)); + // Changes the class of the provider. During initialize this class will be used for instantiation + super.apiProvider = DirigeraAPISimu.class; + } + + /** + * for unit testing + */ + @Override + public @Nullable ThingHandlerCallback getCallback() { + return super.getCallback(); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/HandlerFactoryMock.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/HandlerFactoryMock.java new file mode 100644 index 00000000000..a66a36df11d --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/HandlerFactoryMock.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.mock; + +import static org.mockito.Mockito.mock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dirigera.internal.DirigeraCommandProvider; +import org.openhab.binding.dirigera.internal.DirigeraHandlerFactory; +import org.openhab.binding.dirigera.internal.DirigeraStateDescriptionProvider; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.i18n.LocationProvider; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; + +/** + * The {@link HandlerFactoryMock} basic DeviceHandler for all devices + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class HandlerFactoryMock extends DirigeraHandlerFactory { + public HandlerFactoryMock(StorageService storageService) { + super(storageService, new DicoveryServiceMock(), mock(LocationProvider.class), + mock(DirigeraCommandProvider.class), new DirigeraStateDescriptionProvider(mock(EventPublisher.class), + mock(ItemChannelLinkRegistry.class), mock(ChannelTypeI18nLocalizationService.class))); + } + + @Override + public @Nullable ThingHandler createHandler(Thing thing) { + return super.createHandler(thing); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/TemperatureLightHandlerMock.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/TemperatureLightHandlerMock.java new file mode 100644 index 00000000000..6311debea57 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/mock/TemperatureLightHandlerMock.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.mock; + +import static org.mockito.Mockito.mock; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.dirigera.internal.DirigeraStateDescriptionProvider; +import org.openhab.binding.dirigera.internal.handler.light.TemperatureLightHandler; +import org.openhab.core.thing.Thing; + +/** + * {@link TemperatureLightHandlerMock} mock for accessing protected methods + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class TemperatureLightHandlerMock extends TemperatureLightHandler { + public TemperatureLightHandlerMock() { + super(mock(Thing.class), Map.of(), mock(DirigeraStateDescriptionProvider.class)); + } + + @Override + public long getKelvin(int percent) { + return super.getKelvin(percent); + } + + @Override + public int getPercent(long kelvin) { + return super.getPercent(kelvin); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/model/TestColorModel.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/model/TestColorModel.java new file mode 100644 index 00000000000..f8a69760198 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/model/TestColorModel.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.model; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.mock.TemperatureLightHandlerMock; +import org.openhab.core.library.types.HSBType; + +/** + * {@link TestColorModel} some basic tests + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestColorModel { + + @Test + void hsbCloseTo() { + // test with 2% means max distance between 2 values is 360 * 0.02 = 7.2 + HSBType first = new HSBType("50, 100, 100"); + HSBType second = new HSBType("57.1, 100, 100"); + assertTrue(ColorModel.closeTo(first, second, 0.02), "Hue is close"); + assertTrue(ColorModel.closeTo(second, first, 0.02), "Hue is close"); + second = new HSBType("57.3, 100, 100"); + assertFalse(ColorModel.closeTo(first, second, 0.02), "Hue bit over boundary"); + assertFalse(ColorModel.closeTo(second, first, 0.02), "Hue bit over boundary"); + + // test boundary at min and max hue values + first = new HSBType("359, 100, 100"); + second = new HSBType("6.1, 100, 100"); + assertTrue(ColorModel.closeTo(first, second, 0.02), "Hue is close"); + assertTrue(ColorModel.closeTo(second, first, 0.02), "Hue is close"); + second = new HSBType("6.3, 100, 100"); + assertFalse(ColorModel.closeTo(first, second, 0.02), "Hue bit over boundary"); + assertFalse(ColorModel.closeTo(second, first, 0.02), "Hue bit over boundary"); + + // test saturation + first = new HSBType("359, 50, 100"); + second = new HSBType("359, 51.9, 100"); + assertTrue(ColorModel.closeTo(first, second, 0.02), "Saturation is close"); + assertTrue(ColorModel.closeTo(second, first, 0.02), "Saturation is close"); + second = new HSBType("359, 52.1, 100"); + assertFalse(ColorModel.closeTo(first, second, 0.02), "Saturation bit over boundary"); + assertFalse(ColorModel.closeTo(second, first, 0.02), "Saturation bit over boundary"); + } + + @Test + void testKelvinToHSB() { + TemperatureLightHandlerMock handler = new TemperatureLightHandlerMock(); + HSBType hsb = ColorModel.kelvin2Hsb(6200); + long kelvinCalculated = ColorModel.hsb2Kelvin(hsb); + assertEquals(0, handler.getPercent(kelvinCalculated), "Below boundary"); + + hsb = ColorModel.kelvin2Hsb(1000); + kelvinCalculated = ColorModel.hsb2Kelvin(hsb); + assertEquals(100, handler.getPercent(kelvinCalculated), "Above boundary"); + + hsb = ColorModel.kelvin2Hsb(2200 + 900); + kelvinCalculated = ColorModel.hsb2Kelvin(hsb); + assertEquals(50, handler.getPercent(kelvinCalculated), 2, "Middle ~50% temperature"); + + for (int kelvinInput = 2000; kelvinInput < 6501; kelvinInput++) { + hsb = ColorModel.kelvin2Hsb(kelvinInput); + kelvinCalculated = ColorModel.hsb2Kelvin(hsb); + // assure all values has max difference of 50 + assertEquals(kelvinInput, kelvinCalculated, 50, "Diff " + (kelvinInput - kelvinCalculated)); + } + + // test if kelvin is matching with IKEA TRADFRI bulb values + hsb = ColorModel.kelvin2Hsb(2200); + assertEquals(29.7, hsb.getHue().doubleValue(), 0.1, "Hue for 2200 K"); + assertEquals(84.7, hsb.getSaturation().doubleValue(), 0.1, "Saturation for 2200 K"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/model/TestModel.java b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/model/TestModel.java new file mode 100644 index 00000000000..fc549c4918f --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/java/org/openhab/binding/dirigera/internal/model/TestModel.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2010-2025 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.dirigera.internal.model; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.dirigera.internal.Constants.*; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dirigera.internal.FileReader; +import org.openhab.binding.dirigera.internal.handler.DirigeraBridgeProvider; +import org.openhab.binding.dirigera.internal.handler.DirigeraHandler; +import org.openhab.binding.dirigera.internal.handler.sensor.WaterSensorHandler; +import org.openhab.binding.dirigera.internal.interfaces.Gateway; +import org.openhab.binding.dirigera.internal.mock.CallbackMock; +import org.openhab.binding.dirigera.internal.mock.DicoveryServiceMock; +import org.openhab.binding.dirigera.internal.mock.DirigeraAPISimu; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerCallback; + +/** + * {@link TestModel} some basic tests + * + * @author Bernd Weymann - Initial Contribution + */ +@NonNullByDefault +class TestModel { + + @Test + void testCustomName() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + + // test device with given custom name + assertEquals("Loft Floor Lamp", gateway.model().getCustonNameFor("891790db-8c17-483a-a1a6-c85bffd3a373_1"), + "Floor Lamp name"); + // test device without custom name - take model name + assertEquals("VALLHORN Wireless Motion Sensor", + gateway.model().getCustonNameFor("5ac5e131-44a4-4d75-be78-759a095d31fb_3"), "Motion Sensor name"); + // test device without custom name and no model name + assertEquals("light", gateway.model().getCustonNameFor("c27faa27-4c18-464f-81a0-a31ce57d83d5_1"), "Lamp"); + } + + @Test + void testMotionSensors() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + + // VALLHORN + String vallhornId = "5ac5e131-44a4-4d75-be78-759a095d31fb_1"; + ThingTypeUID motionLightUID = gateway.model().identifyDeviceFromModel(vallhornId); + assertEquals(THING_TYPE_MOTION_LIGHT_SENSOR, motionLightUID, "VALLHORN TTUID"); + Map relationsMap = gateway.model().getRelations("5ac5e131-44a4-4d75-be78-759a095d31fb"); + assertEquals(2, relationsMap.size(), "Relations"); + assertTrue(relationsMap.containsKey("5ac5e131-44a4-4d75-be78-759a095d31fb_1"), "Motion Sensor"); + assertEquals("motionSensor", relationsMap.get("5ac5e131-44a4-4d75-be78-759a095d31fb_1"), "Motion Sensor"); + assertTrue(relationsMap.containsKey("5ac5e131-44a4-4d75-be78-759a095d31fb_3"), "Light Sensor"); + assertEquals("lightSensor", relationsMap.get("5ac5e131-44a4-4d75-be78-759a095d31fb_3"), "Light Sensor"); + + // TRADFRI + String tradfriId = "ee61c57f-8efa-44f4-ba8a-d108ae054138_1"; + ThingTypeUID motionUID = gateway.model().identifyDeviceFromModel(tradfriId); + assertEquals(THING_TYPE_MOTION_SENSOR, motionUID, "TRADFRI TTUID"); + relationsMap = gateway.model().getRelations(tradfriId); + assertEquals(0, relationsMap.size(), "Twins"); + } + + @Test + void testPlugs() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + + // VALLHORN + String tretaktId = "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1"; + ThingTypeUID plugTTUID = gateway.model().identifyDeviceFromModel(tretaktId); + assertEquals(THING_TYPE_POWER_PLUG, plugTTUID, "TRETAKT TTUID"); + + // TRADFRI + String inspelningId = "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1"; + ThingTypeUID motionUID = gateway.model().identifyDeviceFromModel(inspelningId); + assertEquals(THING_TYPE_SMART_PLUG, motionUID, "INSPELNING TTUID"); + } + + @Test + void testSceneName() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge(); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + + String lightSceneId = "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb"; + ThingTypeUID sceneTTUID = gateway.model().identifyDeviceFromModel(lightSceneId); + assertEquals(THING_TYPE_SCENE, sceneTTUID, "Scene TTUID"); + } + + @Test + void testInitialDiscovery() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/home/home.json", true, + List.of()); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + + DicoveryServiceMock discovery = (DicoveryServiceMock) gateway.discovery(); + assertEquals(25, discovery.discoveries.size(), "Initial discoveries"); + } + + @Test + void testDiscoveryDisabled() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/home/home.json", false, + List.of()); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + + DicoveryServiceMock discovery = (DicoveryServiceMock) gateway.discovery(); + assertTrue(discovery.discoveries.isEmpty(), "Discovery disabled"); + } + + @Test + void testKnownDevices() { + String knownDevice = "9af826ad-a8ad-40bf-8aed-125300bccd20_1"; + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/home/home.json", true, + List.of(knownDevice)); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + + DicoveryServiceMock discovery = (DicoveryServiceMock) gateway.discovery(); + assertEquals(24, discovery.discoveries.size(), "Initial discoveries"); + assertFalse(discovery.discoveries.containsKey(knownDevice)); + } + + @Test + void testDiscoveryAfterHandlerRemoval() { + Bridge hubBridge = DirigeraBridgeProvider.prepareSimuBridge("src/test/resources/home/home-one-device.json", + true, List.of()); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + + DicoveryServiceMock discovery = (DicoveryServiceMock) gateway.discovery(); + assertEquals(1, discovery.discoveries.size(), "Initial discoveries"); + + ThingHandler factoryHandler = DirigeraBridgeProvider.createHandler(THING_TYPE_WATER_SENSOR, hubBridge, + "9af826ad-a8ad-40bf-8aed-125300bccd20_1"); + assertTrue(factoryHandler instanceof WaterSensorHandler); + WaterSensorHandler handler = (WaterSensorHandler) factoryHandler; + ThingHandlerCallback proxyCallback = handler.getCallback(); + assertNotNull(proxyCallback); + assertTrue(proxyCallback instanceof CallbackMock); + CallbackMock callback = (CallbackMock) proxyCallback; + callback.waitForOnline(); + + DirigeraHandler.detectionTimeSeonds = 0; + discovery.discoveries.clear(); + assertEquals(0, discovery.discoveries.size(), "Cleanup after handler creation"); + handler.dispose(); + handler.handleRemoval(); + discovery.waitForDetection(); + assertEquals(1, discovery.discoveries.size(), "After removal new discovery result shall be present "); + } + + @Test + void testResolvedRelations() { + Bridge hubBridge = DirigeraBridgeProvider + .prepareSimuBridge("src/test/resources/websocket/device-added/home-before.json", true, List.of()); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + List all = gateway.model().getAllDeviceIds(); + List resolved = gateway.model().getResolvedDeviceList(); + + all.removeAll(resolved); + // 2 resolved devices + assertEquals(2, all.size(), "2 devices resolved"); + } + + @Test + void testDeviceAdded() { + Bridge hubBridge = DirigeraBridgeProvider + .prepareSimuBridge("src/test/resources/websocket/device-added/home-before.json", true, List.of()); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + + DicoveryServiceMock discovery = (DicoveryServiceMock) gateway.discovery(); + assertEquals(25, discovery.discoveries.size(), "Initial discoveries"); + + // Prepare update message + String update = FileReader.readFileInString("src/test/resources/websocket/device-added/device-added.json"); + // prepare mock + DirigeraAPISimu.fileName = "src/test/resources/websocket/device-added/home-after.json"; + try { + gateway.websocketUpdate(update); + // give the gateway some time to handle the message + Thread.sleep(500); + } catch (InterruptedException e) { + fail(); + } + assertEquals(25 + 1, discovery.discoveries.size(), "One more discovery"); + } + + @Test + void testDeviceRemoved() { + Bridge hubBridge = DirigeraBridgeProvider + .prepareSimuBridge("src/test/resources/websocket/device-removed/home-before.json", true, List.of()); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + + DicoveryServiceMock discovery = (DicoveryServiceMock) gateway.discovery(); + assertEquals(26, discovery.discoveries.size(), "Initial discoveries"); + + // Prepare update message + String update = FileReader.readFileInString("src/test/resources/websocket/device-removed/device-removed.json"); + // prepare mock + DirigeraAPISimu.fileName = "src/test/resources/websocket/device-removed/home-after.json"; + try { + gateway.websocketUpdate(update); + // give the gateway some time to handle the message + Thread.sleep(500); + } catch (InterruptedException e) { + fail(); + } + assertEquals(26 - 1, discovery.discoveries.size(), "One less discovery"); + assertEquals(1, discovery.deletes.size(), "One deletion"); + } + + @Test + void testSceneCreated() { + Bridge hubBridge = DirigeraBridgeProvider + .prepareSimuBridge("src/test/resources/websocket/scene-created/home-before.json", true, List.of()); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + + DicoveryServiceMock discovery = (DicoveryServiceMock) gateway.discovery(); + assertEquals(24, discovery.discoveries.size(), "Initial discoveries"); + + // Prepare update message + String update = FileReader.readFileInString("src/test/resources/websocket/scene-created/scene-created.json"); + // prepare mock + DirigeraAPISimu.fileName = "src/test/resources/websocket/scene-created/home-after.json"; + try { + gateway.websocketUpdate(update); + // give the gateway some time to handle the message + Thread.sleep(500); + } catch (InterruptedException e) { + fail(); + } + assertEquals(24 + 1, discovery.discoveries.size(), "One more discovery"); + } + + @Test + void testSceneDeleted() { + Bridge hubBridge = DirigeraBridgeProvider + .prepareSimuBridge("src/test/resources/websocket/scene-deleted/home-before.json", true, List.of()); + Gateway gateway = (Gateway) hubBridge.getHandler(); + assertNotNull(gateway); + + DicoveryServiceMock discovery = (DicoveryServiceMock) gateway.discovery(); + assertEquals(25, discovery.discoveries.size(), "Initial discoveries"); + + // Prepare update message + String update = FileReader.readFileInString("src/test/resources/websocket/scene-deleted/scene-deleted.json"); + // prepare mock + DirigeraAPISimu.fileName = "src/test/resources/websocket/scene-deleted/home-after.json"; + try { + gateway.websocketUpdate(update); + // give the gateway some time to handle the message + Thread.sleep(500); + } catch (InterruptedException e) { + fail(); + } + assertEquals(25 - 1, discovery.discoveries.size(), "One more discovery"); + assertEquals(1, discovery.deletes.size(), "One deletion"); + } +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/coverart/sonos-radio-cocktail-hour.avif b/bundles/org.openhab.binding.dirigera/src/test/resources/coverart/sonos-radio-cocktail-hour.avif new file mode 100644 index 0000000000000000000000000000000000000000..10d2afd2f8b3fcd98164f823ed7997ce17d4dd1e GIT binary patch literal 3157 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/devices/home-all-devices.json b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/home-all-devices.json new file mode 100644 index 00000000000..894547ea910 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/home-all-devices.json @@ -0,0 +1,2090 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "id": "854bdf30-86b8-48f5-b070-16ff5ab12be4_1", + "relationId": "854bdf30-86b8-48f5-b070-16ff5ab12be4", + "type": "controller", + "deviceType": "shortcutController", + "createdAt": "2024-10-17T14:23:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-27T18:11:55.000Z", + "attributes": { + "customName": "Clicky", + "firmwareVersion": "1.0.21", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "SOMRIG shortcut button", + "productCode": "E2213", + "serialNumber": "ECF64CFFFE1EE52F", + "batteryPercentage": 89, + "switchLabel": "Shortcut 1", + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [ + "singlePress", + "longPress", + "doublePress" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "854bdf30-86b8-48f5-b070-16ff5ab12be4_2", + "relationId": "854bdf30-86b8-48f5-b070-16ff5ab12be4", + "type": "controller", + "deviceType": "shortcutController", + "createdAt": "2024-10-17T14:23:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-27T18:11:55.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.21", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "SOMRIG shortcut button", + "productCode": "E2213", + "serialNumber": "ECF64CFFFE1EE52F", + "switchLabel": "Shortcut 2", + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [ + "singlePress", + "longPress", + "doublePress" + ], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "eb9a4367-9e23-4d37-9566-401a7ae7caf0_1", + "type": "light", + "deviceType": "light", + "createdAt": "2022-11-01T08:33:27.000Z", + "isReachable": true, + "lastSeen": "2022-11-12T10:31:23.000Z", + "customIcon": "lighting_spot_chandelier", + "attributes": { + "customName": "Vasken", + "model": "TRADFRI Driver 30W", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "B4E3F9FFFE128FF2", + "productCode": "ICPSHC2430EUIL1", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 100, + "identifyStarted": "2022-11-01T08:34:45.000Z", + "identifyPeriod": 15, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel" + ] + }, + "room": { + "id": "45e3cc5e-8c64-478b-ac32-25762b431db4", + "name": "Kjøkken", + "color": "ikea_green_no_65", + "icon": "rooms_arm_chair" + }, + "deviceSet": [], + "remoteLinks": [ + "d0aa3515-cef7-465f-8bcf-19caf59f1a98_1" + ], + "isHidden": false + }, + { + "id": "eb9a4367-9e23-4d37-9566-401a7ae7caf0_2", + "type": "light", + "deviceType": "light", + "createdAt": "2022-11-01T08:33:27.000Z", + "isReachable": true, + "lastSeen": "2022-11-12T10:31:23.000Z", + "customIcon": "lighting_spot_chandelier", + "attributes": { + "customName": "Vasken", + "model": "TRADFRI Driver 30W", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "B4E3F9FFFE128FF2", + "productCode": "ICPSHC2430EUIL1", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 100, + "identifyStarted": "2022-11-01T08:34:45.000Z", + "identifyPeriod": 15, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn" + ] + }, + "room": { + "id": "45e3cc5e-8c64-478b-ac32-25762b431db4", + "name": "Kjøkken", + "color": "ikea_green_no_65", + "icon": "rooms_arm_chair" + }, + "deviceSet": [], + "remoteLinks": [ + "d0aa3515-cef7-465f-8bcf-19caf59f1a98_1" + ], + "isHidden": false + }, + { + "id": "92dbcea1-3d7e-4d6a-a009-bdf3a1ae6691_1", + "type": "controller", + "deviceType": "shortcutController", + "createdAt": "2022-11-11T16:46:15.000Z", + "isReachable": true, + "lastSeen": "2022-11-11T16:47:24.000Z", + "attributes": { + "customName": "Fjärrkontroll 1", + "firmwareVersion": "2.3.015", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI SHORTCUT Button", + "productCode": "E1812", + "serialNumber": "84BA20FFFEAD3E56", + "batteryPercentage": 100, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel", + "blindsState" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "decd27b3-f54a-4211-9a8f-e7bf70f832eb", + "name": "A", + "color": "ikea_green_no_65", + "icon": "rooms_arm_chair" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "cec4c170-7846-4e22-b681-d8a912181cca_1", + "type": "controller", + "deviceType": "soundController", + "createdAt": "2022-11-08T02:11:49.000Z", + "isReachable": true, + "lastSeen": "2022-11-09T17:53:12.000Z", + "attributes": { + "customName": "Remote 2", + "model": "SYMFONISK Sound Controller", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "serialNumber": "943469FFFE636247", + "productCode": "E1744", + "batteryPercentage": 90, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "297071f0-e98a-4ba0-8c05-9b88a1dbc6c4", + "name": "Living Room ", + "color": "ikea_green_no_65", + "icon": "rooms_arm_chair" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9f96eced-7674-4b9f-bbf9-b9575d888638_1", + "type": "controller", + "deviceType": "blindsController", + "createdAt": "2022-11-01T22:43:56.000Z", + "isReachable": true, + "lastSeen": "2022-11-17T21:48:45.000Z", + "attributes": { + "customName": "Office Blinder Remote ", + "model": "TRADFRI open/close remote", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "serialNumber": "5C0272FFFE6C613C", + "productCode": "E1766", + "batteryPercentage": 65, + "isOn": false, + "lightLevel": 1, + "blindsCurrentLevel": 0, + "blindsState": "", + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel", + "blindsState" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "36e2e693-a042-4119-84ec-42d45a347534", + "name": "Office", + "color": "ikea_blue_no_63", + "icon": "rooms_display" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "eadfad54-9d23-4475-92b6-0ee3d6f8b481_1", + "type": "blinds", + "deviceType": "blinds", + "createdAt": "2022-11-01T22:32:41.000Z", + "isReachable": true, + "lastSeen": "2022-11-18T22:11:51.000Z", + "attributes": { + "customName": "Office Blinder", + "model": "PRAKTLYSING cellular blind", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.088", + "hardwareVersion": "1", + "serialNumber": "0C4314FFFE293771", + "productCode": "E2021", + "batteryPercentage": 92, + "blindsTargetLevel": 60, + "blindsCurrentLevel": 60, + "blindsState": "stopped", + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "blindsCurrentLevel", + "blindsTargetLevel", + "blindsState" + ] + }, + "room": { + "id": "36e2e693-a042-4119-84ec-42d45a347534", + "name": "Office", + "color": "ikea_blue_no_63", + "icon": "rooms_display" + }, + "deviceSet": [], + "remoteLinks": [ + "9f96eced-7674-4b9f-bbf9-b9575d888638_1" + ], + "isHidden": false + }, + { + "id": "a8319695-0729-428c-9465-aadc0b738995", + "type": "airPurifier", + "deviceType": "airPurifier", + "createdAt": "2022-11-05T22:40:42.000Z", + "isReachable": true, + "lastSeen": "2022-11-16T17:46:45.000Z", + "attributes": { + "customName": "Luftrenare hall", + "model": "STARKVIND Air purifier", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.001", + "hardwareVersion": "1", + "serialNumber": "XXXXXXXXXXXX", + "productCode": "E2007", + "fanMode": "auto", + "fanModeSequence": "lowMediumHighAuto", + "motorState": 10, + "motorRuntime": 472283, + "filterElapsedTime": 193540, + "filterAlarmStatus": false, + "filterLifetime": 259200, + "childLock": false, + "statusLight": true, + "currentPM25": 8, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "fanMode", + "fanModeSequence", + "motorState", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "a7301b11-95f6-4ee3-a764-22d4ea52d161", + "name": "Hall", + "color": "ikea_orange_no_11", + "icon": "rooms_coat_hanger" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "6379a590-dc0a-47b5-b35b-7b46dfefd282_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2022-11-11T17:05:54.000Z", + "isReachable": true, + "lastSeen": "2022-11-11T17:06:02.000Z", + "attributes": { + "customName": "Uttag 1", + "firmwareVersion": "2.3.089", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI control outlet", + "productCode": "E1603", + "serialNumber": "D0CF5EFFFEEF2B3F", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel" + ] + }, + "room": { + "id": "decd27b3-f54a-4211-9a8f-e7bf70f832eb", + "name": "A", + "color": "ikea_green_no_65", + "icon": "rooms_arm_chair" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "deviceType": "environmentSensor", + "remoteLinks": [], + "createdAt": "2024-10-17T08:00:25.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T08:07:42.000Z", + "capabilities": { + "canReceive": [ + "customName" + ], + "canSend": [] + }, + "attributes": { + "maxMeasuredPM25": 999, + "identifyPeriod": 0, + "currentTemperature": 20, + "permittingJoin": false, + "serialNumber": "28DBA7FFFEDFFBF6", + "minMeasuredPM25": 0, + "otaScheduleStart": "00:00", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "customName": "Cleany", + "currentPM25": 11, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "vocIndex": 100, + "manufacturer": "IKEA of Sweden", + "productCode": "E2112", + "otaState": "readyToCheck", + "hardwareVersion": "1", + "model": "VINDSTYRKA", + "firmwareVersion": "1.0.11", + "currentRH": 76 + }, + "id": "f80cac12-65a4-47b4-9f68-a0456a349a43_1", + "type": "sensor", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false + }, + { + "id": "5ffa4a1e-6fe2-4a24-b0af-b216937c4e8c_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:04:54.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:08:02.000Z", + "attributes": { + "customName": "Lampe Rechts", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "30FB10FFFEE44843", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 54, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-11T11:33:27.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T11:22:41.000Z", + "attributes": { + "customName": "Remote Control Loft", + "firmwareVersion": "2.4.16", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "Remote Control N2", + "productCode": "E2001", + "serialNumber": "94B216FFFE6F9BA6", + "batteryPercentage": 85, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "044b63e7-999d-4caa-8a76-fb8cfd32b381_1", + "type": "repeater", + "deviceType": "repeater", + "createdAt": "2024-10-10T10:10:37.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T10:12:28.000Z", + "attributes": { + "customName": "Reaper", + "model": "TRADFRI Signal Repeater", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "14B457FFFEF8DC61", + "productCode": "E1746", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "45165c60-0c1a-4b9f-ae09-c4a6220d1420_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:09:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:02:56.000Z", + "attributes": { + "customName": "Lampe Links", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "881A14FFFE479E82", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 51, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Esszimmer Hinten", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:20:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "attributes": { + "customName": "Loft Floor Lamp", + "model": "TRADFRI bulb E27 CWS opal 600lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "EC1BBDFFFEC17EA1", + "productCode": "LED1624G9E27EU", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "338bb721-35bb-4775-8cd0-ba70fc37ab10_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-04T17:21:49.446Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:09:15.285Z", + "attributes": { + "customName": "Loft", + "model": "SYMFONISK Bookshelf S21", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.3.3-2.0", + "serialNumber": "34-7E-5C-F5-45-88:F", + "productCode": "S21", + "identifyStarted": "2024-10-04T17:21:49.447Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-13T21:09:15.284Z", + "playbackAudio": { + "serviceType": "sonos", + "providerType": "Mixcloud", + "playItem": { + "id": "eyJhY2NvdW50VHlwZSI6InNvbm9zIiwiY29udGVudFR5cGUiOiJjb250YWluZXIiLCJjb250ZW50Ijp7ImNvbnRhaW5lciI6eyJhY2NvdW50SWQiOiJzbl8zIiwib2JqZWN0SWQiOiJjbG91ZGNhc3Q6MjE3OTQzNDcxNCIsInNlcnZpY2VJZCI6IjE4MSIsInR5cGUiOiJ0cmFjayJ9fX0=", + "title": "The Anjunadeep Edition 521 with Leaving Laurel & yehno", + "artist": "Anjunadeep", + "imageURL": "http://192.168.1.95:1400/getaa?s=1&u=x-sonos-http%3acloudcast%253a2179434714.unknown%3fsid%3d181%26flags%3d0%26sn%3d3", + "duration": 3617000 + } + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-13T21:09:15.284Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": true, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": true, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 16, + "isMuted": false, + "audioGroup": "1f386990-bb4f-4a00-befd-646b5ef52c47" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "03c716a9-900e-4bb0-8694-eaf837bfa3fb_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-06T09:06:12.272Z", + "isReachable": false, + "lastSeen": "2024-10-08T16:30:36.504Z", + "attributes": { + "customName": "Moritz", + "model": "SYMFONISK Table lamp S20", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.2.3-2.0", + "serialNumber": "78-28-CA-81-8E-F2:1", + "productCode": "S20", + "identifyStarted": "2024-10-06T09:06:12.272Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-06T09:06:12.272Z", + "playbackAudio": { + + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-06T09:06:12.032Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": false, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": false, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 15, + "isMuted": false, + "audioGroup": "71aa115c-fadd-4c43-aaf7-d537fa939638" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": true + }, + { + "id": "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-09T21:23:19.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:22:08.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Sensor Kitchen", + "model": "PARASOLL Door/Window Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "serialNumber": "D44867FFFEC2D1F0", + "productCode": "E2013", + "batteryPercentage": 84, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:00:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:02.000Z", + "customIcon": "lighting_cone_pendant", + "attributes": { + "customName": "Esszimmer Vorne", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFEC9113D", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 0, + "colorSaturation": 1, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-10T14:40:38.000Z", + "attributes": { + "customName": "Smart-Home-Steckdose 1", + "model": "INSPELNING Smart plug", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.4.45", + "hardwareVersion": "1", + "serialNumber": "ECF64CFFFEF28EC3", + "productCode": "E2224", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 1, + "startUpCurrentLevel": -1, + "currentVoltage": 236.8000030517578, + "currentAmps": 0.003000000026077032, + "currentActivePower": 0, + "totalEnergyConsumed": 0.01899999938905239, + "totalEnergyConsumedLastUpdated": "2024-10-10T14:40:05.000Z", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "childLock": false, + "statusLight": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T17:38:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T17:18:25.000Z", + "attributes": { + "customName": "", + "model": "", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.10", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE66D457", + "productCode": "L2112", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 95, + "startUpCurrentLevel": -1, + "colorHue": 29.9981689453125, + "colorSaturation": 0.6496062992125984, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_1", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "Motion Sensor Gäste WC", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "batteryPercentage": 85, + "isOn": false, + "motionDetectedDelay": 20, + "isDetected": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": true, + "onDuration": 300, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_3", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "illuminance": 1, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [] + }, + { + "id": "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:46:04.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:06:16.000Z", + "attributes": { + "customName": "Bathroom Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x90", + "productCode": "", + "serialNumber": "804B50FFFEF79502", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 49, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "073b40df-e6a0-486d-99a3-6134effe4c59_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:48:44.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:21:17.000Z", + "customIcon": "lighting_fan", + "attributes": { + "customName": "HWR Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x30", + "productCode": "", + "serialNumber": "BC33ACFFFE9761AA", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 73, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [ + "ee61c57f-8efa-44f4-ba8a-d108ae054138_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "ee61c57f-8efa-44f4-ba8a-d108ae054138_1", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:56:23.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:18:17.000Z", + "attributes": { + "customName": "HWR Motion", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + "productCode": "E1745", + "serialNumber": "BC33ACFFFE8739CC", + "batteryPercentage": 20, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "isDetected": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "sensorConfig": { + "scheduleOn": true, + "onDuration": 180, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "3267c0f3-069a-4894-b5d0-a30d26386af8_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:00:00.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T13:31:10.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Dining Room", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "D44867FFFEB36824", + "batteryPercentage": 82, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "bab217dc-5d46-42af-a3a4-6a53b9a953da_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:05:20.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:36:48.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Entry Door", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "F84477FFFEDC7871", + "batteryPercentage": 81, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_1", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "Bedroom Motion", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "batteryPercentage": 84, + "isOn": false, + "isDetected": false, + "motionDetectedDelay": 20, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": false, + "onDuration": 180 + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "illuminance": 1, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9e9cb6b1-7981-443a-bd1e-5301815a25bd_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-13T20:09:35.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T00:32:59.000Z", + "attributes": { + "customName": "HWR Remote", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + "productCode": "E1810", + "serialNumber": "BC33ACFFFE9B308C", + "batteryPercentage": 30, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + { + "id": "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58", + "info": { + "name": "Lightmode Christmas", + "icon": "scenes_tree" + }, + "type": "userScene", + "triggers": [ + { + "id": "987ec615-24d8-411c-9aa9-d03c447b246b", + "type": "app", + "triggeredAt": "2024-10-16T00:21:15.942Z", + "disabled": false + } + ], + "actions": [ + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 0, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T11:13:42.273Z", + "lastCompleted": "2024-10-16T00:21:15.977Z", + "lastTriggered": "2024-10-16T00:21:15.977Z", + "undoAllowedDuration": 30, + "lastUndo": "2024-10-15T19:36:59.273Z" + }, + { + "id": "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb", + "info": { + "name": "Lightmode off", + "icon": "scenes_disco_ball" + }, + "type": "userScene", + "triggers": [ + { + "id": "6fff3797-c798-4d0c-abe0-ce46cc389104", + "type": "app", + "triggeredAt": "2024-10-16T00:38:16.257Z", + "disabled": false + } + ], + "actions": [ + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 359.83392, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T23:14:12.399Z", + "lastCompleted": "2024-10-16T00:38:16.267Z", + "lastTriggered": "2024-10-16T00:38:16.267Z", + "undoAllowedDuration": 30 + } + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/devices/inspelning.json b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/inspelning.json new file mode 100644 index 00000000000..a95f036f1ba --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/inspelning.json @@ -0,0 +1,59 @@ +{ + "deviceType": "outlet", + "remoteLinks": [], + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": false, + "lastSeen": "2024-10-10T10:47:13.000Z", + "capabilities": { + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ], + "canSend": [] + }, + "attributes": { + "identifyPeriod": 0, + "permittingJoin": false, + "currentAmps": 0.003000000026077032, + "currentVoltage": 75.5999984741211, + "otaScheduleEnd": "00:00", + "currentActivePower": 0, + "otaStatus": "upToDate", + "totalEnergyConsumedLastUpdated": "2024-10-08T20:46:36.000Z", + "manufacturer": "IKEA of Sweden", + "statusLight": false, + "otaState": "readyToCheck", + "isOn": false, + "totalEnergyConsumed": 0, + "model": "INSPELNING Smart plug", + "hardwareVersion": "1", + "firmwareVersion": "2.4.45", + "serialNumber": "ECF64CFFFEF28EC3", + "otaScheduleStart": "00:00", + "otaProgress": 0, + "startUpCurrentLevel": -1, + "customName": "Smart-Home-Steckdose 1", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "lightLevel": 100, + "productCode": "E2206", + "childLock": false, + "startupOnOff": "startPrevious" + }, + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/devices/praktlysing.json b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/praktlysing.json new file mode 100644 index 00000000000..3decd88ace6 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/praktlysing.json @@ -0,0 +1,48 @@ +{ + "id": "eadfad54-9d23-4475-92b6-0ee3d6f8b481_1", + "type": "blinds", + "deviceType": "blinds", + "createdAt": "2022-11-01T22:32:41.000Z", + "isReachable": true, + "lastSeen": "2022-11-18T22:11:51.000Z", + "attributes": { + "customName": "Office Blinder", + "model": "PRAKTLYSING cellular blind", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.088", + "hardwareVersion": "1", + "serialNumber": "0C4314FFFE293771", + "productCode": "E2021", + "batteryPercentage": 92, + "blindsTargetLevel": 60, + "blindsCurrentLevel": 60, + "blindsState": "stopped", + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "blindsCurrentLevel", + "blindsTargetLevel", + "blindsState" + ] + }, + "room": { + "id": "36e2e693-a042-4119-84ec-42d45a347534", + "name": "Office", + "color": "ikea_blue_no_63", + "icon": "rooms_display" + }, + "deviceSet": [], + "remoteLinks": [ + "9f96eced-7674-4b9f-bbf9-b9575d888638_1" + ], + "isHidden": false +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/devices/somrig-shortcut-cotroller.json b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/somrig-shortcut-cotroller.json new file mode 100644 index 00000000000..6c32290c434 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/somrig-shortcut-cotroller.json @@ -0,0 +1,91 @@ +[ + { + "id": "854bdf30-86b8-48f5-b070-16ff5ab12be4_1", + "relationId": "854bdf30-86b8-48f5-b070-16ff5ab12be4", + "type": "controller", + "deviceType": "shortcutController", + "createdAt": "2024-10-17T14:23:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-27T18:11:55.000Z", + "attributes": { + "customName": "Clicky", + "firmwareVersion": "1.0.21", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "SOMRIG shortcut button", + "productCode": "E2213", + "serialNumber": "ECF64CFFFE1EE52F", + "batteryPercentage": 89, + "switchLabel": "Shortcut 1", + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [ + "singlePress", + "longPress", + "doublePress" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "854bdf30-86b8-48f5-b070-16ff5ab12be4_2", + "relationId": "854bdf30-86b8-48f5-b070-16ff5ab12be4", + "type": "controller", + "deviceType": "shortcutController", + "createdAt": "2024-10-17T14:23:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-27T18:11:55.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.21", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "SOMRIG shortcut button", + "productCode": "E2213", + "serialNumber": "ECF64CFFFE1EE52F", + "switchLabel": "Shortcut 2", + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [ + "singlePress", + "longPress", + "doublePress" + ], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } +] \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/devices/starkvind.json b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/starkvind.json new file mode 100644 index 00000000000..95bcb3fd612 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/starkvind.json @@ -0,0 +1,56 @@ +{ + "id": "a8319695-0729-428c-9465-aadc0b738995", + "type": "airPurifier", + "deviceType": "airPurifier", + "createdAt": "2022-11-05T22:40:42.000Z", + "isReachable": true, + "lastSeen": "2022-11-16T17:46:45.000Z", + "attributes": { + "customName": "Luftrenare hall", + "model": "STARKVIND Air purifier", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.001", + "hardwareVersion": "1", + "serialNumber": "XXXXXXXXXXXX", + "productCode": "E2007", + "fanMode": "auto", + "fanModeSequence": "lowMediumHighAuto", + "motorState": 10, + "motorRuntime": 472283, + "filterElapsedTime": 193540, + "filterAlarmStatus": false, + "filterLifetime": 259200, + "childLock": false, + "statusLight": true, + "currentPM25": 8, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "fanMode", + "fanModeSequence", + "motorState", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "a7301b11-95f6-4ee3-a764-22d4ea52d161", + "name": "Hall", + "color": "ikea_orange_no_11", + "icon": "rooms_coat_hanger" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/devices/symfonsik-sound-controller.json b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/symfonsik-sound-controller.json new file mode 100644 index 00000000000..ab2cb5f0752 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/symfonsik-sound-controller.json @@ -0,0 +1,45 @@ +{ + "id": "cec4c170-7846-4e22-b681-d8a912181cca_1", + "type": "controller", + "deviceType": "soundController", + "createdAt": "2022-11-08T02:11:49.000Z", + "isReachable": true, + "lastSeen": "2022-11-09T17:53:12.000Z", + "attributes": { + "customName": "Remote 2", + "model": "SYMFONISK Sound Controller", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "serialNumber": "943469FFFE636247", + "productCode": "E1744", + "batteryPercentage": 90, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "297071f0-e98a-4ba0-8c05-9b88a1dbc6c4", + "name": "Living Room ", + "color": "ikea_green_no_65", + "icon": "rooms_arm_chair" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfir-shortcut-controller.json b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfir-shortcut-controller.json new file mode 100644 index 00000000000..379833b345d --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfir-shortcut-controller.json @@ -0,0 +1,46 @@ +{ + "id": "92dbcea1-3d7e-4d6a-a009-bdf3a1ae6691_1", + "type": "controller", + "deviceType": "shortcutController", + "createdAt": "2022-11-11T16:46:15.000Z", + "isReachable": true, + "lastSeen": "2022-11-11T16:47:24.000Z", + "attributes": { + "customName": "Fjärrkontroll 1", + "firmwareVersion": "2.3.015", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI SHORTCUT Button", + "productCode": "E1812", + "serialNumber": "84BA20FFFEAD3E56", + "batteryPercentage": 100, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel", + "blindsState" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "decd27b3-f54a-4211-9a8f-e7bf70f832eb", + "name": "A", + "color": "ikea_green_no_65", + "icon": "rooms_arm_chair" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-blinds-controller.json b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-blinds-controller.json new file mode 100644 index 00000000000..a6c0b834221 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-blinds-controller.json @@ -0,0 +1,48 @@ + { + "id": "9f96eced-7674-4b9f-bbf9-b9575d888638_1", + "type": "controller", + "deviceType": "blindsController", + "createdAt": "2022-11-01T22:43:56.000Z", + "isReachable": true, + "lastSeen": "2022-11-17T21:48:45.000Z", + "attributes": { + "customName": "Office Blinder Remote ", + "model": "TRADFRI open/close remote", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "serialNumber": "5C0272FFFE6C613C", + "productCode": "E1766", + "batteryPercentage": 65, + "isOn": false, + "lightLevel": 1, + "blindsCurrentLevel": 0, + "blindsState": "", + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel", + "blindsState" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "36e2e693-a042-4119-84ec-42d45a347534", + "name": "Office", + "color": "ikea_blue_no_63", + "icon": "rooms_display" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-color-lightbulb.json b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-color-lightbulb.json new file mode 100644 index 00000000000..e754a14a393 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-color-lightbulb.json @@ -0,0 +1,61 @@ + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Dining Room", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": {} +} diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-light-driver.json b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-light-driver.json new file mode 100644 index 00000000000..0a2218dccf6 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-light-driver.json @@ -0,0 +1,49 @@ +{ + "id": "eb9a4367-9e23-4d37-9566-401a7ae7caf0_1", + "type": "light", + "deviceType": "light", + "createdAt": "2022-11-01T08:33:27.000Z", + "isReachable": true, + "lastSeen": "2022-11-12T10:31:23.000Z", + "customIcon": "lighting_spot_chandelier", + "attributes": { + "customName": "Vasken", + "model": "TRADFRI Driver 30W", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "B4E3F9FFFE128FF2", + "productCode": "ICPSHC2430EUIL1", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 100, + "identifyStarted": "2022-11-01T08:34:45.000Z", + "identifyPeriod": 15, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel" + ] + }, + "room": { + "id": "45e3cc5e-8c64-478b-ac32-25762b431db4", + "name": "Kjøkken", + "color": "ikea_green_no_65", + "icon": "rooms_arm_chair" + }, + "deviceSet": [], + "remoteLinks": [ + "d0aa3515-cef7-465f-8bcf-19caf59f1a98_1" + ], + "isHidden": false +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-outlet.json b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-outlet.json new file mode 100644 index 00000000000..9e9774042f7 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tradfri-outlet.json @@ -0,0 +1,42 @@ +{ + "id" : "6379a590-dc0a-47b5-b35b-7b46dfefd282_1", + "type" : "outlet", + "deviceType" : "outlet", + "createdAt" : "2022-11-11T17:05:54.000Z", + "isReachable" : true, + "lastSeen" : "2022-11-11T17:06:02.000Z", + "attributes" : { + "customName" : "Uttag 1", + "firmwareVersion" : "2.3.089", + "hardwareVersion" : "1", + "manufacturer" : "IKEA of Sweden", + "model" : "TRADFRI control outlet", + "productCode" : "E1603", + "serialNumber" : "D0CF5EFFFEEF2B3F", + "isOn" : true, + "startupOnOff" : "startPrevious", + "lightLevel" : 100, + "identifyPeriod" : 0, + "identifyStarted" : "2000-01-01T00:00:00.000Z", + "permittingJoin" : false, + "otaPolicy" : "autoUpdate", + "otaProgress" : 0, + "otaScheduleEnd" : "00:00", + "otaScheduleStart" : "00:00", + "otaState" : "readyToCheck", + "otaStatus" : "upToDate" + }, + "capabilities" : { + "canSend" : [ ], + "canReceive" : [ "customName", "isOn", "lightLevel" ] + }, + "room" : { + "id" : "decd27b3-f54a-4211-9a8f-e7bf70f832eb", + "name" : "A", + "color" : "ikea_green_no_65", + "icon" : "rooms_arm_chair" + }, + "deviceSet" : [ ], + "remoteLinks" : [ ], + "isHidden" : false + } \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tretakt-plug.json b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tretakt-plug.json new file mode 100644 index 00000000000..3bb904070dc --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/devices/tretakt-plug.json @@ -0,0 +1,45 @@ + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/gateway/home-with-coordinates.json b/bundles/org.openhab.binding.dirigera/src/test/resources/gateway/home-with-coordinates.json new file mode 100644 index 00000000000..038ccd33080 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/gateway/home-with-coordinates.json @@ -0,0 +1,1615 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "deviceType": "environmentSensor", + "remoteLinks": [], + "createdAt": "2024-10-17T08:00:25.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T08:07:42.000Z", + "capabilities": { + "canReceive": [ + "customName" + ], + "canSend": [] + }, + "attributes": { + "maxMeasuredPM25": 999, + "identifyPeriod": 0, + "currentTemperature": 20, + "permittingJoin": false, + "serialNumber": "28DBA7FFFEDFFBF6", + "minMeasuredPM25": 0, + "otaScheduleStart": "00:00", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "customName": "Cleany", + "currentPM25": 11, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "vocIndex": 100, + "manufacturer": "IKEA of Sweden", + "productCode": "E2112", + "otaState": "readyToCheck", + "hardwareVersion": "1", + "model": "VINDSTYRKA", + "firmwareVersion": "1.0.11", + "currentRH": 76 + }, + "id": "f80cac12-65a4-47b4-9f68-a0456a349a43_1", + "type": "sensor", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false + }, + { + "id": "5ffa4a1e-6fe2-4a24-b0af-b216937c4e8c_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:04:54.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:08:02.000Z", + "attributes": { + "customName": "Lampe Rechts", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "30FB10FFFEE44843", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 54, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-11T11:33:27.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T11:22:41.000Z", + "attributes": { + "customName": "Remote Control Loft", + "firmwareVersion": "2.4.16", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "Remote Control N2", + "productCode": "E2001", + "serialNumber": "94B216FFFE6F9BA6", + "batteryPercentage": 85, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "044b63e7-999d-4caa-8a76-fb8cfd32b381_1", + "type": "repeater", + "deviceType": "repeater", + "createdAt": "2024-10-10T10:10:37.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T10:12:28.000Z", + "attributes": { + "customName": "Reaper", + "model": "TRADFRI Signal Repeater", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "14B457FFFEF8DC61", + "productCode": "E1746", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "45165c60-0c1a-4b9f-ae09-c4a6220d1420_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:09:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:02:56.000Z", + "attributes": { + "customName": "Lampe Links", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "881A14FFFE479E82", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 51, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Esszimmer Hinten", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:20:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "attributes": { + "customName": "Loft Floor Lamp", + "model": "TRADFRI bulb E27 CWS opal 600lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "EC1BBDFFFEC17EA1", + "productCode": "LED1624G9E27EU", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "338bb721-35bb-4775-8cd0-ba70fc37ab10_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-04T17:21:49.446Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:09:15.285Z", + "attributes": { + "customName": "Loft", + "model": "SYMFONISK Bookshelf S21", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.3.3-2.0", + "serialNumber": "34-7E-5C-F5-45-88:F", + "productCode": "S21", + "identifyStarted": "2024-10-04T17:21:49.447Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-13T21:09:15.284Z", + "playbackAudio": { + "serviceType": "sonos", + "providerType": "Mixcloud", + "playItem": { + "id": "eyJhY2NvdW50VHlwZSI6InNvbm9zIiwiY29udGVudFR5cGUiOiJjb250YWluZXIiLCJjb250ZW50Ijp7ImNvbnRhaW5lciI6eyJhY2NvdW50SWQiOiJzbl8zIiwib2JqZWN0SWQiOiJjbG91ZGNhc3Q6MjE3OTQzNDcxNCIsInNlcnZpY2VJZCI6IjE4MSIsInR5cGUiOiJ0cmFjayJ9fX0=", + "title": "The Anjunadeep Edition 521 with Leaving Laurel & yehno", + "artist": "Anjunadeep", + "imageURL": "http://192.168.1.95:1400/getaa?s=1&u=x-sonos-http%3acloudcast%253a2179434714.unknown%3fsid%3d181%26flags%3d0%26sn%3d3", + "duration": 3617000 + } + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-13T21:09:15.284Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": true, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": true, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 16, + "isMuted": false, + "audioGroup": "1f386990-bb4f-4a00-befd-646b5ef52c47" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "03c716a9-900e-4bb0-8694-eaf837bfa3fb_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-06T09:06:12.272Z", + "isReachable": false, + "lastSeen": "2024-10-08T16:30:36.504Z", + "attributes": { + "customName": "Moritz", + "model": "SYMFONISK Table lamp S20", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.2.3-2.0", + "serialNumber": "78-28-CA-81-8E-F2:1", + "productCode": "S20", + "identifyStarted": "2024-10-06T09:06:12.272Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-06T09:06:12.272Z", + "playbackAudio": { + + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-06T09:06:12.032Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": false, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": false, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 15, + "isMuted": false, + "audioGroup": "71aa115c-fadd-4c43-aaf7-d537fa939638" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": true + }, + { + "id": "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-09T21:23:19.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:22:08.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Sensor Kitchen", + "model": "PARASOLL Door/Window Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "serialNumber": "D44867FFFEC2D1F0", + "productCode": "E2013", + "batteryPercentage": 84, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:00:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:02.000Z", + "customIcon": "lighting_cone_pendant", + "attributes": { + "customName": "Esszimmer Vorne", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFEC9113D", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 0, + "colorSaturation": 1, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-10T14:40:38.000Z", + "attributes": { + "customName": "Smart-Home-Steckdose 1", + "model": "INSPELNING Smart plug", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.4.45", + "hardwareVersion": "1", + "serialNumber": "ECF64CFFFEF28EC3", + "productCode": "E2206", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 1, + "startUpCurrentLevel": -1, + "currentVoltage": 236.8000030517578, + "currentAmps": 0.003000000026077032, + "currentActivePower": 0, + "totalEnergyConsumed": 0.01899999938905239, + "totalEnergyConsumedLastUpdated": "2024-10-10T14:40:05.000Z", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "childLock": false, + "statusLight": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T17:38:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T17:18:25.000Z", + "attributes": { + "customName": "", + "model": "", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.10", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE66D457", + "productCode": "L2112", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 95, + "startUpCurrentLevel": -1, + "colorHue": 29.9981689453125, + "colorSaturation": 0.6496062992125984, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_1", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "Motion Sensor Gäste WC", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "batteryPercentage": 85, + "isOn": false, + "motionDetectedDelay": 20, + "isDetected": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": true, + "onDuration": 300, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_3", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "illuminance": 1, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [] + }, + { + "id": "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:46:04.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:06:16.000Z", + "attributes": { + "customName": "Bathroom Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x90", + "productCode": "", + "serialNumber": "804B50FFFEF79502", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 49, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "073b40df-e6a0-486d-99a3-6134effe4c59_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:48:44.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:21:17.000Z", + "customIcon": "lighting_fan", + "attributes": { + "customName": "HWR Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x30", + "productCode": "", + "serialNumber": "BC33ACFFFE9761AA", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 73, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [ + "ee61c57f-8efa-44f4-ba8a-d108ae054138_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "ee61c57f-8efa-44f4-ba8a-d108ae054138_1", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:56:23.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:18:17.000Z", + "attributes": { + "customName": "HWR Motion", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + "productCode": "E1745", + "serialNumber": "BC33ACFFFE8739CC", + "batteryPercentage": 20, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "isDetected": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "sensorConfig": { + "scheduleOn": true, + "onDuration": 180, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "3267c0f3-069a-4894-b5d0-a30d26386af8_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:00:00.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T13:31:10.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Dining Room", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "D44867FFFEB36824", + "batteryPercentage": 82, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "bab217dc-5d46-42af-a3a4-6a53b9a953da_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:05:20.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:36:48.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Entry Door", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "F84477FFFEDC7871", + "batteryPercentage": 81, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_1", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "Bedroom Motion", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "batteryPercentage": 84, + "isOn": false, + "isDetected": false, + "motionDetectedDelay": 20, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": false, + "onDuration": 180 + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "illuminance": 1, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9e9cb6b1-7981-443a-bd1e-5301815a25bd_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-13T20:09:35.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T00:32:59.000Z", + "attributes": { + "customName": "HWR Remote", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + "productCode": "E1810", + "serialNumber": "BC33ACFFFE9B308C", + "batteryPercentage": 30, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + { + "id": "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58", + "info": { + "name": "Lightmode Christmas", + "icon": "scenes_tree" + }, + "type": "userScene", + "triggers": [ + { + "id": "987ec615-24d8-411c-9aa9-d03c447b246b", + "type": "app", + "triggeredAt": "2024-10-16T00:21:15.942Z", + "disabled": false + } + ], + "actions": [ + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 0, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T11:13:42.273Z", + "lastCompleted": "2024-10-16T00:21:15.977Z", + "lastTriggered": "2024-10-16T00:21:15.977Z", + "undoAllowedDuration": 30, + "lastUndo": "2024-10-15T19:36:59.273Z" + }, + { + "id": "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb", + "info": { + "name": "Lightmode off", + "icon": "scenes_disco_ball" + }, + "type": "userScene", + "triggers": [ + { + "id": "6fff3797-c798-4d0c-abe0-ce46cc389104", + "type": "app", + "triggeredAt": "2024-10-16T00:38:16.257Z", + "disabled": false + } + ], + "actions": [ + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 359.83392, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T23:14:12.399Z", + "lastCompleted": "2024-10-16T00:38:16.267Z", + "lastTriggered": "2024-10-16T00:38:16.267Z", + "undoAllowedDuration": 30 + } + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/gateway/home-without-coordinates.json b/bundles/org.openhab.binding.dirigera/src/test/resources/gateway/home-without-coordinates.json new file mode 100644 index 00000000000..b3caf2ad974 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/gateway/home-without-coordinates.json @@ -0,0 +1,1605 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": null, + "nextSunRise": null, + "homestate": "home", + "countryCode": "XZ", + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "deviceType": "environmentSensor", + "remoteLinks": [], + "createdAt": "2024-10-17T08:00:25.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T08:07:42.000Z", + "capabilities": { + "canReceive": [ + "customName" + ], + "canSend": [] + }, + "attributes": { + "maxMeasuredPM25": 999, + "identifyPeriod": 0, + "currentTemperature": 20, + "permittingJoin": false, + "serialNumber": "28DBA7FFFEDFFBF6", + "minMeasuredPM25": 0, + "otaScheduleStart": "00:00", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "customName": "Cleany", + "currentPM25": 11, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "vocIndex": 100, + "manufacturer": "IKEA of Sweden", + "productCode": "E2112", + "otaState": "readyToCheck", + "hardwareVersion": "1", + "model": "VINDSTYRKA", + "firmwareVersion": "1.0.11", + "currentRH": 76 + }, + "id": "f80cac12-65a4-47b4-9f68-a0456a349a43_1", + "type": "sensor", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false + }, + { + "id": "5ffa4a1e-6fe2-4a24-b0af-b216937c4e8c_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:04:54.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:08:02.000Z", + "attributes": { + "customName": "Lampe Rechts", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "30FB10FFFEE44843", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 54, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-11T11:33:27.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T11:22:41.000Z", + "attributes": { + "customName": "Remote Control Loft", + "firmwareVersion": "2.4.16", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "Remote Control N2", + "productCode": "E2001", + "serialNumber": "94B216FFFE6F9BA6", + "batteryPercentage": 85, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "044b63e7-999d-4caa-8a76-fb8cfd32b381_1", + "type": "repeater", + "deviceType": "repeater", + "createdAt": "2024-10-10T10:10:37.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T10:12:28.000Z", + "attributes": { + "customName": "Reaper", + "model": "TRADFRI Signal Repeater", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "14B457FFFEF8DC61", + "productCode": "E1746", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "45165c60-0c1a-4b9f-ae09-c4a6220d1420_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:09:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:02:56.000Z", + "attributes": { + "customName": "Lampe Links", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "881A14FFFE479E82", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 51, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Esszimmer Hinten", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:20:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "attributes": { + "customName": "Loft Floor Lamp", + "model": "TRADFRI bulb E27 CWS opal 600lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "EC1BBDFFFEC17EA1", + "productCode": "LED1624G9E27EU", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "338bb721-35bb-4775-8cd0-ba70fc37ab10_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-04T17:21:49.446Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:09:15.285Z", + "attributes": { + "customName": "Loft", + "model": "SYMFONISK Bookshelf S21", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.3.3-2.0", + "serialNumber": "34-7E-5C-F5-45-88:F", + "productCode": "S21", + "identifyStarted": "2024-10-04T17:21:49.447Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-13T21:09:15.284Z", + "playbackAudio": { + "serviceType": "sonos", + "providerType": "Mixcloud", + "playItem": { + "id": "eyJhY2NvdW50VHlwZSI6InNvbm9zIiwiY29udGVudFR5cGUiOiJjb250YWluZXIiLCJjb250ZW50Ijp7ImNvbnRhaW5lciI6eyJhY2NvdW50SWQiOiJzbl8zIiwib2JqZWN0SWQiOiJjbG91ZGNhc3Q6MjE3OTQzNDcxNCIsInNlcnZpY2VJZCI6IjE4MSIsInR5cGUiOiJ0cmFjayJ9fX0=", + "title": "The Anjunadeep Edition 521 with Leaving Laurel & yehno", + "artist": "Anjunadeep", + "imageURL": "http://192.168.1.95:1400/getaa?s=1&u=x-sonos-http%3acloudcast%253a2179434714.unknown%3fsid%3d181%26flags%3d0%26sn%3d3", + "duration": 3617000 + } + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-13T21:09:15.284Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": true, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": true, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 16, + "isMuted": false, + "audioGroup": "1f386990-bb4f-4a00-befd-646b5ef52c47" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "03c716a9-900e-4bb0-8694-eaf837bfa3fb_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-06T09:06:12.272Z", + "isReachable": false, + "lastSeen": "2024-10-08T16:30:36.504Z", + "attributes": { + "customName": "Moritz", + "model": "SYMFONISK Table lamp S20", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.2.3-2.0", + "serialNumber": "78-28-CA-81-8E-F2:1", + "productCode": "S20", + "identifyStarted": "2024-10-06T09:06:12.272Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-06T09:06:12.272Z", + "playbackAudio": { + + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-06T09:06:12.032Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": false, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": false, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 15, + "isMuted": false, + "audioGroup": "71aa115c-fadd-4c43-aaf7-d537fa939638" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": true + }, + { + "id": "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-09T21:23:19.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:22:08.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Sensor Kitchen", + "model": "PARASOLL Door/Window Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "serialNumber": "D44867FFFEC2D1F0", + "productCode": "E2013", + "batteryPercentage": 84, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:00:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:02.000Z", + "customIcon": "lighting_cone_pendant", + "attributes": { + "customName": "Esszimmer Vorne", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFEC9113D", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 0, + "colorSaturation": 1, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-10T14:40:38.000Z", + "attributes": { + "customName": "Smart-Home-Steckdose 1", + "model": "INSPELNING Smart plug", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.4.45", + "hardwareVersion": "1", + "serialNumber": "ECF64CFFFEF28EC3", + "productCode": "E2206", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 1, + "startUpCurrentLevel": -1, + "currentVoltage": 236.8000030517578, + "currentAmps": 0.003000000026077032, + "currentActivePower": 0, + "totalEnergyConsumed": 0.01899999938905239, + "totalEnergyConsumedLastUpdated": "2024-10-10T14:40:05.000Z", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "childLock": false, + "statusLight": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T17:38:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T17:18:25.000Z", + "attributes": { + "customName": "", + "model": "", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.10", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE66D457", + "productCode": "L2112", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 95, + "startUpCurrentLevel": -1, + "colorHue": 29.9981689453125, + "colorSaturation": 0.6496062992125984, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_1", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "Motion Sensor Gäste WC", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "batteryPercentage": 85, + "isOn": false, + "motionDetectedDelay": 20, + "isDetected": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": true, + "onDuration": 300, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_3", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "illuminance": 1, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": null, + "nextSunRise": null, + "homestate": "home", + "countryCode": "XZ", + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [] + }, + { + "id": "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:46:04.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:06:16.000Z", + "attributes": { + "customName": "Bathroom Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x90", + "productCode": "", + "serialNumber": "804B50FFFEF79502", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 49, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "073b40df-e6a0-486d-99a3-6134effe4c59_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:48:44.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:21:17.000Z", + "customIcon": "lighting_fan", + "attributes": { + "customName": "HWR Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x30", + "productCode": "", + "serialNumber": "BC33ACFFFE9761AA", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 73, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [ + "ee61c57f-8efa-44f4-ba8a-d108ae054138_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "ee61c57f-8efa-44f4-ba8a-d108ae054138_1", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:56:23.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:18:17.000Z", + "attributes": { + "customName": "HWR Motion", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + "productCode": "E1745", + "serialNumber": "BC33ACFFFE8739CC", + "batteryPercentage": 20, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "isDetected": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "sensorConfig": { + "scheduleOn": true, + "onDuration": 180, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "3267c0f3-069a-4894-b5d0-a30d26386af8_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:00:00.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T13:31:10.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Dining Room", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "D44867FFFEB36824", + "batteryPercentage": 82, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "bab217dc-5d46-42af-a3a4-6a53b9a953da_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:05:20.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:36:48.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Entry Door", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "F84477FFFEDC7871", + "batteryPercentage": 81, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_1", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "Bedroom Motion", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "batteryPercentage": 84, + "isOn": false, + "isDetected": false, + "motionDetectedDelay": 20, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": false, + "onDuration": 180 + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "illuminance": 1, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9e9cb6b1-7981-443a-bd1e-5301815a25bd_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-13T20:09:35.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T00:32:59.000Z", + "attributes": { + "customName": "HWR Remote", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + "productCode": "E1810", + "serialNumber": "BC33ACFFFE9B308C", + "batteryPercentage": 30, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + { + "id": "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58", + "info": { + "name": "Lightmode Christmas", + "icon": "scenes_tree" + }, + "type": "userScene", + "triggers": [ + { + "id": "987ec615-24d8-411c-9aa9-d03c447b246b", + "type": "app", + "triggeredAt": "2024-10-16T00:21:15.942Z", + "disabled": false + } + ], + "actions": [ + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 0, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T11:13:42.273Z", + "lastCompleted": "2024-10-16T00:21:15.977Z", + "lastTriggered": "2024-10-16T00:21:15.977Z", + "undoAllowedDuration": 30, + "lastUndo": "2024-10-15T19:36:59.273Z" + }, + { + "id": "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb", + "info": { + "name": "Lightmode off", + "icon": "scenes_disco_ball" + }, + "type": "userScene", + "triggers": [ + { + "id": "6fff3797-c798-4d0c-abe0-ce46cc389104", + "type": "app", + "triggeredAt": "2024-10-16T00:38:16.257Z", + "disabled": false + } + ], + "actions": [ + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 359.83392, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T23:14:12.399Z", + "lastCompleted": "2024-10-16T00:38:16.267Z", + "lastTriggered": "2024-10-16T00:38:16.267Z", + "undoAllowedDuration": 30 + } + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/home/home-one-device.json b/bundles/org.openhab.binding.dirigera/src/test/resources/home/home-one-device.json new file mode 100644 index 00000000000..d7e813003a5 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/home/home-one-device.json @@ -0,0 +1,174 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/home/home-with-double-shortcut.json b/bundles/org.openhab.binding.dirigera/src/test/resources/home/home-with-double-shortcut.json new file mode 100644 index 00000000000..8cd7c3943ae --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/home/home-with-double-shortcut.json @@ -0,0 +1,259 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-27T20:14:00.811Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.5", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2024-10-17T02:30:32.000Z", + "identifyPeriod": 65534, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 100, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-28T16:10:00.000Z", + "nextSunRise": "2024-10-28T06:11:00.000Z", + "homestate": "home", + "countryCode": "EN", + "coordinates": { + "latitude": 1.23, + "longitude": 9.876, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "id": "854bdf30-86b8-48f5-b070-16ff5ab12be4_1", + "relationId": "854bdf30-86b8-48f5-b070-16ff5ab12be4", + "type": "controller", + "deviceType": "shortcutController", + "createdAt": "2024-10-17T14:23:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-27T18:11:55.000Z", + "attributes": { + "customName": "Clicky", + "firmwareVersion": "1.0.21", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "SOMRIG shortcut button", + "productCode": "E2213", + "serialNumber": "ECF64CFFFE1EE52F", + "batteryPercentage": 89, + "switchLabel": "Shortcut 1", + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [ + "singlePress", + "longPress", + "doublePress" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "854bdf30-86b8-48f5-b070-16ff5ab12be4_2", + "relationId": "854bdf30-86b8-48f5-b070-16ff5ab12be4", + "type": "controller", + "deviceType": "shortcutController", + "createdAt": "2024-10-17T14:23:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-27T18:11:55.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.21", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "SOMRIG shortcut button", + "productCode": "E2213", + "serialNumber": "ECF64CFFFE1EE52F", + "switchLabel": "Shortcut 2", + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [ + "singlePress", + "longPress", + "doublePress" + ], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [ + ], + "scenes": [ + ], + "rooms": [ + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + } + ], + "deviceSets": [], + "adaptiveProfiles": [ + { + "id": "f7fd67f5-4c3f-401b-9dea-211fba48a844", + "adaptiveSchedule": [ + { + "startTime": "07:00", + "lightLevel": 25, + "colorTemperature": 3000 + }, + { + "startTime": "09:00", + "lightLevel": 75, + "colorTemperature": 3900 + }, + { + "startTime": "13:00", + "lightLevel": 100, + "colorTemperature": 4000 + }, + { + "startTime": "17:00", + "lightLevel": 70, + "colorTemperature": 4000 + }, + { + "startTime": "20:00", + "lightLevel": 20, + "colorTemperature": 3900 + }, + { + "startTime": "22:00", + "lightLevel": 1, + "colorTemperature": 3000 + } + ] + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/home/home.json b/bundles/org.openhab.binding.dirigera/src/test/resources/home/home.json new file mode 100644 index 00000000000..038ccd33080 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/home/home.json @@ -0,0 +1,1615 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "deviceType": "environmentSensor", + "remoteLinks": [], + "createdAt": "2024-10-17T08:00:25.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T08:07:42.000Z", + "capabilities": { + "canReceive": [ + "customName" + ], + "canSend": [] + }, + "attributes": { + "maxMeasuredPM25": 999, + "identifyPeriod": 0, + "currentTemperature": 20, + "permittingJoin": false, + "serialNumber": "28DBA7FFFEDFFBF6", + "minMeasuredPM25": 0, + "otaScheduleStart": "00:00", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "customName": "Cleany", + "currentPM25": 11, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "vocIndex": 100, + "manufacturer": "IKEA of Sweden", + "productCode": "E2112", + "otaState": "readyToCheck", + "hardwareVersion": "1", + "model": "VINDSTYRKA", + "firmwareVersion": "1.0.11", + "currentRH": 76 + }, + "id": "f80cac12-65a4-47b4-9f68-a0456a349a43_1", + "type": "sensor", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false + }, + { + "id": "5ffa4a1e-6fe2-4a24-b0af-b216937c4e8c_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:04:54.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:08:02.000Z", + "attributes": { + "customName": "Lampe Rechts", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "30FB10FFFEE44843", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 54, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-11T11:33:27.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T11:22:41.000Z", + "attributes": { + "customName": "Remote Control Loft", + "firmwareVersion": "2.4.16", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "Remote Control N2", + "productCode": "E2001", + "serialNumber": "94B216FFFE6F9BA6", + "batteryPercentage": 85, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "044b63e7-999d-4caa-8a76-fb8cfd32b381_1", + "type": "repeater", + "deviceType": "repeater", + "createdAt": "2024-10-10T10:10:37.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T10:12:28.000Z", + "attributes": { + "customName": "Reaper", + "model": "TRADFRI Signal Repeater", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "14B457FFFEF8DC61", + "productCode": "E1746", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "45165c60-0c1a-4b9f-ae09-c4a6220d1420_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:09:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:02:56.000Z", + "attributes": { + "customName": "Lampe Links", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "881A14FFFE479E82", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 51, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Esszimmer Hinten", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:20:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "attributes": { + "customName": "Loft Floor Lamp", + "model": "TRADFRI bulb E27 CWS opal 600lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "EC1BBDFFFEC17EA1", + "productCode": "LED1624G9E27EU", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "338bb721-35bb-4775-8cd0-ba70fc37ab10_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-04T17:21:49.446Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:09:15.285Z", + "attributes": { + "customName": "Loft", + "model": "SYMFONISK Bookshelf S21", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.3.3-2.0", + "serialNumber": "34-7E-5C-F5-45-88:F", + "productCode": "S21", + "identifyStarted": "2024-10-04T17:21:49.447Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-13T21:09:15.284Z", + "playbackAudio": { + "serviceType": "sonos", + "providerType": "Mixcloud", + "playItem": { + "id": "eyJhY2NvdW50VHlwZSI6InNvbm9zIiwiY29udGVudFR5cGUiOiJjb250YWluZXIiLCJjb250ZW50Ijp7ImNvbnRhaW5lciI6eyJhY2NvdW50SWQiOiJzbl8zIiwib2JqZWN0SWQiOiJjbG91ZGNhc3Q6MjE3OTQzNDcxNCIsInNlcnZpY2VJZCI6IjE4MSIsInR5cGUiOiJ0cmFjayJ9fX0=", + "title": "The Anjunadeep Edition 521 with Leaving Laurel & yehno", + "artist": "Anjunadeep", + "imageURL": "http://192.168.1.95:1400/getaa?s=1&u=x-sonos-http%3acloudcast%253a2179434714.unknown%3fsid%3d181%26flags%3d0%26sn%3d3", + "duration": 3617000 + } + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-13T21:09:15.284Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": true, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": true, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 16, + "isMuted": false, + "audioGroup": "1f386990-bb4f-4a00-befd-646b5ef52c47" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "03c716a9-900e-4bb0-8694-eaf837bfa3fb_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-06T09:06:12.272Z", + "isReachable": false, + "lastSeen": "2024-10-08T16:30:36.504Z", + "attributes": { + "customName": "Moritz", + "model": "SYMFONISK Table lamp S20", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.2.3-2.0", + "serialNumber": "78-28-CA-81-8E-F2:1", + "productCode": "S20", + "identifyStarted": "2024-10-06T09:06:12.272Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-06T09:06:12.272Z", + "playbackAudio": { + + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-06T09:06:12.032Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": false, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": false, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 15, + "isMuted": false, + "audioGroup": "71aa115c-fadd-4c43-aaf7-d537fa939638" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": true + }, + { + "id": "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-09T21:23:19.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:22:08.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Sensor Kitchen", + "model": "PARASOLL Door/Window Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "serialNumber": "D44867FFFEC2D1F0", + "productCode": "E2013", + "batteryPercentage": 84, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:00:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:02.000Z", + "customIcon": "lighting_cone_pendant", + "attributes": { + "customName": "Esszimmer Vorne", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFEC9113D", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 0, + "colorSaturation": 1, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-10T14:40:38.000Z", + "attributes": { + "customName": "Smart-Home-Steckdose 1", + "model": "INSPELNING Smart plug", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.4.45", + "hardwareVersion": "1", + "serialNumber": "ECF64CFFFEF28EC3", + "productCode": "E2206", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 1, + "startUpCurrentLevel": -1, + "currentVoltage": 236.8000030517578, + "currentAmps": 0.003000000026077032, + "currentActivePower": 0, + "totalEnergyConsumed": 0.01899999938905239, + "totalEnergyConsumedLastUpdated": "2024-10-10T14:40:05.000Z", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "childLock": false, + "statusLight": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T17:38:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T17:18:25.000Z", + "attributes": { + "customName": "", + "model": "", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.10", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE66D457", + "productCode": "L2112", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 95, + "startUpCurrentLevel": -1, + "colorHue": 29.9981689453125, + "colorSaturation": 0.6496062992125984, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_1", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "Motion Sensor Gäste WC", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "batteryPercentage": 85, + "isOn": false, + "motionDetectedDelay": 20, + "isDetected": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": true, + "onDuration": 300, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_3", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "illuminance": 1, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [] + }, + { + "id": "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:46:04.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:06:16.000Z", + "attributes": { + "customName": "Bathroom Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x90", + "productCode": "", + "serialNumber": "804B50FFFEF79502", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 49, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "073b40df-e6a0-486d-99a3-6134effe4c59_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:48:44.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:21:17.000Z", + "customIcon": "lighting_fan", + "attributes": { + "customName": "HWR Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x30", + "productCode": "", + "serialNumber": "BC33ACFFFE9761AA", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 73, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [ + "ee61c57f-8efa-44f4-ba8a-d108ae054138_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "ee61c57f-8efa-44f4-ba8a-d108ae054138_1", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:56:23.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:18:17.000Z", + "attributes": { + "customName": "HWR Motion", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + "productCode": "E1745", + "serialNumber": "BC33ACFFFE8739CC", + "batteryPercentage": 20, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "isDetected": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "sensorConfig": { + "scheduleOn": true, + "onDuration": 180, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "3267c0f3-069a-4894-b5d0-a30d26386af8_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:00:00.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T13:31:10.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Dining Room", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "D44867FFFEB36824", + "batteryPercentage": 82, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "bab217dc-5d46-42af-a3a4-6a53b9a953da_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:05:20.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:36:48.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Entry Door", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "F84477FFFEDC7871", + "batteryPercentage": 81, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_1", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "Bedroom Motion", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "batteryPercentage": 84, + "isOn": false, + "isDetected": false, + "motionDetectedDelay": 20, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": false, + "onDuration": 180 + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "illuminance": 1, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9e9cb6b1-7981-443a-bd1e-5301815a25bd_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-13T20:09:35.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T00:32:59.000Z", + "attributes": { + "customName": "HWR Remote", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + "productCode": "E1810", + "serialNumber": "BC33ACFFFE9B308C", + "batteryPercentage": 30, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + { + "id": "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58", + "info": { + "name": "Lightmode Christmas", + "icon": "scenes_tree" + }, + "type": "userScene", + "triggers": [ + { + "id": "987ec615-24d8-411c-9aa9-d03c447b246b", + "type": "app", + "triggeredAt": "2024-10-16T00:21:15.942Z", + "disabled": false + } + ], + "actions": [ + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 0, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T11:13:42.273Z", + "lastCompleted": "2024-10-16T00:21:15.977Z", + "lastTriggered": "2024-10-16T00:21:15.977Z", + "undoAllowedDuration": 30, + "lastUndo": "2024-10-15T19:36:59.273Z" + }, + { + "id": "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb", + "info": { + "name": "Lightmode off", + "icon": "scenes_disco_ball" + }, + "type": "userScene", + "triggers": [ + { + "id": "6fff3797-c798-4d0c-abe0-ce46cc389104", + "type": "app", + "triggeredAt": "2024-10-16T00:38:16.257Z", + "disabled": false + } + ], + "actions": [ + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 359.83392, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T23:14:12.399Z", + "lastCompleted": "2024-10-16T00:38:16.267Z", + "lastTriggered": "2024-10-16T00:38:16.267Z", + "undoAllowedDuration": 30 + } + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-added/device-added.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-added/device-added.json new file mode 100644 index 00000000000..0ff6d092067 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-added/device-added.json @@ -0,0 +1,51 @@ +{ + "data": { + "deviceType": "outlet", + "remoteLinks": [], + "createdAt": "2024-10-17T21:17:45.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T21:17:48.000Z", + "capabilities": { + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ], + "canSend": [] + }, + "attributes": { + "identifyPeriod": 0, + "permittingJoin": false, + "serialNumber": "D44867FFFE140EDE", + "otaScheduleStart": "00:00", + "otaProgress": 0, + "startUpCurrentLevel": -1, + "otaScheduleEnd": "00:00", + "customName": "", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "manufacturer": "IKEA of Sweden", + "lightLevel": 100, + "productCode": "E2204", + "statusLight": true, + "otaState": "readyToCheck", + "childLock": false, + "isOn": false, + "hardwareVersion": "1", + "model": "TRETAKT Smart plug", + "startupOnOff": "startPrevious", + "firmwareVersion": "2.4.4" + }, + "id": "339a37ea-791d-4b5d-8a1d-21d068578421_1", + "type": "outlet", + "deviceSet": [] + }, + "specversion": "3.163.0", + "id": "382f5a61-0b3a-4c55-b601-d89c003ac27c", + "time": "2024-10-17T21:17:48.000Z", + "source": "urn:com:ikea:homesmart:iotc:zigbee", + "type": "deviceAdded" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-added/home-after.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-added/home-after.json new file mode 100644 index 00000000000..590771a4cf9 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-added/home-after.json @@ -0,0 +1,1659 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "deviceType": "outlet", + "remoteLinks": [], + "createdAt": "2024-10-17T21:17:45.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T21:17:48.000Z", + "capabilities": { + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ], + "canSend": [] + }, + "attributes": { + "identifyPeriod": 0, + "permittingJoin": false, + "serialNumber": "D44867FFFE140EDE", + "otaScheduleStart": "00:00", + "otaProgress": 0, + "startUpCurrentLevel": -1, + "otaScheduleEnd": "00:00", + "customName": "", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "manufacturer": "IKEA of Sweden", + "lightLevel": 100, + "productCode": "E2204", + "statusLight": true, + "otaState": "readyToCheck", + "childLock": false, + "isOn": false, + "hardwareVersion": "1", + "model": "TRETAKT Smart plug", + "startupOnOff": "startPrevious", + "firmwareVersion": "2.4.4" + }, + "id": "339a37ea-791d-4b5d-8a1d-21d068578421_1", + "type": "outlet", + "deviceSet": [] + }, + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "deviceType": "environmentSensor", + "remoteLinks": [], + "createdAt": "2024-10-17T08:00:25.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T08:07:42.000Z", + "capabilities": { + "canReceive": [ + "customName" + ], + "canSend": [] + }, + "attributes": { + "maxMeasuredPM25": 999, + "identifyPeriod": 0, + "currentTemperature": 20, + "permittingJoin": false, + "serialNumber": "28DBA7FFFEDFFBF6", + "minMeasuredPM25": 0, + "otaScheduleStart": "00:00", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "customName": "Cleany", + "currentPM25": 11, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "vocIndex": 100, + "manufacturer": "IKEA of Sweden", + "productCode": "E2112", + "otaState": "readyToCheck", + "hardwareVersion": "1", + "model": "VINDSTYRKA", + "firmwareVersion": "1.0.11", + "currentRH": 76 + }, + "id": "f80cac12-65a4-47b4-9f68-a0456a349a43_1", + "type": "sensor", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false + }, + { + "id": "5ffa4a1e-6fe2-4a24-b0af-b216937c4e8c_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:04:54.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:08:02.000Z", + "attributes": { + "customName": "Lampe Rechts", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "30FB10FFFEE44843", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 54, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-11T11:33:27.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T11:22:41.000Z", + "attributes": { + "customName": "Remote Control Loft", + "firmwareVersion": "2.4.16", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "Remote Control N2", + "productCode": "E2001", + "serialNumber": "94B216FFFE6F9BA6", + "batteryPercentage": 85, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "044b63e7-999d-4caa-8a76-fb8cfd32b381_1", + "type": "repeater", + "deviceType": "repeater", + "createdAt": "2024-10-10T10:10:37.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T10:12:28.000Z", + "attributes": { + "customName": "Reaper", + "model": "TRADFRI Signal Repeater", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "14B457FFFEF8DC61", + "productCode": "E1746", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "45165c60-0c1a-4b9f-ae09-c4a6220d1420_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:09:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:02:56.000Z", + "attributes": { + "customName": "Lampe Links", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "881A14FFFE479E82", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 51, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Esszimmer Hinten", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:20:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "attributes": { + "customName": "Loft Floor Lamp", + "model": "TRADFRI bulb E27 CWS opal 600lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "EC1BBDFFFEC17EA1", + "productCode": "LED1624G9E27EU", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "338bb721-35bb-4775-8cd0-ba70fc37ab10_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-04T17:21:49.446Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:09:15.285Z", + "attributes": { + "customName": "Loft", + "model": "SYMFONISK Bookshelf S21", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.3.3-2.0", + "serialNumber": "34-7E-5C-F5-45-88:F", + "productCode": "S21", + "identifyStarted": "2024-10-04T17:21:49.447Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-13T21:09:15.284Z", + "playbackAudio": { + "serviceType": "sonos", + "providerType": "Mixcloud", + "playItem": { + "id": "eyJhY2NvdW50VHlwZSI6InNvbm9zIiwiY29udGVudFR5cGUiOiJjb250YWluZXIiLCJjb250ZW50Ijp7ImNvbnRhaW5lciI6eyJhY2NvdW50SWQiOiJzbl8zIiwib2JqZWN0SWQiOiJjbG91ZGNhc3Q6MjE3OTQzNDcxNCIsInNlcnZpY2VJZCI6IjE4MSIsInR5cGUiOiJ0cmFjayJ9fX0=", + "title": "The Anjunadeep Edition 521 with Leaving Laurel & yehno", + "artist": "Anjunadeep", + "imageURL": "http://192.168.1.95:1400/getaa?s=1&u=x-sonos-http%3acloudcast%253a2179434714.unknown%3fsid%3d181%26flags%3d0%26sn%3d3", + "duration": 3617000 + } + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-13T21:09:15.284Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": true, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": true, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 16, + "isMuted": false, + "audioGroup": "1f386990-bb4f-4a00-befd-646b5ef52c47" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "03c716a9-900e-4bb0-8694-eaf837bfa3fb_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-06T09:06:12.272Z", + "isReachable": false, + "lastSeen": "2024-10-08T16:30:36.504Z", + "attributes": { + "customName": "Moritz", + "model": "SYMFONISK Table lamp S20", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.2.3-2.0", + "serialNumber": "78-28-CA-81-8E-F2:1", + "productCode": "S20", + "identifyStarted": "2024-10-06T09:06:12.272Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-06T09:06:12.272Z", + "playbackAudio": { + + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-06T09:06:12.032Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": false, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": false, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 15, + "isMuted": false, + "audioGroup": "71aa115c-fadd-4c43-aaf7-d537fa939638" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": true + }, + { + "id": "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-09T21:23:19.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:22:08.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Sensor Kitchen", + "model": "PARASOLL Door/Window Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "serialNumber": "D44867FFFEC2D1F0", + "productCode": "E2013", + "batteryPercentage": 84, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:00:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:02.000Z", + "customIcon": "lighting_cone_pendant", + "attributes": { + "customName": "Esszimmer Vorne", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFEC9113D", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 0, + "colorSaturation": 1, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-10T14:40:38.000Z", + "attributes": { + "customName": "Smart-Home-Steckdose 1", + "model": "INSPELNING Smart plug", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.4.45", + "hardwareVersion": "1", + "serialNumber": "ECF64CFFFEF28EC3", + "productCode": "E2206", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 1, + "startUpCurrentLevel": -1, + "currentVoltage": 236.8000030517578, + "currentAmps": 0.003000000026077032, + "currentActivePower": 0, + "totalEnergyConsumed": 0.01899999938905239, + "totalEnergyConsumedLastUpdated": "2024-10-10T14:40:05.000Z", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "childLock": false, + "statusLight": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T17:38:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T17:18:25.000Z", + "attributes": { + "customName": "", + "model": "", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.10", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE66D457", + "productCode": "L2112", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 95, + "startUpCurrentLevel": -1, + "colorHue": 29.9981689453125, + "colorSaturation": 0.6496062992125984, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_1", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "Motion Sensor Gäste WC", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "batteryPercentage": 85, + "isOn": false, + "motionDetectedDelay": 20, + "isDetected": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": true, + "onDuration": 300, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_3", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "illuminance": 1, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [] + }, + { + "id": "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:46:04.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:06:16.000Z", + "attributes": { + "customName": "Bathroom Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x90", + "productCode": "", + "serialNumber": "804B50FFFEF79502", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 49, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "073b40df-e6a0-486d-99a3-6134effe4c59_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:48:44.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:21:17.000Z", + "customIcon": "lighting_fan", + "attributes": { + "customName": "HWR Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x30", + "productCode": "", + "serialNumber": "BC33ACFFFE9761AA", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 73, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [ + "ee61c57f-8efa-44f4-ba8a-d108ae054138_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "ee61c57f-8efa-44f4-ba8a-d108ae054138_1", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:56:23.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:18:17.000Z", + "attributes": { + "customName": "HWR Motion", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + "productCode": "E1745", + "serialNumber": "BC33ACFFFE8739CC", + "batteryPercentage": 20, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "isDetected": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "sensorConfig": { + "scheduleOn": true, + "onDuration": 180, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "3267c0f3-069a-4894-b5d0-a30d26386af8_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:00:00.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T13:31:10.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Dining Room", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "D44867FFFEB36824", + "batteryPercentage": 82, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "bab217dc-5d46-42af-a3a4-6a53b9a953da_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:05:20.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:36:48.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Entry Door", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "F84477FFFEDC7871", + "batteryPercentage": 81, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_1", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "Bedroom Motion", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "batteryPercentage": 84, + "isOn": false, + "isDetected": false, + "motionDetectedDelay": 20, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": false, + "onDuration": 180 + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "illuminance": 1, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9e9cb6b1-7981-443a-bd1e-5301815a25bd_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-13T20:09:35.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T00:32:59.000Z", + "attributes": { + "customName": "HWR Remote", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + "productCode": "E1810", + "serialNumber": "BC33ACFFFE9B308C", + "batteryPercentage": 30, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + { + "id": "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58", + "info": { + "name": "Lightmode Christmas", + "icon": "scenes_tree" + }, + "type": "userScene", + "triggers": [ + { + "id": "987ec615-24d8-411c-9aa9-d03c447b246b", + "type": "app", + "triggeredAt": "2024-10-16T00:21:15.942Z", + "disabled": false + } + ], + "actions": [ + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 0, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T11:13:42.273Z", + "lastCompleted": "2024-10-16T00:21:15.977Z", + "lastTriggered": "2024-10-16T00:21:15.977Z", + "undoAllowedDuration": 30, + "lastUndo": "2024-10-15T19:36:59.273Z" + }, + { + "id": "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb", + "info": { + "name": "Lightmode off", + "icon": "scenes_disco_ball" + }, + "type": "userScene", + "triggers": [ + { + "id": "6fff3797-c798-4d0c-abe0-ce46cc389104", + "type": "app", + "triggeredAt": "2024-10-16T00:38:16.257Z", + "disabled": false + } + ], + "actions": [ + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 359.83392, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T23:14:12.399Z", + "lastCompleted": "2024-10-16T00:38:16.267Z", + "lastTriggered": "2024-10-16T00:38:16.267Z", + "undoAllowedDuration": 30 + } + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-added/home-before.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-added/home-before.json new file mode 100644 index 00000000000..038ccd33080 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-added/home-before.json @@ -0,0 +1,1615 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "deviceType": "environmentSensor", + "remoteLinks": [], + "createdAt": "2024-10-17T08:00:25.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T08:07:42.000Z", + "capabilities": { + "canReceive": [ + "customName" + ], + "canSend": [] + }, + "attributes": { + "maxMeasuredPM25": 999, + "identifyPeriod": 0, + "currentTemperature": 20, + "permittingJoin": false, + "serialNumber": "28DBA7FFFEDFFBF6", + "minMeasuredPM25": 0, + "otaScheduleStart": "00:00", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "customName": "Cleany", + "currentPM25": 11, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "vocIndex": 100, + "manufacturer": "IKEA of Sweden", + "productCode": "E2112", + "otaState": "readyToCheck", + "hardwareVersion": "1", + "model": "VINDSTYRKA", + "firmwareVersion": "1.0.11", + "currentRH": 76 + }, + "id": "f80cac12-65a4-47b4-9f68-a0456a349a43_1", + "type": "sensor", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false + }, + { + "id": "5ffa4a1e-6fe2-4a24-b0af-b216937c4e8c_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:04:54.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:08:02.000Z", + "attributes": { + "customName": "Lampe Rechts", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "30FB10FFFEE44843", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 54, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-11T11:33:27.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T11:22:41.000Z", + "attributes": { + "customName": "Remote Control Loft", + "firmwareVersion": "2.4.16", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "Remote Control N2", + "productCode": "E2001", + "serialNumber": "94B216FFFE6F9BA6", + "batteryPercentage": 85, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "044b63e7-999d-4caa-8a76-fb8cfd32b381_1", + "type": "repeater", + "deviceType": "repeater", + "createdAt": "2024-10-10T10:10:37.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T10:12:28.000Z", + "attributes": { + "customName": "Reaper", + "model": "TRADFRI Signal Repeater", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "14B457FFFEF8DC61", + "productCode": "E1746", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "45165c60-0c1a-4b9f-ae09-c4a6220d1420_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:09:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:02:56.000Z", + "attributes": { + "customName": "Lampe Links", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "881A14FFFE479E82", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 51, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Esszimmer Hinten", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:20:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "attributes": { + "customName": "Loft Floor Lamp", + "model": "TRADFRI bulb E27 CWS opal 600lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "EC1BBDFFFEC17EA1", + "productCode": "LED1624G9E27EU", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "338bb721-35bb-4775-8cd0-ba70fc37ab10_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-04T17:21:49.446Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:09:15.285Z", + "attributes": { + "customName": "Loft", + "model": "SYMFONISK Bookshelf S21", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.3.3-2.0", + "serialNumber": "34-7E-5C-F5-45-88:F", + "productCode": "S21", + "identifyStarted": "2024-10-04T17:21:49.447Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-13T21:09:15.284Z", + "playbackAudio": { + "serviceType": "sonos", + "providerType": "Mixcloud", + "playItem": { + "id": "eyJhY2NvdW50VHlwZSI6InNvbm9zIiwiY29udGVudFR5cGUiOiJjb250YWluZXIiLCJjb250ZW50Ijp7ImNvbnRhaW5lciI6eyJhY2NvdW50SWQiOiJzbl8zIiwib2JqZWN0SWQiOiJjbG91ZGNhc3Q6MjE3OTQzNDcxNCIsInNlcnZpY2VJZCI6IjE4MSIsInR5cGUiOiJ0cmFjayJ9fX0=", + "title": "The Anjunadeep Edition 521 with Leaving Laurel & yehno", + "artist": "Anjunadeep", + "imageURL": "http://192.168.1.95:1400/getaa?s=1&u=x-sonos-http%3acloudcast%253a2179434714.unknown%3fsid%3d181%26flags%3d0%26sn%3d3", + "duration": 3617000 + } + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-13T21:09:15.284Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": true, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": true, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 16, + "isMuted": false, + "audioGroup": "1f386990-bb4f-4a00-befd-646b5ef52c47" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "03c716a9-900e-4bb0-8694-eaf837bfa3fb_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-06T09:06:12.272Z", + "isReachable": false, + "lastSeen": "2024-10-08T16:30:36.504Z", + "attributes": { + "customName": "Moritz", + "model": "SYMFONISK Table lamp S20", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.2.3-2.0", + "serialNumber": "78-28-CA-81-8E-F2:1", + "productCode": "S20", + "identifyStarted": "2024-10-06T09:06:12.272Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-06T09:06:12.272Z", + "playbackAudio": { + + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-06T09:06:12.032Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": false, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": false, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 15, + "isMuted": false, + "audioGroup": "71aa115c-fadd-4c43-aaf7-d537fa939638" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": true + }, + { + "id": "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-09T21:23:19.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:22:08.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Sensor Kitchen", + "model": "PARASOLL Door/Window Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "serialNumber": "D44867FFFEC2D1F0", + "productCode": "E2013", + "batteryPercentage": 84, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:00:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:02.000Z", + "customIcon": "lighting_cone_pendant", + "attributes": { + "customName": "Esszimmer Vorne", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFEC9113D", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 0, + "colorSaturation": 1, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-10T14:40:38.000Z", + "attributes": { + "customName": "Smart-Home-Steckdose 1", + "model": "INSPELNING Smart plug", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.4.45", + "hardwareVersion": "1", + "serialNumber": "ECF64CFFFEF28EC3", + "productCode": "E2206", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 1, + "startUpCurrentLevel": -1, + "currentVoltage": 236.8000030517578, + "currentAmps": 0.003000000026077032, + "currentActivePower": 0, + "totalEnergyConsumed": 0.01899999938905239, + "totalEnergyConsumedLastUpdated": "2024-10-10T14:40:05.000Z", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "childLock": false, + "statusLight": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T17:38:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T17:18:25.000Z", + "attributes": { + "customName": "", + "model": "", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.10", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE66D457", + "productCode": "L2112", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 95, + "startUpCurrentLevel": -1, + "colorHue": 29.9981689453125, + "colorSaturation": 0.6496062992125984, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_1", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "Motion Sensor Gäste WC", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "batteryPercentage": 85, + "isOn": false, + "motionDetectedDelay": 20, + "isDetected": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": true, + "onDuration": 300, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_3", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "illuminance": 1, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [] + }, + { + "id": "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:46:04.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:06:16.000Z", + "attributes": { + "customName": "Bathroom Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x90", + "productCode": "", + "serialNumber": "804B50FFFEF79502", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 49, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "073b40df-e6a0-486d-99a3-6134effe4c59_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:48:44.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:21:17.000Z", + "customIcon": "lighting_fan", + "attributes": { + "customName": "HWR Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x30", + "productCode": "", + "serialNumber": "BC33ACFFFE9761AA", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 73, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [ + "ee61c57f-8efa-44f4-ba8a-d108ae054138_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "ee61c57f-8efa-44f4-ba8a-d108ae054138_1", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:56:23.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:18:17.000Z", + "attributes": { + "customName": "HWR Motion", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + "productCode": "E1745", + "serialNumber": "BC33ACFFFE8739CC", + "batteryPercentage": 20, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "isDetected": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "sensorConfig": { + "scheduleOn": true, + "onDuration": 180, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "3267c0f3-069a-4894-b5d0-a30d26386af8_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:00:00.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T13:31:10.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Dining Room", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "D44867FFFEB36824", + "batteryPercentage": 82, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "bab217dc-5d46-42af-a3a4-6a53b9a953da_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:05:20.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:36:48.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Entry Door", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "F84477FFFEDC7871", + "batteryPercentage": 81, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_1", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "Bedroom Motion", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "batteryPercentage": 84, + "isOn": false, + "isDetected": false, + "motionDetectedDelay": 20, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": false, + "onDuration": 180 + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "illuminance": 1, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9e9cb6b1-7981-443a-bd1e-5301815a25bd_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-13T20:09:35.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T00:32:59.000Z", + "attributes": { + "customName": "HWR Remote", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + "productCode": "E1810", + "serialNumber": "BC33ACFFFE9B308C", + "batteryPercentage": 30, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + { + "id": "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58", + "info": { + "name": "Lightmode Christmas", + "icon": "scenes_tree" + }, + "type": "userScene", + "triggers": [ + { + "id": "987ec615-24d8-411c-9aa9-d03c447b246b", + "type": "app", + "triggeredAt": "2024-10-16T00:21:15.942Z", + "disabled": false + } + ], + "actions": [ + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 0, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T11:13:42.273Z", + "lastCompleted": "2024-10-16T00:21:15.977Z", + "lastTriggered": "2024-10-16T00:21:15.977Z", + "undoAllowedDuration": 30, + "lastUndo": "2024-10-15T19:36:59.273Z" + }, + { + "id": "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb", + "info": { + "name": "Lightmode off", + "icon": "scenes_disco_ball" + }, + "type": "userScene", + "triggers": [ + { + "id": "6fff3797-c798-4d0c-abe0-ce46cc389104", + "type": "app", + "triggeredAt": "2024-10-16T00:38:16.257Z", + "disabled": false + } + ], + "actions": [ + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 359.83392, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T23:14:12.399Z", + "lastCompleted": "2024-10-16T00:38:16.267Z", + "lastTriggered": "2024-10-16T00:38:16.267Z", + "undoAllowedDuration": 30 + } + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-removed/device-removed.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-removed/device-removed.json new file mode 100644 index 00000000000..9c997074ea3 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-removed/device-removed.json @@ -0,0 +1,51 @@ +{ + "data": { + "deviceType": "outlet", + "remoteLinks": [], + "createdAt": "2024-10-17T21:17:45.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T21:17:48.000Z", + "capabilities": { + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ], + "canSend": [] + }, + "attributes": { + "identifyPeriod": 0, + "permittingJoin": false, + "serialNumber": "D44867FFFE140EDE", + "otaScheduleStart": "00:00", + "otaProgress": 0, + "startUpCurrentLevel": -1, + "otaScheduleEnd": "00:00", + "customName": "", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "manufacturer": "IKEA of Sweden", + "lightLevel": 100, + "productCode": "E2204", + "statusLight": true, + "otaState": "readyToCheck", + "childLock": false, + "isOn": false, + "hardwareVersion": "1", + "model": "TRETAKT Smart plug", + "startupOnOff": "startPrevious", + "firmwareVersion": "2.4.4" + }, + "id": "339a37ea-791d-4b5d-8a1d-21d068578421_1", + "type": "outlet", + "deviceSet": [] + }, + "specversion": "3.163.0", + "id": "382f5a61-0b3a-4c55-b601-d89c003ac27c", + "time": "2024-10-17T21:17:48.000Z", + "source": "urn:com:ikea:homesmart:iotc:zigbee", + "type": "deviceRemoved" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-removed/home-after.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-removed/home-after.json new file mode 100644 index 00000000000..038ccd33080 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-removed/home-after.json @@ -0,0 +1,1615 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "deviceType": "environmentSensor", + "remoteLinks": [], + "createdAt": "2024-10-17T08:00:25.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T08:07:42.000Z", + "capabilities": { + "canReceive": [ + "customName" + ], + "canSend": [] + }, + "attributes": { + "maxMeasuredPM25": 999, + "identifyPeriod": 0, + "currentTemperature": 20, + "permittingJoin": false, + "serialNumber": "28DBA7FFFEDFFBF6", + "minMeasuredPM25": 0, + "otaScheduleStart": "00:00", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "customName": "Cleany", + "currentPM25": 11, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "vocIndex": 100, + "manufacturer": "IKEA of Sweden", + "productCode": "E2112", + "otaState": "readyToCheck", + "hardwareVersion": "1", + "model": "VINDSTYRKA", + "firmwareVersion": "1.0.11", + "currentRH": 76 + }, + "id": "f80cac12-65a4-47b4-9f68-a0456a349a43_1", + "type": "sensor", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false + }, + { + "id": "5ffa4a1e-6fe2-4a24-b0af-b216937c4e8c_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:04:54.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:08:02.000Z", + "attributes": { + "customName": "Lampe Rechts", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "30FB10FFFEE44843", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 54, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-11T11:33:27.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T11:22:41.000Z", + "attributes": { + "customName": "Remote Control Loft", + "firmwareVersion": "2.4.16", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "Remote Control N2", + "productCode": "E2001", + "serialNumber": "94B216FFFE6F9BA6", + "batteryPercentage": 85, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "044b63e7-999d-4caa-8a76-fb8cfd32b381_1", + "type": "repeater", + "deviceType": "repeater", + "createdAt": "2024-10-10T10:10:37.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T10:12:28.000Z", + "attributes": { + "customName": "Reaper", + "model": "TRADFRI Signal Repeater", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "14B457FFFEF8DC61", + "productCode": "E1746", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "45165c60-0c1a-4b9f-ae09-c4a6220d1420_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:09:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:02:56.000Z", + "attributes": { + "customName": "Lampe Links", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "881A14FFFE479E82", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 51, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Esszimmer Hinten", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:20:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "attributes": { + "customName": "Loft Floor Lamp", + "model": "TRADFRI bulb E27 CWS opal 600lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "EC1BBDFFFEC17EA1", + "productCode": "LED1624G9E27EU", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "338bb721-35bb-4775-8cd0-ba70fc37ab10_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-04T17:21:49.446Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:09:15.285Z", + "attributes": { + "customName": "Loft", + "model": "SYMFONISK Bookshelf S21", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.3.3-2.0", + "serialNumber": "34-7E-5C-F5-45-88:F", + "productCode": "S21", + "identifyStarted": "2024-10-04T17:21:49.447Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-13T21:09:15.284Z", + "playbackAudio": { + "serviceType": "sonos", + "providerType": "Mixcloud", + "playItem": { + "id": "eyJhY2NvdW50VHlwZSI6InNvbm9zIiwiY29udGVudFR5cGUiOiJjb250YWluZXIiLCJjb250ZW50Ijp7ImNvbnRhaW5lciI6eyJhY2NvdW50SWQiOiJzbl8zIiwib2JqZWN0SWQiOiJjbG91ZGNhc3Q6MjE3OTQzNDcxNCIsInNlcnZpY2VJZCI6IjE4MSIsInR5cGUiOiJ0cmFjayJ9fX0=", + "title": "The Anjunadeep Edition 521 with Leaving Laurel & yehno", + "artist": "Anjunadeep", + "imageURL": "http://192.168.1.95:1400/getaa?s=1&u=x-sonos-http%3acloudcast%253a2179434714.unknown%3fsid%3d181%26flags%3d0%26sn%3d3", + "duration": 3617000 + } + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-13T21:09:15.284Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": true, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": true, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 16, + "isMuted": false, + "audioGroup": "1f386990-bb4f-4a00-befd-646b5ef52c47" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "03c716a9-900e-4bb0-8694-eaf837bfa3fb_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-06T09:06:12.272Z", + "isReachable": false, + "lastSeen": "2024-10-08T16:30:36.504Z", + "attributes": { + "customName": "Moritz", + "model": "SYMFONISK Table lamp S20", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.2.3-2.0", + "serialNumber": "78-28-CA-81-8E-F2:1", + "productCode": "S20", + "identifyStarted": "2024-10-06T09:06:12.272Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-06T09:06:12.272Z", + "playbackAudio": { + + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-06T09:06:12.032Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": false, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": false, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 15, + "isMuted": false, + "audioGroup": "71aa115c-fadd-4c43-aaf7-d537fa939638" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": true + }, + { + "id": "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-09T21:23:19.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:22:08.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Sensor Kitchen", + "model": "PARASOLL Door/Window Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "serialNumber": "D44867FFFEC2D1F0", + "productCode": "E2013", + "batteryPercentage": 84, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:00:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:02.000Z", + "customIcon": "lighting_cone_pendant", + "attributes": { + "customName": "Esszimmer Vorne", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFEC9113D", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 0, + "colorSaturation": 1, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-10T14:40:38.000Z", + "attributes": { + "customName": "Smart-Home-Steckdose 1", + "model": "INSPELNING Smart plug", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.4.45", + "hardwareVersion": "1", + "serialNumber": "ECF64CFFFEF28EC3", + "productCode": "E2206", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 1, + "startUpCurrentLevel": -1, + "currentVoltage": 236.8000030517578, + "currentAmps": 0.003000000026077032, + "currentActivePower": 0, + "totalEnergyConsumed": 0.01899999938905239, + "totalEnergyConsumedLastUpdated": "2024-10-10T14:40:05.000Z", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "childLock": false, + "statusLight": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T17:38:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T17:18:25.000Z", + "attributes": { + "customName": "", + "model": "", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.10", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE66D457", + "productCode": "L2112", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 95, + "startUpCurrentLevel": -1, + "colorHue": 29.9981689453125, + "colorSaturation": 0.6496062992125984, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_1", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "Motion Sensor Gäste WC", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "batteryPercentage": 85, + "isOn": false, + "motionDetectedDelay": 20, + "isDetected": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": true, + "onDuration": 300, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_3", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "illuminance": 1, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [] + }, + { + "id": "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:46:04.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:06:16.000Z", + "attributes": { + "customName": "Bathroom Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x90", + "productCode": "", + "serialNumber": "804B50FFFEF79502", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 49, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "073b40df-e6a0-486d-99a3-6134effe4c59_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:48:44.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:21:17.000Z", + "customIcon": "lighting_fan", + "attributes": { + "customName": "HWR Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x30", + "productCode": "", + "serialNumber": "BC33ACFFFE9761AA", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 73, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [ + "ee61c57f-8efa-44f4-ba8a-d108ae054138_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "ee61c57f-8efa-44f4-ba8a-d108ae054138_1", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:56:23.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:18:17.000Z", + "attributes": { + "customName": "HWR Motion", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + "productCode": "E1745", + "serialNumber": "BC33ACFFFE8739CC", + "batteryPercentage": 20, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "isDetected": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "sensorConfig": { + "scheduleOn": true, + "onDuration": 180, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "3267c0f3-069a-4894-b5d0-a30d26386af8_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:00:00.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T13:31:10.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Dining Room", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "D44867FFFEB36824", + "batteryPercentage": 82, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "bab217dc-5d46-42af-a3a4-6a53b9a953da_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:05:20.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:36:48.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Entry Door", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "F84477FFFEDC7871", + "batteryPercentage": 81, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_1", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "Bedroom Motion", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "batteryPercentage": 84, + "isOn": false, + "isDetected": false, + "motionDetectedDelay": 20, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": false, + "onDuration": 180 + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "illuminance": 1, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9e9cb6b1-7981-443a-bd1e-5301815a25bd_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-13T20:09:35.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T00:32:59.000Z", + "attributes": { + "customName": "HWR Remote", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + "productCode": "E1810", + "serialNumber": "BC33ACFFFE9B308C", + "batteryPercentage": 30, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + { + "id": "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58", + "info": { + "name": "Lightmode Christmas", + "icon": "scenes_tree" + }, + "type": "userScene", + "triggers": [ + { + "id": "987ec615-24d8-411c-9aa9-d03c447b246b", + "type": "app", + "triggeredAt": "2024-10-16T00:21:15.942Z", + "disabled": false + } + ], + "actions": [ + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 0, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T11:13:42.273Z", + "lastCompleted": "2024-10-16T00:21:15.977Z", + "lastTriggered": "2024-10-16T00:21:15.977Z", + "undoAllowedDuration": 30, + "lastUndo": "2024-10-15T19:36:59.273Z" + }, + { + "id": "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb", + "info": { + "name": "Lightmode off", + "icon": "scenes_disco_ball" + }, + "type": "userScene", + "triggers": [ + { + "id": "6fff3797-c798-4d0c-abe0-ce46cc389104", + "type": "app", + "triggeredAt": "2024-10-16T00:38:16.257Z", + "disabled": false + } + ], + "actions": [ + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 359.83392, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T23:14:12.399Z", + "lastCompleted": "2024-10-16T00:38:16.267Z", + "lastTriggered": "2024-10-16T00:38:16.267Z", + "undoAllowedDuration": 30 + } + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-removed/home-before.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-removed/home-before.json new file mode 100644 index 00000000000..590771a4cf9 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/device-removed/home-before.json @@ -0,0 +1,1659 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "deviceType": "outlet", + "remoteLinks": [], + "createdAt": "2024-10-17T21:17:45.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T21:17:48.000Z", + "capabilities": { + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ], + "canSend": [] + }, + "attributes": { + "identifyPeriod": 0, + "permittingJoin": false, + "serialNumber": "D44867FFFE140EDE", + "otaScheduleStart": "00:00", + "otaProgress": 0, + "startUpCurrentLevel": -1, + "otaScheduleEnd": "00:00", + "customName": "", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "manufacturer": "IKEA of Sweden", + "lightLevel": 100, + "productCode": "E2204", + "statusLight": true, + "otaState": "readyToCheck", + "childLock": false, + "isOn": false, + "hardwareVersion": "1", + "model": "TRETAKT Smart plug", + "startupOnOff": "startPrevious", + "firmwareVersion": "2.4.4" + }, + "id": "339a37ea-791d-4b5d-8a1d-21d068578421_1", + "type": "outlet", + "deviceSet": [] + }, + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "deviceType": "environmentSensor", + "remoteLinks": [], + "createdAt": "2024-10-17T08:00:25.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T08:07:42.000Z", + "capabilities": { + "canReceive": [ + "customName" + ], + "canSend": [] + }, + "attributes": { + "maxMeasuredPM25": 999, + "identifyPeriod": 0, + "currentTemperature": 20, + "permittingJoin": false, + "serialNumber": "28DBA7FFFEDFFBF6", + "minMeasuredPM25": 0, + "otaScheduleStart": "00:00", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "customName": "Cleany", + "currentPM25": 11, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "vocIndex": 100, + "manufacturer": "IKEA of Sweden", + "productCode": "E2112", + "otaState": "readyToCheck", + "hardwareVersion": "1", + "model": "VINDSTYRKA", + "firmwareVersion": "1.0.11", + "currentRH": 76 + }, + "id": "f80cac12-65a4-47b4-9f68-a0456a349a43_1", + "type": "sensor", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false + }, + { + "id": "5ffa4a1e-6fe2-4a24-b0af-b216937c4e8c_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:04:54.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:08:02.000Z", + "attributes": { + "customName": "Lampe Rechts", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "30FB10FFFEE44843", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 54, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-11T11:33:27.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T11:22:41.000Z", + "attributes": { + "customName": "Remote Control Loft", + "firmwareVersion": "2.4.16", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "Remote Control N2", + "productCode": "E2001", + "serialNumber": "94B216FFFE6F9BA6", + "batteryPercentage": 85, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "044b63e7-999d-4caa-8a76-fb8cfd32b381_1", + "type": "repeater", + "deviceType": "repeater", + "createdAt": "2024-10-10T10:10:37.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T10:12:28.000Z", + "attributes": { + "customName": "Reaper", + "model": "TRADFRI Signal Repeater", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "14B457FFFEF8DC61", + "productCode": "E1746", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "45165c60-0c1a-4b9f-ae09-c4a6220d1420_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:09:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:02:56.000Z", + "attributes": { + "customName": "Lampe Links", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "881A14FFFE479E82", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 51, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Esszimmer Hinten", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:20:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "attributes": { + "customName": "Loft Floor Lamp", + "model": "TRADFRI bulb E27 CWS opal 600lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "EC1BBDFFFEC17EA1", + "productCode": "LED1624G9E27EU", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "338bb721-35bb-4775-8cd0-ba70fc37ab10_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-04T17:21:49.446Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:09:15.285Z", + "attributes": { + "customName": "Loft", + "model": "SYMFONISK Bookshelf S21", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.3.3-2.0", + "serialNumber": "34-7E-5C-F5-45-88:F", + "productCode": "S21", + "identifyStarted": "2024-10-04T17:21:49.447Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-13T21:09:15.284Z", + "playbackAudio": { + "serviceType": "sonos", + "providerType": "Mixcloud", + "playItem": { + "id": "eyJhY2NvdW50VHlwZSI6InNvbm9zIiwiY29udGVudFR5cGUiOiJjb250YWluZXIiLCJjb250ZW50Ijp7ImNvbnRhaW5lciI6eyJhY2NvdW50SWQiOiJzbl8zIiwib2JqZWN0SWQiOiJjbG91ZGNhc3Q6MjE3OTQzNDcxNCIsInNlcnZpY2VJZCI6IjE4MSIsInR5cGUiOiJ0cmFjayJ9fX0=", + "title": "The Anjunadeep Edition 521 with Leaving Laurel & yehno", + "artist": "Anjunadeep", + "imageURL": "http://192.168.1.95:1400/getaa?s=1&u=x-sonos-http%3acloudcast%253a2179434714.unknown%3fsid%3d181%26flags%3d0%26sn%3d3", + "duration": 3617000 + } + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-13T21:09:15.284Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": true, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": true, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 16, + "isMuted": false, + "audioGroup": "1f386990-bb4f-4a00-befd-646b5ef52c47" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "03c716a9-900e-4bb0-8694-eaf837bfa3fb_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-06T09:06:12.272Z", + "isReachable": false, + "lastSeen": "2024-10-08T16:30:36.504Z", + "attributes": { + "customName": "Moritz", + "model": "SYMFONISK Table lamp S20", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.2.3-2.0", + "serialNumber": "78-28-CA-81-8E-F2:1", + "productCode": "S20", + "identifyStarted": "2024-10-06T09:06:12.272Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-06T09:06:12.272Z", + "playbackAudio": { + + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-06T09:06:12.032Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": false, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": false, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 15, + "isMuted": false, + "audioGroup": "71aa115c-fadd-4c43-aaf7-d537fa939638" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": true + }, + { + "id": "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-09T21:23:19.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:22:08.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Sensor Kitchen", + "model": "PARASOLL Door/Window Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "serialNumber": "D44867FFFEC2D1F0", + "productCode": "E2013", + "batteryPercentage": 84, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:00:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:02.000Z", + "customIcon": "lighting_cone_pendant", + "attributes": { + "customName": "Esszimmer Vorne", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFEC9113D", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 0, + "colorSaturation": 1, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-10T14:40:38.000Z", + "attributes": { + "customName": "Smart-Home-Steckdose 1", + "model": "INSPELNING Smart plug", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.4.45", + "hardwareVersion": "1", + "serialNumber": "ECF64CFFFEF28EC3", + "productCode": "E2206", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 1, + "startUpCurrentLevel": -1, + "currentVoltage": 236.8000030517578, + "currentAmps": 0.003000000026077032, + "currentActivePower": 0, + "totalEnergyConsumed": 0.01899999938905239, + "totalEnergyConsumedLastUpdated": "2024-10-10T14:40:05.000Z", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "childLock": false, + "statusLight": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T17:38:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T17:18:25.000Z", + "attributes": { + "customName": "", + "model": "", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.10", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE66D457", + "productCode": "L2112", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 95, + "startUpCurrentLevel": -1, + "colorHue": 29.9981689453125, + "colorSaturation": 0.6496062992125984, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_1", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "Motion Sensor Gäste WC", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "batteryPercentage": 85, + "isOn": false, + "motionDetectedDelay": 20, + "isDetected": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": true, + "onDuration": 300, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_3", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "illuminance": 1, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [] + }, + { + "id": "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:46:04.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:06:16.000Z", + "attributes": { + "customName": "Bathroom Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x90", + "productCode": "", + "serialNumber": "804B50FFFEF79502", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 49, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "073b40df-e6a0-486d-99a3-6134effe4c59_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:48:44.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:21:17.000Z", + "customIcon": "lighting_fan", + "attributes": { + "customName": "HWR Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x30", + "productCode": "", + "serialNumber": "BC33ACFFFE9761AA", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 73, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [ + "ee61c57f-8efa-44f4-ba8a-d108ae054138_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "ee61c57f-8efa-44f4-ba8a-d108ae054138_1", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:56:23.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:18:17.000Z", + "attributes": { + "customName": "HWR Motion", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + "productCode": "E1745", + "serialNumber": "BC33ACFFFE8739CC", + "batteryPercentage": 20, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "isDetected": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "sensorConfig": { + "scheduleOn": true, + "onDuration": 180, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "3267c0f3-069a-4894-b5d0-a30d26386af8_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:00:00.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T13:31:10.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Dining Room", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "D44867FFFEB36824", + "batteryPercentage": 82, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "bab217dc-5d46-42af-a3a4-6a53b9a953da_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:05:20.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:36:48.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Entry Door", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "F84477FFFEDC7871", + "batteryPercentage": 81, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_1", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "Bedroom Motion", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "batteryPercentage": 84, + "isOn": false, + "isDetected": false, + "motionDetectedDelay": 20, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": false, + "onDuration": 180 + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "illuminance": 1, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9e9cb6b1-7981-443a-bd1e-5301815a25bd_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-13T20:09:35.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T00:32:59.000Z", + "attributes": { + "customName": "HWR Remote", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + "productCode": "E1810", + "serialNumber": "BC33ACFFFE9B308C", + "batteryPercentage": 30, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + { + "id": "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58", + "info": { + "name": "Lightmode Christmas", + "icon": "scenes_tree" + }, + "type": "userScene", + "triggers": [ + { + "id": "987ec615-24d8-411c-9aa9-d03c447b246b", + "type": "app", + "triggeredAt": "2024-10-16T00:21:15.942Z", + "disabled": false + } + ], + "actions": [ + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 0, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T11:13:42.273Z", + "lastCompleted": "2024-10-16T00:21:15.977Z", + "lastTriggered": "2024-10-16T00:21:15.977Z", + "undoAllowedDuration": 30, + "lastUndo": "2024-10-15T19:36:59.273Z" + }, + { + "id": "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb", + "info": { + "name": "Lightmode off", + "icon": "scenes_disco_ball" + }, + "type": "userScene", + "triggers": [ + { + "id": "6fff3797-c798-4d0c-abe0-ce46cc389104", + "type": "app", + "triggeredAt": "2024-10-16T00:38:16.257Z", + "disabled": false + } + ], + "actions": [ + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 359.83392, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T23:14:12.399Z", + "lastCompleted": "2024-10-16T00:38:16.267Z", + "lastTriggered": "2024-10-16T00:38:16.267Z", + "undoAllowedDuration": 30 + } + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-created/home-after.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-created/home-after.json new file mode 100644 index 00000000000..038ccd33080 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-created/home-after.json @@ -0,0 +1,1615 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "deviceType": "environmentSensor", + "remoteLinks": [], + "createdAt": "2024-10-17T08:00:25.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T08:07:42.000Z", + "capabilities": { + "canReceive": [ + "customName" + ], + "canSend": [] + }, + "attributes": { + "maxMeasuredPM25": 999, + "identifyPeriod": 0, + "currentTemperature": 20, + "permittingJoin": false, + "serialNumber": "28DBA7FFFEDFFBF6", + "minMeasuredPM25": 0, + "otaScheduleStart": "00:00", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "customName": "Cleany", + "currentPM25": 11, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "vocIndex": 100, + "manufacturer": "IKEA of Sweden", + "productCode": "E2112", + "otaState": "readyToCheck", + "hardwareVersion": "1", + "model": "VINDSTYRKA", + "firmwareVersion": "1.0.11", + "currentRH": 76 + }, + "id": "f80cac12-65a4-47b4-9f68-a0456a349a43_1", + "type": "sensor", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false + }, + { + "id": "5ffa4a1e-6fe2-4a24-b0af-b216937c4e8c_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:04:54.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:08:02.000Z", + "attributes": { + "customName": "Lampe Rechts", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "30FB10FFFEE44843", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 54, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-11T11:33:27.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T11:22:41.000Z", + "attributes": { + "customName": "Remote Control Loft", + "firmwareVersion": "2.4.16", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "Remote Control N2", + "productCode": "E2001", + "serialNumber": "94B216FFFE6F9BA6", + "batteryPercentage": 85, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "044b63e7-999d-4caa-8a76-fb8cfd32b381_1", + "type": "repeater", + "deviceType": "repeater", + "createdAt": "2024-10-10T10:10:37.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T10:12:28.000Z", + "attributes": { + "customName": "Reaper", + "model": "TRADFRI Signal Repeater", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "14B457FFFEF8DC61", + "productCode": "E1746", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "45165c60-0c1a-4b9f-ae09-c4a6220d1420_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:09:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:02:56.000Z", + "attributes": { + "customName": "Lampe Links", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "881A14FFFE479E82", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 51, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Esszimmer Hinten", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:20:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "attributes": { + "customName": "Loft Floor Lamp", + "model": "TRADFRI bulb E27 CWS opal 600lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "EC1BBDFFFEC17EA1", + "productCode": "LED1624G9E27EU", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "338bb721-35bb-4775-8cd0-ba70fc37ab10_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-04T17:21:49.446Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:09:15.285Z", + "attributes": { + "customName": "Loft", + "model": "SYMFONISK Bookshelf S21", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.3.3-2.0", + "serialNumber": "34-7E-5C-F5-45-88:F", + "productCode": "S21", + "identifyStarted": "2024-10-04T17:21:49.447Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-13T21:09:15.284Z", + "playbackAudio": { + "serviceType": "sonos", + "providerType": "Mixcloud", + "playItem": { + "id": "eyJhY2NvdW50VHlwZSI6InNvbm9zIiwiY29udGVudFR5cGUiOiJjb250YWluZXIiLCJjb250ZW50Ijp7ImNvbnRhaW5lciI6eyJhY2NvdW50SWQiOiJzbl8zIiwib2JqZWN0SWQiOiJjbG91ZGNhc3Q6MjE3OTQzNDcxNCIsInNlcnZpY2VJZCI6IjE4MSIsInR5cGUiOiJ0cmFjayJ9fX0=", + "title": "The Anjunadeep Edition 521 with Leaving Laurel & yehno", + "artist": "Anjunadeep", + "imageURL": "http://192.168.1.95:1400/getaa?s=1&u=x-sonos-http%3acloudcast%253a2179434714.unknown%3fsid%3d181%26flags%3d0%26sn%3d3", + "duration": 3617000 + } + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-13T21:09:15.284Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": true, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": true, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 16, + "isMuted": false, + "audioGroup": "1f386990-bb4f-4a00-befd-646b5ef52c47" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "03c716a9-900e-4bb0-8694-eaf837bfa3fb_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-06T09:06:12.272Z", + "isReachable": false, + "lastSeen": "2024-10-08T16:30:36.504Z", + "attributes": { + "customName": "Moritz", + "model": "SYMFONISK Table lamp S20", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.2.3-2.0", + "serialNumber": "78-28-CA-81-8E-F2:1", + "productCode": "S20", + "identifyStarted": "2024-10-06T09:06:12.272Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-06T09:06:12.272Z", + "playbackAudio": { + + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-06T09:06:12.032Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": false, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": false, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 15, + "isMuted": false, + "audioGroup": "71aa115c-fadd-4c43-aaf7-d537fa939638" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": true + }, + { + "id": "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-09T21:23:19.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:22:08.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Sensor Kitchen", + "model": "PARASOLL Door/Window Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "serialNumber": "D44867FFFEC2D1F0", + "productCode": "E2013", + "batteryPercentage": 84, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:00:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:02.000Z", + "customIcon": "lighting_cone_pendant", + "attributes": { + "customName": "Esszimmer Vorne", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFEC9113D", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 0, + "colorSaturation": 1, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-10T14:40:38.000Z", + "attributes": { + "customName": "Smart-Home-Steckdose 1", + "model": "INSPELNING Smart plug", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.4.45", + "hardwareVersion": "1", + "serialNumber": "ECF64CFFFEF28EC3", + "productCode": "E2206", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 1, + "startUpCurrentLevel": -1, + "currentVoltage": 236.8000030517578, + "currentAmps": 0.003000000026077032, + "currentActivePower": 0, + "totalEnergyConsumed": 0.01899999938905239, + "totalEnergyConsumedLastUpdated": "2024-10-10T14:40:05.000Z", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "childLock": false, + "statusLight": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T17:38:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T17:18:25.000Z", + "attributes": { + "customName": "", + "model": "", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.10", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE66D457", + "productCode": "L2112", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 95, + "startUpCurrentLevel": -1, + "colorHue": 29.9981689453125, + "colorSaturation": 0.6496062992125984, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_1", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "Motion Sensor Gäste WC", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "batteryPercentage": 85, + "isOn": false, + "motionDetectedDelay": 20, + "isDetected": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": true, + "onDuration": 300, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_3", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "illuminance": 1, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [] + }, + { + "id": "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:46:04.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:06:16.000Z", + "attributes": { + "customName": "Bathroom Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x90", + "productCode": "", + "serialNumber": "804B50FFFEF79502", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 49, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "073b40df-e6a0-486d-99a3-6134effe4c59_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:48:44.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:21:17.000Z", + "customIcon": "lighting_fan", + "attributes": { + "customName": "HWR Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x30", + "productCode": "", + "serialNumber": "BC33ACFFFE9761AA", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 73, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [ + "ee61c57f-8efa-44f4-ba8a-d108ae054138_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "ee61c57f-8efa-44f4-ba8a-d108ae054138_1", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:56:23.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:18:17.000Z", + "attributes": { + "customName": "HWR Motion", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + "productCode": "E1745", + "serialNumber": "BC33ACFFFE8739CC", + "batteryPercentage": 20, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "isDetected": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "sensorConfig": { + "scheduleOn": true, + "onDuration": 180, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "3267c0f3-069a-4894-b5d0-a30d26386af8_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:00:00.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T13:31:10.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Dining Room", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "D44867FFFEB36824", + "batteryPercentage": 82, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "bab217dc-5d46-42af-a3a4-6a53b9a953da_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:05:20.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:36:48.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Entry Door", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "F84477FFFEDC7871", + "batteryPercentage": 81, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_1", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "Bedroom Motion", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "batteryPercentage": 84, + "isOn": false, + "isDetected": false, + "motionDetectedDelay": 20, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": false, + "onDuration": 180 + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "illuminance": 1, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9e9cb6b1-7981-443a-bd1e-5301815a25bd_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-13T20:09:35.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T00:32:59.000Z", + "attributes": { + "customName": "HWR Remote", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + "productCode": "E1810", + "serialNumber": "BC33ACFFFE9B308C", + "batteryPercentage": 30, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + { + "id": "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58", + "info": { + "name": "Lightmode Christmas", + "icon": "scenes_tree" + }, + "type": "userScene", + "triggers": [ + { + "id": "987ec615-24d8-411c-9aa9-d03c447b246b", + "type": "app", + "triggeredAt": "2024-10-16T00:21:15.942Z", + "disabled": false + } + ], + "actions": [ + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 0, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T11:13:42.273Z", + "lastCompleted": "2024-10-16T00:21:15.977Z", + "lastTriggered": "2024-10-16T00:21:15.977Z", + "undoAllowedDuration": 30, + "lastUndo": "2024-10-15T19:36:59.273Z" + }, + { + "id": "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb", + "info": { + "name": "Lightmode off", + "icon": "scenes_disco_ball" + }, + "type": "userScene", + "triggers": [ + { + "id": "6fff3797-c798-4d0c-abe0-ce46cc389104", + "type": "app", + "triggeredAt": "2024-10-16T00:38:16.257Z", + "disabled": false + } + ], + "actions": [ + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 359.83392, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T23:14:12.399Z", + "lastCompleted": "2024-10-16T00:38:16.267Z", + "lastTriggered": "2024-10-16T00:38:16.267Z", + "undoAllowedDuration": 30 + } + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-created/home-before.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-created/home-before.json new file mode 100644 index 00000000000..0f9ac019715 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-created/home-before.json @@ -0,0 +1,1543 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "deviceType": "environmentSensor", + "remoteLinks": [], + "createdAt": "2024-10-17T08:00:25.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T08:07:42.000Z", + "capabilities": { + "canReceive": [ + "customName" + ], + "canSend": [] + }, + "attributes": { + "maxMeasuredPM25": 999, + "identifyPeriod": 0, + "currentTemperature": 20, + "permittingJoin": false, + "serialNumber": "28DBA7FFFEDFFBF6", + "minMeasuredPM25": 0, + "otaScheduleStart": "00:00", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "customName": "Cleany", + "currentPM25": 11, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "vocIndex": 100, + "manufacturer": "IKEA of Sweden", + "productCode": "E2112", + "otaState": "readyToCheck", + "hardwareVersion": "1", + "model": "VINDSTYRKA", + "firmwareVersion": "1.0.11", + "currentRH": 76 + }, + "id": "f80cac12-65a4-47b4-9f68-a0456a349a43_1", + "type": "sensor", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false + }, + { + "id": "5ffa4a1e-6fe2-4a24-b0af-b216937c4e8c_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:04:54.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:08:02.000Z", + "attributes": { + "customName": "Lampe Rechts", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "30FB10FFFEE44843", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 54, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-11T11:33:27.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T11:22:41.000Z", + "attributes": { + "customName": "Remote Control Loft", + "firmwareVersion": "2.4.16", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "Remote Control N2", + "productCode": "E2001", + "serialNumber": "94B216FFFE6F9BA6", + "batteryPercentage": 85, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "044b63e7-999d-4caa-8a76-fb8cfd32b381_1", + "type": "repeater", + "deviceType": "repeater", + "createdAt": "2024-10-10T10:10:37.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T10:12:28.000Z", + "attributes": { + "customName": "Reaper", + "model": "TRADFRI Signal Repeater", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "14B457FFFEF8DC61", + "productCode": "E1746", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "45165c60-0c1a-4b9f-ae09-c4a6220d1420_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:09:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:02:56.000Z", + "attributes": { + "customName": "Lampe Links", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "881A14FFFE479E82", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 51, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Esszimmer Hinten", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:20:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "attributes": { + "customName": "Loft Floor Lamp", + "model": "TRADFRI bulb E27 CWS opal 600lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "EC1BBDFFFEC17EA1", + "productCode": "LED1624G9E27EU", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "338bb721-35bb-4775-8cd0-ba70fc37ab10_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-04T17:21:49.446Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:09:15.285Z", + "attributes": { + "customName": "Loft", + "model": "SYMFONISK Bookshelf S21", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.3.3-2.0", + "serialNumber": "34-7E-5C-F5-45-88:F", + "productCode": "S21", + "identifyStarted": "2024-10-04T17:21:49.447Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-13T21:09:15.284Z", + "playbackAudio": { + "serviceType": "sonos", + "providerType": "Mixcloud", + "playItem": { + "id": "eyJhY2NvdW50VHlwZSI6InNvbm9zIiwiY29udGVudFR5cGUiOiJjb250YWluZXIiLCJjb250ZW50Ijp7ImNvbnRhaW5lciI6eyJhY2NvdW50SWQiOiJzbl8zIiwib2JqZWN0SWQiOiJjbG91ZGNhc3Q6MjE3OTQzNDcxNCIsInNlcnZpY2VJZCI6IjE4MSIsInR5cGUiOiJ0cmFjayJ9fX0=", + "title": "The Anjunadeep Edition 521 with Leaving Laurel & yehno", + "artist": "Anjunadeep", + "imageURL": "http://192.168.1.95:1400/getaa?s=1&u=x-sonos-http%3acloudcast%253a2179434714.unknown%3fsid%3d181%26flags%3d0%26sn%3d3", + "duration": 3617000 + } + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-13T21:09:15.284Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": true, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": true, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 16, + "isMuted": false, + "audioGroup": "1f386990-bb4f-4a00-befd-646b5ef52c47" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "03c716a9-900e-4bb0-8694-eaf837bfa3fb_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-06T09:06:12.272Z", + "isReachable": false, + "lastSeen": "2024-10-08T16:30:36.504Z", + "attributes": { + "customName": "Moritz", + "model": "SYMFONISK Table lamp S20", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.2.3-2.0", + "serialNumber": "78-28-CA-81-8E-F2:1", + "productCode": "S20", + "identifyStarted": "2024-10-06T09:06:12.272Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-06T09:06:12.272Z", + "playbackAudio": { + + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-06T09:06:12.032Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": false, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": false, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 15, + "isMuted": false, + "audioGroup": "71aa115c-fadd-4c43-aaf7-d537fa939638" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": true + }, + { + "id": "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-09T21:23:19.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:22:08.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Sensor Kitchen", + "model": "PARASOLL Door/Window Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "serialNumber": "D44867FFFEC2D1F0", + "productCode": "E2013", + "batteryPercentage": 84, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:00:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:02.000Z", + "customIcon": "lighting_cone_pendant", + "attributes": { + "customName": "Esszimmer Vorne", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFEC9113D", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 0, + "colorSaturation": 1, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-10T14:40:38.000Z", + "attributes": { + "customName": "Smart-Home-Steckdose 1", + "model": "INSPELNING Smart plug", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.4.45", + "hardwareVersion": "1", + "serialNumber": "ECF64CFFFEF28EC3", + "productCode": "E2206", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 1, + "startUpCurrentLevel": -1, + "currentVoltage": 236.8000030517578, + "currentAmps": 0.003000000026077032, + "currentActivePower": 0, + "totalEnergyConsumed": 0.01899999938905239, + "totalEnergyConsumedLastUpdated": "2024-10-10T14:40:05.000Z", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "childLock": false, + "statusLight": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T17:38:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T17:18:25.000Z", + "attributes": { + "customName": "", + "model": "", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.10", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE66D457", + "productCode": "L2112", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 95, + "startUpCurrentLevel": -1, + "colorHue": 29.9981689453125, + "colorSaturation": 0.6496062992125984, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_1", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "Motion Sensor Gäste WC", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "batteryPercentage": 85, + "isOn": false, + "motionDetectedDelay": 20, + "isDetected": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": true, + "onDuration": 300, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_3", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "illuminance": 1, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [] + }, + { + "id": "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:46:04.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:06:16.000Z", + "attributes": { + "customName": "Bathroom Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x90", + "productCode": "", + "serialNumber": "804B50FFFEF79502", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 49, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "073b40df-e6a0-486d-99a3-6134effe4c59_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:48:44.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:21:17.000Z", + "customIcon": "lighting_fan", + "attributes": { + "customName": "HWR Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x30", + "productCode": "", + "serialNumber": "BC33ACFFFE9761AA", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 73, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [ + "ee61c57f-8efa-44f4-ba8a-d108ae054138_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "ee61c57f-8efa-44f4-ba8a-d108ae054138_1", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:56:23.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:18:17.000Z", + "attributes": { + "customName": "HWR Motion", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + "productCode": "E1745", + "serialNumber": "BC33ACFFFE8739CC", + "batteryPercentage": 20, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "isDetected": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "sensorConfig": { + "scheduleOn": true, + "onDuration": 180, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "3267c0f3-069a-4894-b5d0-a30d26386af8_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:00:00.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T13:31:10.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Dining Room", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "D44867FFFEB36824", + "batteryPercentage": 82, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "bab217dc-5d46-42af-a3a4-6a53b9a953da_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:05:20.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:36:48.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Entry Door", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "F84477FFFEDC7871", + "batteryPercentage": 81, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_1", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "Bedroom Motion", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "batteryPercentage": 84, + "isOn": false, + "isDetected": false, + "motionDetectedDelay": 20, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": false, + "onDuration": 180 + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "illuminance": 1, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9e9cb6b1-7981-443a-bd1e-5301815a25bd_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-13T20:09:35.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T00:32:59.000Z", + "attributes": { + "customName": "HWR Remote", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + "productCode": "E1810", + "serialNumber": "BC33ACFFFE9B308C", + "batteryPercentage": 30, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + { + "id": "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb", + "info": { + "name": "Lightmode off", + "icon": "scenes_disco_ball" + }, + "type": "userScene", + "triggers": [ + { + "id": "6fff3797-c798-4d0c-abe0-ce46cc389104", + "type": "app", + "triggeredAt": "2024-10-16T00:38:16.257Z", + "disabled": false + } + ], + "actions": [ + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 359.83392, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T23:14:12.399Z", + "lastCompleted": "2024-10-16T00:38:16.267Z", + "lastTriggered": "2024-10-16T00:38:16.267Z", + "undoAllowedDuration": 30 + } + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-created/scene-created.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-created/scene-created.json new file mode 100644 index 00000000000..22c32f76469 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-created/scene-created.json @@ -0,0 +1,79 @@ +{ + "data": { + "id": "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58", + "info": { + "name": "Lightmode Christmas", + "icon": "scenes_tree" + }, + "type": "userScene", + "triggers": [ + { + "id": "987ec615-24d8-411c-9aa9-d03c447b246b", + "type": "app", + "triggeredAt": "2024-10-16T00:21:15.942Z", + "disabled": false + } + ], + "actions": [ + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 0, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T11:13:42.273Z", + "lastCompleted": "2024-10-16T00:21:15.977Z", + "lastTriggered": "2024-10-16T00:21:15.977Z", + "undoAllowedDuration": 30, + "lastUndo": "2024-10-15T19:36:59.273Z" + }, + "specversion": "3.163.0", + "id": "382f5a61-0b3a-4c55-b601-d89c003ac27c", + "time": "2024-10-17T21:17:48.000Z", + "source": "urn:com:ikea:homesmart:iotc:zigbee", + "type": "sceneCreated" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-deleted/home-after.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-deleted/home-after.json new file mode 100644 index 00000000000..0f9ac019715 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-deleted/home-after.json @@ -0,0 +1,1543 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "deviceType": "environmentSensor", + "remoteLinks": [], + "createdAt": "2024-10-17T08:00:25.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T08:07:42.000Z", + "capabilities": { + "canReceive": [ + "customName" + ], + "canSend": [] + }, + "attributes": { + "maxMeasuredPM25": 999, + "identifyPeriod": 0, + "currentTemperature": 20, + "permittingJoin": false, + "serialNumber": "28DBA7FFFEDFFBF6", + "minMeasuredPM25": 0, + "otaScheduleStart": "00:00", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "customName": "Cleany", + "currentPM25": 11, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "vocIndex": 100, + "manufacturer": "IKEA of Sweden", + "productCode": "E2112", + "otaState": "readyToCheck", + "hardwareVersion": "1", + "model": "VINDSTYRKA", + "firmwareVersion": "1.0.11", + "currentRH": 76 + }, + "id": "f80cac12-65a4-47b4-9f68-a0456a349a43_1", + "type": "sensor", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false + }, + { + "id": "5ffa4a1e-6fe2-4a24-b0af-b216937c4e8c_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:04:54.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:08:02.000Z", + "attributes": { + "customName": "Lampe Rechts", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "30FB10FFFEE44843", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 54, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-11T11:33:27.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T11:22:41.000Z", + "attributes": { + "customName": "Remote Control Loft", + "firmwareVersion": "2.4.16", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "Remote Control N2", + "productCode": "E2001", + "serialNumber": "94B216FFFE6F9BA6", + "batteryPercentage": 85, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "044b63e7-999d-4caa-8a76-fb8cfd32b381_1", + "type": "repeater", + "deviceType": "repeater", + "createdAt": "2024-10-10T10:10:37.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T10:12:28.000Z", + "attributes": { + "customName": "Reaper", + "model": "TRADFRI Signal Repeater", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "14B457FFFEF8DC61", + "productCode": "E1746", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "45165c60-0c1a-4b9f-ae09-c4a6220d1420_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:09:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:02:56.000Z", + "attributes": { + "customName": "Lampe Links", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "881A14FFFE479E82", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 51, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Esszimmer Hinten", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:20:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "attributes": { + "customName": "Loft Floor Lamp", + "model": "TRADFRI bulb E27 CWS opal 600lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "EC1BBDFFFEC17EA1", + "productCode": "LED1624G9E27EU", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "338bb721-35bb-4775-8cd0-ba70fc37ab10_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-04T17:21:49.446Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:09:15.285Z", + "attributes": { + "customName": "Loft", + "model": "SYMFONISK Bookshelf S21", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.3.3-2.0", + "serialNumber": "34-7E-5C-F5-45-88:F", + "productCode": "S21", + "identifyStarted": "2024-10-04T17:21:49.447Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-13T21:09:15.284Z", + "playbackAudio": { + "serviceType": "sonos", + "providerType": "Mixcloud", + "playItem": { + "id": "eyJhY2NvdW50VHlwZSI6InNvbm9zIiwiY29udGVudFR5cGUiOiJjb250YWluZXIiLCJjb250ZW50Ijp7ImNvbnRhaW5lciI6eyJhY2NvdW50SWQiOiJzbl8zIiwib2JqZWN0SWQiOiJjbG91ZGNhc3Q6MjE3OTQzNDcxNCIsInNlcnZpY2VJZCI6IjE4MSIsInR5cGUiOiJ0cmFjayJ9fX0=", + "title": "The Anjunadeep Edition 521 with Leaving Laurel & yehno", + "artist": "Anjunadeep", + "imageURL": "http://192.168.1.95:1400/getaa?s=1&u=x-sonos-http%3acloudcast%253a2179434714.unknown%3fsid%3d181%26flags%3d0%26sn%3d3", + "duration": 3617000 + } + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-13T21:09:15.284Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": true, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": true, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 16, + "isMuted": false, + "audioGroup": "1f386990-bb4f-4a00-befd-646b5ef52c47" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "03c716a9-900e-4bb0-8694-eaf837bfa3fb_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-06T09:06:12.272Z", + "isReachable": false, + "lastSeen": "2024-10-08T16:30:36.504Z", + "attributes": { + "customName": "Moritz", + "model": "SYMFONISK Table lamp S20", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.2.3-2.0", + "serialNumber": "78-28-CA-81-8E-F2:1", + "productCode": "S20", + "identifyStarted": "2024-10-06T09:06:12.272Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-06T09:06:12.272Z", + "playbackAudio": { + + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-06T09:06:12.032Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": false, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": false, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 15, + "isMuted": false, + "audioGroup": "71aa115c-fadd-4c43-aaf7-d537fa939638" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": true + }, + { + "id": "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-09T21:23:19.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:22:08.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Sensor Kitchen", + "model": "PARASOLL Door/Window Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "serialNumber": "D44867FFFEC2D1F0", + "productCode": "E2013", + "batteryPercentage": 84, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:00:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:02.000Z", + "customIcon": "lighting_cone_pendant", + "attributes": { + "customName": "Esszimmer Vorne", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFEC9113D", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 0, + "colorSaturation": 1, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-10T14:40:38.000Z", + "attributes": { + "customName": "Smart-Home-Steckdose 1", + "model": "INSPELNING Smart plug", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.4.45", + "hardwareVersion": "1", + "serialNumber": "ECF64CFFFEF28EC3", + "productCode": "E2206", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 1, + "startUpCurrentLevel": -1, + "currentVoltage": 236.8000030517578, + "currentAmps": 0.003000000026077032, + "currentActivePower": 0, + "totalEnergyConsumed": 0.01899999938905239, + "totalEnergyConsumedLastUpdated": "2024-10-10T14:40:05.000Z", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "childLock": false, + "statusLight": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T17:38:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T17:18:25.000Z", + "attributes": { + "customName": "", + "model": "", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.10", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE66D457", + "productCode": "L2112", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 95, + "startUpCurrentLevel": -1, + "colorHue": 29.9981689453125, + "colorSaturation": 0.6496062992125984, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_1", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "Motion Sensor Gäste WC", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "batteryPercentage": 85, + "isOn": false, + "motionDetectedDelay": 20, + "isDetected": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": true, + "onDuration": 300, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_3", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "illuminance": 1, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [] + }, + { + "id": "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:46:04.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:06:16.000Z", + "attributes": { + "customName": "Bathroom Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x90", + "productCode": "", + "serialNumber": "804B50FFFEF79502", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 49, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "073b40df-e6a0-486d-99a3-6134effe4c59_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:48:44.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:21:17.000Z", + "customIcon": "lighting_fan", + "attributes": { + "customName": "HWR Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x30", + "productCode": "", + "serialNumber": "BC33ACFFFE9761AA", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 73, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [ + "ee61c57f-8efa-44f4-ba8a-d108ae054138_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "ee61c57f-8efa-44f4-ba8a-d108ae054138_1", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:56:23.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:18:17.000Z", + "attributes": { + "customName": "HWR Motion", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + "productCode": "E1745", + "serialNumber": "BC33ACFFFE8739CC", + "batteryPercentage": 20, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "isDetected": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "sensorConfig": { + "scheduleOn": true, + "onDuration": 180, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "3267c0f3-069a-4894-b5d0-a30d26386af8_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:00:00.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T13:31:10.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Dining Room", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "D44867FFFEB36824", + "batteryPercentage": 82, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "bab217dc-5d46-42af-a3a4-6a53b9a953da_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:05:20.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:36:48.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Entry Door", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "F84477FFFEDC7871", + "batteryPercentage": 81, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_1", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "Bedroom Motion", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "batteryPercentage": 84, + "isOn": false, + "isDetected": false, + "motionDetectedDelay": 20, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": false, + "onDuration": 180 + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "illuminance": 1, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9e9cb6b1-7981-443a-bd1e-5301815a25bd_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-13T20:09:35.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T00:32:59.000Z", + "attributes": { + "customName": "HWR Remote", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + "productCode": "E1810", + "serialNumber": "BC33ACFFFE9B308C", + "batteryPercentage": 30, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + { + "id": "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb", + "info": { + "name": "Lightmode off", + "icon": "scenes_disco_ball" + }, + "type": "userScene", + "triggers": [ + { + "id": "6fff3797-c798-4d0c-abe0-ce46cc389104", + "type": "app", + "triggeredAt": "2024-10-16T00:38:16.257Z", + "disabled": false + } + ], + "actions": [ + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 359.83392, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T23:14:12.399Z", + "lastCompleted": "2024-10-16T00:38:16.267Z", + "lastTriggered": "2024-10-16T00:38:16.267Z", + "undoAllowedDuration": 30 + } + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-deleted/home-before.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-deleted/home-before.json new file mode 100644 index 00000000000..038ccd33080 --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-deleted/home-before.json @@ -0,0 +1,1615 @@ +{ + "user": { + "uid": "e86c72ef-93f8-4121-bd21-b634ba5d4f51", + "name": "openHAB", + "audience": "homesmart.local", + "email": "", + "createdTimestamp": "2024-10-08T14:14:33.000Z", + "verifiedUid": "", + "role": "home_member", + "remoteUser": 0 + }, + "hub": { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "environment": "prod" + }, + "devices": [ + { + "id": "9af826ad-a8ad-40bf-8aed-125300bccd20_1", + "type": "sensor", + "deviceType": "waterSensor", + "createdAt": "2024-10-17T12:38:02.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T14:22:17.000Z", + "attributes": { + "customName": "Washy", + "firmwareVersion": "1.0.7", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "BADRING Water Leakage Sensor", + "productCode": "E2202", + "serialNumber": "D44867FFFE147386", + "batteryPercentage": 55, + "waterLeakDetected": false, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "deviceType": "environmentSensor", + "remoteLinks": [], + "createdAt": "2024-10-17T08:00:25.000Z", + "isReachable": true, + "lastSeen": "2024-10-17T08:07:42.000Z", + "capabilities": { + "canReceive": [ + "customName" + ], + "canSend": [] + }, + "attributes": { + "maxMeasuredPM25": 999, + "identifyPeriod": 0, + "currentTemperature": 20, + "permittingJoin": false, + "serialNumber": "28DBA7FFFEDFFBF6", + "minMeasuredPM25": 0, + "otaScheduleStart": "00:00", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "customName": "Cleany", + "currentPM25": 11, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "otaPolicy": "autoUpdate", + "otaStatus": "upToDate", + "vocIndex": 100, + "manufacturer": "IKEA of Sweden", + "productCode": "E2112", + "otaState": "readyToCheck", + "hardwareVersion": "1", + "model": "VINDSTYRKA", + "firmwareVersion": "1.0.11", + "currentRH": 76 + }, + "id": "f80cac12-65a4-47b4-9f68-a0456a349a43_1", + "type": "sensor", + "deviceSet": [], + "room": { + "color": "ikea_green_no_65", + "name": "Loft", + "icon": "rooms_sofa", + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d" + }, + "isHidden": false + }, + { + "id": "5ffa4a1e-6fe2-4a24-b0af-b216937c4e8c_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:04:54.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:08:02.000Z", + "attributes": { + "customName": "Lampe Rechts", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "30FB10FFFEE44843", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 54, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-11T11:33:27.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T11:22:41.000Z", + "attributes": { + "customName": "Remote Control Loft", + "firmwareVersion": "2.4.16", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "Remote Control N2", + "productCode": "E2001", + "serialNumber": "94B216FFFE6F9BA6", + "batteryPercentage": 85, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "044b63e7-999d-4caa-8a76-fb8cfd32b381_1", + "type": "repeater", + "deviceType": "repeater", + "createdAt": "2024-10-10T10:10:37.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T10:12:28.000Z", + "attributes": { + "customName": "Reaper", + "model": "TRADFRI Signal Repeater", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "14B457FFFEF8DC61", + "productCode": "E1746", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "45165c60-0c1a-4b9f-ae09-c4a6220d1420_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T18:09:58.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T20:02:56.000Z", + "attributes": { + "customName": "Lampe Links", + "model": "TRADFRI bulb E14 WS candle 470lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "3.0.21", + "hardwareVersion": "1", + "serialNumber": "881A14FFFE479E82", + "productCode": "LED2107C4", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 51, + "startUpCurrentLevel": -1, + "colorTemperature": 2188, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "temperature", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [ + "5ac5e131-44a4-4d75-be78-759a095d31fb_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T17:41:34.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "customIcon": "lighting_pendant_light", + "attributes": { + "customName": "Esszimmer Hinten", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE5FFFA2", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 92, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 0.7, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:20:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:01.000Z", + "attributes": { + "customName": "Loft Floor Lamp", + "model": "TRADFRI bulb E27 CWS opal 600lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.3.086", + "hardwareVersion": "1", + "serialNumber": "EC1BBDFFFEC17EA1", + "productCode": "LED1624G9E27EU", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 119.9981689453125, + "colorSaturation": 1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "338bb721-35bb-4775-8cd0-ba70fc37ab10_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-04T17:21:49.446Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:09:15.285Z", + "attributes": { + "customName": "Loft", + "model": "SYMFONISK Bookshelf S21", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.3.3-2.0", + "serialNumber": "34-7E-5C-F5-45-88:F", + "productCode": "S21", + "identifyStarted": "2024-10-04T17:21:49.447Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-13T21:09:15.284Z", + "playbackAudio": { + "serviceType": "sonos", + "providerType": "Mixcloud", + "playItem": { + "id": "eyJhY2NvdW50VHlwZSI6InNvbm9zIiwiY29udGVudFR5cGUiOiJjb250YWluZXIiLCJjb250ZW50Ijp7ImNvbnRhaW5lciI6eyJhY2NvdW50SWQiOiJzbl8zIiwib2JqZWN0SWQiOiJjbG91ZGNhc3Q6MjE3OTQzNDcxNCIsInNlcnZpY2VJZCI6IjE4MSIsInR5cGUiOiJ0cmFjayJ9fX0=", + "title": "The Anjunadeep Edition 521 with Leaving Laurel & yehno", + "artist": "Anjunadeep", + "imageURL": "http://192.168.1.95:1400/getaa?s=1&u=x-sonos-http%3acloudcast%253a2179434714.unknown%3fsid%3d181%26flags%3d0%26sn%3d3", + "duration": 3617000 + } + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-13T21:09:15.284Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": true, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": true, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 16, + "isMuted": false, + "audioGroup": "1f386990-bb4f-4a00-befd-646b5ef52c47" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "03c716a9-900e-4bb0-8694-eaf837bfa3fb_1", + "type": "speaker", + "deviceType": "speaker", + "createdAt": "2024-10-06T09:06:12.272Z", + "isReachable": false, + "lastSeen": "2024-10-08T16:30:36.504Z", + "attributes": { + "customName": "Moritz", + "model": "SYMFONISK Table lamp S20", + "manufacturer": "Sonos, Inc.", + "firmwareVersion": "80.1-56190", + "hardwareVersion": "1.20.2.3-2.0", + "serialNumber": "78-28-CA-81-8E-F2:1", + "productCode": "S20", + "identifyStarted": "2024-10-06T09:06:12.272Z", + "identifyPeriod": 0, + "playback": "playbackIdle", + "playbackLastChangedTimestamp": "2024-10-06T09:06:12.272Z", + "playbackAudio": { + + }, + "playbackPosition": { + "position": 0, + "timestamp": "2024-10-06T09:06:12.032Z" + }, + "playbackAvailableActions": { + "crossfade": true, + "pause": false, + "repeat": [ + "off", + "playItem", + "playlist" + ], + "seek": false, + "shuffle": true, + "playbackNext": false, + "playbackPrev": false + }, + "playbackModes": { + "crossfade": false, + "repeat": "off", + "shuffle": false + }, + "volume": 15, + "isMuted": false, + "audioGroup": "71aa115c-fadd-4c43-aaf7-d537fa939638" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "playback", + "playbackAudio", + "volume", + "isMuted" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": true + }, + { + "id": "07cca6c2-f2b6-4f57-bfd9-a788a16d1eef_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-09T21:23:19.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:22:08.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Sensor Kitchen", + "model": "PARASOLL Door/Window Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "serialNumber": "D44867FFFEC2D1F0", + "productCode": "E2013", + "batteryPercentage": 84, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-04T18:00:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:17:02.000Z", + "customIcon": "lighting_cone_pendant", + "attributes": { + "customName": "Esszimmer Vorne", + "model": "TRADFRI bulb E27 CWS globe 806lm", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.38", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFEC9113D", + "productCode": "LED2109G6", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "colorHue": 0, + "colorSaturation": 1, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "a4c6a33a-9c6a-44bf-bdde-f99aff00eca4_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-16T12:57:26.000Z", + "isReachable": true, + "lastSeen": "2024-10-16T13:00:58.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "2.4.4", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRETAKT Smart plug", + "productCode": "E2204", + "serialNumber": "D44867FFFE140EDE", + "isOn": false, + "startupOnOff": "startPrevious", + "lightLevel": 100, + "startUpCurrentLevel": -1, + "childLock": false, + "statusLight": true, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "childLock", + "statusLight" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ec549fa8-4e35-4f27-90e9-bb67e68311f2_1", + "type": "outlet", + "deviceType": "outlet", + "createdAt": "2024-10-08T20:43:31.000Z", + "isReachable": true, + "lastSeen": "2024-10-10T14:40:38.000Z", + "attributes": { + "customName": "Smart-Home-Steckdose 1", + "model": "INSPELNING Smart plug", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.4.45", + "hardwareVersion": "1", + "serialNumber": "ECF64CFFFEF28EC3", + "productCode": "E2206", + "isOn": true, + "startupOnOff": "startPrevious", + "lightLevel": 1, + "startUpCurrentLevel": -1, + "currentVoltage": 236.8000030517578, + "currentAmps": 0.003000000026077032, + "currentActivePower": 0, + "totalEnergyConsumed": 0.01899999938905239, + "totalEnergyConsumedLastUpdated": "2024-10-10T14:40:05.000Z", + "energyConsumedAtLastReset": 0, + "timeOfLastEnergyReset": "2024-10-08T20:10:31.000Z", + "childLock": false, + "statusLight": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "energyConsumedAtLastReset", + "childLock", + "statusLight" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-09T17:38:43.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T17:18:25.000Z", + "attributes": { + "customName": "", + "model": "", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.1.10", + "hardwareVersion": "1", + "serialNumber": "7CC6B6FFFE66D457", + "productCode": "L2112", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 95, + "startUpCurrentLevel": -1, + "colorHue": 29.9981689453125, + "colorSaturation": 0.6496062992125984, + "colorTemperature": 2702, + "colorTemperatureMin": 4000, + "colorTemperatureMax": 2202, + "startupTemperature": -1, + "colorMode": "color", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoUpdate", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature", + "colorHue", + "colorSaturation" + ] + }, + "room": { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + "deviceSet": [], + "remoteLinks": [ + "22e4b77b-9a60-4727-944b-0d5e3e33b58f_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_1", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "Motion Sensor Gäste WC", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "batteryPercentage": 85, + "isOn": false, + "motionDetectedDelay": 20, + "isDetected": false, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": true, + "onDuration": 300, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "5ac5e131-44a4-4d75-be78-759a095d31fb_3", + "relationId": "5ac5e131-44a4-4d75-be78-759a095d31fb", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-09T17:29:53.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T18:39:59.000Z", + "attributes": { + "customName": "", + "model": "VALLHORN Wireless Motion Sensor", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "serialNumber": "8C65A3FFFE35C8D4", + "productCode": "E2134", + "illuminance": 1, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29_1", + "relationId": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "type": "gateway", + "deviceType": "gateway", + "createdAt": "2024-03-08T06:39:04.019Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:29:00.066Z", + "attributes": { + "customName": "Townhouse", + "model": "DIRIGERA Hub for smart products", + "manufacturer": "IKEA of Sweden", + "firmwareVersion": "2.660.2", + "hardwareVersion": "P2.5", + "serialNumber": "594197c3-23c9-4dc7-a6ca-1fe6a8455d29", + "identifyStarted": "2000-01-01T00:00:00.000Z", + "identifyPeriod": 0, + "otaStatus": "upToDate", + "otaState": "readyToCheck", + "otaProgress": 0, + "otaPolicy": "autoDownload", + "otaScheduleStart": "00:00", + "otaScheduleEnd": "00:00", + "permittingJoin": false, + "backendConnected": true, + "backendConnectionPersistent": true, + "backendOnboardingComplete": true, + "backendRegion": "eu-central-1", + "backendCountryCode": "DE", + "userConsents": [ + { + "name": "analytics", + "value": "disabled" + }, + { + "name": "diagnostics", + "value": "enabled" + } + ], + "logLevel": 3, + "coredump": false, + "timezone": "Europe/Berlin", + "nextSunSet": "2024-10-14T16:37:00.000Z", + "nextSunRise": "2024-10-14T05:48:00.000Z", + "homestate": "home", + "countryCode": "XZ", + "coordinates": { + "latitude": 9.876, + "longitude": 1.234, + "accuracy": -1 + }, + "isOn": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "permittingJoin", + "userConsents", + "logLevel", + "time", + "timezone", + "countryCode", + "coordinates" + ] + }, + "deviceSet": [], + "remoteLinks": [] + }, + { + "id": "a1e1eacc-2dcf-45bd-9f93-62a436b6a7ed_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:46:04.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:06:16.000Z", + "attributes": { + "customName": "Bathroom Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x90", + "productCode": "", + "serialNumber": "804B50FFFEF79502", + "isOn": true, + "startupOnOff": "startOn", + "lightLevel": 49, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "073b40df-e6a0-486d-99a3-6134effe4c59_1", + "type": "light", + "deviceType": "light", + "createdAt": "2024-10-10T13:48:44.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:21:17.000Z", + "customIcon": "lighting_fan", + "attributes": { + "customName": "HWR Panel", + "firmwareVersion": "2.3.087", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "FLOALT panel WS 30x30", + "productCode": "", + "serialNumber": "BC33ACFFFE9761AA", + "isOn": false, + "startupOnOff": "startOn", + "lightLevel": 73, + "startUpCurrentLevel": -1, + "colorMode": "temperature", + "startupTemperature": -1, + "colorTemperature": 4000, + "colorTemperatureMax": 2202, + "colorTemperatureMin": 4000, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate" + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName", + "isOn", + "lightLevel", + "colorTemperature" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [ + "ee61c57f-8efa-44f4-ba8a-d108ae054138_1" + ], + "isHidden": false, + "adaptiveProfile": { + + } + }, + { + "id": "ee61c57f-8efa-44f4-ba8a-d108ae054138_1", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:56:23.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T23:18:17.000Z", + "attributes": { + "customName": "HWR Motion", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + "productCode": "E1745", + "serialNumber": "BC33ACFFFE8739CC", + "batteryPercentage": 20, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "isDetected": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "sensorConfig": { + "scheduleOn": true, + "onDuration": 180, + "schedule": { + "onCondition": { + "time": "sunset" + }, + "offCondition": { + "time": "sunrise" + } + } + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "3267c0f3-069a-4894-b5d0-a30d26386af8_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:00:00.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T13:31:10.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Door Dining Room", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "D44867FFFEB36824", + "batteryPercentage": 82, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "bab217dc-5d46-42af-a3a4-6a53b9a953da_1", + "type": "sensor", + "deviceType": "openCloseSensor", + "createdAt": "2024-10-10T14:05:20.000Z", + "isReachable": true, + "lastSeen": "2024-10-13T21:36:48.000Z", + "customIcon": "placement_door", + "attributes": { + "customName": "Entry Door", + "firmwareVersion": "1.0.19", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "PARASOLL Door/Window Sensor", + "productCode": "E2013", + "serialNumber": "F84477FFFEDC7871", + "batteryPercentage": 81, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "isOpen": false, + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_1", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "sensor", + "deviceType": "motionSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "Bedroom Motion", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "batteryPercentage": 84, + "isOn": false, + "isDetected": false, + "motionDetectedDelay": 20, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false, + "sensorConfig": { + "scheduleOn": false, + "onDuration": 180 + }, + "circadianPresets": [] + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "ca856a7d-a715-42f7-84a1-7caae41e6ff2_3", + "relationId": "ca856a7d-a715-42f7-84a1-7caae41e6ff2", + "type": "unknown", + "deviceType": "lightSensor", + "createdAt": "2024-10-10T14:24:07.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T05:15:44.000Z", + "attributes": { + "customName": "", + "firmwareVersion": "1.0.64", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "VALLHORN Wireless Motion Sensor", + "productCode": "E2134", + "serialNumber": "8C65A3FFFE355FB0", + "illuminance": 1, + "identifyPeriod": 0, + "identifyStarted": "2000-01-01T00:00:00.000Z", + "permittingJoin": false + }, + "capabilities": { + "canSend": [], + "canReceive": [ + "customName" + ] + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + }, + { + "id": "9e9cb6b1-7981-443a-bd1e-5301815a25bd_1", + "type": "controller", + "deviceType": "lightController", + "createdAt": "2024-10-13T20:09:35.000Z", + "isReachable": true, + "lastSeen": "2024-10-14T00:32:59.000Z", + "attributes": { + "customName": "HWR Remote", + "firmwareVersion": "24.4.5", + "hardwareVersion": "1", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + "productCode": "E1810", + "serialNumber": "BC33ACFFFE9B308C", + "batteryPercentage": 30, + "isOn": false, + "lightLevel": 1, + "permittingJoin": false, + "otaPolicy": "autoUpdate", + "otaProgress": 0, + "otaScheduleEnd": "00:00", + "otaScheduleStart": "00:00", + "otaState": "readyToCheck", + "otaStatus": "upToDate", + "circadianPresets": [] + }, + "capabilities": { + "canSend": [ + "isOn", + "lightLevel" + ], + "canReceive": [ + "customName" + ] + }, + "room": { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + "deviceSet": [], + "remoteLinks": [], + "isHidden": false + } + ], + "users": [], + "scenes": [ + { + "id": "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58", + "info": { + "name": "Lightmode Christmas", + "icon": "scenes_tree" + }, + "type": "userScene", + "triggers": [ + { + "id": "987ec615-24d8-411c-9aa9-d03c447b246b", + "type": "app", + "triggeredAt": "2024-10-16T00:21:15.942Z", + "disabled": false + } + ], + "actions": [ + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 0, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T11:13:42.273Z", + "lastCompleted": "2024-10-16T00:21:15.977Z", + "lastTriggered": "2024-10-16T00:21:15.977Z", + "undoAllowedDuration": 30, + "lastUndo": "2024-10-15T19:36:59.273Z" + }, + { + "id": "3090ba82-3f5e-442f-8e49-f3eac9b7b0eb", + "info": { + "name": "Lightmode off", + "icon": "scenes_disco_ball" + }, + "type": "userScene", + "triggers": [ + { + "id": "6fff3797-c798-4d0c-abe0-ce46cc389104", + "type": "app", + "triggeredAt": "2024-10-16T00:38:16.257Z", + "disabled": false + } + ], + "actions": [ + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + }, + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 359.83392, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": false, + "lightLevel": 100, + "colorHue": 89.681786, + "colorSaturation": 0.99900264 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T23:14:12.399Z", + "lastCompleted": "2024-10-16T00:38:16.267Z", + "lastTriggered": "2024-10-16T00:38:16.267Z", + "undoAllowedDuration": 30 + } + ], + "rooms": [ + { + "id": "921edb07-e344-4c70-a09e-11935c727f58", + "name": "Esszimmer", + "color": "ikea_yellow_no_31", + "icon": "rooms_cutlery" + }, + { + "id": "2abdfb7b-d080-4b00-b576-0c6fb73e9b3b", + "name": "Lobby HWR", + "color": "ikea_pink_no_4", + "icon": "rooms_drill" + }, + { + "id": "7bc86420-cf4a-4a58-beff-7dcf462258da", + "name": "Badezimmer", + "color": "ikea_blue_no_58", + "icon": "rooms_bathtub" + }, + { + "id": "88f27266-14a8-4d66-8e4f-e10a3318868e", + "name": "Küche", + "color": "ikea_red_no_39", + "icon": "rooms_kitchen" + }, + { + "id": "893d50e0-f4bb-46ba-978d-e9280c6e299d", + "name": "Loft", + "color": "ikea_green_no_65", + "icon": "rooms_sofa" + }, + { + "id": "4328471f-03cd-4372-aa1e-e3e9f3be3da4", + "name": "Schlafzimmer ", + "color": "ikea_beige_no_3", + "icon": "rooms_bed" + }, + { + "id": "c9e407e5-5bfa-4e73-aaf3-c0258d9ccb5f", + "name": "Gäste WC", + "color": "ikea_blue_no_58", + "icon": "rooms_sink" + } + ], + "deviceSets": [], + "adaptiveProfiles": [] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-deleted/scene-deleted.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-deleted/scene-deleted.json new file mode 100644 index 00000000000..bd1d540e6db --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-deleted/scene-deleted.json @@ -0,0 +1,79 @@ +{ + "data": { + "id": "086f4a37-ebe8-4fd4-9a25-a0220a1e5f58", + "info": { + "name": "Lightmode Christmas", + "icon": "scenes_tree" + }, + "type": "userScene", + "triggers": [ + { + "id": "987ec615-24d8-411c-9aa9-d03c447b246b", + "type": "app", + "triggeredAt": "2024-10-16T00:21:15.942Z", + "disabled": false + } + ], + "actions": [ + { + "id": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "type": "device", + "deviceId": "c27faa27-4c18-464f-81a0-a31ce57d83d5_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 0, + "colorSaturation": 1 + } + }, + { + "id": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "type": "device", + "deviceId": "891790db-8c17-483a-a1a6-c85bffd3a373_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "type": "device", + "deviceId": "d791dc18-9180-45c7-92fc-604481a34f15_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + }, + { + "id": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "type": "device", + "deviceId": "3c8b0049-eb5c-4ea1-9da3-cdedc50366ef_1", + "enabled": true, + "attributes": { + "isOn": true, + "lightLevel": 100, + "colorHue": 90, + "colorSaturation": 1 + } + } + ], + "commands": [], + "createdAt": "2024-10-15T11:13:42.273Z", + "lastCompleted": "2024-10-16T00:21:15.977Z", + "lastTriggered": "2024-10-16T00:21:15.977Z", + "undoAllowedDuration": 30, + "lastUndo": "2024-10-15T19:36:59.273Z" + }, + "specversion": "3.163.0", + "id": "382f5a61-0b3a-4c55-b601-d89c003ac27c", + "time": "2024-10-17T21:17:48.000Z", + "source": "urn:com:ikea:homesmart:iotc:zigbee", + "type": "sceneDeleted" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-pressed/scene-trigger-sequence.json b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-pressed/scene-trigger-sequence.json new file mode 100644 index 00000000000..4a04a8b93ab --- /dev/null +++ b/bundles/org.openhab.binding.dirigera/src/test/resources/websocket/scene-pressed/scene-trigger-sequence.json @@ -0,0 +1,98 @@ +[ + { + "data": { + "createdAt": "2024-10-27T19:47:59.155Z", + "undoAllowedDuration": 30, + "id": "3090ba82-aaaa-442f-8e49-f3eac9b7b0eb", + "type": "userScene", + "triggers": [ + { + "disabled": false, + "id": "5ec0d314-bbbb-470e-a92c-93beb7fbe09e", + "trigger": { + "controllerType": "shortcutController", + "buttonIndex": 0, + "days": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ], + "clickPattern": "singlePress", + "deviceId": "854bdf30-86b8-48f5-b070-16ff5ab12be4_1" + }, + "type": "controller" + }, + { + "disabled": false, + "id": "e615f61f-cccc-490c-98cf-1ad804f6b52d", + "triggeredAt": "2024-10-27T19:48:21.063Z", + "type": "app" + } + ], + "actions": [], + "commands": [], + "info": { + "name": "OHButtonConnector", + "icon": "scenes_disco_ball" + } + }, + "specversion": "3.163.0", + "id": "24c50fa3-035e-4d77-a9d4-b121c17e08a0", + "time": "2024-10-27T19:48:21.063Z", + "source": "urn:com:ikea:homesmart:iotc:rulesengine", + "type": "sceneUpdated" + }, + { + "data": { + "createdAt": "2024-10-27T19:47:59.155Z", + "lastTriggered": "2024-10-27T19:48:21.075Z", + "undoAllowedDuration": 30, + "lastCompleted": "2024-10-27T19:48:21.075Z", + "id": "3090ba82-aaaa-442f-8e49-f3eac9b7b0eb", + "type": "userScene", + "triggers": [ + { + "disabled": false, + "id": "5ec0d314-bbbb-470e-a92c-93beb7fbe09e", + "trigger": { + "controllerType": "shortcutController", + "buttonIndex": 0, + "days": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ], + "clickPattern": "singlePress", + "deviceId": "854bdf30-86b8-48f5-b070-16ff5ab12be4_1" + }, + "type": "controller" + }, + { + "disabled": false, + "id": "e615f61f-cccc-490c-98cf-1ad804f6b52d", + "triggeredAt": "2024-10-27T19:48:21.063Z", + "type": "app" + } + ], + "actions": [], + "commands": [], + "info": { + "name": "OHButtonConnector", + "icon": "scenes_disco_ball" + } + }, + "specversion": "3.163.0", + "id": "d4843df1-8c69-4c82-8a15-f611361455e4", + "time": "2024-10-27T19:48:21.084Z", + "source": "urn:com:ikea:homesmart:iotc:rulesengine", + "type": "sceneUpdated" + } +] \ No newline at end of file diff --git a/bundles/pom.xml b/bundles/pom.xml index 3d133fc04cb..2e149a1e213 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -117,6 +117,7 @@ org.openhab.binding.deutschebahn org.openhab.binding.digiplex org.openhab.binding.digitalstrom + org.openhab.binding.dirigera org.openhab.binding.dlinksmarthome org.openhab.binding.dmx org.openhab.binding.dolbycp