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