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.api.dto.clip2.helper;
15 import java.math.BigDecimal;
16 import java.time.Duration;
17 import java.util.Collection;
18 import java.util.HashMap;
19 import java.util.Iterator;
20 import java.util.List;
22 import java.util.Objects;
25 import javax.measure.Unit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.hue.internal.api.dto.clip2.Alerts;
30 import org.openhab.binding.hue.internal.api.dto.clip2.ColorTemperature;
31 import org.openhab.binding.hue.internal.api.dto.clip2.ColorXy;
32 import org.openhab.binding.hue.internal.api.dto.clip2.Dimming;
33 import org.openhab.binding.hue.internal.api.dto.clip2.Effects;
34 import org.openhab.binding.hue.internal.api.dto.clip2.MetaData;
35 import org.openhab.binding.hue.internal.api.dto.clip2.MirekSchema;
36 import org.openhab.binding.hue.internal.api.dto.clip2.OnState;
37 import org.openhab.binding.hue.internal.api.dto.clip2.Resource;
38 import org.openhab.binding.hue.internal.api.dto.clip2.TimedEffects;
39 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ActionType;
40 import org.openhab.binding.hue.internal.api.dto.clip2.enums.EffectType;
41 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ResourceType;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.HSBType;
44 import org.openhab.core.library.types.PercentType;
45 import org.openhab.core.library.types.QuantityType;
46 import org.openhab.core.library.types.StringType;
47 import org.openhab.core.library.unit.Units;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.util.ColorUtil;
50 import org.openhab.core.util.ColorUtil.Gamut;
53 * Advanced setter methods for fields in the Resource class for special cases where setting the new value in the target
54 * resource depends on logic using the values of other fields in a another source Resource.
56 * @author Andrew Fiddian-Green - Initial contribution
59 public class Setters {
61 private static final Set<ResourceType> LIGHT_TYPES = Set.of(ResourceType.LIGHT, ResourceType.GROUPED_LIGHT);
64 * Setter for Alert field:
65 * Use the given command value to set the target resource DTO value based on the attributes of the source resource
68 * @param target the target resource.
69 * @param command the new state command should be a StringType.
70 * @param source another resource containing the allowed alert action values.
72 * @return the target resource.
74 public static Resource setAlert(Resource target, Command command, @Nullable Resource source) {
75 if ((command instanceof StringType) && Objects.nonNull(source)) {
76 Alerts otherAlert = source.getAlerts();
77 if (Objects.nonNull(otherAlert)) {
78 ActionType actionType = ActionType.of(((StringType) command).toString());
79 if (otherAlert.getActionValues().contains(actionType)) {
80 target.setAlerts(new Alerts().setAction(actionType));
88 * Setter for Color Temperature field:
89 * Use the given command value to set the target resource DTO value based on the attributes of the source resource
92 * @param target the target resource.
93 * @param command the new state command should be a {@code QuantityType<Temperature>} (but it can also handle
94 * {@code DecimalType}).
95 * @param source another resource containing the MirekSchema.
97 * @return the target resource.
99 public static Resource setColorTemperatureAbsolute(Resource target, Command command, @Nullable Resource source) {
100 QuantityType<?> mirek;
101 if (command instanceof QuantityType<?>) {
102 QuantityType<?> quantity = (QuantityType<?>) command;
103 Unit<?> unit = quantity.getUnit();
104 if (Units.KELVIN.equals(unit)) {
105 mirek = quantity.toInvertibleUnit(Units.MIRED);
106 } else if (Units.MIRED.equals(unit)) {
109 QuantityType<?> kelvin = quantity.toInvertibleUnit(Units.KELVIN);
110 mirek = Objects.nonNull(kelvin) ? kelvin.toInvertibleUnit(Units.MIRED) : null;
112 } else if (command instanceof DecimalType) {
113 mirek = QuantityType.valueOf(((DecimalType) command).doubleValue(), Units.KELVIN)
114 .toInvertibleUnit(Units.MIRED);
118 if (Objects.nonNull(mirek)) {
119 MirekSchema schema = target.getMirekSchema();
120 schema = Objects.nonNull(schema) ? schema : Objects.nonNull(source) ? source.getMirekSchema() : null;
121 schema = Objects.nonNull(schema) ? schema : MirekSchema.DEFAULT_SCHEMA;
122 ColorTemperature colorTemperature = target.getColorTemperature();
123 colorTemperature = Objects.nonNull(colorTemperature) ? colorTemperature : new ColorTemperature();
124 double min = schema.getMirekMinimum();
125 double max = schema.getMirekMaximum();
126 double val = Math.max(min, Math.min(max, mirek.doubleValue()));
127 target.setColorTemperature(colorTemperature.setMirek(val));
133 * Setter for Color Temperature field:
134 * Use the given command value to set the target resource DTO value based on the attributes of the source resource
137 * @param target the target resource.
138 * @param command the new state command should be a PercentType.
139 * @param source another resource containing the MirekSchema.
141 * @return the target resource.
143 public static Resource setColorTemperaturePercent(Resource target, Command command, @Nullable Resource source) {
144 if (command instanceof PercentType) {
145 MirekSchema schema = target.getMirekSchema();
146 schema = Objects.nonNull(schema) ? schema : Objects.nonNull(source) ? source.getMirekSchema() : null;
147 schema = Objects.nonNull(schema) ? schema : MirekSchema.DEFAULT_SCHEMA;
148 ColorTemperature colorTemperature = target.getColorTemperature();
149 colorTemperature = Objects.nonNull(colorTemperature) ? colorTemperature : new ColorTemperature();
150 double min = schema.getMirekMinimum();
151 double max = schema.getMirekMaximum();
152 double val = min + ((max - min) * ((PercentType) command).doubleValue() / 100f);
153 target.setColorTemperature(colorTemperature.setMirek(val));
159 * Setter for Color Xy field:
160 * Use the given command value to set the target resource DTO value based on the attributes of the source resource
161 * (if any). Use the HS parts of the HSB value to set the value of the 'ColorXy' JSON element, and ignore the 'B'
164 * @param target the target resource.
165 * @param command the new state command should be an HSBType with the new color XY value.
166 * @param source another resource containing the color Gamut.
168 * @return the target resource.
170 public static Resource setColorXy(Resource target, Command command, @Nullable Resource source) {
171 if (command instanceof HSBType) {
172 Gamut gamut = target.getGamut();
173 gamut = Objects.nonNull(gamut) ? gamut : Objects.nonNull(source) ? source.getGamut() : null;
174 gamut = Objects.nonNull(gamut) ? gamut : ColorUtil.DEFAULT_GAMUT;
175 HSBType hsb = (HSBType) command;
176 ColorXy color = target.getColorXy();
177 target.setColorXy((Objects.nonNull(color) ? color : new ColorXy()).setXY(ColorUtil.hsbToXY(hsb, gamut)));
183 * Setter for Dimming field:
184 * Use the given command value to set the target resource DTO value based on the attributes of the source resource
187 * @param target the target resource.
188 * @param command the new state command should be a PercentType with the new dimming parameter.
189 * @param source another resource containing the minimum dimming level.
191 * @return the target resource.
193 public static Resource setDimming(Resource target, Command command, @Nullable Resource source) {
194 if (command instanceof PercentType) {
195 Double min = target.getMinimumDimmingLevel();
196 min = Objects.nonNull(min) ? min : Objects.nonNull(source) ? source.getMinimumDimmingLevel() : null;
197 min = Objects.nonNull(min) ? min : Dimming.DEFAULT_MINIMUM_DIMMIMG_LEVEL;
198 PercentType brightness = (PercentType) command;
199 if (brightness.doubleValue() < min.doubleValue()) {
200 brightness = new PercentType(new BigDecimal(min, Resource.PERCENT_MATH_CONTEXT));
202 Dimming dimming = target.getDimming();
203 dimming = Objects.nonNull(dimming) ? dimming : new Dimming();
204 dimming.setBrightness(brightness.doubleValue());
205 target.setDimming(dimming);
211 * Setter for fixed or timed effect field:
212 * Use the given command value to set the target fixed or timed effects resource DTO value based on the attributes
213 * of the source resource (if any).
215 * @param target the target resource.
216 * @param command the new state command should be a StringType.
217 * @param source another resource containing the allowed effect action values.
219 * @return the target resource.
221 public static Resource setEffect(Resource target, Command command, @Nullable Resource source) {
222 if ((command instanceof StringType) && Objects.nonNull(source)) {
223 EffectType commandEffectType = EffectType.of(((StringType) command).toString());
224 Effects sourceFixedEffects = source.getFixedEffects();
225 if (Objects.nonNull(sourceFixedEffects) && sourceFixedEffects.allows(commandEffectType)) {
226 target.setFixedEffects(new Effects().setEffect(commandEffectType));
228 TimedEffects sourceTimedEffects = source.getTimedEffects();
229 if (Objects.nonNull(sourceTimedEffects) && sourceTimedEffects.allows(commandEffectType)) {
230 Duration duration = sourceTimedEffects.getDuration();
231 target.setTimedEffects(((TimedEffects) new TimedEffects().setEffect(commandEffectType))
232 .setDuration(Objects.nonNull(duration) ? duration : TimedEffects.DEFAULT_DURATION));
239 * Setter to copy persisted fields from the source Resource into the target Resource. If the field in the target is
240 * null and the same field in the source is not null, then the value from the source is copied to the target. This
241 * method allows 'hasSparseData' resources to expand themselves to include necessary fields taken over from a
242 * previously cached full data resource.
244 * @param target the target resource.
245 * @param source another resource containing the values to be taken over.
247 * @return the target resource.
249 public static Resource setResource(Resource target, Resource source) {
251 OnState targetOnOff = target.getOnState();
252 OnState sourceOnOff = source.getOnState();
253 if (Objects.isNull(targetOnOff) && Objects.nonNull(sourceOnOff)) {
254 target.setOnState(sourceOnOff);
258 Dimming targetDimming = target.getDimming();
259 Dimming sourceDimming = source.getDimming();
260 if (Objects.isNull(targetDimming) && Objects.nonNull(sourceDimming)) {
261 target.setDimming(sourceDimming);
262 targetDimming = target.getDimming();
265 // minimum dimming level
266 if (Objects.nonNull(targetDimming)) {
267 Double sourceMinDimLevel = Objects.isNull(sourceDimming) ? null : sourceDimming.getMinimumDimmingLevel();
268 if (Objects.nonNull(sourceMinDimLevel)) {
269 targetDimming.setMinimumDimmingLevel(sourceMinDimLevel);
274 ColorXy targetColor = target.getColorXy();
275 ColorXy sourceColor = source.getColorXy();
276 if (Objects.isNull(targetColor) && Objects.nonNull(sourceColor)) {
277 target.setColorXy(sourceColor);
278 targetColor = target.getColorXy();
282 Gamut sourceGamut = Objects.isNull(sourceColor) ? null : sourceColor.getGamut();
283 if (Objects.nonNull(targetColor) && Objects.nonNull(sourceGamut)) {
284 targetColor.setGamut(sourceGamut);
288 ColorTemperature targetColorTemp = target.getColorTemperature();
289 ColorTemperature sourceColorTemp = source.getColorTemperature();
290 if (Objects.isNull(targetColorTemp) && Objects.nonNull(sourceColorTemp)) {
291 target.setColorTemperature(sourceColorTemp);
292 targetColorTemp = target.getColorTemperature();
296 if (Objects.nonNull(targetColorTemp)) {
297 MirekSchema sourceMirekSchema = Objects.isNull(sourceColorTemp) ? null : sourceColorTemp.getMirekSchema();
298 if (Objects.nonNull(sourceMirekSchema)) {
299 targetColorTemp.setMirekSchema(sourceMirekSchema);
304 MetaData targetMetaData = target.getMetaData();
305 MetaData sourceMetaData = source.getMetaData();
306 if (Objects.isNull(targetMetaData) && Objects.nonNull(sourceMetaData)) {
307 target.setMetadata(sourceMetaData);
311 Alerts targetAlerts = target.getAlerts();
312 Alerts sourceAlerts = source.getAlerts();
313 if (Objects.isNull(targetAlerts) && Objects.nonNull(sourceAlerts)) {
314 target.setAlerts(sourceAlerts);
318 Effects targetFixedEffects = target.getFixedEffects();
319 Effects sourceFixedEffects = source.getFixedEffects();
320 if (Objects.isNull(targetFixedEffects) && Objects.nonNull(sourceFixedEffects)) {
321 target.setFixedEffects(sourceFixedEffects);
322 targetFixedEffects = target.getFixedEffects();
325 // fixed effects allowed values
326 if (Objects.nonNull(targetFixedEffects)) {
327 List<String> values = Objects.isNull(sourceFixedEffects) ? List.of() : sourceFixedEffects.getStatusValues();
328 if (!values.isEmpty()) {
329 targetFixedEffects.setStatusValues(values);
334 TimedEffects targetTimedEffects = target.getTimedEffects();
335 TimedEffects sourceTimedEffects = source.getTimedEffects();
336 if (Objects.isNull(targetTimedEffects) && Objects.nonNull(sourceTimedEffects)) {
337 target.setTimedEffects(sourceTimedEffects);
338 targetTimedEffects = target.getTimedEffects();
341 // timed effects allowed values and duration
342 if (Objects.nonNull(targetTimedEffects)) {
343 List<String> values = Objects.isNull(sourceTimedEffects) ? List.of() : sourceTimedEffects.getStatusValues();
344 if (!values.isEmpty()) {
345 targetTimedEffects.setStatusValues(values);
347 Duration duration = Objects.isNull(sourceTimedEffects) ? null : sourceTimedEffects.getDuration();
348 if (Objects.nonNull(duration)) {
349 targetTimedEffects.setDuration(duration);
356 * Merge on/dimming/color fields from light and grouped light resources.
357 * Subsequent resources will be merged into the first one.
358 * Full state resources are not supported by this method.
360 public static Collection<Resource> mergeLightResources(Collection<Resource> resources) {
361 Map<String, Resource> resourceIndex = new HashMap<>();
362 Iterator<Resource> iterator = resources.iterator();
363 while (iterator.hasNext()) {
364 Resource resource = iterator.next();
365 String id = resource.getId();
367 if (resource.hasFullState()) {
368 throw new IllegalStateException("Resource " + id + " has full state, this is not expected");
371 Resource indexedResource = resourceIndex.get(id);
372 if (indexedResource == null) {
373 resourceIndex.put(id, resource);
377 if (!LIGHT_TYPES.contains(resource.getType()) || !resource.hasHSBField()) {
381 OnState onState = resource.getOnState();
382 if (onState != null) {
383 indexedResource.setOnState(onState);
384 resource.setOnState(null);
386 Dimming dimming = resource.getDimming();
387 if (dimming != null) {
388 indexedResource.setDimming(dimming);
389 resource.setDimming(null);
391 ColorXy colorXy = resource.getColorXy();
392 if (colorXy != null) {
393 indexedResource.setColorXy(colorXy);
394 resource.setColorXy(null);
397 if (!resource.hasAnyRelevantField()) {