Skip to content

Commit

Permalink
[hue] Support timed effects (openhab#15408)
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Fiddian-Green <[email protected]>
  • Loading branch information
andrewfg authored Oct 14, 2023
1 parent 4b0c551 commit 247c097
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 57 deletions.
9 changes: 8 additions & 1 deletion bundles/org.openhab.binding.hue/doc/readme_v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,13 @@ Device things support some of the following channels:
The exact list of channels in a given device is determined at run time when the system is started.
Each device reports its own live list of capabilities, and the respective list of channels is created accordingly.

The channels `color-xy-only`, `dimming-only` and `on-off-only` are *advanced* channels - see [below](###advanced-channels-for-devices-,-rooms-and-zones) for more details.
The channels `color-xy-only`, `dimming-only` and `on-off-only` are *advanced* channels - see [below](#advanced-channels-for-devices-rooms-and-zones) for more details.

The `effect` channel is an amalgamation of 'normal' and 'timed' effects.
To activate a 'normal' effect, the binding sends a single command to activate the respective effect.
To activate a 'timed' effect, the binding sends a first command to set the timing followed a second command to activate the effect.
You can explicitly send the timing command via the [dynamics channel](#the-dynamics-channel) before you send the effect command.
Or otherwise the binding will send a default timing command of 15 minutes.

The `button-last-event` channel is a trigger channel.
When the button is pressed the channel receives a number as calculated by the following formula:
Expand Down Expand Up @@ -140,6 +146,7 @@ When you set a value for the `dynamics` channel (e.g. 2000 milliseconds) and the
When the `dynamics` channel value is changed, it triggers a time window of ten seconds during which the value is active.
If the second command is sent within the active time window, it will be executed gradually according to the `dynamics` channel value.
However, if the second command is sent after the active time window has expired, then it will be executed immediately.
If the second command is a 'timed' effect, then the dynamics duration will be applied to that effect.

### Advanced Channels for Devices, Rooms and Zones

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public class HueBindingConstants {

// channel IDs that (optionally) support dynamics
public static final Set<String> DYNAMIC_CHANNELS = Set.of(CHANNEL_2_BRIGHTNESS, CHANNEL_2_COLOR,
CHANNEL_2_COLOR_TEMP_PERCENT, CHANNEL_2_COLOR_TEMP_ABSOLUTE, CHANNEL_2_SCENE);
CHANNEL_2_COLOR_TEMP_PERCENT, CHANNEL_2_COLOR_TEMP_ABSOLUTE, CHANNEL_2_SCENE, CHANNEL_2_EFFECT);

/*
* Map of API v1 channel IDs against API v2 channel IDs where, if the v1 channel exists in the system, then we
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType;
import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType;
import org.openhab.binding.hue.internal.dto.clip2.enums.RecallAction;
import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType;
import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus;
Expand Down Expand Up @@ -320,13 +321,33 @@ public State getDimmingState() {
return UnDefType.NULL;
}

public @Nullable Effects getEffects() {
public @Nullable Effects getFixedEffects() {
return effects;
}

/**
* Get the amalgamated effect state. The result may be either from an 'effects' field or from a 'timedEffects'
* field. If both fields are missing it returns UnDefType.NULL, otherwise if either field is present and has an
* active value (other than EffectType.NO_EFFECT) it returns a StringType of the name of the respective active
* effect; and if none of the above apply, it returns a StringType of 'NO_EFFECT'.
*
* @return either a StringType value or UnDefType.NULL
*/
public State getEffectState() {
Effects effects = this.effects;
return Objects.nonNull(effects) ? new StringType(effects.getStatus().name()) : UnDefType.NULL;
TimedEffects timedEffects = this.timedEffects;
if (Objects.isNull(effects) && Objects.isNull(timedEffects)) {
return UnDefType.NULL;
}
EffectType effect = Objects.nonNull(effects) ? effects.getStatus() : null;
if (Objects.nonNull(effect) && effect != EffectType.NO_EFFECT) {
return new StringType(effect.name());
}
EffectType timedEffect = Objects.nonNull(timedEffects) ? timedEffects.getStatus() : null;
if (Objects.nonNull(timedEffect) && timedEffect != EffectType.NO_EFFECT) {
return new StringType(timedEffect.name());
}
return new StringType(EffectType.NO_EFFECT.name());
}

public @Nullable Boolean getEnabled() {
Expand Down Expand Up @@ -517,7 +538,7 @@ public State getTemperatureValidState() {
return Objects.nonNull(temperature) ? temperature.getTemperatureValidState() : UnDefType.NULL;
}

public @Nullable Effects getTimedEffects() {
public @Nullable TimedEffects getTimedEffects() {
return timedEffects;
}

Expand Down Expand Up @@ -577,7 +598,7 @@ public Resource setDynamicsDuration(Duration duration) {
return this;
}

public Resource setEffects(Effects effect) {
public Resource setFixedEffects(Effects effect) {
this.effects = effect;
return this;
}
Expand Down Expand Up @@ -640,6 +661,19 @@ public Resource setRecallDuration(Duration recallDuration) {
return this;
}

public Resource setTimedEffects(TimedEffects timedEffects) {
this.timedEffects = timedEffects;
return this;
}

public Resource setTimedEffectsDuration(Duration dynamicsDuration) {
TimedEffects timedEffects = this.timedEffects;
if (Objects.nonNull(timedEffects)) {
timedEffects.setDuration(dynamicsDuration);
}
return this;
}

public Resource setType(ResourceType resourceType) {
this.type = resourceType.name().toLowerCase();
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
*/
@NonNullByDefault
public class TimedEffects extends Effects {
public static final Duration DEFAULT_DURATION = Duration.ofMinutes(15);

private @Nullable Long duration;

public @Nullable Duration getDuration() {
Long duration = this.duration;
return Objects.nonNull(duration) ? Duration.ofMillis(duration) : Duration.ZERO;
return Objects.nonNull(duration) ? Duration.ofMillis(duration) : null;
}

public TimedEffects setDuration(Duration duration) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.openhab.binding.hue.internal.dto.clip2.helper;

import java.math.BigDecimal;
import java.time.Duration;
import java.util.List;
import java.util.Objects;

Expand All @@ -29,6 +30,7 @@
import org.openhab.binding.hue.internal.dto.clip2.MirekSchema;
import org.openhab.binding.hue.internal.dto.clip2.OnState;
import org.openhab.binding.hue.internal.dto.clip2.Resource;
import org.openhab.binding.hue.internal.dto.clip2.TimedEffects;
import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType;
import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType;
import org.openhab.core.library.types.DecimalType;
Expand Down Expand Up @@ -198,9 +200,9 @@ public static Resource setDimming(Resource target, Command command, @Nullable Re
}

/**
* Setter for Effect field:
* Use the given command value to set the target resource DTO value based on the attributes of the source resource
* (if any).
* Setter for fixed or timed effect field:
* Use the given command value to set the target fixed or timed effects resource DTO value based on the attributes
* of the source resource (if any).
*
* @param target the target resource.
* @param command the new state command should be a StringType.
Expand All @@ -210,12 +212,16 @@ public static Resource setDimming(Resource target, Command command, @Nullable Re
*/
public static Resource setEffect(Resource target, Command command, @Nullable Resource source) {
if ((command instanceof StringType) && Objects.nonNull(source)) {
Effects otherEffects = source.getEffects();
if (Objects.nonNull(otherEffects)) {
EffectType effectType = EffectType.of(((StringType) command).toString());
if (otherEffects.allows(effectType)) {
target.setEffects(new Effects().setEffect(effectType));
}
EffectType commandEffectType = EffectType.of(((StringType) command).toString());
Effects sourceFixedEffects = source.getFixedEffects();
if (Objects.nonNull(sourceFixedEffects) && sourceFixedEffects.allows(commandEffectType)) {
target.setFixedEffects(new Effects().setEffect(commandEffectType));
}
TimedEffects sourceTimedEffects = source.getTimedEffects();
if (Objects.nonNull(sourceTimedEffects) && sourceTimedEffects.allows(commandEffectType)) {
Duration duration = sourceTimedEffects.getDuration();
target.setTimedEffects(((TimedEffects) new TimedEffects().setEffect(commandEffectType))
.setDuration(Objects.nonNull(duration) ? duration : TimedEffects.DEFAULT_DURATION));
}
}
return target;
Expand All @@ -239,75 +245,103 @@ public static Resource setResource(Resource target, Resource source) {
if (Objects.isNull(targetOnOff) && Objects.nonNull(sourceOnOff)) {
target.setOnState(sourceOnOff);
}

// dimming
Dimming targetDimming = target.getDimming();
Dimming sourceDimming = source.getDimming();
if (Objects.isNull(targetDimming) && Objects.nonNull(sourceDimming)) {
target.setDimming(sourceDimming);
targetDimming = target.getDimming();
}

// minimum dimming level
Double targetMinDimmingLevel = Objects.nonNull(targetDimming) ? targetDimming.getMinimumDimmingLevel() : null;
Double sourceMinDimmingLevel = Objects.nonNull(sourceDimming) ? sourceDimming.getMinimumDimmingLevel() : null;
if (Objects.isNull(targetMinDimmingLevel) && Objects.nonNull(sourceMinDimmingLevel)) {
targetDimming = Objects.nonNull(targetDimming) ? targetDimming : new Dimming();
targetDimming.setMinimumDimmingLevel(sourceMinDimmingLevel);
if (Objects.nonNull(targetDimming)) {
Double sourceMinDimLevel = Objects.isNull(sourceDimming) ? null : sourceDimming.getMinimumDimmingLevel();
if (Objects.nonNull(sourceMinDimLevel)) {
targetDimming.setMinimumDimmingLevel(sourceMinDimLevel);
}
}

// color
ColorXy targetColor = target.getColorXy();
ColorXy sourceColor = source.getColorXy();
if (Objects.isNull(targetColor) && Objects.nonNull(sourceColor)) {
target.setColorXy(sourceColor);
targetColor = target.getColorXy();
}

// color gamut
Gamut targetGamut = Objects.nonNull(targetColor) ? targetColor.getGamut() : null;
Gamut sourceGamut = Objects.nonNull(sourceColor) ? sourceColor.getGamut() : null;
if (Objects.isNull(targetGamut) && Objects.nonNull(sourceGamut)) {
targetColor = Objects.nonNull(targetColor) ? targetColor : new ColorXy();
Gamut sourceGamut = Objects.isNull(sourceColor) ? null : sourceColor.getGamut();
if (Objects.nonNull(targetColor) && Objects.nonNull(sourceGamut)) {
targetColor.setGamut(sourceGamut);
}

// color temperature
ColorTemperature targetColorTemp = target.getColorTemperature();
ColorTemperature sourceColorTemp = source.getColorTemperature();
if (Objects.isNull(targetColorTemp) && Objects.nonNull(sourceColorTemp)) {
target.setColorTemperature(sourceColorTemp);
targetColorTemp = target.getColorTemperature();
}

// mirek schema
MirekSchema targetMirekSchema = Objects.nonNull(targetColorTemp) ? targetColorTemp.getMirekSchema() : null;
MirekSchema sourceMirekSchema = Objects.nonNull(sourceColorTemp) ? sourceColorTemp.getMirekSchema() : null;
if (Objects.isNull(targetMirekSchema) && Objects.nonNull(sourceMirekSchema)) {
targetColorTemp = Objects.nonNull(targetColorTemp) ? targetColorTemp : new ColorTemperature();
targetColorTemp.setMirekSchema(sourceMirekSchema);
if (Objects.nonNull(targetColorTemp)) {
MirekSchema sourceMirekSchema = Objects.isNull(sourceColorTemp) ? null : sourceColorTemp.getMirekSchema();
if (Objects.nonNull(sourceMirekSchema)) {
targetColorTemp.setMirekSchema(sourceMirekSchema);
}
}

// metadata
MetaData targetMetaData = target.getMetaData();
MetaData sourceMetaData = source.getMetaData();
if (Objects.isNull(targetMetaData) && Objects.nonNull(sourceMetaData)) {
target.setMetadata(sourceMetaData);
}

// alerts
Alerts targetAlerts = target.getAlerts();
Alerts sourceAlerts = source.getAlerts();
if (Objects.isNull(targetAlerts) && Objects.nonNull(sourceAlerts)) {
target.setAlerts(sourceAlerts);
}
// effects
Effects targetEffects = target.getEffects();
Effects sourceEffects = source.getEffects();
if (Objects.isNull(targetEffects) && Objects.nonNull(sourceEffects)) {
targetEffects = sourceEffects;
target.setEffects(sourceEffects);
targetEffects = target.getEffects();

// fixed effects
Effects targetFixedEffects = target.getFixedEffects();
Effects sourceFixedEffects = source.getFixedEffects();
if (Objects.isNull(targetFixedEffects) && Objects.nonNull(sourceFixedEffects)) {
target.setFixedEffects(sourceFixedEffects);
targetFixedEffects = target.getFixedEffects();
}

// fixed effects allowed values
if (Objects.nonNull(targetFixedEffects)) {
List<String> values = Objects.isNull(sourceFixedEffects) ? List.of() : sourceFixedEffects.getStatusValues();
if (!values.isEmpty()) {
targetFixedEffects.setStatusValues(values);
}
}

// timed effects
TimedEffects targetTimedEffects = target.getTimedEffects();
TimedEffects sourceTimedEffects = source.getTimedEffects();
if (Objects.isNull(targetTimedEffects) && Objects.nonNull(sourceTimedEffects)) {
target.setTimedEffects(sourceTimedEffects);
targetTimedEffects = target.getTimedEffects();
}
// effects values
List<String> targetStatusValues = Objects.nonNull(targetEffects) ? targetEffects.getStatusValues() : null;
List<String> sourceStatusValues = Objects.nonNull(sourceEffects) ? sourceEffects.getStatusValues() : null;
if (Objects.isNull(targetStatusValues) && Objects.nonNull(sourceStatusValues)) {
targetEffects = Objects.nonNull(targetEffects) ? targetEffects : new Effects();
targetEffects.setStatusValues(sourceStatusValues);

// timed effects allowed values and duration
if (Objects.nonNull(targetTimedEffects)) {
List<String> values = Objects.isNull(sourceTimedEffects) ? List.of() : sourceTimedEffects.getStatusValues();
if (!values.isEmpty()) {
targetTimedEffects.setStatusValues(values);
}
Duration duration = Objects.isNull(sourceTimedEffects) ? null : sourceTimedEffects.getDuration();
if (Objects.nonNull(duration)) {
targetTimedEffects.setDuration(duration);
}
}

return target;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
Expand All @@ -45,6 +46,7 @@
import org.openhab.binding.hue.internal.dto.clip2.Resource;
import org.openhab.binding.hue.internal.dto.clip2.ResourceReference;
import org.openhab.binding.hue.internal.dto.clip2.Resources;
import org.openhab.binding.hue.internal.dto.clip2.TimedEffects;
import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType;
import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType;
import org.openhab.binding.hue.internal.dto.clip2.enums.RecallAction;
Expand Down Expand Up @@ -337,8 +339,7 @@ public void handleCommand(ChannelUID channelUID, Command commandParam) {
break;

case CHANNEL_2_EFFECT:
putResource = Setters.setEffect(new Resource(lightResourceType), command, cache);
putResource.setOnOff(OnOffType.ON);
putResource = Setters.setEffect(new Resource(lightResourceType), command, cache).setOnOff(OnOffType.ON);
break;

case CHANNEL_2_COLOR_TEMP_PERCENT:
Expand Down Expand Up @@ -487,6 +488,8 @@ public void handleCommand(ChannelUID channelUID, Command commandParam) {
&& !dynamicsDuration.isNegative()) {
if (ResourceType.SCENE == putResource.getType()) {
putResource.setRecallDuration(dynamicsDuration);
} else if (CHANNEL_2_EFFECT == channelId) {
putResource.setTimedEffectsDuration(dynamicsDuration);
} else {
putResource.setDynamicsDuration(dynamicsDuration);
}
Expand Down Expand Up @@ -945,21 +948,23 @@ private synchronized void updateDependencies() {
}

/**
* Process the incoming Resource to initialize the effects channel.
* Process the incoming Resource to initialize the fixed resp. timed effects channel.
*
* @param resource a Resource possibly with an Effects element.
* @param resource a Resource possibly containing a fixed and/or timed effects element.
*/
public void updateEffectChannel(Resource resource) {
Effects effects = resource.getEffects();
if (Objects.nonNull(effects)) {
List<StateOption> stateOptions = effects.getStatusValues().stream()
.map(effect -> EffectType.of(effect).name()).map(effectId -> new StateOption(effectId, effectId))
.collect(Collectors.toList());
if (!stateOptions.isEmpty()) {
stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_EFFECT),
stateOptions);
logger.debug("{} -> updateEffects() found {} effects", resourceId, stateOptions.size());
}
Effects fixedEffects = resource.getFixedEffects();
TimedEffects timedEffects = resource.getTimedEffects();
List<StateOption> stateOptions = Stream
.concat(Objects.nonNull(fixedEffects) ? fixedEffects.getStatusValues().stream() : Stream.empty(),
Objects.nonNull(timedEffects) ? timedEffects.getStatusValues().stream() : Stream.empty())
.map(effect -> {
String effectName = EffectType.of(effect).name();
return new StateOption(effectName, effectName);
}).distinct().collect(Collectors.toList());
if (!stateOptions.isEmpty()) {
stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_EFFECT), stateOptions);
logger.debug("{} -> updateEffects() found {} effects", resourceId, stateOptions.size());
}
}

Expand Down
Loading

0 comments on commit 247c097

Please sign in to comment.