| 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` |
| 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.
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);
} 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;
*/
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;
@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 + '}';
}
}
}
/**
- * 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 -> {
} 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();
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;
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
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;
}
@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);
}
}
}
- if (lightMessage.state.effect != null) {
+ LightState lightState = lightMessage.state;
+ if (lightState != null && lightState.effect != null) {
checkAndUpdateEffectChannels(lightMessage);
}
}
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());
* @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);
}
import java.net.URI;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
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;
<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>
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;
}
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