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:
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
// 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
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;
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() {
return Objects.nonNull(temperature) ? temperature.getTemperatureValidState() : UnDefType.NULL;
}
- public @Nullable Effects getTimedEffects() {
+ public @Nullable TimedEffects getTimedEffects() {
return timedEffects;
}
return this;
}
- public Resource setEffects(Effects effect) {
+ public Resource setFixedEffects(Effects effect) {
this.effects = effect;
return this;
}
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;
*/
@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) {
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;
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;
}
/**
- * 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.
*/
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;
if (Objects.isNull(targetOnOff) && Objects.nonNull(sourceOnOff)) {
target.setOnState(sourceOnOff);
}
+
// dimming
Dimming targetDimming = target.getDimming();
Dimming sourceDimming = source.getDimming();
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();
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();
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;
}
}
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;
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;
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:
&& !dynamicsDuration.isNegative()) {
if (ResourceType.SCENE == putResource.getType()) {
putResource.setRecallDuration(dynamicsDuration);
+ } else if (CHANNEL_2_EFFECT == channelId) {
+ putResource.setTimedEffectsDuration(dynamicsDuration);
} else {
putResource.setDynamicsDuration(dynamicsDuration);
}
}
/**
- * 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());
}
}
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;
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;
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;
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());
+ }
}