[hue] Support timed effects (#15408)

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
pull/15756/head
Andrew Fiddian-Green 2023-10-14 18:30:15 +01:00 committed by GitHub
parent 4b0c551065
commit 247c0973b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 254 additions and 57 deletions

View File

@ -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:
@ -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

View File

@ -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

View File

@ -24,6 +24,7 @@ import java.util.Optional;
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;
@ -320,13 +321,33 @@ public class Resource {
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() {
@ -517,7 +538,7 @@ public class Resource {
return Objects.nonNull(temperature) ? temperature.getTemperatureValidState() : UnDefType.NULL;
}
public @Nullable Effects getTimedEffects() {
public @Nullable TimedEffects getTimedEffects() {
return timedEffects;
}
@ -577,7 +598,7 @@ public class Resource {
return this;
}
public Resource setEffects(Effects effect) {
public Resource setFixedEffects(Effects effect) {
this.effects = effect;
return this;
}
@ -640,6 +661,19 @@ public class Resource {
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;

View File

@ -25,11 +25,13 @@ import org.eclipse.jdt.annotation.Nullable;
*/
@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) {

View File

@ -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;
@ -29,6 +30,7 @@ import org.openhab.binding.hue.internal.dto.clip2.MetaData;
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;
@ -198,9 +200,9 @@ public class Setters {
}
/**
* 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.
@ -210,12 +212,16 @@ public class Setters {
*/
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;
@ -239,6 +245,7 @@ public class Setters {
if (Objects.isNull(targetOnOff) && Objects.nonNull(sourceOnOff)) {
target.setOnState(sourceOnOff);
}
// dimming
Dimming targetDimming = target.getDimming();
Dimming sourceDimming = source.getDimming();
@ -246,13 +253,15 @@ public class Setters {
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();
@ -260,13 +269,13 @@ public class Setters {
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();
@ -274,40 +283,65 @@ public class Setters {
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();
}
// 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);
// 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();
}
// 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;
}
}

View File

@ -29,6 +29,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
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;
@ -45,6 +46,7 @@ import org.openhab.binding.hue.internal.dto.clip2.ProductData;
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;
@ -337,8 +339,7 @@ public class Clip2ThingHandler extends BaseThingHandler {
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:
@ -487,6 +488,8 @@ public class Clip2ThingHandler extends BaseThingHandler {
&& !dynamicsDuration.isNegative()) {
if (ResourceType.SCENE == putResource.getType()) {
putResource.setRecallDuration(dynamicsDuration);
} else if (CHANNEL_2_EFFECT == channelId) {
putResource.setTimedEffectsDuration(dynamicsDuration);
} else {
putResource.setDynamicsDuration(dynamicsDuration);
}
@ -945,21 +948,23 @@ public class Clip2ThingHandler extends BaseThingHandler {
}
/**
* 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());
}
}

View File

@ -18,6 +18,7 @@ import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@ -28,6 +29,7 @@ import org.openhab.binding.hue.internal.dto.clip2.ActionEntry;
import org.openhab.binding.hue.internal.dto.clip2.Alerts;
import org.openhab.binding.hue.internal.dto.clip2.Button;
import org.openhab.binding.hue.internal.dto.clip2.Dimming;
import org.openhab.binding.hue.internal.dto.clip2.Effects;
import org.openhab.binding.hue.internal.dto.clip2.Event;
import org.openhab.binding.hue.internal.dto.clip2.LightLevel;
import org.openhab.binding.hue.internal.dto.clip2.MetaData;
@ -42,11 +44,13 @@ import org.openhab.binding.hue.internal.dto.clip2.Resources;
import org.openhab.binding.hue.internal.dto.clip2.Rotation;
import org.openhab.binding.hue.internal.dto.clip2.RotationEvent;
import org.openhab.binding.hue.internal.dto.clip2.Temperature;
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.Archetype;
import org.openhab.binding.hue.internal.dto.clip2.enums.BatteryStateType;
import org.openhab.binding.hue.internal.dto.clip2.enums.ButtonEventType;
import org.openhab.binding.hue.internal.dto.clip2.enums.DirectionType;
import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType;
import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType;
import org.openhab.binding.hue.internal.dto.clip2.enums.RotationEventType;
import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus;
@ -597,4 +601,115 @@ class Clip2DtoTest {
assertEquals("db4fd630-3798-40de-b642-c1ef464bf770", service.getId());
assertEquals(ResourceType.GROUPED_LIGHT, service.getType());
}
@Test
void testFixedEffectSetter() {
Resource source;
Resource target;
Effects resultEffect;
// no source effects
source = new Resource(ResourceType.LIGHT);
target = new Resource(ResourceType.LIGHT);
Setters.setResource(target, source);
assertNull(target.getFixedEffects());
// valid source fixed effects
source = new Resource(ResourceType.LIGHT).setFixedEffects(
new Effects().setStatusValues(List.of("NO_EFFECT", "SPARKLE", "CANDLE")).setEffect(EffectType.SPARKLE));
target = new Resource(ResourceType.LIGHT);
Setters.setResource(target, source);
resultEffect = target.getFixedEffects();
assertNotNull(resultEffect);
assertEquals(EffectType.SPARKLE, resultEffect.getEffect());
assertEquals(3, resultEffect.getStatusValues().size());
// valid but different source and target fixed effects
source = new Resource(ResourceType.LIGHT).setFixedEffects(
new Effects().setStatusValues(List.of("NO_EFFECT", "SPARKLE", "CANDLE")).setEffect(EffectType.SPARKLE));
target = new Resource(ResourceType.LIGHT).setFixedEffects(
new Effects().setStatusValues(List.of("NO_EFFECT", "FIRE")).setEffect(EffectType.FIRE));
Setters.setResource(target, source);
resultEffect = target.getFixedEffects();
assertNotNull(resultEffect);
assertNotEquals(EffectType.SPARKLE, resultEffect.getEffect());
assertEquals(3, resultEffect.getStatusValues().size());
// partly valid source fixed effects
source = new Resource(ResourceType.LIGHT).setFixedEffects(new Effects().setStatusValues(List.of("SPARKLE"))
.setEffect(EffectType.SPARKLE).setStatusValues(List.of()));
target = new Resource(ResourceType.LIGHT);
Setters.setResource(target, source);
resultEffect = target.getFixedEffects();
assertNotNull(resultEffect);
assertEquals(EffectType.SPARKLE, resultEffect.getEffect());
assertEquals(0, resultEffect.getStatusValues().size());
assertFalse(resultEffect.allows(EffectType.SPARKLE));
assertFalse(resultEffect.allows(EffectType.NO_EFFECT));
}
@Test
void testTimedEffectSetter() {
Resource source;
Resource target;
Effects resultEffect;
// no source effects
source = new Resource(ResourceType.LIGHT);
target = new Resource(ResourceType.LIGHT);
Setters.setResource(target, source);
assertNull(target.getTimedEffects());
// valid source timed effects
source = new Resource(ResourceType.LIGHT).setTimedEffects((TimedEffects) new TimedEffects()
.setStatusValues(List.of("NO_EFFECT", "SUNRISE")).setEffect(EffectType.NO_EFFECT));
target = new Resource(ResourceType.LIGHT);
Setters.setResource(target, source);
resultEffect = target.getTimedEffects();
assertNotNull(resultEffect);
assertEquals(EffectType.NO_EFFECT, resultEffect.getEffect());
assertEquals(2, resultEffect.getStatusValues().size());
// valid but different source and target timed effects
source = new Resource(ResourceType.LIGHT)
.setTimedEffects((TimedEffects) new TimedEffects().setDuration(Duration.ofMinutes(11))
.setStatusValues(List.of("NO_EFFECT", "SPARKLE", "CANDLE")).setEffect(EffectType.SPARKLE));
target = new Resource(ResourceType.LIGHT).setTimedEffects((TimedEffects) new TimedEffects()
.setStatusValues(List.of("NO_EFFECT", "FIRE")).setEffect(EffectType.FIRE));
Setters.setResource(target, source);
resultEffect = target.getTimedEffects();
assertNotNull(resultEffect);
assertNotEquals(EffectType.SPARKLE, resultEffect.getEffect());
assertEquals(3, resultEffect.getStatusValues().size());
assertTrue(resultEffect instanceof TimedEffects);
assertEquals(Duration.ofMinutes(11), ((TimedEffects) resultEffect).getDuration());
// partly valid source timed effects
source = new Resource(ResourceType.LIGHT).setTimedEffects((TimedEffects) new TimedEffects()
.setStatusValues(List.of("SUNRISE")).setEffect(EffectType.SUNRISE).setStatusValues(List.of()));
target = new Resource(ResourceType.LIGHT);
Setters.setResource(target, source);
resultEffect = target.getTimedEffects();
assertNotNull(resultEffect);
assertEquals(EffectType.SUNRISE, resultEffect.getEffect());
assertEquals(0, resultEffect.getStatusValues().size());
assertFalse(resultEffect.allows(EffectType.SPARKLE));
assertFalse(resultEffect.allows(EffectType.NO_EFFECT));
assertTrue(resultEffect instanceof TimedEffects);
assertNull(((TimedEffects) resultEffect).getDuration());
target.setTimedEffectsDuration(Duration.ofSeconds(22));
assertEquals(Duration.ofSeconds(22), ((TimedEffects) resultEffect).getDuration());
// source timed effect with duration
source = new Resource(ResourceType.LIGHT)
.setTimedEffects((TimedEffects) new TimedEffects().setDuration(Duration.ofMillis(44))
.setStatusValues(List.of("SUNRISE")).setEffect(EffectType.SUNRISE).setStatusValues(List.of()));
target = new Resource(ResourceType.LIGHT);
Setters.setResource(target, source);
resultEffect = target.getTimedEffects();
assertNotNull(resultEffect);
assertTrue(resultEffect instanceof TimedEffects);
assertEquals(Duration.ofMillis(44), ((TimedEffects) resultEffect).getDuration());
}
}