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.io.hueemulation.internal;
15 import java.util.ArrayList;
16 import java.util.List;
19 import java.util.TreeMap;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.core.items.Item;
24 import org.openhab.core.library.CoreItemFactory;
25 import org.openhab.core.library.types.HSBType;
26 import org.openhab.core.library.types.OnOffType;
27 import org.openhab.core.library.types.PercentType;
28 import org.openhab.core.types.Command;
29 import org.openhab.core.types.State;
30 import org.openhab.io.hueemulation.internal.dto.AbstractHueState;
31 import org.openhab.io.hueemulation.internal.dto.HueStateBulb;
32 import org.openhab.io.hueemulation.internal.dto.HueStateColorBulb;
33 import org.openhab.io.hueemulation.internal.dto.HueStateColorBulb.ColorMode;
34 import org.openhab.io.hueemulation.internal.dto.HueStatePlug;
35 import org.openhab.io.hueemulation.internal.dto.changerequest.HueStateChange;
36 import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
37 import org.openhab.io.hueemulation.internal.dto.response.HueResponse.HueErrorMessage;
38 import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseStateChanged;
41 * This utility class provides all kind of functions to convert between openHAB item states to Hue states and back
42 * as well as applying a hue state change request to a hue state or openHAB item state.
44 * It also provides methods to determine the hue type (plug, white bulb, coloured bulb), given an item.
46 * @author David Graeff - Initial contribution
49 public class StateUtils {
52 * Compute the hue state from a given item state and a device type.
54 * @param itemState The item state
55 * @param deviceType The device type
56 * @return A hue light state
58 public static AbstractHueState colorStateFromItemState(State itemState, @Nullable DeviceType deviceType) {
59 if (deviceType == null) {
60 return new HueStatePlug(false);
62 AbstractHueState state;
65 if (itemState instanceof HSBType) {
66 state = new HueStateColorBulb((HSBType) itemState);
67 } else if (itemState instanceof PercentType) {
68 state = new HueStateColorBulb((PercentType) itemState, ((PercentType) itemState).intValue() > 0);
69 } else if (itemState instanceof OnOffType) {
70 OnOffType t = (OnOffType) itemState;
71 state = new HueStateColorBulb(t == OnOffType.ON);
73 state = new HueStateColorBulb(new HSBType());
77 case WhiteTemperatureType:
78 if (itemState instanceof HSBType) {
79 PercentType brightness = ((HSBType) itemState).getBrightness();
80 state = new HueStateBulb(brightness, brightness.intValue() > 0);
81 } else if (itemState instanceof PercentType) {
82 PercentType brightness = (PercentType) itemState;
83 state = new HueStateBulb(brightness, brightness.intValue() > 0);
84 } else if (itemState instanceof OnOffType) {
85 OnOffType t = (OnOffType) itemState;
86 state = new HueStateBulb(t == OnOffType.ON);
88 state = new HueStateBulb(new PercentType(0), false);
93 if (itemState instanceof OnOffType) {
94 OnOffType t = (OnOffType) itemState;
95 state = new HueStatePlug(t == OnOffType.ON);
97 state = new HueStatePlug(false);
104 * Computes an openHAB item state, given a hue state.
107 * This only proxies to the respective call
108 * on the concrete hue state implementation.
110 * @throws IllegalStateException Thrown if the concrete hue state is not yet handled by this method.
112 public static State itemStateByHueState(AbstractHueState state) throws IllegalStateException {
113 if (state instanceof HueStateColorBulb) {
114 return state.as(HueStateColorBulb.class).toHSBType();
115 } else if (state instanceof HueStateBulb) {
116 return state.as(HueStateBulb.class).toBrightnessType();
117 } else if (state instanceof HueStatePlug) {
118 return state.as(HueStatePlug.class).toOnOffType();
120 throw new IllegalStateException();
125 * An openHAB state is usually also a command. Cast the state.
127 * @throws IllegalStateException Throws if the cast fails.
129 public static Command commandByItemState(State state) throws IllegalStateException {
130 if (state instanceof Command) {
131 return (Command) state;
133 throw new IllegalStateException();
138 * Computes an openHAB command, given a hue state change request.
140 * @param changeRequest The change request
142 public static @Nullable Command computeCommandByChangeRequest(HueStateChange changeRequest) {
143 List<HueResponse> responses = new ArrayList<>();
144 return computeCommandByState(responses, "", new HueStateColorBulb(false), changeRequest);
148 * Apply the new received state from the REST PUT request.
150 * @param responses Creates a response entry for each success and each error. There is one entry per non-null field
151 * of {@link HueStateChange} created.
152 * @param prefix The response entry prefix, for example "/groups/mygroupid/state/"
153 * @param state The current item state
154 * @param newState A state change DTO
155 * @return Return a command computed via the incoming state object.
157 public static @Nullable Command computeCommandByState(List<HueResponse> responses, String prefix,
158 AbstractHueState state, HueStateChange newState) {
159 // Apply new state and collect success, error items
160 Map<String, Object> successApplied = new TreeMap<>();
161 List<String> errorApplied = new ArrayList<>();
163 Command command = null;
164 if (newState.on != null) {
166 state.as(HueStatePlug.class).on = newState.on;
167 command = OnOffType.from(newState.on);
168 successApplied.put("on", newState.on);
169 } catch (ClassCastException e) {
170 errorApplied.add("on");
174 if (newState.bri != null) {
176 state.as(HueStateBulb.class).bri = newState.bri;
177 command = new PercentType((int) (newState.bri * 100.0 / HueStateBulb.MAX_BRI + 0.5));
178 successApplied.put("bri", newState.bri);
179 } catch (ClassCastException e) {
180 errorApplied.add("bri");
184 if (newState.bri_inc != null) {
186 int newBri = state.as(HueStateBulb.class).bri + newState.bri_inc;
187 if (newBri < 0 || newBri > HueStateBulb.MAX_BRI) {
188 throw new IllegalArgumentException();
190 command = new PercentType((int) (newBri * 100.0 / HueStateBulb.MAX_BRI + 0.5));
191 successApplied.put("bri", newState.bri);
192 } catch (ClassCastException e) {
193 errorApplied.add("bri_inc");
194 } catch (IllegalArgumentException e) {
195 errorApplied.add("bri_inc");
199 if (newState.sat != null) {
201 HueStateColorBulb c = state.as(HueStateColorBulb.class);
202 c.sat = newState.sat;
203 c.colormode = ColorMode.hs;
204 command = c.toHSBType();
205 successApplied.put("sat", newState.sat);
206 } catch (ClassCastException e) {
207 errorApplied.add("sat");
211 if (newState.sat_inc != null) {
213 HueStateColorBulb c = state.as(HueStateColorBulb.class);
214 int newV = c.sat + newState.sat_inc;
215 if (newV < 0 || newV > HueStateColorBulb.MAX_SAT) {
216 throw new IllegalArgumentException();
218 c.colormode = ColorMode.hs;
220 command = c.toHSBType();
221 successApplied.put("sat", newState.sat);
222 } catch (ClassCastException e) {
223 errorApplied.add("sat_inc");
224 } catch (IllegalArgumentException e) {
225 errorApplied.add("sat_inc");
229 if (newState.hue != null) {
231 HueStateColorBulb c = state.as(HueStateColorBulb.class);
232 c.colormode = ColorMode.hs;
233 c.hue = newState.hue;
234 command = c.toHSBType();
235 successApplied.put("hue", newState.hue);
236 } catch (ClassCastException e) {
237 errorApplied.add("hue");
241 if (newState.hue_inc != null) {
243 HueStateColorBulb c = state.as(HueStateColorBulb.class);
244 int newV = c.hue + newState.hue_inc;
245 if (newV < 0 || newV > HueStateColorBulb.MAX_HUE) {
246 throw new IllegalArgumentException();
248 c.colormode = ColorMode.hs;
250 command = c.toHSBType();
251 successApplied.put("hue", newState.hue);
252 } catch (ClassCastException e) {
253 errorApplied.add("hue_inc");
254 } catch (IllegalArgumentException e) {
255 errorApplied.add("hue_inc");
259 if (newState.ct != null) {
261 // We can't do anything here with a white color temperature.
262 // The color type does not support setting it.
264 // Adjusting the color temperature implies setting the mode to ct
265 if (state instanceof HueStateColorBulb) {
266 HueStateColorBulb c = state.as(HueStateColorBulb.class);
268 c.colormode = ColorMode.ct;
269 command = c.toHSBType();
271 successApplied.put("colormode", ColorMode.ct);
272 successApplied.put("sat", 0);
273 successApplied.put("ct", newState.ct);
274 } catch (ClassCastException e) {
275 errorApplied.add("ct");
279 if (newState.ct_inc != null) {
281 // We can't do anything here with a white color temperature.
282 // The color type does not support setting it.
284 // Adjusting the color temperature implies setting the mode to ct
285 if (state instanceof HueStateColorBulb) {
286 HueStateColorBulb c = state.as(HueStateColorBulb.class);
287 if (c.colormode != ColorMode.ct) {
289 command = c.toHSBType();
290 successApplied.put("colormode", c.colormode);
293 successApplied.put("ct", newState.ct);
294 } catch (ClassCastException e) {
295 errorApplied.add("ct_inc");
299 if (newState.transitiontime != null) {
300 successApplied.put("transitiontime", newState.transitiontime); // Pretend that worked
302 if (newState.alert != null) {
303 successApplied.put("alert", newState.alert); // Pretend that worked
305 if (newState.effect != null) {
306 successApplied.put("effect", newState.effect); // Pretend that worked
308 if (newState.xy != null) {
310 HueStateColorBulb c = state.as(HueStateColorBulb.class);
311 c.colormode = ColorMode.xy;
312 c.bri = state.as(HueStateBulb.class).bri;
313 c.xy[0] = newState.xy.get(0);
314 c.xy[1] = newState.xy.get(1);
315 command = c.toHSBType();
316 successApplied.put("xy", newState.xy);
317 } catch (ClassCastException e) {
318 errorApplied.add("xy");
321 if (newState.xy_inc != null) {
323 HueStateColorBulb c = state.as(HueStateColorBulb.class);
324 double newX = c.xy[0] + newState.xy_inc.get(0);
325 double newY = c.xy[1] + newState.xy_inc.get(1);
326 if (newX < 0 || newX > 1 || newY < 0 || newY > 1) {
327 throw new IllegalArgumentException();
329 c.colormode = ColorMode.xy;
330 c.bri = state.as(HueStateBulb.class).bri;
333 command = c.toHSBType();
334 successApplied.put("xy", newState.xy_inc);
335 } catch (ClassCastException e) {
336 errorApplied.add("xy_inc");
337 } catch (IllegalArgumentException e) {
338 errorApplied.add("xy_inc");
342 // Generate the response. The response consists of a list with an entry each for all
343 // submitted change requests. If for example "on" and "bri" was send, 2 entries in the response are
345 successApplied.forEach((t, v) -> {
346 responses.add(new HueResponse(new HueSuccessResponseStateChanged(prefix + "/" + t, v)));
348 errorApplied.forEach(v -> {
350 new HueResponse(new HueErrorMessage(HueResponse.NOT_AVAILABLE, prefix + "/" + v, "Could not set")));
356 public static @Nullable DeviceType determineTargetType(ConfigStore cs, Item element) {
357 String category = element.getCategory();
358 String type = element.getType();
359 Set<String> tags = element.getTags();
361 // Determine type, heuristically
364 // The user wants this item to be not exposed
365 if (cs.ignoreItemsFilter.stream().anyMatch(tags::contains)) {
369 // First consider the category
370 if (category != null) {
373 t = DeviceType.ColorType;
376 t = DeviceType.SwitchType;
381 if (cs.switchFilter.stream().anyMatch(tags::contains)) {
382 t = DeviceType.SwitchType;
384 if (cs.whiteFilter.stream().anyMatch(tags::contains)) {
385 t = DeviceType.WhiteTemperatureType;
387 if (cs.colorFilter.stream().anyMatch(tags::contains)) {
388 t = DeviceType.ColorType;
391 // Last but not least, the item type
394 case CoreItemFactory.COLOR:
395 if (cs.colorFilter.isEmpty()) {
396 t = DeviceType.ColorType;
399 case CoreItemFactory.DIMMER:
400 case CoreItemFactory.ROLLERSHUTTER:
401 if (cs.whiteFilter.isEmpty()) {
402 t = DeviceType.WhiteTemperatureType;
405 case CoreItemFactory.SWITCH:
406 if (cs.switchFilter.isEmpty()) {
407 t = DeviceType.SwitchType;
416 * Compute the hue state from a given item state and a device type.
417 * If the item state matches the last command. the hue state is adjusted
418 * to use the values from the last hue state change. This is done to prevent
419 * Alexa reporting device errors.
421 * @param itemState The item state
422 * @param deviceType The device type
423 * @param lastCommand The last command
424 * @param lastHueChange The last hue state change
425 * @return A hue light state
427 public static AbstractHueState adjustedColorStateFromItemState(State itemState, @Nullable DeviceType deviceType,
428 @Nullable Command lastCommand, @Nullable HueStateChange lastHueChange) {
429 AbstractHueState hueState = colorStateFromItemState(itemState, deviceType);
431 if (lastCommand != null && lastHueChange != null) {
432 if (lastCommand instanceof HSBType) {
433 if (hueState instanceof HueStateColorBulb && itemState.as(HSBType.class).equals(lastCommand)) {
434 HueStateColorBulb c = (HueStateColorBulb) hueState;
436 if (lastHueChange.bri != null) {
437 c.bri = lastHueChange.bri;
439 if (lastHueChange.hue != null) {
440 c.hue = lastHueChange.hue;
442 if (lastHueChange.sat != null) {
443 c.sat = lastHueChange.sat;
445 // Although we can't set a colour temperature in OH
446 // this keeps Alexa happy when asking to turn a light
448 if (lastHueChange.ct != null) {
449 c.ct = lastHueChange.ct;
452 } else if (lastCommand instanceof PercentType) {
453 if (hueState instanceof HueStateBulb && itemState != null
454 && lastCommand.equals(itemState.as(PercentType.class))) {
455 if (lastHueChange.bri != null) {
456 ((HueStateBulb) hueState).bri = lastHueChange.bri;