[deconz] initial support for scenes (#9345)
* initial support for scenes * add documentation Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
This commit is contained in:
parent
12e5e38cb0
commit
af3f8774d0
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 + '}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
@ -67,4 +68,13 @@
|
|||
<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>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue