]> git.basschouten.com Git - openhab-addons.git/blob
20dd1e7beb8074533e2830f4a185022d581b3543
[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.RecallAction;
28 import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType;
29 import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus;
30 import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.HSBType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.PercentType;
35 import org.openhab.core.library.types.QuantityType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.types.Command;
38 import org.openhab.core.types.State;
39 import org.openhab.core.types.UnDefType;
40 import org.openhab.core.util.ColorUtil;
41 import org.openhab.core.util.ColorUtil.Gamut;
42
43 import com.google.gson.JsonElement;
44 import com.google.gson.JsonObject;
45 import com.google.gson.annotations.SerializedName;
46
47 /**
48  * Complete Resource information DTO for CLIP 2.
49  *
50  * Note: all fields are @Nullable because some cases do not (must not) use them.
51  *
52  * @author Andrew Fiddian-Green - Initial contribution
53  */
54 @NonNullByDefault
55 public class Resource {
56
57     public static final double PERCENT_DELTA = 30f;
58     public static final MathContext PERCENT_MATH_CONTEXT = new MathContext(4, RoundingMode.HALF_UP);
59
60     /**
61      * The SSE event mechanism sends resources in a sparse (skeleton) format that only includes state fields whose
62      * values have changed. A sparse resource does not contain the full state of the resource. And the absence of any
63      * field from such a resource does not indicate that the field value is UNDEF, but rather that the value is the same
64      * as what it was previously set to by the last non-sparse resource.
65      */
66     private transient boolean hasSparseData;
67
68     private @Nullable String type;
69     private @Nullable String id;
70     private @Nullable @SerializedName("bridge_id") String bridgeId;
71     private @Nullable @SerializedName("id_v1") String idV1;
72     private @Nullable ResourceReference owner;
73     private @Nullable MetaData metadata;
74     private @Nullable @SerializedName("product_data") ProductData productData;
75     private @Nullable List<ResourceReference> services;
76     private @Nullable OnState on;
77     private @Nullable Dimming dimming;
78     private @Nullable @SerializedName("color_temperature") ColorTemperature colorTemperature;
79     private @Nullable ColorXy color;
80     private @Nullable Alerts alert;
81     private @Nullable Effects effects;
82     private @Nullable @SerializedName("timed_effects") TimedEffects timedEffects;
83     private @Nullable ResourceReference group;
84     private @Nullable List<ActionEntry> actions;
85     private @Nullable Recall recall;
86     private @Nullable Boolean enabled;
87     private @Nullable LightLevel light;
88     private @Nullable Button button;
89     private @Nullable Temperature temperature;
90     private @Nullable Motion motion;
91     private @Nullable @SerializedName("power_state") Power powerState;
92     private @Nullable @SerializedName("relative_rotary") RelativeRotary relativeRotary;
93     private @Nullable List<ResourceReference> children;
94     private @Nullable JsonElement status;
95     private @Nullable @SuppressWarnings("unused") Dynamics dynamics;
96
97     /**
98      * Constructor
99      *
100      * @param resourceType
101      */
102     public Resource(@Nullable ResourceType resourceType) {
103         if (Objects.nonNull(resourceType)) {
104             setType(resourceType);
105         }
106     }
107
108     public @Nullable List<ActionEntry> getActions() {
109         return actions;
110     }
111
112     public @Nullable Alerts getAlerts() {
113         return alert;
114     }
115
116     public State getAlertState() {
117         Alerts alerts = this.alert;
118         if (Objects.nonNull(alerts)) {
119             if (!alerts.getActionValues().isEmpty()) {
120                 ActionType alertType = alerts.getAction();
121                 if (Objects.nonNull(alertType)) {
122                     return new StringType(alertType.name());
123                 }
124                 return new StringType(ActionType.NO_ACTION.name());
125             }
126         }
127         return UnDefType.NULL;
128     }
129
130     public String getArchetype() {
131         MetaData metaData = getMetaData();
132         if (Objects.nonNull(metaData)) {
133             return metaData.getArchetype().toString();
134         }
135         return getType().toString();
136     }
137
138     public State getBatteryLevelState() {
139         Power powerState = this.powerState;
140         return Objects.nonNull(powerState) ? powerState.getBatteryLevelState() : UnDefType.NULL;
141     }
142
143     public State getBatteryLowState() {
144         Power powerState = this.powerState;
145         return Objects.nonNull(powerState) ? powerState.getBatteryLowState() : UnDefType.NULL;
146     }
147
148     public @Nullable String getBridgeId() {
149         String bridgeId = this.bridgeId;
150         return Objects.isNull(bridgeId) || bridgeId.isBlank() ? null : bridgeId;
151     }
152
153     /**
154      * Get the brightness as a PercentType. If off the brightness is 0, otherwise use dimming value.
155      *
156      * @return a PercentType with the dimming state, or UNDEF, or NULL
157      */
158     public State getBrightnessState() {
159         Dimming dimming = this.dimming;
160         if (Objects.nonNull(dimming)) {
161             try {
162                 // if off the brightness is 0, otherwise it is dimming value
163                 OnState on = this.on;
164                 double brightness = Objects.nonNull(on) && !on.isOn() ? 0f
165                         : Math.max(0f, Math.min(100f, dimming.getBrightness()));
166                 return new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT));
167             } catch (DTOPresentButEmptyException e) {
168                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
169             }
170         }
171         return UnDefType.NULL;
172     }
173
174     public @Nullable Button getButton() {
175         return button;
176     }
177
178     /**
179      * Get the state corresponding to a button's last event value multiplied by the controlId found for it in the given
180      * controlIds map. States are decimal values formatted like '1002' where the first digit is the button's controlId
181      * and the last digit is the ordinal value of the button's last event.
182      *
183      * @param controlIds the map of control ids to be referenced.
184      * @return the state.
185      */
186     public State getButtonEventState(Map<String, Integer> controlIds) {
187         Button button = this.button;
188         if (Objects.nonNull(button)) {
189             try {
190                 return new DecimalType(
191                         (controlIds.getOrDefault(getId(), 0).intValue() * 1000) + button.getLastEvent().ordinal());
192             } catch (IllegalArgumentException e) {
193                 // fall through
194             }
195         }
196         return UnDefType.NULL;
197     }
198
199     public State getButtonLastEventState() {
200         Button button = this.button;
201         return Objects.nonNull(button) ? button.getLastEventState() : UnDefType.NULL;
202     }
203
204     public List<ResourceReference> getChildren() {
205         List<ResourceReference> children = this.children;
206         return Objects.nonNull(children) ? children : List.of();
207     }
208
209     /**
210      * Get the color as an HSBType. This returns an HSB that is based on an amalgamation of the color xy, dimming, and
211      * on/off JSON elements. It takes its 'H' & 'S' parts from the 'ColorXy' JSON element, and its 'B' part from the
212      * on/off resp. dimming JSON elements. If off the B part is 0, otherwise it is the dimming element value. Note: this
213      * method is only to be used on cached state DTOs which already have a defined color gamut.
214      *
215      * @return an HSBType containing the current color and brightness level, or UNDEF or NULL.
216      */
217     public State getColorState() {
218         ColorXy color = this.color;
219         if (Objects.nonNull(color)) {
220             try {
221                 Gamut gamut = color.getGamut();
222                 gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT;
223                 HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut);
224                 OnState on = this.on;
225                 Dimming dimming = this.dimming;
226                 double brightness = Objects.nonNull(on) && !on.isOn() ? 0
227                         : Objects.nonNull(dimming) ? Math.max(0, Math.min(100, dimming.getBrightness())) : 50;
228                 return new HSBType(hsb.getHue(), hsb.getSaturation(),
229                         new PercentType(new BigDecimal(brightness, PERCENT_MATH_CONTEXT)));
230             } catch (DTOPresentButEmptyException e) {
231                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
232             }
233         }
234         return UnDefType.NULL;
235     }
236
237     public @Nullable ColorTemperature getColorTemperature() {
238         return colorTemperature;
239     }
240
241     public State getColorTemperatureAbsoluteState() {
242         ColorTemperature colorTemp = colorTemperature;
243         if (Objects.nonNull(colorTemp)) {
244             try {
245                 QuantityType<?> colorTemperature = colorTemp.getAbsolute();
246                 if (Objects.nonNull(colorTemperature)) {
247                     return colorTemperature;
248                 }
249             } catch (DTOPresentButEmptyException e) {
250                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
251             }
252         }
253         return UnDefType.NULL;
254     }
255
256     /**
257      * Get the colour temperature in percent. Note: this method is only to be used on cached state DTOs which already
258      * have a defined mirek schema.
259      *
260      * @return a PercentType with the colour temperature percentage.
261      */
262     public State getColorTemperaturePercentState() {
263         ColorTemperature colorTemperature = this.colorTemperature;
264         if (Objects.nonNull(colorTemperature)) {
265             try {
266                 Double percent = colorTemperature.getPercent();
267                 if (Objects.nonNull(percent)) {
268                     return new PercentType(new BigDecimal(percent, PERCENT_MATH_CONTEXT));
269                 }
270             } catch (DTOPresentButEmptyException e) {
271                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
272             }
273         }
274         return UnDefType.NULL;
275     }
276
277     public @Nullable ColorXy getColorXy() {
278         return color;
279     }
280
281     /**
282      * Return an HSB where the HS part is derived from the color xy JSON element (only), so the B part is 100%
283      *
284      * @return an HSBType.
285      */
286     public State getColorXyState() {
287         ColorXy color = this.color;
288         if (Objects.nonNull(color)) {
289             try {
290                 Gamut gamut = color.getGamut();
291                 gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT;
292                 HSBType hsb = ColorUtil.xyToHsb(color.getXY(), gamut);
293                 return new HSBType(hsb.getHue(), hsb.getSaturation(), PercentType.HUNDRED);
294             } catch (DTOPresentButEmptyException e) {
295                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
296             }
297         }
298         return UnDefType.NULL;
299     }
300
301     public int getControlId() {
302         MetaData metadata = this.metadata;
303         return Objects.nonNull(metadata) ? metadata.getControlId() : 0;
304     }
305
306     public @Nullable Dimming getDimming() {
307         return dimming;
308     }
309
310     /**
311      * Return a PercentType which is derived from the dimming JSON element (only).
312      *
313      * @return a PercentType.
314      */
315     public State getDimmingState() {
316         Dimming dimming = this.dimming;
317         if (Objects.nonNull(dimming)) {
318             try {
319                 double dimmingValue = Math.max(0f, Math.min(100f, dimming.getBrightness()));
320                 return new PercentType(new BigDecimal(dimmingValue, PERCENT_MATH_CONTEXT));
321             } catch (DTOPresentButEmptyException e) {
322                 return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
323             }
324         }
325         return UnDefType.NULL;
326     }
327
328     public @Nullable Effects getEffects() {
329         return effects;
330     }
331
332     public State getEffectState() {
333         Effects effects = this.effects;
334         return Objects.nonNull(effects) ? new StringType(effects.getStatus().name()) : UnDefType.NULL;
335     }
336
337     public @Nullable Boolean getEnabled() {
338         return enabled;
339     }
340
341     public State getEnabledState() {
342         Boolean enabled = this.enabled;
343         return Objects.nonNull(enabled) ? OnOffType.from(enabled.booleanValue()) : UnDefType.NULL;
344     }
345
346     public @Nullable Gamut getGamut() {
347         ColorXy color = this.color;
348         return Objects.nonNull(color) ? color.getGamut() : null;
349     }
350
351     public @Nullable ResourceReference getGroup() {
352         return group;
353     }
354
355     public String getId() {
356         String id = this.id;
357         return Objects.nonNull(id) ? id : "";
358     }
359
360     public String getIdV1() {
361         String idV1 = this.idV1;
362         return Objects.nonNull(idV1) ? idV1 : "";
363     }
364
365     public @Nullable LightLevel getLightLevel() {
366         return light;
367     }
368
369     public State getLightLevelState() {
370         LightLevel light = this.light;
371         return Objects.nonNull(light) ? light.getLightLevelState() : UnDefType.NULL;
372     }
373
374     public @Nullable MetaData getMetaData() {
375         return metadata;
376     }
377
378     public @Nullable Double getMinimumDimmingLevel() {
379         Dimming dimming = this.dimming;
380         return Objects.nonNull(dimming) ? dimming.getMinimumDimmingLevel() : null;
381     }
382
383     public @Nullable MirekSchema getMirekSchema() {
384         ColorTemperature colorTemp = this.colorTemperature;
385         return Objects.nonNull(colorTemp) ? colorTemp.getMirekSchema() : null;
386     }
387
388     public @Nullable Motion getMotion() {
389         return motion;
390     }
391
392     public State getMotionState() {
393         Motion motion = this.motion;
394         return Objects.nonNull(motion) ? motion.getMotionState() : UnDefType.NULL;
395     }
396
397     public State getMotionValidState() {
398         Motion motion = this.motion;
399         return Objects.nonNull(motion) ? motion.getMotionValidState() : UnDefType.NULL;
400     }
401
402     public String getName() {
403         MetaData metaData = getMetaData();
404         if (Objects.nonNull(metaData)) {
405             String name = metaData.getName();
406             if (Objects.nonNull(name)) {
407                 return name;
408             }
409         }
410         return getType().toString();
411     }
412
413     /**
414      * Return the state of the On/Off element (only).
415      */
416     public State getOnOffState() {
417         try {
418             OnState on = this.on;
419             return Objects.nonNull(on) ? OnOffType.from(on.isOn()) : UnDefType.NULL;
420         } catch (DTOPresentButEmptyException e) {
421             return UnDefType.UNDEF; // indicates the DTO is present but its inner fields are missing
422         }
423     }
424
425     public @Nullable OnState getOnState() {
426         return on;
427     }
428
429     public @Nullable ResourceReference getOwner() {
430         return owner;
431     }
432
433     public @Nullable Power getPowerState() {
434         return powerState;
435     }
436
437     public @Nullable ProductData getProductData() {
438         return productData;
439     }
440
441     public String getProductName() {
442         ProductData productData = getProductData();
443         if (Objects.nonNull(productData)) {
444             return productData.getProductName();
445         }
446         return getType().toString();
447     }
448
449     public @Nullable Recall getRecall() {
450         return recall;
451     }
452
453     public @Nullable RelativeRotary getRelativeRotary() {
454         return relativeRotary;
455     }
456
457     public State getRelativeRotaryActionState() {
458         RelativeRotary relativeRotary = this.relativeRotary;
459         return Objects.nonNull(relativeRotary) ? relativeRotary.getActionState() : UnDefType.NULL;
460     }
461
462     public State getRotaryStepsState() {
463         RelativeRotary relativeRotary = this.relativeRotary;
464         return Objects.nonNull(relativeRotary) ? relativeRotary.getStepsState() : UnDefType.NULL;
465     }
466
467     /**
468      * Check if the scene resource contains a 'status.active' element. If such an element is present, returns a Boolean
469      * Optional whose value depends on the value of that element, or an empty Optional if it is not.
470      *
471      * @return true, false, or empty.
472      */
473     public Optional<Boolean> getSceneActive() {
474         if (ResourceType.SCENE == getType()) {
475             JsonElement status = this.status;
476             if (Objects.nonNull(status) && status.isJsonObject()) {
477                 JsonElement active = ((JsonObject) status).get("active");
478                 if (Objects.nonNull(active) && active.isJsonPrimitive()) {
479                     return Optional.of(!"inactive".equalsIgnoreCase(active.getAsString()));
480                 }
481             }
482         }
483         return Optional.empty();
484     }
485
486     /**
487      * If the getSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result is
488      * present and 'true' (i.e. the scene is active) return the scene name. Or finally (the optional result is present
489      * and 'false') return 'UnDefType.UNDEF'.
490      *
491      * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
492      */
493     public State getSceneState() {
494         Optional<Boolean> active = getSceneActive();
495         return active.isEmpty() ? UnDefType.NULL : active.get() ? new StringType(getName()) : UnDefType.UNDEF;
496     }
497
498     public List<ResourceReference> getServiceReferences() {
499         List<ResourceReference> services = this.services;
500         return Objects.nonNull(services) ? services : List.of();
501     }
502
503     public JsonObject getStatus() {
504         JsonElement status = this.status;
505         if (Objects.nonNull(status) && status.isJsonObject()) {
506             return status.getAsJsonObject();
507         }
508         return new JsonObject();
509     }
510
511     public @Nullable Temperature getTemperature() {
512         return temperature;
513     }
514
515     public State getTemperatureState() {
516         Temperature temperature = this.temperature;
517         return Objects.nonNull(temperature) ? temperature.getTemperatureState() : UnDefType.NULL;
518     }
519
520     public State getTemperatureValidState() {
521         Temperature temperature = this.temperature;
522         return Objects.nonNull(temperature) ? temperature.getTemperatureValidState() : UnDefType.NULL;
523     }
524
525     public @Nullable Effects getTimedEffects() {
526         return timedEffects;
527     }
528
529     public ResourceType getType() {
530         return ResourceType.of(type);
531     }
532
533     public State getZigbeeState() {
534         ZigbeeStatus zigbeeStatus = getZigbeeStatus();
535         return Objects.nonNull(zigbeeStatus) ? new StringType(zigbeeStatus.toString()) : UnDefType.NULL;
536     }
537
538     public @Nullable ZigbeeStatus getZigbeeStatus() {
539         JsonElement status = this.status;
540         if (Objects.nonNull(status) && status.isJsonPrimitive()) {
541             return ZigbeeStatus.of(status.getAsString());
542         }
543         return null;
544     }
545
546     public boolean hasFullState() {
547         return !hasSparseData;
548     }
549
550     /**
551      * Mark that the resource has sparse data.
552      *
553      * @return this instance.
554      */
555     public Resource markAsSparse() {
556         hasSparseData = true;
557         return this;
558     }
559
560     public Resource setAlerts(Alerts alert) {
561         this.alert = alert;
562         return this;
563     }
564
565     public Resource setColorTemperature(ColorTemperature colorTemperature) {
566         this.colorTemperature = colorTemperature;
567         return this;
568     }
569
570     public Resource setColorXy(ColorXy color) {
571         this.color = color;
572         return this;
573     }
574
575     public Resource setDimming(Dimming dimming) {
576         this.dimming = dimming;
577         return this;
578     }
579
580     public Resource setDynamicsDuration(Duration duration) {
581         dynamics = new Dynamics().setDuration(duration);
582         return this;
583     }
584
585     public Resource setEffects(Effects effect) {
586         this.effects = effect;
587         return this;
588     }
589
590     public Resource setEnabled(Command command) {
591         if (command instanceof OnOffType) {
592             this.enabled = ((OnOffType) command) == OnOffType.ON;
593         }
594         return this;
595     }
596
597     public Resource setId(String id) {
598         this.id = id;
599         return this;
600     }
601
602     public Resource setMetadata(MetaData metadata) {
603         this.metadata = metadata;
604         return this;
605     }
606
607     public Resource setMirekSchema(@Nullable MirekSchema schema) {
608         ColorTemperature colorTemperature = this.colorTemperature;
609         if (Objects.nonNull(colorTemperature)) {
610             colorTemperature.setMirekSchema(schema);
611         }
612         return this;
613     }
614
615     /**
616      * Set the on/off JSON element (only).
617      *
618      * @param command an OnOffTypee command value.
619      * @return this resource instance.
620      */
621     public Resource setOnOff(Command command) {
622         if (command instanceof OnOffType) {
623             OnOffType onOff = (OnOffType) command;
624             OnState on = this.on;
625             on = Objects.nonNull(on) ? on : new OnState();
626             on.setOn(OnOffType.ON.equals(onOff));
627             this.on = on;
628         }
629         return this;
630     }
631
632     public void setOnState(OnState on) {
633         this.on = on;
634     }
635
636     public Resource setRecallAction(RecallAction recallAction) {
637         Recall recall = this.recall;
638         this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
639         return this;
640     }
641
642     public Resource setRecallDuration(Duration recallDuration) {
643         Recall recall = this.recall;
644         this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setDuration(recallDuration);
645         return this;
646     }
647
648     public Resource setType(ResourceType resourceType) {
649         this.type = resourceType.name().toLowerCase();
650         return this;
651     }
652
653     @Override
654     public String toString() {
655         String id = this.id;
656         return String.format("id:%s, type:%s", Objects.nonNull(id) ? id : "?" + " ".repeat(35),
657                 getType().name().toLowerCase());
658     }
659 }