2 * Copyright (c) 2010-2023 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.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.util.List;
21 import java.util.Objects;
22 import java.util.Optional;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType;
27 import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType;
28 import org.openhab.binding.hue.internal.dto.clip2.enums.RecallAction;
29 import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType;
30 import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus;
31 import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.HSBType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.PercentType;
36 import org.openhab.core.library.types.QuantityType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.State;
40 import org.openhab.core.types.UnDefType;
41 import org.openhab.core.util.ColorUtil;
42 import org.openhab.core.util.ColorUtil.Gamut;
44 import com.google.gson.JsonElement;
45 import com.google.gson.JsonObject;
46 import com.google.gson.annotations.SerializedName;
49 * Complete Resource information DTO for CLIP 2.
51 * Note: all fields are @Nullable because some cases do not (must not) use them.
53 * @author Andrew Fiddian-Green - Initial contribution
56 public class Resource {
58 public static final double PERCENT_DELTA = 30f;
59 public static final MathContext PERCENT_MATH_CONTEXT = new MathContext(4, RoundingMode.HALF_UP);
62 * The SSE event mechanism sends resources in a sparse (skeleton) format that only includes state fields whose
63 * values have changed. A sparse resource does not contain the full state of the resource. And the absence of any
64 * field from such a resource does not indicate that the field value is UNDEF, but rather that the value is the same
65 * as what it was previously set to by the last non-sparse resource.
67 private transient boolean hasSparseData;
69 private @Nullable String type;
70 private @Nullable String id;
71 private @Nullable @SerializedName("bridge_id") String bridgeId;
72 private @Nullable @SerializedName("id_v1") String idV1;
73 private @Nullable ResourceReference owner;
74 private @Nullable MetaData metadata;
75 private @Nullable @SerializedName("product_data") ProductData productData;
76 private @Nullable List<ResourceReference> services;
77 private @Nullable OnState on;
78 private @Nullable Dimming dimming;
79 private @Nullable @SerializedName("color_temperature") ColorTemperature colorTemperature;
80 private @Nullable ColorXy color;
81 private @Nullable Alerts alert;
82 private @Nullable Effects effects;
83 private @Nullable @SerializedName("timed_effects") TimedEffects timedEffects;
84 private @Nullable ResourceReference group;
85 private @Nullable List<ActionEntry> actions;
86 private @Nullable Recall recall;
87 private @Nullable Boolean enabled;
88 private @Nullable LightLevel light;
89 private @Nullable Button button;
90 private @Nullable Temperature temperature;
91 private @Nullable Motion motion;
92 private @Nullable @SerializedName("power_state") Power powerState;
93 private @Nullable @SerializedName("relative_rotary") RelativeRotary relativeRotary;
94 private @Nullable List<ResourceReference> children;
95 private @Nullable JsonElement status;
96 private @Nullable @SuppressWarnings("unused") Dynamics dynamics;
101 * @param resourceType
103 public Resource(@Nullable ResourceType resourceType) {
104 if (Objects.nonNull(resourceType)) {
105 setType(resourceType);
109 public @Nullable List<ActionEntry> getActions() {
113 public @Nullable Alerts getAlerts() {
117 public State getAlertState() {
118 Alerts alerts = this.alert;
119 if (Objects.nonNull(alerts)) {
120 if (!alerts.getActionValues().isEmpty()) {
121 ActionType alertType = alerts.getAction();
122 if (Objects.nonNull(alertType)) {
123 return new StringType(alertType.name());
125 return new StringType(ActionType.NO_ACTION.name());
128 return UnDefType.NULL;
131 public String getArchetype() {
132 MetaData metaData = getMetaData();
133 if (Objects.nonNull(metaData)) {
134 return metaData.getArchetype().toString();
136 return getType().toString();
139 public State getBatteryLevelState() {
140 Power powerState = this.powerState;
141 return Objects.nonNull(powerState) ? powerState.getBatteryLevelState() : UnDefType.NULL;
144 public State getBatteryLowState() {
145 Power powerState = this.powerState;
146 return Objects.nonNull(powerState) ? powerState.getBatteryLowState() : UnDefType.NULL;
149 public @Nullable String getBridgeId() {
150 String bridgeId = this.bridgeId;
151 return Objects.isNull(bridgeId) || bridgeId.isBlank() ? null : bridgeId;
155 * Get the brightness as a PercentType. If off the brightness is 0, otherwise use dimming value.
157 * @return a PercentType with the dimming state, or UNDEF, or NULL
159 public State getBrightnessState() {
160 Dimming dimming = this.dimming;
161 if (Objects.nonNull(dimming)) {
163 // if off the brightness is 0, otherwise it is dimming value
164 OnState on = this.on;
165 double brightness = Objects.nonNull(on) && !on.isOn() ? 0f
166 : Math.max(0f, Math.min(100f, dimming.getBrightness()));
167 return new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT));
168 } catch (DTOPresentButEmptyException e) {
169 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
172 return UnDefType.NULL;
175 public @Nullable Button getButton() {
180 * Get the state corresponding to a button's last event value multiplied by the controlId found for it in the given
181 * controlIds map. States are decimal values formatted like '1002' where the first digit is the button's controlId
182 * and the last digit is the ordinal value of the button's last event.
184 * @param controlIds the map of control ids to be referenced.
187 public State getButtonEventState(Map<String, Integer> controlIds) {
188 Button button = this.button;
189 if (Objects.nonNull(button)) {
191 return new DecimalType(
192 (controlIds.getOrDefault(getId(), 0).intValue() * 1000) + button.getLastEvent().ordinal());
193 } catch (IllegalArgumentException e) {
197 return UnDefType.NULL;
200 public List<ResourceReference> getChildren() {
201 List<ResourceReference> children = this.children;
202 return Objects.nonNull(children) ? children : List.of();
206 * Get the color as an HSBType. This returns an HSB that is based on an amalgamation of the color xy, dimming, and
207 * on/off JSON elements. It takes its 'H' and 'S' parts from the 'ColorXy' JSON element, and its 'B' part from the
208 * on/off resp. dimming JSON elements. If off the B part is 0, otherwise it is the dimming element value. Note: this
209 * method is only to be used on cached state DTOs which already have a defined color gamut.
211 * @return an HSBType containing the current color and brightness level, or UNDEF or NULL.
213 public State getColorState() {
214 ColorXy color = this.color;
215 if (Objects.nonNull(color)) {
217 Gamut gamut = color.getGamut();
218 gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT;
219 HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut);
220 OnState on = this.on;
221 Dimming dimming = this.dimming;
222 double brightness = Objects.nonNull(on) && !on.isOn() ? 0
223 : Objects.nonNull(dimming) ? Math.max(0, Math.min(100, dimming.getBrightness())) : 50;
224 return new HSBType(hsb.getHue(), hsb.getSaturation(),
225 new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT)));
226 } catch (DTOPresentButEmptyException e) {
227 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
230 return UnDefType.NULL;
233 public @Nullable ColorTemperature getColorTemperature() {
234 return colorTemperature;
237 public State getColorTemperatureAbsoluteState() {
238 ColorTemperature colorTemp = colorTemperature;
239 if (Objects.nonNull(colorTemp)) {
241 QuantityType<?> colorTemperature = colorTemp.getAbsolute();
242 if (Objects.nonNull(colorTemperature)) {
243 return colorTemperature;
245 } catch (DTOPresentButEmptyException e) {
246 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
249 return UnDefType.NULL;
253 * Get the colour temperature in percent. Note: this method is only to be used on cached state DTOs which already
254 * have a defined mirek schema.
256 * @return a PercentType with the colour temperature percentage.
258 public State getColorTemperaturePercentState() {
259 ColorTemperature colorTemperature = this.colorTemperature;
260 if (Objects.nonNull(colorTemperature)) {
262 Double percent = colorTemperature.getPercent();
263 if (Objects.nonNull(percent)) {
264 return new PercentType(new BigDecimal(percent, PERCENT_MATH_CONTEXT));
266 } catch (DTOPresentButEmptyException e) {
267 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
270 return UnDefType.NULL;
273 public @Nullable ColorXy getColorXy() {
278 * Return an HSB where the HS part is derived from the color xy JSON element (only), so the B part is 100%
280 * @return an HSBType.
282 public State getColorXyState() {
283 ColorXy color = this.color;
284 if (Objects.nonNull(color)) {
286 Gamut gamut = color.getGamut();
287 gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT;
288 HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut);
289 return new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED);
290 } catch (DTOPresentButEmptyException e) {
291 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
294 return UnDefType.NULL;
297 public int getControlId() {
298 MetaData metadata = this.metadata;
299 return Objects.nonNull(metadata) ? metadata.getControlId() : 0;
302 public @Nullable Dimming getDimming() {
307 * Return a PercentType which is derived from the dimming JSON element (only).
309 * @return a PercentType.
311 public State getDimmingState() {
312 Dimming dimming = this.dimming;
313 if (Objects.nonNull(dimming)) {
315 double dimmingValue = Math.max(0f, Math.min(100f, dimming.getBrightness()));
316 return new PercentType(new BigDecimal(dimmingValue, PERCENT_MATH_CONTEXT));
317 } catch (DTOPresentButEmptyException e) {
318 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
321 return UnDefType.NULL;
324 public @Nullable Effects getFixedEffects() {
329 * Get the amalgamated effect state. The result may be either from an 'effects' field or from a 'timedEffects'
330 * field. If both fields are missing it returns UnDefType.NULL, otherwise if either field is present and has an
331 * active value (other than EffectType.NO_EFFECT) it returns a StringType of the name of the respective active
332 * effect; and if none of the above apply, it returns a StringType of 'NO_EFFECT'.
334 * @return either a StringType value or UnDefType.NULL
336 public State getEffectState() {
337 Effects effects = this.effects;
338 TimedEffects timedEffects = this.timedEffects;
339 if (Objects.isNull(effects) && Objects.isNull(timedEffects)) {
340 return UnDefType.NULL;
342 EffectType effect = Objects.nonNull(effects) ? effects.getStatus() : null;
343 if (Objects.nonNull(effect) && effect != EffectType.NO_EFFECT) {
344 return new StringType(effect.name());
346 EffectType timedEffect = Objects.nonNull(timedEffects) ? timedEffects.getStatus() : null;
347 if (Objects.nonNull(timedEffect) && timedEffect != EffectType.NO_EFFECT) {
348 return new StringType(timedEffect.name());
350 return new StringType(EffectType.NO_EFFECT.name());
353 public @Nullable Boolean getEnabled() {
357 public State getEnabledState() {
358 Boolean enabled = this.enabled;
359 return Objects.nonNull(enabled) ? OnOffType.from(enabled.booleanValue()) : UnDefType.NULL;
362 public @Nullable Gamut getGamut() {
363 ColorXy color = this.color;
364 return Objects.nonNull(color) ? color.getGamut() : null;
367 public @Nullable ResourceReference getGroup() {
371 public String getId() {
373 return Objects.nonNull(id) ? id : "";
376 public String getIdV1() {
377 String idV1 = this.idV1;
378 return Objects.nonNull(idV1) ? idV1 : "";
381 public @Nullable LightLevel getLightLevel() {
385 public State getLightLevelState() {
386 LightLevel light = this.light;
387 return Objects.nonNull(light) ? light.getLightLevelState() : UnDefType.NULL;
390 public @Nullable MetaData getMetaData() {
394 public @Nullable Double getMinimumDimmingLevel() {
395 Dimming dimming = this.dimming;
396 return Objects.nonNull(dimming) ? dimming.getMinimumDimmingLevel() : null;
399 public @Nullable MirekSchema getMirekSchema() {
400 ColorTemperature colorTemp = this.colorTemperature;
401 return Objects.nonNull(colorTemp) ? colorTemp.getMirekSchema() : null;
404 public @Nullable Motion getMotion() {
408 public State getMotionState() {
409 Motion motion = this.motion;
410 return Objects.nonNull(motion) ? motion.getMotionState() : UnDefType.NULL;
413 public State getMotionValidState() {
414 Motion motion = this.motion;
415 return Objects.nonNull(motion) ? motion.getMotionValidState() : UnDefType.NULL;
418 public String getName() {
419 MetaData metaData = getMetaData();
420 if (Objects.nonNull(metaData)) {
421 String name = metaData.getName();
422 if (Objects.nonNull(name)) {
426 return getType().toString();
430 * Return the state of the On/Off element (only).
432 public State getOnOffState() {
434 OnState on = this.on;
435 return Objects.nonNull(on) ? OnOffType.from(on.isOn()) : UnDefType.NULL;
436 } catch (DTOPresentButEmptyException e) {
437 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
441 public @Nullable OnState getOnState() {
445 public @Nullable ResourceReference getOwner() {
449 public @Nullable Power getPowerState() {
453 public @Nullable ProductData getProductData() {
457 public String getProductName() {
458 ProductData productData = getProductData();
459 if (Objects.nonNull(productData)) {
460 return productData.getProductName();
462 return getType().toString();
465 public @Nullable Recall getRecall() {
469 public @Nullable RelativeRotary getRelativeRotary() {
470 return relativeRotary;
473 public State getRelativeRotaryActionState() {
474 RelativeRotary relativeRotary = this.relativeRotary;
475 return Objects.nonNull(relativeRotary) ? relativeRotary.getActionState() : UnDefType.NULL;
478 public State getRotaryStepsState() {
479 RelativeRotary relativeRotary = this.relativeRotary;
480 return Objects.nonNull(relativeRotary) ? relativeRotary.getStepsState() : UnDefType.NULL;
484 * Check if the scene resource contains a 'status.active' element. If such an element is present, returns a Boolean
485 * Optional whose value depends on the value of that element, or an empty Optional if it is not.
487 * @return true, false, or empty.
489 public Optional<Boolean> getSceneActive() {
490 if (ResourceType.SCENE == getType()) {
491 JsonElement status = this.status;
492 if (Objects.nonNull(status) && status.isJsonObject()) {
493 JsonElement active = ((JsonObject) status).get("active");
494 if (Objects.nonNull(active) && active.isJsonPrimitive()) {
495 return Optional.of(!"inactive".equalsIgnoreCase(active.getAsString()));
499 return Optional.empty();
503 * If the getSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result is
504 * present and 'true' (i.e. the scene is active) return the scene name. Or finally (the optional result is present
505 * and 'false') return 'UnDefType.UNDEF'.
507 * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
509 public State getSceneState() {
510 Optional<Boolean> active = getSceneActive();
511 return active.isEmpty() ? UnDefType.NULL : active.get() ? new StringType(getName()) : UnDefType.UNDEF;
514 public List<ResourceReference> getServiceReferences() {
515 List<ResourceReference> services = this.services;
516 return Objects.nonNull(services) ? services : List.of();
519 public JsonObject getStatus() {
520 JsonElement status = this.status;
521 if (Objects.nonNull(status) && status.isJsonObject()) {
522 return status.getAsJsonObject();
524 return new JsonObject();
527 public @Nullable Temperature getTemperature() {
531 public State getTemperatureState() {
532 Temperature temperature = this.temperature;
533 return Objects.nonNull(temperature) ? temperature.getTemperatureState() : UnDefType.NULL;
536 public State getTemperatureValidState() {
537 Temperature temperature = this.temperature;
538 return Objects.nonNull(temperature) ? temperature.getTemperatureValidState() : UnDefType.NULL;
541 public @Nullable TimedEffects getTimedEffects() {
545 public ResourceType getType() {
546 return ResourceType.of(type);
549 public State getZigbeeState() {
550 ZigbeeStatus zigbeeStatus = getZigbeeStatus();
551 return Objects.nonNull(zigbeeStatus) ? new StringType(zigbeeStatus.toString()) : UnDefType.NULL;
554 public @Nullable ZigbeeStatus getZigbeeStatus() {
555 JsonElement status = this.status;
556 if (Objects.nonNull(status) && status.isJsonPrimitive()) {
557 return ZigbeeStatus.of(status.getAsString());
562 public boolean hasFullState() {
563 return !hasSparseData;
567 * Mark that the resource has sparse data.
569 * @return this instance.
571 public Resource markAsSparse() {
572 hasSparseData = true;
576 public Resource setAlerts(Alerts alert) {
581 public Resource setColorTemperature(ColorTemperature colorTemperature) {
582 this.colorTemperature = colorTemperature;
586 public Resource setColorXy(ColorXy color) {
591 public Resource setDimming(Dimming dimming) {
592 this.dimming = dimming;
596 public Resource setDynamicsDuration(Duration duration) {
597 dynamics = new Dynamics().setDuration(duration);
601 public Resource setFixedEffects(Effects effect) {
602 this.effects = effect;
606 public Resource setEnabled(Command command) {
607 if (command instanceof OnOffType) {
608 this.enabled = ((OnOffType) command) == OnOffType.ON;
613 public Resource setId(String id) {
618 public Resource setMetadata(MetaData metadata) {
619 this.metadata = metadata;
623 public Resource setMirekSchema(@Nullable MirekSchema schema) {
624 ColorTemperature colorTemperature = this.colorTemperature;
625 if (Objects.nonNull(colorTemperature)) {
626 colorTemperature.setMirekSchema(schema);
632 * Set the on/off JSON element (only).
634 * @param command an OnOffTypee command value.
635 * @return this resource instance.
637 public Resource setOnOff(Command command) {
638 if (command instanceof OnOffType) {
639 OnOffType onOff = (OnOffType) command;
640 OnState on = this.on;
641 on = Objects.nonNull(on) ? on : new OnState();
642 on.setOn(OnOffType.ON.equals(onOff));
648 public void setOnState(OnState on) {
652 public Resource setRecallAction(RecallAction recallAction) {
653 Recall recall = this.recall;
654 this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
658 public Resource setRecallDuration(Duration recallDuration) {
659 Recall recall = this.recall;
660 this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setDuration(recallDuration);
664 public Resource setTimedEffects(TimedEffects timedEffects) {
665 this.timedEffects = timedEffects;
669 public Resource setTimedEffectsDuration(Duration dynamicsDuration) {
670 TimedEffects timedEffects = this.timedEffects;
671 if (Objects.nonNull(timedEffects)) {
672 timedEffects.setDuration(dynamicsDuration);
677 public Resource setType(ResourceType resourceType) {
678 this.type = resourceType.name().toLowerCase();
683 public String toString() {
685 return String.format("id:%s, type:%s", Objects.nonNull(id) ? id : "?" + " ".repeat(35),
686 getType().name().toLowerCase());