diff --git a/bundles/org.openhab.binding.deconz/README.md b/bundles/org.openhab.binding.deconz/README.md index 66991bd17..e2ef2c1f9 100644 --- a/bundles/org.openhab.binding.deconz/README.md +++ b/bundles/org.openhab.binding.deconz/README.md @@ -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. diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java index 78f68f157..0fb0b81a9 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java @@ -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); diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java index 8e005d3ad..5ecac53ae 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java @@ -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; 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 index 307d51601..176bf8a3f 100644 --- 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 @@ -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 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 lights = List.of(); + public List lightsequence = List.of(); + public List multideviceids = List.of(); + public List 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 + '}'; } } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java index 3834ccbec..200b811bb 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBaseThingHandler.java @@ -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 -> { diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeHandler.java index a489f6a0e..7a4b1633d 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/DeconzBridgeHandler.java @@ -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(); 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 index 52930db7c..b53e09ae6 100644 --- 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 @@ -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 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 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); } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java index 6995955c5..8dfe010ca 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java @@ -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); } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java index be2b04f7e..d53cff2af 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java @@ -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()); diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/AsyncHttpClient.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/AsyncHttpClient.java index b3b98c092..9e0b0326a 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/AsyncHttpClient.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/AsyncHttpClient.java @@ -60,7 +60,7 @@ public class AsyncHttpClient { * @param timeout A timeout * @return The result */ - public CompletableFuture put(String address, String jsonString, int timeout) { + public CompletableFuture put(String address, @Nullable String jsonString, int timeout) { return doNetwork(HttpMethod.PUT, address, jsonString, timeout); } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnection.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnection.java index 3e9e2ed5a..4d0fb4c4e 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnection.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/netutils/WebSocketConnection.java @@ -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; 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 index 691c21022..f8b1633f0 100644 --- 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 @@ -16,6 +16,7 @@ + uid @@ -67,4 +68,13 @@ + + String + + + Lighting + + + + diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java index 2bffe3410..2ab8d443a 100644 --- a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java @@ -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 getObjectFromJson(String filename, Class 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