2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.hue.internal.api.dto.clip2;
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;
24 import java.util.Objects;
25 import java.util.Optional;
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;
56 import com.google.gson.JsonElement;
57 import com.google.gson.JsonObject;
58 import com.google.gson.annotations.SerializedName;
61 * Complete Resource information DTO for CLIP 2.
63 * Note: all fields are @Nullable because some cases do not (must not) use them.
65 * @author Andrew Fiddian-Green - Initial contribution
68 public class Resource {
70 public static final double PERCENT_DELTA = 30f;
71 public static final MathContext PERCENT_MATH_CONTEXT = new MathContext(4, RoundingMode.HALF_UP);
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.
79 private transient boolean hasSparseData;
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;
116 * @param resourceType
118 public Resource(@Nullable ResourceType resourceType) {
119 if (Objects.nonNull(resourceType)) {
120 setType(resourceType);
125 * Check if <code>light</code> or <code>grouped_light</code> resource contains any
126 * relevant fields to process according to its type.
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.
131 * @return true is resource contains any relevant field
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()");
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>).
148 * @return true if resource has any HSB field
150 public boolean hasHSBField() {
151 return on != null || dimming != null || color != null;
154 public @Nullable List<ActionEntry> getActions() {
158 public @Nullable Alerts getAlerts() {
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());
170 return new StringType(ActionType.NO_ACTION.name());
173 return UnDefType.NULL;
176 public String getArchetype() {
177 MetaData metaData = getMetaData();
178 if (Objects.nonNull(metaData)) {
179 return metaData.getArchetype().toString();
181 return getType().toString();
184 public State getBatteryLevelState() {
185 Power powerState = this.powerState;
186 return Objects.nonNull(powerState) ? powerState.getBatteryLevelState() : UnDefType.NULL;
189 public State getBatteryLowState() {
190 Power powerState = this.powerState;
191 return Objects.nonNull(powerState) ? powerState.getBatteryLowState() : UnDefType.NULL;
194 public @Nullable String getBridgeId() {
195 String bridgeId = this.bridgeId;
196 return Objects.isNull(bridgeId) || bridgeId.isBlank() ? null : bridgeId;
200 * Get the brightness as a PercentType. If off the brightness is 0, otherwise use dimming value.
202 * @return a PercentType with the dimming state, or UNDEF, or NULL
204 public State getBrightnessState() {
205 Dimming dimming = this.dimming;
206 if (Objects.nonNull(dimming)) {
208 // if off the brightness is 0, otherwise it is the larger of dimming value or minimum dimming level
209 OnState on = this.on;
211 if (Objects.nonNull(on) && !on.isOn()) {
214 Double minimumDimmingLevel = dimming.getMinimumDimmingLevel();
215 brightness = Math.max(Objects.nonNull(minimumDimmingLevel) ? minimumDimmingLevel
216 : Dimming.DEFAULT_MINIMUM_DIMMIMG_LEVEL, Math.min(100f, dimming.getBrightness()));
218 return new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT));
219 } catch (DTOPresentButEmptyException e) {
220 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
223 return UnDefType.NULL;
226 public @Nullable Button getButton() {
231 * Get the state corresponding to a button's last event value multiplied by the controlId found for it in the given
232 * controlIds map. States are decimal values formatted like '1002' where the first digit is the button's controlId
233 * and the last digit is the ordinal value of the button's last event.
235 * @param controlIds the map of control ids to be referenced.
238 public State getButtonEventState(Map<String, Integer> controlIds) {
239 Button button = this.button;
240 if (button == null) {
241 return UnDefType.NULL;
243 ButtonEventType event;
244 ButtonReport buttonReport = button.getButtonReport();
245 if (buttonReport == null) {
246 event = button.getLastEvent();
248 event = buttonReport.getLastEvent();
251 return UnDefType.NULL;
253 return new DecimalType((controlIds.getOrDefault(getId(), 0).intValue() * 1000) + event.ordinal());
256 public State getButtonLastUpdatedState(ZoneId zoneId) {
257 Button button = this.button;
258 if (button == null) {
259 return UnDefType.NULL;
261 ButtonReport buttonReport = button.getButtonReport();
262 if (buttonReport == null) {
263 return UnDefType.UNDEF;
265 Instant lastChanged = buttonReport.getLastChanged();
266 if (Instant.EPOCH.equals(lastChanged)) {
267 return UnDefType.UNDEF;
269 return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
272 public List<ResourceReference> getChildren() {
273 List<ResourceReference> children = this.children;
274 return Objects.nonNull(children) ? children : List.of();
278 * Get the color as an HSBType. This returns an HSB that is based on an amalgamation of the color xy, dimming, and
279 * on/off JSON elements. It takes its 'H' and 'S' parts from the 'ColorXy' JSON element, and its 'B' part from the
280 * on/off resp. dimming JSON elements. If off the B part is 0, otherwise it is the dimming element value. Note: this
281 * method is only to be used on cached state DTOs which already have a defined color gamut.
283 * @return an HSBType containing the current color and brightness level, or UNDEF or NULL.
285 public State getColorState() {
286 ColorXy color = this.color;
287 if (Objects.nonNull(color)) {
289 Gamut gamut = color.getGamut();
290 gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT;
291 HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut);
292 OnState on = this.on;
293 Dimming dimming = this.dimming;
294 double brightness = Objects.nonNull(on) && !on.isOn() ? 0
295 : Objects.nonNull(dimming) ? Math.max(0, Math.min(100, dimming.getBrightness())) : 50;
296 return new HSBType(hsb.getHue(), hsb.getSaturation(),
297 new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT)));
298 } catch (DTOPresentButEmptyException e) {
299 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
302 return UnDefType.NULL;
305 public @Nullable ColorTemperature getColorTemperature() {
306 return colorTemperature;
309 public State getColorTemperatureAbsoluteState() {
310 ColorTemperature colorTemp = colorTemperature;
311 if (Objects.nonNull(colorTemp)) {
313 QuantityType<?> colorTemperature = colorTemp.getAbsolute();
314 if (Objects.nonNull(colorTemperature)) {
315 return colorTemperature;
317 } catch (DTOPresentButEmptyException e) {
318 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
321 return UnDefType.NULL;
325 * Get the colour temperature in percent. Note: this method is only to be used on cached state DTOs which already
326 * have a defined mirek schema.
328 * @return a PercentType with the colour temperature percentage.
330 public State getColorTemperaturePercentState() {
331 ColorTemperature colorTemperature = this.colorTemperature;
332 if (Objects.nonNull(colorTemperature)) {
334 Double percent = colorTemperature.getPercent();
335 if (Objects.nonNull(percent)) {
336 return new PercentType(new BigDecimal(percent, PERCENT_MATH_CONTEXT));
338 } catch (DTOPresentButEmptyException e) {
339 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
342 return UnDefType.NULL;
345 public @Nullable ColorXy getColorXy() {
350 * Return an HSB where the HS part is derived from the color xy JSON element (only), so the B part is 100%
352 * @return an HSBType.
354 public State getColorXyState() {
355 ColorXy color = this.color;
356 if (Objects.nonNull(color)) {
358 Gamut gamut = color.getGamut();
359 gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT;
360 HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut);
361 return new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED);
362 } catch (DTOPresentButEmptyException e) {
363 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
366 return UnDefType.NULL;
369 public State getContactLastUpdatedState(ZoneId zoneId) {
370 ContactReport contactReport = this.contactReport;
371 return Objects.nonNull(contactReport)
372 ? new DateTimeType(ZonedDateTime.ofInstant(contactReport.getLastChanged(), zoneId))
376 public State getContactState() {
377 ContactReport contactReport = this.contactReport;
378 return Objects.isNull(contactReport) ? UnDefType.NULL
379 : ContactStateType.CONTACT == contactReport.getContactState() ? OpenClosedType.CLOSED
380 : OpenClosedType.OPEN;
383 public int getControlId() {
384 MetaData metadata = this.metadata;
385 return Objects.nonNull(metadata) ? metadata.getControlId() : 0;
388 public @Nullable Dimming getDimming() {
393 * Return a PercentType which is derived from the dimming JSON element (only).
395 * @return a PercentType.
397 public State getDimmingState() {
398 Dimming dimming = this.dimming;
399 if (Objects.nonNull(dimming)) {
401 double dimmingValue = Math.max(0f, Math.min(100f, dimming.getBrightness()));
402 return new PercentType(new BigDecimal(dimmingValue, PERCENT_MATH_CONTEXT));
403 } catch (DTOPresentButEmptyException e) {
404 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
407 return UnDefType.NULL;
410 public @Nullable Effects getFixedEffects() {
415 * Get the amalgamated effect state. The result may be either from an 'effects' field or from a 'timedEffects'
416 * field. If both fields are missing it returns UnDefType.NULL, otherwise if either field is present and has an
417 * active value (other than EffectType.NO_EFFECT) it returns a StringType of the name of the respective active
418 * effect; and if none of the above apply, it returns a StringType of 'NO_EFFECT'.
420 * @return either a StringType value or UnDefType.NULL
422 public State getEffectState() {
423 Effects effects = this.effects;
424 TimedEffects timedEffects = this.timedEffects;
425 if (Objects.isNull(effects) && Objects.isNull(timedEffects)) {
426 return UnDefType.NULL;
428 EffectType effect = Objects.nonNull(effects) ? effects.getStatus() : null;
429 if (Objects.nonNull(effect) && effect != EffectType.NO_EFFECT) {
430 return new StringType(effect.name());
432 EffectType timedEffect = Objects.nonNull(timedEffects) ? timedEffects.getStatus() : null;
433 if (Objects.nonNull(timedEffect) && timedEffect != EffectType.NO_EFFECT) {
434 return new StringType(timedEffect.name());
436 return new StringType(EffectType.NO_EFFECT.name());
439 public @Nullable Boolean getEnabled() {
443 public State getEnabledState() {
444 Boolean enabled = this.enabled;
445 return Objects.nonNull(enabled) ? OnOffType.from(enabled.booleanValue()) : UnDefType.NULL;
448 public @Nullable Gamut getGamut() {
449 ColorXy color = this.color;
450 return Objects.nonNull(color) ? color.getGamut() : null;
453 public @Nullable ResourceReference getGroup() {
457 public String getId() {
459 return Objects.nonNull(id) ? id : "";
462 public String getIdV1() {
463 String idV1 = this.idV1;
464 return Objects.nonNull(idV1) ? idV1 : "";
467 public @Nullable LightLevel getLightLevel() {
471 public State getLightLevelState() {
472 LightLevel lightLevel = this.light;
473 if (lightLevel == null) {
474 return UnDefType.NULL;
476 LightLevelReport lightLevelReport = lightLevel.getLightLevelReport();
477 if (lightLevelReport == null) {
478 return lightLevel.getLightLevelState();
480 return new QuantityType<>(Math.pow(10f, (double) lightLevelReport.getLightLevel() / 10000f) - 1f, Units.LUX);
483 public State getLightLevelLastUpdatedState(ZoneId zoneId) {
484 LightLevel lightLevel = this.light;
485 if (lightLevel == null) {
486 return UnDefType.NULL;
488 LightLevelReport lightLevelReport = lightLevel.getLightLevelReport();
489 if (lightLevelReport == null) {
490 return UnDefType.UNDEF;
492 Instant lastChanged = lightLevelReport.getLastChanged();
493 if (Instant.EPOCH.equals(lastChanged)) {
494 return UnDefType.UNDEF;
496 return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
499 public @Nullable MetaData getMetaData() {
503 public @Nullable Double getMinimumDimmingLevel() {
504 Dimming dimming = this.dimming;
505 return Objects.nonNull(dimming) ? dimming.getMinimumDimmingLevel() : null;
508 public @Nullable MirekSchema getMirekSchema() {
509 ColorTemperature colorTemp = this.colorTemperature;
510 return Objects.nonNull(colorTemp) ? colorTemp.getMirekSchema() : null;
513 public @Nullable Motion getMotion() {
517 public State getMotionState() {
518 Motion motion = this.motion;
519 if (motion == null) {
520 return UnDefType.NULL;
522 MotionReport motionReport = motion.getMotionReport();
523 if (motionReport == null) {
524 return motion.getMotionState();
526 return OnOffType.from(motionReport.isMotion());
529 public State getMotionLastUpdatedState(ZoneId zoneId) {
530 Motion motion = this.motion;
531 if (motion == null) {
532 return UnDefType.NULL;
534 MotionReport motionReport = motion.getMotionReport();
535 if (motionReport == null) {
536 return UnDefType.UNDEF;
538 Instant lastChanged = motionReport.getLastChanged();
539 if (Instant.EPOCH.equals(lastChanged)) {
540 return UnDefType.UNDEF;
542 return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
545 public State getMotionValidState() {
546 Motion motion = this.motion;
547 return Objects.nonNull(motion) ? motion.getMotionValidState() : UnDefType.NULL;
550 public String getName() {
551 MetaData metaData = getMetaData();
552 if (Objects.nonNull(metaData)) {
553 String name = metaData.getName();
554 if (Objects.nonNull(name)) {
558 return getType().toString();
562 * Return the state of the On/Off element (only).
564 public State getOnOffState() {
566 OnState on = this.on;
567 return Objects.nonNull(on) ? OnOffType.from(on.isOn()) : UnDefType.NULL;
568 } catch (DTOPresentButEmptyException e) {
569 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
573 public @Nullable OnState getOnState() {
577 public @Nullable ResourceReference getOwner() {
581 public @Nullable Power getPowerState() {
585 public @Nullable ProductData getProductData() {
589 public String getProductName() {
590 ProductData productData = getProductData();
591 if (Objects.nonNull(productData)) {
592 return productData.getProductName();
594 return getType().toString();
597 public @Nullable Recall getRecall() {
601 public @Nullable RelativeRotary getRelativeRotary() {
602 return relativeRotary;
605 public State getRotaryStepsState() {
606 RelativeRotary relativeRotary = this.relativeRotary;
607 if (relativeRotary == null) {
608 return UnDefType.NULL;
610 RotaryReport rotaryReport = relativeRotary.getRotaryReport();
611 if (rotaryReport == null) {
612 return relativeRotary.getStepsState();
614 Rotation rotation = rotaryReport.getRotation();
615 if (rotation == null) {
616 return UnDefType.NULL;
618 return rotation.getStepsState();
621 public State getRotaryStepsLastUpdatedState(ZoneId zoneId) {
622 RelativeRotary relativeRotary = this.relativeRotary;
623 if (relativeRotary == null) {
624 return UnDefType.NULL;
626 RotaryReport rotaryReport = relativeRotary.getRotaryReport();
627 if (rotaryReport == null) {
628 return UnDefType.UNDEF;
630 Instant lastChanged = rotaryReport.getLastChanged();
631 if (Instant.EPOCH.equals(lastChanged)) {
632 return UnDefType.UNDEF;
634 return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
638 * Check if the scene resource contains a 'status.active' element. If such an element is present, returns a Boolean
639 * Optional whose value depends on the value of that element, or an empty Optional if it is not.
641 * @return true, false, or empty.
643 public Optional<Boolean> getSceneActive() {
644 if (ResourceType.SCENE == getType()) {
645 JsonElement status = this.status;
646 if (Objects.nonNull(status) && status.isJsonObject()) {
647 JsonElement active = ((JsonObject) status).get("active");
648 if (Objects.nonNull(active) && active.isJsonPrimitive()) {
649 return Optional.of(!"inactive".equalsIgnoreCase(active.getAsString()));
653 return Optional.empty();
657 * If the getSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result is
658 * present and 'true' (i.e. the scene is active) return the scene name. Or finally (the optional result is present
659 * and 'false') return 'UnDefType.UNDEF'.
661 * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
663 public State getSceneState() {
664 return getSceneActive().map(a -> a ? new StringType(getName()) : UnDefType.UNDEF).orElse(UnDefType.NULL);
668 * Check if the smart scene resource contains a 'state' element. If such an element is present, returns a Boolean
669 * Optional whose value depends on the value of that element, or an empty Optional if it is not.
671 * @return true, false, or empty.
673 public Optional<Boolean> getSmartSceneActive() {
674 if (ResourceType.SMART_SCENE == getType()) {
675 String state = this.state;
676 if (Objects.nonNull(state)) {
677 return Optional.of(SmartSceneState.ACTIVE == SmartSceneState.of(state));
680 return Optional.empty();
684 * If the getSmartSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result
685 * is present and 'true' (i.e. the scene is active) return the smart scene name. Or finally (the optional result is
686 * present and 'false') return 'UnDefType.UNDEF'.
688 * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
690 public State getSmartSceneState() {
691 return getSmartSceneActive().map(a -> a ? new StringType(getName()) : UnDefType.UNDEF).orElse(UnDefType.NULL);
694 public List<ResourceReference> getServiceReferences() {
695 List<ResourceReference> services = this.services;
696 return Objects.nonNull(services) ? services : List.of();
699 public JsonObject getStatus() {
700 JsonElement status = this.status;
701 if (Objects.nonNull(status) && status.isJsonObject()) {
702 return status.getAsJsonObject();
704 return new JsonObject();
707 public State getTamperLastUpdatedState(ZoneId zoneId) {
708 TamperReport report = getTamperReportsLatest();
709 return Objects.nonNull(report) ? new DateTimeType(ZonedDateTime.ofInstant(report.getLastChanged(), zoneId))
714 * The the Hue bridge could return its raw list of tamper reports in any order, so sort the list (latest entry
715 * first) according to the respective 'changed' instant and return the first entry i.e. the latest changed entry.
717 * @return the latest changed tamper report
719 private @Nullable TamperReport getTamperReportsLatest() {
720 List<TamperReport> reports = this.tamperReports;
721 return Objects.nonNull(reports)
722 ? reports.stream().sorted((e1, e2) -> e2.getLastChanged().compareTo(e1.getLastChanged())).findFirst()
727 public State getTamperState() {
728 TamperReport report = getTamperReportsLatest();
729 return Objects.nonNull(report)
730 ? TamperStateType.TAMPERED == report.getTamperState() ? OpenClosedType.OPEN : OpenClosedType.CLOSED
734 public @Nullable Temperature getTemperature() {
738 public State getTemperatureState() {
739 Temperature temperature = this.temperature;
740 if (temperature == null) {
741 return UnDefType.NULL;
743 TemperatureReport temperatureReport = temperature.getTemperatureReport();
744 if (temperatureReport == null) {
745 return temperature.getTemperatureState();
747 return new QuantityType<>(temperatureReport.getTemperature(), SIUnits.CELSIUS);
750 public State getTemperatureLastUpdatedState(ZoneId zoneId) {
751 Temperature temperature = this.temperature;
752 if (temperature == null) {
753 return UnDefType.NULL;
755 TemperatureReport temperatureReport = temperature.getTemperatureReport();
756 if (temperatureReport == null) {
757 return UnDefType.UNDEF;
759 Instant lastChanged = temperatureReport.getLastChanged();
760 if (Instant.EPOCH.equals(lastChanged)) {
761 return UnDefType.UNDEF;
763 return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
766 public State getTemperatureValidState() {
767 Temperature temperature = this.temperature;
768 return Objects.nonNull(temperature) ? temperature.getTemperatureValidState() : UnDefType.NULL;
771 public @Nullable TimedEffects getTimedEffects() {
775 public ResourceType getType() {
776 return ResourceType.of(type);
779 public State getZigbeeState() {
780 ZigbeeStatus zigbeeStatus = getZigbeeStatus();
781 return Objects.nonNull(zigbeeStatus) ? new StringType(zigbeeStatus.toString()) : UnDefType.NULL;
784 public @Nullable ZigbeeStatus getZigbeeStatus() {
785 JsonElement status = this.status;
786 if (Objects.nonNull(status) && status.isJsonPrimitive()) {
787 return ZigbeeStatus.of(status.getAsString());
792 public boolean hasFullState() {
793 return !hasSparseData;
797 * Mark that the resource has sparse data.
799 * @return this instance.
801 public Resource markAsSparse() {
802 hasSparseData = true;
806 public Resource setAlerts(Alerts alert) {
811 public Resource setColorTemperature(ColorTemperature colorTemperature) {
812 this.colorTemperature = colorTemperature;
816 public Resource setColorXy(@Nullable ColorXy color) {
821 public Resource setContactReport(ContactReport contactReport) {
822 this.contactReport = contactReport;
826 public Resource setDimming(@Nullable Dimming dimming) {
827 this.dimming = dimming;
831 public Resource setDynamicsDuration(Duration duration) {
832 dynamics = new Dynamics().setDuration(duration);
836 public Resource setFixedEffects(Effects effect) {
837 this.effects = effect;
841 public Resource setEnabled(Command command) {
842 if (command instanceof OnOffType) {
843 this.enabled = ((OnOffType) command) == OnOffType.ON;
848 public Resource setId(String id) {
853 public Resource setMetadata(MetaData metadata) {
854 this.metadata = metadata;
858 public Resource setMirekSchema(@Nullable MirekSchema schema) {
859 ColorTemperature colorTemperature = this.colorTemperature;
860 if (Objects.nonNull(colorTemperature)) {
861 colorTemperature.setMirekSchema(schema);
867 * Set the on/off JSON element (only).
869 * @param command an OnOffTypee command value.
870 * @return this resource instance.
872 public Resource setOnOff(Command command) {
873 if (command instanceof OnOffType) {
874 OnOffType onOff = (OnOffType) command;
875 OnState on = this.on;
876 on = Objects.nonNull(on) ? on : new OnState();
877 on.setOn(OnOffType.ON.equals(onOff));
883 public Resource setOnState(@Nullable OnState on) {
888 public Resource setRecallAction(SceneRecallAction recallAction) {
889 Recall recall = this.recall;
890 this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
894 public Resource setRecallAction(SmartSceneRecallAction recallAction) {
895 Recall recall = this.recall;
896 this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
900 public Resource setRecallDuration(Duration recallDuration) {
901 Recall recall = this.recall;
902 this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setDuration(recallDuration);
906 public Resource setTamperReports(List<TamperReport> tamperReports) {
907 this.tamperReports = tamperReports;
911 public Resource setTimedEffects(TimedEffects timedEffects) {
912 this.timedEffects = timedEffects;
916 public Resource setTimedEffectsDuration(Duration dynamicsDuration) {
917 TimedEffects timedEffects = this.timedEffects;
918 if (Objects.nonNull(timedEffects)) {
919 timedEffects.setDuration(dynamicsDuration);
924 public Resource setType(ResourceType resourceType) {
925 this.type = resourceType.name().toLowerCase();
930 public String toString() {
932 return String.format("id:%s, type:%s", Objects.nonNull(id) ? id : "?" + " ".repeat(35),
933 getType().name().toLowerCase());