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.ResourceType;
29 import org.openhab.binding.hue.internal.dto.clip2.enums.SceneRecallAction;
30 import org.openhab.binding.hue.internal.dto.clip2.enums.SmartSceneRecallAction;
31 import org.openhab.binding.hue.internal.dto.clip2.enums.SmartSceneState;
32 import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus;
33 import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException;
34 import org.openhab.core.library.types.DecimalType;
35 import org.openhab.core.library.types.HSBType;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.PercentType;
38 import org.openhab.core.library.types.QuantityType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.State;
42 import org.openhab.core.types.UnDefType;
43 import org.openhab.core.util.ColorUtil;
44 import org.openhab.core.util.ColorUtil.Gamut;
46 import com.google.gson.JsonElement;
47 import com.google.gson.JsonObject;
48 import com.google.gson.annotations.SerializedName;
51 * Complete Resource information DTO for CLIP 2.
53 * Note: all fields are @Nullable because some cases do not (must not) use them.
55 * @author Andrew Fiddian-Green - Initial contribution
58 public class Resource {
60 public static final double PERCENT_DELTA = 30f;
61 public static final MathContext PERCENT_MATH_CONTEXT = new MathContext(4, RoundingMode.HALF_UP);
64 * The SSE event mechanism sends resources in a sparse (skeleton) format that only includes state fields whose
65 * values have changed. A sparse resource does not contain the full state of the resource. And the absence of any
66 * field from such a resource does not indicate that the field value is UNDEF, but rather that the value is the same
67 * as what it was previously set to by the last non-sparse resource.
69 private transient boolean hasSparseData;
71 private @Nullable String type;
72 private @Nullable String id;
73 private @Nullable @SerializedName("bridge_id") String bridgeId;
74 private @Nullable @SerializedName("id_v1") String idV1;
75 private @Nullable ResourceReference owner;
76 private @Nullable MetaData metadata;
77 private @Nullable @SerializedName("product_data") ProductData productData;
78 private @Nullable List<ResourceReference> services;
79 private @Nullable OnState on;
80 private @Nullable Dimming dimming;
81 private @Nullable @SerializedName("color_temperature") ColorTemperature colorTemperature;
82 private @Nullable ColorXy color;
83 private @Nullable Alerts alert;
84 private @Nullable Effects effects;
85 private @Nullable @SerializedName("timed_effects") TimedEffects timedEffects;
86 private @Nullable ResourceReference group;
87 private @Nullable List<ActionEntry> actions;
88 private @Nullable Recall recall;
89 private @Nullable Boolean enabled;
90 private @Nullable LightLevel light;
91 private @Nullable Button button;
92 private @Nullable Temperature temperature;
93 private @Nullable Motion motion;
94 private @Nullable @SerializedName("power_state") Power powerState;
95 private @Nullable @SerializedName("relative_rotary") RelativeRotary relativeRotary;
96 private @Nullable List<ResourceReference> children;
97 private @Nullable JsonElement status;
98 private @Nullable @SuppressWarnings("unused") Dynamics dynamics;
99 private @Nullable String state;
104 * @param resourceType
106 public Resource(@Nullable ResourceType resourceType) {
107 if (Objects.nonNull(resourceType)) {
108 setType(resourceType);
112 public @Nullable List<ActionEntry> getActions() {
116 public @Nullable Alerts getAlerts() {
120 public State getAlertState() {
121 Alerts alerts = this.alert;
122 if (Objects.nonNull(alerts)) {
123 if (!alerts.getActionValues().isEmpty()) {
124 ActionType alertType = alerts.getAction();
125 if (Objects.nonNull(alertType)) {
126 return new StringType(alertType.name());
128 return new StringType(ActionType.NO_ACTION.name());
131 return UnDefType.NULL;
134 public String getArchetype() {
135 MetaData metaData = getMetaData();
136 if (Objects.nonNull(metaData)) {
137 return metaData.getArchetype().toString();
139 return getType().toString();
142 public State getBatteryLevelState() {
143 Power powerState = this.powerState;
144 return Objects.nonNull(powerState) ? powerState.getBatteryLevelState() : UnDefType.NULL;
147 public State getBatteryLowState() {
148 Power powerState = this.powerState;
149 return Objects.nonNull(powerState) ? powerState.getBatteryLowState() : UnDefType.NULL;
152 public @Nullable String getBridgeId() {
153 String bridgeId = this.bridgeId;
154 return Objects.isNull(bridgeId) || bridgeId.isBlank() ? null : bridgeId;
158 * Get the brightness as a PercentType. If off the brightness is 0, otherwise use dimming value.
160 * @return a PercentType with the dimming state, or UNDEF, or NULL
162 public State getBrightnessState() {
163 Dimming dimming = this.dimming;
164 if (Objects.nonNull(dimming)) {
166 // if off the brightness is 0, otherwise it is dimming value
167 OnState on = this.on;
168 double brightness = Objects.nonNull(on) && !on.isOn() ? 0f
169 : Math.max(0f, Math.min(100f, dimming.getBrightness()));
170 return new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT));
171 } catch (DTOPresentButEmptyException e) {
172 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
175 return UnDefType.NULL;
178 public @Nullable Button getButton() {
183 * Get the state corresponding to a button's last event value multiplied by the controlId found for it in the given
184 * controlIds map. States are decimal values formatted like '1002' where the first digit is the button's controlId
185 * and the last digit is the ordinal value of the button's last event.
187 * @param controlIds the map of control ids to be referenced.
190 public State getButtonEventState(Map<String, Integer> controlIds) {
191 Button button = this.button;
192 if (Objects.nonNull(button)) {
194 return new DecimalType(
195 (controlIds.getOrDefault(getId(), 0).intValue() * 1000) + button.getLastEvent().ordinal());
196 } catch (IllegalArgumentException e) {
200 return UnDefType.NULL;
203 public List<ResourceReference> getChildren() {
204 List<ResourceReference> children = this.children;
205 return Objects.nonNull(children) ? children : List.of();
209 * Get the color as an HSBType. This returns an HSB that is based on an amalgamation of the color xy, dimming, and
210 * on/off JSON elements. It takes its 'H' and 'S' parts from the 'ColorXy' JSON element, and its 'B' part from the
211 * on/off resp. dimming JSON elements. If off the B part is 0, otherwise it is the dimming element value. Note: this
212 * method is only to be used on cached state DTOs which already have a defined color gamut.
214 * @return an HSBType containing the current color and brightness level, or UNDEF or NULL.
216 public State getColorState() {
217 ColorXy color = this.color;
218 if (Objects.nonNull(color)) {
220 Gamut gamut = color.getGamut();
221 gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT;
222 HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut);
223 OnState on = this.on;
224 Dimming dimming = this.dimming;
225 double brightness = Objects.nonNull(on) && !on.isOn() ? 0
226 : Objects.nonNull(dimming) ? Math.max(0, Math.min(100, dimming.getBrightness())) : 50;
227 return new HSBType(hsb.getHue(), hsb.getSaturation(),
228 new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT)));
229 } catch (DTOPresentButEmptyException e) {
230 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
233 return UnDefType.NULL;
236 public @Nullable ColorTemperature getColorTemperature() {
237 return colorTemperature;
240 public State getColorTemperatureAbsoluteState() {
241 ColorTemperature colorTemp = colorTemperature;
242 if (Objects.nonNull(colorTemp)) {
244 QuantityType<?> colorTemperature = colorTemp.getAbsolute();
245 if (Objects.nonNull(colorTemperature)) {
246 return colorTemperature;
248 } catch (DTOPresentButEmptyException e) {
249 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
252 return UnDefType.NULL;
256 * Get the colour temperature in percent. Note: this method is only to be used on cached state DTOs which already
257 * have a defined mirek schema.
259 * @return a PercentType with the colour temperature percentage.
261 public State getColorTemperaturePercentState() {
262 ColorTemperature colorTemperature = this.colorTemperature;
263 if (Objects.nonNull(colorTemperature)) {
265 Double percent = colorTemperature.getPercent();
266 if (Objects.nonNull(percent)) {
267 return new PercentType(new BigDecimal(percent, PERCENT_MATH_CONTEXT));
269 } catch (DTOPresentButEmptyException e) {
270 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
273 return UnDefType.NULL;
276 public @Nullable ColorXy getColorXy() {
281 * Return an HSB where the HS part is derived from the color xy JSON element (only), so the B part is 100%
283 * @return an HSBType.
285 public State getColorXyState() {
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 return new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED);
293 } catch (DTOPresentButEmptyException e) {
294 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
297 return UnDefType.NULL;
300 public int getControlId() {
301 MetaData metadata = this.metadata;
302 return Objects.nonNull(metadata) ? metadata.getControlId() : 0;
305 public @Nullable Dimming getDimming() {
310 * Return a PercentType which is derived from the dimming JSON element (only).
312 * @return a PercentType.
314 public State getDimmingState() {
315 Dimming dimming = this.dimming;
316 if (Objects.nonNull(dimming)) {
318 double dimmingValue = Math.max(0f, Math.min(100f, dimming.getBrightness()));
319 return new PercentType(new BigDecimal(dimmingValue, PERCENT_MATH_CONTEXT));
320 } catch (DTOPresentButEmptyException e) {
321 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
324 return UnDefType.NULL;
327 public @Nullable Effects getFixedEffects() {
332 * Get the amalgamated effect state. The result may be either from an 'effects' field or from a 'timedEffects'
333 * field. If both fields are missing it returns UnDefType.NULL, otherwise if either field is present and has an
334 * active value (other than EffectType.NO_EFFECT) it returns a StringType of the name of the respective active
335 * effect; and if none of the above apply, it returns a StringType of 'NO_EFFECT'.
337 * @return either a StringType value or UnDefType.NULL
339 public State getEffectState() {
340 Effects effects = this.effects;
341 TimedEffects timedEffects = this.timedEffects;
342 if (Objects.isNull(effects) && Objects.isNull(timedEffects)) {
343 return UnDefType.NULL;
345 EffectType effect = Objects.nonNull(effects) ? effects.getStatus() : null;
346 if (Objects.nonNull(effect) && effect != EffectType.NO_EFFECT) {
347 return new StringType(effect.name());
349 EffectType timedEffect = Objects.nonNull(timedEffects) ? timedEffects.getStatus() : null;
350 if (Objects.nonNull(timedEffect) && timedEffect != EffectType.NO_EFFECT) {
351 return new StringType(timedEffect.name());
353 return new StringType(EffectType.NO_EFFECT.name());
356 public @Nullable Boolean getEnabled() {
360 public State getEnabledState() {
361 Boolean enabled = this.enabled;
362 return Objects.nonNull(enabled) ? OnOffType.from(enabled.booleanValue()) : UnDefType.NULL;
365 public @Nullable Gamut getGamut() {
366 ColorXy color = this.color;
367 return Objects.nonNull(color) ? color.getGamut() : null;
370 public @Nullable ResourceReference getGroup() {
374 public String getId() {
376 return Objects.nonNull(id) ? id : "";
379 public String getIdV1() {
380 String idV1 = this.idV1;
381 return Objects.nonNull(idV1) ? idV1 : "";
384 public @Nullable LightLevel getLightLevel() {
388 public State getLightLevelState() {
389 LightLevel light = this.light;
390 return Objects.nonNull(light) ? light.getLightLevelState() : UnDefType.NULL;
393 public @Nullable MetaData getMetaData() {
397 public @Nullable Double getMinimumDimmingLevel() {
398 Dimming dimming = this.dimming;
399 return Objects.nonNull(dimming) ? dimming.getMinimumDimmingLevel() : null;
402 public @Nullable MirekSchema getMirekSchema() {
403 ColorTemperature colorTemp = this.colorTemperature;
404 return Objects.nonNull(colorTemp) ? colorTemp.getMirekSchema() : null;
407 public @Nullable Motion getMotion() {
411 public State getMotionState() {
412 Motion motion = this.motion;
413 return Objects.nonNull(motion) ? motion.getMotionState() : UnDefType.NULL;
416 public State getMotionValidState() {
417 Motion motion = this.motion;
418 return Objects.nonNull(motion) ? motion.getMotionValidState() : UnDefType.NULL;
421 public String getName() {
422 MetaData metaData = getMetaData();
423 if (Objects.nonNull(metaData)) {
424 String name = metaData.getName();
425 if (Objects.nonNull(name)) {
429 return getType().toString();
433 * Return the state of the On/Off element (only).
435 public State getOnOffState() {
437 OnState on = this.on;
438 return Objects.nonNull(on) ? OnOffType.from(on.isOn()) : UnDefType.NULL;
439 } catch (DTOPresentButEmptyException e) {
440 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
444 public @Nullable OnState getOnState() {
448 public @Nullable ResourceReference getOwner() {
452 public @Nullable Power getPowerState() {
456 public @Nullable ProductData getProductData() {
460 public String getProductName() {
461 ProductData productData = getProductData();
462 if (Objects.nonNull(productData)) {
463 return productData.getProductName();
465 return getType().toString();
468 public @Nullable Recall getRecall() {
472 public @Nullable RelativeRotary getRelativeRotary() {
473 return relativeRotary;
476 public State getRelativeRotaryActionState() {
477 RelativeRotary relativeRotary = this.relativeRotary;
478 return Objects.nonNull(relativeRotary) ? relativeRotary.getActionState() : UnDefType.NULL;
481 public State getRotaryStepsState() {
482 RelativeRotary relativeRotary = this.relativeRotary;
483 return Objects.nonNull(relativeRotary) ? relativeRotary.getStepsState() : UnDefType.NULL;
487 * Check if the scene resource contains a 'status.active' element. If such an element is present, returns a Boolean
488 * Optional whose value depends on the value of that element, or an empty Optional if it is not.
490 * @return true, false, or empty.
492 public Optional<Boolean> getSceneActive() {
493 if (ResourceType.SCENE == getType()) {
494 JsonElement status = this.status;
495 if (Objects.nonNull(status) && status.isJsonObject()) {
496 JsonElement active = ((JsonObject) status).get("active");
497 if (Objects.nonNull(active) && active.isJsonPrimitive()) {
498 return Optional.of(!"inactive".equalsIgnoreCase(active.getAsString()));
502 return Optional.empty();
506 * If the getSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result is
507 * present and 'true' (i.e. the scene is active) return the scene name. Or finally (the optional result is present
508 * and 'false') return 'UnDefType.UNDEF'.
510 * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
512 public State getSceneState() {
513 return getSceneActive().map(a -> a ? new StringType(getName()) : UnDefType.UNDEF).orElse(UnDefType.NULL);
517 * Check if the smart scene resource contains a 'state' element. If such an element is present, returns a Boolean
518 * Optional whose value depends on the value of that element, or an empty Optional if it is not.
520 * @return true, false, or empty.
522 public Optional<Boolean> getSmartSceneActive() {
523 if (ResourceType.SMART_SCENE == getType()) {
524 String state = this.state;
525 if (Objects.nonNull(state)) {
526 return Optional.of(SmartSceneState.ACTIVE == SmartSceneState.of(state));
529 return Optional.empty();
533 * If the getSmartSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result
534 * is present and 'true' (i.e. the scene is active) return the smart scene name. Or finally (the optional result is
535 * present and 'false') return 'UnDefType.UNDEF'.
537 * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
539 public State getSmartSceneState() {
540 return getSmartSceneActive().map(a -> a ? new StringType(getName()) : UnDefType.UNDEF).orElse(UnDefType.NULL);
543 public List<ResourceReference> getServiceReferences() {
544 List<ResourceReference> services = this.services;
545 return Objects.nonNull(services) ? services : List.of();
548 public JsonObject getStatus() {
549 JsonElement status = this.status;
550 if (Objects.nonNull(status) && status.isJsonObject()) {
551 return status.getAsJsonObject();
553 return new JsonObject();
556 public @Nullable Temperature getTemperature() {
560 public State getTemperatureState() {
561 Temperature temperature = this.temperature;
562 return Objects.nonNull(temperature) ? temperature.getTemperatureState() : UnDefType.NULL;
565 public State getTemperatureValidState() {
566 Temperature temperature = this.temperature;
567 return Objects.nonNull(temperature) ? temperature.getTemperatureValidState() : UnDefType.NULL;
570 public @Nullable TimedEffects getTimedEffects() {
574 public ResourceType getType() {
575 return ResourceType.of(type);
578 public State getZigbeeState() {
579 ZigbeeStatus zigbeeStatus = getZigbeeStatus();
580 return Objects.nonNull(zigbeeStatus) ? new StringType(zigbeeStatus.toString()) : UnDefType.NULL;
583 public @Nullable ZigbeeStatus getZigbeeStatus() {
584 JsonElement status = this.status;
585 if (Objects.nonNull(status) && status.isJsonPrimitive()) {
586 return ZigbeeStatus.of(status.getAsString());
591 public boolean hasFullState() {
592 return !hasSparseData;
596 * Mark that the resource has sparse data.
598 * @return this instance.
600 public Resource markAsSparse() {
601 hasSparseData = true;
605 public Resource setAlerts(Alerts alert) {
610 public Resource setColorTemperature(ColorTemperature colorTemperature) {
611 this.colorTemperature = colorTemperature;
615 public Resource setColorXy(ColorXy color) {
620 public Resource setDimming(Dimming dimming) {
621 this.dimming = dimming;
625 public Resource setDynamicsDuration(Duration duration) {
626 dynamics = new Dynamics().setDuration(duration);
630 public Resource setFixedEffects(Effects effect) {
631 this.effects = effect;
635 public Resource setEnabled(Command command) {
636 if (command instanceof OnOffType) {
637 this.enabled = ((OnOffType) command) == OnOffType.ON;
642 public Resource setId(String id) {
647 public Resource setMetadata(MetaData metadata) {
648 this.metadata = metadata;
652 public Resource setMirekSchema(@Nullable MirekSchema schema) {
653 ColorTemperature colorTemperature = this.colorTemperature;
654 if (Objects.nonNull(colorTemperature)) {
655 colorTemperature.setMirekSchema(schema);
661 * Set the on/off JSON element (only).
663 * @param command an OnOffTypee command value.
664 * @return this resource instance.
666 public Resource setOnOff(Command command) {
667 if (command instanceof OnOffType) {
668 OnOffType onOff = (OnOffType) command;
669 OnState on = this.on;
670 on = Objects.nonNull(on) ? on : new OnState();
671 on.setOn(OnOffType.ON.equals(onOff));
677 public void setOnState(OnState on) {
681 public Resource setRecallAction(SceneRecallAction recallAction) {
682 Recall recall = this.recall;
683 this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
687 public Resource setRecallAction(SmartSceneRecallAction recallAction) {
688 Recall recall = this.recall;
689 this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
693 public Resource setRecallDuration(Duration recallDuration) {
694 Recall recall = this.recall;
695 this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setDuration(recallDuration);
699 public Resource setTimedEffects(TimedEffects timedEffects) {
700 this.timedEffects = timedEffects;
704 public Resource setTimedEffectsDuration(Duration dynamicsDuration) {
705 TimedEffects timedEffects = this.timedEffects;
706 if (Objects.nonNull(timedEffects)) {
707 timedEffects.setDuration(dynamicsDuration);
712 public Resource setType(ResourceType resourceType) {
713 this.type = resourceType.name().toLowerCase();
718 public String toString() {
720 return String.format("id:%s, type:%s", Objects.nonNull(id) ? id : "?" + " ".repeat(35),
721 getType().name().toLowerCase());