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.CategoryType;
32 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContactStateType;
33 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContentType;
34 import org.openhab.binding.hue.internal.api.dto.clip2.enums.EffectType;
35 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ResourceType;
36 import org.openhab.binding.hue.internal.api.dto.clip2.enums.SceneRecallAction;
37 import org.openhab.binding.hue.internal.api.dto.clip2.enums.SmartSceneRecallAction;
38 import org.openhab.binding.hue.internal.api.dto.clip2.enums.SmartSceneState;
39 import org.openhab.binding.hue.internal.api.dto.clip2.enums.TamperStateType;
40 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ZigbeeStatus;
41 import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException;
42 import org.openhab.core.library.types.DateTimeType;
43 import org.openhab.core.library.types.DecimalType;
44 import org.openhab.core.library.types.HSBType;
45 import org.openhab.core.library.types.OnOffType;
46 import org.openhab.core.library.types.OpenClosedType;
47 import org.openhab.core.library.types.PercentType;
48 import org.openhab.core.library.types.QuantityType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.library.unit.SIUnits;
51 import org.openhab.core.library.unit.Units;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.State;
54 import org.openhab.core.types.UnDefType;
55 import org.openhab.core.util.ColorUtil;
56 import org.openhab.core.util.ColorUtil.Gamut;
58 import com.google.gson.JsonElement;
59 import com.google.gson.JsonObject;
60 import com.google.gson.JsonPrimitive;
61 import com.google.gson.annotations.SerializedName;
64 * Complete Resource information DTO for CLIP 2.
66 * Note: all fields are @Nullable because some cases do not (must not) use them.
68 * @author Andrew Fiddian-Green - Initial contribution
71 public class Resource {
73 public static final MathContext PERCENT_MATH_CONTEXT = new MathContext(4, RoundingMode.HALF_UP);
76 * The SSE event mechanism sends resources in a sparse (skeleton) format that only includes state fields whose
77 * values have changed. A sparse resource does not contain the full state of the resource. And the absence of any
78 * field from such a resource does not indicate that the field value is UNDEF, but rather that the value is the same
79 * as what it was previously set to by the last non-sparse resource.
81 * The following content types are defined:
83 * <li><b>ADD</b> resource being added; contains (assumed) all fields</li>
84 * <li><b>DELETE</b> resource being deleted; contains id and type only</li>
85 * <li><b>UPDATE</b> resource being updated; contains id, type and changed fields</li>
86 * <li><b>ERROR</b> resource with error; contents unknown</li>
87 * <li><b>FULL_STATE</b> existing resource being downloaded; contains all fields</li>
89 private transient ContentType contentType;
91 private @Nullable String type;
92 private @Nullable String id;
93 private @Nullable @SerializedName("bridge_id") String bridgeId;
94 private @Nullable @SerializedName("id_v1") String idV1;
95 private @Nullable ResourceReference owner;
96 private @Nullable MetaData metadata;
97 private @Nullable @SerializedName("product_data") ProductData productData;
98 private @Nullable List<ResourceReference> services;
99 private @Nullable OnState on;
100 private @Nullable Dimming dimming;
101 private @Nullable @SerializedName("color_temperature") ColorTemperature colorTemperature;
102 private @Nullable ColorXy color;
103 private @Nullable Alerts alert;
104 private @Nullable Effects effects;
105 private @Nullable @SerializedName("timed_effects") TimedEffects timedEffects;
106 private @Nullable ResourceReference group;
107 private @Nullable List<ActionEntry> actions;
108 private @Nullable Recall recall;
109 private @Nullable Boolean enabled;
110 private @Nullable LightLevel light;
111 private @Nullable Button button;
112 private @Nullable Temperature temperature;
113 private @Nullable Motion motion;
114 private @Nullable @SerializedName("power_state") Power powerState;
115 private @Nullable @SerializedName("relative_rotary") RelativeRotary relativeRotary;
116 private @Nullable List<ResourceReference> children;
117 private @Nullable JsonElement status;
118 private @Nullable Dynamics dynamics;
119 private @Nullable @SerializedName("contact_report") ContactReport contactReport;
120 private @Nullable @SerializedName("tamper_reports") List<TamperReport> tamperReports;
121 private @Nullable JsonElement state;
122 private @Nullable @SerializedName("script_id") String scriptId;
128 contentType = ContentType.FULL_STATE;
134 * @param resourceType
136 public Resource(@Nullable ResourceType resourceType) {
138 if (Objects.nonNull(resourceType)) {
139 setType(resourceType);
144 * Check if <code>light</code> or <code>grouped_light</code> resource contains any
145 * relevant fields to process according to its type.
147 * As an example, {@link #colorTemperature} is relevant for a <code>light</code>
148 * resource because it's needed for updating the color-temperature channels.
150 * @return true is resource contains any relevant field
152 public boolean hasAnyRelevantField() {
153 return switch (getType()) {
154 // https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_light_get
155 case LIGHT -> hasHSBField() || colorTemperature != null || dynamics != null || effects != null
156 || timedEffects != null;
157 // https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_grouped_light_get
158 case GROUPED_LIGHT -> on != null || dimming != null || alert != null;
159 default -> throw new IllegalStateException(type + " is not supported by hasAnyRelevantField()");
164 * Check if resource contains any field which is needed to represent an HSB value
165 * (<code>on</code>, <code>dimming</code> or <code>color</code>).
167 * @return true if resource has any HSB field
169 public boolean hasHSBField() {
170 return on != null || dimming != null || color != null;
173 public @Nullable List<ActionEntry> getActions() {
177 public @Nullable Alerts getAlerts() {
181 public State getAlertState() {
182 Alerts alerts = this.alert;
183 if (Objects.nonNull(alerts)) {
184 if (!alerts.getActionValues().isEmpty()) {
185 ActionType alertType = alerts.getAction();
186 if (Objects.nonNull(alertType)) {
187 return new StringType(alertType.name());
189 return new StringType(ActionType.NO_ACTION.name());
192 return UnDefType.NULL;
195 public String getArchetype() {
196 MetaData metaData = getMetaData();
197 if (Objects.nonNull(metaData)) {
198 return metaData.getArchetype().toString();
200 return getType().toString();
203 public State getBatteryLevelState() {
204 Power powerState = this.powerState;
205 return Objects.nonNull(powerState) ? powerState.getBatteryLevelState() : UnDefType.NULL;
208 public State getBatteryLowState() {
209 Power powerState = this.powerState;
210 return Objects.nonNull(powerState) ? powerState.getBatteryLowState() : UnDefType.NULL;
213 public @Nullable String getBridgeId() {
214 String bridgeId = this.bridgeId;
215 return Objects.isNull(bridgeId) || bridgeId.isBlank() ? null : bridgeId;
219 * Get the brightness as a PercentType. If off the brightness is 0, otherwise use dimming value.
221 * @return a PercentType with the dimming state, or UNDEF, or NULL
223 public State getBrightnessState() {
224 Dimming dimming = this.dimming;
225 if (Objects.nonNull(dimming)) {
227 // if off the brightness is 0, otherwise it is the larger of dimming value or minimum dimming level
228 OnState on = this.on;
230 if (Objects.nonNull(on) && !on.isOn()) {
233 Double minimumDimmingLevel = dimming.getMinimumDimmingLevel();
234 brightness = Math.max(Objects.nonNull(minimumDimmingLevel) ? minimumDimmingLevel
235 : Dimming.DEFAULT_MINIMUM_DIMMIMG_LEVEL, Math.min(100f, dimming.getBrightness()));
237 return new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT));
238 } catch (DTOPresentButEmptyException e) {
239 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
242 return UnDefType.NULL;
245 public @Nullable Button getButton() {
250 * Get the state corresponding to a button's last event value multiplied by the controlId found for it in the given
251 * controlIds map. States are decimal values formatted like '1002' where the first digit is the button's controlId
252 * and the last digit is the ordinal value of the button's last event.
254 * @param controlIds the map of control ids to be referenced.
257 public State getButtonEventState(Map<String, Integer> controlIds) {
258 Button button = this.button;
259 if (button == null) {
260 return UnDefType.NULL;
262 ButtonEventType event;
263 ButtonReport buttonReport = button.getButtonReport();
264 if (buttonReport == null) {
265 event = button.getLastEvent();
267 event = buttonReport.getLastEvent();
270 return UnDefType.NULL;
272 return new DecimalType((controlIds.getOrDefault(getId(), 0).intValue() * 1000) + event.ordinal());
275 public State getButtonLastUpdatedState(ZoneId zoneId) {
276 Button button = this.button;
277 if (button == null) {
278 return UnDefType.NULL;
280 ButtonReport buttonReport = button.getButtonReport();
281 if (buttonReport == null) {
282 return UnDefType.UNDEF;
284 Instant lastChanged = buttonReport.getLastChanged();
285 if (Instant.EPOCH.equals(lastChanged)) {
286 return UnDefType.UNDEF;
288 return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
291 public List<ResourceReference> getChildren() {
292 List<ResourceReference> children = this.children;
293 return Objects.nonNull(children) ? children : List.of();
297 * Get the color as an HSBType. This returns an HSB that is based on an amalgamation of the color xy, dimming, and
298 * on/off JSON elements. It takes its 'H' and 'S' parts from the 'ColorXy' JSON element, and its 'B' part from the
299 * on/off resp. dimming JSON elements. If off the B part is 0, otherwise it is the dimming element value. Note: this
300 * method is only to be used on cached state DTOs which already have a defined color gamut.
302 * @return an HSBType containing the current color and brightness level, or UNDEF or NULL.
304 public State getColorState() {
305 ColorXy color = this.color;
306 if (Objects.nonNull(color)) {
308 HSBType hsb = ColorUtil.xyToHsb(color.getXY());
309 OnState on = this.on;
310 Dimming dimming = this.dimming;
311 double brightness = Objects.nonNull(on) && !on.isOn() ? 0
312 : Objects.nonNull(dimming) ? Math.max(0, Math.min(100, dimming.getBrightness())) : 50;
313 return new HSBType(hsb.getHue(), hsb.getSaturation(),
314 new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT)));
315 } catch (DTOPresentButEmptyException e) {
316 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
319 return UnDefType.NULL;
322 public @Nullable ColorTemperature getColorTemperature() {
323 return colorTemperature;
326 public State getColorTemperatureAbsoluteState() {
327 ColorTemperature colorTemp = colorTemperature;
328 if (Objects.nonNull(colorTemp)) {
330 QuantityType<?> colorTemperature = colorTemp.getAbsolute();
331 if (Objects.nonNull(colorTemperature)) {
332 return colorTemperature;
334 } catch (DTOPresentButEmptyException e) {
335 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
338 return UnDefType.NULL;
342 * Get the colour temperature in percent. Note: this method is only to be used on cached state DTOs which already
343 * have a defined mirek schema.
345 * @return a PercentType with the colour temperature percentage.
347 public State getColorTemperaturePercentState() {
348 ColorTemperature colorTemperature = this.colorTemperature;
349 if (Objects.nonNull(colorTemperature)) {
351 Double percent = colorTemperature.getPercent();
352 if (Objects.nonNull(percent)) {
353 return new PercentType(new BigDecimal(percent, PERCENT_MATH_CONTEXT));
355 } catch (DTOPresentButEmptyException e) {
356 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
359 return UnDefType.NULL;
362 public @Nullable ColorXy getColorXy() {
367 * Return the resource's metadata category.
369 public CategoryType getCategory() {
370 MetaData metaData = getMetaData();
371 return Objects.nonNull(metaData) ? metaData.getCategory() : CategoryType.NULL;
375 * Return an HSB where the HS part is derived from the color xy JSON element (only), so the B part is 100%
377 * @return an HSBType.
379 public State getColorXyState() {
380 ColorXy color = this.color;
381 if (Objects.nonNull(color)) {
383 HSBType hsb = ColorUtil.xyToHsb(color.getXY());
384 return new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED);
385 } catch (DTOPresentButEmptyException e) {
386 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
389 return UnDefType.NULL;
392 public State getContactLastUpdatedState(ZoneId zoneId) {
393 ContactReport contactReport = this.contactReport;
394 return Objects.nonNull(contactReport)
395 ? new DateTimeType(ZonedDateTime.ofInstant(contactReport.getLastChanged(), zoneId))
399 public State getContactState() {
400 ContactReport contactReport = this.contactReport;
401 return Objects.isNull(contactReport) ? UnDefType.NULL
402 : ContactStateType.CONTACT == contactReport.getContactState() ? OpenClosedType.CLOSED
403 : OpenClosedType.OPEN;
406 public ContentType getContentType() {
410 public int getControlId() {
411 MetaData metadata = this.metadata;
412 return Objects.nonNull(metadata) ? metadata.getControlId() : 0;
415 public @Nullable Dimming getDimming() {
420 * Return a PercentType which is derived from the dimming JSON element (only).
422 * @return a PercentType.
424 public State getDimmingState() {
425 Dimming dimming = this.dimming;
426 if (Objects.nonNull(dimming)) {
428 double dimmingValue = Math.max(0f, Math.min(100f, dimming.getBrightness()));
429 return new PercentType(new BigDecimal(dimmingValue, PERCENT_MATH_CONTEXT));
430 } catch (DTOPresentButEmptyException e) {
431 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
434 return UnDefType.NULL;
437 public @Nullable Effects getFixedEffects() {
442 * Get the amalgamated effect state. The result may be either from an 'effects' field or from a 'timedEffects'
443 * field. If both fields are missing it returns UnDefType.NULL, otherwise if either field is present and has an
444 * active value (other than EffectType.NO_EFFECT) it returns a StringType of the name of the respective active
445 * effect; and if none of the above apply, it returns a StringType of 'NO_EFFECT'.
447 * @return either a StringType value or UnDefType.NULL
449 public State getEffectState() {
450 Effects effects = this.effects;
451 TimedEffects timedEffects = this.timedEffects;
452 if (Objects.isNull(effects) && Objects.isNull(timedEffects)) {
453 return UnDefType.NULL;
455 EffectType effect = Objects.nonNull(effects) ? effects.getStatus() : null;
456 if (Objects.nonNull(effect) && effect != EffectType.NO_EFFECT) {
457 return new StringType(effect.name());
459 EffectType timedEffect = Objects.nonNull(timedEffects) ? timedEffects.getStatus() : null;
460 if (Objects.nonNull(timedEffect) && timedEffect != EffectType.NO_EFFECT) {
461 return new StringType(timedEffect.name());
463 return new StringType(EffectType.NO_EFFECT.name());
466 public @Nullable Boolean getEnabled() {
470 public State getEnabledState() {
471 Boolean enabled = this.enabled;
472 return Objects.nonNull(enabled) ? OnOffType.from(enabled.booleanValue()) : UnDefType.NULL;
475 public @Nullable Gamut getGamut() {
476 ColorXy color = this.color;
477 return Objects.nonNull(color) ? color.getGamut() : null;
480 public @Nullable ResourceReference getGroup() {
484 public String getId() {
486 return Objects.nonNull(id) ? id : "";
489 public String getIdV1() {
490 String idV1 = this.idV1;
491 return Objects.nonNull(idV1) ? idV1 : "";
494 public @Nullable LightLevel getLightLevel() {
498 public State getLightLevelState() {
499 LightLevel lightLevel = this.light;
500 if (lightLevel == null) {
501 return UnDefType.NULL;
503 LightLevelReport lightLevelReport = lightLevel.getLightLevelReport();
504 if (lightLevelReport == null) {
505 return lightLevel.getLightLevelState();
507 return new QuantityType<>(Math.pow(10f, (double) lightLevelReport.getLightLevel() / 10000f) - 1f, Units.LUX);
510 public State getLightLevelLastUpdatedState(ZoneId zoneId) {
511 LightLevel lightLevel = this.light;
512 if (lightLevel == null) {
513 return UnDefType.NULL;
515 LightLevelReport lightLevelReport = lightLevel.getLightLevelReport();
516 if (lightLevelReport == null) {
517 return UnDefType.UNDEF;
519 Instant lastChanged = lightLevelReport.getLastChanged();
520 if (Instant.EPOCH.equals(lastChanged)) {
521 return UnDefType.UNDEF;
523 return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
526 public @Nullable MetaData getMetaData() {
530 public @Nullable Double getMinimumDimmingLevel() {
531 Dimming dimming = this.dimming;
532 return Objects.nonNull(dimming) ? dimming.getMinimumDimmingLevel() : null;
535 public @Nullable MirekSchema getMirekSchema() {
536 ColorTemperature colorTemp = this.colorTemperature;
537 return Objects.nonNull(colorTemp) ? colorTemp.getMirekSchema() : null;
540 public @Nullable Motion getMotion() {
544 public State getMotionState() {
545 Motion motion = this.motion;
546 if (motion == null) {
547 return UnDefType.NULL;
549 MotionReport motionReport = motion.getMotionReport();
550 if (motionReport == null) {
551 return motion.getMotionState();
553 return OnOffType.from(motionReport.isMotion());
556 public State getMotionLastUpdatedState(ZoneId zoneId) {
557 Motion motion = this.motion;
558 if (motion == null) {
559 return UnDefType.NULL;
561 MotionReport motionReport = motion.getMotionReport();
562 if (motionReport == null) {
563 return UnDefType.UNDEF;
565 Instant lastChanged = motionReport.getLastChanged();
566 if (Instant.EPOCH.equals(lastChanged)) {
567 return UnDefType.UNDEF;
569 return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
572 public State getMotionValidState() {
573 Motion motion = this.motion;
574 return Objects.nonNull(motion) ? motion.getMotionValidState() : UnDefType.NULL;
577 public String getName() {
578 MetaData metaData = getMetaData();
579 if (Objects.nonNull(metaData)) {
580 String name = metaData.getName();
581 if (Objects.nonNull(name)) {
585 return getType().toString();
589 * Return the state of the On/Off element (only).
591 public State getOnOffState() {
593 OnState on = this.on;
594 return Objects.nonNull(on) ? OnOffType.from(on.isOn()) : UnDefType.NULL;
595 } catch (DTOPresentButEmptyException e) {
596 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
600 public @Nullable OnState getOnState() {
604 public @Nullable ResourceReference getOwner() {
608 public @Nullable Power getPowerState() {
612 public @Nullable ProductData getProductData() {
616 public String getProductName() {
617 ProductData productData = getProductData();
618 if (Objects.nonNull(productData)) {
619 return productData.getProductName();
621 return getType().toString();
624 public @Nullable Recall getRecall() {
628 public @Nullable RelativeRotary getRelativeRotary() {
629 return relativeRotary;
632 public State getRotaryStepsState() {
633 RelativeRotary relativeRotary = this.relativeRotary;
634 if (relativeRotary == null) {
635 return UnDefType.NULL;
637 RotaryReport rotaryReport = relativeRotary.getRotaryReport();
638 if (rotaryReport == null) {
639 return relativeRotary.getStepsState();
641 Rotation rotation = rotaryReport.getRotation();
642 if (rotation == null) {
643 return UnDefType.NULL;
645 return rotation.getStepsState();
648 public State getRotaryStepsLastUpdatedState(ZoneId zoneId) {
649 RelativeRotary relativeRotary = this.relativeRotary;
650 if (relativeRotary == null) {
651 return UnDefType.NULL;
653 RotaryReport rotaryReport = relativeRotary.getRotaryReport();
654 if (rotaryReport == null) {
655 return UnDefType.UNDEF;
657 Instant lastChanged = rotaryReport.getLastChanged();
658 if (Instant.EPOCH.equals(lastChanged)) {
659 return UnDefType.UNDEF;
661 return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
665 * Check if the scene resource contains a 'status.active' element. If such an element is present, returns a Boolean
666 * Optional whose value depends on the value of that element, or an empty Optional if it is not.
668 * @return true, false, or empty.
670 public Optional<Boolean> getSceneActive() {
671 if (ResourceType.SCENE == getType()) {
672 JsonElement status = this.status;
673 if (Objects.nonNull(status) && status.isJsonObject()) {
674 JsonElement active = ((JsonObject) status).get("active");
675 if (Objects.nonNull(active) && active.isJsonPrimitive()) {
676 return Optional.of(!"inactive".equalsIgnoreCase(active.getAsString()));
680 return Optional.empty();
684 * Return the scriptId if any.
686 public @Nullable String getScriptId() {
691 * If the getSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result is
692 * present and 'true' (i.e. the scene is active) return the scene name. Or finally (the optional result is present
693 * and 'false') return 'UnDefType.UNDEF'.
695 * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
697 public State getSceneState() {
698 return getSceneActive().map(a -> a ? new StringType(getName()) : UnDefType.UNDEF).orElse(UnDefType.NULL);
702 * Check if the smart scene resource contains a 'state' element. If such an element is present, returns a Boolean
703 * Optional whose value depends on the value of that element, or an empty Optional if it is not. Note that in some
704 * resource types the 'state' element is not a String primitive.
706 * @return true, false, or empty.
708 public Optional<Boolean> getSmartSceneActive() {
709 if (ResourceType.SMART_SCENE == getType() && (state instanceof JsonPrimitive statePrimitive)) {
710 String state = statePrimitive.getAsString();
711 if (Objects.nonNull(state)) {
712 return Optional.of(SmartSceneState.ACTIVE == SmartSceneState.of(state));
715 return Optional.empty();
719 * If the getSmartSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result
720 * is present and 'true' (i.e. the scene is active) return the smart scene name. Or finally (the optional result is
721 * present and 'false') return 'UnDefType.UNDEF'.
723 * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
725 public State getSmartSceneState() {
726 return getSmartSceneActive().map(a -> a ? new StringType(getName()) : UnDefType.UNDEF).orElse(UnDefType.NULL);
729 public List<ResourceReference> getServiceReferences() {
730 List<ResourceReference> services = this.services;
731 return Objects.nonNull(services) ? services : List.of();
734 public JsonObject getStatus() {
735 JsonElement status = this.status;
736 if (Objects.nonNull(status) && status.isJsonObject()) {
737 return status.getAsJsonObject();
739 return new JsonObject();
742 public State getTamperLastUpdatedState(ZoneId zoneId) {
743 TamperReport report = getTamperReportsLatest();
744 return Objects.nonNull(report) ? new DateTimeType(ZonedDateTime.ofInstant(report.getLastChanged(), zoneId))
749 * The the Hue bridge could return its raw list of tamper reports in any order, so sort the list (latest entry
750 * first) according to the respective 'changed' instant and return the first entry i.e. the latest changed entry.
752 * @return the latest changed tamper report
754 private @Nullable TamperReport getTamperReportsLatest() {
755 List<TamperReport> reports = this.tamperReports;
756 return Objects.nonNull(reports)
757 ? reports.stream().sorted((e1, e2) -> e2.getLastChanged().compareTo(e1.getLastChanged())).findFirst()
762 public State getTamperState() {
763 TamperReport report = getTamperReportsLatest();
764 return Objects.nonNull(report)
765 ? TamperStateType.TAMPERED == report.getTamperState() ? OpenClosedType.OPEN : OpenClosedType.CLOSED
769 public @Nullable Temperature getTemperature() {
773 public State getTemperatureState() {
774 Temperature temperature = this.temperature;
775 if (temperature == null) {
776 return UnDefType.NULL;
778 TemperatureReport temperatureReport = temperature.getTemperatureReport();
779 if (temperatureReport == null) {
780 return temperature.getTemperatureState();
782 return new QuantityType<>(temperatureReport.getTemperature(), SIUnits.CELSIUS);
785 public State getTemperatureLastUpdatedState(ZoneId zoneId) {
786 Temperature temperature = this.temperature;
787 if (temperature == null) {
788 return UnDefType.NULL;
790 TemperatureReport temperatureReport = temperature.getTemperatureReport();
791 if (temperatureReport == null) {
792 return UnDefType.UNDEF;
794 Instant lastChanged = temperatureReport.getLastChanged();
795 if (Instant.EPOCH.equals(lastChanged)) {
796 return UnDefType.UNDEF;
798 return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId));
801 public State getTemperatureValidState() {
802 Temperature temperature = this.temperature;
803 return Objects.nonNull(temperature) ? temperature.getTemperatureValidState() : UnDefType.NULL;
806 public @Nullable TimedEffects getTimedEffects() {
810 public ResourceType getType() {
811 return ResourceType.of(type);
814 public State getZigbeeState() {
815 ZigbeeStatus zigbeeStatus = getZigbeeStatus();
816 return Objects.nonNull(zigbeeStatus) ? new StringType(zigbeeStatus.toString()) : UnDefType.NULL;
819 public @Nullable ZigbeeStatus getZigbeeStatus() {
820 JsonElement status = this.status;
821 if (Objects.nonNull(status) && status.isJsonPrimitive()) {
822 return ZigbeeStatus.of(status.getAsString());
827 public boolean hasFullState() {
828 return ContentType.FULL_STATE == contentType;
831 public boolean hasName() {
832 MetaData metaData = getMetaData();
833 return Objects.nonNull(metaData) && Objects.nonNull(metaData.getName());
836 public Resource setAlerts(Alerts alert) {
841 public Resource setColorTemperature(ColorTemperature colorTemperature) {
842 this.colorTemperature = colorTemperature;
846 public Resource setColorXy(@Nullable ColorXy color) {
851 public Resource setContactReport(ContactReport contactReport) {
852 this.contactReport = contactReport;
856 public Resource setContentType(ContentType contentType) {
857 this.contentType = contentType;
861 public Resource setDimming(@Nullable Dimming dimming) {
862 this.dimming = dimming;
866 public Resource setDynamicsDuration(Duration duration) {
867 dynamics = new Dynamics().setDuration(duration);
871 public Resource setFixedEffects(Effects effect) {
872 this.effects = effect;
876 public Resource setEnabled(Command command) {
877 if (command instanceof OnOffType) {
878 this.enabled = ((OnOffType) command) == OnOffType.ON;
883 public Resource setId(String id) {
888 public Resource setMetadata(MetaData metadata) {
889 this.metadata = metadata;
893 public Resource setMirekSchema(@Nullable MirekSchema schema) {
894 ColorTemperature colorTemperature = this.colorTemperature;
895 if (Objects.nonNull(colorTemperature)) {
896 colorTemperature.setMirekSchema(schema);
902 * Set the on/off JSON element (only).
904 * @param command an OnOffTypee command value.
905 * @return this resource instance.
907 public Resource setOnOff(Command command) {
908 if (command instanceof OnOffType) {
909 OnOffType onOff = (OnOffType) command;
910 OnState on = this.on;
911 on = Objects.nonNull(on) ? on : new OnState();
912 on.setOn(OnOffType.ON.equals(onOff));
918 public Resource setOnState(@Nullable OnState on) {
923 public Resource setRecallAction(SceneRecallAction recallAction) {
924 Recall recall = this.recall;
925 this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
929 public Resource setRecallAction(SmartSceneRecallAction recallAction) {
930 Recall recall = this.recall;
931 this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
935 public Resource setRecallDuration(Duration recallDuration) {
936 Recall recall = this.recall;
937 this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setDuration(recallDuration);
941 public Resource setTamperReports(List<TamperReport> tamperReports) {
942 this.tamperReports = tamperReports;
946 public Resource setTimedEffects(TimedEffects timedEffects) {
947 this.timedEffects = timedEffects;
951 public Resource setTimedEffectsDuration(Duration dynamicsDuration) {
952 TimedEffects timedEffects = this.timedEffects;
953 if (Objects.nonNull(timedEffects)) {
954 timedEffects.setDuration(dynamicsDuration);
959 public Resource setType(ResourceType resourceType) {
960 this.type = resourceType.name().toLowerCase();
965 public String toString() {
967 return String.format("id:%s, type:%s", Objects.nonNull(id) ? id : "?" + " ".repeat(35),
968 getType().name().toLowerCase());