From 247c0973b6b4e7962762d669b0a5b058abde7581 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Sat, 14 Oct 2023 18:30:15 +0100 Subject: [PATCH] [hue] Support timed effects (#15408) Signed-off-by: Andrew Fiddian-Green --- .../org.openhab.binding.hue/doc/readme_v2.md | 9 +- .../hue/internal/HueBindingConstants.java | 2 +- .../hue/internal/dto/clip2/Resource.java | 42 ++++++- .../hue/internal/dto/clip2/TimedEffects.java | 4 +- .../internal/dto/clip2/helper/Setters.java | 106 ++++++++++------ .../internal/handler/Clip2ThingHandler.java | 33 ++--- .../hue/internal/clip2/Clip2DtoTest.java | 115 ++++++++++++++++++ 7 files changed, 254 insertions(+), 57 deletions(-) diff --git a/bundles/org.openhab.binding.hue/doc/readme_v2.md b/bundles/org.openhab.binding.hue/doc/readme_v2.md index 2bec43af0e1..2d7b38780b5 100644 --- a/bundles/org.openhab.binding.hue/doc/readme_v2.md +++ b/bundles/org.openhab.binding.hue/doc/readme_v2.md @@ -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 diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java index 11ad57e7256..83c6ac420d1 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java @@ -167,7 +167,7 @@ public class HueBindingConstants { // channel IDs that (optionally) support dynamics public static final Set 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 diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resource.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resource.java index c9e21460883..02330cd20d9 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resource.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resource.java @@ -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; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/TimedEffects.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/TimedEffects.java index 2591d9f1e87..e59dc68f470 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/TimedEffects.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/TimedEffects.java @@ -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) { diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/helper/Setters.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/helper/Setters.java index 17dc1b6d2b8..df400725a31 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/helper/Setters.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/helper/Setters.java @@ -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 targetStatusValues = Objects.nonNull(targetEffects) ? targetEffects.getStatusValues() : null; - List 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 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 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; } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2ThingHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2ThingHandler.java index 6a2e561363a..93037557c26 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2ThingHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2ThingHandler.java @@ -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 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 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()); } } diff --git a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java index fb32ce5743f..84e7623e61e 100644 --- a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java +++ b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java @@ -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()); + } }