]> git.basschouten.com Git - openhab-addons.git/blob
c41ce35f94e0127614565b5ff63289070087721b
[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.api.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.time.Instant;
20 import java.time.ZoneId;
21 import java.time.ZonedDateTime;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Objects;
25 import java.util.Optional;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ActionType;
30 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ButtonEventType;
31 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContactStateType;
32 import org.openhab.binding.hue.internal.api.dto.clip2.enums.EffectType;
33 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ResourceType;
34 import org.openhab.binding.hue.internal.api.dto.clip2.enums.SceneRecallAction;
35 import org.openhab.binding.hue.internal.api.dto.clip2.enums.SmartSceneRecallAction;
36 import org.openhab.binding.hue.internal.api.dto.clip2.enums.SmartSceneState;
37 import org.openhab.binding.hue.internal.api.dto.clip2.enums.TamperStateType;
38 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ZigbeeStatus;
39 import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException;
40 import org.openhab.core.library.types.DateTimeType;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.library.types.HSBType;
43 import org.openhab.core.library.types.OnOffType;
44 import org.openhab.core.library.types.OpenClosedType;
45 import org.openhab.core.library.types.PercentType;
46 import org.openhab.core.library.types.QuantityType;
47 import org.openhab.core.library.types.StringType;
48 import org.openhab.core.library.unit.SIUnits;
49 import org.openhab.core.library.unit.Units;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.State;
52 import org.openhab.core.types.UnDefType;
53 import org.openhab.core.util.ColorUtil;
54 import org.openhab.core.util.ColorUtil.Gamut;
55
56 import com.google.gson.JsonElement;
57 import com.google.gson.JsonObject;
58 import com.google.gson.annotations.SerializedName;
59
60 /**
61  * Complete Resource information DTO for CLIP 2.
62  *
63  * Note: all fields are @Nullable because some cases do not (must not) use them.
64  *
65  * @author Andrew Fiddian-Green - Initial contribution
66  */
67 @NonNullByDefault
68 public class Resource {
69
70     public static final double PERCENT_DELTA = 30f;
71     public static final MathContext PERCENT_MATH_CONTEXT = new MathContext(4, RoundingMode.HALF_UP);
72
73     /**
74      * The SSE event mechanism sends resources in a sparse (skeleton) format that only includes state fields whose
75      * values have changed. A sparse resource does not contain the full state of the resource. And the absence of any
76      * field from such a resource does not indicate that the field value is UNDEF, but rather that the value is the same
77      * as what it was previously set to by the last non-sparse resource.
78      */
79     private transient boolean hasSparseData;
80
81     private @Nullable String type;
82     private @Nullable String id;
83     private @Nullable @SerializedName("bridge_id") String bridgeId;
84     private @Nullable @SerializedName("id_v1") String idV1;
85     private @Nullable ResourceReference owner;
86     private @Nullable MetaData metadata;
87     private @Nullable @SerializedName("product_data") ProductData productData;
88     private @Nullable List<ResourceReference> services;
89     private @Nullable OnState on;
90     private @Nullable Dimming dimming;
91     private @Nullable @SerializedName("color_temperature") ColorTemperature colorTemperature;
92     private @Nullable ColorXy color;
93     private @Nullable Alerts alert;
94     private @Nullable Effects effects;
95     private @Nullable @SerializedName("timed_effects") TimedEffects timedEffects;
96     private @Nullable ResourceReference group;
97     private @Nullable List<ActionEntry> actions;
98     private @Nullable Recall recall;
99     private @Nullable Boolean enabled;
100     private @Nullable LightLevel light;
101     private @Nullable Button button;
102     private @Nullable Temperature temperature;
103     private @Nullable Motion motion;
104     private @Nullable @SerializedName("power_state") Power powerState;
105     private @Nullable @SerializedName("relative_rotary") RelativeRotary relativeRotary;
106     private @Nullable List<ResourceReference> children;
107     private @Nullable JsonElement status;
108     private @Nullable Dynamics dynamics;
109     private @Nullable @SerializedName("contact_report") ContactReport contactReport;
110     private @Nullable @SerializedName("tamper_reports") List<TamperReport> tamperReports;
111     private @Nullable String state;
112
113     /**
114      * Constructor
115      *
116      * @param resourceType
117      */
118     public Resource(@Nullable ResourceType resourceType) {
119         if (Objects.nonNull(resourceType)) {
120             setType(resourceType);
121         }
122     }
123
124     /**
125      * Check if <code>light</code> or <code>grouped_light</code> resource contains any
126      * relevant fields to process according to its type.
127      * 
128      * As an example, {@link #colorTemperature} is relevant for a <code>light</code>
129      * resource because it's needed for updating the color-temperature channels.
130      *
131      * @return true is resource contains any relevant field
132      */
133     public boolean hasAnyRelevantField() {
134         return switch (getType()) {
135             // https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_light_get
136             case LIGHT -> hasHSBField() || colorTemperature != null || dynamics != null || effects != null
137                     || timedEffects != null;
138             // https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_grouped_light_get
139             case GROUPED_LIGHT -> on != null || dimming != null || alert != null;
140             default -> throw new IllegalStateException(type + " is not supported by hasAnyRelevantField()");
141         };
142     }
143
144     /**
145      * Check if resource contains any field which is needed to represent an HSB value
146      * (<code>on</code>, <code>dimming</code> or <code>color</code>).
147      *
148      * @return true if resource has any HSB field
149      */
150     public boolean hasHSBField() {
151         return on != null || dimming != null || color != null;
152     }
153
154     public @Nullable List<ActionEntry> getActions() {
155         return actions;
156     }
157
158     public @Nullable Alerts getAlerts() {
159         return alert;
160     }
161
162     public State getAlertState() {
163         Alerts alerts = this.alert;
164         if (Objects.nonNull(alerts)) {
165             if (!alerts.getActionValues().isEmpty()) {
166                 ActionType alertType = alerts.getAction();
167                 if (Objects.nonNull(alertType)) {
168                     return new StringType(alertType.name());
169                 }
170                 return new StringType(ActionType.NO_ACTION.name());
171             }
172         }
173         return UnDefType.NULL;
174     }
175
176     public String getArchetype() {
177         MetaData metaData = getMetaData();
178         if (Objects.nonNull(metaData)) {
179             return metaData.getArchetype().toString();
180         }
181         return getType().toString();
182     }
183
184     public State getBatteryLevelState() {
185         Power powerState = this.powerState;
186         return Objects.nonNull(powerState) ? powerState.getBatteryLevelState() : UnDefType.NULL;
187     }
188
189     public State getBatteryLowState() {
190         Power powerState = this.powerState;
191         return Objects.nonNull(powerState) ? powerState.getBatteryLowState() : UnDefType.NULL;
192     }
193
194     public @Nullable String getBridgeId() {
195         String bridgeId = this.bridgeId;
196         return Objects.isNull(bridgeId) || bridgeId.isBlank() ? null : bridgeId;
197     }
198
199     /**
200      * Get the brightness as a PercentType. If off the brightness is 0, otherwise use dimming value.
201      *
202      * @return a PercentType with the dimming state, or UNDEF, or NULL
203      */
204     public State getBrightnessState() {
205         Dimming dimming = this.dimming;
206         if (Objects.nonNull(dimming)) {
207             try {
208                 // if off the brightness is 0, otherwise it is dimming value
209                 OnState on = this.on;
210                 double brightness = Objects.nonNull(on) && !on.isOn() ? 0f
211                         : Math.max(0f, Math.min(100f, dimming.getBrightness()));
212                 return new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT));
213             } catch (DTOPresentButEmptyException e) {
214                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
215             }
216         }
217         return UnDefType.NULL;
218     }
219
220     public @Nullable Button getButton() {
221         return button;
222     }
223
224     /**
225      * Get the state corresponding to a button's last event value multiplied by the controlId found for it in the given
226      * controlIds map. States are decimal values formatted like '1002' where the first digit is the button's controlId
227      * and the last digit is the ordinal value of the button's last event.
228      *
229      * @param controlIds the map of control ids to be referenced.
230      * @return the state.
231      */
232     public State getButtonEventState(Map<String, Integer> controlIds) {
233         Button button = this.button;
234         if (button == null) {
235             return UnDefType.NULL;
236         }
237         ButtonEventType event;
238         ButtonReport buttonReport = button.getButtonReport();
239         if (buttonReport == null) {
240             event = button.getLastEvent();
241         } else {
242             event = buttonReport.getLastEvent();
243         }
244         if (event == null) {
245             return UnDefType.NULL;
246         }
247         return new DecimalType((controlIds.getOrDefault(getId(), 0).intValue() * 1000) + event.ordinal());
248     }
249
250     public State getButtonLastUpdatedState(ZoneId zoneId) {
251         Button button = this.button;
252         if (button == null) {
253             return UnDefType.NULL;
254         }
255         ButtonReport buttonReport = button.getButtonReport();
256         if (buttonReport == null) {
257             return UnDefType.UNDEF;
258         }
259         Instant lastChanged = buttonReport.getLastChanged();
260         if (Instant.EPOCH.equals(lastChanged)) {
261             return UnDefType.UNDEF;
262         }
263         return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
264     }
265
266     public List<ResourceReference> getChildren() {
267         List<ResourceReference> children = this.children;
268         return Objects.nonNull(children) ? children : List.of();
269     }
270
271     /**
272      * Get the color as an HSBType. This returns an HSB that is based on an amalgamation of the color xy, dimming, and
273      * on/off JSON elements. It takes its 'H' and 'S' parts from the 'ColorXy' JSON element, and its 'B' part from the
274      * on/off resp. dimming JSON elements. If off the B part is 0, otherwise it is the dimming element value. Note: this
275      * method is only to be used on cached state DTOs which already have a defined color gamut.
276      *
277      * @return an HSBType containing the current color and brightness level, or UNDEF or NULL.
278      */
279     public State getColorState() {
280         ColorXy color = this.color;
281         if (Objects.nonNull(color)) {
282             try {
283                 Gamut gamut = color.getGamut();
284                 gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT;
285                 HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut);
286                 OnState on = this.on;
287                 Dimming dimming = this.dimming;
288                 double brightness = Objects.nonNull(on) && !on.isOn() ? 0
289                         : Objects.nonNull(dimming) ? Math.max(0, Math.min(100, dimming.getBrightness())) : 50;
290                 return new HSBType(hsb.getHue(), hsb.getSaturation(),
291                         new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT)));
292             } catch (DTOPresentButEmptyException e) {
293                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
294             }
295         }
296         return UnDefType.NULL;
297     }
298
299     public @Nullable ColorTemperature getColorTemperature() {
300         return colorTemperature;
301     }
302
303     public State getColorTemperatureAbsoluteState() {
304         ColorTemperature colorTemp = colorTemperature;
305         if (Objects.nonNull(colorTemp)) {
306             try {
307                 QuantityType<?> colorTemperature = colorTemp.getAbsolute();
308                 if (Objects.nonNull(colorTemperature)) {
309                     return colorTemperature;
310                 }
311             } catch (DTOPresentButEmptyException e) {
312                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
313             }
314         }
315         return UnDefType.NULL;
316     }
317
318     /**
319      * Get the colour temperature in percent. Note: this method is only to be used on cached state DTOs which already
320      * have a defined mirek schema.
321      *
322      * @return a PercentType with the colour temperature percentage.
323      */
324     public State getColorTemperaturePercentState() {
325         ColorTemperature colorTemperature = this.colorTemperature;
326         if (Objects.nonNull(colorTemperature)) {
327             try {
328                 Double percent = colorTemperature.getPercent();
329                 if (Objects.nonNull(percent)) {
330                     return new PercentType(new BigDecimal(percent, PERCENT_MATH_CONTEXT));
331                 }
332             } catch (DTOPresentButEmptyException e) {
333                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
334             }
335         }
336         return UnDefType.NULL;
337     }
338
339     public @Nullable ColorXy getColorXy() {
340         return color;
341     }
342
343     /**
344      * Return an HSB where the HS part is derived from the color xy JSON element (only), so the B part is 100%
345      *
346      * @return an HSBType.
347      */
348     public State getColorXyState() {
349         ColorXy color = this.color;
350         if (Objects.nonNull(color)) {
351             try {
352                 Gamut gamut = color.getGamut();
353                 gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT;
354                 HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut);
355                 return new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED);
356             } catch (DTOPresentButEmptyException e) {
357                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
358             }
359         }
360         return UnDefType.NULL;
361     }
362
363     public State getContactLastUpdatedState(ZoneId zoneId) {
364         ContactReport contactReport = this.contactReport;
365         return Objects.nonNull(contactReport)
366                 ? new DateTimeType(ZonedDateTime.ofInstant(contactReport.getLastChanged(), zoneId))
367                 : UnDefType.NULL;
368     }
369
370     public State getContactState() {
371         ContactReport contactReport = this.contactReport;
372         return Objects.isNull(contactReport) ? UnDefType.NULL
373                 : ContactStateType.CONTACT == contactReport.getContactState() ? OpenClosedType.CLOSED
374                         : OpenClosedType.OPEN;
375     }
376
377     public int getControlId() {
378         MetaData metadata = this.metadata;
379         return Objects.nonNull(metadata) ? metadata.getControlId() : 0;
380     }
381
382     public @Nullable Dimming getDimming() {
383         return dimming;
384     }
385
386     /**
387      * Return a PercentType which is derived from the dimming JSON element (only).
388      *
389      * @return a PercentType.
390      */
391     public State getDimmingState() {
392         Dimming dimming = this.dimming;
393         if (Objects.nonNull(dimming)) {
394             try {
395                 double dimmingValue = Math.max(0f, Math.min(100f, dimming.getBrightness()));
396                 return new PercentType(new BigDecimal(dimmingValue, PERCENT_MATH_CONTEXT));
397             } catch (DTOPresentButEmptyException e) {
398                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
399             }
400         }
401         return UnDefType.NULL;
402     }
403
404     public @Nullable Effects getFixedEffects() {
405         return effects;
406     }
407
408     /**
409      * Get the amalgamated effect state. The result may be either from an 'effects' field or from a 'timedEffects'
410      * field. If both fields are missing it returns UnDefType.NULL, otherwise if either field is present and has an
411      * active value (other than EffectType.NO_EFFECT) it returns a StringType of the name of the respective active
412      * effect; and if none of the above apply, it returns a StringType of 'NO_EFFECT'.
413      *
414      * @return either a StringType value or UnDefType.NULL
415      */
416     public State getEffectState() {
417         Effects effects = this.effects;
418         TimedEffects timedEffects = this.timedEffects;
419         if (Objects.isNull(effects) && Objects.isNull(timedEffects)) {
420             return UnDefType.NULL;
421         }
422         EffectType effect = Objects.nonNull(effects) ? effects.getStatus() : null;
423         if (Objects.nonNull(effect) && effect != EffectType.NO_EFFECT) {
424             return new StringType(effect.name());
425         }
426         EffectType timedEffect = Objects.nonNull(timedEffects) ? timedEffects.getStatus() : null;
427         if (Objects.nonNull(timedEffect) && timedEffect != EffectType.NO_EFFECT) {
428             return new StringType(timedEffect.name());
429         }
430         return new StringType(EffectType.NO_EFFECT.name());
431     }
432
433     public @Nullable Boolean getEnabled() {
434         return enabled;
435     }
436
437     public State getEnabledState() {
438         Boolean enabled = this.enabled;
439         return Objects.nonNull(enabled) ? OnOffType.from(enabled.booleanValue()) : UnDefType.NULL;
440     }
441
442     public @Nullable Gamut getGamut() {
443         ColorXy color = this.color;
444         return Objects.nonNull(color) ? color.getGamut() : null;
445     }
446
447     public @Nullable ResourceReference getGroup() {
448         return group;
449     }
450
451     public String getId() {
452         String id = this.id;
453         return Objects.nonNull(id) ? id : "";
454     }
455
456     public String getIdV1() {
457         String idV1 = this.idV1;
458         return Objects.nonNull(idV1) ? idV1 : "";
459     }
460
461     public @Nullable LightLevel getLightLevel() {
462         return light;
463     }
464
465     public State getLightLevelState() {
466         LightLevel lightLevel = this.light;
467         if (lightLevel == null) {
468             return UnDefType.NULL;
469         }
470         LightLevelReport lightLevelReport = lightLevel.getLightLevelReport();
471         if (lightLevelReport == null) {
472             return lightLevel.getLightLevelState();
473         }
474         return new QuantityType<>(Math.pow(10f, (double) lightLevelReport.getLightLevel() / 10000f) - 1f, Units.LUX);
475     }
476
477     public State getLightLevelLastUpdatedState(ZoneId zoneId) {
478         LightLevel lightLevel = this.light;
479         if (lightLevel == null) {
480             return UnDefType.NULL;
481         }
482         LightLevelReport lightLevelReport = lightLevel.getLightLevelReport();
483         if (lightLevelReport == null) {
484             return UnDefType.UNDEF;
485         }
486         Instant lastChanged = lightLevelReport.getLastChanged();
487         if (Instant.EPOCH.equals(lastChanged)) {
488             return UnDefType.UNDEF;
489         }
490         return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
491     }
492
493     public @Nullable MetaData getMetaData() {
494         return metadata;
495     }
496
497     public @Nullable Double getMinimumDimmingLevel() {
498         Dimming dimming = this.dimming;
499         return Objects.nonNull(dimming) ? dimming.getMinimumDimmingLevel() : null;
500     }
501
502     public @Nullable MirekSchema getMirekSchema() {
503         ColorTemperature colorTemp = this.colorTemperature;
504         return Objects.nonNull(colorTemp) ? colorTemp.getMirekSchema() : null;
505     }
506
507     public @Nullable Motion getMotion() {
508         return motion;
509     }
510
511     public State getMotionState() {
512         Motion motion = this.motion;
513         if (motion == null) {
514             return UnDefType.NULL;
515         }
516         MotionReport motionReport = motion.getMotionReport();
517         if (motionReport == null) {
518             return motion.getMotionState();
519         }
520         return OnOffType.from(motionReport.isMotion());
521     }
522
523     public State getMotionLastUpdatedState(ZoneId zoneId) {
524         Motion motion = this.motion;
525         if (motion == null) {
526             return UnDefType.NULL;
527         }
528         MotionReport motionReport = motion.getMotionReport();
529         if (motionReport == null) {
530             return UnDefType.UNDEF;
531         }
532         Instant lastChanged = motionReport.getLastChanged();
533         if (Instant.EPOCH.equals(lastChanged)) {
534             return UnDefType.UNDEF;
535         }
536         return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
537     }
538
539     public State getMotionValidState() {
540         Motion motion = this.motion;
541         return Objects.nonNull(motion) ? motion.getMotionValidState() : UnDefType.NULL;
542     }
543
544     public String getName() {
545         MetaData metaData = getMetaData();
546         if (Objects.nonNull(metaData)) {
547             String name = metaData.getName();
548             if (Objects.nonNull(name)) {
549                 return name;
550             }
551         }
552         return getType().toString();
553     }
554
555     /**
556      * Return the state of the On/Off element (only).
557      */
558     public State getOnOffState() {
559         try {
560             OnState on = this.on;
561             return Objects.nonNull(on) ? OnOffType.from(on.isOn()) : UnDefType.NULL;
562         } catch (DTOPresentButEmptyException e) {
563             return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
564         }
565     }
566
567     public @Nullable OnState getOnState() {
568         return on;
569     }
570
571     public @Nullable ResourceReference getOwner() {
572         return owner;
573     }
574
575     public @Nullable Power getPowerState() {
576         return powerState;
577     }
578
579     public @Nullable ProductData getProductData() {
580         return productData;
581     }
582
583     public String getProductName() {
584         ProductData productData = getProductData();
585         if (Objects.nonNull(productData)) {
586             return productData.getProductName();
587         }
588         return getType().toString();
589     }
590
591     public @Nullable Recall getRecall() {
592         return recall;
593     }
594
595     public @Nullable RelativeRotary getRelativeRotary() {
596         return relativeRotary;
597     }
598
599     public State getRotaryStepsState() {
600         RelativeRotary relativeRotary = this.relativeRotary;
601         if (relativeRotary == null) {
602             return UnDefType.NULL;
603         }
604         RotaryReport rotaryReport = relativeRotary.getRotaryReport();
605         if (rotaryReport == null) {
606             return relativeRotary.getStepsState();
607         }
608         Rotation rotation = rotaryReport.getRotation();
609         if (rotation == null) {
610             return UnDefType.NULL;
611         }
612         return rotation.getStepsState();
613     }
614
615     public State getRotaryStepsLastUpdatedState(ZoneId zoneId) {
616         RelativeRotary relativeRotary = this.relativeRotary;
617         if (relativeRotary == null) {
618             return UnDefType.NULL;
619         }
620         RotaryReport rotaryReport = relativeRotary.getRotaryReport();
621         if (rotaryReport == null) {
622             return UnDefType.UNDEF;
623         }
624         Instant lastChanged = rotaryReport.getLastChanged();
625         if (Instant.EPOCH.equals(lastChanged)) {
626             return UnDefType.UNDEF;
627         }
628         return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
629     }
630
631     /**
632      * Check if the scene resource contains a 'status.active' element. If such an element is present, returns a Boolean
633      * Optional whose value depends on the value of that element, or an empty Optional if it is not.
634      *
635      * @return true, false, or empty.
636      */
637     public Optional<Boolean> getSceneActive() {
638         if (ResourceType.SCENE == getType()) {
639             JsonElement status = this.status;
640             if (Objects.nonNull(status) && status.isJsonObject()) {
641                 JsonElement active = ((JsonObject) status).get("active");
642                 if (Objects.nonNull(active) && active.isJsonPrimitive()) {
643                     return Optional.of(!"inactive".equalsIgnoreCase(active.getAsString()));
644                 }
645             }
646         }
647         return Optional.empty();
648     }
649
650     /**
651      * If the getSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result is
652      * present and 'true' (i.e. the scene is active) return the scene name. Or finally (the optional result is present
653      * and 'false') return 'UnDefType.UNDEF'.
654      *
655      * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
656      */
657     public State getSceneState() {
658         return getSceneActive().map(a -> a ? new StringType(getName()) : UnDefType.UNDEF).orElse(UnDefType.NULL);
659     }
660
661     /**
662      * Check if the smart scene resource contains a 'state' element. If such an element is present, returns a Boolean
663      * Optional whose value depends on the value of that element, or an empty Optional if it is not.
664      *
665      * @return true, false, or empty.
666      */
667     public Optional<Boolean> getSmartSceneActive() {
668         if (ResourceType.SMART_SCENE == getType()) {
669             String state = this.state;
670             if (Objects.nonNull(state)) {
671                 return Optional.of(SmartSceneState.ACTIVE == SmartSceneState.of(state));
672             }
673         }
674         return Optional.empty();
675     }
676
677     /**
678      * If the getSmartSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result
679      * is present and 'true' (i.e. the scene is active) return the smart scene name. Or finally (the optional result is
680      * present and 'false') return 'UnDefType.UNDEF'.
681      *
682      * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
683      */
684     public State getSmartSceneState() {
685         return getSmartSceneActive().map(a -> a ? new StringType(getName()) : UnDefType.UNDEF).orElse(UnDefType.NULL);
686     }
687
688     public List<ResourceReference> getServiceReferences() {
689         List<ResourceReference> services = this.services;
690         return Objects.nonNull(services) ? services : List.of();
691     }
692
693     public JsonObject getStatus() {
694         JsonElement status = this.status;
695         if (Objects.nonNull(status) && status.isJsonObject()) {
696             return status.getAsJsonObject();
697         }
698         return new JsonObject();
699     }
700
701     public State getTamperLastUpdatedState(ZoneId zoneId) {
702         TamperReport report = getTamperReportsLatest();
703         return Objects.nonNull(report) ? new DateTimeType(ZonedDateTime.ofInstant(report.getLastChanged(), zoneId))
704                 : UnDefType.NULL;
705     }
706
707     /**
708      * The the Hue bridge could return its raw list of tamper reports in any order, so sort the list (latest entry
709      * first) according to the respective 'changed' instant and return the first entry i.e. the latest changed entry.
710      *
711      * @return the latest changed tamper report
712      */
713     private @Nullable TamperReport getTamperReportsLatest() {
714         List<TamperReport> reports = this.tamperReports;
715         return Objects.nonNull(reports)
716                 ? reports.stream().sorted((e1, e2) -> e2.getLastChanged().compareTo(e1.getLastChanged())).findFirst()
717                         .orElse(null)
718                 : null;
719     }
720
721     public State getTamperState() {
722         TamperReport report = getTamperReportsLatest();
723         return Objects.nonNull(report)
724                 ? TamperStateType.TAMPERED == report.getTamperState() ? OpenClosedType.OPEN : OpenClosedType.CLOSED
725                 : UnDefType.NULL;
726     }
727
728     public @Nullable Temperature getTemperature() {
729         return temperature;
730     }
731
732     public State getTemperatureState() {
733         Temperature temperature = this.temperature;
734         if (temperature == null) {
735             return UnDefType.NULL;
736         }
737         TemperatureReport temperatureReport = temperature.getTemperatureReport();
738         if (temperatureReport == null) {
739             return temperature.getTemperatureState();
740         }
741         return new QuantityType<>(temperatureReport.getTemperature(), SIUnits.CELSIUS);
742     }
743
744     public State getTemperatureLastUpdatedState(ZoneId zoneId) {
745         Temperature temperature = this.temperature;
746         if (temperature == null) {
747             return UnDefType.NULL;
748         }
749         TemperatureReport temperatureReport = temperature.getTemperatureReport();
750         if (temperatureReport == null) {
751             return UnDefType.UNDEF;
752         }
753         Instant lastChanged = temperatureReport.getLastChanged();
754         if (Instant.EPOCH.equals(lastChanged)) {
755             return UnDefType.UNDEF;
756         }
757         return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
758     }
759
760     public State getTemperatureValidState() {
761         Temperature temperature = this.temperature;
762         return Objects.nonNull(temperature) ? temperature.getTemperatureValidState() : UnDefType.NULL;
763     }
764
765     public @Nullable TimedEffects getTimedEffects() {
766         return timedEffects;
767     }
768
769     public ResourceType getType() {
770         return ResourceType.of(type);
771     }
772
773     public State getZigbeeState() {
774         ZigbeeStatus zigbeeStatus = getZigbeeStatus();
775         return Objects.nonNull(zigbeeStatus) ? new StringType(zigbeeStatus.toString()) : UnDefType.NULL;
776     }
777
778     public @Nullable ZigbeeStatus getZigbeeStatus() {
779         JsonElement status = this.status;
780         if (Objects.nonNull(status) && status.isJsonPrimitive()) {
781             return ZigbeeStatus.of(status.getAsString());
782         }
783         return null;
784     }
785
786     public boolean hasFullState() {
787         return !hasSparseData;
788     }
789
790     /**
791      * Mark that the resource has sparse data.
792      *
793      * @return this instance.
794      */
795     public Resource markAsSparse() {
796         hasSparseData = true;
797         return this;
798     }
799
800     public Resource setAlerts(Alerts alert) {
801         this.alert = alert;
802         return this;
803     }
804
805     public Resource setColorTemperature(ColorTemperature colorTemperature) {
806         this.colorTemperature = colorTemperature;
807         return this;
808     }
809
810     public Resource setColorXy(@Nullable ColorXy color) {
811         this.color = color;
812         return this;
813     }
814
815     public Resource setContactReport(ContactReport contactReport) {
816         this.contactReport = contactReport;
817         return this;
818     }
819
820     public Resource setDimming(@Nullable Dimming dimming) {
821         this.dimming = dimming;
822         return this;
823     }
824
825     public Resource setDynamicsDuration(Duration duration) {
826         dynamics = new Dynamics().setDuration(duration);
827         return this;
828     }
829
830     public Resource setFixedEffects(Effects effect) {
831         this.effects = effect;
832         return this;
833     }
834
835     public Resource setEnabled(Command command) {
836         if (command instanceof OnOffType) {
837             this.enabled = ((OnOffType) command) == OnOffType.ON;
838         }
839         return this;
840     }
841
842     public Resource setId(String id) {
843         this.id = id;
844         return this;
845     }
846
847     public Resource setMetadata(MetaData metadata) {
848         this.metadata = metadata;
849         return this;
850     }
851
852     public Resource setMirekSchema(@Nullable MirekSchema schema) {
853         ColorTemperature colorTemperature = this.colorTemperature;
854         if (Objects.nonNull(colorTemperature)) {
855             colorTemperature.setMirekSchema(schema);
856         }
857         return this;
858     }
859
860     /**
861      * Set the on/off JSON element (only).
862      *
863      * @param command an OnOffTypee command value.
864      * @return this resource instance.
865      */
866     public Resource setOnOff(Command command) {
867         if (command instanceof OnOffType) {
868             OnOffType onOff = (OnOffType) command;
869             OnState on = this.on;
870             on = Objects.nonNull(on) ? on : new OnState();
871             on.setOn(OnOffType.ON.equals(onOff));
872             this.on = on;
873         }
874         return this;
875     }
876
877     public void setOnState(@Nullable OnState on) {
878         this.on = on;
879     }
880
881     public Resource setRecallAction(SceneRecallAction recallAction) {
882         Recall recall = this.recall;
883         this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
884         return this;
885     }
886
887     public Resource setRecallAction(SmartSceneRecallAction recallAction) {
888         Recall recall = this.recall;
889         this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
890         return this;
891     }
892
893     public Resource setRecallDuration(Duration recallDuration) {
894         Recall recall = this.recall;
895         this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setDuration(recallDuration);
896         return this;
897     }
898
899     public Resource setTamperReports(List<TamperReport> tamperReports) {
900         this.tamperReports = tamperReports;
901         return this;
902     }
903
904     public Resource setTimedEffects(TimedEffects timedEffects) {
905         this.timedEffects = timedEffects;
906         return this;
907     }
908
909     public Resource setTimedEffectsDuration(Duration dynamicsDuration) {
910         TimedEffects timedEffects = this.timedEffects;
911         if (Objects.nonNull(timedEffects)) {
912             timedEffects.setDuration(dynamicsDuration);
913         }
914         return this;
915     }
916
917     public Resource setType(ResourceType resourceType) {
918         this.type = resourceType.name().toLowerCase();
919         return this;
920     }
921
922     @Override
923     public String toString() {
924         String id = this.id;
925         return String.format("id:%s, type:%s", Objects.nonNull(id) ? id : "?" + " ".repeat(35),
926                 getType().name().toLowerCase());
927     }
928 }