Skip to content

Commit

Permalink
[deconz] add support for effects on color lights (openhab#9238)
Browse files Browse the repository at this point in the history
* add support for effects
* add tags
* remove unnecessary constants
* fix state update

Signed-off-by: Jan N. Klug <[email protected]>
  • Loading branch information
J-N-K authored Dec 9, 2020
1 parent 6fe75cb commit 27a8455
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 25 deletions.
5 changes: 4 additions & 1 deletion bundles/org.openhab.binding.deconz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ Other devices support
| 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` |
Expand All @@ -174,6 +176,7 @@ Other devices support
**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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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
Expand Down Expand Up @@ -112,6 +113,13 @@ public class BindingConstants {
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";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,17 @@ public class DeconzHandlerFactory extends BaseThingHandlerFactory {
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());
Expand All @@ -85,7 +88,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class LightState {
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;
Expand Down Expand Up @@ -66,6 +67,7 @@ public void clear() {
alert = null;
colormode = null;
effect = null;
effectSpeed = null;

hue = null;
sat = null;
Expand All @@ -81,8 +83,9 @@ private <T> boolean equalsIgnoreNull(T o1, T o2) {

@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 + '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
}
}

/**
* 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);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ private void stopTimer() {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
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;
Expand All @@ -35,11 +35,9 @@
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;

Expand Down Expand Up @@ -71,6 +69,7 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
private final Logger logger = LoggerFactory.getLogger(LightThingHandler.class);

private final StateDescriptionProvider stateDescriptionProvider;
private final CommandDescriptionProvider commandDescriptionProvider;

private long lastCommandExpireTimestamp = 0;
private boolean needsPropertyUpdate = false;
Expand All @@ -85,9 +84,11 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
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
Expand Down Expand Up @@ -136,6 +137,24 @@ public void handleCommand(ChannelUID channelUID, Command command) {
} 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) {
Expand All @@ -161,7 +180,6 @@ public void handleCommand(ChannelUID channelUID, Command command) {
}
} else if (command instanceof HSBType) {
HSBType hsbCommand = (HSBType) command;

if ("xy".equals(lightStateCache.colormode)) {
PercentType[] xy = hsbCommand.toXY();
if (xy.length < 2) {
Expand Down Expand Up @@ -249,7 +267,7 @@ public void handleCommand(ChannelUID channelUID, Command command) {
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;
Expand All @@ -276,10 +294,72 @@ protected void processStateResponse(@Nullable LightMessage stateResponse) {
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;
Expand Down Expand Up @@ -327,6 +407,19 @@ private void valueUpdated(String channelId, LightState newState) {
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:
}
}
Expand Down
Loading

0 comments on commit 27a8455

Please sign in to comment.