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