From 044e9a33882a54d5b8098ba79a2e7fffa414df36 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Wed, 4 Dec 2024 09:32:09 -0700 Subject: [PATCH] [wiz] Initial contribution (#17681) Also-by: Joshua Freeman Also-by: Stefan Fussenegger Also-by: Sara Damiano Signed-off-by: Cody Cutrer --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.wiz/NOTICE | 13 + bundles/org.openhab.binding.wiz/README.md | 150 +++ bundles/org.openhab.binding.wiz/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../wiz/internal/WizBindingConstants.java | 140 +++ .../wiz/internal/WizHandlerFactory.java | 83 ++ .../internal/WizStateDescriptionProvider.java | 73 ++ .../config/WizDeviceConfiguration.java | 36 + .../discovery/WizDiscoveryService.java | 271 ++++++ .../internal/entities/ColorRequestParam.java | 105 +++ .../ColorTemperatureRequestParam.java | 45 + .../entities/DimmingRequestParam.java | 55 ++ .../entities/ErrorResponseResult.java | 38 + .../entities/FanModeRequestParam.java | 40 + .../entities/FanReverseRequestParam.java | 40 + .../entities/FanSpeedRequestParam.java | 40 + .../entities/FanStateRequestParam.java | 40 + .../entities/FirstBeatResponseParam.java | 42 + .../internal/entities/ModelConfigResult.java | 69 ++ .../binding/wiz/internal/entities/Param.java | 26 + .../internal/entities/PulseRequestParam.java | 56 ++ .../entities/RegistrationRequestParam.java | 84 ++ .../internal/entities/SceneRequestParam.java | 46 + .../internal/entities/SpeedRequestParam.java | 49 + .../internal/entities/StateRequestParam.java | 41 + .../internal/entities/SystemConfigResult.java | 74 ++ .../wiz/internal/entities/WizRequest.java | 77 ++ .../wiz/internal/entities/WizResponse.java | 159 ++++ .../wiz/internal/entities/WizSyncState.java | 147 +++ .../wiz/internal/enums/WizColorMode.java | 50 + .../wiz/internal/enums/WizLightMode.java | 111 +++ .../wiz/internal/enums/WizMethodType.java | 150 +++ .../wiz/internal/enums/WizModuleType.java | 65 ++ .../wiz/internal/handler/WizHandler.java | 882 ++++++++++++++++++ .../wiz/internal/handler/WizMediator.java | 87 ++ .../wiz/internal/handler/WizMediatorImpl.java | 232 +++++ .../runnable/WizUpdateReceiverRunnable.java | 141 +++ .../wiz/internal/utils/NetworkUtils.java | 75 ++ .../wiz/internal/utils/ValidationUtils.java | 46 + .../wiz/internal/utils/WizColorConverter.java | 118 +++ .../internal/utils/WizPacketConverter.java | 94 ++ .../utils/WizResponseDeserializer.java | 228 +++++ .../src/main/resources/OH-INF/addon/addon.xml | 11 + .../main/resources/OH-INF/config/config.xml | 42 + .../main/resources/OH-INF/i18n/wiz.properties | 116 +++ .../resources/OH-INF/thing/thing-types.xml | 222 +++++ bundles/pom.xml | 1 + 49 files changed, 4742 insertions(+) create mode 100644 bundles/org.openhab.binding.wiz/NOTICE create mode 100644 bundles/org.openhab.binding.wiz/README.md create mode 100644 bundles/org.openhab.binding.wiz/pom.xml create mode 100644 bundles/org.openhab.binding.wiz/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizBindingConstants.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizHandlerFactory.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizStateDescriptionProvider.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/config/WizDeviceConfiguration.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/discovery/WizDiscoveryService.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorRequestParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorTemperatureRequestParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/DimmingRequestParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ErrorResponseResult.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanModeRequestParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanReverseRequestParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanSpeedRequestParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanStateRequestParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FirstBeatResponseParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ModelConfigResult.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/Param.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/PulseRequestParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/RegistrationRequestParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SceneRequestParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SpeedRequestParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/StateRequestParam.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SystemConfigResult.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizRequest.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizResponse.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizSyncState.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizColorMode.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizLightMode.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizMethodType.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizModuleType.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizHandler.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediator.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediatorImpl.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/runnable/WizUpdateReceiverRunnable.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/NetworkUtils.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/ValidationUtils.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizColorConverter.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizPacketConverter.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizResponseDeserializer.java create mode 100644 bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/config/config.xml create mode 100644 bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/i18n/wiz.properties create mode 100644 bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index dd285213ecc2a..0526f553154f3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -421,6 +421,7 @@ /bundles/org.openhab.binding.wemo/ @hmerk @jlaur /bundles/org.openhab.binding.wifiled/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.windcentrale/ @marcelrv @wborn +/bundles/org.openhab.binding.wiz/ @ccutrer @frejos /bundles/org.openhab.binding.wlanthermo/ @CSchlipp /bundles/org.openhab.binding.wled/ @Skinah /bundles/org.openhab.binding.wolfsmartset/ @BoBiene diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index f2684cc5cd208..a546e9c9cbed4 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -2081,6 +2081,11 @@ org.openhab.binding.windcentrale ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.wiz + ${project.version} + org.openhab.addons.bundles org.openhab.binding.wlanthermo diff --git a/bundles/org.openhab.binding.wiz/NOTICE b/bundles/org.openhab.binding.wiz/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/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.wiz/README.md b/bundles/org.openhab.binding.wiz/README.md new file mode 100644 index 0000000000000..55d2816f61d3d --- /dev/null +++ b/bundles/org.openhab.binding.wiz/README.md @@ -0,0 +1,150 @@ +# WiZ Binding + +This binding integrates [WiZ Connected](https://www.wizconnected.com/en-US/) smart devices. +These inexpensive devices, typically smart bulbs, are available online and in most Home Depot stores. +They come in a variety of bulb shapes and sizes with options of full color with tunable white, tunable white, and dimmable white. +This binding has been tested with various bulbs and switchable plugs. +They are sold under the Philips brand name. +(Wiz is owned by Signify (formerly Philips Lighting).) +*Note* that while both are sold by Philips, WiZ bulbs are *not* part of the Hue ecosystem. + +This binding operates completely within the local network - the discovery, control, and status monitoring is entirely over UDP in the local network. +The binding never attempts to contact the WiZ servers in any way but does not stop them from doing so independently. +It should not interfere in any way with control of the bulbs via the WiZ app or any other service integrated with the WiZ app (e.g. Alexa, IFTTT, SmartThings). +Any changes made to the bulb state outside of openHAB should be detected by the binding and vice-versa. +Before using the binding, the bulbs must be set up using the WiZ iOS or Android app. +Local control must also be enabled with-in the WiZ app in the app settings. +(This is the default.) + +## Supported Things + +- WiZ Full Color with Tunable White Bulbs +- WiZ Tunable White Bulbs +- WiZ Dimmable single-color bulbs +- WiZ Smart Plugs +- Smart fans (with or without a dimmable light) + +**NOTE:** This binding was created for and tested on the full color with tunable white bulbs, however, users have reported success with other bulb types and plugs. + +## Discovery + +New devices can be discovered by scanning and may also be discovered by background discovery. +All discovered devices will default to 'Full Color' bulbs if unable to automatically detect the specific device type. +You may need to create devices manually if desired. + +Devices must first have been set up using the WiZ iOS or Android app. +If the binding cannot discover your device, try unplugging it, wait several seconds, and plug it back in. + +## Binding Configuration + +The binding does not require any special configuration. +You can optionally manually set the IP and MAC address of the openHAB instance; if you do not set them, the binding will use the system defaults. + +## Thing Configuration + +To create or configure a device manually you need its IP address and MAC address. +These can be quickly found in the iOS or Android app by entering the settings for device in question and clicking on the model name. +The refresh interval may also be set; if unset it defaults to 30 seconds. +If you desire instant updates, you may also enable "heart-beat" synchronization with the bulbs. +Heart-beats are not used by default. +When heart-beats are enabled, the binding will continuously re-register with the bulbs to receive sync packets on every state change and on every 5 seconds. +Enabling heart-beats causes the refresh-interval to be ignored. +If heart-beats are not enabled, the channels are only updated when polled at the set interval and thus will be slightly delayed with regard to changes made to the bulb state outside of the binding (e.g. via the WiZ app). + +**NOTE:** While the bulb's IP address is needed for initial manual configuration, this binding _does not_ require you to use a static IP for each bulb. +After initial discovery or setup, the binding will automatically search for and re-match bulbs with changed IP addresses by MAC address once every hour. + +Thing parameters: + +| Parameter ID | Parameter Type | Mandatory | Description | Default | +|-------------------|----------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| +| macAddress | text | true | The MAC address of the bulb | | +| ipAddress | text | true | The IP of the bulb | | +| updateInterval | integer | false | Update time interval in seconds to request the status of the bulb. | 60 | +| useHeartBeats | boolean | false | Whether to register for continuous 5s heart-beats | false | +| reconnectInterval | integer | false | Interval in minutes between attempts to reconnect with a bulb that is no longer responding to status queries. When the bulb first connects to the network, it should send out a firstBeat message allowing openHAB to immediately detect it. This is only as a back-up to re-find the bulb. | 15 | + +Example Thing: + +```java +Thing wiz:bulb:lamp "My Lamp" @ "Living Room" [ macAddress="accf23343cxx", ipAddress="192.168.0.xx" ] +``` + +## Channels + +The binding supports the following channels. If a device is only a light or only a fan, the channels will +not be in a group. + +| Channel ID | Item Type | Description | Access | +|------------------------|----------------------|-------------------------------------------------------|--------| +| light#color | Color | State, intensity, and color of the LEDs | R/W | +| light#temperature | Dimmer | Color temperature of the bulb | R/W | +| light#temperature-abs | Number:Temperature | Color temperature of the bulb in Kelvin | R/W | +| light#brightness | Dimmer | The brightness of the bulb | R/W | +| light#state | Switch | Whether the bulb is on or off | R/W | +| light#light-mode | Number | Preset light mode name to run | R/W | +| light#speed | Dimmer | Speed of the color changes in dynamic light modes | R/W | +| fan#state | Switch | Whether the fan is on or off | R/W | +| fan#speed | Number | Speed of the fan, in arbitrary steps | R/W | +| fan#reverse | Switch | Whether the fan direction is reversed | R/W | +| fan#mode | Number | Special fan modes (Breeze) | R/W | +| device#last-update | Time | The last time an an update was received from the bulb | R | +| device#signal-strength | Number | Quality of the bulb's WiFi connection | R | +| device#rssi | Number:Dimensionless | WiFi Received Signal Strength Indicator (in dB) | R | + +## Light Modes + +The binding supports the following Light Modes + +| ID | Scene Name | +|----|---------------| +| 1 | Ocean | +| 2 | Romance | +| 3 | Sunset | +| 4 | Party | +| 5 | Fireplace | +| 6 | Cozy White | +| 7 | Forest | +| 8 | Pastel Colors | +| 9 | Wakeup | +| 10 | Bed Time | +| 11 | Warm White | +| 12 | Daylight | +| 13 | Cool White | +| 14 | Night Light | +| 15 | Focus | +| 16 | Relax | +| 17 | True Colors | +| 18 | TV Time | +| 19 | Plant Growth | +| 20 | Spring | +| 21 | Summer | +| 22 | Fall | +| 23 | Deep Dive | +| 24 | Jungle | +| 25 | Mojito | +| 26 | Club | +| 27 | Christmas | +| 28 | Halloween | +| 29 | Candlelight | +| 30 | Golden White | +| 31 | Pulse | +| 32 | Steampunk | + +## Bulb Limitations + +- Full-color bulbs operate in either color mode OR tunable white/color temperature mode. +The RGB LED's are NOT used to control temperature - separate warm and cool white LED's are used. +Sending a command on the color channel or the temperature channel will cause the bulb to switch the relevant mode. +- Dimmable bulbs do not dim below 10%. +- The binding attempts to immediately retrieve the actual state from the device after each command is acknowledged, sometimes this means your settings don't 'stick' this is because the device itself did not accept the command or setting. +- Parameters can not be changed while the bulbs are off, sending any commands to change any settings will cause the bulbs to turn on. +- Power on behavior is configured in the app. +- Fade in/out times are configured in the app. +- Sending too many commands to the bulbs too quickly can cause them to stop responding for a period of time. + +## Example Item Linked To a Channel + +```java +Color LivingRoom_Light_Color "Living Room Lamp" (gLivingroom) {channel="wiz:color-bulb:accf23343cxx:color"} +``` diff --git a/bundles/org.openhab.binding.wiz/pom.xml b/bundles/org.openhab.binding.wiz/pom.xml new file mode 100644 index 0000000000000..f1940b89af96c --- /dev/null +++ b/bundles/org.openhab.binding.wiz/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.3.0-SNAPSHOT + + + org.openhab.binding.wiz + + openHAB Add-ons :: Bundles :: WiZ Binding + + diff --git a/bundles/org.openhab.binding.wiz/src/main/feature/feature.xml b/bundles/org.openhab.binding.wiz/src/main/feature/feature.xml new file mode 100644 index 0000000000000..7f4ccfbea2b4c --- /dev/null +++ b/bundles/org.openhab.binding.wiz/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.wiz/${project.version} + + diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizBindingConstants.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizBindingConstants.java new file mode 100644 index 0000000000000..d3ffd29364a27 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizBindingConstants.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link WizBindingConstants} class defines common constants, which + * are used across the whole binding. + * + * @author Sriram Balakrishnan - Initial contribution + * @author Joshua Freeman - update version + */ +@NonNullByDefault +public class WizBindingConstants { + + /** + * The binding id. + */ + public static final String BINDING_ID = "wiz"; + + /** + * List of all Thing Type UIDs. + */ + public static final ThingTypeUID THING_TYPE_COLOR_BULB = new ThingTypeUID(BINDING_ID, "color-bulb"); + public static final ThingTypeUID THING_TYPE_TUNABLE_BULB = new ThingTypeUID(BINDING_ID, "tunable-bulb"); + public static final ThingTypeUID THING_TYPE_DIMMABLE_BULB = new ThingTypeUID(BINDING_ID, "dimmable-bulb"); + public static final ThingTypeUID THING_TYPE_SMART_PLUG = new ThingTypeUID(BINDING_ID, "plug"); + public static final ThingTypeUID THING_TYPE_FAN = new ThingTypeUID(BINDING_ID, "fan"); + public static final ThingTypeUID THING_TYPE_FAN_WITH_DIMMABLE_BULB = new ThingTypeUID(BINDING_ID, + "fan-with-dimmable-bulb"); + + /** + * The supported thing types. + */ + public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_COLOR_BULB, THING_TYPE_TUNABLE_BULB, + THING_TYPE_DIMMABLE_BULB, THING_TYPE_SMART_PLUG, THING_TYPE_FAN, THING_TYPE_FAN_WITH_DIMMABLE_BULB); + + /** + * List of all Channel ids + */ + public static final String CHANNEL_BRIGHTNESS = "brightness"; + public static final String CHANNEL_COLOR = "color"; + public static final String CHANNEL_LAST_UPDATE = "last-update"; + public static final String CHANNEL_MODE = "mode"; + public static final String CHANNEL_REVERSE = "reverse"; + public static final String CHANNEL_RSSI = "rssi"; + public static final String CHANNEL_SIGNAL_STRENGTH = "signal-strength"; + public static final String CHANNEL_SPEED = "speed"; + public static final String CHANNEL_STATE = "state"; + public static final String CHANNEL_TEMPERATURE = "temperature"; + public static final String CHANNEL_TEMPERATURE_ABS = "temperature-abs"; + + public static final String CHANNEL_GROUP_DEVICE = "device"; + public static final String CHANNEL_GROUP_LIGHT = "light"; + public static final String CHANNEL_GROUP_FAN = "fan"; + + // -------------- Configuration arguments ---------------- + /** + * Mac address configuration argument key. + */ + public static final String CONFIG_MAC_ADDRESS = "macAddress"; + + /** + * Host address configuration argument key. + */ + public static final String CONFIG_IP_ADDRESS = "ipAddress"; + + /** + * Wifi socket update interval configuration argument key. + */ + public static final String CONFIG_UPDATE_INTERVAL = "updateInterval"; + public static final long DEFAULT_REFRESH_INTERVAL_SEC = 60; + + /** + * Wifi socket update interval configuration argument key. + */ + public static final String CONFIG_RECONNECT_INTERVAL = "reconnectInterval"; + public static final long DEFAULT_RECONNECT_INTERVAL_MIN = 15; + + // -------------- Default values ---------------- + + /** + * The number of refresh intervals without a response before a bulb is marked + * offline + */ + public static final int MARK_OFFLINE_AFTER_SEC = 5 * 60; + + /** + * Default Wifi socket default UDP port. + */ + public static final int DEFAULT_UDP_PORT = 38899; + + /** + * Default listener socket default UDP port. + */ + public static final int DEFAULT_LISTENER_UDP_PORT = 38900; + + /** + * How long before active discovery times out. + */ + public static final int DISCOVERY_TIMEOUT_SECONDS = 2; + + // -------------- Constants Used ---------------- + + /** + * The color temperature range of the WiZ bulbs + */ + public static final int MIN_COLOR_TEMPERATURE = 2200; + public static final int MAX_COLOR_TEMPERATURE = 6500; + + // -------------- Bulb Properties ---------------- + + public static final String PROPERTY_IP_ADDRESS = "ipAddress"; + + public static final String PROPERTY_HOME_ID = "homeId"; + public static final String PROPERTY_ROOM_ID = "roomId"; + public static final String PROPERTY_HOME_LOCK = "homeLock"; + public static final String PROPERTY_PAIRING_LOCK = "pairingLock"; + public static final String PROPERTY_TYPE_ID = "typeId"; + public static final String PROPERTY_MODULE_NAME = "moduleName"; + public static final String PROPERTY_GROUP_ID = "groupId"; + + public static final String EXPECTED_MODULE_NAME = "ESP01_SHRGB1C_31"; + public static final String LAST_KNOWN_FIRMWARE_VERSION = "1.18.0"; + public static final String MODEL_CONFIG_MINIMUM_FIRMWARE_VERSION = "1.22"; +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizHandlerFactory.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizHandlerFactory.java new file mode 100644 index 0000000000000..d15abedfb0075 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizHandlerFactory.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal; + +import static org.openhab.binding.wiz.internal.WizBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.wiz.internal.handler.WizHandler; +import org.openhab.binding.wiz.internal.handler.WizMediator; +import org.openhab.core.i18n.TimeZoneProvider; +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.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link WizHandlerFactory} is responsible for creating things and + * thing handlers. + * + * @author Sriram Balakrishnan - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.wiz", service = ThingHandlerFactory.class) +public class WizHandlerFactory extends BaseThingHandlerFactory { + private final Logger logger = LoggerFactory.getLogger(WizHandlerFactory.class); + + private final WizMediator mediator; + private final WizStateDescriptionProvider stateDescriptionProvider; + private final TimeZoneProvider timeZoneProvider; + + @Activate + public WizHandlerFactory(@Reference WizMediator mediator, + @Reference WizStateDescriptionProvider stateDescriptionProvider, + @Reference TimeZoneProvider timeZoneProvider) { + this.mediator = mediator; + this.stateDescriptionProvider = stateDescriptionProvider; + this.timeZoneProvider = timeZoneProvider; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(final Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (supportsThingType(thing.getThingTypeUID())) { + WizHandler handler; + + handler = new WizHandler(thing, mediator, stateDescriptionProvider, timeZoneProvider); + + mediator.registerThingAndWizBulbHandler(thing, handler); + return handler; + } else { + logger.warn("Thing type {} not supported.", thingTypeUID); + } + return null; + } + + @Override + public void unregisterHandler(final Thing thing) { + mediator.unregisterWizBulbHandlerByThing(thing); + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizStateDescriptionProvider.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizStateDescriptionProvider.java new file mode 100644 index 0000000000000..9d462a7982c82 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/WizStateDescriptionProvider.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal; + +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.openhab.core.types.StateDescription; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provide a dynamic state description for color temp to define the min/max as provided by the + * actual bulb. + * This service is started on-demand only, as soon as {@link WizThingHandlerFactory} requires it. + * + * @author Cody Cutrer - Initial contribution + */ +@Component(service = { DynamicStateDescriptionProvider.class, WizStateDescriptionProvider.class }) +@NonNullByDefault +public class WizStateDescriptionProvider implements DynamicStateDescriptionProvider { + + private final Map stateDescriptions = new ConcurrentHashMap<>(); + private final Logger logger = LoggerFactory.getLogger(WizStateDescriptionProvider.class); + + /** + * Set a state description for a channel. This description will be used when preparing the channel state by + * the framework for presentation. A previous description, if existed, will be replaced. + * + * @param channelUID channel UID + * @param description state description for the channel + */ + public void setDescription(ChannelUID channelUID, StateDescription description) { + logger.debug("Adding state description for channel {}: {}", channelUID, description); + stateDescriptions.put(channelUID, description); + } + + @Override + public @Nullable StateDescription getStateDescription(Channel channel, + @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { + StateDescription description = stateDescriptions.get(channel.getUID()); + if (description != null) { + logger.trace("Providing state description for channel {}", channel.getUID()); + } + return description; + } + + /** + * Removes the given channel description. + * + * @param channel The channel + */ + public void remove(ChannelUID channel) { + stateDescriptions.remove(channel); + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/config/WizDeviceConfiguration.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/config/WizDeviceConfiguration.java new file mode 100644 index 0000000000000..abf108fdd42cd --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/config/WizDeviceConfiguration.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.config; + +import static org.openhab.binding.wiz.internal.WizBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link BondHomeConfiguration} class contains fields mapping thing + * configuration parameters. + * + * @author Sara Geleskie Damiano - Initial contribution + */ +@NonNullByDefault +public class WizDeviceConfiguration { + + /** + * Configuration for a WiZ Device + */ + public String macAddress = ""; + public String ipAddress = ""; + public long updateInterval = DEFAULT_REFRESH_INTERVAL_SEC; + public boolean useHeartBeats = false; // true: register to get 5s heart-beats + public long reconnectInterval = DEFAULT_RECONNECT_INTERVAL_MIN; +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/discovery/WizDiscoveryService.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/discovery/WizDiscoveryService.java new file mode 100644 index 0000000000000..6f980523be433 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/discovery/WizDiscoveryService.java @@ -0,0 +1,271 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.discovery; + +import static org.openhab.binding.wiz.internal.WizBindingConstants.*; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketTimeoutException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.wiz.internal.entities.RegistrationRequestParam; +import org.openhab.binding.wiz.internal.entities.SystemConfigResult; +import org.openhab.binding.wiz.internal.entities.WizRequest; +import org.openhab.binding.wiz.internal.entities.WizResponse; +import org.openhab.binding.wiz.internal.enums.WizMethodType; +import org.openhab.binding.wiz.internal.handler.WizMediator; +import org.openhab.binding.wiz.internal.utils.WizPacketConverter; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.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.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the {@link DiscoveryService} for the WiZ Things. + * + * @author Sriram Balakrishnan - Initial contribution + * @author Joshua Freeman - use configured Broadcast address instead of guessing, discovery of plugs + * + */ +@Component(configurationPid = "discovery.wiz", service = DiscoveryService.class, immediate = true) +@NonNullByDefault +public class WizDiscoveryService extends AbstractDiscoveryService { + + private final Logger logger = LoggerFactory.getLogger(WizDiscoveryService.class); + + private final WizMediator mediator; + + private final WizPacketConverter converter = new WizPacketConverter(); + + private @Nullable ScheduledFuture backgroundDiscovery; + + /** + * Constructor of the discovery service. + * + * @throws IllegalArgumentException if the timeout < 0 + */ + @Activate + public WizDiscoveryService( + @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC) WizMediator mediator) + throws IllegalArgumentException { + super(SUPPORTED_THING_TYPES, DISCOVERY_TIMEOUT_SECONDS, true); + this.mediator = mediator; + mediator.setDiscoveryService(this); + } + + @Override + public Set getSupportedThingTypes() { + return SUPPORTED_THING_TYPES; + } + + /** + * This method is called when {@link AbstractDiscoveryService#setBackgroundDiscoveryEnabled(boolean)} + * is called with true as parameter and when the component is being activated + * (see {@link AbstractDiscoveryService#activate()}. + * + * This will also serve to "re-discover" any devices that have changed to a new IP address. + */ + @Override + protected void startBackgroundDiscovery() { + ScheduledFuture backgroundDiscovery = this.backgroundDiscovery; + if (backgroundDiscovery == null || backgroundDiscovery.isCancelled()) { + this.backgroundDiscovery = scheduler.scheduleWithFixedDelay(this::startScan, 1, 60, TimeUnit.MINUTES); + } + } + + @Override + protected void stopBackgroundDiscovery() { + ScheduledFuture backgroundDiscovery = this.backgroundDiscovery; + if (backgroundDiscovery != null && !backgroundDiscovery.isCancelled()) { + backgroundDiscovery.cancel(true); + this.backgroundDiscovery = null; + } + } + + @Override + protected void startScan() { + DatagramSocket dsocket = null; + try { + String broadcastIp = this.mediator.getNetworkAddressService().getConfiguredBroadcastAddress(); + if (broadcastIp != null) { + InetAddress address = InetAddress.getByName(broadcastIp); + RegistrationRequestParam registrationRequestParam = mediator.getRegistrationParams(); + WizRequest request = new WizRequest(WizMethodType.Registration, registrationRequestParam); + request.setId(0); + + byte[] message = this.converter.transformToByteMessage(request); + + // Initialize a datagram packet with data and address + DatagramPacket packet = new DatagramPacket(message, message.length, address, DEFAULT_UDP_PORT); + + // Create a datagram socket, send the packet through it, close it. + // For discovery we will "fire and forget" and let the mediator take care of the + // responses + dsocket = new DatagramSocket(); + dsocket.send(packet); + logger.debug("Broadcast packet to address: {} and port {}", address, DEFAULT_UDP_PORT); + } else { + logger.warn("No broadcast address was configured or discovered! No broadcast sent."); + } + } catch (IllegalStateException e) { + logger.debug("Unable to start background scan: {}", e.getMessage()); + } catch (IOException exception) { + logger.debug("Something wrong happened when broadcasting the packet to port {}... msg: {}", + DEFAULT_UDP_PORT, exception.getMessage()); + } finally { + if (dsocket != null) { + dsocket.close(); + } + } + } + + /** + * Method called by mediator, after receiving a packet from an unknown WiZ device + * + * @param macAddress the mac address from the device. + * @param ipAddress the host address from the device. + */ + public void discoveredLight(final String macAddress, final String ipAddress) { + Map properties = new HashMap<>(2); + properties.put(CONFIG_MAC_ADDRESS, macAddress); + properties.put(CONFIG_IP_ADDRESS, ipAddress); + logger.trace("New device discovered at {} with MAC {}. Requesting configuration info from it.", ipAddress, + macAddress); + + // Assume it is a full color bulb, unless we get confirmation otherwise. + // This will ensure the maximum number of channels will be created so there's no + // missing functionality. + // There's nothing a simple dimmable bulb can do that a full color bulb can't. + // It's easy for a user to ignore or not link anything to a non-working channel, + // but impossible to add a new channel if it's wanted. + // The bulbs will merely ignore or return an error for specific commands they + // cannot carry-out (ie, setting color on a non-color bulb) and continue to + // function as they were before the bad command. + ThingTypeUID thisDeviceType = THING_TYPE_COLOR_BULB; + String thisDeviceLabel = "WiZ Full Color Bulb at " + ipAddress; + ThingUID newThingId = new ThingUID(thisDeviceType, macAddress); + + WizResponse configResponse = getDiscoveredDeviceConfig(ipAddress); + if (configResponse != null) { + SystemConfigResult discoveredDeviceConfig = configResponse.getSystemConfigResults(); + if (discoveredDeviceConfig != null) { + String discoveredModel = discoveredDeviceConfig.moduleName.toUpperCase(); + logger.trace("Returned model from discovered device at {}: {}", ipAddress, discoveredModel); + + // “moduleName”:“ESP10_SOCKET_06” confirmed example module name for Wiz Smart Plug + // Check for "SOCKET" this seems safe based on other naming conventions observed + if (discoveredModel.contains("SOCKET")) { + thisDeviceType = THING_TYPE_SMART_PLUG; + thisDeviceLabel = "WiZ Smart Plug at " + ipAddress; + newThingId = new ThingUID(thisDeviceType, macAddress); + logger.trace("New device appears to be a smart plug and will be given the UUID: {}", newThingId); + + // We'll try to key off "TW" for tunable white + } else if (discoveredModel.contains("TW")) { + thisDeviceType = THING_TYPE_TUNABLE_BULB; + thisDeviceLabel = "WiZ Tunable White Bulb at " + ipAddress; + newThingId = new ThingUID(thisDeviceType, macAddress); + logger.trace("New device appears to be a tunable white bulb and will be given the UUID: {}", + newThingId); + + // Check for "FANDIMS" as in confirmed example ESP03_FANDIMS_31 for Faro Barcelona Smart Fan + } else if (discoveredModel.contains("FANDIMS")) { + thisDeviceType = THING_TYPE_FAN_WITH_DIMMABLE_BULB; + thisDeviceLabel = "WiZ Smart Fan at " + ipAddress; + newThingId = new ThingUID(thisDeviceType, macAddress); + logger.trace("New device appears to be a smart fan and will be given the UUID: {}", newThingId); + + // We key off "RGB" for color bulbs + } else if (!discoveredModel.contains("RGB")) { + thisDeviceType = THING_TYPE_DIMMABLE_BULB; + thisDeviceLabel = "WiZ Dimmable White Bulb at " + ipAddress; + newThingId = new ThingUID(thisDeviceType, macAddress); + logger.trace( + "New device appears not to be either tunable white bulb or full color and will be called a dimmable only bulb and given the UUID: {}", + newThingId); + } + + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(newThingId).withProperties(properties) + .withLabel(thisDeviceLabel).withRepresentationProperty(CONFIG_MAC_ADDRESS).build(); + + this.thingDiscovered(discoveryResult); + } + } else { + logger.trace( + "Couldn't get or couldn't parse configuration information from discovered device. Discovery result will not be created."); + } + } + + private synchronized @Nullable WizResponse getDiscoveredDeviceConfig(final String lightIpAddress) { + DatagramSocket dsocket = null; + try { + WizRequest request = new WizRequest(WizMethodType.GetSystemConfig, null); + request.setId(1); + + byte[] message = this.converter.transformToByteMessage(request); + + // Initialize a datagram packet with data and address + InetAddress address = InetAddress.getByName(lightIpAddress); + DatagramPacket packet = new DatagramPacket(message, message.length, address, DEFAULT_UDP_PORT); + + // Create a datagram socket, send the packet through it, close it. + dsocket = new DatagramSocket(); + dsocket.send(packet); + logger.debug("Sent packet to address: {} and port {}", address, DEFAULT_UDP_PORT); + + byte[] responseMessage = new byte[1024]; + packet = new DatagramPacket(responseMessage, responseMessage.length); + dsocket.receive(packet); + + return converter.transformResponsePacket(packet); + } catch (SocketTimeoutException e) { + logger.trace("Socket timeout after sending command; no response from {} within 500ms", lightIpAddress); + } catch (IOException exception) { + logger.debug("Something wrong happened when sending the packet to address: {} and port {}... msg: {}", + lightIpAddress, DEFAULT_UDP_PORT, exception.getMessage()); + } finally { + if (dsocket != null) { + dsocket.close(); + } + } + return null; + } + + // SETTERS AND GETTERS + /** + * Gets the {@link WizMediator} of this binding. + * + * @return {@link WizMediator}. + */ + public WizMediator getMediator() { + return this.mediator; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorRequestParam.java new file mode 100644 index 0000000000000..b2f7bb41e4af3 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorRequestParam.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.wiz.internal.utils.WizColorConverter; +import org.openhab.core.library.types.HSBType; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents Color Request Param + * + * Outgoing JSON should look like this: + * + * {"id": 24, "method": "setPilot", "params": {"r": 0, "g": 230, "b": 80, "w": + * 130, "c": 0, "dimming": 12}} + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public class ColorRequestParam extends DimmingRequestParam { + @Expose + private int r; // red 0-255 + @Expose + private int g; // green 0-255 + @Expose + private int b; // blue 0-255 + @Expose + private int w; // warm white LED's 0-255 + @Expose + private int c; // cool white LED's 0-255 + @Expose(serialize = false, deserialize = false) + private WizColorConverter colorConverter = new WizColorConverter(); + + public ColorRequestParam(int r, int g, int b, int w, int c, int dimming) { + super(dimming); + this.r = r; + this.g = g; + this.b = b; + this.w = w; + this.c = c; + } + + public ColorRequestParam(HSBType hsb) { + super(hsb.getBrightness().intValue()); + int rgbw[] = colorConverter.hsbToRgbw(hsb); + this.r = rgbw[0]; + this.g = rgbw[1]; + this.b = rgbw[2]; + this.w = rgbw[3]; + this.c = 0; + } + + public int getB() { + return b; + } + + public void setB(int b) { + this.b = b; + } + + public int getG() { + return g; + } + + public void setG(int g) { + this.g = g; + } + + public int getR() { + return r; + } + + public void setR(int r) { + this.r = r; + } + + public int getW() { + return w; + } + + public void setW(int w) { + this.w = w; + } + + public int getC() { + return c; + } + + public void setC(int c) { + this.c = c; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorTemperatureRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorTemperatureRequestParam.java new file mode 100644 index 0000000000000..ba0dd4faf5063 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ColorTemperatureRequestParam.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents Color Request Param + * + * The outgoing JSON should look like this: + * + * {"id": 24, "method": "setPilot", "params": {"temp": 3000}} + * + * @author Alexander Seeliger - Initial contribution + * + */ +@NonNullByDefault +public class ColorTemperatureRequestParam implements Param { + @Expose + private int temp; + + public ColorTemperatureRequestParam(int temp) { + this.temp = temp; + } + + public int getColorTemperature() { + return temp; + } + + public void setColorTemperature(int temp) { + this.temp = temp; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/DimmingRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/DimmingRequestParam.java new file mode 100644 index 0000000000000..64359cf059cb4 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/DimmingRequestParam.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents Dimming Request Param + * + * The outgoing JSON should look like this: + * + * {"id": 24, "method": "setPilot", "params": {"dimming": 10}} + * + * NOTE: Dimming cannot be set below 10%. Sending a command with a value of less + * than 10 will cause the bulb to reply with an error. + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public class DimmingRequestParam extends StateRequestParam { + @Expose + private int dimming; + + public DimmingRequestParam(int dimming) { + super(true); + setDimming(dimming); + } + + public int getDimming() { + return dimming; + } + + public void setDimming(int dimming) { + if (dimming <= 10) { + dimming = 10; + } + if (dimming >= 100) { + dimming = 100; + } + this.dimming = dimming; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ErrorResponseResult.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ErrorResponseResult.java new file mode 100644 index 0000000000000..d7d355ab4fdd8 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ErrorResponseResult.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents the "result" of one WiZ Response "results" are + * returned from registration, pulse, setPilot, and (presumably) setSysConfig + * commands + * + * Incoming JSON might look like this: + * + * {"env":"pro","error":{"code":-32700,"message":"Parse error"}} + * + * @author Sara Geleskie - Initial contribution + * + */ +@NonNullByDefault +public class ErrorResponseResult { + @Expose + public int code; + @Expose + public @Nullable String message; +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanModeRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanModeRequestParam.java new file mode 100644 index 0000000000000..b7f3185c05994 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanModeRequestParam.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents Fan Reverse Request Param + * + * @author Cody Cutrer - Initial Contribution + */ +@NonNullByDefault +public class FanModeRequestParam implements Param { + @Expose + private int fanMode; // true = 1, false = 0 + + public FanModeRequestParam(int fanMode) { + this.fanMode = fanMode; + } + + public int getFanMode() { + return fanMode; + } + + public void setFanMode(int fanMode) { + this.fanMode = fanMode; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanReverseRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanReverseRequestParam.java new file mode 100644 index 0000000000000..18788f33f1f26 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanReverseRequestParam.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents Fan Reverse Request Param + * + * @author Cody Cutrer - Initial Contribution + */ +@NonNullByDefault +public class FanReverseRequestParam implements Param { + @Expose + private int fanRevrs; // true = 1, false = 0 + + public FanReverseRequestParam(int reverse) { + this.fanRevrs = reverse; + } + + public int getReverse() { + return fanRevrs; + } + + public void setReverse(int reverse) { + this.fanRevrs = reverse; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanSpeedRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanSpeedRequestParam.java new file mode 100644 index 0000000000000..1c090420a09b5 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanSpeedRequestParam.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents Fan Speed Request Param + * + * @author Cody Cutrer - Initial Contribution + */ +@NonNullByDefault +public class FanSpeedRequestParam implements Param { + @Expose + private int fanSpeed; // 0-6 + + public FanSpeedRequestParam(int fanSpeed) { + this.fanSpeed = fanSpeed; + } + + public int getFanSpeed() { + return fanSpeed; + } + + public void setFanSpeed(int fanSpeed) { + this.fanSpeed = fanSpeed; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanStateRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanStateRequestParam.java new file mode 100644 index 0000000000000..049e48a9564db --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FanStateRequestParam.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents Fan State Request Param + * + * @author Stefan Fussenegger - Initial Contribution + */ +@NonNullByDefault +public class FanStateRequestParam implements Param { + @Expose + private int fanState; // true = 1, false = 0 + + public FanStateRequestParam(int fanState) { + this.fanState = fanState; + } + + public int getFanState() { + return fanState; + } + + public void setFanState(int fanState) { + this.fanState = fanState; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FirstBeatResponseParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FirstBeatResponseParam.java new file mode 100644 index 0000000000000..1631dc626253e --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/FirstBeatResponseParam.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import static org.openhab.binding.wiz.internal.WizBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents the "params" returned in a "firstBeat" + * + * The incoming JSON looks like this: + * + * {"method": "firstBeat", "id": 0, "env": "pro", "params": {"mac": "theBulbMacAddress", + * "homeId": xxxxxx, "fwVersion": "1.15.2"}} + * + * @author Sara Geleskie Damiano - Initial contribution + */ +@NonNullByDefault +public class FirstBeatResponseParam { + // The MAC address the response is coming from + @Expose + public String mac = ""; + // Home ID of the bulb + @Expose(serialize = false) + public int homeId; + // Firmware version of the bulb + @Expose + public String fwVersion = LAST_KNOWN_FIRMWARE_VERSION; +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ModelConfigResult.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ModelConfigResult.java new file mode 100644 index 0000000000000..d52114d54c622 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/ModelConfigResult.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents the "result" of one request for a bulb's model + * configuration + * + * { + * "method": "getModelConfig", "id": 1, "env":"pro", + * "result": { + * "ps":2, "pwmFreq":1000, "pwmRes":11, "pwmRange":[0,100], + * "wcr":20, "nowc":1, "cctRange": [1800,2100,2100,2100], + * "renderFactor": [120,255,255,255,0,0,20,90,255,255], "hasCctTable": 6, + * "wizc1": { + * "mode": [0,0,0,0,0,0,2100], + * "opts": { "dim": 100 } + * }, + * "wizc2": { + * "mode": [0,0,0,0,0,0,2100], + * "opts": { "dim": 50 } + * }, + * "drvIface":4, + * "i2cDrv": [ + * { + * "chip": "BP5758D", + * "addr": 255, + * "freq": 200, + * "curr": [30,30,30,36,36], + * "output":[3,2,1,4,5] + * }, { + * "chip": "NONE", + * "addr": 0, + * "freq": 0, + * "curr": [0,0,0,0,0], + * "output": [0,0,0,0,0] + * }, { + * "chip": "NONE", + * "addr": 0, + * "freq": 0, + * "curr": [0,0,0,0,0], + * "output":[0,0,0,0,0] + * } + * ] + * } + * } + * + * @author Cody Cutrer - Initial contribution + * + */ +@NonNullByDefault +public class ModelConfigResult { + @Expose + public int[] cctRange = {}; +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/Param.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/Param.java new file mode 100644 index 0000000000000..f086955880119 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/Param.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This POJO represents an abstract Request Param + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public interface Param { + +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/PulseRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/PulseRequestParam.java new file mode 100644 index 0000000000000..aa146a7554366 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/PulseRequestParam.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents Pulse Request Param + * + * The outgoing JSON should look like this: + * + * {"id": 22, "method": "pulse", "params": {"delta": 30, "duration": 900}} + * + * @author Sara Geleskie Damiano - Initial contribution + * + */ +@NonNullByDefault +public class PulseRequestParam implements Param { + @Expose + private int delta; + @Expose + private int duration; + + public PulseRequestParam(int delta, int duration) { + this.delta = delta; + this.duration = duration; + } + + public int getDelta() { + return delta; + } + + public void setDelta(int delta) { + this.delta = delta; + } + + public int getDuration() { + return duration; + } + + public void setDuration(int duration) { + this.delta = duration; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/RegistrationRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/RegistrationRequestParam.java new file mode 100644 index 0000000000000..e7dd90b7b8b86 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/RegistrationRequestParam.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents Registration request param + * + * The outgoing JSON should look like this: + * + * {"id": 22, "method": "registration", "params": {"phoneIp": "10.0.0.xx", + * "register": true, "homeId": xxx, "phoneMac": "xxx"}} + * + * NOTE: This can be sent directly to a single bulb or as a UDP broadcast. When + * sent as a broadcast, all bulbs in the network should respond. + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public class RegistrationRequestParam implements Param { + @Expose + private String phoneIp; + @Expose + private boolean register; + // NOTE: We are NOT exposing the Home id for serialization because it's not + // necessary and it's a PITA to find it + @Expose(serialize = false) + private int homeId; + @Expose + private String phoneMac; + + public RegistrationRequestParam(String phoneIp, boolean register, int homeId, String phoneMac) { + this.phoneIp = phoneIp; + this.register = register; + this.homeId = homeId; + this.phoneMac = phoneMac; + } + + public String getPhoneIp() { + return phoneIp; + } + + public void setPhoneIp(String phoneIp) { + this.phoneIp = phoneIp; + } + + public boolean getRegister() { + return register; + } + + public void setRegister(boolean register) { + this.register = register; + } + + public int getHomeId() { + return homeId; + } + + public void setHomeId(int homeId) { + this.homeId = homeId; + } + + public String getPhoneMac() { + return phoneMac; + } + + public void setPhoneMac(String phoneMac) { + this.phoneMac = phoneMac; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SceneRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SceneRequestParam.java new file mode 100644 index 0000000000000..d0c5aa7301188 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SceneRequestParam.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents one Scene Request Param + * + * The outgoing JSON should look like this: + * + * {"id": 22, "method": "setPilot", "params": {"sceneId": 3}} * + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public class SceneRequestParam extends StateRequestParam { + @Expose + private int sceneId; + + public SceneRequestParam(int sceneId) { + super(true); + this.sceneId = sceneId; + } + + public int getSceneId() { + return sceneId; + } + + public void setSceneId(int sceneId) { + this.sceneId = sceneId; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SpeedRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SpeedRequestParam.java new file mode 100644 index 0000000000000..568b9307a0367 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SpeedRequestParam.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents Speed Request Param + * + * The outgoing JSON should look like this: + * + * {"id": 23, "method": "setPilot", "params": {"sceneId":3,"speed": 20}} + * + * NOTE: A sceneId MUST also be specified in the request or the bulb will reply + * with an error. + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public class SpeedRequestParam extends SceneRequestParam { + @Expose + private int speed; + + public SpeedRequestParam(int sceneId, int speed) { + super(sceneId); + this.speed = speed; + } + + public int getSpeed() { + return speed; + } + + public void setSpeed(int speed) { + this.speed = speed; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/StateRequestParam.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/StateRequestParam.java new file mode 100644 index 0000000000000..390bbc2626e09 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/StateRequestParam.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents State Request Param + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public class StateRequestParam implements Param { + @Expose + private boolean state; // true = ON, false = OFF + + public StateRequestParam(boolean state) { + this.state = state; + } + + public boolean getState() { + return state; + } + + public void setState(boolean state) { + this.state = state; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SystemConfigResult.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SystemConfigResult.java new file mode 100644 index 0000000000000..638063cba044a --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/SystemConfigResult.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import static org.openhab.binding.wiz.internal.WizBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents the "result" of one request for a bulb's system + * configuration I assume the same packet could be used as the param of a + * 'setSystemConfig' request, but I'm not willing to risk ruining my bulbs by + * trying it. + * + * The incoming JSON looks like this: + * + * {"method": "getSystemConfig", "id": 22, "env": "pro", "result": {"mac": + * "theBulbMacAddress", "homeId": xxxxxx, "roomId": xxxxxx, "homeLock": false, + * "pairingLock": false, "typeId": 0, "moduleName": "ESP01_SHRGB1C_31", + * "fwVersion": "1.15.2", "groupId": 0, "drvConf":[33,1]}} + * + * @author Sara Geleskie Damiano - Initial contribution + * + */ +@NonNullByDefault +public class SystemConfigResult { + // The MAC address the response is coming from + @Expose + public String mac = ""; + // Home ID of the bulb + @Expose + public int homeId; + // The ID of room the bulb is assigned to + @Expose + public int roomId; + // Not sure what the home lock is + @Expose + public boolean homeLock; + // Also not sure about the pairing lock + @Expose + public boolean pairingLock; + // Obviously a type ID + // The value is 0 for both BR30 and A19 full color bulbs + @Expose + public int typeId; + // The module name + // The value is "ESP01_SHRGB1C_31" for both BR30 and A19 full color bulbs + @Expose + public String moduleName = EXPECTED_MODULE_NAME; + // Firmware version of the bulb + @Expose + public String fwVersion = LAST_KNOWN_FIRMWARE_VERSION; + // The ID of group the bulb is assigned to + // I don't know how to group bulbs, all of mine return 0 + @Expose + public int groupId; + // Not sure what the numbers mean + // For a full color A19 I get [33,1] + // For a full coloer BR30 I get [37,1] + @Expose + public int[] drvConf = {}; +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizRequest.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizRequest.java new file mode 100644 index 0000000000000..c98d76fba16c4 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizRequest.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.wiz.internal.enums.WizMethodType; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents one WiZ UDP Request. + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public class WizRequest { + @Expose + private int id; + + @Expose + private WizMethodType method; + + @Expose(serialize = false, deserialize = false) + private String methodName; + + @Expose(deserialize = false) + private @Nullable Param params; + + /** + * Default constructor. + * + * @param type the {@link WizMethodType} + * @param params {@link Param} + */ + public WizRequest(final WizMethodType method, final @Nullable Param params) { + this.method = method; + this.methodName = method.getMethodName(); + this.params = params; + } + + public @Nullable Param getParams() { + return this.params; + } + + public void setParams(final Param params) { + this.params = params; + } + + public WizMethodType getMethod() { + return this.method; + } + + public void setMethod(final WizMethodType method) { + this.method = method; + this.methodName = method.getMethodName(); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizResponse.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizResponse.java new file mode 100644 index 0000000000000..1801c8b2989b6 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizResponse.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.wiz.internal.enums.WizMethodType; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents one WiZ Response + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public class WizResponse { + + // The IP address we're coming from + @Expose(deserialize = true) + private String wizResponseIpAddress = ""; + + // Increasing numeric value. + // Bulb doesn't seem to care if it receives the same id multiple time + // or commands with lower numbers after higher ones. + @Expose + private int id; + // Not sure what env is - value always seems to be "pro" + @Expose + private @Nullable String env; + + // An error response + @Expose + private @Nullable ErrorResponseResult methodError; + + // The method being used - see the enum for details + // We're setting this to "unknown" + @Expose + private WizMethodType method = WizMethodType.UnknownMethod; + + // The MAC address the response is coming from + @Expose + private String mac = ""; + + // Whether or not a command succeeded (if the response is from a command) + @Expose + private boolean success = false; + + // The system configuration result, if present + @Expose + private @Nullable SystemConfigResult systemConfigResult; + + // The modeul configuration result, if present + @Expose + private @Nullable ModelConfigResult modelConfigResult; + + // The parameters or result of a command/response + // A "result" is generally returned when solicited using a set/get method and a + // "params" is retuned with an unsolicited sync/heartbeat. The result returned + // from a get method is generally identical to the params returned in the + // heartbeat. + @Expose + private @Nullable WizSyncState params; + + /** + * Setters and Getters + */ + + public @Nullable SystemConfigResult getSystemConfigResults() { + return this.systemConfigResult; + } + + public void setSystemConfigResult(final SystemConfigResult configResult) { + this.systemConfigResult = configResult; + } + + public @Nullable ModelConfigResult getModelConfigResults() { + return this.modelConfigResult; + } + + public void setModelConfigResult(final ModelConfigResult configResult) { + this.modelConfigResult = configResult; + } + + public boolean getResultSuccess() { + return this.success; + } + + public void setResultSucess(final boolean success) { + this.success = success; + } + + public @Nullable WizSyncState getSyncState() { + return this.params; + } + + public void setSyncParams(final WizSyncState params) { + this.params = params; + } + + public String getWizResponseMacAddress() { + return this.mac; + } + + public void setWizResponseMacAddress(final String wizResponseMacAddress) { + this.mac = wizResponseMacAddress; + } + + public String getWizResponseIpAddress() { + return this.wizResponseIpAddress; + } + + public void setWizResponseIpAddress(final String wizResponseIpAddress) { + this.wizResponseIpAddress = wizResponseIpAddress; + } + + public int getId() { + return id; + } + + public void setId(final int id) { + this.id = id; + } + + public @Nullable WizMethodType getMethod() { + return method; + } + + public void setMethod(final WizMethodType method) { + this.method = method; + } + + public @Nullable String getEnv() { + return env; + } + + public void setEnv(final String env) { + this.env = env; + } + + public @Nullable ErrorResponseResult getError() { + return methodError; + } + + public void setError(ErrorResponseResult error) { + this.methodError = error; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizSyncState.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizSyncState.java new file mode 100644 index 0000000000000..de6421b8dfe0a --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/entities/WizSyncState.java @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.entities; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.wiz.internal.enums.WizColorMode; +import org.openhab.binding.wiz.internal.utils.WizColorConverter; +import org.openhab.core.library.types.HSBType; + +import com.google.gson.annotations.Expose; + +/** + * This POJO represents the "params" of the current state of a WiZ bulb. + * These are retruned as the "params" in getPilot, sync, and heartbeat packets + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public class WizSyncState { + // The MAC address the response is coming from + @Expose + public String mac = ""; + + // The current color mode of the bulb + // We will assume by default that it's a single color bulb + @Expose(serialize = false, deserialize = false) + public WizColorMode colorMode = WizColorMode.SingleColorMode; + @Expose(serialize = false, deserialize = false) + private WizColorConverter colorConverter = new WizColorConverter(); + + /* + * Extra Information only in 'hb' params + */ + // Not sure exactly what this means, seems to be a boolean + // I believe the bulb communicates with the WiZ servers via MQTT + @Expose + public int mqttCd; + + /* + * Bulb state information - not all fields are populated + */ + + // The bulb's WiFi signal strength + @Expose + public int rssi; + // The overall state of the bulb - on/off + @Expose + public boolean state; + // The numeric identifier for a preset lighting mode + @Expose + public int sceneId; + // Unknown - not seen by SRGD + @Expose + public boolean play; + // The speed of color changes in dynamic lighting modes + @Expose + public int speed; + // Strength of the red channel (0-255) + @Expose + public int r; + // Strength of the green channel (0-255) + @Expose + public int g; + // Strength of the blue channel (0-255) + @Expose + public int b; + // Intensity of the cool white channel (0-255) + @Expose + public int c; + // Intensity of the warm white channel (0-255) + @Expose + public int w; + // Dimming percent (10-100) + @Expose + public int dimming; + // Color temperature - sent in place of r/g/b/c/w + // If temperatures are sent, color LED's are not in use + @Expose + public int temp; + // Indicates if the light mode is applied following a pre-set "rhythm" + @Expose + public int schdPsetId; + + @Expose + public int fanState; + @Expose + public int fanSpeed; + @Expose + public int fanMode; + @Expose + public int fanRevrs; + + public WizColorMode getColorMode() { + if (r != 0 || g != 0 || b != 0) { + return WizColorMode.RGBMode; + } else if (temp != 0) { + return WizColorMode.CTMode; + } else { + return WizColorMode.SingleColorMode; + } + } + + public HSBType getHSBColor() { + if (getColorMode() == WizColorMode.RGBMode) { + HSBType newColor = colorConverter.rgbwDimmingToHSB(r, g, b, w, dimming); + // NOTE: The WiZ bulbs do not use the cool white LED's in full color mode. + return newColor; + } else { + // If a rgb color isn't returned, simply call it simply white. + // Do not attempt any conversions given a color temperature. + return HSBType.WHITE; + } + } + + public void setHSBColor(HSBType hsb) { + this.dimming = hsb.getBrightness().intValue(); + int rgbw[] = colorConverter.hsbToRgbw(hsb); + this.r = rgbw[0]; + this.g = rgbw[1]; + this.b = rgbw[2]; + this.w = rgbw[3]; + this.c = 0; + } + + public int getTemperature() { + return temp; + } + + public void setTemperature(int temp) { + this.temp = temp; + } + + public int getDimming() { + return this.dimming; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizColorMode.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizColorMode.java new file mode 100644 index 0000000000000..7ce62bd743829 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizColorMode.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This enum represents the possible color modes for WiZ bulbs. + * The bulbs come in three types - full color with tunable white, + * tunable white, and dimmable with set white. The full color and + * tunable white bulbs operate EITHER in color mode OR in tunable + * white mode. + * + * @author Sara Geleskie Damiano - Initial contribution + * + */ +@NonNullByDefault +public enum WizColorMode { + // Full color mode + RGBMode("Full Color"), + // Tunable white (color temperature) mode + CTMode("Tunable White"), + // Dimming only + SingleColorMode("Dimming Only"); + + private String colorMode; + + private WizColorMode(final String colorMode) { + this.colorMode = colorMode; + } + + /** + * Gets the colorMode name for request colorMode + * + * @return the colorMode name + */ + public String getColorMode() { + return colorMode; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizLightMode.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizLightMode.java new file mode 100644 index 0000000000000..a5f29fb379dbd --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizLightMode.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.enums; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This enum represents the possible scene modes. + * + * @author Sara Geleskie Damiano - Initial contribution + * + */ +@NonNullByDefault +public enum WizLightMode { + Ocean("Ocean", 1), + Romance("Romance", 2), + Sunset("Sunset", 3), + Party("Party", 4), + Fireplace("Fireplace", 5), + CozyWhite("Cozy White", 6), + Forest("Forest", 7), + PastelColors("Pastel Colors", 8), + Wakeup("Wakeup", 9), + BedTime("Bed Time", 10), + WarmWhite("Warm White", 11), + Daylight("Daylight", 12), + CoolWhite("Cool White", 13), + NightLight("Night Light", 14), + Focus("Focus", 15), + Relax("Relax", 16), + TrueColors("True Colors", 17), + TVTime("TV Time", 18), + PlantGrowth("Plant Growth", 19), + Spring("Spring", 20), + Summer("Summer", 21), + Fall("Fall", 22), + DeepDive("Deep Dive", 23), + Jungle("Jungle", 24), + Mojito("Mojito", 25), + Club("Club", 26), + Christmas("Christmas", 27), + Halloween("Halloween", 28), + Candlelight("Candlelight", 29), + GoldenWhite("Golden White", 30), + Pulse("Pulse", 31), + Steampunk("Steampunk", 32); + + private String colorModeName; + private int sceneId; + + private WizLightMode(final String colorModeName, final int sceneId) { + this.colorModeName = colorModeName; + this.sceneId = sceneId; + } + + /** + * Gets the colorMode name for request colorMode + * + * @return the colorMode name + */ + public String getColorMode() { + return colorModeName; + } + + public int getSceneId() { + return sceneId; + } + + private static final Map LIGHT_MODE_MAP_BY_ID; + private static final Map LIGHT_MODE_MAP_BY_NAME; + + static { + LIGHT_MODE_MAP_BY_ID = new HashMap(); + LIGHT_MODE_MAP_BY_NAME = new HashMap(); + + for (WizLightMode v : WizLightMode.values()) { + LIGHT_MODE_MAP_BY_ID.put(v.sceneId, v); + LIGHT_MODE_MAP_BY_NAME.put(v.colorModeName.toLowerCase().replaceAll("\\W+", ""), v); + } + } + + public static @Nullable WizLightMode fromSceneId(int id) { + WizLightMode r = null; + if (id > 0 && id < 33) { + r = LIGHT_MODE_MAP_BY_ID.get(id); + } + return r; + } + + public static @Nullable WizLightMode fromSceneName(String name) { + WizLightMode r = null; + if (!name.isEmpty()) { + r = LIGHT_MODE_MAP_BY_NAME.get(name.toLowerCase().replaceAll("\\W+", "")); + } + return r; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizMethodType.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizMethodType.java new file mode 100644 index 0000000000000..e8b05a1d56916 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizMethodType.java @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * This enum represents the available WiZ Request Methods + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public enum WizMethodType { + /** + * Registration - used to "register" with the bulb: This notifies the bult that + * it you want it to send you heartbeat sync packets. + * NOTE: The homeId value is optional, other values are required + * NOTE: There is no need to register before calling other methods. + * Example Request: + * {"method": "registration", "id": 1, "params": + * {"phoneIp": "10.0.0.xxx", "register": true, "homeId": xxxxxx, "phoneMac": "macOfopenHAB"}} + * Example Response: + * {"method": "registration", "id": 1, "env": "pro", "result": + * {"mac": "macOfopenHAB", "success": true}} + */ + @SerializedName("registration") + Registration("registration"), + /** + * Pulse - tells the bulb to briely change brightness (by delta % for duration ms) + * Example Request: + * {"method": "pulse", "id": 22, "params": {"delta": -30, "duration": 900}} + * Example Response: + * {"method": "pulse", "id": 22, "env": "pro", "result": {"success": true}} + */ + @SerializedName("pulse") + Pulse("pulse"), + /** + * setPilot - used to tell the bulb to change color/temp/state + * Example Request: + * {"method": "setPilot", "id": 24, "params": {"state": 1}} + * Example Response: + * {"method": "setPilot", "id": 24, "env": "pro", "result": {"success": true}} + */ + @SerializedName("setPilot") + SetPilot("setPilot"), + /** + * getPilot - gets the current bulb state - no paramters need to be included + * Example Request: + * {"method": "getPilot", "id": 24} + * Example Response: + * {"method": "getPilot", "id": 22, "env": "pro", "result": {"mac": + * "a8bb508f570a", "rssi":-76, "state": true, "sceneId": 0, "temp": 2700, + * "dimming": 42, "schdPsetId": 5}} + */ + @SerializedName("getPilot") + GetPilot("getPilot"), + /** + * syncPilot - sent by the bulb as heart-beats + * Example: + * {"method": "syncPilot", "id": 218, "env": "pro", "params": + * { "mac": "theBulbMacAddress", "rssi": -72, "src": "udp", "state": true, "sceneId": 0, + * "temp": 3362, "dimming": 69, "schdPsetId": 5}} + * Another Example: + * {"method": "syncPilot", "id": 219, "env": "pro", "params": + * { "mac": "theBulbMacAddress", "rssi": -72, "src": "hb", "mqttCd": 0, "state": true, + * "sceneId": 0, "temp": 3362, "dimming": 69, "schdPsetId": 5}} + */ + @SerializedName("syncPilot") + SyncPilot("syncPilot"), + /** + * getModelConfig - gets more details on the bulb + */ + @SerializedName("getModelConfig") + GetModelConfig("getModelConfig"), + /** + * getSystemConfig - gets the current system configuration - no paramters need + * to be included + * Example Request: + * {"method": "getSystemConfig", "id": 24} + * Example Response: + * {"method": "getSystemConfig", "id": 22, "env": "pro", + * "result": {"mac": "theBulbMacAddress", "homeId": xxxxxx, "roomId": xxxxxx, + * "homeLock": false, "pairingLock": false, "typeId": 0, "moduleName": + * "ESP01_SHRGB1C_31", "fwVersion": "1.15.2", "groupId": 0, "drvConf":[33,1]}} + */ + @SerializedName("getSystemConfig") + GetSystemConfig("getSystemConfig"), + /** + * setSystemConfig - presumably sets up the system + * I have NOT attempted to call this method + */ + @SerializedName("setSystemConfig") + SetSystemConfig("setSystemConfig"), + /** + * getWifiConfig - gets the current wifi configuration - no paramters need to be + * included + * Example Request: + * {"id": 22, "method": "getWifiConfig"} + * Example Response: + * {"method": "getWifiConfig", "id": 22, "env": "pro", "result": + * {:["encryptedString"]} + */ + @SerializedName("getWifiConfig") + GetWifiConfig("getWifiConfig"), + /** + * setWifiConfig - presumably sets up the system I have NOT attempted to use this method + */ + @SerializedName("setWifiConfig") + SetWifiConfig("setWifiConfig"), + /** + * firstBeat - set by a bulb upon power up + * Example: + * {"method": "firstBeat", "id": 0, "env": "pro", "params": + * {"mac": "theBulbMacAddress", "homeId": xxxxxx, "fwVersion": "1.15.2"}} + */ + @SerializedName("firstBeat") + FirstBeat("firstBeat"), + /** + * Unknown - using as a default for inproperly received responses + */ + UnknownMethod("unknownMethod"); + + private final String methodName; + + private WizMethodType(final String methodName) { + this.methodName = methodName; + } + + /** + * Gets the method name for request method + * + * @return the method name + */ + public String getMethodName() { + return methodName; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizModuleType.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizModuleType.java new file mode 100644 index 0000000000000..294d151b6ffc0 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/enums/WizModuleType.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.enums; + +import static org.openhab.binding.wiz.internal.WizBindingConstants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.ThingTypeUID; + +/** + * This enum represents the possible scene modes. + * + * @author Sara Geleskie Damiano - Initial contribution + * + */ +@NonNullByDefault +public enum WizModuleType { + FullColorWifi("ESP01_SHRGB1C_31", THING_TYPE_COLOR_BULB), + TunableWhiteWifi("ESP56_SHTW3_01", THING_TYPE_TUNABLE_BULB), + DimmableWifi("TBD", THING_TYPE_DIMMABLE_BULB), + SmartPlug("TBD", THING_TYPE_SMART_PLUG); + + private final String moduleName; + private final ThingTypeUID thingTypeUID; + + private WizModuleType(final String moduleName, final ThingTypeUID thingTypeUID) { + this.moduleName = moduleName; + this.thingTypeUID = thingTypeUID; + } + + /** + * Gets the colorMode name for request colorMode + * + * @return the colorMode name + */ + public String getModuleName() { + return moduleName; + } + + private static final Map MODULE_NAME_MAP; + static { + MODULE_NAME_MAP = new HashMap(); + for (WizModuleType v : WizModuleType.values()) { + MODULE_NAME_MAP.put(v.moduleName, v.thingTypeUID); + } + } + + public static @Nullable ThingTypeUID getThingTypeUIDFromModuleName(String moduleName) { + return MODULE_NAME_MAP.get(moduleName); + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizHandler.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizHandler.java new file mode 100644 index 0000000000000..1b91a47ebf7f0 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizHandler.java @@ -0,0 +1,882 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.handler; + +import static org.openhab.binding.wiz.internal.WizBindingConstants.*; +import static org.openhab.core.thing.Thing.*; + +import java.io.IOException; +import java.math.BigDecimal; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketTimeoutException; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.wiz.internal.WizStateDescriptionProvider; +import org.openhab.binding.wiz.internal.config.WizDeviceConfiguration; +import org.openhab.binding.wiz.internal.entities.ColorRequestParam; +import org.openhab.binding.wiz.internal.entities.ColorTemperatureRequestParam; +import org.openhab.binding.wiz.internal.entities.DimmingRequestParam; +import org.openhab.binding.wiz.internal.entities.FanModeRequestParam; +import org.openhab.binding.wiz.internal.entities.FanReverseRequestParam; +import org.openhab.binding.wiz.internal.entities.FanSpeedRequestParam; +import org.openhab.binding.wiz.internal.entities.FanStateRequestParam; +import org.openhab.binding.wiz.internal.entities.ModelConfigResult; +import org.openhab.binding.wiz.internal.entities.Param; +import org.openhab.binding.wiz.internal.entities.RegistrationRequestParam; +import org.openhab.binding.wiz.internal.entities.SceneRequestParam; +import org.openhab.binding.wiz.internal.entities.SpeedRequestParam; +import org.openhab.binding.wiz.internal.entities.StateRequestParam; +import org.openhab.binding.wiz.internal.entities.SystemConfigResult; +import org.openhab.binding.wiz.internal.entities.WizRequest; +import org.openhab.binding.wiz.internal.entities.WizResponse; +import org.openhab.binding.wiz.internal.entities.WizSyncState; +import org.openhab.binding.wiz.internal.enums.WizLightMode; +import org.openhab.binding.wiz.internal.enums.WizMethodType; +import org.openhab.binding.wiz.internal.utils.ValidationUtils; +import org.openhab.binding.wiz.internal.utils.WizPacketConverter; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; +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.ThingStatusInfo; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragmentBuilder; +import org.openhab.core.types.UnDefType; +import org.openhab.core.util.ColorUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link WizHandler} is responsible for handling commands, which + * are sent to one of the channels. + * + * @author Sriram Balakrishnan - Initial contribution + */ +@NonNullByDefault +public class WizHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(WizHandler.class); + + private @NonNullByDefault({}) WizDeviceConfiguration config; + private @Nullable RegistrationRequestParam registrationRequestParam; + private int homeId; + + private WizSyncState mostRecentState; + + private final WizPacketConverter converter = new WizPacketConverter(); + private final WizStateDescriptionProvider stateDescriptionProvider; + private final TimeZoneProvider timeZoneProvider; + private final ChannelUID colorTempChannelUID; + private @Nullable ScheduledFuture keepAliveJob; + private long latestUpdate = -1; + private long latestOfflineRefresh = -1; + private int requestId = 0; + private final boolean isFan; + private final boolean isFanOnly; + private int minColorTemp = MIN_COLOR_TEMPERATURE; + private int maxColorTemp = MAX_COLOR_TEMPERATURE; + + private volatile boolean disposed; + private volatile boolean fullyInitialized; + + /** + * Default constructor. + * + * @param thing the thing of the handler. + * @param stateDescriptionProvider A state description provider + */ + public WizHandler(final Thing thing, final WizMediator mediator, + WizStateDescriptionProvider stateDescriptionProvider, TimeZoneProvider timeZoneProvider) { + super(thing); + try { + registrationRequestParam = mediator.getRegistrationParams(); + } catch (IllegalStateException e) { + registrationRequestParam = null; + } + this.stateDescriptionProvider = stateDescriptionProvider; + this.timeZoneProvider = timeZoneProvider; + this.mostRecentState = new WizSyncState(); + this.isFan = thing.getThingTypeUID().equals(THING_TYPE_FAN) + || thing.getThingTypeUID().equals(THING_TYPE_FAN_WITH_DIMMABLE_BULB); + this.isFanOnly = thing.getThingTypeUID().equals(THING_TYPE_FAN); + colorTempChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_TEMPERATURE_ABS); + fullyInitialized = false; + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + if (hasConfigurationError() || disposed || !fullyInitialized) { + logger.debug( + "[{}] WiZ handler for device {} received command {} on channel {} but is not yet prepared to handle it.", + config.ipAddress, config.macAddress, command, channelUID); + return; + } + + if (command instanceof RefreshType) { + long now = System.currentTimeMillis(); + long timePassedFromLastUpdateInSeconds = (now - latestUpdate) / 1000; + // Be patient... + if (latestUpdate < 0 || timePassedFromLastUpdateInSeconds > 5) { + getPilot(); + } + return; + } + + if (isFanOnly || (isFan && CHANNEL_GROUP_FAN.equals(channelUID.getGroupId()))) { + handleFanCommand(channelUID.getIdWithoutGroup(), command); + } else if (!isFan || (isFan && CHANNEL_GROUP_LIGHT.equals(channelUID.getGroupId()))) { + handleLightCommand(channelUID.getIdWithoutGroup(), command); + } + } + + private void handleLightCommand(final String channelId, final Command command) { + switch (channelId) { + case CHANNEL_COLOR: + if (command instanceof HSBType hsbCommand) { + handleHSBCommand(hsbCommand); + } else if (command instanceof PercentType percentCommand) { + handlePercentCommand(percentCommand); + } else if (command instanceof OnOffType onOffCommand) { + handleOnOffCommand(onOffCommand); + } else if (command instanceof IncreaseDecreaseType) { + handleIncreaseDecreaseCommand(command == IncreaseDecreaseType.INCREASE); + } + break; + case CHANNEL_TEMPERATURE: + if (command instanceof PercentType percentCommand) { + handleTemperatureCommand(percentToColorTemp(percentCommand)); + } else if (command instanceof OnOffType onOffCommand) { + handleTemperatureCommand( + percentToColorTemp(onOffCommand == OnOffType.ON ? PercentType.HUNDRED : PercentType.ZERO)); + } else if (command instanceof IncreaseDecreaseType) { + handleIncreaseDecreaseTemperatureCommand(command == IncreaseDecreaseType.INCREASE); + } + break; + case CHANNEL_TEMPERATURE_ABS: + QuantityType kelvinQt; + if (command instanceof QuantityType commandQt + && (kelvinQt = commandQt.toInvertibleUnit(Units.KELVIN)) != null) { + handleTemperatureCommand(kelvinQt.intValue()); + } else { + handleTemperatureCommand(Integer.valueOf(command.toString())); + } + case CHANNEL_BRIGHTNESS: + if (command instanceof PercentType percentCommand) { + handlePercentCommand(percentCommand); + } else if (command instanceof OnOffType onOffCommand) { + handleOnOffCommand(onOffCommand); + } else if (command instanceof IncreaseDecreaseType) { + handleIncreaseDecreaseCommand(command == IncreaseDecreaseType.INCREASE); + } + break; + case CHANNEL_STATE: + if (command instanceof OnOffType onOffCommand) { + handleOnOffCommand(onOffCommand); + } + break; + case CHANNEL_MODE: + handleLightModeCommand(command); + break; + case CHANNEL_SPEED: + if (command instanceof PercentType percentCommand) { + handleSpeedCommand(percentCommand); + } else if (command instanceof OnOffType onOffCommand) { + handleSpeedCommand(onOffCommand == OnOffType.ON ? PercentType.HUNDRED : PercentType.ZERO); + } else if (command instanceof IncreaseDecreaseType) { + handleIncreaseDecreaseSpeedCommand(command == IncreaseDecreaseType.INCREASE); + } + break; + } + } + + private void handleFanCommand(final String channelId, final Command command) { + switch (channelId) { + case CHANNEL_STATE: + if (command instanceof OnOffType onOffCommand) { + handleFanOnOffCommand(onOffCommand); + } + break; + case CHANNEL_MODE: + if (command instanceof DecimalType decimalCommand) { + handleFanModeCommand(decimalCommand); + } + break; + case CHANNEL_SPEED: + if (command instanceof DecimalType numberCommand) { + if (numberCommand.equals(DecimalType.ZERO)) { + handleFanOnOffCommand(OnOffType.OFF); + } else { + handleFanSpeedCommand(numberCommand); + } + } + break; + case CHANNEL_REVERSE: + if (command instanceof OnOffType onOffCommand) { + handleFanReverseCommand(onOffCommand); + } + break; + } + } + + @Override + public void handleRemoval() { + disposed = true; + fullyInitialized = false; + // stop update thread + ScheduledFuture keepAliveJob = this.keepAliveJob; + if (keepAliveJob != null) { + keepAliveJob.cancel(true); + this.keepAliveJob = null; + } + super.handleRemoval(); + } + + private void handleLightModeCommand(Command command) { + String commandAsString = command.toString(); + + Integer commandAsInt = Integer.MIN_VALUE; + WizLightMode commandAsLightMode = null; + + try { + commandAsInt = Integer.parseInt(commandAsString); + } catch (Exception ex) { + } + + if (commandAsInt > 0) { + commandAsLightMode = WizLightMode.fromSceneId(commandAsInt); + } + + if (commandAsLightMode == null) { + commandAsLightMode = WizLightMode.fromSceneName(commandAsString); + } + + if (commandAsLightMode != null) { + mostRecentState.sceneId = commandAsLightMode.getSceneId(); + setPilotCommand(new SceneRequestParam(commandAsLightMode.getSceneId())); + } else { + logger.warn("[{}] Command [{}] not a recognized Light Mode!", config.ipAddress, command); + } + } + + private void handleHSBCommand(HSBType hsb) { + if (hsb.getBrightness().intValue() == 0) { + logger.debug("[{}] Zero intensity requested, turning bulb off.", config.ipAddress); + setPilotCommand(new StateRequestParam(false)); + } else { + setPilotCommand(new ColorRequestParam(hsb)); + } + mostRecentState.setHSBColor(hsb); + } + + private void handlePercentCommand(PercentType brightness) { + if (brightness.equals(PercentType.ZERO)) { + logger.debug("[{}] Zero brightness requested, turning bulb off.", config.ipAddress); + setPilotCommand(new StateRequestParam(false)); + } else { + setPilotCommand(new DimmingRequestParam(brightness.intValue())); + } + mostRecentState.dimming = brightness.intValue(); + } + + private void handleOnOffCommand(OnOffType onOff) { + setPilotCommand(new StateRequestParam(onOff == OnOffType.ON)); + mostRecentState.state = onOff == OnOffType.ON; + } + + private void handleFanOnOffCommand(OnOffType onOff) { + int value = onOff == OnOffType.ON ? 1 : 0; + setPilotCommand(new FanStateRequestParam(value)); + mostRecentState.fanState = value; + } + + private void handleFanSpeedCommand(DecimalType speed) { + setPilotCommand(new FanSpeedRequestParam(speed.intValue())); + mostRecentState.fanSpeed = speed.intValue(); + } + + private void handleFanReverseCommand(OnOffType onOff) { + int value = onOff == OnOffType.ON ? 1 : 0; + setPilotCommand(new FanReverseRequestParam(value)); + mostRecentState.fanRevrs = value; + } + + private void handleFanModeCommand(DecimalType mode) { + setPilotCommand(new FanModeRequestParam(mode.intValue())); + mostRecentState.fanMode = mode.intValue(); + } + + private void handleIncreaseDecreaseCommand(boolean isIncrease) { + int oldDimming = mostRecentState.dimming; + int newDimming; + if (isIncrease) { + newDimming = Math.min(100, oldDimming + 5); + } else { + newDimming = Math.max(10, oldDimming - 5); + } + logger.debug("[{}] Changing bulb brightness from {}% to {}%.", config.ipAddress, oldDimming, newDimming); + handlePercentCommand(new PercentType(newDimming)); + } + + private void handleTemperatureCommand(int temperature) { + setPilotCommand(new ColorTemperatureRequestParam(temperature)); + mostRecentState.setTemperature(temperature); + } + + private void handleIncreaseDecreaseTemperatureCommand(boolean isIncrease) { + float oldTempPct = colorTempToPercent(mostRecentState.getTemperature()).floatValue(); + float newTempPct; + if (isIncrease) { + newTempPct = Math.min(100, oldTempPct + 5); + } else { + newTempPct = Math.max(0, oldTempPct - 5); + } + logger.debug("[{}] Changing color temperature from {}% to {}%.", config.ipAddress, oldTempPct, newTempPct); + handleTemperatureCommand(percentToColorTemp(new PercentType(BigDecimal.valueOf(newTempPct)))); + } + + private void handleSpeedCommand(PercentType speed) { + // NOTE: We cannot set the speed without also setting the scene + int currentScene = mostRecentState.sceneId; + setPilotCommand(new SpeedRequestParam(currentScene, speed.intValue())); + mostRecentState.speed = speed.intValue(); + } + + private void handleIncreaseDecreaseSpeedCommand(boolean isIncrease) { + int oldSpeed = mostRecentState.speed; + int newSpeed; + if (isIncrease) { + newSpeed = Math.min(100, oldSpeed + 5); + } else { + newSpeed = Math.max(10, oldSpeed - 5); + } + handleSpeedCommand(new PercentType(newSpeed)); + } + + /** + * Starts one thread that querys the state of the socket, after the defined + * refresh interval. + */ + private synchronized void initGetStatusAndKeepAliveThread() { + ScheduledFuture keepAliveJob = this.keepAliveJob; + if (keepAliveJob != null) { + keepAliveJob.cancel(true); + } + + Runnable runnable = () -> { + long now = System.currentTimeMillis(); + long timePassedFromLastUpdateInSeconds = (now - latestUpdate) / 1000; + long timePassedFromLastRefreshInSeconds = (now - latestOfflineRefresh) / 1000; + + // If the device has an online status, check if we it's been too long since the + // last response and re-set offline accordingly + if (getThing().getStatus() == ThingStatus.ONLINE) { + logger.trace("[{}] MAC address: {} Latest Update: {} Now: {} Delta: {} seconds", config.ipAddress, + config.macAddress, latestUpdate, now, timePassedFromLastUpdateInSeconds); + + boolean considerThingOffline = (latestUpdate < 0) + || (timePassedFromLastUpdateInSeconds > MARK_OFFLINE_AFTER_SEC); + if (considerThingOffline) { + logger.debug( + "[{}] Since no updates have been received from mac address {} in {} seconds, setting its status to OFFLINE and discontinuing polling.", + config.ipAddress, config.macAddress, MARK_OFFLINE_AFTER_SEC); + updateStatus(ThingStatus.OFFLINE); + + } + } + + // If we're not offline ither re-register for heart-beats or request status + if (getThing().getStatus() != ThingStatus.OFFLINE) { + if (config.useHeartBeats) { + // If we're using 5s heart-beats, we must re-register every 30s to maintain + // connection + logger.debug("[{}] Re-registering for heart-beats.", config.ipAddress); + registerWithDevice(); + } else { + // If we're not using heart-beats, just request the current status + logger.debug("[{}] Polling for status from device at {}.", config.ipAddress, config.macAddress); + getPilot(); + } + + // Else if we are offline, but it's been a while, re-check if the device re-appeared + } else if (timePassedFromLastRefreshInSeconds > config.reconnectInterval * 60) { + // Request the current status + logger.debug("[{}] Checking for reappearance of offline device at {}.", config.ipAddress, + config.macAddress); + latestOfflineRefresh = now; + getPilot(); + } + }; + /** + * Schedule the keep-alive job. + * + * The scheduling inteval is: + * - every 30 seconds for online devices receiving heart-beats + * - every config.updateInterval for other online devices + */ + long updateIntervalInUse = config.useHeartBeats ? 30 : config.updateInterval; + logger.debug("[{}] Scheduling reoccuring keep alive for every {} seconds for device at {}.", config.ipAddress, + updateIntervalInUse, config.macAddress); + this.keepAliveJob = scheduler.scheduleWithFixedDelay(runnable, 1, updateIntervalInUse, TimeUnit.SECONDS); + } + + @Override + public void initialize() { + this.config = getConfigAs(WizDeviceConfiguration.class); + fullyInitialized = false; + disposed = false; + + if (registrationRequestParam == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Unable to determine openHAB's IP or MAC address"); + return; + } + if (!ValidationUtils.isMacValid(config.macAddress)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MAC address is not valid"); + return; + } + + // set the thing status to UNKNOWN temporarily + updateStatus(ThingStatus.UNKNOWN); + updateDeviceProperties(); + initGetStatusAndKeepAliveThread(); + fullyInitialized = true; + } + + @Override + public void dispose() { + disposed = true; + fullyInitialized = false; + // stop update thread + ScheduledFuture keepAliveJob = this.keepAliveJob; + if (keepAliveJob != null) { + keepAliveJob.cancel(true); + this.keepAliveJob = null; + } + stateDescriptionProvider.remove(colorTempChannelUID); + super.dispose(); + } + + private synchronized void getPilot() { + WizResponse response = sendRequestPacket(WizMethodType.GetPilot, null); + if (response != null) { + WizSyncState rParam = response.getSyncState(); + if (rParam != null) { + updateTimestamps(); + updateStatesFromParams(rParam); + } else { + logger.trace("[{}] No parameters in getPilot response!", config.ipAddress); + } + } else { + logger.trace("[{}] No response from getPilot request!", config.ipAddress); + } + } + + /** + * Method called by {@link WizMediator} when any "unsolicited" messages + * come in on the listening socket and appear to be a WiZ device. "Unsolicited" + * messages from the device could be: + * - a "firstBeat" broadcast to the subnet by the device on first powering up + * - an "hb" (heartbeat) specifically directed to openHAB within 30 seconds of registration + * - or a response to a registration request broadcast by this binding to all devices on the subnet + * + * @note The mediator finds the correct handler for the device based on the (unchanging) device + * MAC address. If the mediator matches a message to the handler by MAC address, but the IP address + * the message came from doesn't match the device's configured IP address, this will update the + * device's configuration to reflect whatever the current IP is. + * + * @param receivedMessage the received {@link WizResponse}. + */ + public synchronized void newReceivedResponseMessage(final WizResponse receivedMessage) { + Boolean updatePropertiesAfterParams = false; + + // Check if the device still has the same IP address it had previously + // If not, we need to update the configuration for the thing. + if (!receivedMessage.getWizResponseIpAddress().isEmpty() + && !receivedMessage.getWizResponseIpAddress().equals(this.getIpAddress())) { + // get the old config + Configuration priorConfig = getConfig(); + // change the ip address property + priorConfig.put(CONFIG_IP_ADDRESS, receivedMessage.getWizResponseIpAddress()); + // save the changes to the thing + updateConfiguration(priorConfig); + // and then refresh the config within the handler + this.config = getConfigAs(WizDeviceConfiguration.class); + // finally, make note that we want to update properties + updatePropertiesAfterParams = true; + } + + // Grab the ID number and mark the device online + requestId = receivedMessage.getId(); + updateTimestamps(); + + // Update the state from the parameters, if possible + WizSyncState params = receivedMessage.getSyncState(); + if (params != null) { + updateStatesFromParams(params); + } + + // After updating state, we'll update all other device parameters from devices that + // presented with a new IP address. + if (updatePropertiesAfterParams) { + updateDeviceProperties(); + } + } + + /** + * Updates the channel states based on incoming parameters + * + * @param receivedParam The received {@link WizSyncState} + */ + private synchronized void updateStatesFromParams(final WizSyncState receivedParam) { + // Save the current state + this.mostRecentState = receivedParam; + + if (hasConfigurationError() || disposed) { + return; + } + + if (isFan) { + updateFanStatesFromParams(receivedParam); + } + if (!isFanOnly) { + updateLightStatesFromParams(receivedParam); + } + + // update signal strength + if (receivedParam.rssi != 0) { + int strength = -1; + if (receivedParam.rssi < -90) { + strength = 0; + } else if (receivedParam.rssi < -80) { + strength = 1; + } else if (receivedParam.rssi < -70) { + strength = 2; + } else if (receivedParam.rssi < -67) { + strength = 3; + } else { + strength = 4; + } + updateDeviceState(CHANNEL_SIGNAL_STRENGTH, new DecimalType(strength)); + updateDeviceState(CHANNEL_RSSI, new QuantityType<>(receivedParam.rssi, Units.DECIBEL)); + } + } + + /** + * Updates the channel states for a light based on incoming parameters + * + * @param receivedParam The received {@link WizSyncState} + */ + private void updateLightStatesFromParams(final WizSyncState receivedParam) { + if (!receivedParam.state) { + updateLightState(CHANNEL_COLOR, HSBType.BLACK); + updateLightState(CHANNEL_BRIGHTNESS, PercentType.ZERO); + updateLightState(CHANNEL_STATE, OnOffType.OFF); + updateLightState(CHANNEL_TEMPERATURE, UnDefType.UNDEF); + updateLightState(CHANNEL_TEMPERATURE_ABS, UnDefType.UNDEF); + } else { + updateLightState(CHANNEL_BRIGHTNESS, new PercentType(receivedParam.dimming)); + updateLightState(CHANNEL_STATE, OnOffType.ON); + switch (receivedParam.getColorMode()) { + case RGBMode: + logger.trace( + "[{}] Received color values - R: {} G: {} B: {} W: {} C: {} Dimming: {}; translate to HSBType: {}", + config.ipAddress, receivedParam.r, receivedParam.g, receivedParam.b, receivedParam.w, + receivedParam.c, receivedParam.dimming, receivedParam.getHSBColor()); + + updateLightState(CHANNEL_COLOR, receivedParam.getHSBColor()); + updateLightState(CHANNEL_TEMPERATURE, UnDefType.UNDEF); + updateLightState(CHANNEL_TEMPERATURE_ABS, UnDefType.UNDEF); + break; + case CTMode: + double[] xy = ColorUtil.kelvinToXY(receivedParam.getTemperature()); + HSBType color = ColorUtil.xyToHsb(xy); + updateLightState(CHANNEL_COLOR, new HSBType(color.getHue(), color.getSaturation(), + new PercentType(receivedParam.getDimming()))); + updateLightState(CHANNEL_TEMPERATURE, colorTempToPercent(receivedParam.getTemperature())); + updateLightState(CHANNEL_TEMPERATURE_ABS, + new QuantityType<>(receivedParam.getTemperature(), Units.KELVIN)); + break; + case SingleColorMode: + updateLightState(CHANNEL_COLOR, new HSBType(DecimalType.ZERO, PercentType.ZERO, + new PercentType(receivedParam.getDimming()))); + updateLightState(CHANNEL_TEMPERATURE, UnDefType.UNDEF); + updateLightState(CHANNEL_TEMPERATURE_ABS, UnDefType.UNDEF); + break; + } + } + + updateLightState(CHANNEL_MODE, new StringType(String.valueOf(receivedParam.sceneId))); + updateLightState(CHANNEL_SPEED, new PercentType(receivedParam.speed)); + } + + /** + * Updates the channel states for a fan based on incoming parameters + * + * @param receivedParam The received {@link WizSyncState} + */ + private void updateFanStatesFromParams(final WizSyncState receivedParam) { + updateFanState(CHANNEL_STATE, receivedParam.fanState == 0 ? OnOffType.OFF : OnOffType.ON); + updateFanState(CHANNEL_SPEED, new DecimalType(receivedParam.fanSpeed)); + updateFanState(CHANNEL_REVERSE, receivedParam.fanRevrs == 0 ? OnOffType.OFF : OnOffType.ON); + updateFanState(CHANNEL_MODE, new DecimalType(receivedParam.fanMode)); + } + + /** + * Sends {@link WizRequest} to the passed {@link InetAddress}. + * + * @param requestPacket the {@link WizRequest}. + * @param address the {@link InetAddress}. + */ + private synchronized @Nullable WizResponse sendRequestPacket(final WizMethodType method, + final @Nullable Param param) { + DatagramSocket dsocket = null; + try { + InetAddress address = InetAddress.getByName(config.ipAddress); + if (address != null) { + WizRequest request = new WizRequest(method, param); + request.setId(requestId++); + + byte[] message = this.converter.transformToByteMessage(request); + logger.trace("Raw packet to send: {}", message); + + // Initialize a datagram packet with data and address + DatagramPacket packet = new DatagramPacket(message, message.length, address, DEFAULT_UDP_PORT); + + // Create a datagram socket, send the packet through it, close it. + dsocket = new DatagramSocket(null); + dsocket.setReuseAddress(true); + dsocket.setBroadcast(true); + dsocket.setSoTimeout(500); // Timeout in 500ms + dsocket.send(packet); + logger.debug("[{}] Sent packet to address: {} and port {}", config.ipAddress, address, + DEFAULT_UDP_PORT); + + byte[] responseMessage = new byte[1024]; + packet = new DatagramPacket(responseMessage, responseMessage.length); + dsocket.receive(packet); + + return converter.transformResponsePacket(packet); + } + } catch (SocketTimeoutException e) { + logger.trace("[{}] Socket timeout after sending command; no response from {} within 500ms", + config.ipAddress, config.macAddress); + } catch (IOException exception) { + logger.debug("[{}] Something wrong happened when sending the packet to port {}... msg: {}", + config.ipAddress, DEFAULT_UDP_PORT, exception.getMessage()); + } finally { + if (dsocket != null) { + dsocket.close(); + } + } + return null; + } + + /** + * Sends a setPilot request and checks for success + */ + private synchronized boolean setPilotCommand(final @Nullable Param param) { + WizResponse response = sendRequestPacket(WizMethodType.SetPilot, param); + if (response != null) { + boolean setSucceeded = response.getResultSuccess(); + if (setSucceeded) { + // can't process this response it doens't have a syncstate, so request updated state + // let the getPilot response update the timestamps + try { + // wait for state change to apply + Thread.sleep(1000L); + } catch (InterruptedException e) { + } + getPilot(); + return setSucceeded; + } + } + return false; + } + + /** + * Makes note of the latest timestamps and sets the device online + */ + private synchronized void updateTimestamps() { + if (hasConfigurationError() || disposed) { + return; + } + updateStatus(ThingStatus.ONLINE); + latestUpdate = System.currentTimeMillis(); + latestOfflineRefresh = System.currentTimeMillis(); + final ZonedDateTime zonedDateTime = ZonedDateTime.now(timeZoneProvider.getTimeZone()); + updateDeviceState(CHANNEL_LAST_UPDATE, new DateTimeType(zonedDateTime)); + } + + /** + * Asks the device for its current system configuration + */ + private synchronized void updateDeviceProperties() { + if (hasConfigurationError() || disposed) { + return; + } + WizResponse registrationResponse = sendRequestPacket(WizMethodType.GetSystemConfig, null); + if (registrationResponse != null) { + SystemConfigResult systemConfigResult = registrationResponse.getSystemConfigResults(); + if (systemConfigResult != null) { + // Update all the thing properties based on the result + Map thingProperties = new HashMap(); + thingProperties.put(PROPERTY_VENDOR, "WiZ Connected"); + thingProperties.put(PROPERTY_FIRMWARE_VERSION, systemConfigResult.fwVersion); + thingProperties.put(PROPERTY_MAC_ADDRESS, systemConfigResult.mac); + thingProperties.put(PROPERTY_IP_ADDRESS, registrationResponse.getWizResponseIpAddress()); + thingProperties.put(PROPERTY_HOME_ID, String.valueOf(systemConfigResult.homeId)); + thingProperties.put(PROPERTY_ROOM_ID, String.valueOf(systemConfigResult.roomId)); + thingProperties.put(PROPERTY_HOME_LOCK, String.valueOf(systemConfigResult.homeLock)); + thingProperties.put(PROPERTY_PAIRING_LOCK, String.valueOf(systemConfigResult.pairingLock)); + thingProperties.put(PROPERTY_TYPE_ID, String.valueOf(systemConfigResult.typeId)); + thingProperties.put(PROPERTY_MODULE_NAME, systemConfigResult.moduleName); + thingProperties.put(PROPERTY_GROUP_ID, String.valueOf(systemConfigResult.groupId)); + updateProperties(thingProperties); + updateTimestamps(); + } else { + logger.debug( + "[{}] Received response to getConfigRequest from device at {}, but it did not contain device configuration information.", + config.ipAddress, config.macAddress); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + } + + // Firmware versions > 1.22 support more details + registrationResponse = sendRequestPacket(WizMethodType.GetModelConfig, null); + if (registrationResponse != null) { + ModelConfigResult modelConfigResult = registrationResponse.getModelConfigResults(); + if (modelConfigResult != null && modelConfigResult.cctRange.length > 0) { + minColorTemp = Arrays.stream(modelConfigResult.cctRange).min().getAsInt(); + maxColorTemp = Arrays.stream(modelConfigResult.cctRange).max().getAsInt(); + StateDescription stateDescription = StateDescriptionFragmentBuilder.create() + .withMinimum(BigDecimal.valueOf(minColorTemp)).withMaximum(BigDecimal.valueOf(maxColorTemp)) + .withPattern("%.0f K").build().toStateDescription(); + stateDescriptionProvider.setDescription(colorTempChannelUID, + Objects.requireNonNull(stateDescription)); + } + } else { + // Not a big deal; probably just an older device + logger.warn("[{}] No response to getModelConfig request from device", config.ipAddress); + } + } else { + logger.debug("[{}] No response to getSystemConfig request from device at {}", config.ipAddress, + config.macAddress); + // Not calling it "gone" because it's probably just been powered off and will beback any time + updateStatus(ThingStatus.OFFLINE); + } + } + + /** + * Registers with the device - this tells the device to begin sending 5-second + * heartbeat (hb) status updates. Status updates are sent by the device every 5 + * sec and on any state change for 30s after registration. For continuous + * heart-beats the registration must be re-sent after 30s. + */ + private synchronized void registerWithDevice() { + WizResponse registrationResponse = sendRequestPacket(WizMethodType.Registration, + Objects.requireNonNull(registrationRequestParam)); + if (registrationResponse != null) { + if (registrationResponse.getResultSuccess()) { + updateTimestamps(); + } else { + logger.debug( + "[{}] Received response to getConfigRequest from device at {}, but it did not contain device configuration information.", + config.ipAddress, config.macAddress); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + } + } else { + logger.debug("[{}] No response to registration request from device at {}", config.ipAddress, + config.macAddress); + // Not calling it "gone" because it's probably just been powered off and will be + // back any time + updateStatus(ThingStatus.OFFLINE); + } + } + + private boolean hasConfigurationError() { + ThingStatusInfo statusInfo = getThing().getStatusInfo(); + return statusInfo.getStatus() == ThingStatus.OFFLINE + && statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR; + } + + private int percentToColorTemp(PercentType command) { + int range = maxColorTemp - minColorTemp; + // NOTE: 0% is cold (highest K) and 100% is warm (lowest K) + return maxColorTemp - Math.round((range * command.floatValue()) / 100); + } + + private PercentType colorTempToPercent(int temp) { + return new PercentType(BigDecimal.valueOf(((float) temp - minColorTemp) / (maxColorTemp - minColorTemp) * 100)); + } + + // SETTERS AND GETTERS + public String getIpAddress() { + return config.ipAddress; + } + + public String getMacAddress() { + return config.macAddress; + } + + public int getHomeId() { + return homeId; + } + + private void updateLightState(String channelId, State state) { + if (isFan) { + updateState(new ChannelUID(this.getThing().getUID(), CHANNEL_GROUP_LIGHT, channelId), state); + } else { + updateState(channelId, state); + } + } + + private void updateFanState(String channelId, State state) { + if (isFanOnly) { + updateState(channelId, state); + } else { + updateState(new ChannelUID(this.getThing().getUID(), CHANNEL_GROUP_FAN, channelId), state); + } + } + + private void updateDeviceState(String channelId, State state) { + if (isFan && !isFanOnly) { + updateState(new ChannelUID(this.getThing().getUID(), CHANNEL_GROUP_DEVICE, channelId), state); + } else { + updateState(channelId, state); + } + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediator.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediator.java new file mode 100644 index 0000000000000..29ee6ff6fc774 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediator.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.handler; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.wiz.internal.discovery.WizDiscoveryService; +import org.openhab.binding.wiz.internal.entities.RegistrationRequestParam; +import org.openhab.binding.wiz.internal.entities.WizResponse; +import org.openhab.core.net.NetworkAddressService; +import org.openhab.core.thing.Thing; + +/** + * The {@link WizMediator} is responsible for receiving all the sync + * packets and route correctly to each handler. + * + * @author Sriram Balakrishnan - Initial contribution + * @author Joshua Freeman - pass through NetworkAddressService + */ +@NonNullByDefault +public interface WizMediator { + + /** + * This method is called by the {@link WizUpdateReceiverRunnable}, when + * one new message has been received. + * + * @param receivedMessage the {@link WizResponse} message. + */ + void processReceivedPacket(final WizResponse receivedMessage); + + /** + * Returns a {@link RegistrationRequestParam} based on the current OpenHAB + * connection. + * + */ + RegistrationRequestParam getRegistrationParams() throws IllegalStateException; + + /** + * Registers a new {@link Thing} and the corresponding + * {@link WizHandler}. + * + * @param thing the {@link Thing}. + * @param handler the {@link WizHandler}. + */ + void registerThingAndWizBulbHandler(final Thing thing, final WizHandler handler); + + /** + * Unregisters a {@link WizHandler} by the corresponding {@link Thing}. + * + * @param thing the {@link Thing}. + */ + void unregisterWizBulbHandlerByThing(final Thing thing); + + /** + * Returns all the {@link Thing} registered. + * + * @returns all the {@link Thing}. + */ + Set getAllThingsRegistered(); + + /** + * Sets the discovery service to inform the user when one new thing has been + * found. + * + * @param discoveryService the discovery service. + */ + void setDiscoveryService(final @Nullable WizDiscoveryService discoveryService); + + /** + * Gets the NetworkAddressService used to configure the mediator instance. + * + * @return networkAddressService + */ + NetworkAddressService getNetworkAddressService(); +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediatorImpl.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediatorImpl.java new file mode 100644 index 0000000000000..c354df448f779 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/handler/WizMediatorImpl.java @@ -0,0 +1,232 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.handler; + +import static org.openhab.binding.wiz.internal.WizBindingConstants.*; + +import java.net.SocketException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.wiz.internal.WizBindingConstants; +import org.openhab.binding.wiz.internal.discovery.WizDiscoveryService; +import org.openhab.binding.wiz.internal.entities.RegistrationRequestParam; +import org.openhab.binding.wiz.internal.entities.WizResponse; +import org.openhab.binding.wiz.internal.runnable.WizUpdateReceiverRunnable; +import org.openhab.binding.wiz.internal.utils.NetworkUtils; +import org.openhab.core.net.NetworkAddressService; +import org.openhab.core.thing.Thing; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link WizMediatorImpl} is responsible for receiving all the sync + * packets and route correctly to each handler. + * + * @author Sriram Balakrishnan - Initial contribution + * @author Joshua Freeman - pass through NetworkAddressService + */ +@Component(configurationPid = "WizMediator", service = WizMediator.class) +@NonNullByDefault +public class WizMediatorImpl implements WizMediator { + + private final Logger logger = LoggerFactory.getLogger(WizMediatorImpl.class); + + private final Map handlersRegisteredByThing = new HashMap<>(); + + private @Nullable WizUpdateReceiverRunnable receiver; + private @Nullable Thread receiverThread; + + private @Nullable WizDiscoveryService wizDiscoveryService; + + private final NetworkAddressService networkAddressService; + + /** + * Constructor for the mediator implementation. + * + * @param IllegalArgumentException if the timeout < 0 + */ + @Activate + public WizMediatorImpl( + @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC) NetworkAddressService networkAddressService) { + this.networkAddressService = networkAddressService; + this.initMediatorWizBulbUpdateReceiverRunnable(); + } + + /** + * Called at the service deactivation. + * + * @param componentContext the componentContext + */ + protected void deactivate(final ComponentContext componentContext) { + WizUpdateReceiverRunnable receiver = this.receiver; + if (receiver != null) { + receiver.shutdown(); + } + } + + /** + * This method is called by the {@link WizUpdateReceiverRunnable}, when + * one new message has been received. + * + * @param receivedMessage the {@link WizResponse} message. + */ + @Override + public void processReceivedPacket(final WizResponse receivedMessage) { + logger.debug("Received packet from: {} - {} with method: [{}]", receivedMessage.getWizResponseIpAddress(), + receivedMessage.getWizResponseMacAddress(), receivedMessage.getMethod()); + + String bulbIp = receivedMessage.getWizResponseIpAddress(); + String bulbMac = receivedMessage.getWizResponseMacAddress(); + + if (!bulbMac.isEmpty()) { + @Nullable + WizHandler handler = this.getHandlerRegisteredByMac(bulbMac); + + if (handler != null) { + // deliver message to handler. + handler.newReceivedResponseMessage(receivedMessage); + } else if (!bulbIp.isEmpty()) { + logger.debug("There is no handler registered for mac address: {}", + receivedMessage.getWizResponseMacAddress()); + WizDiscoveryService discoveryServe = this.wizDiscoveryService; + if (discoveryServe != null) { + discoveryServe.discoveredLight(bulbMac, bulbIp); + logger.trace("Sending a new thing to the discovery service. MAC: {} IP: {}", bulbMac, bulbIp); + } else { + logger.trace("There is no discovery service registered to receive the new bulb!"); + } + } + } else { + logger.warn("The sync response did not contain a valid mac address, it cannot be processed."); + } + } + + /** + * Register one new {@link Thing} and the corresponding + * {@link WizHandler}. + * + * @param thing the {@link Thing}. + * @param handler the {@link WizHandler}. + */ + @Override + public void registerThingAndWizBulbHandler(final Thing thing, final WizHandler handler) { + this.handlersRegisteredByThing.put(thing, handler); + } + + /** + * Unregister one {@link WizHandler} by the corresponding {@link Thing}. + * + * @param thing the {@link Thing}. + */ + @Override + public void unregisterWizBulbHandlerByThing(final Thing thing) { + this.handlersRegisteredByThing.remove(thing); + } + + /** + * Utility method to get the registered thing handler in mediator by the mac + * address. + * + * @param macAddress the mac address of the thing of the handler. + * @return {@link WizHandler} if found. + */ + private @Nullable WizHandler getHandlerRegisteredByMac(final String macAddress) { + WizHandler searchedHandler = null; + for (WizHandler handler : this.handlersRegisteredByThing.values()) { + if (macAddress.equalsIgnoreCase(handler.getMacAddress())) { + searchedHandler = handler; + // don't spend more computation. Found the handler. + break; + } + } + return searchedHandler; + } + + /** + * Inits the mediator WizBulbUpdateReceiverRunnable thread. This thread is + * responsible to receive all packets from Wiz Bulbs, and redirect the messages + * to mediator. + */ + private void initMediatorWizBulbUpdateReceiverRunnable() { + WizUpdateReceiverRunnable receiver = this.receiver; + Thread receiverThread = this.receiverThread; + // try with handler port if is null + if ((receiver == null) + || ((receiverThread != null) && (receiverThread.isInterrupted() || !receiverThread.isAlive()))) { + try { + logger.trace("Receiver thread is either null, interrupted, or dead."); + WizUpdateReceiverRunnable newReceiver = new WizUpdateReceiverRunnable(this, DEFAULT_LISTENER_UDP_PORT); + Thread newThread = new Thread(newReceiver, + "OH-binding-" + WizBindingConstants.BINDING_ID + "-ReceiverThread"); + newThread.setDaemon(true); + newThread.start(); + this.receiver = newReceiver; + this.receiverThread = newThread; + } catch (SocketException e) { + logger.debug("Cannot start the socket with default port {}...", e.getMessage()); + } + } + } + + /** + * Returns all the {@link Thing} registered. + * + * @returns all the {@link Thing}. + */ + @Override + public Set getAllThingsRegistered() { + return this.handlersRegisteredByThing.keySet(); + } + + /** + * Returns a {@link RegistrationRequestParam} based on the current openHAB + * connection. + * + * @throws IllegalStateException + */ + public RegistrationRequestParam getRegistrationParams() throws IllegalStateException { + String ipAddress = networkAddressService.getPrimaryIpv4HostAddress(); + String macAddress = null; + if (ipAddress != null) { + macAddress = NetworkUtils.getMacAddress(ipAddress); + } + if (ipAddress == null || macAddress == null) { + throw new IllegalStateException("Unable to determine openHAB's IP and/or MAC address"); + } + return new RegistrationRequestParam(ipAddress, true, 0, macAddress); + } + + @Override + public void setDiscoveryService(final @Nullable WizDiscoveryService discoveryService) { + this.wizDiscoveryService = discoveryService; + } + + public @Nullable WizDiscoveryService getDiscoveryService() { + return this.wizDiscoveryService; + } + + @Override + public NetworkAddressService getNetworkAddressService() { + return this.networkAddressService; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/runnable/WizUpdateReceiverRunnable.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/runnable/WizUpdateReceiverRunnable.java new file mode 100644 index 0000000000000..0946a5168b392 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/runnable/WizUpdateReceiverRunnable.java @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.runnable; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.net.SocketTimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.wiz.internal.entities.WizResponse; +import org.openhab.binding.wiz.internal.handler.WizMediator; +import org.openhab.binding.wiz.internal.utils.WizPacketConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This Thread is responsible for receiving all sync messages and redirecting them to + * {@link WizMediator}. + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public class WizUpdateReceiverRunnable implements Runnable { + + private static final int TIMEOUT_TO_DATAGRAM_RECEPTION_MILLISECONDS = 15000; + + private final Logger logger = LoggerFactory.getLogger(WizUpdateReceiverRunnable.class); + + private DatagramSocket datagramSocket; + private final WizMediator mediator; + private final WizPacketConverter packetConverter = new WizPacketConverter(); + + private boolean shutdown; + private int listeningPort; + + /** + * Constructor of the receiver runnable thread. + * + * @param mediator the {@link WizMediator} + * @param listeningPort the listening UDP port + * @throws SocketException is some problem occurs opening the socket. + */ + public WizUpdateReceiverRunnable(final WizMediator mediator, final int listeningPort) throws SocketException { + this.listeningPort = listeningPort; + this.mediator = mediator; + + // Create a socket to listen on the port. + logger.debug("Opening socket and start listening UDP port: {}", listeningPort); + DatagramSocket dsocket = new DatagramSocket(null); + dsocket.setReuseAddress(true); + dsocket.setBroadcast(true); + dsocket.setSoTimeout(TIMEOUT_TO_DATAGRAM_RECEPTION_MILLISECONDS); + dsocket.bind(new InetSocketAddress(listeningPort)); + this.datagramSocket = dsocket; + + this.shutdown = false; + } + + @Override + public void run() { + try { + // Now loop forever, waiting to receive packets and redirect them to mediator. + while (!this.shutdown) { + datagramSocketHealthRoutine(); + + // Create a buffer to read datagrams into. If a + // packet is larger than this buffer, the + // excess will simply be discarded! + byte[] buffer = new byte[2048]; + + // Create a packet to receive data into the buffer + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + + // Wait to receive a datagram + try { + datagramSocket.receive(packet); + + // Redirect packet to the mediator + WizResponse response = this.packetConverter.transformResponsePacket(packet); + if (response != null) { + this.mediator.processReceivedPacket(response); + } else { + logger.debug("No WizResponse was parsed from returned packet"); + } + } catch (SocketTimeoutException e) { + logger.trace("No incoming data on port {} during {} ms socket was listening.", listeningPort, + TIMEOUT_TO_DATAGRAM_RECEPTION_MILLISECONDS); + } catch (IOException e) { + logger.debug("One exception has occurred: {} ", e.getMessage()); + } + } + } finally { + // close the socket + datagramSocket.close(); + } + } + + private void datagramSocketHealthRoutine() { + DatagramSocket datagramSocket = this.datagramSocket; + if (datagramSocket.isClosed() || !datagramSocket.isConnected()) { + logger.trace("Datagram Socket is disconnected or has been closed (probably timed out), reconnecting..."); + try { + // close the socket before trying to reopen + this.datagramSocket.close(); + logger.trace("Old socket closed."); + DatagramSocket dsocket = new DatagramSocket(null); + dsocket.setReuseAddress(true); + dsocket.setBroadcast(true); + dsocket.setSoTimeout(TIMEOUT_TO_DATAGRAM_RECEPTION_MILLISECONDS); + dsocket.bind(new InetSocketAddress(listeningPort)); + this.datagramSocket = dsocket; + logger.trace("Datagram Socket reconnected."); + } catch (SocketException exception) { + logger.debug("Problem creating one new socket on port {}. Error: {}", listeningPort, + exception.getLocalizedMessage()); + } + } + } + + /** + * Gracefully shutdown thread. Worst case takes TIMEOUT_TO_DATAGRAM_RECEPTION_MILLISECONDS to + * shutdown. + */ + public void shutdown() { + this.shutdown = true; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/NetworkUtils.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/NetworkUtils.java new file mode 100644 index 0000000000000..1987c5c24eecc --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/NetworkUtils.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.utils; + +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Utility class to perform some network routines. + * + * @author Sriram Balakrishnan - Initial contribution + * @author Joshua Freeman - Modified to get MAC matching IP + * + */ +@NonNullByDefault +public final class NetworkUtils { + /** + * Returns the MAC address of the openHAB first network device. + * + * @return The MAC address of the openHAB network device. + */ + public static @Nullable String getMacAddress(String matchIP) { + try { + Enumeration networks = NetworkInterface.getNetworkInterfaces(); + while (networks.hasMoreElements()) { + NetworkInterface network = networks.nextElement(); + + if (networkMatchesIP(network, matchIP)) { + byte[] hardwareAddress = network.getHardwareAddress(); + if (hardwareAddress == null) { + continue; + } + return convertBytesToMACString(hardwareAddress); + } + } + } catch (SocketException e) { + } + return null; + } + + private static boolean networkMatchesIP(NetworkInterface network, String ip) { + for (InterfaceAddress interfaceAddress : network.getInterfaceAddresses()) { + String hostAddress = interfaceAddress.getAddress().getHostAddress(); + if (ip.equals(hostAddress)) { + return true; + } + } + + return false; + } + + private static String convertBytesToMACString(byte[] hardwareAddress) { + StringBuilder macAddressBuilder = new StringBuilder(); + for (int macAddressByteIndex = 0; macAddressByteIndex < hardwareAddress.length; macAddressByteIndex++) { + String macAddressHexByte = String.format("%02X", hardwareAddress[macAddressByteIndex]); + macAddressBuilder.append(macAddressHexByte); + } + return macAddressBuilder.toString(); + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/ValidationUtils.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/ValidationUtils.java new file mode 100644 index 0000000000000..5ae40af4d3183 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/ValidationUtils.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Utility static class to perform some validations. + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public final class ValidationUtils { + + private ValidationUtils() { + // avoid instantiation. + } + + public static final String MAC_PATTERN = "^([0-9A-Fa-f]{2}[:-]*){5}([0-9A-Fa-f]{2})$"; + private static final Pattern VALID_PATTERN = Pattern.compile(ValidationUtils.MAC_PATTERN); + + /** + * Validates if one Mac address is valid. + * + * @param mac the mac, with or without : + * @return true if is valid. + */ + public static boolean isMacValid(final String mac) { + Matcher matcher = VALID_PATTERN.matcher(mac); + return matcher.matches(); + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizColorConverter.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizColorConverter.java new file mode 100644 index 0000000000000..75423fad37e3f --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizColorConverter.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.utils; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.util.ColorUtil; + +/** + * Utilities for converting colors and color temperatures + * + * The full color WiZ bulbs can produce colors and various temperatures of + * "whites" by mixing any of the available LEDs: RGBWwarm = RGBWWCwarm = Red, + * Green, Blue, Warm White, Cool White. When operating in full color mode, the + * warm whites are used to increase saturation (RGBW style). Temperatures of + * white can also be called directly as K instead of mixing cw/ww (c/w) The + * colors and temperatures need to be converted to the HSBType/PercentType + * supported by openHAB. + * + * @author Sara Geleskie Damiano - Initial contribution + * + */ +@NonNullByDefault +public class WizColorConverter { + /** + * Converts an {@link DecimalType} hue and a {@link PercentType} saturation to + * red, green, blue, and white (RGBW) components. Because the WiZ bulbs keep + * dimming in a separate channel, we only take account hue and saturation for + * the color channels. When creating colors, the WiZ bulbs only use the warm + * white channel, the cool white channel is ignored. + * + * Taken from Tasmota HsToRGB + * + * @param hsbColor the {@link HSBType}. + * + * @return an interger array of the color components + */ + public int[] hsbToRgbw(HSBType hsb) { + // Since we're going to use the white lights to control saturation, recalculate what + // the HSBvalue would be if the color was at full brightness and saturation + HSBType hsbFullBrightness = new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED); + PercentType[] rgbPercent = ColorUtil.hsbToRgbPercent(hsbFullBrightness); + double redD = rgbPercent[0].doubleValue(); + double greenD = rgbPercent[1].doubleValue(); + double blueD = rgbPercent[2].doubleValue(); + + double saturationPercent = hsb.getSaturation().doubleValue() / 100; + + int red; + int green; + int blue; + int white; + + // Calculate the white intensity from saturation and adjust down the other colors + // This is approximately what the WiZ app does. Personally, I think it undersaturates everything + if (saturationPercent < 0.5) { + // At less than 50% saturation, maximize white and lower the other intensities by 2x of the saturation + // percent. (2x to give us full range between 0-50%) + // white = 255; + // ^^ WiZ does this.. I think it's very undersaturated that way + white = 255 / 2; // Divide by two to not undersaturate + red = (int) (redD * (2 * saturationPercent)); + green = (int) (greenD * (2 * saturationPercent)); + blue = (int) (blueD * (2 * saturationPercent)); + } else { + // At >50% saturation, colors are at full and increase saturation by decreasing the white intensity. + // white = (int) (255 * 2 * (1 - saturationPercent)); + // ^^ WiZ does this.. I think it's very undersaturated that way + white = (int) ((255 / 2) * 2 * (1 - saturationPercent)); + red = (int) redD; + green = (int) greenD; + blue = (int) blueD; + } + + // Note: We're keeping the brightness in a totally separate channel + return new int[] { red, green, blue, white }; + } + + /** + * Converts Red/Green/Blue/White components to Hue and saturation. + * + * @param int red - the value of the red component (0-255) + * @param int green - the value of the green component (0-255) + * @param int blue - the value of the blue component (0-255) + * @param int white - the value of the white component (0-255) + * @param int dimming - the brightness of the bulb, independent of the RGB color (0-100) + * + * Totally made this up. + * + * @return a {@link HSBType} with the color components + */ + public HSBType rgbwDimmingToHSB(int red, int green, int blue, int white, int dimming) { + // Can get hue from the ratios of the colors. + // The calculated *hue* component of the HSB should be correct regardless of the + // state of the white lights because it's strictly based on the ratio of the colors + DecimalType hue = HSBType.fromRGB(red, green, blue).getHue(); + double saturationPercent; + if (white < 255) { + saturationPercent = (int) (1 - (white / (255 * 2))); + } else { + saturationPercent = Math.max(red, Math.max(green, blue)) / (255 * 2); + } + HSBType out = new HSBType(hue, new PercentType((int) saturationPercent * 100), new PercentType(dimming)); + return out; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizPacketConverter.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizPacketConverter.java new file mode 100644 index 0000000000000..bfe78eeabfeb1 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizPacketConverter.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.utils; + +import static java.nio.charset.StandardCharsets.*; + +import java.net.DatagramPacket; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.wiz.internal.entities.WizRequest; +import org.openhab.binding.wiz.internal.entities.WizResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; + +/** + * Transforms the datagram packet to request/response + * + * @author Sriram Balakrishnan - Initial contribution + * + */ +@NonNullByDefault +public class WizPacketConverter { + + private final Logger logger = LoggerFactory.getLogger(WizPacketConverter.class); + + private Gson wizGsonBuilder; + + /** + * Default constructor of the packet converter. + */ + public WizPacketConverter() { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(WizResponse.class, new WizResponseDeserializer()); + gsonBuilder.excludeFieldsWithoutExposeAnnotation(); + Gson gson = gsonBuilder.create(); + this.wizGsonBuilder = gson; + } + + /** + * Method that transforms one {@link WizRequest} to json requst + * + * @param requestPacket the {@link WizRequest}. + * @return the byte array with the message. + */ + public byte[] transformToByteMessage(final WizRequest requestPacket) { + byte[] requestDatagram = null; + + // {"id":20,"method":"setPilot","params":{"sceneId":18}} + String jsonCmd = this.wizGsonBuilder.toJson(requestPacket); + + requestDatagram = jsonCmd.getBytes(UTF_8); + return requestDatagram; + } + + /** + * Method that transforms {@link DatagramPacket} to a + * {@link WizResponse} Object + * + * @param packet the {@link DatagramPacket} + * @return the {@link WizResponse} + */ + public @Nullable WizResponse transformResponsePacket(final DatagramPacket packet) { + String responseJson = new String(packet.getData(), 0, packet.getLength(), UTF_8); + logger.debug("Incoming packet from {} to convert -> {}", packet.getAddress().getHostAddress(), responseJson); + + @Nullable + WizResponse response = null; + try { + response = this.wizGsonBuilder.fromJson(responseJson, WizResponse.class); + if (response == null) { + throw new JsonParseException("JSON is empty"); + } + response.setWizResponseIpAddress(packet.getAddress().getHostAddress()); + } catch (JsonParseException e) { + logger.debug("Error parsing json! {}", e.getMessage()); + } + return response; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizResponseDeserializer.java b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizResponseDeserializer.java new file mode 100644 index 0000000000000..6cfc24ad85d34 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/java/org/openhab/binding/wiz/internal/utils/WizResponseDeserializer.java @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2010-2024 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.wiz.internal.utils; + +import java.lang.reflect.Type; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.wiz.internal.entities.ErrorResponseResult; +import org.openhab.binding.wiz.internal.entities.ModelConfigResult; +import org.openhab.binding.wiz.internal.entities.SystemConfigResult; +import org.openhab.binding.wiz.internal.entities.WizResponse; +import org.openhab.binding.wiz.internal.entities.WizSyncState; +import org.openhab.binding.wiz.internal.enums.WizMethodType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * Deserializes incoming json + * + * @author Sara Geleskie Damiano - Initial contribution + * + */ +@NonNullByDefault +public class WizResponseDeserializer implements JsonDeserializer { + private final Logger logger = LoggerFactory.getLogger(WizResponseDeserializer.class); + + @Override + @Nullable + public WizResponse deserialize(@Nullable JsonElement json, @Nullable Type typeOfT, + @Nullable JsonDeserializationContext context) throws JsonParseException { + // The outgoing response + WizResponse deserializedResponse = new WizResponse(); + + // The incoming JSON + JsonObject jobject; + if (json == null) { + logger.trace("No json provided to parse."); + } else if (context == null) { + logger.trace("No context available for parsing sub-objects."); + } else { + jobject = json.getAsJsonObject(); + + // Parse the ID + if (jobject.has("id")) { + deserializedResponse.setId(jobject.get("id").getAsInt()); + } + // Parse the environment - I think this is always sent, but I'm checking anyway + if (jobject.has("env")) { + deserializedResponse.setEnv(jobject.get("env").getAsString()); + } + + // Check if the response contains an error + // Return without completing parsing if there's an error + if (jobject.has("error")) { + ErrorResponseResult error = context.deserialize(jobject.getAsJsonObject("error"), + ErrorResponseResult.class); + deserializedResponse.setError(error); + if (jobject.has("method")) { + logger.debug("Bulb returned an error on method {}: {}, {}", jobject.get("method"), error.code, + error.message); + } else { + logger.debug("Bulb returned an error: {}", error.code); + } + return deserializedResponse; + } + + // Parse the method. We will use the method to decide how to continue to parse + // Bail out of everything if we cannot understand the method. + WizMethodType method; + if (jobject.has("method")) { + try { + String inMethod = jobject.get("method").getAsString(); + String properCaseMethod = inMethod.substring(0, 1).toUpperCase() + inMethod.substring(1); + method = WizMethodType.valueOf(properCaseMethod); + deserializedResponse.setMethod(method); + } catch (IllegalArgumentException e) { + logger.debug("Bulb returned an invalid method: {}", jobject.get("method")); + return deserializedResponse; + } + } else { + throw new JsonParseException("Incoming message did not contain a method and cannot be parsed!"); + } + + switch (method) { + case Registration: + // {"method": "registration", "id": 1, "env": "pro", "result": {"mac": + // "macOfopenHAB", "success": true}} + if (!jobject.has("result")) { + throw new JsonParseException("registration received, but no result object present"); + } + JsonObject registrationResult = jobject.getAsJsonObject("result"); + if (!registrationResult.has("mac")) { + throw new JsonParseException("registration received, but no MAC address present"); + } + String mac = registrationResult.get("mac").getAsString(); + deserializedResponse.setWizResponseMacAddress(mac); + deserializedResponse.setResultSucess(registrationResult.get("success").getAsBoolean()); + logger.trace("Registration result deserialized with mac {} and success {}", mac, + registrationResult.get("success").getAsBoolean()); + break; + + case Pulse: + // {"method":"pulse","id":22,"env":"pro","result":{"success":true}} + case SetPilot: + // {"method":"setPilot","id":24,"env":"pro","result":{"success":true}} + if (!jobject.has("result")) { + throw new JsonParseException("pulse or setPilot method received, but no result object present"); + } + JsonObject setResult = jobject.getAsJsonObject("result"); + deserializedResponse.setResultSucess(setResult.get("success").getAsBoolean()); + logger.trace("Result deserialized - command success {}", setResult.get("success").getAsBoolean()); + break; + + case FirstBeat: + // {"method": "firstBeat", "id": 0, "env": "pro", "params": {"mac": "theBulbMacAddress", + // "homeId": xxxxxx, "fwVersion": "1.15.2"}} + if (!jobject.has("params")) { + throw new JsonParseException("firstBeat received, but no params object present"); + } + SystemConfigResult parsedFBParams = context.deserialize(jobject.getAsJsonObject("params"), + SystemConfigResult.class); + if (parsedFBParams.mac.isEmpty()) { + throw new JsonParseException("firstBeat received, but no MAC address present"); + } + deserializedResponse.setWizResponseMacAddress(parsedFBParams.mac); + deserializedResponse.setResultSucess(true); + deserializedResponse.setSystemConfigResult(parsedFBParams); + logger.trace("firstBeat result deserialized with mac {}", parsedFBParams.mac); + break; + + case GetModelConfig: + if (!jobject.has("result")) { + throw new JsonParseException("getModelConfig received, but no result object present"); + } + ModelConfigResult parsedMResult = context.deserialize(jobject.getAsJsonObject("result"), + ModelConfigResult.class); + deserializedResponse.setResultSucess(true); + deserializedResponse.setModelConfigResult(parsedMResult); + break; + + case GetSystemConfig: + // {"method": "getSystemConfig", "id": 22, "env": "pro", + // "result": {"mac": "theBulbMacAddress", "homeId": xxxxxx, "roomId": xxxxxx, + // "homeLock": false, "pairingLock": false, "typeId": 0, "moduleName": + // "ESP01_SHRGB1C_31", "fwVersion": "1.15.2", "groupId": 0, "drvConf":[33,1]}} + if (!jobject.has("result")) { + throw new JsonParseException("getSystemConfig received, but no result object present"); + } + SystemConfigResult parsedCResult = context.deserialize(jobject.getAsJsonObject("result"), + SystemConfigResult.class); + if (parsedCResult.mac.isEmpty()) { + throw new JsonParseException("getSystemConfig received, but no MAC address present"); + } + deserializedResponse.setWizResponseMacAddress(parsedCResult.mac); + deserializedResponse.setResultSucess(true); + deserializedResponse.setSystemConfigResult(parsedCResult); + logger.trace("systemConfig result deserialized with mac {}", parsedCResult.mac); + break; + + case GetPilot: + // {"method": "getPilot", "id": 22, "env": "pro", "result": {"mac": + // "theBulbMacAddress", "rssi":-76, "state": true, "sceneId": 0, "temp": 2700, + // "dimming": 42, "schdPsetId": 5}} + if (!jobject.has("result")) { + throw new JsonParseException("getPilot received, but no result object present"); + } + WizSyncState parsedPResult = context.deserialize(jobject.getAsJsonObject("result"), + WizSyncState.class); + if (parsedPResult.mac.isEmpty()) { + throw new JsonParseException("getPilot received, but no MAC address present"); + } + deserializedResponse.setWizResponseMacAddress(parsedPResult.mac); + deserializedResponse.setResultSucess(true); + deserializedResponse.setSyncParams(parsedPResult); + logger.trace("getPilot result deserialized with mac {}", parsedPResult.mac); + break; + + case SyncPilot: + // {"method": "syncPilot", "id": 219, "env": "pro", "params": { "mac": + // "theBulbMacAddress", "rssi": -72, "src": "hb", "mqttCd": 0, "state": true, "sceneId": + // 0, "temp": 3362, "dimming": 69, "schdPsetId": 5}} + if (!jobject.has("params")) { + throw new JsonParseException("syncPilot received, but no params object present"); + } + WizSyncState parsedPParam = context.deserialize(jobject.getAsJsonObject("params"), + WizSyncState.class); + if (parsedPParam.mac.isEmpty()) { + throw new JsonParseException("syncPilot received, but no MAC address present"); + } + deserializedResponse.setWizResponseMacAddress(parsedPParam.mac); + deserializedResponse.setResultSucess(true); + deserializedResponse.setSyncParams(parsedPParam); + logger.trace("syncPilot result deserialized with mac {}", parsedPParam.mac); + break; + + case SetSystemConfig: + // ?? I'm not trying this at home! + case SetWifiConfig: + // ?? I'm not trying this at home! + case GetWifiConfig: + // The returns an encrypted string and I'm not using it so I'm not bothering to parse it + // {"method":"getWifiConfig","id":22,"env":"pro","result":{:["longStringInEncryptedUnicode"]}} + case UnknownMethod: + // This should just never happen + break; + } + } + + return deserializedResponse; + } +} diff --git a/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..4878c61dfc82c --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,11 @@ + + + + binding + WiZ Binding + Binding for WiZ smart devices. + local + + diff --git a/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000000..bcea53ad47031 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,42 @@ + + + + + + + MAC address of the device + true + + + + network-address + IP address of the device + true + + + + Update time interval in seconds to request the status of the device while it is connected to the + network. + 60 + true + + + + True to request continuous 5s heartbeats from device. Update interval is ignored when using heartbeats. + true + false + + + + Interval in minutes between attempts to reconnect with a device that is no longer responding to status + queries. When the device first connects to the network, it should send out a firstBeat message allowing openHAB to + immediately detect it. This is only as a back-up to re-find the device. + 15 + true + + + + diff --git a/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/i18n/wiz.properties b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/i18n/wiz.properties new file mode 100644 index 0000000000000..620005071a551 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/i18n/wiz.properties @@ -0,0 +1,116 @@ +# add-on + +addon.wiz.name = WiZ Binding +addon.wiz.description = Binding for WiZ smart devices. + +# thing types + +thing-type.wiz.color-bulb.label = WiZ Color Bulb +thing-type.wiz.color-bulb.description = Supports WiZ Full Color with Tunable White Bulbs +thing-type.wiz.dimmable-bulb.label = WiZ Dimmable Bulb +thing-type.wiz.dimmable-bulb.description = Supports WiZ Single Color Dimmable Bulbs +thing-type.wiz.fan-with-dimmable-bulb.label = WiZ Ceiling Fan With Dimmable Bulb +thing-type.wiz.fan-with-dimmable-bulb.description = Supports WiZ Ceiling Fans With a Dimmable Bulb +thing-type.wiz.fan.label = WiZ Ceiling Fan +thing-type.wiz.fan.description = Supports WiZ Ceiling Fans +thing-type.wiz.plug.label = WiZ Smart Plug +thing-type.wiz.plug.description = Supports WiZ Smart Plugs +thing-type.wiz.tunable-bulb.label = WiZ Tunable Bulb +thing-type.wiz.tunable-bulb.description = Supports WiZ Tunable White Bulbs + +# thing types config + +thing-type.config.wiz.device.ipAddress.label = IP Address +thing-type.config.wiz.device.ipAddress.description = IP address of the device +thing-type.config.wiz.device.macAddress.label = MAC Address +thing-type.config.wiz.device.macAddress.description = MAC address of the device +thing-type.config.wiz.device.reconnectInterval.label = Reconnect Interval +thing-type.config.wiz.device.reconnectInterval.description = Interval in minutes between attempts to reconnect with a device that is no longer responding to status queries. When the device first connects to the network, it should send out a firstBeat message allowing openHAB to immediately detect it. This is only as a back-up to re-find the device. +thing-type.config.wiz.device.updateInterval.label = Update Interval +thing-type.config.wiz.device.updateInterval.description = Update time interval in seconds to request the status of the device while it is connected to the network. +thing-type.config.wiz.device.useHeartBeats.label = Use Heartbeats +thing-type.config.wiz.device.useHeartBeats.description = True to request continuous 5s heartbeats from device. Update interval is ignored when using heartbeats. + +# channel group types + +channel-group-type.wiz.device-channels.label = Device +channel-group-type.wiz.dimmable-light.label = Light +channel-group-type.wiz.fan-group.label = Fan + +# channel types + +channel-type.wiz.fan-mode.label = Mode +channel-type.wiz.fan-mode.state.option.1 = Normal +channel-type.wiz.fan-mode.state.option.2 = Breeze +channel-type.wiz.fan-reverse.label = Reverse +channel-type.wiz.fan-reverse.state.option.OFF = Forward +channel-type.wiz.fan-reverse.state.option.ON = Reverse +channel-type.wiz.fan-speed.label = Fan Speed +channel-type.wiz.fan-speed.description = Speed of the fan, in arbitrary steps +channel-type.wiz.last-update.label = Last Update +channel-type.wiz.last-update.description = Timestamp of last status update +channel-type.wiz.light-mode-speed.label = Dynamic Light Mode Speed +channel-type.wiz.light-mode-speed.description = Speed of color/intensity changes in dynamic light modes +channel-type.wiz.light-mode.label = Light Mode +channel-type.wiz.light-mode.state.option.1 = Ocean +channel-type.wiz.light-mode.state.option.2 = Romance +channel-type.wiz.light-mode.state.option.3 = Sunset +channel-type.wiz.light-mode.state.option.4 = Party +channel-type.wiz.light-mode.state.option.5 = Fireplace +channel-type.wiz.light-mode.state.option.6 = Cozy White +channel-type.wiz.light-mode.state.option.7 = Forest +channel-type.wiz.light-mode.state.option.8 = Pastel Colors +channel-type.wiz.light-mode.state.option.9 = Wakeup +channel-type.wiz.light-mode.state.option.10 = Bed Time +channel-type.wiz.light-mode.state.option.11 = Warm White +channel-type.wiz.light-mode.state.option.12 = Daylight +channel-type.wiz.light-mode.state.option.13 = Cool White +channel-type.wiz.light-mode.state.option.14 = Night Light +channel-type.wiz.light-mode.state.option.15 = Focus +channel-type.wiz.light-mode.state.option.16 = Relax +channel-type.wiz.light-mode.state.option.17 = True Colors +channel-type.wiz.light-mode.state.option.18 = TV Time +channel-type.wiz.light-mode.state.option.19 = Plant Growth +channel-type.wiz.light-mode.state.option.20 = Spring +channel-type.wiz.light-mode.state.option.21 = Summer +channel-type.wiz.light-mode.state.option.22 = Fall +channel-type.wiz.light-mode.state.option.23 = Deep Dive +channel-type.wiz.light-mode.state.option.24 = Jungle +channel-type.wiz.light-mode.state.option.25 = Mojito +channel-type.wiz.light-mode.state.option.26 = Club +channel-type.wiz.light-mode.state.option.27 = Christmas +channel-type.wiz.light-mode.state.option.28 = Halloween +channel-type.wiz.light-mode.state.option.29 = Candlelight +channel-type.wiz.light-mode.state.option.30 = Golden White +channel-type.wiz.light-mode.state.option.31 = Pulse +channel-type.wiz.light-mode.state.option.32 = Steampunk +channel-type.wiz.rssi.label = RSSI +channel-type.wiz.rssi.description = WiFi Received Signal Strength Indicator + +# thing types config + +thing-type.config.wiz.light.ipAddress.label = Bulb IP Address +thing-type.config.wiz.light.ipAddress.description = IP address of the bulb +thing-type.config.wiz.light.bulbMacAddress.label = Bulb MAC Address +thing-type.config.wiz.light.bulbMacAddress.description = MAC address of the bulb +thing-type.config.wiz.light.reconnectInterval.label = Reconnect Interval +thing-type.config.wiz.light.reconnectInterval.description = Interval in minutes between attempts to reconnect with a bulb that is no longer responding to status queries. When the bulb first connects to the network, it should send out a firstBeat message allowing OpenHab to immediately detect it. This is only as a back-up to re-find the bulb. +thing-type.config.wiz.light.updateInterval.label = Update Interval +thing-type.config.wiz.light.updateInterval.description = Update time interval in seconds to request the status of the bulb while it is connected to the network. +thing-type.config.wiz.light.useHeartBeats.label = Use Heartbeats +thing-type.config.wiz.light.useHeartBeats.description = True to request continuous 5s heartbeats from bulb. Update interval is ignored when using heartbeats. + +# channel types + +channel-type.wiz.speed.label = Dynamic Light Mode Speed +channel-type.wiz.speed.description = Speed of color/intensity changes in dynamic light modes + +# thing types + +thing-type.wiz.ceiling-fan.label = WiZ Ceiling Fan +thing-type.wiz.ceiling-fan.description = Supports WiZ Ceiling Fans + +# channel types + +channel-type.wiz.color-temperature-abs.label = Color Temperature +channel-type.wiz.color-temperature-abs.description = Controls the color temperature of the light in Kelvin diff --git a/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..9b6b103643dc4 --- /dev/null +++ b/bundles/org.openhab.binding.wiz/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,222 @@ + + + + + + Supports WiZ Full Color with Tunable White Bulbs + + + + + + + + + + + + + + + + + Supports WiZ Tunable White Bulbs + + + + + + + + + + + + + + + + + Supports WiZ Single Color Dimmable Bulbs + + + + + + + + + + + + + + + Supports WiZ Smart Plugs + + + + + + + + + + + + + Supports WiZ Ceiling Fans + + + + + + + + + + + + + + + + Supports WiZ Ceiling Fans With a Dimmable Bulb + + + + + + + + + + + String + + + Lighting + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dimmer + + Speed of color/intensity changes in dynamic light modes + + Lighting + + + + + Number + + Speed of the fan, in arbitrary steps + Fan + + + + + Switch + + + + + + + + + + + Number + + + + + + + + + + + DateTime + + Timestamp of last status update + Time + + + + + Number:Dimensionless + + WiFi Received Signal Strength Indicator + QualityOfService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index df87fecff2612..f8e41e838d9a3 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -454,6 +454,7 @@ org.openhab.binding.wemo org.openhab.binding.wifiled org.openhab.binding.windcentrale + org.openhab.binding.wiz org.openhab.binding.wlanthermo org.openhab.binding.wled org.openhab.binding.wolfsmartset