| 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` |
+| 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` |
| 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` |
**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.
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link BindingConstants} class defines common constants, which are
public static final String CHANNEL_ALL_ON = "all_on";
public static final String CHANNEL_ANY_ON = "any_on";
public static final String CHANNEL_LOCK = "lock";
+ public static final String CHANNEL_EFFECT = "effect";
+ public static final String CHANNEL_EFFECT_SPEED = "effectSpeed";
+
+ // channel uids
+ public static final ChannelTypeUID CHANNEL_EFFECT_TYPE_UID = new ChannelTypeUID(BINDING_ID, CHANNEL_EFFECT);
+ public static final ChannelTypeUID CHANNEL_EFFECT_SPEED_TYPE_UID = new ChannelTypeUID(BINDING_ID,
+ CHANNEL_EFFECT_SPEED);
// Thing configuration
public static final String CONFIG_HOST = "host";
--- /dev/null
+/**
+ * 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;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
+import org.openhab.core.types.CommandDescription;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Dynamic channel command description provider.
+ * Overrides the command description for the controls, which receive its configuration in the runtime.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { DynamicCommandDescriptionProvider.class, CommandDescriptionProvider.class })
+public class CommandDescriptionProvider implements DynamicCommandDescriptionProvider {
+
+ private final Map<ChannelUID, CommandDescription> descriptions = new ConcurrentHashMap<>();
+ private final Logger logger = LoggerFactory.getLogger(CommandDescriptionProvider.class);
+
+ /**
+ * Set a command description for a channel. This description will be used when preparing the channel command by
+ * the framework for presentation. A previous description, if existed, will be replaced.
+ *
+ * @param channelUID
+ * channel UID
+ * @param description
+ * state description for the channel
+ */
+ public void setDescription(ChannelUID channelUID, CommandDescription description) {
+ logger.trace("adding command description for channel {}", channelUID);
+ descriptions.put(channelUID, description);
+ }
+
+ /**
+ * remove all descriptions for a given thing
+ *
+ * @param thingUID the thing's UID
+ */
+ public void removeDescriptionsForThing(ThingUID thingUID) {
+ logger.trace("removing state description for thing {}", thingUID);
+ descriptions.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID));
+ }
+
+ @Override
+ public @Nullable CommandDescription getCommandDescription(Channel channel,
+ @Nullable CommandDescription originalStateDescription, @Nullable Locale locale) {
+ if (descriptions.containsKey(channel.getUID())) {
+ logger.trace("returning new stateDescription for {}", channel.getUID());
+ return descriptions.get(channel.getUID());
+ } else {
+ return null;
+ }
+ }
+}
private final WebSocketFactory webSocketFactory;
private final HttpClientFactory httpClientFactory;
private final StateDescriptionProvider stateDescriptionProvider;
+ private final CommandDescriptionProvider commandDescriptionProvider;
@Activate
public DeconzHandlerFactory(final @Reference WebSocketFactory webSocketFactory,
final @Reference HttpClientFactory httpClientFactory,
- final @Reference StateDescriptionProvider stateDescriptionProvider) {
+ final @Reference StateDescriptionProvider stateDescriptionProvider,
+ final @Reference CommandDescriptionProvider commandDescriptionProvider) {
this.webSocketFactory = webSocketFactory;
this.httpClientFactory = httpClientFactory;
this.stateDescriptionProvider = stateDescriptionProvider;
+ this.commandDescriptionProvider = commandDescriptionProvider;
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
return new DeconzBridgeHandler((Bridge) thing, webSocketFactory,
new AsyncHttpClient(httpClientFactory.getCommonHttpClient()), gson);
} else if (LightThingHandler.SUPPORTED_THING_TYPE_UIDS.contains(thingTypeUID)) {
- return new LightThingHandler(thing, gson, stateDescriptionProvider);
+ return new LightThingHandler(thing, gson, stateDescriptionProvider, commandDescriptionProvider);
} else if (SensorThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new SensorThingHandler(thing, gson);
} else if (SensorThermostatThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
public @Nullable String alert;
public @Nullable String colormode;
public @Nullable String effect;
+ public @Nullable Integer effectSpeed;
// depending on the type of light
public @Nullable Integer hue;
alert = null;
colormode = null;
effect = null;
+ effectSpeed = null;
hue = null;
sat = null;
@Override
public String toString() {
- return "LightState{reachable=" + reachable + ", on=" + on + ", bri=" + bri + ", alert='" + alert + '\''
- + ", colormode='" + colormode + '\'' + ", effect='" + effect + '\'' + ", hue=" + hue + ", sat=" + sat
- + ", ct=" + ct + ", xy=" + Arrays.toString(xy) + ", transitiontime=" + transitiontime + '}';
+ return "LightState{" + "reachable=" + reachable + ", on=" + on + ", bri=" + bri + ", alert='" + alert + '\''
+ + ", colormode='" + colormode + '\'' + ", effect='" + effect + '\'' + ", effectSpeed=" + effectSpeed
+ + ", hue=" + hue + ", sat=" + sat + ", ct=" + ct + ", xy=" + Arrays.toString(xy) + ", transitiontime="
+ + transitiontime + '}';
}
}
}
}
+ /**
+ * parse the initial state response message
+ *
+ * @param r AsyncHttpClient.Result with the state response result
+ * @return a message of the correct type
+ */
protected abstract @Nullable T parseStateResponse(AsyncHttpClient.Result r);
/**
private void parseAPIKeyResponse(AsyncHttpClient.Result r) {
if (r.getResponseCode() == 403) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
- "Allow authentification for 3rd party apps. Trying again in " + POLL_FREQUENCY_SEC + " seconds");
+ "Allow authentication for 3rd party apps. Trying again in " + POLL_FREQUENCY_SEC + " seconds");
stopTimer();
scheduledFuture = scheduler.schedule(() -> requestApiKey(), POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
} else if (r.getResponseCode() == 200) {
import static org.openhab.binding.deconz.internal.Util.*;
import java.math.BigDecimal;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
+import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.deconz.internal.CommandDescriptionProvider;
import org.openhab.binding.deconz.internal.StateDescriptionProvider;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.openhab.core.types.StateDescription;
-import org.openhab.core.types.StateDescriptionFragmentBuilder;
-import org.openhab.core.types.UnDefType;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.types.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private final Logger logger = LoggerFactory.getLogger(LightThingHandler.class);
private final StateDescriptionProvider stateDescriptionProvider;
+ private final CommandDescriptionProvider commandDescriptionProvider;
private long lastCommandExpireTimestamp = 0;
private boolean needsPropertyUpdate = false;
private int ctMax = ZCL_CT_MAX;
private int ctMin = ZCL_CT_MIN;
- public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider) {
+ public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider,
+ CommandDescriptionProvider commandDescriptionProvider) {
super(thing, gson, ResourceType.LIGHTS);
this.stateDescriptionProvider = stateDescriptionProvider;
+ this.commandDescriptionProvider = commandDescriptionProvider;
}
@Override
} else {
return;
}
+ break;
+ case CHANNEL_EFFECT:
+ if (command instanceof StringType) {
+ // effect command only allowed for lights that are turned on
+ newLightState.on = true;
+ newLightState.effect = command.toString();
+ } else {
+ return;
+ }
+ break;
+ case CHANNEL_EFFECT_SPEED:
+ if (command instanceof DecimalType) {
+ newLightState.on = true;
+ newLightState.effectSpeed = Util.constrainToRange(((DecimalType) command).intValue(), 0, 10);
+ } else {
+ return;
+ }
+ break;
case CHANNEL_SWITCH:
case CHANNEL_LOCK:
if (command instanceof OnOffType) {
}
} else if (command instanceof HSBType) {
HSBType hsbCommand = (HSBType) command;
-
if ("xy".equals(lightStateCache.colormode)) {
PercentType[] xy = hsbCommand.toXY();
if (xy.length < 2) {
if (r.getResponseCode() == 403) {
return null;
} else if (r.getResponseCode() == 200) {
- LightMessage lightMessage = gson.fromJson(r.getBody(), LightMessage.class);
+ LightMessage lightMessage = Objects.requireNonNull(gson.fromJson(r.getBody(), LightMessage.class));
if (needsPropertyUpdate) {
// if we did not receive an ctmin/ctmax, then we probably don't need it
needsPropertyUpdate = false;
if (stateResponse == null) {
return;
}
-
+ if (stateResponse.state.effect != null) {
+ checkAndUpdateEffectChannels(stateResponse);
+ }
messageReceived(config.id, stateResponse);
}
+ private enum EffectLightModel {
+ LIDL_MELINARA,
+ TINT_MUELLER,
+ UNKNOWN;
+ }
+
+ private void checkAndUpdateEffectChannels(LightMessage lightMessage) {
+ EffectLightModel model = EffectLightModel.UNKNOWN;
+ // try to determine which model we have
+ if (lightMessage.manufacturername.equals("_TZE200_s8gkrkxk")) {
+ // the LIDL Melinara string does not report a proper model name
+ model = EffectLightModel.LIDL_MELINARA;
+ } else if (lightMessage.manufacturername.equals("MLI")) {
+ model = EffectLightModel.TINT_MUELLER;
+ } else {
+ logger.info("Could not determine effect light type for thing {}, please request adding support on GitHub.",
+ thing.getUID());
+ }
+
+ ChannelUID effectChannelUID = new ChannelUID(thing.getUID(), CHANNEL_EFFECT);
+ ChannelUID effectSpeedChannelUID = new ChannelUID(thing.getUID(), CHANNEL_EFFECT_SPEED);
+
+ if (thing.getChannel(CHANNEL_EFFECT) == null) {
+ ThingBuilder thingBuilder = editThing();
+ thingBuilder.withChannel(
+ ChannelBuilder.create(effectChannelUID, "String").withType(CHANNEL_EFFECT_TYPE_UID).build());
+ if (model == EffectLightModel.LIDL_MELINARA) {
+ // additional channels
+ thingBuilder.withChannel(ChannelBuilder.create(effectSpeedChannelUID, "Number")
+ .withType(CHANNEL_EFFECT_SPEED_TYPE_UID).build());
+ }
+ updateThing(thingBuilder.build());
+ }
+
+ switch (model) {
+ case LIDL_MELINARA:
+ List<String> options = List.of("none", "steady", "snow", "rainbow", "snake", "tinkle", "fireworks",
+ "flag", "waves", "updown", "vintage", "fading", "collide", "strobe", "sparkles", "carnival",
+ "glow");
+ commandDescriptionProvider.setDescription(effectChannelUID,
+ CommandDescriptionBuilder.create().withCommandOptions(toCommandOptionList(options)).build());
+ break;
+ case TINT_MUELLER:
+ options = List.of("none", "colorloop", "sunset", "party", "worklight", "campfire", "romance",
+ "nightlight");
+ commandDescriptionProvider.setDescription(effectChannelUID,
+ CommandDescriptionBuilder.create().withCommandOptions(toCommandOptionList(options)).build());
+ break;
+ default:
+ options = List.of("none", "colorloop");
+ commandDescriptionProvider.setDescription(effectChannelUID,
+ CommandDescriptionBuilder.create().withCommandOptions(toCommandOptionList(options)).build());
+
+ }
+ }
+
+ private List<CommandOption> toCommandOptionList(List<String> options) {
+ return options.stream().map(c -> new CommandOption(c, c)).collect(Collectors.toList());
+ }
+
private void valueUpdated(String channelId, LightState newState) {
Integer bri = newState.bri;
Integer hue = newState.hue;
if (bri != null) {
updateState(channelId, toPercentType(bri));
}
+ break;
+ case CHANNEL_EFFECT:
+ String effect = newState.effect;
+ if (effect != null) {
+ updateState(channelId, new StringType(effect));
+ }
+ break;
+ case CHANNEL_EFFECT_SPEED:
+ Integer effectSpeed = newState.effectSpeed;
+ if (effectSpeed != null) {
+ updateState(channelId, new DecimalType(effectSpeed));
+ }
+ break;
default:
}
}
<state pattern="%d K" min="15" max="100000" step="100"/>
</channel-type>
+ <channel-type id="effect">
+ <item-type>String</item-type>
+ <label>Effect Channel</label>
+ <tags>
+ <tag>Lighting</tag>
+ </tags>
+ </channel-type>
+
+ <channel-type id="effectSpeed">
+ <item-type>Number</item-type>
+ <label>Effect Speed Channel</label>
+ <tags>
+ <tag>Lighting</tag>
+ </tags>
+ <state min="0" max="10" step="1"/>
+ </channel-type>
+
<channel-type id="alert">
<item-type>Switch</item-type>
<label>Alert</label>
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.deconz.internal.CommandDescriptionProvider;
import org.openhab.binding.deconz.internal.StateDescriptionProvider;
import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.handler.LightThingHandler;
private @Mock @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
private @Mock @NonNullByDefault({}) StateDescriptionProvider stateDescriptionProvider;
+ private @Mock @NonNullByDefault({}) CommandDescriptionProvider commandDescriptionProvider;
@BeforeEach
public void initialize() {
Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build())
.withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build();
- LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
+ LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
+ commandDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage);
Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID).withProperties(properties)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build())
.withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build();
- LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider) {
+ LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
+ commandDescriptionProvider) {
// avoid warning when initializing
@Override
public @Nullable Bridge getBridge() {
Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build();
- LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
+ LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
+ commandDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage);
Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build();
- LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
+ LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
+ commandDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage);
Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build();
- LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
+ LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
+ commandDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage);
Thing light = ThingBuilder.create(THING_TYPE_WINDOW_COVERING, thingUID)
.withChannel(ChannelBuilder.create(channelUID_pos, "Rollershutter").build()).build();
- LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
+ LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider,
+ commandDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage);