]> git.basschouten.com Git - openhab-addons.git/commitdiff
[deconz] initial support for scenes (#9345)
authorJ-N-K <J-N-K@users.noreply.github.com>
Sat, 12 Dec 2020 23:30:43 +0000 (00:30 +0100)
committerGitHub <noreply@github.com>
Sat, 12 Dec 2020 23:30:43 +0000 (15:30 -0800)
* initial support for scenes
* add documentation

Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
13 files changed:
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/dto/GroupMessage.java
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/DeconzBridgeHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/GroupThingHandler.java
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/SensorThermostatThingHandler.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/AsyncHttpClient.java
bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnection.java
bundles/org.openhab.binding.deconz/src/main/resources/OH-INF/thing/group-thing-types.xml
bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java

index 66991bd175628d9a09e27de966494143f8dde05e..e2ef2c1f984dfcc6c1a13c38357fda26c088a5a3 100644 (file)
@@ -163,7 +163,7 @@ Other devices support
 | 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` |
 | effect            | String                   |     R/W     | Effect selection. Allowed commands are set dynamically                                  | `colorlight`                                    |
-| effectSpeed       | Number                   |     R/W     | Effect Speed                          | `colorlight`                                    |
+| effectSpeed       | Number                   |     W       | Effect Speed                          | `colorlight`                                    |
 | lock              | Switch                   |     R/W     | Lock (ON) or unlock (OFF) the doorlock| `doorlock`                                      |                 
 | position          | Rollershutter            |     R/W     | Position of the blind                 | `windowcovering`                                |
 | heatsetpoint      | Number:Temperature       |     R/W     | Target Temperature in °C              | `thermostat`                                    |
@@ -173,11 +173,11 @@ Other devices support
 | 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`                                    |
+| scene             | String                   |     W       | Recall a scene. Allowed commands are set dynamically                                    | `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
 
 The dimmer switch additionally supports trigger channels.
index 78f68f1579d1c6ddeb48be2837927887edcadd1f..0fb0b81a9345f107372afa30f8a32560926eabf5 100644 (file)
@@ -116,6 +116,7 @@ public class BindingConstants {
     public static final String CHANNEL_LOCK = "lock";
     public static final String CHANNEL_EFFECT = "effect";
     public static final String CHANNEL_EFFECT_SPEED = "effectSpeed";
+    public static final String CHANNEL_SCENE = "scene";
 
     // channel uids
     public static final ChannelTypeUID CHANNEL_EFFECT_TYPE_UID = new ChannelTypeUID(BINDING_ID, CHANNEL_EFFECT);
index 8e005d3adbfa4c460aaeea67f1332dbf4f70c32c..5ecac53aea67a081b4008bff6e580bb95f133173 100644 (file)
@@ -94,7 +94,7 @@ public class DeconzHandlerFactory extends BaseThingHandlerFactory {
         } 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 new GroupThingHandler(thing, gson, commandDescriptionProvider);
         }
 
         return null;
index 307d51601986e073c08ce14569e5b8756e53d940..176bf8a3f63436364b99433f778d01772fc69967 100644 (file)
@@ -12,7 +12,7 @@
  */
 package org.openhab.binding.deconz.internal.dto;
 
-import java.util.Arrays;
+import java.util.List;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -27,20 +27,22 @@ import org.openhab.binding.deconz.internal.types.GroupType;
 @NonNullByDefault
 public class GroupMessage extends DeconzBaseMessage {
     public @Nullable GroupAction action;
-    public String @Nullable [] devicemembership;
+    public List<String> devicemembership = List.of();
     public @Nullable Boolean hidden;
-    public String @Nullable [] lights;
-    public String @Nullable [] lightsequence;
-    public String @Nullable [] multideviceids;
-    public Scene @Nullable [] scenes;
+    public List<String> lights = List.of();
+    public List<String> lightsequence = List.of();
+    public List<String> multideviceids = List.of();
+    public List<Scene> scenes = List.of();
     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 + '}';
+        return "GroupMessage{" + "e='" + e + '\'' + ", r=" + r + ", t='" + t + '\'' + ", id='" + id + '\''
+                + ", manufacturername='" + manufacturername + '\'' + ", modelid='" + modelid + '\'' + ", name='" + name
+                + '\'' + ", swversion='" + swversion + '\'' + ", ep='" + ep + '\'' + ", lastseen='" + lastseen + '\''
+                + ", uniqueid='" + uniqueid + '\'' + ", action=" + action + ", devicemembership=" + devicemembership
+                + ", hidden=" + hidden + ", lights=" + lights + ", lightsequence=" + lightsequence + ", multideviceids="
+                + multideviceids + ", scenes=" + scenes + ", state=" + state + ", type=" + type + '}';
     }
 }
index 3834ccbeca23e67217d867318a6031abd1c641f5..200b811bb6c39541cd6f4fe43d65008d903d9f27 100644 (file)
@@ -159,23 +159,37 @@ public abstract class DeconzBaseThingHandler extends BaseThingHandler implements
     }
 
     /**
-     * sends a command to the bridge
+     * sends a command to the bridge with the default command URL
      *
      * @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,
+    protected void sendCommand(@Nullable Object object, Command originalCommand, ChannelUID channelUID,
             @Nullable Runnable acceptProcessing) {
+        sendCommand(object, originalCommand, channelUID, resourceType.getCommandUrl(), acceptProcessing);
+    }
+
+    /**
+     * sends a command to the bridge with a caller-defined command URL
+     *
+     * @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 commandUrl the command URL
+     * @param acceptProcessing additional processing after the command was successfully send (might be null)
+     */
+    protected void sendCommand(@Nullable Object object, Command originalCommand, ChannelUID channelUID,
+            String commandUrl, @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());
+                resourceType.getIdentifier(), config.id, commandUrl);
 
-        String json = gson.toJson(object);
+        String json = object == null ? null : gson.toJson(object);
         logger.trace("Sending {} to {} {} via {}", json, resourceType, config.id, url);
 
         asyncHttpClient.put(url, json, bridgeConfig.timeout).thenAccept(v -> {
index a489f6a0e2898eb878f272693193e069d062cd24..7a4b1633df74fab61aa09d2586ab0b5c69d109e0 100644 (file)
@@ -178,7 +178,7 @@ public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketC
             } else if (t instanceof SocketTimeoutException || t instanceof TimeoutException
                     || t instanceof CompletionException) {
                 logger.debug("Get full state failed", t);
-            } else if (t != null) {
+            } else {
                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, t.getMessage());
             }
             return Optional.empty();
index 52930db7c0ebbc7afadb76f8c88fa804576fcba8..b53e09ae6fc8a0b821a6ee133db85dc3ad80c1a5 100644 (file)
@@ -14,24 +14,26 @@ package org.openhab.binding.deconz.internal.handler;
 
 import static org.openhab.binding.deconz.internal.BindingConstants.*;
 
+import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.deconz.internal.CommandDescriptionProvider;
 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.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.library.types.*;
 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.CommandDescriptionBuilder;
+import org.openhab.core.types.CommandOption;
 import org.openhab.core.types.RefreshType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -39,31 +41,25 @@ import org.slf4j.LoggerFactory;
 import com.google.gson.Gson;
 
 /**
- * This light thing doesn't establish any connections, that is done by the bridge Thing.
+ * This group 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 {
     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_LIGHTGROUP);
     private final Logger logger = LoggerFactory.getLogger(GroupThingHandler.class);
+    private final CommandDescriptionProvider commandDescriptionProvider;
 
-    /**
-     * The group state.
-     */
+    private Map<String, String> scenes = Map.of();
     private GroupState groupStateCache = new GroupState();
 
-    public GroupThingHandler(Thing thing, Gson gson) {
+    public GroupThingHandler(Thing thing, Gson gson, CommandDescriptionProvider commandDescriptionProvider) {
         super(thing, gson, ResourceType.GROUPS);
+        this.commandDescriptionProvider = commandDescriptionProvider;
     }
 
     @Override
@@ -113,6 +109,17 @@ public class GroupThingHandler extends DeconzBaseThingHandler {
                     return;
                 }
                 break;
+            case CHANNEL_SCENE:
+                if (command instanceof StringType) {
+                    String sceneId = scenes.get(command.toString());
+                    if (sceneId != null) {
+                        sendCommand(null, command, channelUID, "scene/" + sceneId + "/recall", null);
+                    } else {
+                        logger.debug("Ignoring command {} for {}, scene is not found in available scenes: {}", command,
+                                channelUID, scenes);
+                    }
+                }
+                return;
             default:
                 return;
         }
@@ -127,6 +134,16 @@ public class GroupThingHandler extends DeconzBaseThingHandler {
 
     @Override
     protected void processStateResponse(DeconzBaseMessage stateResponse) {
+        if (stateResponse instanceof GroupMessage) {
+            GroupMessage groupMessage = (GroupMessage) stateResponse;
+            scenes = groupMessage.scenes.stream().collect(Collectors.toMap(scene -> scene.name, scene -> scene.id));
+            ChannelUID channelUID = new ChannelUID(thing.getUID(), CHANNEL_SCENE);
+            commandDescriptionProvider.setDescription(channelUID,
+                    CommandDescriptionBuilder.create().withCommandOptions(groupMessage.scenes.stream()
+                            .map(scene -> new CommandOption(scene.name, scene.name)).collect(Collectors.toList()))
+                            .build());
+
+        }
         messageReceived(config.id, stateResponse);
     }
 
index 6995955c5ef35151ca2bd5a5af574c9493de0516..8dfe010cab2c8347d25dc4ceafdf1ccfc19c59e5 100644 (file)
@@ -282,7 +282,8 @@ public class LightThingHandler extends DeconzBaseThingHandler {
             }
         }
 
-        if (lightMessage.state.effect != null) {
+        LightState lightState = lightMessage.state;
+        if (lightState != null && lightState.effect != null) {
             checkAndUpdateEffectChannels(lightMessage);
         }
 
index be2b04f7e7f140d4789fac6785b81c6d4caf90b9..d53cff2afd0ddfd66ad81030a7112a0784864354 100644 (file)
@@ -198,7 +198,8 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
         }
 
         SensorMessage sensorMessage = (SensorMessage) stateResponse;
-        if (sensorMessage.state.windowopen != null && thing.getChannel(CHANNEL_WINDOWOPEN) == null) {
+        SensorState sensorState = sensorMessage.state;
+        if (sensorState != null && sensorState.windowopen != null && thing.getChannel(CHANNEL_WINDOWOPEN) == null) {
             ThingBuilder thingBuilder = editThing();
             thingBuilder.withChannel(ChannelBuilder.create(new ChannelUID(thing.getUID(), CHANNEL_WINDOWOPEN), "String")
                     .withType(new ChannelTypeUID(BINDING_ID, "open")).build());
index b3b98c092d92c3535302d357147883d27c9ac3d5..9e0b0326a5b021c9e921fee5bc64d1632d0b8fe0 100644 (file)
@@ -60,7 +60,7 @@ public class AsyncHttpClient {
      * @param timeout A timeout
      * @return The result
      */
-    public CompletableFuture<Result> put(String address, String jsonString, int timeout) {
+    public CompletableFuture<Result> put(String address, @Nullable String jsonString, int timeout) {
         return doNetwork(HttpMethod.PUT, address, jsonString, timeout);
     }
 
index 3e9e2ed5aad5163582d725c5c421c0bbc94b94e3..4d0fb4c4e1494ffa95092ef0968222b202a9745d 100644 (file)
@@ -14,6 +14,7 @@ package org.openhab.binding.deconz.internal.netutils;
 
 import java.net.URI;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -104,12 +105,12 @@ public class WebSocketConnection {
         connectionListener.connectionEstablished();
     }
 
-    @SuppressWarnings("null, unused")
+    @SuppressWarnings({ "null", "unused" })
     @OnWebSocketMessage
     public void onMessage(String message) {
         logger.trace("Raw data received by websocket {}: {}", socketName, message);
 
-        DeconzBaseMessage changedMessage = gson.fromJson(message, DeconzBaseMessage.class);
+        DeconzBaseMessage changedMessage = Objects.requireNonNull(gson.fromJson(message, DeconzBaseMessage.class));
         if (changedMessage.r == ResourceType.UNKNOWN) {
             logger.trace("Received message has unknown resource type. Skipping message.");
             return;
index 691c210220e2079fb868d03a695eb42259ad1f2f..f8b1633f0d5b543601efe90a84de075a27be19d1 100644 (file)
@@ -16,6 +16,7 @@
                        <channel typeId="alert" id="alert"/>
                        <channel typeId="color" id="color"/>
                        <channel typeId="ct" id="color_temperature"/>
+                       <channel typeId="scene" id="scene"/>
                </channels>
 
                <representation-property>uid</representation-property>
                <state pattern="%d K" min="15" max="100000" step="100"/>
        </channel-type>
 
+       <channel-type id="scene">
+               <item-type>String</item-type>
+               <label>Recall Scene</label>
+               <tags>
+                       <tag>Lighting</tag>
+               </tags>
+       </channel-type>
+
+
 </thing:thing-descriptions>
index 2bffe3410dab4b6d1c6ca8eaa35dd3a3e8f6a32d..2ab8d443ac4f90408c22441b284352ac79c403f3 100644 (file)
@@ -17,9 +17,11 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.times;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
@@ -90,8 +92,17 @@ public class DeconzTest {
     }
 
     public static <T> T getObjectFromJson(String filename, Class<T> clazz, Gson gson) throws IOException {
-        String json = new String(DeconzTest.class.getResourceAsStream(filename).readAllBytes(), StandardCharsets.UTF_8);
-        return gson.fromJson(json, clazz);
+        try (InputStream inputStream = DeconzTest.class.getResourceAsStream(filename)) {
+            if (inputStream == null) {
+                throw new IOException("inputstream is null");
+            }
+            byte[] bytes = inputStream.readAllBytes();
+            if (bytes == null) {
+                throw new IOException("Resulting byte-array empty");
+            }
+            String json = new String(bytes, StandardCharsets.UTF_8);
+            return Objects.requireNonNull(gson.fromJson(json, clazz));
+        }
     }
 
     @Test