]> git.basschouten.com Git - openhab-addons.git/blob
02330cd20d9be52cc0b14327297f4cc578bd547d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.hue.internal.dto.clip2;
14
15 import java.math.BigDecimal;
16 import java.math.MathContext;
17 import java.math.RoundingMode;
18 import java.time.Duration;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Objects;
22 import java.util.Optional;
23
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;
43
44 import com.google.gson.JsonElement;
45 import com.google.gson.JsonObject;
46 import com.google.gson.annotations.SerializedName;
47
48 /**
49  * Complete Resource information DTO for CLIP 2.
50  *
51  * Note: all fields are @Nullable because some cases do not (must not) use them.
52  *
53  * @author Andrew Fiddian-Green - Initial contribution
54  */
55 @NonNullByDefault
56 public class Resource {
57
58     public static final double PERCENT_DELTA = 30f;
59     public static final MathContext PERCENT_MATH_CONTEXT = new MathContext(4, RoundingMode.HALF_UP);
60
61     /**
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.
66      */
67     private transient boolean hasSparseData;
68
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;
97
98     /**
99      * Constructor
100      *
101      * @param resourceType
102      */
103     public Resource(@Nullable ResourceType resourceType) {
104         if (Objects.nonNull(resourceType)) {
105             setType(resourceType);
106         }
107     }
108
109     public @Nullable List<ActionEntry> getActions() {
110         return actions;
111     }
112
113     public @Nullable Alerts getAlerts() {
114         return alert;
115     }
116
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());
124                 }
125                 return new StringType(ActionType.NO_ACTION.name());
126             }
127         }
128         return UnDefType.NULL;
129     }
130
131     public String getArchetype() {
132         MetaData metaData = getMetaData();
133         if (Objects.nonNull(metaData)) {
134             return metaData.getArchetype().toString();
135         }
136         return getType().toString();
137     }
138
139     public State getBatteryLevelState() {
140         Power powerState = this.powerState;
141         return Objects.nonNull(powerState) ? powerState.getBatteryLevelState() : UnDefType.NULL;
142     }
143
144     public State getBatteryLowState() {
145         Power powerState = this.powerState;
146         return Objects.nonNull(powerState) ? powerState.getBatteryLowState() : UnDefType.NULL;
147     }
148
149     public @Nullable String getBridgeId() {
150         String bridgeId = this.bridgeId;
151         return Objects.isNull(bridgeId) || bridgeId.isBlank() ? null : bridgeId;
152     }
153
154     /**
155      * Get the brightness as a PercentType. If off the brightness is 0, otherwise use dimming value.
156      *
157      * @return a PercentType with the dimming state, or UNDEF, or NULL
158      */
159     public State getBrightnessState() {
160         Dimming dimming = this.dimming;
161         if (Objects.nonNull(dimming)) {
162             try {
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
170             }
171         }
172         return UnDefType.NULL;
173     }
174
175     public @Nullable Button getButton() {
176         return button;
177     }
178
179     /**
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.
183      *
184      * @param controlIds the map of control ids to be referenced.
185      * @return the state.
186      */
187     public State getButtonEventState(Map<String, Integer> controlIds) {
188         Button button = this.button;
189         if (Objects.nonNull(button)) {
190             try {
191                 return new DecimalType(
192                         (controlIds.getOrDefault(getId(), 0).intValue() * 1000) + button.getLastEvent().ordinal());
193             } catch (IllegalArgumentException e) {
194                 // fall through
195             }
196         }
197         return UnDefType.NULL;
198     }
199
200     public List<ResourceReference> getChildren() {
201         List<ResourceReference> children = this.children;
202         return Objects.nonNull(children) ? children : List.of();
203     }
204
205     /**
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.
210      *
211      * @return an HSBType containing the current color and brightness level, or UNDEF or NULL.
212      */
213     public State getColorState() {
214         ColorXy color = this.color;
215         if (Objects.nonNull(color)) {
216             try {
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
228             }
229         }
230         return UnDefType.NULL;
231     }
232
233     public @Nullable ColorTemperature getColorTemperature() {
234         return colorTemperature;
235     }
236
237     public State getColorTemperatureAbsoluteState() {
238         ColorTemperature colorTemp = colorTemperature;
239         if (Objects.nonNull(colorTemp)) {
240             try {
241                 QuantityType<?> colorTemperature = colorTemp.getAbsolute();
242                 if (Objects.nonNull(colorTemperature)) {
243                     return colorTemperature;
244                 }
245             } catch (DTOPresentButEmptyException e) {
246                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
247             }
248         }
249         return UnDefType.NULL;
250     }
251
252     /**
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.
255      *
256      * @return a PercentType with the colour temperature percentage.
257      */
258     public State getColorTemperaturePercentState() {
259         ColorTemperature colorTemperature = this.colorTemperature;
260         if (Objects.nonNull(colorTemperature)) {
261             try {
262                 Double percent = colorTemperature.getPercent();
263                 if (Objects.nonNull(percent)) {
264                     return new PercentType(new BigDecimal(percent, PERCENT_MATH_CONTEXT));
265                 }
266             } catch (DTOPresentButEmptyException e) {
267                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
268             }
269         }
270         return UnDefType.NULL;
271     }
272
273     public @Nullable ColorXy getColorXy() {
274         return color;
275     }
276
277     /**
278      * Return an HSB where the HS part is derived from the color xy JSON element (only), so the B part is 100%
279      *
280      * @return an HSBType.
281      */
282     public State getColorXyState() {
283         ColorXy color = this.color;
284         if (Objects.nonNull(color)) {
285             try {
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
292             }
293         }
294         return UnDefType.NULL;
295     }
296
297     public int getControlId() {
298         MetaData metadata = this.metadata;
299         return Objects.nonNull(metadata) ? metadata.getControlId() : 0;
300     }
301
302     public @Nullable Dimming getDimming() {
303         return dimming;
304     }
305
306     /**
307      * Return a PercentType which is derived from the dimming JSON element (only).
308      *
309      * @return a PercentType.
310      */
311     public State getDimmingState() {
312         Dimming dimming = this.dimming;
313         if (Objects.nonNull(dimming)) {
314             try {
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
319             }
320         }
321         return UnDefType.NULL;
322     }
323
324     public @Nullable Effects getFixedEffects() {
325         return effects;
326     }
327
328     /**
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'.
333      *
334      * @return either a StringType value or UnDefType.NULL
335      */
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;
341         }
342         EffectType effect = Objects.nonNull(effects) ? effects.getStatus() : null;
343         if (Objects.nonNull(effect) && effect != EffectType.NO_EFFECT) {
344             return new StringType(effect.name());
345         }
346         EffectType timedEffect = Objects.nonNull(timedEffects) ? timedEffects.getStatus() : null;
347         if (Objects.nonNull(timedEffect) && timedEffect != EffectType.NO_EFFECT) {
348             return new StringType(timedEffect.name());
349         }
350         return new StringType(EffectType.NO_EFFECT.name());
351     }
352
353     public @Nullable Boolean getEnabled() {
354         return enabled;
355     }
356
357     public State getEnabledState() {
358         Boolean enabled = this.enabled;
359         return Objects.nonNull(enabled) ? OnOffType.from(enabled.booleanValue()) : UnDefType.NULL;
360     }
361
362     public @Nullable Gamut getGamut() {
363         ColorXy color = this.color;
364         return Objects.nonNull(color) ? color.getGamut() : null;
365     }
366
367     public @Nullable ResourceReference getGroup() {
368         return group;
369     }
370
371     public String getId() {
372         String id = this.id;
373         return Objects.nonNull(id) ? id : "";
374     }
375
376     public String getIdV1() {
377         String idV1 = this.idV1;
378         return Objects.nonNull(idV1) ? idV1 : "";
379     }
380
381     public @Nullable LightLevel getLightLevel() {
382         return light;
383     }
384
385     public State getLightLevelState() {
386         LightLevel light = this.light;
387         return Objects.nonNull(light) ? light.getLightLevelState() : UnDefType.NULL;
388     }
389
390     public @Nullable MetaData getMetaData() {
391         return metadata;
392     }
393
394     public @Nullable Double getMinimumDimmingLevel() {
395         Dimming dimming = this.dimming;
396         return Objects.nonNull(dimming) ? dimming.getMinimumDimmingLevel() : null;
397     }
398
399     public @Nullable MirekSchema getMirekSchema() {
400         ColorTemperature colorTemp = this.colorTemperature;
401         return Objects.nonNull(colorTemp) ? colorTemp.getMirekSchema() : null;
402     }
403
404     public @Nullable Motion getMotion() {
405         return motion;
406     }
407
408     public State getMotionState() {
409         Motion motion = this.motion;
410         return Objects.nonNull(motion) ? motion.getMotionState() : UnDefType.NULL;
411     }
412
413     public State getMotionValidState() {
414         Motion motion = this.motion;
415         return Objects.nonNull(motion) ? motion.getMotionValidState() : UnDefType.NULL;
416     }
417
418     public String getName() {
419         MetaData metaData = getMetaData();
420         if (Objects.nonNull(metaData)) {
421             String name = metaData.getName();
422             if (Objects.nonNull(name)) {
423                 return name;
424             }
425         }
426         return getType().toString();
427     }
428
429     /**
430      * Return the state of the On/Off element (only).
431      */
432     public State getOnOffState() {
433         try {
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
438         }
439     }
440
441     public @Nullable OnState getOnState() {
442         return on;
443     }
444
445     public @Nullable ResourceReference getOwner() {
446         return owner;
447     }
448
449     public @Nullable Power getPowerState() {
450         return powerState;
451     }
452
453     public @Nullable ProductData getProductData() {
454         return productData;
455     }
456
457     public String getProductName() {
458         ProductData productData = getProductData();
459         if (Objects.nonNull(productData)) {
460             return productData.getProductName();
461         }
462         return getType().toString();
463     }
464
465     public @Nullable Recall getRecall() {
466         return recall;
467     }
468
469     public @Nullable RelativeRotary getRelativeRotary() {
470         return relativeRotary;
471     }
472
473     public State getRelativeRotaryActionState() {
474         RelativeRotary relativeRotary = this.relativeRotary;
475         return Objects.nonNull(relativeRotary) ? relativeRotary.getActionState() : UnDefType.NULL;
476     }
477
478     public State getRotaryStepsState() {
479         RelativeRotary relativeRotary = this.relativeRotary;
480         return Objects.nonNull(relativeRotary) ? relativeRotary.getStepsState() : UnDefType.NULL;
481     }
482
483     /**
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.
486      *
487      * @return true, false, or empty.
488      */
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()));
496                 }
497             }
498         }
499         return Optional.empty();
500     }
501
502     /**
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'.
506      *
507      * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
508      */
509     public State getSceneState() {
510         Optional<Boolean> active = getSceneActive();
511         return active.isEmpty() ? UnDefType.NULL : active.get() ? new StringType(getName()) : UnDefType.UNDEF;
512     }
513
514     public List<ResourceReference> getServiceReferences() {
515         List<ResourceReference> services = this.services;
516         return Objects.nonNull(services) ? services : List.of();
517     }
518
519     public JsonObject getStatus() {
520         JsonElement status = this.status;
521         if (Objects.nonNull(status) && status.isJsonObject()) {
522             return status.getAsJsonObject();
523         }
524         return new JsonObject();
525     }
526
527     public @Nullable Temperature getTemperature() {
528         return temperature;
529     }
530
531     public State getTemperatureState() {
532         Temperature temperature = this.temperature;
533         return Objects.nonNull(temperature) ? temperature.getTemperatureState() : UnDefType.NULL;
534     }
535
536     public State getTemperatureValidState() {
537         Temperature temperature = this.temperature;
538         return Objects.nonNull(temperature) ? temperature.getTemperatureValidState() : UnDefType.NULL;
539     }
540
541     public @Nullable TimedEffects getTimedEffects() {
542         return timedEffects;
543     }
544
545     public ResourceType getType() {
546         return ResourceType.of(type);
547     }
548
549     public State getZigbeeState() {
550         ZigbeeStatus zigbeeStatus = getZigbeeStatus();
551         return Objects.nonNull(zigbeeStatus) ? new StringType(zigbeeStatus.toString()) : UnDefType.NULL;
552     }
553
554     public @Nullable ZigbeeStatus getZigbeeStatus() {
555         JsonElement status = this.status;
556         if (Objects.nonNull(status) && status.isJsonPrimitive()) {
557             return ZigbeeStatus.of(status.getAsString());
558         }
559         return null;
560     }
561
562     public boolean hasFullState() {
563         return !hasSparseData;
564     }
565
566     /**
567      * Mark that the resource has sparse data.
568      *
569      * @return this instance.
570      */
571     public Resource markAsSparse() {
572         hasSparseData = true;
573         return this;
574     }
575
576     public Resource setAlerts(Alerts alert) {
577         this.alert = alert;
578         return this;
579     }
580
581     public Resource setColorTemperature(ColorTemperature colorTemperature) {
582         this.colorTemperature = colorTemperature;
583         return this;
584     }
585
586     public Resource setColorXy(ColorXy color) {
587         this.color = color;
588         return this;
589     }
590
591     public Resource setDimming(Dimming dimming) {
592         this.dimming = dimming;
593         return this;
594     }
595
596     public Resource setDynamicsDuration(Duration duration) {
597         dynamics = new Dynamics().setDuration(duration);
598         return this;
599     }
600
601     public Resource setFixedEffects(Effects effect) {
602         this.effects = effect;
603         return this;
604     }
605
606     public Resource setEnabled(Command command) {
607         if (command instanceof OnOffType) {
608             this.enabled = ((OnOffType) command) == OnOffType.ON;
609         }
610         return this;
611     }
612
613     public Resource setId(String id) {
614         this.id = id;
615         return this;
616     }
617
618     public Resource setMetadata(MetaData metadata) {
619         this.metadata = metadata;
620         return this;
621     }
622
623     public Resource setMirekSchema(@Nullable MirekSchema schema) {
624         ColorTemperature colorTemperature = this.colorTemperature;
625         if (Objects.nonNull(colorTemperature)) {
626             colorTemperature.setMirekSchema(schema);
627         }
628         return this;
629     }
630
631     /**
632      * Set the on/off JSON element (only).
633      *
634      * @param command an OnOffTypee command value.
635      * @return this resource instance.
636      */
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));
643             this.on = on;
644         }
645         return this;
646     }
647
648     public void setOnState(OnState on) {
649         this.on = on;
650     }
651
652     public Resource setRecallAction(RecallAction recallAction) {
653         Recall recall = this.recall;
654         this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
655         return this;
656     }
657
658     public Resource setRecallDuration(Duration recallDuration) {
659         Recall recall = this.recall;
660         this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setDuration(recallDuration);
661         return this;
662     }
663
664     public Resource setTimedEffects(TimedEffects timedEffects) {
665         this.timedEffects = timedEffects;
666         return this;
667     }
668
669     public Resource setTimedEffectsDuration(Duration dynamicsDuration) {
670         TimedEffects timedEffects = this.timedEffects;
671         if (Objects.nonNull(timedEffects)) {
672             timedEffects.setDuration(dynamicsDuration);
673         }
674         return this;
675     }
676
677     public Resource setType(ResourceType resourceType) {
678         this.type = resourceType.name().toLowerCase();
679         return this;
680     }
681
682     @Override
683     public String toString() {
684         String id = this.id;
685         return String.format("id:%s, type:%s", Objects.nonNull(id) ? id : "?" + " ".repeat(35),
686                 getType().name().toLowerCase());
687     }
688 }