]> git.basschouten.com Git - openhab-addons.git/blob
b749da3c9bd706e8893b37022d7be56ca872a6a0
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.hue.internal.dto.clip2;
14
15 import java.math.BigDecimal;
16 import java.math.MathContext;
17 import java.math.RoundingMode;
18 import java.time.Duration;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Objects;
22 import java.util.Optional;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType;
27 import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType;
28 import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType;
29 import org.openhab.binding.hue.internal.dto.clip2.enums.SceneRecallAction;
30 import org.openhab.binding.hue.internal.dto.clip2.enums.SmartSceneRecallAction;
31 import org.openhab.binding.hue.internal.dto.clip2.enums.SmartSceneState;
32 import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus;
33 import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException;
34 import org.openhab.core.library.types.DecimalType;
35 import org.openhab.core.library.types.HSBType;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.PercentType;
38 import org.openhab.core.library.types.QuantityType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.State;
42 import org.openhab.core.types.UnDefType;
43 import org.openhab.core.util.ColorUtil;
44 import org.openhab.core.util.ColorUtil.Gamut;
45
46 import com.google.gson.JsonElement;
47 import com.google.gson.JsonObject;
48 import com.google.gson.annotations.SerializedName;
49
50 /**
51  * Complete Resource information DTO for CLIP 2.
52  *
53  * Note: all fields are @Nullable because some cases do not (must not) use them.
54  *
55  * @author Andrew Fiddian-Green - Initial contribution
56  */
57 @NonNullByDefault
58 public class Resource {
59
60     public static final double PERCENT_DELTA = 30f;
61     public static final MathContext PERCENT_MATH_CONTEXT = new MathContext(4, RoundingMode.HALF_UP);
62
63     /**
64      * The SSE event mechanism sends resources in a sparse (skeleton) format that only includes state fields whose
65      * values have changed. A sparse resource does not contain the full state of the resource. And the absence of any
66      * field from such a resource does not indicate that the field value is UNDEF, but rather that the value is the same
67      * as what it was previously set to by the last non-sparse resource.
68      */
69     private transient boolean hasSparseData;
70
71     private @Nullable String type;
72     private @Nullable String id;
73     private @Nullable @SerializedName("bridge_id") String bridgeId;
74     private @Nullable @SerializedName("id_v1") String idV1;
75     private @Nullable ResourceReference owner;
76     private @Nullable MetaData metadata;
77     private @Nullable @SerializedName("product_data") ProductData productData;
78     private @Nullable List<ResourceReference> services;
79     private @Nullable OnState on;
80     private @Nullable Dimming dimming;
81     private @Nullable @SerializedName("color_temperature") ColorTemperature colorTemperature;
82     private @Nullable ColorXy color;
83     private @Nullable Alerts alert;
84     private @Nullable Effects effects;
85     private @Nullable @SerializedName("timed_effects") TimedEffects timedEffects;
86     private @Nullable ResourceReference group;
87     private @Nullable List<ActionEntry> actions;
88     private @Nullable Recall recall;
89     private @Nullable Boolean enabled;
90     private @Nullable LightLevel light;
91     private @Nullable Button button;
92     private @Nullable Temperature temperature;
93     private @Nullable Motion motion;
94     private @Nullable @SerializedName("power_state") Power powerState;
95     private @Nullable @SerializedName("relative_rotary") RelativeRotary relativeRotary;
96     private @Nullable List<ResourceReference> children;
97     private @Nullable JsonElement status;
98     private @Nullable @SuppressWarnings("unused") Dynamics dynamics;
99     private @Nullable String state;
100
101     /**
102      * Constructor
103      *
104      * @param resourceType
105      */
106     public Resource(@Nullable ResourceType resourceType) {
107         if (Objects.nonNull(resourceType)) {
108             setType(resourceType);
109         }
110     }
111
112     public @Nullable List<ActionEntry> getActions() {
113         return actions;
114     }
115
116     public @Nullable Alerts getAlerts() {
117         return alert;
118     }
119
120     public State getAlertState() {
121         Alerts alerts = this.alert;
122         if (Objects.nonNull(alerts)) {
123             if (!alerts.getActionValues().isEmpty()) {
124                 ActionType alertType = alerts.getAction();
125                 if (Objects.nonNull(alertType)) {
126                     return new StringType(alertType.name());
127                 }
128                 return new StringType(ActionType.NO_ACTION.name());
129             }
130         }
131         return UnDefType.NULL;
132     }
133
134     public String getArchetype() {
135         MetaData metaData = getMetaData();
136         if (Objects.nonNull(metaData)) {
137             return metaData.getArchetype().toString();
138         }
139         return getType().toString();
140     }
141
142     public State getBatteryLevelState() {
143         Power powerState = this.powerState;
144         return Objects.nonNull(powerState) ? powerState.getBatteryLevelState() : UnDefType.NULL;
145     }
146
147     public State getBatteryLowState() {
148         Power powerState = this.powerState;
149         return Objects.nonNull(powerState) ? powerState.getBatteryLowState() : UnDefType.NULL;
150     }
151
152     public @Nullable String getBridgeId() {
153         String bridgeId = this.bridgeId;
154         return Objects.isNull(bridgeId) || bridgeId.isBlank() ? null : bridgeId;
155     }
156
157     /**
158      * Get the brightness as a PercentType. If off the brightness is 0, otherwise use dimming value.
159      *
160      * @return a PercentType with the dimming state, or UNDEF, or NULL
161      */
162     public State getBrightnessState() {
163         Dimming dimming = this.dimming;
164         if (Objects.nonNull(dimming)) {
165             try {
166                 // if off the brightness is 0, otherwise it is dimming value
167                 OnState on = this.on;
168                 double brightness = Objects.nonNull(on) && !on.isOn() ? 0f
169                         : Math.max(0f, Math.min(100f, dimming.getBrightness()));
170                 return new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT));
171             } catch (DTOPresentButEmptyException e) {
172                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
173             }
174         }
175         return UnDefType.NULL;
176     }
177
178     public @Nullable Button getButton() {
179         return button;
180     }
181
182     /**
183      * Get the state corresponding to a button's last event value multiplied by the controlId found for it in the given
184      * controlIds map. States are decimal values formatted like '1002' where the first digit is the button's controlId
185      * and the last digit is the ordinal value of the button's last event.
186      *
187      * @param controlIds the map of control ids to be referenced.
188      * @return the state.
189      */
190     public State getButtonEventState(Map<String, Integer> controlIds) {
191         Button button = this.button;
192         if (Objects.nonNull(button)) {
193             try {
194                 return new DecimalType(
195                         (controlIds.getOrDefault(getId(), 0).intValue() * 1000) + button.getLastEvent().ordinal());
196             } catch (IllegalArgumentException e) {
197                 // fall through
198             }
199         }
200         return UnDefType.NULL;
201     }
202
203     public List<ResourceReference> getChildren() {
204         List<ResourceReference> children = this.children;
205         return Objects.nonNull(children) ? children : List.of();
206     }
207
208     /**
209      * Get the color as an HSBType. This returns an HSB that is based on an amalgamation of the color xy, dimming, and
210      * on/off JSON elements. It takes its 'H' and 'S' parts from the 'ColorXy' JSON element, and its 'B' part from the
211      * on/off resp. dimming JSON elements. If off the B part is 0, otherwise it is the dimming element value. Note: this
212      * method is only to be used on cached state DTOs which already have a defined color gamut.
213      *
214      * @return an HSBType containing the current color and brightness level, or UNDEF or NULL.
215      */
216     public State getColorState() {
217         ColorXy color = this.color;
218         if (Objects.nonNull(color)) {
219             try {
220                 Gamut gamut = color.getGamut();
221                 gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT;
222                 HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut);
223                 OnState on = this.on;
224                 Dimming dimming = this.dimming;
225                 double brightness = Objects.nonNull(on) && !on.isOn() ? 0
226                         : Objects.nonNull(dimming) ? Math.max(0, Math.min(100, dimming.getBrightness())) : 50;
227                 return new HSBType(hsb.getHue(), hsb.getSaturation(),
228                         new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT)));
229             } catch (DTOPresentButEmptyException e) {
230                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
231             }
232         }
233         return UnDefType.NULL;
234     }
235
236     public @Nullable ColorTemperature getColorTemperature() {
237         return colorTemperature;
238     }
239
240     public State getColorTemperatureAbsoluteState() {
241         ColorTemperature colorTemp = colorTemperature;
242         if (Objects.nonNull(colorTemp)) {
243             try {
244                 QuantityType<?> colorTemperature = colorTemp.getAbsolute();
245                 if (Objects.nonNull(colorTemperature)) {
246                     return colorTemperature;
247                 }
248             } catch (DTOPresentButEmptyException e) {
249                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
250             }
251         }
252         return UnDefType.NULL;
253     }
254
255     /**
256      * Get the colour temperature in percent. Note: this method is only to be used on cached state DTOs which already
257      * have a defined mirek schema.
258      *
259      * @return a PercentType with the colour temperature percentage.
260      */
261     public State getColorTemperaturePercentState() {
262         ColorTemperature colorTemperature = this.colorTemperature;
263         if (Objects.nonNull(colorTemperature)) {
264             try {
265                 Double percent = colorTemperature.getPercent();
266                 if (Objects.nonNull(percent)) {
267                     return new PercentType(new BigDecimal(percent, PERCENT_MATH_CONTEXT));
268                 }
269             } catch (DTOPresentButEmptyException e) {
270                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
271             }
272         }
273         return UnDefType.NULL;
274     }
275
276     public @Nullable ColorXy getColorXy() {
277         return color;
278     }
279
280     /**
281      * Return an HSB where the HS part is derived from the color xy JSON element (only), so the B part is 100%
282      *
283      * @return an HSBType.
284      */
285     public State getColorXyState() {
286         ColorXy color = this.color;
287         if (Objects.nonNull(color)) {
288             try {
289                 Gamut gamut = color.getGamut();
290                 gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT;
291                 HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut);
292                 return new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED);
293             } catch (DTOPresentButEmptyException e) {
294                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
295             }
296         }
297         return UnDefType.NULL;
298     }
299
300     public int getControlId() {
301         MetaData metadata = this.metadata;
302         return Objects.nonNull(metadata) ? metadata.getControlId() : 0;
303     }
304
305     public @Nullable Dimming getDimming() {
306         return dimming;
307     }
308
309     /**
310      * Return a PercentType which is derived from the dimming JSON element (only).
311      *
312      * @return a PercentType.
313      */
314     public State getDimmingState() {
315         Dimming dimming = this.dimming;
316         if (Objects.nonNull(dimming)) {
317             try {
318                 double dimmingValue = Math.max(0f, Math.min(100f, dimming.getBrightness()));
319                 return new PercentType(new BigDecimal(dimmingValue, PERCENT_MATH_CONTEXT));
320             } catch (DTOPresentButEmptyException e) {
321                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
322             }
323         }
324         return UnDefType.NULL;
325     }
326
327     public @Nullable Effects getFixedEffects() {
328         return effects;
329     }
330
331     /**
332      * Get the amalgamated effect state. The result may be either from an 'effects' field or from a 'timedEffects'
333      * field. If both fields are missing it returns UnDefType.NULL, otherwise if either field is present and has an
334      * active value (other than EffectType.NO_EFFECT) it returns a StringType of the name of the respective active
335      * effect; and if none of the above apply, it returns a StringType of 'NO_EFFECT'.
336      *
337      * @return either a StringType value or UnDefType.NULL
338      */
339     public State getEffectState() {
340         Effects effects = this.effects;
341         TimedEffects timedEffects = this.timedEffects;
342         if (Objects.isNull(effects) && Objects.isNull(timedEffects)) {
343             return UnDefType.NULL;
344         }
345         EffectType effect = Objects.nonNull(effects) ? effects.getStatus() : null;
346         if (Objects.nonNull(effect) && effect != EffectType.NO_EFFECT) {
347             return new StringType(effect.name());
348         }
349         EffectType timedEffect = Objects.nonNull(timedEffects) ? timedEffects.getStatus() : null;
350         if (Objects.nonNull(timedEffect) && timedEffect != EffectType.NO_EFFECT) {
351             return new StringType(timedEffect.name());
352         }
353         return new StringType(EffectType.NO_EFFECT.name());
354     }
355
356     public @Nullable Boolean getEnabled() {
357         return enabled;
358     }
359
360     public State getEnabledState() {
361         Boolean enabled = this.enabled;
362         return Objects.nonNull(enabled) ? OnOffType.from(enabled.booleanValue()) : UnDefType.NULL;
363     }
364
365     public @Nullable Gamut getGamut() {
366         ColorXy color = this.color;
367         return Objects.nonNull(color) ? color.getGamut() : null;
368     }
369
370     public @Nullable ResourceReference getGroup() {
371         return group;
372     }
373
374     public String getId() {
375         String id = this.id;
376         return Objects.nonNull(id) ? id : "";
377     }
378
379     public String getIdV1() {
380         String idV1 = this.idV1;
381         return Objects.nonNull(idV1) ? idV1 : "";
382     }
383
384     public @Nullable LightLevel getLightLevel() {
385         return light;
386     }
387
388     public State getLightLevelState() {
389         LightLevel light = this.light;
390         return Objects.nonNull(light) ? light.getLightLevelState() : UnDefType.NULL;
391     }
392
393     public @Nullable MetaData getMetaData() {
394         return metadata;
395     }
396
397     public @Nullable Double getMinimumDimmingLevel() {
398         Dimming dimming = this.dimming;
399         return Objects.nonNull(dimming) ? dimming.getMinimumDimmingLevel() : null;
400     }
401
402     public @Nullable MirekSchema getMirekSchema() {
403         ColorTemperature colorTemp = this.colorTemperature;
404         return Objects.nonNull(colorTemp) ? colorTemp.getMirekSchema() : null;
405     }
406
407     public @Nullable Motion getMotion() {
408         return motion;
409     }
410
411     public State getMotionState() {
412         Motion motion = this.motion;
413         return Objects.nonNull(motion) ? motion.getMotionState() : UnDefType.NULL;
414     }
415
416     public State getMotionValidState() {
417         Motion motion = this.motion;
418         return Objects.nonNull(motion) ? motion.getMotionValidState() : UnDefType.NULL;
419     }
420
421     public String getName() {
422         MetaData metaData = getMetaData();
423         if (Objects.nonNull(metaData)) {
424             String name = metaData.getName();
425             if (Objects.nonNull(name)) {
426                 return name;
427             }
428         }
429         return getType().toString();
430     }
431
432     /**
433      * Return the state of the On/Off element (only).
434      */
435     public State getOnOffState() {
436         try {
437             OnState on = this.on;
438             return Objects.nonNull(on) ? OnOffType.from(on.isOn()) : UnDefType.NULL;
439         } catch (DTOPresentButEmptyException e) {
440             return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
441         }
442     }
443
444     public @Nullable OnState getOnState() {
445         return on;
446     }
447
448     public @Nullable ResourceReference getOwner() {
449         return owner;
450     }
451
452     public @Nullable Power getPowerState() {
453         return powerState;
454     }
455
456     public @Nullable ProductData getProductData() {
457         return productData;
458     }
459
460     public String getProductName() {
461         ProductData productData = getProductData();
462         if (Objects.nonNull(productData)) {
463             return productData.getProductName();
464         }
465         return getType().toString();
466     }
467
468     public @Nullable Recall getRecall() {
469         return recall;
470     }
471
472     public @Nullable RelativeRotary getRelativeRotary() {
473         return relativeRotary;
474     }
475
476     public State getRelativeRotaryActionState() {
477         RelativeRotary relativeRotary = this.relativeRotary;
478         return Objects.nonNull(relativeRotary) ? relativeRotary.getActionState() : UnDefType.NULL;
479     }
480
481     public State getRotaryStepsState() {
482         RelativeRotary relativeRotary = this.relativeRotary;
483         return Objects.nonNull(relativeRotary) ? relativeRotary.getStepsState() : UnDefType.NULL;
484     }
485
486     /**
487      * Check if the scene resource contains a 'status.active' element. If such an element is present, returns a Boolean
488      * Optional whose value depends on the value of that element, or an empty Optional if it is not.
489      *
490      * @return true, false, or empty.
491      */
492     public Optional<Boolean> getSceneActive() {
493         if (ResourceType.SCENE == getType()) {
494             JsonElement status = this.status;
495             if (Objects.nonNull(status) && status.isJsonObject()) {
496                 JsonElement active = ((JsonObject) status).get("active");
497                 if (Objects.nonNull(active) && active.isJsonPrimitive()) {
498                     return Optional.of(!"inactive".equalsIgnoreCase(active.getAsString()));
499                 }
500             }
501         }
502         return Optional.empty();
503     }
504
505     /**
506      * If the getSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result is
507      * present and 'true' (i.e. the scene is active) return the scene name. Or finally (the optional result is present
508      * and 'false') return 'UnDefType.UNDEF'.
509      *
510      * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
511      */
512     public State getSceneState() {
513         return getSceneActive().map(a -> a ? new StringType(getName()) : UnDefType.UNDEF).orElse(UnDefType.NULL);
514     }
515
516     /**
517      * Check if the smart scene resource contains a 'state' element. If such an element is present, returns a Boolean
518      * Optional whose value depends on the value of that element, or an empty Optional if it is not.
519      *
520      * @return true, false, or empty.
521      */
522     public Optional<Boolean> getSmartSceneActive() {
523         if (ResourceType.SMART_SCENE == getType()) {
524             String state = this.state;
525             if (Objects.nonNull(state)) {
526                 return Optional.of(SmartSceneState.ACTIVE == SmartSceneState.of(state));
527             }
528         }
529         return Optional.empty();
530     }
531
532     /**
533      * If the getSmartSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result
534      * is present and 'true' (i.e. the scene is active) return the smart scene name. Or finally (the optional result is
535      * present and 'false') return 'UnDefType.UNDEF'.
536      *
537      * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
538      */
539     public State getSmartSceneState() {
540         return getSmartSceneActive().map(a -> a ? new StringType(getName()) : UnDefType.UNDEF).orElse(UnDefType.NULL);
541     }
542
543     public List<ResourceReference> getServiceReferences() {
544         List<ResourceReference> services = this.services;
545         return Objects.nonNull(services) ? services : List.of();
546     }
547
548     public JsonObject getStatus() {
549         JsonElement status = this.status;
550         if (Objects.nonNull(status) && status.isJsonObject()) {
551             return status.getAsJsonObject();
552         }
553         return new JsonObject();
554     }
555
556     public @Nullable Temperature getTemperature() {
557         return temperature;
558     }
559
560     public State getTemperatureState() {
561         Temperature temperature = this.temperature;
562         return Objects.nonNull(temperature) ? temperature.getTemperatureState() : UnDefType.NULL;
563     }
564
565     public State getTemperatureValidState() {
566         Temperature temperature = this.temperature;
567         return Objects.nonNull(temperature) ? temperature.getTemperatureValidState() : UnDefType.NULL;
568     }
569
570     public @Nullable TimedEffects getTimedEffects() {
571         return timedEffects;
572     }
573
574     public ResourceType getType() {
575         return ResourceType.of(type);
576     }
577
578     public State getZigbeeState() {
579         ZigbeeStatus zigbeeStatus = getZigbeeStatus();
580         return Objects.nonNull(zigbeeStatus) ? new StringType(zigbeeStatus.toString()) : UnDefType.NULL;
581     }
582
583     public @Nullable ZigbeeStatus getZigbeeStatus() {
584         JsonElement status = this.status;
585         if (Objects.nonNull(status) && status.isJsonPrimitive()) {
586             return ZigbeeStatus.of(status.getAsString());
587         }
588         return null;
589     }
590
591     public boolean hasFullState() {
592         return !hasSparseData;
593     }
594
595     /**
596      * Mark that the resource has sparse data.
597      *
598      * @return this instance.
599      */
600     public Resource markAsSparse() {
601         hasSparseData = true;
602         return this;
603     }
604
605     public Resource setAlerts(Alerts alert) {
606         this.alert = alert;
607         return this;
608     }
609
610     public Resource setColorTemperature(ColorTemperature colorTemperature) {
611         this.colorTemperature = colorTemperature;
612         return this;
613     }
614
615     public Resource setColorXy(ColorXy color) {
616         this.color = color;
617         return this;
618     }
619
620     public Resource setDimming(Dimming dimming) {
621         this.dimming = dimming;
622         return this;
623     }
624
625     public Resource setDynamicsDuration(Duration duration) {
626         dynamics = new Dynamics().setDuration(duration);
627         return this;
628     }
629
630     public Resource setFixedEffects(Effects effect) {
631         this.effects = effect;
632         return this;
633     }
634
635     public Resource setEnabled(Command command) {
636         if (command instanceof OnOffType) {
637             this.enabled = ((OnOffType) command) == OnOffType.ON;
638         }
639         return this;
640     }
641
642     public Resource setId(String id) {
643         this.id = id;
644         return this;
645     }
646
647     public Resource setMetadata(MetaData metadata) {
648         this.metadata = metadata;
649         return this;
650     }
651
652     public Resource setMirekSchema(@Nullable MirekSchema schema) {
653         ColorTemperature colorTemperature = this.colorTemperature;
654         if (Objects.nonNull(colorTemperature)) {
655             colorTemperature.setMirekSchema(schema);
656         }
657         return this;
658     }
659
660     /**
661      * Set the on/off JSON element (only).
662      *
663      * @param command an OnOffTypee command value.
664      * @return this resource instance.
665      */
666     public Resource setOnOff(Command command) {
667         if (command instanceof OnOffType) {
668             OnOffType onOff = (OnOffType) command;
669             OnState on = this.on;
670             on = Objects.nonNull(on) ? on : new OnState();
671             on.setOn(OnOffType.ON.equals(onOff));
672             this.on = on;
673         }
674         return this;
675     }
676
677     public void setOnState(OnState on) {
678         this.on = on;
679     }
680
681     public Resource setRecallAction(SceneRecallAction recallAction) {
682         Recall recall = this.recall;
683         this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
684         return this;
685     }
686
687     public Resource setRecallAction(SmartSceneRecallAction recallAction) {
688         Recall recall = this.recall;
689         this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
690         return this;
691     }
692
693     public Resource setRecallDuration(Duration recallDuration) {
694         Recall recall = this.recall;
695         this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setDuration(recallDuration);
696         return this;
697     }
698
699     public Resource setTimedEffects(TimedEffects timedEffects) {
700         this.timedEffects = timedEffects;
701         return this;
702     }
703
704     public Resource setTimedEffectsDuration(Duration dynamicsDuration) {
705         TimedEffects timedEffects = this.timedEffects;
706         if (Objects.nonNull(timedEffects)) {
707             timedEffects.setDuration(dynamicsDuration);
708         }
709         return this;
710     }
711
712     public Resource setType(ResourceType resourceType) {
713         this.type = resourceType.name().toLowerCase();
714         return this;
715     }
716
717     @Override
718     public String toString() {
719         String id = this.id;
720         return String.format("id:%s, type:%s", Objects.nonNull(id) ? id : "?" + " ".repeat(35),
721                 getType().name().toLowerCase());
722     }
723 }