]> git.basschouten.com Git - openhab-addons.git/commitdiff
[deconz] add group support (#8715)
authorJ-N-K <J-N-K@users.noreply.github.com>
Sat, 31 Oct 2020 16:17:06 +0000 (17:17 +0100)
committerGitHub <noreply@github.com>
Sat, 31 Oct 2020 16:17:06 +0000 (09:17 -0700)
* add group message

Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
28 files changed:
CODEOWNERS
bundles/org.openhab.binding.deconz/README.md
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/Util.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/BridgeFullState.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/DeconzBaseMessage.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupAction.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupMessage.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupState.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/Scene.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/GroupThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnection.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketMessageListener.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupType.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupTypeDeserializer.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/LightTypeDeserializer.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceType.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceTypeDeserializer.java [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/group-thing-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/light-thing-types.xml
bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java

index ab7d0b6686eb9bb401e4be1cefd23f0ee4cbe686..7222bf6c389ccdfe6d67e16afaeb31fbb46ab160 100644 (file)
@@ -44,7 +44,7 @@
 /bundles/org.openhab.binding.daikin/ @caffineehacker
 /bundles/org.openhab.binding.danfossairunit/ @pravussum
 /bundles/org.openhab.binding.darksky/ @cweitkamp
-/bundles/org.openhab.binding.deconz/ @davidgraeff
+/bundles/org.openhab.binding.deconz/ @J-N-K
 /bundles/org.openhab.binding.denonmarantz/ @jwveldhuis
 /bundles/org.openhab.binding.digiplex/ @rmichalak
 /bundles/org.openhab.binding.digitalstrom/ @MichaelOchel @msiegele
index d6a19d6f7abe99d30bc8ec63a78a41491ec01ebc..188da3cb0f6e6d39b7fb3ca272de396ca5da8af4 100644 (file)
@@ -42,10 +42,12 @@ Additionally lights, window coverings (blinds) and thermostats are supported:
 | Thermostat                           | ZHAThermostat                                 | `thermostat`            |
 | Warning Device (Siren)               | Warning device                                | `warningdevice`         |
 
+Currently only light-groups are supported via the thing-type `lightgroup`.
+
 ## Discovery
 
 deCONZ software instances are discovered automatically in the same subnet.
-Sensors, switches, lights and blinds are discovered as soon as a `deconz` bridge thing comes online.
+Sensors, switches, groups, lights and blinds are discovered as soon as a `deconz` bridge thing comes online.
 If your device is not discovered, please check the DEBUG log for unknown devices and report your findings.
 
 ## Thing Configuration
@@ -81,13 +83,11 @@ Due to limitations in the API of deCONZ, the `lastSeen` channel (available some
 Allowed values are all positive integers, the unit is minutes.
 The default-value is `0`, which means "no polling at all".
 
-
 `dimmablelight`, `extendedcolorlight`, `colorlight` and `colortemperaturelight` have an additional optional parameter `transitiontime`.
 The transition time is the time to move between two states and is configured in seconds.
 The resolution provided is 1/10s.
 If no value is provided, the default value of the device is used.
 
-
 ### Textual Thing Configuration - Retrieving an API Key
 
 If you use the textual configuration, the thing file without an API key will look like this, for example:
@@ -154,18 +154,23 @@ The `last_seen` channel is added when it is available AND the `lastSeenPolling`
 
 Other devices support
 
-| Channel Type ID   | Item Type                | Access Mode | Description                           | Thing types                                   |
-|-------------------|--------------------------|:-----------:|---------------------------------------|-----------------------------------------------|
-| brightness        | Dimmer                   |     R/W     | Brightness of the light               | `dimmablelight`, `colortemperaturelight`      |                                 
-| switch            | Switch                   |     R/W     | State of a ON/OFF device              | `onofflight`                                  |
-| color             | Color                    |     R/W     | Color of an multi-color light         | `colorlight`, `extendedcolorlight`            |
-| color_temperature | Number                   |     R/W     | Color temperature in kelvin. The value range is determined by each individual light          | `colortemperaturelight`, `extendedcolorlight` |
-| position          | Rollershutter            |     R/W     | Position of the blind                 | `windowcovering`                              |
-| heatsetpoint      | Number:Temperature       |     R/W     | Target Temperature in °C              | `thermostat`                                  |
-| valve             | Number:Dimensionless     |     R       | Valve position in %                   | `thermostat`                                  |
-| mode              | String                   |     R/W     | Mode: "auto", "heat" and "off"        | `thermostat`                                  |
-| offset            | Number                   |     R       | Temperature offset for sensor         | `thermostat`                                  |
-| alert             | Switch                   |     R/W     | Turn alerts on/off                    | `warningdevice`                               |
+| Channel Type ID   | Item Type                | Access Mode | Description                           | Thing types                                     |
+|-------------------|--------------------------|:-----------:|---------------------------------------|-------------------------------------------------|
+| brightness        | Dimmer                   |     R/W     | Brightness of the light               | `dimmablelight`, `colortemperaturelight`        |                                 
+| switch            | Switch                   |     R/W     | State of a ON/OFF device              | `onofflight`                                    |
+| color             | Color                    |     R/W     | Color of an multi-color light         | `colorlight`, `extendedcolorlight`, `lightgroup`|
+| color_temperature | Number                   |     R/W     | Color temperature in kelvin. The value range is determined by each individual light          | `colortemperaturelight`, `extendedcolorlight`, `lightgroup` |
+| position          | Rollershutter            |     R/W     | Position of the blind                 | `windowcovering`                                |
+| heatsetpoint      | Number:Temperature       |     R/W     | Target Temperature in °C              | `thermostat`                                    |
+| valve             | Number:Dimensionless     |     R       | Valve position in %                   | `thermostat`                                    |
+| mode              | String                   |     R/W     | Mode: "auto", "heat" and "off"        | `thermostat`                                    |
+| offset            | Number                   |     R       | Temperature offset for sensor         | `thermostat`                                    |
+| alert             | Switch                   |     R/W     | Turn alerts on/off                    | `warningdevice`, `lightgroup`                   |
+| all_on            | Switch                   |     R       | All lights in group are on            | `lightgroup`                                    |
+| any_on            | Switch                   |     R       | Any light in group is on              | `lightgroup`                                    |
+
+**NOTE:** For groups `color` and `color_temperature`  are used for sending commands to the group.
+Their state represents the last command send to the group, not necessarily the actual state of the group.
 
 ### Trigger Channels
 
@@ -207,6 +212,7 @@ Bridge deconz:deconz:homeserver [ host="192.168.0.10", apikey="ABCDEFGHIJ" ] {
     waterleakagesensor  basement-water-leakage  "Basement Water Leakage"    [ id="7" ]
     alarmsensor         basement-alarm          "Basement Alarm Sensor"     [ id="8", lastSeenPolling=5 ]
     dimmablelight       livingroom-ceiling      "Livingroom Ceiling"        [ id="1" ]
+    lightgroup          livingroom              "Livingroom"                [ id="1" ]
 }
 ```
 
@@ -221,6 +227,7 @@ Contact                 Livingroom_Window       "Window Livingroom [%s]"
 Switch                  Basement_Water_Leakage  "Basement Water Leakage [%s]"                       { channel="deconz:waterleakagesensor:homeserver:basement-water-leakage:waterleakage" }
 Switch                  Basement_Alarm          "Basement Alarm Triggered [%s]"                     { channel="deconz:alarmsensor:homeserver:basement-alarm:alarm" }
 Dimmer                  Livingroom_Ceiling      "Livingroom Ceiling [%d]"           <light>         { channel="deconz:dimmablelight:homeserver:livingroom-ceiling:brightness" }                 
+Color                   Livingroom              "Livingroom Light Control"
 ```
 
 ### Events
index 3edf3a01a1dea7851b6a4e53752eaf6c84a11289..123f86000c8a14619831c4a73a69648e365092e0 100644 (file)
@@ -23,7 +23,6 @@ import org.openhab.core.thing.ThingTypeUID;
  */
 @NonNullByDefault
 public class BindingConstants {
-
     public static final String BINDING_ID = "deconz";
 
     // List of all Thing Type UIDs
@@ -63,7 +62,10 @@ public class BindingConstants {
     public static final ThingTypeUID THING_TYPE_WINDOW_COVERING = new ThingTypeUID(BINDING_ID, "windowcovering");
     public static final ThingTypeUID THING_TYPE_WARNING_DEVICE = new ThingTypeUID(BINDING_ID, "warningdevice");
 
-    // List of all Channel ids
+    // groups
+    public static final ThingTypeUID THING_TYPE_LIGHTGROUP = new ThingTypeUID(BINDING_ID, "lightgroup");
+
+    // sensor channel ids
     public static final String CHANNEL_PRESENCE = "presence";
     public static final String CHANNEL_LAST_UPDATED = "last_updated";
     public static final String CHANNEL_LAST_SEEN = "last_seen";
@@ -98,19 +100,22 @@ public class BindingConstants {
     public static final String CHANNEL_TEMPERATURE_OFFSET = "offset";
     public static final String CHANNEL_VALVE_POSITION = "valve";
 
+    // group + light channel ids
     public static final String CHANNEL_SWITCH = "switch";
     public static final String CHANNEL_BRIGHTNESS = "brightness";
     public static final String CHANNEL_COLOR_TEMPERATURE = "color_temperature";
     public static final String CHANNEL_COLOR = "color";
     public static final String CHANNEL_POSITION = "position";
     public static final String CHANNEL_ALERT = "alert";
+    public static final String CHANNEL_ALL_ON = "all_on";
+    public static final String CHANNEL_ANY_ON = "any_on";
 
     // Thing configuration
     public static final String CONFIG_HOST = "host";
     public static final String CONFIG_HTTP_PORT = "httpPort";
     public static final String CONFIG_APIKEY = "apikey";
     public static final String PROPERTY_UDN = "UDN";
-
+    public static final String CONFIG_ID = "id";
     public static final String UNIQUE_ID = "uid";
 
     public static final String PROPERTY_CT_MIN = "ctmin";
@@ -121,4 +126,7 @@ public class BindingConstants {
     public static final int ZCL_CT_MIN = 1;
     public static final int ZCL_CT_MAX = 65279; // 0xFEFF
     public static final int ZCL_CT_INVALID = 65535; // 0xFFFF
+
+    public static final double HUE_FACTOR = 65535 / 360.0;
+    public static final double BRIGHTNESS_FACTOR = 2.54;
 }
index 5cda9603a5e7c2fd2ebe59049d62f14a61beab0b..273e1b6389b3fd62282d47287641e6f7e44ac064 100644 (file)
@@ -18,15 +18,9 @@ import java.util.stream.Stream;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
-import org.openhab.binding.deconz.internal.handler.LightThingHandler;
-import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler;
-import org.openhab.binding.deconz.internal.handler.SensorThingHandler;
+import org.openhab.binding.deconz.internal.handler.*;
 import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
-import org.openhab.binding.deconz.internal.types.LightType;
-import org.openhab.binding.deconz.internal.types.LightTypeDeserializer;
-import org.openhab.binding.deconz.internal.types.ThermostatMode;
-import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter;
+import org.openhab.binding.deconz.internal.types.*;
 import org.openhab.core.io.net.http.HttpClientFactory;
 import org.openhab.core.io.net.http.WebSocketFactory;
 import org.openhab.core.thing.Bridge;
@@ -53,7 +47,8 @@ import com.google.gson.GsonBuilder;
 public class DeconzHandlerFactory extends BaseThingHandlerFactory {
     private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
             .of(DeconzBridgeHandler.SUPPORTED_THING_TYPES, LightThingHandler.SUPPORTED_THING_TYPE_UIDS,
-                    SensorThingHandler.SUPPORTED_THING_TYPES, SensorThermostatThingHandler.SUPPORTED_THING_TYPES)
+                    SensorThingHandler.SUPPORTED_THING_TYPES, SensorThermostatThingHandler.SUPPORTED_THING_TYPES,
+                    GroupThingHandler.SUPPORTED_THING_TYPE_UIDS)
             .flatMap(Set::stream).collect(Collectors.toSet());
 
     private final Gson gson;
@@ -71,6 +66,8 @@ public class DeconzHandlerFactory extends BaseThingHandlerFactory {
 
         GsonBuilder gsonBuilder = new GsonBuilder();
         gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
+        gsonBuilder.registerTypeAdapter(GroupType.class, new GroupTypeDeserializer());
+        gsonBuilder.registerTypeAdapter(ResourceType.class, new ResourceTypeDeserializer());
         gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
         gson = gsonBuilder.create();
     }
@@ -93,6 +90,8 @@ public class DeconzHandlerFactory extends BaseThingHandlerFactory {
             return new SensorThingHandler(thing, gson);
         } else if (SensorThermostatThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
             return new SensorThermostatThingHandler(thing, gson);
+        } else if (GroupThingHandler.SUPPORTED_THING_TYPE_UIDS.contains(thingTypeUID)) {
+            return new GroupThingHandler(thing, gson);
         }
 
         return null;
index e8379a331819914fb6e3284b70122a464941e562..3f662bdcb2528ee552ec6cdd5c2da590b13dd21e 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.deconz.internal;
 
+import static org.openhab.binding.deconz.internal.BindingConstants.BRIGHTNESS_FACTOR;
+
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
@@ -22,6 +24,9 @@ import java.util.stream.Stream;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.PercentType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * The {@link Util} class defines common utility methods
@@ -30,6 +35,8 @@ import org.openhab.core.library.types.DateTimeType;
  */
 @NonNullByDefault
 public class Util {
+    private static final Logger LOGGER = LoggerFactory.getLogger(Util.class);
+
     public static String buildUrl(String host, int port, String... urlParts) {
         StringBuilder url = new StringBuilder();
         url.append("http://");
@@ -54,6 +61,33 @@ public class Util {
         return Math.max(min, Math.min(intValue, max));
     }
 
+    /**
+     * convert a brightness value from int to PercentType
+     *
+     * @param val the value
+     * @return the corresponding PercentType value
+     */
+    public static PercentType toPercentType(int val) {
+        int scaledValue = (int) Math.ceil(val / BRIGHTNESS_FACTOR);
+        if (scaledValue < 0 || scaledValue > 100) {
+            LOGGER.trace("received value {} (converted to {}). Coercing.", val, scaledValue);
+            scaledValue = scaledValue < 0 ? 0 : scaledValue;
+            scaledValue = scaledValue > 100 ? 100 : scaledValue;
+        }
+
+        return new PercentType(scaledValue);
+    }
+
+    /**
+     * convert a brightness value from PercentType to int
+     *
+     * @param val the value
+     * @return the corresponding int value
+     */
+    public static int fromPercentType(PercentType val) {
+        return (int) Math.floor(val.doubleValue() * BRIGHTNESS_FACTOR);
+    }
+
     /**
      * convert a timestamp string to a DateTimeType
      *
index 28497b70d06383fdaf9c3e0ed3bc9e1bf898b907..5cfbfeb876b8f88f781399a5319c38e66fb77a91 100644 (file)
@@ -26,12 +26,14 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.deconz.internal.Util;
 import org.openhab.binding.deconz.internal.dto.BridgeFullState;
+import org.openhab.binding.deconz.internal.dto.GroupMessage;
 import org.openhab.binding.deconz.internal.dto.LightMessage;
 import org.openhab.binding.deconz.internal.dto.SensorMessage;
 import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
 import org.openhab.binding.deconz.internal.handler.LightThingHandler;
 import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler;
 import org.openhab.binding.deconz.internal.handler.SensorThingHandler;
+import org.openhab.binding.deconz.internal.types.GroupType;
 import org.openhab.binding.deconz.internal.types.LightType;
 import org.openhab.core.config.discovery.AbstractDiscoveryService;
 import org.openhab.core.config.discovery.DiscoveryResult;
@@ -93,12 +95,53 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
     }
 
     /**
-     * Add a sensor device to the discovery inbox.
+     * Add a group to the discovery inbox.
+     *
+     * @param groupId The id of the light
+     * @param group The group description
+     */
+    private void addGroup(String groupId, GroupMessage group) {
+        final ThingUID bridgeUID = this.bridgeUID;
+        if (bridgeUID == null) {
+            logger.warn("Received a message from non-existent bridge. This most likely is a bug.");
+            return;
+        }
+
+        ThingTypeUID thingTypeUID;
+        GroupType groupType = group.type;
+
+        if (groupType == null) {
+            logger.warn("No group type reported for group {} ({})", group.modelid, group.name);
+            return;
+        }
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(CONFIG_ID, groupId);
+
+        switch (groupType) {
+            case LIGHT_GROUP:
+                thingTypeUID = THING_TYPE_LIGHTGROUP;
+                break;
+            default:
+                logger.debug(
+                        "Found group: {} ({}), type {} but no thing type defined for that type. This should be reported.",
+                        group.id, group.name, group.type);
+                return;
+        }
+
+        ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, group.id);
+        DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID).withLabel(group.name)
+                .withProperties(properties).withRepresentationProperty(CONFIG_ID).build();
+        thingDiscovered(discoveryResult);
+    }
+
+    /**
+     * Add a light device to the discovery inbox.
      *
-     * @param lightID The id of the light
-     * @param light The sensor description
+     * @param lightId The id of the light
+     * @param light The light description
      */
-    private void addLight(String lightID, LightMessage light) {
+    private void addLight(String lightId, LightMessage light) {
         final ThingUID bridgeUID = this.bridgeUID;
         if (bridgeUID == null) {
             logger.warn("Received a message from non-existent bridge. This most likely is a bug.");
@@ -114,7 +157,7 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
         }
 
         Map<String, Object> properties = new HashMap<>();
-        properties.put("id", lightID);
+        properties.put(CONFIG_ID, lightId);
         properties.put(UNIQUE_ID, light.uniqueid);
         properties.put(Thing.PROPERTY_FIRMWARE_VERSION, light.swversion);
         properties.put(Thing.PROPERTY_VENDOR, light.manufacturername);
@@ -227,7 +270,7 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
         ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, sensor.uniqueid.replaceAll("[^a-z0-9\\[\\]]", ""));
 
         DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
-                .withLabel(sensor.name + " (" + sensor.manufacturername + ")").withProperty("id", sensorID)
+                .withLabel(sensor.name + " (" + sensor.manufacturername + ")").withProperty(CONFIG_ID, sensorID)
                 .withProperty(UNIQUE_ID, sensor.uniqueid).withRepresentationProperty(UNIQUE_ID).build();
         thingDiscovered(discoveryResult);
     }
@@ -268,6 +311,7 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
         if (fullState != null) {
             fullState.sensors.forEach(this::addSensor);
             fullState.lights.forEach(this::addLight);
+            fullState.groups.forEach(this::addGroup);
         }
     }
 }
index 406f9a8e2c5ced7ef202cea5882675dca55d9843..2aa2585021f5a5429b5917ea7e8a752ffba52d7f 100644 (file)
@@ -41,4 +41,5 @@ public class BridgeFullState {
 
     public Map<String, SensorMessage> sensors = Collections.emptyMap();
     public Map<String, LightMessage> lights = Collections.emptyMap();
+    public Map<String, GroupMessage> groups = Collections.emptyMap();
 }
index c7a70c56f2382e7b61e9b871de9c6f004a969b0d..2c1a263b487e1f116b5a37cb7d7874dfa6b2f6a1 100644 (file)
@@ -14,6 +14,7 @@ package org.openhab.binding.deconz.internal.dto;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.deconz.internal.types.ResourceType;
 
 /**
  * The REST interface and websocket connection are using the same fields.
@@ -25,7 +26,7 @@ import org.eclipse.jdt.annotation.Nullable;
 public class DeconzBaseMessage {
     // For websocket change events
     public String e = ""; // "changed"
-    public String r = ""; // "sensors"
+    public ResourceType r = ResourceType.UNKNOWN; // "sensors"
     public String t = ""; // "event"
     public String id = ""; // "3"
 
diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupAction.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupAction.java
new file mode 100644 (file)
index 0000000..636cbbe
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2020 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.deconz.internal.dto;
+
+import java.util.Arrays;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link GroupAction} is send by the websocket connection as well as the Rest API.
+ * It is part of a {@link GroupMessage}.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class GroupAction {
+    public @Nullable Boolean on;
+    public @Nullable Boolean toggle;
+    public @Nullable Integer bri;
+    public @Nullable Integer hue;
+    public @Nullable Integer sat;
+    public @Nullable Integer ct;
+    public double @Nullable [] xy;
+    public @Nullable String alert;
+    public @Nullable String effect;
+    public @Nullable Integer colorloopspeed;
+    public @Nullable Integer transitiontime;
+
+    @Override
+    public String toString() {
+        return "GroupAction{" + "on=" + on + ", toggle=" + toggle + ", bri=" + bri + ", hue=" + hue + ", sat=" + sat
+                + ", ct=" + ct + ", xy=" + Arrays.toString(xy) + ", alert='" + alert + '\'' + ", effect='" + effect
+                + '\'' + ", colorloopspeed=" + colorloopspeed + ", transitiontime=" + transitiontime + '}';
+    }
+}
diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupMessage.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupMessage.java
new file mode 100644 (file)
index 0000000..307d516
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2020 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.deconz.internal.dto;
+
+import java.util.Arrays;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.deconz.internal.types.GroupType;
+
+/**
+ * The REST interface and websocket connection are using the same fields.
+ * The REST data contains more descriptive info like the manufacturer and name.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class GroupMessage extends DeconzBaseMessage {
+    public @Nullable GroupAction action;
+    public String @Nullable [] devicemembership;
+    public @Nullable Boolean hidden;
+    public String @Nullable [] lights;
+    public String @Nullable [] lightsequence;
+    public String @Nullable [] multideviceids;
+    public Scene @Nullable [] scenes;
+    public @Nullable GroupState state;
+    public @Nullable GroupType type;
+
+    @Override
+    public String toString() {
+        return "GroupMessage{" + "action=" + action + ", devicemembership=" + Arrays.toString(devicemembership)
+                + ", hidden=" + hidden + ", lights=" + Arrays.toString(lights) + ", lightsequence="
+                + Arrays.toString(lightsequence) + ", multideviceids=" + Arrays.toString(multideviceids) + ", scenes="
+                + Arrays.toString(scenes) + ", state=" + state + ", type=" + type + '}';
+    }
+}
diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupState.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/GroupState.java
new file mode 100644 (file)
index 0000000..bd13812
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2010-2020 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.deconz.internal.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link GroupState} is send by the websocket connection as well as the Rest API.
+ * It is part of a {@link GroupMessage}.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class GroupState {
+    public boolean all_on;
+    public boolean any_on;
+
+    @Override
+    public String toString() {
+        return "GroupState{" + "all_on=" + all_on + ", any_on=" + any_on + '}';
+    }
+}
diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/Scene.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/Scene.java
new file mode 100644 (file)
index 0000000..f6b1911
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2020 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.deconz.internal.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Scene} is send by the websocket connection as well as the Rest API.
+ * It is part of a {@link GroupMessage}.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Scene {
+    public String id = "";
+    public String name = "";
+}
index 4ecf2f27bcb5dac0a76d9cc79521da469d57e41c..8125b2cde97f30237dfea130eba37ce9f1af8667 100644 (file)
@@ -26,12 +26,10 @@ import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
 import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
 import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
 import org.openhab.binding.deconz.internal.netutils.WebSocketMessageListener;
-import org.openhab.core.thing.Bridge;
-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.binding.deconz.internal.types.ResourceType;
+import org.openhab.core.thing.*;
 import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -42,9 +40,7 @@ import com.google.gson.Gson;
  *
  * It waits for the bridge to come online, grab the websocket connection and bridge configuration
  * and registers to the websocket connection as a listener.
- *
- * A REST API call is made to get the initial light/rollershutter state.
- *
+ **
  * @author David Graeff - Initial contribution
  * @author Jan N. Klug - Refactored to abstract class
  */
@@ -52,6 +48,7 @@ import com.google.gson.Gson;
 public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extends BaseThingHandler
         implements WebSocketMessageListener {
     private final Logger logger = LoggerFactory.getLogger(DeconzBaseThingHandler.class);
+    protected final ResourceType resourceType;
     protected ThingConfig config = new ThingConfig();
     protected DeconzBridgeConfig bridgeConfig = new DeconzBridgeConfig();
     protected final Gson gson;
@@ -59,9 +56,10 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
     protected @Nullable WebSocketConnection connection;
     protected @Nullable AsyncHttpClient http;
 
-    public DeconzBaseThingHandler(Thing thing, Gson gson) {
+    public DeconzBaseThingHandler(Thing thing, Gson gson, ResourceType resourceType) {
         super(thing);
         this.gson = gson;
+        this.resourceType = resourceType;
     }
 
     /**
@@ -75,9 +73,19 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
         }
     }
 
-    protected abstract void registerListener();
+    private void registerListener() {
+        WebSocketConnection conn = connection;
+        if (conn != null) {
+            conn.registerListener(resourceType, config.id, this);
+        }
+    }
 
-    protected abstract void unregisterListener();
+    private void unregisterListener() {
+        WebSocketConnection conn = connection;
+        if (conn != null) {
+            conn.unregisterListener(resourceType, config.id);
+        }
+    }
 
     @Override
     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
@@ -86,39 +94,38 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
             return;
         }
 
-        if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
-            unregisterListener();
-            return;
-        }
-
-        if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
-            return;
-        }
-
-        Bridge bridge = getBridge();
-        if (bridge == null) {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
-            return;
-        }
-        DeconzBridgeHandler bridgeHandler = (DeconzBridgeHandler) bridge.getHandler();
-        if (bridgeHandler == null) {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
-            return;
-        }
+        if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
+            // the bridge is ONLINE, we can communicate with the gateway, so we update the connection parameters and
+            // register the listener
+            Bridge bridge = getBridge();
+            if (bridge == null) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+                return;
+            }
+            DeconzBridgeHandler bridgeHandler = (DeconzBridgeHandler) bridge.getHandler();
+            if (bridgeHandler == null) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+                return;
+            }
 
-        final WebSocketConnection webSocketConnection = bridgeHandler.getWebsocketConnection();
-        this.connection = webSocketConnection;
-        this.http = bridgeHandler.getHttp();
-        this.bridgeConfig = bridgeHandler.getBridgeConfig();
+            final WebSocketConnection webSocketConnection = bridgeHandler.getWebsocketConnection();
+            this.connection = webSocketConnection;
+            this.http = bridgeHandler.getHttp();
+            this.bridgeConfig = bridgeHandler.getBridgeConfig();
 
-        updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
+            updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
 
-        // Real-time data
-        registerListener();
+            // Real-time data
+            registerListener();
 
-        // get initial values
-        requestState();
+            // get initial values
+            requestState();
+        } else {
+            // if the bridge is not ONLINE, we assume communication is not possible, so we unregister the listener and
+            // set the thing status to OFFLINE
+            unregisterListener();
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+        }
     }
 
     protected abstract @Nullable T parseStateResponse(AsyncHttpClient.Result r);
@@ -132,21 +139,17 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
      */
     protected abstract void processStateResponse(@Nullable T stateResponse);
 
-    /**
-     * call requestState(type) in this method only
-     */
-    protected abstract void requestState();
-
     /**
      * Perform a request to the REST API for retrieving the full light state with all data and configuration.
      */
-    protected void requestState(String type) {
+    protected void requestState() {
         AsyncHttpClient asyncHttpClient = http;
         if (asyncHttpClient == null) {
             return;
         }
 
-        String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey, type, config.id);
+        String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey,
+                resourceType.getIdentifier(), config.id);
         logger.trace("Requesting URL for initial data: {}", url);
 
         // Get initial data
@@ -165,13 +168,42 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
         }).thenAccept(this::processStateResponse);
     }
 
+    /**
+     * sends a command to the bridge
+     *
+     * @param object must be serializable and contain the command
+     * @param originalCommand the original openHAB command (used for logging purposes)
+     * @param channelUID the channel that this command was send to (used for logging purposes)
+     * @param acceptProcessing additional processing after the command was successfully send (might be null)
+     */
+    protected void sendCommand(Object object, Command originalCommand, ChannelUID channelUID,
+            @Nullable Runnable acceptProcessing) {
+        AsyncHttpClient asyncHttpClient = http;
+        if (asyncHttpClient == null) {
+            return;
+        }
+        String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey,
+                resourceType.getIdentifier(), config.id, resourceType.getCommandUrl());
+
+        String json = gson.toJson(object);
+        logger.trace("Sending {} to {} {} via {}", json, resourceType, config.id, url);
+
+        asyncHttpClient.put(url, json, bridgeConfig.timeout).thenAccept(v -> {
+            if (acceptProcessing != null) {
+                acceptProcessing.run();
+            }
+            logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody());
+        }).exceptionally(e -> {
+            logger.debug("Sending command {} to channel {} failed: {} - {}", originalCommand, channelUID, e.getClass(),
+                    e.getMessage());
+            return null;
+        });
+    }
+
     @Override
     public void dispose() {
         stopInitializationJob();
-        WebSocketConnection webSocketConnection = connection;
-        if (webSocketConnection != null) {
-            webSocketConnection.unregisterLightListener(config.id);
-        }
+        unregisterListener();
         super.dispose();
     }
 
diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/GroupThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/GroupThingHandler.java
new file mode 100644 (file)
index 0000000..4d1acae
--- /dev/null
@@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2010-2020 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.deconz.internal.handler;
+
+import static org.openhab.binding.deconz.internal.BindingConstants.*;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.deconz.internal.Util;
+import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
+import org.openhab.binding.deconz.internal.dto.GroupAction;
+import org.openhab.binding.deconz.internal.dto.GroupMessage;
+import org.openhab.binding.deconz.internal.dto.GroupState;
+import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
+import org.openhab.binding.deconz.internal.types.ResourceType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * This light thing doesn't establish any connections, that is done by the bridge Thing.
+ *
+ * It waits for the bridge to come online, grab the websocket connection and bridge configuration
+ * and registers to the websocket connection as a listener.
+ *
+ * A REST API call is made to get the initial light/rollershutter state.
+ *
+ * Every light and rollershutter is supported by this Thing, because a unified state is kept
+ * in {@link #groupStateCache}. Every field that got received by the REST API for this specific
+ * sensor is published to the framework.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class GroupThingHandler extends DeconzBaseThingHandler<GroupMessage> {
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_LIGHTGROUP);
+    private final Logger logger = LoggerFactory.getLogger(GroupThingHandler.class);
+
+    /**
+     * The group state.
+     */
+    private GroupState groupStateCache = new GroupState();
+
+    public GroupThingHandler(Thing thing, Gson gson) {
+        super(thing, gson, ResourceType.GROUPS);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        String channelId = channelUID.getId();
+
+        GroupAction newGroupAction = new GroupAction();
+        switch (channelId) {
+            case CHANNEL_ALL_ON:
+            case CHANNEL_ANY_ON:
+                if (command instanceof RefreshType) {
+                    valueUpdated(channelUID.getId(), groupStateCache);
+                    return;
+                }
+                break;
+            case CHANNEL_ALERT:
+                if (command instanceof OnOffType) {
+                    newGroupAction.alert = command == OnOffType.ON ? "alert" : "none";
+                } else {
+                    return;
+                }
+                break;
+            case CHANNEL_COLOR:
+                if (command instanceof HSBType) {
+                    HSBType hsbCommand = (HSBType) command;
+                    newGroupAction.bri = Util.fromPercentType(hsbCommand.getBrightness());
+                    if (newGroupAction.bri > 0) {
+                        newGroupAction.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
+                        newGroupAction.sat = Util.fromPercentType(hsbCommand.getSaturation());
+                    }
+                } else if (command instanceof PercentType) {
+                    newGroupAction.bri = Util.fromPercentType((PercentType) command);
+                } else if (command instanceof DecimalType) {
+                    newGroupAction.bri = ((DecimalType) command).intValue();
+                } else if (command instanceof OnOffType) {
+                    newGroupAction.on = OnOffType.ON.equals(command);
+                } else {
+                    return;
+                }
+                break;
+            case CHANNEL_COLOR_TEMPERATURE:
+                if (command instanceof DecimalType) {
+                    int miredValue = Util.kelvinToMired(((DecimalType) command).intValue());
+                    newGroupAction.ct = Util.constrainToRange(miredValue, ZCL_CT_MIN, ZCL_CT_MAX);
+                } else {
+                    return;
+                }
+                break;
+            default:
+                return;
+        }
+
+        if (newGroupAction.bri != null && newGroupAction.bri > 0) {
+            newGroupAction.on = true;
+        }
+
+        sendCommand(newGroupAction, command, channelUID, null);
+    }
+
+    @Override
+    protected @Nullable GroupMessage parseStateResponse(AsyncHttpClient.Result r) {
+        if (r.getResponseCode() == 403) {
+            return null;
+        } else if (r.getResponseCode() == 200) {
+            return gson.fromJson(r.getBody(), GroupMessage.class);
+        } else {
+            throw new IllegalStateException("Unknown status code " + r.getResponseCode() + " for full state request");
+        }
+    }
+
+    @Override
+    protected void processStateResponse(@Nullable GroupMessage stateResponse) {
+        if (stateResponse == null) {
+            return;
+        }
+        messageReceived(config.id, stateResponse);
+    }
+
+    private void valueUpdated(String channelId, GroupState newState) {
+        switch (channelId) {
+            case CHANNEL_ALL_ON:
+                updateState(channelId, OnOffType.from(newState.all_on));
+                break;
+            case CHANNEL_ANY_ON:
+                updateState(channelId, OnOffType.from(newState.any_on));
+                break;
+            default:
+        }
+    }
+
+    @Override
+    public void messageReceived(String sensorID, DeconzBaseMessage message) {
+        if (message instanceof GroupMessage) {
+            GroupMessage groupMessage = (GroupMessage) message;
+            logger.trace("{} received {}", thing.getUID(), groupMessage);
+            GroupState groupState = groupMessage.state;
+            if (groupState != null) {
+                updateStatus(ThingStatus.ONLINE);
+                thing.getChannels().stream().map(c -> c.getUID().getId()).forEach(c -> valueUpdated(c, groupState));
+                groupStateCache = groupState;
+            }
+        }
+    }
+}
index 6b966fb246de3ad3dbc659be8ae4340c982e72dc..8c788d901370dfb9a5c0fd27ec17e6acdac909db 100644 (file)
@@ -19,8 +19,6 @@ import java.math.BigDecimal;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -30,7 +28,7 @@ import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
 import org.openhab.binding.deconz.internal.dto.LightMessage;
 import org.openhab.binding.deconz.internal.dto.LightState;
 import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
-import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
+import org.openhab.binding.deconz.internal.types.ResourceType;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.HSBType;
 import org.openhab.core.library.types.OnOffType;
@@ -68,12 +66,10 @@ import com.google.gson.Gson;
  */
 @NonNullByDefault
 public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
-    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Stream.of(THING_TYPE_COLOR_TEMPERATURE_LIGHT,
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_COLOR_TEMPERATURE_LIGHT,
             THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_COLOR_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT, THING_TYPE_ONOFF_LIGHT,
-            THING_TYPE_WINDOW_COVERING, THING_TYPE_WARNING_DEVICE).collect(Collectors.toSet());
+            THING_TYPE_WINDOW_COVERING, THING_TYPE_WARNING_DEVICE);
 
-    private static final double HUE_FACTOR = 65535 / 360.0;
-    private static final double BRIGHTNESS_FACTOR = 2.54;
     private static final long DEFAULT_COMMAND_EXPIRY_TIME = 250; // in ms
 
     private final Logger logger = LoggerFactory.getLogger(LightThingHandler.class);
@@ -94,8 +90,7 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
     private int ctMin = ZCL_CT_MIN;
 
     public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider) {
-        super(thing, gson);
-
+        super(thing, gson, ResourceType.LIGHTS);
         this.stateDescriptionProvider = stateDescriptionProvider;
     }
 
@@ -127,27 +122,6 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
         super.initialize();
     }
 
-    @Override
-    protected void registerListener() {
-        WebSocketConnection conn = connection;
-        if (conn != null) {
-            conn.registerLightListener(config.id, this);
-        }
-    }
-
-    @Override
-    protected void unregisterListener() {
-        WebSocketConnection conn = connection;
-        if (conn != null) {
-            conn.unregisterLightListener(config.id);
-        }
-    }
-
-    @Override
-    protected void requestState() {
-        requestState("lights");
-    }
-
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
         if (command instanceof RefreshType) {
@@ -186,15 +160,15 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
                             logger.warn("Failed to convert {} to xy-values", command);
                         }
                         newLightState.xy = new double[] { xy[0].doubleValue() / 100.0, xy[1].doubleValue() / 100.0 };
-                        newLightState.bri = fromPercentType(hsbCommand.getBrightness());
+                        newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness());
                     } else {
                         // default is colormode "hs" (used when colormode "hs" is set or colormode is unknown)
-                        newLightState.bri = fromPercentType(hsbCommand.getBrightness());
+                        newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness());
                         newLightState.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
-                        newLightState.sat = fromPercentType(hsbCommand.getSaturation());
+                        newLightState.sat = Util.fromPercentType(hsbCommand.getSaturation());
                     }
                 } else if (command instanceof PercentType) {
-                    newLightState.bri = fromPercentType((PercentType) command);
+                    newLightState.bri = Util.fromPercentType((PercentType) command);
                 } else if (command instanceof DecimalType) {
                     newLightState.bri = ((DecimalType) command).intValue();
                 } else {
@@ -203,7 +177,7 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
 
                 // send on/off state together with brightness if not already set or unknown
                 Integer newBri = newLightState.bri;
-                if ((newBri != null) && ((currentOn == null) || ((newBri > 0) != currentOn))) {
+                if (newBri != null) {
                     newLightState.on = (newBri > 0);
                 }
 
@@ -222,13 +196,7 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
                 if (command instanceof DecimalType) {
                     int miredValue = kelvinToMired(((DecimalType) command).intValue());
                     newLightState.ct = constrainToRange(miredValue, ctMin, ctMax);
-
-                    if (currentOn != null && !currentOn) {
-                        // sending new color temperature is only allowed when light is on
-                        newLightState.on = true;
-                    }
-                } else {
-                    return;
+                    newLightState.on = true;
                 }
                 break;
             case CHANNEL_POSITION:
@@ -253,31 +221,17 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
                 return;
         }
 
-        AsyncHttpClient asyncHttpClient = http;
-        if (asyncHttpClient == null) {
-            return;
-        }
-        String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey, "lights", config.id,
-                "state");
-
         if (newLightState.on != null && !newLightState.on) {
             // if light shall be off, no other commands are allowed, so reset the new light state
             newLightState.clear();
             newLightState.on = false;
         }
 
-        String json = gson.toJson(newLightState);
-        logger.trace("Sending {} to light {} via {}", json, config.id, url);
-
-        asyncHttpClient.put(url, json, bridgeConfig.timeout).thenAccept(v -> {
+        sendCommand(newLightState, command, channelUID, () -> {
             lastCommandExpireTimestamp = System.currentTimeMillis()
                     + (newLightState.transitiontime != null ? newLightState.transitiontime
                             : DEFAULT_COMMAND_EXPIRY_TIME);
             lastCommand = newLightState;
-            logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody());
-        }).exceptionally(e -> {
-            logger.debug("Sending command {} to channel {} failed:", command, channelUID, e);
-            return null;
         });
     }
 
@@ -389,19 +343,4 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
             }
         }
     }
-
-    private PercentType toPercentType(int val) {
-        int scaledValue = (int) Math.ceil(val / BRIGHTNESS_FACTOR);
-        if (scaledValue < 0 || scaledValue > 100) {
-            logger.trace("received value {} (converted to {}). Coercing.", val, scaledValue);
-            scaledValue = scaledValue < 0 ? 0 : scaledValue;
-            scaledValue = scaledValue > 100 ? 100 : scaledValue;
-        }
-        logger.debug("val = '{}', scaledValue = '{}'", val, scaledValue);
-        return new PercentType(scaledValue);
-    }
-
-    private int fromPercentType(PercentType val) {
-        return (int) Math.floor(val.doubleValue() * BRIGHTNESS_FACTOR);
-    }
 }
index 587e041a7b4fedd8a7fc6e1c0c3a5725f4dcb57a..9f9ff4284f3f395124d570a78f02261aad35b3d1 100644 (file)
@@ -29,7 +29,7 @@ import org.openhab.binding.deconz.internal.dto.SensorConfig;
 import org.openhab.binding.deconz.internal.dto.SensorMessage;
 import org.openhab.binding.deconz.internal.dto.SensorState;
 import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
-import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
+import org.openhab.binding.deconz.internal.types.ResourceType;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.QuantityType;
@@ -77,28 +77,7 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler<Sens
     private @Nullable ScheduledFuture<?> lastSeenPollingJob;
 
     public SensorBaseThingHandler(Thing thing, Gson gson) {
-        super(thing, gson);
-    }
-
-    @Override
-    protected void requestState() {
-        requestState("sensors");
-    }
-
-    @Override
-    protected void registerListener() {
-        WebSocketConnection conn = connection;
-        if (conn != null) {
-            conn.registerSensorListener(config.id, this);
-        }
-    }
-
-    @Override
-    protected void unregisterListener() {
-        WebSocketConnection conn = connection;
-        if (conn != null) {
-            conn.unregisterSensorListener(config.id);
-        }
+        super(thing, gson, ResourceType.SENSORS);
     }
 
     @Override
index dba497c2f11a9af8acd1e89b0583ac58008a56c6..ee4ace7a8c36ce284a9699a2fe91dbda512c2dec 100644 (file)
@@ -13,7 +13,6 @@
 package org.openhab.binding.deconz.internal.handler;
 
 import static org.openhab.binding.deconz.internal.BindingConstants.*;
-import static org.openhab.binding.deconz.internal.Util.buildUrl;
 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
 import static org.openhab.core.library.unit.SmartHomeUnits.PERCENT;
 
@@ -28,7 +27,6 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.deconz.internal.dto.SensorConfig;
 import org.openhab.binding.deconz.internal.dto.SensorState;
 import org.openhab.binding.deconz.internal.dto.ThermostatConfig;
-import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
 import org.openhab.binding.deconz.internal.types.ThermostatMode;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.QuantityType;
@@ -121,26 +119,7 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
 
         }
 
-        AsyncHttpClient asyncHttpClient = http;
-        if (asyncHttpClient == null) {
-            return;
-        }
-        String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey, "sensors", config.id,
-                "config");
-
-        String json = gson.toJson(newConfig);
-        logger.trace("Sending {} to sensor {} via {}", json, config.id, url);
-        asyncHttpClient.put(url, json, bridgeConfig.timeout).thenAccept(v -> {
-            String bodyContent = v.getBody();
-            logger.trace("Result code={}, body={}", v.getResponseCode(), bodyContent);
-            if (!bodyContent.contains("success")) {
-                logger.debug("Sending command {} to channel {} failed: {}", command, channelUID, bodyContent);
-            }
-
-        }).exceptionally(e -> {
-            logger.debug("Sending command {} to channel {} failed:", command, channelUID, e);
-            return null;
-        });
+        sendCommand(newConfig, command, channelUID, null);
     }
 
     @Override
index 1b299e739cb9730c96151c1869d46f3cb29f9cf8..21217ed21f1b4930c1f80b9f6c4d95914bcba277 100644 (file)
@@ -18,16 +18,12 @@ import static org.openhab.core.library.unit.SIUnits.*;
 import static org.openhab.core.library.unit.SmartHomeUnits.*;
 
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.deconz.internal.dto.SensorConfig;
-import org.openhab.binding.deconz.internal.dto.SensorState;
+import org.openhab.binding.deconz.internal.dto.*;
 import org.openhab.core.library.types.HSBType;
 import org.openhab.core.library.types.OpenClosedType;
 import org.openhab.core.library.types.QuantityType;
@@ -59,13 +55,12 @@ import com.google.gson.Gson;
  */
 @NonNullByDefault
 public class SensorThingHandler extends SensorBaseThingHandler {
-    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections
-            .unmodifiableSet(Stream.of(THING_TYPE_PRESENCE_SENSOR, THING_TYPE_DAYLIGHT_SENSOR, THING_TYPE_POWER_SENSOR,
-                    THING_TYPE_CONSUMPTION_SENSOR, THING_TYPE_LIGHT_SENSOR, THING_TYPE_TEMPERATURE_SENSOR,
-                    THING_TYPE_HUMIDITY_SENSOR, THING_TYPE_PRESSURE_SENSOR, THING_TYPE_SWITCH,
-                    THING_TYPE_OPENCLOSE_SENSOR, THING_TYPE_WATERLEAKAGE_SENSOR, THING_TYPE_FIRE_SENSOR,
-                    THING_TYPE_ALARM_SENSOR, THING_TYPE_VIBRATION_SENSOR, THING_TYPE_BATTERY_SENSOR,
-                    THING_TYPE_CARBONMONOXIDE_SENSOR, THING_TYPE_COLOR_CONTROL).collect(Collectors.toSet()));
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_PRESENCE_SENSOR,
+            THING_TYPE_DAYLIGHT_SENSOR, THING_TYPE_POWER_SENSOR, THING_TYPE_CONSUMPTION_SENSOR, THING_TYPE_LIGHT_SENSOR,
+            THING_TYPE_TEMPERATURE_SENSOR, THING_TYPE_HUMIDITY_SENSOR, THING_TYPE_PRESSURE_SENSOR, THING_TYPE_SWITCH,
+            THING_TYPE_OPENCLOSE_SENSOR, THING_TYPE_WATERLEAKAGE_SENSOR, THING_TYPE_FIRE_SENSOR,
+            THING_TYPE_ALARM_SENSOR, THING_TYPE_VIBRATION_SENSOR, THING_TYPE_BATTERY_SENSOR,
+            THING_TYPE_CARBONMONOXIDE_SENSOR, THING_TYPE_COLOR_CONTROL);
 
     private static final List<String> CONFIG_CHANNELS = Arrays.asList(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW,
             CHANNEL_TEMPERATURE);
index 14773c87e1fed8efb0cbbfb668c31d84dfdc5866..d8d7a923add954c6c0da2cc1bc0250794c981f89 100644 (file)
@@ -25,8 +25,10 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
 import org.eclipse.jetty.websocket.client.WebSocketClient;
 import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
+import org.openhab.binding.deconz.internal.dto.GroupMessage;
 import org.openhab.binding.deconz.internal.dto.LightMessage;
 import org.openhab.binding.deconz.internal.dto.SensorMessage;
+import org.openhab.binding.deconz.internal.types.ResourceType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -42,12 +44,16 @@ import com.google.gson.Gson;
 @WebSocket
 @NonNullByDefault
 public class WebSocketConnection {
+    private static final Map<ResourceType, Class<? extends DeconzBaseMessage>> EXPECTED_MESSAGE_TYPES = Map.of(
+            ResourceType.GROUPS, GroupMessage.class, ResourceType.LIGHTS, LightMessage.class, ResourceType.SENSORS,
+            SensorMessage.class);
+
     private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
 
     private final WebSocketClient client;
     private final WebSocketConnectionListener connectionListener;
-    private final Map<String, WebSocketMessageListener> sensorListener = new ConcurrentHashMap<>();
-    private final Map<String, WebSocketMessageListener> lightListener = new ConcurrentHashMap<>();
+    private final Map<Map.Entry<ResourceType, String>, WebSocketMessageListener> listeners = new ConcurrentHashMap<>();
+
     private final Gson gson;
     private boolean connected = false;
 
@@ -84,20 +90,12 @@ public class WebSocketConnection {
         client.destroy();
     }
 
-    public void registerSensorListener(String sensorID, WebSocketMessageListener listener) {
-        sensorListener.put(sensorID, listener);
-    }
-
-    public void unregisterSensorListener(String sensorID) {
-        sensorListener.remove(sensorID);
+    public void registerListener(ResourceType resourceType, String sensorID, WebSocketMessageListener listener) {
+        listeners.put(Map.entry(resourceType, sensorID), listener);
     }
 
-    public void registerLightListener(String lightID, WebSocketMessageListener listener) {
-        lightListener.put(lightID, listener);
-    }
-
-    public void unregisterLightListener(String lightID) {
-        sensorListener.remove(lightID);
+    public void unregisterListener(ResourceType resourceType, String sensorID) {
+        listeners.remove(Map.entry(resourceType, sensorID));
     }
 
     @OnWebSocketConnect
@@ -111,27 +109,29 @@ public class WebSocketConnection {
     @OnWebSocketMessage
     public void onMessage(String message) {
         logger.trace("Raw data received by websocket: {}", message);
+
         DeconzBaseMessage changedMessage = gson.fromJson(message, DeconzBaseMessage.class);
-        switch (changedMessage.r) {
-            case "sensors":
-                WebSocketMessageListener listener = sensorListener.get(changedMessage.id);
-                if (listener != null) {
-                    listener.messageReceived(changedMessage.id, gson.fromJson(message, SensorMessage.class));
-                } else {
-                    logger.trace("Couldn't find sensor listener for id {}", changedMessage.id);
-                }
-                break;
-            case "lights":
-                listener = lightListener.get(changedMessage.id);
-                if (listener != null) {
-                    listener.messageReceived(changedMessage.id, gson.fromJson(message, LightMessage.class));
-                } else {
-                    logger.trace("Couldn't find light listener for id {}", changedMessage.id);
-                }
-                break;
-            default:
-                logger.debug("Unknown message type: {}", changedMessage.r);
+        if (changedMessage.r == ResourceType.UNKNOWN) {
+            logger.trace("Received message has unknown resource type. Skipping message.");
+            return;
         }
+
+        WebSocketMessageListener listener = listeners.get(Map.entry(changedMessage.r, changedMessage.id));
+        if (listener == null) {
+            logger.debug(
+                    "Couldn't find listener for id {} with resource type {}. Either no thing for this id has been defined or this is a bug.",
+                    changedMessage.id, changedMessage.r);
+            return;
+        }
+
+        Class<? extends DeconzBaseMessage> expectedMessageType = EXPECTED_MESSAGE_TYPES.get(changedMessage.r);
+        if (expectedMessageType == null) {
+            logger.warn("BUG! Could not get expected message type for resource type {}. Please report this incident.",
+                    changedMessage.r);
+            return;
+        }
+
+        listener.messageReceived(changedMessage.id, gson.fromJson(message, expectedMessageType));
     }
 
     @OnWebSocketError
index fa4e13654eb9fcd7057898d67b8b7cac43d36dab..96f28183d4d70c91fe2839c95f8235f07165da59 100644 (file)
@@ -28,6 +28,5 @@ public interface WebSocketMessageListener {
      * @param sensorID The sensor ID (API endpoint)
      * @param message The received message
      */
-
     void messageReceived(String sensorID, DeconzBaseMessage message);
 }
diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupType.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupType.java
new file mode 100644 (file)
index 0000000..93a87d5
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2020 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.deconz.internal.types;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Type of a group as reported by the REST API for usage in {@link org.openhab.binding.deconz.internal.dto.LightMessage}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public enum GroupType {
+    LIGHT_GROUP("LightGroup"),
+    UNKNOWN("");
+
+    private static final Map<String, GroupType> MAPPING = Arrays.stream(GroupType.values())
+            .collect(Collectors.toMap(v -> v.type, v -> v));
+    private static final Logger LOGGER = LoggerFactory.getLogger(GroupType.class);
+
+    private String type;
+
+    GroupType(String type) {
+        this.type = type;
+    }
+
+    public static GroupType fromString(String s) {
+        GroupType lightType = MAPPING.getOrDefault(s, UNKNOWN);
+        if (lightType == UNKNOWN) {
+            LOGGER.debug("Unknown group type '{}' found. This should be reported.", s);
+        }
+        return lightType;
+    }
+}
diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupTypeDeserializer.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/GroupTypeDeserializer.java
new file mode 100644 (file)
index 0000000..3bb2c1d
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2020 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.deconz.internal.types;
+
+import java.lang.reflect.Type;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+
+/**
+ * Custom deserializer for {@link GroupType}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class GroupTypeDeserializer implements JsonDeserializer<GroupType> {
+    @Override
+    public GroupType deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
+            @Nullable JsonDeserializationContext context) throws JsonParseException {
+        String s = json != null ? json.getAsString() : null;
+        return s == null ? GroupType.UNKNOWN : GroupType.fromString(s);
+    }
+}
index 9e1fcc0baec0a9ce6d9cdaa7b979edea690c3c2e..8255feb12e0b105ccaa7174688754f7babed01e3 100644 (file)
@@ -14,6 +14,9 @@ package org.openhab.binding.deconz.internal.types;
 
 import java.lang.reflect.Type;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
 import com.google.gson.JsonDeserializationContext;
 import com.google.gson.JsonDeserializer;
 import com.google.gson.JsonElement;
@@ -24,11 +27,12 @@ import com.google.gson.JsonParseException;
  *
  * @author Jan N. Klug - Initial contribution
  */
+@NonNullByDefault
 public class LightTypeDeserializer implements JsonDeserializer<LightType> {
     @Override
-    public LightType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
-            throws JsonParseException {
-        String s = json.getAsString();
+    public LightType deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
+            @Nullable JsonDeserializationContext context) throws JsonParseException {
+        String s = json != null ? json.getAsString() : null;
         return s == null ? LightType.UNKNOWN : LightType.fromString(s);
     }
 }
diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceType.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceType.java
new file mode 100644 (file)
index 0000000..6a33ad0
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2020 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.deconz.internal.types;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ResourceType} defines an enum for websocket messages
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public enum ResourceType {
+    GROUPS("groups", "action"),
+    LIGHTS("lights", "state"),
+    SENSORS("sensors", ""),
+    UNKNOWN("", "");
+
+    private static final Map<String, ResourceType> MAPPING = Arrays.stream(ResourceType.values())
+            .collect(Collectors.toMap(v -> v.identifier, v -> v));
+    private static final Logger LOGGER = LoggerFactory.getLogger(ResourceType.class);
+
+    private String identifier;
+    private String commandUrl;
+
+    ResourceType(String identifier, String commandUrl) {
+        this.identifier = identifier;
+        this.commandUrl = commandUrl;
+    }
+
+    /**
+     * get the identifier string of this resource type
+     *
+     * @return
+     */
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    /**
+     * get the commandUrl part for this resource type
+     *
+     * @return
+     */
+    public String getCommandUrl() {
+        return commandUrl;
+    }
+
+    /**
+     * get the resource type from a string
+     *
+     * @param s the string
+     * @return the corresponding resource type (or UNKNOWN)
+     */
+    public static ResourceType fromString(String s) {
+        ResourceType lightType = MAPPING.getOrDefault(s, UNKNOWN);
+        if (lightType == UNKNOWN) {
+            LOGGER.debug("Unknown resource type '{}' found. This should be reported.", s);
+        }
+        return lightType;
+    }
+}
diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceTypeDeserializer.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ResourceTypeDeserializer.java
new file mode 100644 (file)
index 0000000..b8f6e21
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2020 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.deconz.internal.types;
+
+import java.lang.reflect.Type;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+
+/**
+ * Custom deserializer for {@link ResourceType}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class ResourceTypeDeserializer implements JsonDeserializer<ResourceType> {
+    @Override
+    public ResourceType deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
+            @Nullable JsonDeserializationContext context) throws JsonParseException {
+        String s = json != null ? json.getAsString() : null;
+        return s == null ? ResourceType.UNKNOWN : ResourceType.fromString(s);
+    }
+}
diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/group-thing-types.xml b/bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/group-thing-types.xml
new file mode 100644 (file)
index 0000000..6b6cd10
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="deconz"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="lightgroup">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="deconz"/>
+               </supported-bridge-type-refs>
+               <label>Light Group</label>
+               <channels>
+                       <channel typeId="all_on" id="all_on"/>
+                       <channel typeId="any_on" id="any_on"/>
+                       <channel typeId="alert" id="alert"/>
+                       <channel typeId="color" id="color"/>
+                       <channel typeId="ct" id="color_temperature"/>
+               </channels>
+
+               <representation-property>uid</representation-property>
+
+               <config-description-ref uri="thing-type:deconz:sensor"/>
+       </thing-type>
+
+       <channel-type id="alert">
+               <item-type>Switch</item-type>
+               <label>Alert</label>
+       </channel-type>
+
+       <channel-type id="all_on">
+               <item-type>Switch</item-type>
+               <label>All On</label>
+               <description>"On" if all lights in this group are "On", otherwise "Off".</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="any_on">
+               <item-type>Switch</item-type>
+               <label>Any On</label>
+               <description>"On" if any light in this group is "On", otherwise "Off".</description>
+       </channel-type>
+
+       <channel-type id="color">
+               <item-type>Color</item-type>
+               <label>Color</label>
+       </channel-type>
+
+       <channel-type id="ct">
+               <item-type>Number</item-type>
+               <label>Color Temperature</label>
+               <state pattern="%d K" min="15" max="100000" step="100"/>
+       </channel-type>
+
+</thing:thing-descriptions>
index e0a635a5e0ed44cd33f8ad7a0070eccdda904fd7..e7a3e2a5e584cbf881866f7400f6b0f002f6ff79 100644 (file)
        <channel-type id="ct">
                <item-type>Number</item-type>
                <label>Color Temperature</label>
-               <state pattern="%d" min="15" max="100000" step="100"/>
+               <state pattern="%d K" min="15" max="100000" step="100"/>
        </channel-type>
 
        <channel-type id="alert">
index fae1d38be339c0f3caf776318f25ab3b5e1e441f..eb9aa34f76c59a8bee91321c09979aea077512a2 100644 (file)
@@ -34,10 +34,7 @@ import org.openhab.binding.deconz.internal.Util;
 import org.openhab.binding.deconz.internal.discovery.ThingDiscoveryService;
 import org.openhab.binding.deconz.internal.dto.BridgeFullState;
 import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
-import org.openhab.binding.deconz.internal.types.LightType;
-import org.openhab.binding.deconz.internal.types.LightTypeDeserializer;
-import org.openhab.binding.deconz.internal.types.ThermostatMode;
-import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter;
+import org.openhab.binding.deconz.internal.types.*;
 import org.openhab.core.config.discovery.DiscoveryListener;
 import org.openhab.core.library.types.DateTimeType;
 import org.openhab.core.thing.Bridge;
@@ -68,6 +65,8 @@ public class DeconzTest {
 
         GsonBuilder gsonBuilder = new GsonBuilder();
         gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
+        gsonBuilder.registerTypeAdapter(GroupType.class, new GroupTypeDeserializer());
+        gsonBuilder.registerTypeAdapter(ResourceType.class, new ResourceTypeDeserializer());
         gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
         gson = gsonBuilder.create();
     }
@@ -84,7 +83,7 @@ public class DeconzTest {
         discoveryService.addDiscoveryListener(discoveryListener);
 
         discoveryService.stateRequestFinished(bridgeFullState);
-        Mockito.verify(discoveryListener, times(15)).thingDiscovered(any(), any());
+        Mockito.verify(discoveryListener, times(20)).thingDiscovered(any(), any());
     }
 
     public static <T> T getObjectFromJson(String filename, Class<T> clazz, Gson gson) throws IOException {