diff --git a/bundles/org.openhab.binding.hue/README.md b/bundles/org.openhab.binding.hue/README.md index 332b25cb8..9c2d07e51 100644 --- a/bundles/org.openhab.binding.hue/README.md +++ b/bundles/org.openhab.binding.hue/README.md @@ -158,28 +158,29 @@ The group type also have an optional configuration value to specify the fade tim The devices support some of the following channels: -| Channel Type ID | Item Type | Description | Thing types supporting this channel | -|-------------------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------| -| switch | Switch | This channel supports switching the device on and off. | 0000, 0010, group | -| color | Color | This channel supports full color control with hue, saturation and brightness values. | 0200, 0210, group | -| brightness | Dimmer | This channel supports adjusting the brightness value. Note that this is not available, if the color channel is supported. | 0100, 0110, 0220, group | -| color_temperature | Dimmer | This channel supports adjusting the color temperature from cold (0%) to warm (100%). | 0210, 0220, group | -| alert | String | This channel supports displaying alerts by flashing the bulb either once or multiple times. Valid values are: NONE, SELECT and LSELECT. | 0000, 0100, 0200, 0210, 0220, group | -| effect | Switch | This channel supports color looping. | 0200, 0210, 0220 | -| dimmer_switch | Number | This channel shows which button was last pressed on the dimmer switch. | 0820 | -| illuminance | Number:Illuminance | This channel shows the current illuminance measured by the sensor. | 0106 | -| light_level | Number | This channel shows the current light level measured by the sensor. **Advanced** | 0106 | -| dark | Switch | This channel indicates whether the light level is below the darkness threshold or not. | 0106 | -| daylight | Switch | This channel indicates whether the light level is below the daylight threshold or not. | 0106 | -| presence | Switch | This channel indicates whether a motion is detected by the sensor or not. | 0107 | -| enabled | Switch | This channel activated or deactivates the sensor | 0107 | -| temperature | Number:Temperature | This channel shows the current temperature measured by the sensor. | 0302 | -| flag | Switch | This channel save flag state for a CLIP sensor. | 0850 | -| status | Number | This channel save status state for a CLIP sensor. | 0840 | -| last_updated | DateTime | This channel the date and time when the sensor was last updated. | 0820, 0830, 0840, 0850, 0106, 0107, 0302 | -| battery_level | Number | This channel shows the battery level. | 0820, 0106, 0107, 0302 | -| battery_low | Switch | This channel indicates whether the battery is low or not. | 0820, 0106, 0107, 0302 | -| scene | String | This channel activates the scene with the given ID String. The ID String of each scene is assigned by the Hue bridge. | bridge, group | +| Channel Type ID | Item Type | Description | Thing types supporting this channel | +|-----------------------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------| +| switch | Switch | This channel supports switching the device on and off. | 0000, 0010, group | +| color | Color | This channel supports full color control with hue, saturation and brightness values. | 0200, 0210, group | +| brightness | Dimmer | This channel supports adjusting the brightness value. Note that this is not available, if the color channel is supported. | 0100, 0110, 0220, group | +| color_temperature | Dimmer | This channel supports adjusting the color temperature from cold (0%) to warm (100%). | 0210, 0220, group | +| color_temperature_abs | Number | This channel supports adjusting the color temperature in Kelvin. **Advanced** | 0210, 0220, group | +| alert | String | This channel supports displaying alerts by flashing the bulb either once or multiple times. Valid values are: NONE, SELECT and LSELECT. | 0000, 0100, 0200, 0210, 0220, group | +| effect | Switch | This channel supports color looping. | 0200, 0210, 0220 | +| dimmer_switch | Number | This channel shows which button was last pressed on the dimmer switch. | 0820 | +| illuminance | Number:Illuminance | This channel shows the current illuminance measured by the sensor. | 0106 | +| light_level | Number | This channel shows the current light level measured by the sensor. **Advanced** | 0106 | +| dark | Switch | This channel indicates whether the light level is below the darkness threshold or not. | 0106 | +| daylight | Switch | This channel indicates whether the light level is below the daylight threshold or not. | 0106 | +| presence | Switch | This channel indicates whether a motion is detected by the sensor or not. | 0107 | +| enabled | Switch | This channel activated or deactivates the sensor | 0107 | +| temperature | Number:Temperature | This channel shows the current temperature measured by the sensor. | 0302 | +| flag | Switch | This channel save flag state for a CLIP sensor. | 0850 | +| status | Number | This channel save status state for a CLIP sensor. | 0840 | +| last_updated | DateTime | This channel the date and time when the sensor was last updated. | 0820, 0830, 0840, 0850, 0106, 0107, 0302 | +| battery_level | Number | This channel shows the battery level. | 0820, 0106, 0107, 0302 | +| battery_low | Switch | This channel indicates whether the battery is low or not. | 0820, 0106, 0107, 0302 | +| scene | String | This channel activates the scene with the given ID String. The ID String of each scene is assigned by the Hue bridge. | bridge, group | To load a hue scene inside a rule for example, the ID of the scene will be required. You can list all the scene IDs with the following console commands: `hue scenes` and `hue scenes`. diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/FullLight.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/FullLight.java index cab424c29..ee2fad7fd 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/FullLight.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/FullLight.java @@ -17,6 +17,8 @@ import java.time.Duration; import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.dto.Capabilities; import com.google.gson.reflect.TypeToken; @@ -33,6 +35,7 @@ public class FullLight extends FullHueObject { public static final Type GSON_TYPE = new TypeToken>() { }.getType(); + public @Nullable Capabilities capabilities; private @NonNullByDefault({}) State state; private final long fadetime = 400; // milliseconds diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HttpClient.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HttpClient.java index f4adb9752..8f02a02d6 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HttpClient.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HttpClient.java @@ -93,7 +93,8 @@ public class HttpClient { synchronized (commandsQueue) { if (commandsQueue.isEmpty()) { commandsQueue.offer(asyncPutParameters); - if (job == null || job.isDone()) { + Future localJob = job; + if (localJob == null || localJob.isDone()) { job = scheduler.submit(this::executeCommands); } } else { diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java index a914cf14e..8f38f52b9 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java @@ -58,6 +58,7 @@ public class HueBindingConstants { // List all channels public static final String CHANNEL_COLORTEMPERATURE = "color_temperature"; + public static final String CHANNEL_COLORTEMPERATURE_ABS = "color_temperature_abs"; public static final String CHANNEL_COLOR = "color"; public static final String CHANNEL_BRIGHTNESS = "brightness"; public static final String CHANNEL_ALERT = "alert"; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java index 4bc07f45b..8644676a7 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java @@ -144,7 +144,7 @@ public class HueThingHandlerFactory extends BaseThingHandlerFactory { if (HueBridgeHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { return new HueBridgeHandler((Bridge) thing, stateOptionProvider); } else if (HueLightHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { - return new HueLightHandler(thing); + return new HueLightHandler(thing, stateOptionProvider); } else if (DimmerSwitchHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { return new DimmerSwitchHandler(thing); } else if (TapSwitchHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/State.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/State.java index 7e116f5bf..7ce101bdc 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/State.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/State.java @@ -27,7 +27,7 @@ public class State { int hue; int sat; private float[] xy; - private int ct; + int ct; private String alert; private String effect; String colormode; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/StateUpdate.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/StateUpdate.java index 55f8958cf..28617b345 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/StateUpdate.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/StateUpdate.java @@ -14,6 +14,7 @@ package org.openhab.binding.hue.internal; import org.openhab.binding.hue.internal.State.AlertMode; import org.openhab.binding.hue.internal.State.Effect; +import org.openhab.binding.hue.internal.dto.ColorTemperature; /** * Collection of updates to the state of a light. @@ -139,12 +140,13 @@ public class StateUpdate extends ConfigUpdate { /** * Switch to CT color mode and set color temperature in mired. * - * @param colorTemperature color temperature [153..500] + * @param colorTemperature color temperature * @return this object for chaining calls */ - public StateUpdate setColorTemperature(int colorTemperature) { - if (colorTemperature < 153 || colorTemperature > 500) { - throw new IllegalArgumentException("Color temperature out of range"); + public StateUpdate setColorTemperature(int colorTemperature, ColorTemperature capabilities) { + if (colorTemperature < capabilities.min || colorTemperature > capabilities.max) { + throw new IllegalArgumentException(String.format("Color temperature %d is out of range [%d..%d]", + colorTemperature, capabilities.min, capabilities.max)); } commands.add(new Command("ct", colorTemperature)); diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/Capabilities.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/Capabilities.java new file mode 100644 index 000000000..7b544e923 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/Capabilities.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto; + +/** + * Collection of capabilities for lights. + * + * @author Christoph Weitkamp - Initial contribution + */ +public class Capabilities { + public Control control; +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/ColorTemperature.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/ColorTemperature.java new file mode 100644 index 000000000..655a1505a --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/ColorTemperature.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto; + +/** + * Collection of color temperature capabilities to control lights. + * + * @author Christoph Weitkamp - Initial contribution + */ +public class ColorTemperature { + public int max = 500; + public int min = 153; +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/Control.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/Control.java new file mode 100644 index 000000000..ce2817cd5 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/Control.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto; + +import org.eclipse.jdt.annotation.Nullable; + +/** + * Collection of capabilities to control lights. + * + * @author Christoph Weitkamp - Initial contribution + */ +public class Control { + public @Nullable ColorTemperature ct; +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java index b6139627a..a8e87730a 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java @@ -15,8 +15,6 @@ package org.openhab.binding.hue.internal.handler; import static org.openhab.binding.hue.internal.HueBindingConstants.*; import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; @@ -31,6 +29,8 @@ import org.openhab.binding.hue.internal.Scene; import org.openhab.binding.hue.internal.State; import org.openhab.binding.hue.internal.State.ColorMode; import org.openhab.binding.hue.internal.StateUpdate; +import org.openhab.binding.hue.internal.dto.ColorTemperature; +import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; @@ -59,7 +59,7 @@ import org.slf4j.LoggerFactory; */ @NonNullByDefault public class HueGroupHandler extends BaseThingHandler implements GroupStatusListener { - public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_GROUP); + public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GROUP); private final Logger logger = LoggerFactory.getLogger(HueGroupHandler.class); private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider; @@ -69,6 +69,7 @@ public class HueGroupHandler extends BaseThingHandler implements GroupStatusList private @Nullable Integer lastSentColorTemp; private @Nullable Integer lastSentBrightness; + private ColorTemperature colorTemperatureCapabilties = new ColorTemperature(); private long defaultFadeTime = 400; private @Nullable HueClient hueClient; @@ -76,7 +77,7 @@ public class HueGroupHandler extends BaseThingHandler implements GroupStatusList private @Nullable ScheduledFuture scheduledFuture; private @Nullable FullGroup lastFullGroup; - private List consoleScenesList = new ArrayList<>(); + private List consoleScenesList = List.of(); public HueGroupHandler(Thing thing, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) { super(thing); @@ -201,7 +202,8 @@ public class HueGroupHandler extends BaseThingHandler implements GroupStatusList break; case CHANNEL_COLORTEMPERATURE: if (command instanceof PercentType) { - newState = LightStateConverter.toColorTemperatureLightState((PercentType) command); + newState = LightStateConverter.toColorTemperatureLightStateFromPercentType((PercentType) command, + colorTemperatureCapabilties); newState.setTransitionTime(fadeTime); } else if (command instanceof OnOffType) { newState = LightStateConverter.toOnOffLightState((OnOffType) command); @@ -212,6 +214,13 @@ public class HueGroupHandler extends BaseThingHandler implements GroupStatusList } } break; + case CHANNEL_COLORTEMPERATURE_ABS: + if (command instanceof DecimalType) { + newState = LightStateConverter.toColorTemperatureLightState((DecimalType) command, + colorTemperatureCapabilties); + newState.setTransitionTime(fadeTime); + } + break; case CHANNEL_BRIGHTNESS: if (command instanceof PercentType) { newState = LightStateConverter.toBrightnessLightState((PercentType) command); @@ -228,7 +237,7 @@ public class HueGroupHandler extends BaseThingHandler implements GroupStatusList if (newState != null && lastColorTemp != null) { // make sure that the light also has the latest color temp // this might not have been yet set in the light, if it was off - newState.setColorTemperature(lastColorTemp); + newState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties); newState.setTransitionTime(fadeTime); } break; @@ -240,7 +249,7 @@ public class HueGroupHandler extends BaseThingHandler implements GroupStatusList if (newState != null && lastColorTemp != null) { // make sure that the light also has the latest color temp // this might not have been yet set in the light, if it was off - newState.setColorTemperature(lastColorTemp); + newState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties); newState.setTransitionTime(fadeTime); } break; @@ -296,8 +305,9 @@ public class HueGroupHandler extends BaseThingHandler implements GroupStatusList StateUpdate stateUpdate = null; Integer currentColorTemp = getCurrentColorTemp(group.getState()); if (currentColorTemp != null) { - int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp); - stateUpdate = new StateUpdate().setColorTemperature(newColorTemp); + int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp, + colorTemperatureCapabilties); + stateUpdate = new StateUpdate().setColorTemperature(newColorTemp, colorTemperatureCapabilties); } return stateUpdate; } @@ -380,25 +390,27 @@ public class HueGroupHandler extends BaseThingHandler implements GroupStatusList HSBType hsbType = LightStateConverter.toHSBType(state); if (!state.isOn()) { - hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), new PercentType(0)); + hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), PercentType.ZERO); } updateState(CHANNEL_COLOR, hsbType); ColorMode colorMode = state.getColorMode(); if (ColorMode.CT.equals(colorMode)) { - PercentType colorTempPercentType = LightStateConverter.toColorTemperaturePercentType(state); - updateState(CHANNEL_COLORTEMPERATURE, colorTempPercentType); + updateState(CHANNEL_COLORTEMPERATURE, + LightStateConverter.toColorTemperaturePercentType(state, colorTemperatureCapabilties)); + updateState(CHANNEL_COLORTEMPERATURE_ABS, LightStateConverter.toColorTemperature(state)); } else { - updateState(CHANNEL_COLORTEMPERATURE, UnDefType.NULL); + updateState(CHANNEL_COLORTEMPERATURE, UnDefType.UNDEF); + updateState(CHANNEL_COLORTEMPERATURE_ABS, UnDefType.UNDEF); } PercentType brightnessPercentType = LightStateConverter.toBrightnessPercentType(state); if (!state.isOn()) { - brightnessPercentType = new PercentType(0); + brightnessPercentType = PercentType.ZERO; } updateState(CHANNEL_BRIGHTNESS, brightnessPercentType); - updateState(CHANNEL_SWITCH, state.isOn() ? OnOffType.ON : OnOffType.OFF); + updateState(CHANNEL_SWITCH, OnOffType.from(state.isOn())); return true; } @@ -423,8 +435,8 @@ public class HueGroupHandler extends BaseThingHandler implements GroupStatusList */ @Override public void onScenesUpdated(List updatedScenes) { - List stateOptions = Collections.emptyList(); - consoleScenesList = new ArrayList<>(); + List stateOptions = List.of(); + consoleScenesList = List.of(); HueClient handler = getHueClient(); if (handler != null) { FullGroup group = handler.getGroupById(groupId); diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java index 410fd5539..336791740 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueLightHandler.java @@ -16,18 +16,13 @@ import static org.openhab.binding.hue.internal.HueBindingConstants.*; import static org.openhab.core.thing.Thing.*; import java.math.BigDecimal; -import java.util.AbstractMap.SimpleEntry; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -36,6 +31,9 @@ import org.openhab.binding.hue.internal.State; import org.openhab.binding.hue.internal.State.ColorMode; import org.openhab.binding.hue.internal.StateUpdate; import org.openhab.binding.hue.internal.action.LightActions; +import org.openhab.binding.hue.internal.dto.Capabilities; +import org.openhab.binding.hue.internal.dto.ColorTemperature; +import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; @@ -52,6 +50,8 @@ import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragmentBuilder; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -77,24 +77,20 @@ import org.slf4j.LoggerFactory; @NonNullByDefault public class HueLightHandler extends BaseThingHandler implements LightStatusListener { - public static final Set SUPPORTED_THING_TYPES = Stream.of(THING_TYPE_COLOR_LIGHT, + public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_COLOR_LIGHT, THING_TYPE_COLOR_TEMPERATURE_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT, - THING_TYPE_ON_OFF_LIGHT, THING_TYPE_ON_OFF_PLUG, THING_TYPE_DIMMABLE_PLUG).collect(Collectors.toSet()); + THING_TYPE_ON_OFF_LIGHT, THING_TYPE_ON_OFF_PLUG, THING_TYPE_DIMMABLE_PLUG); - // @formatter:off - private static final Map> VENDOR_MODEL_MAP = Stream.of( - new SimpleEntry<>("Philips", - Arrays.asList("LCT001", "LCT002", "LCT003", "LCT007", "LLC001", "LLC006", "LLC007", "LLC010", - "LLC011", "LLC012", "LLC013", "LLC020", "LST001", "LST002", "LWB004", "LWB006", "LWB007", - "LWL001")), - new SimpleEntry<>("OSRAM", - Arrays.asList("Classic_A60_RGBW", "PAR16_50_TW", "Surface_Light_TW", "Plug_01"))) - .collect(Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue())); - // @formatter:on + private static final Map> VENDOR_MODEL_MAP = Map.of( // + "Philips", List.of("LCT001", "LCT002", "LCT003", "LCT007", "LLC001", "LLC006", "LLC007", "LLC010", // + "LLC011", "LLC012", "LLC013", "LLC020", "LST001", "LST002", "LWB004", "LWB006", "LWB007", // + "LWL001"), + "OSRAM", List.of("Classic_A60_RGBW", "PAR16_50_TW", "Surface_Light_TW", "Plug_01")); private static final String OSRAM_PAR16_50_TW_MODEL_ID = "PAR16_50_TW"; private final Logger logger = LoggerFactory.getLogger(HueLightHandler.class); + private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider; private @NonNullByDefault({}) String lightId; @@ -108,14 +104,17 @@ public class HueLightHandler extends BaseThingHandler implements LightStatusList private boolean isOsramPar16 = false; private boolean propertiesInitializedSuccessfully = false; + private boolean capabilitiesInitializedSuccessfully = false; + private ColorTemperature colorTemperatureCapabilties = new ColorTemperature(); private long defaultFadeTime = 400; private @Nullable HueClient hueClient; private @Nullable ScheduledFuture scheduledFuture; - public HueLightHandler(Thing hueLight) { + public HueLightHandler(Thing hueLight, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) { super(hueLight); + this.stateDescriptionOptionProvider = stateDescriptionOptionProvider; } @Override @@ -145,7 +144,9 @@ public class HueLightHandler extends BaseThingHandler implements LightStatusList HueClient bridgeHandler = getHueClient(); if (bridgeHandler != null) { if (bridgeStatus == ThingStatus.ONLINE) { - initializeProperties(bridgeHandler.getLightById(lightId)); + FullLight fullLight = bridgeHandler.getLightById(lightId); + initializeProperties(fullLight); + initializeCapabilities(fullLight); updateStatus(ThingStatus.ONLINE); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); @@ -187,6 +188,33 @@ public class HueLightHandler extends BaseThingHandler implements LightStatusList } } + private void initializeCapabilities(@Nullable FullLight fullLight) { + if (!capabilitiesInitializedSuccessfully && fullLight != null) { + Capabilities capabilities = fullLight.capabilities; + if (capabilities != null) { + ColorTemperature ct = capabilities.control.ct; + if (ct != null) { + colorTemperatureCapabilties = ct; + + // minimum and maximum are inverted due to mired/Kelvin conversion! + StateDescription stateDescription = StateDescriptionFragmentBuilder.create() + .withMinimum(new BigDecimal(LightStateConverter.miredToKelvin(ct.max))) // + .withMaximum(new BigDecimal(LightStateConverter.miredToKelvin(ct.min))) // + .withStep(new BigDecimal(100)) // + .withPattern("%.0f K") // + .build().toStateDescription(); + if (stateDescription != null) { + stateDescriptionOptionProvider.setDescription( + new ChannelUID(thing.getUID(), CHANNEL_COLORTEMPERATURE_ABS), stateDescription); + } else { + logger.warn("Failed to create state description in thing {}", thing.getUID()); + } + } + } + capabilitiesInitializedSuccessfully = true; + } + } + private @Nullable String getVendor(String modelId) { for (String vendor : VENDOR_MODEL_MAP.keySet()) { if (VENDOR_MODEL_MAP.get(vendor).contains(modelId)) { @@ -235,7 +263,8 @@ public class HueLightHandler extends BaseThingHandler implements LightStatusList switch (channel) { case CHANNEL_COLORTEMPERATURE: if (command instanceof PercentType) { - lightState = LightStateConverter.toColorTemperatureLightState((PercentType) command); + lightState = LightStateConverter.toColorTemperatureLightStateFromPercentType((PercentType) command, + colorTemperatureCapabilties); lightState.setTransitionTime(fadeTime); } else if (command instanceof OnOffType) { lightState = LightStateConverter.toOnOffLightState((OnOffType) command); @@ -248,7 +277,13 @@ public class HueLightHandler extends BaseThingHandler implements LightStatusList lightState.setTransitionTime(fadeTime); } } - + break; + case CHANNEL_COLORTEMPERATURE_ABS: + if (command instanceof DecimalType) { + lightState = LightStateConverter.toColorTemperatureLightState((DecimalType) command, + colorTemperatureCapabilties); + lightState.setTransitionTime(fadeTime); + } break; case CHANNEL_BRIGHTNESS: if (command instanceof PercentType) { @@ -269,7 +304,7 @@ public class HueLightHandler extends BaseThingHandler implements LightStatusList if (lightState != null && lastColorTemp != null) { // make sure that the light also has the latest color temp // this might not have been yet set in the light, if it was off - lightState.setColorTemperature(lastColorTemp); + lightState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties); lightState.setTransitionTime(fadeTime); } break; @@ -285,7 +320,7 @@ public class HueLightHandler extends BaseThingHandler implements LightStatusList if (lightState != null && lastColorTemp != null) { // make sure that the light also has the latest color temp // this might not have been yet set in the light, if it was off - lightState.setColorTemperature(lastColorTemp); + lightState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties); lightState.setTransitionTime(fadeTime); } break; @@ -365,8 +400,9 @@ public class HueLightHandler extends BaseThingHandler implements LightStatusList StateUpdate stateUpdate = null; Integer currentColorTemp = getCurrentColorTemp(light.getState()); if (currentColorTemp != null) { - int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp); - stateUpdate = new StateUpdate().setColorTemperature(newColorTemp); + int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp, + colorTemperatureCapabilties); + stateUpdate = new StateUpdate().setColorTemperature(newColorTemp, colorTemperatureCapabilties); } return stateUpdate; } @@ -482,29 +518,27 @@ public class HueLightHandler extends BaseThingHandler implements LightStatusList HSBType hsbType = LightStateConverter.toHSBType(state); if (!state.isOn()) { - hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), new PercentType(0)); + hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), PercentType.ZERO); } updateState(CHANNEL_COLOR, hsbType); ColorMode colorMode = state.getColorMode(); if (ColorMode.CT.equals(colorMode)) { - PercentType colorTempPercentType = LightStateConverter.toColorTemperaturePercentType(state); - updateState(CHANNEL_COLORTEMPERATURE, colorTempPercentType); + updateState(CHANNEL_COLORTEMPERATURE, + LightStateConverter.toColorTemperaturePercentType(state, colorTemperatureCapabilties)); + updateState(CHANNEL_COLORTEMPERATURE_ABS, LightStateConverter.toColorTemperature(state)); } else { - updateState(CHANNEL_COLORTEMPERATURE, UnDefType.NULL); + updateState(CHANNEL_COLORTEMPERATURE, UnDefType.UNDEF); + updateState(CHANNEL_COLORTEMPERATURE_ABS, UnDefType.UNDEF); } PercentType brightnessPercentType = LightStateConverter.toBrightnessPercentType(state); if (!state.isOn()) { - brightnessPercentType = new PercentType(0); + brightnessPercentType = PercentType.ZERO; } updateState(CHANNEL_BRIGHTNESS, brightnessPercentType); - if (state.isOn()) { - updateState(CHANNEL_SWITCH, OnOffType.ON); - } else { - updateState(CHANNEL_SWITCH, OnOffType.OFF); - } + updateState(CHANNEL_SWITCH, OnOffType.from(state.isOn())); StringType stringType = LightStateConverter.toAlertStringType(state); if (!"NULL".equals(stringType.toString())) { @@ -609,7 +643,7 @@ public class HueLightHandler extends BaseThingHandler implements LightStatusList @Override public Collection> getServices() { - return Collections.singletonList(LightActions.class); + return List.of(LightActions.class); } @Override diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueStateDescriptionOptionProvider.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueStateDescriptionOptionProvider.java index 2f43f88b7..4801c59d6 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueStateDescriptionOptionProvider.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueStateDescriptionOptionProvider.java @@ -12,10 +12,19 @@ */ package org.openhab.binding.hue.internal.handler; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider; import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.openhab.core.types.StateDescription; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -29,14 +38,23 @@ import org.osgi.service.component.annotations.Reference; @NonNullByDefault public class HueStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider { - @Reference - protected void setChannelTypeI18nLocalizationService( - final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + private final Map descriptions = new ConcurrentHashMap<>(); + + @Activate + public HueStateDescriptionOptionProvider( + final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; } - protected void unsetChannelTypeI18nLocalizationService( - final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { - this.channelTypeI18nLocalizationService = null; + public void setDescription(ChannelUID channelUID, StateDescription description) { + descriptions.put(channelUID, description); + } + + @Override + public @Nullable StateDescription getStateDescription(Channel channel, + @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { + StateDescription stateDescription = descriptions.get(channel.getUID()); + return stateDescription != null ? stateDescription + : super.getStateDescription(channel, originalStateDescription, locale); } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/LightStateConverter.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/LightStateConverter.java index 3a45f732f..4057df83a 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/LightStateConverter.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/LightStateConverter.java @@ -19,6 +19,7 @@ import org.openhab.binding.hue.internal.State.AlertMode; import org.openhab.binding.hue.internal.State.ColorMode; import org.openhab.binding.hue.internal.State.Effect; import org.openhab.binding.hue.internal.StateUpdate; +import org.openhab.binding.hue.internal.dto.ColorTemperature; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.IncreaseDecreaseType; @@ -44,10 +45,6 @@ public class LightStateConverter { private static final double SATURATION_FACTOR = 2.54; private static final double BRIGHTNESS_FACTOR = 2.54; - private static final int MIN_COLOR_TEMPERATURE = 153; - private static final int MAX_COLOR_TEMPERATURE = 500; - private static final int COLOR_TEMPERATURE_RANGE = MAX_COLOR_TEMPERATURE - MIN_COLOR_TEMPERATURE; - /** * {@value #ALERT_MODE_NONE}. The light is not performing an alert effect. */ @@ -150,10 +147,26 @@ public class LightStateConverter { * @param percentType color temperature represented as {@link PercentType} * @return light state containing the color temperature */ - public static StateUpdate toColorTemperatureLightState(PercentType percentType) { - int colorTemperature = MIN_COLOR_TEMPERATURE - + Math.round((COLOR_TEMPERATURE_RANGE * percentType.floatValue()) / 100); - return new StateUpdate().setColorTemperature(colorTemperature); + public static StateUpdate toColorTemperatureLightStateFromPercentType(PercentType percentType, + ColorTemperature capabilities) { + int colorTemperature = capabilities.min + + Math.round(((capabilities.max - capabilities.min) * percentType.floatValue()) / 100); + return new StateUpdate().setColorTemperature(colorTemperature, capabilities); + } + + public static int kelvinToMired(int kelvinValue) { + return (int) (1000000.0 / kelvinValue); + } + + /** + * Transforms the given {@link DecimalType} into a light state containing + * the color temperature in Kelvin. + * + * @param decimalType color temperature in Kelvin + * @return light state containing the color temperature + */ + public static StateUpdate toColorTemperatureLightState(DecimalType decimalType, ColorTemperature capabilities) { + return new StateUpdate().setColorTemperature(kelvinToMired(decimalType.intValue()), capabilities); } /** @@ -163,12 +176,13 @@ public class LightStateConverter { * @param currentColorTemp The current color temperature * @return The adjusted color temperature value */ - public static int toAdjustedColorTemp(IncreaseDecreaseType type, int currentColorTemp) { + public static int toAdjustedColorTemp(IncreaseDecreaseType type, int currentColorTemp, + ColorTemperature capabilities) { int newColorTemp; if (type == IncreaseDecreaseType.DECREASE) { - newColorTemp = Math.max(currentColorTemp - DIM_STEPSIZE, MIN_COLOR_TEMPERATURE); + newColorTemp = Math.max(currentColorTemp - DIM_STEPSIZE, capabilities.min); } else { - newColorTemp = Math.min(currentColorTemp + DIM_STEPSIZE, MAX_COLOR_TEMPERATURE); + newColorTemp = Math.min(currentColorTemp + DIM_STEPSIZE, capabilities.max); } return newColorTemp; } @@ -180,12 +194,27 @@ public class LightStateConverter { * @param lightState light state * @return percent type representing the color temperature */ - public static PercentType toColorTemperaturePercentType(State lightState) { - int percent = (int) Math - .round(((lightState.getColorTemperature() - MIN_COLOR_TEMPERATURE) * 100.0) / COLOR_TEMPERATURE_RANGE); + public static PercentType toColorTemperaturePercentType(State lightState, ColorTemperature capabilities) { + int percent = (int) Math.round(((lightState.getColorTemperature() - capabilities.min) * 100.0) + / (capabilities.max - capabilities.min)); return new PercentType(restrictToBounds(percent)); } + public static int miredToKelvin(int miredValue) { + return (int) (1000000.0 / miredValue); + } + + /** + * Transforms Hue Light {@link State} into {@link DecimalType} representing + * the color temperature in Kelvin. + * + * @param lightState light state + * @return percent type representing the color temperature in Kelvin + */ + public static DecimalType toColorTemperature(State lightState) { + return new DecimalType(miredToKelvin(lightState.getColorTemperature())); + } + /** * Transforms Hue Light {@link State} into {@link PercentType} representing * the brightness. diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/ColorTemperatureLight.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/ColorTemperatureLight.xml index e1b759eb7..7bd7ea70f 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/ColorTemperatureLight.xml +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/ColorTemperatureLight.xml @@ -14,6 +14,7 @@ + diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/ExtendedColorLight.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/ExtendedColorLight.xml index a4b4363df..6d64a96ee 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/ExtendedColorLight.xml +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/ExtendedColorLight.xml @@ -15,6 +15,7 @@ + diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/Group.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/Group.xml index a863b9d27..3078bd664 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/Group.xml +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/Group.xml @@ -15,6 +15,7 @@ + diff --git a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/LightStateConverterTest.java b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/LightStateConverterTest.java index 7d7cfcf36..9d151d7eb 100644 --- a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/LightStateConverterTest.java +++ b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/LightStateConverterTest.java @@ -14,10 +14,12 @@ package org.openhab.binding.hue.internal; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; import org.openhab.binding.hue.internal.State.ColorMode; +import org.openhab.binding.hue.internal.dto.ColorTemperature; import org.openhab.binding.hue.internal.handler.LightStateConverter; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; @@ -31,6 +33,38 @@ import org.openhab.core.library.types.PercentType; */ public class LightStateConverterTest { + @Test + public void colorTemperatureLightStateConverterConversionIsBijectiveDefaultColorTemperatureCapabilities() { + final State lightState = new State(); + final ColorTemperature colorTemperature = new ColorTemperature(); + for (int percent = 1; percent <= 100; ++percent) { + StateUpdate stateUpdate = LightStateConverter + .toColorTemperatureLightStateFromPercentType(new PercentType(percent), colorTemperature); + assertThat(stateUpdate.commands, hasSize(1)); + assertThat(stateUpdate.commands.get(0).key, is("ct")); + lightState.ct = Integer.parseInt(stateUpdate.commands.get(0).value.toString()); + assertThat(LightStateConverter.toColorTemperaturePercentType(lightState, colorTemperature).intValue(), + is(percent)); + } + } + + @Test + public void colorTemperatureLightStateConverterConversionIsBijectiveIndividualColorTemperatureCapabilities() { + final State lightState = new State(); + final ColorTemperature colorTemperature = new ColorTemperature(); + colorTemperature.min = 250; + colorTemperature.max = 454; + for (int percent = 1; percent <= 100; ++percent) { + StateUpdate stateUpdate = LightStateConverter + .toColorTemperatureLightStateFromPercentType(new PercentType(percent), colorTemperature); + assertThat(stateUpdate.commands, hasSize(1)); + assertThat(stateUpdate.commands.get(0).key, is("ct")); + lightState.ct = Integer.parseInt(stateUpdate.commands.get(0).value.toString()); + assertThat(LightStateConverter.toColorTemperaturePercentType(lightState, colorTemperature).intValue(), + is(percent)); + } + } + @Test public void brightnessOfZeroIsZero() { final State lightState = new State(); diff --git a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/handler/HueLightHandlerTest.java b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/handler/HueLightHandlerTest.java index 0a21801e1..2d62bbe8a 100644 --- a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/handler/HueLightHandlerTest.java +++ b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/handler/HueLightHandlerTest.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.hue.internal.handler; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.openhab.binding.hue.internal.HueBindingConstants.*; @@ -27,6 +27,7 @@ import org.openhab.binding.hue.internal.FullLight; import org.openhab.binding.hue.internal.State.ColorMode; import org.openhab.binding.hue.internal.StateUpdate; import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; @@ -37,6 +38,7 @@ import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; import org.openhab.core.types.Command; import com.google.gson.Gson; @@ -136,6 +138,24 @@ public class HueLightHandlerTest { assertSendCommandForColorTemp(new PercentType(100), new HueLightState(), expectedReply); } + @Test + public void assertCommandForColorTemperatureAbsChannel6500Kelvin() { + String expectedReply = "{\"ct\" : 153, \"transitiontime\" : 4}"; + assertSendCommandForColorTempAbs(new DecimalType(6500), new HueLightState(), expectedReply); + } + + @Test + public void assertCommandForColorTemperatureAbsChannel4500Kelvin() { + String expectedReply = "{\"ct\" : 222, \"transitiontime\" : 4}"; + assertSendCommandForColorTempAbs(new DecimalType(4500), new HueLightState(), expectedReply); + } + + @Test + public void assertCommandForColorTemperatureAbsChannel2000Kelvin() { + String expectedReply = "{\"ct\" : 500, \"transitiontime\" : 4}"; + assertSendCommandForColorTempAbs(new DecimalType(2000), new HueLightState(), expectedReply); + } + @Test public void assertPercentageValueOfColorTemperatureWhenCt153() { int expectedReply = 0; @@ -337,6 +357,10 @@ public class HueLightHandlerTest { assertSendCommand(CHANNEL_COLORTEMPERATURE, command, currentState, expectedReply); } + private void assertSendCommandForColorTempAbs(Command command, HueLightState currentState, String expectedReply) { + assertSendCommand(CHANNEL_COLORTEMPERATURE_ABS, command, currentState, expectedReply); + } + private void asserttoColorTemperaturePercentType(int ctValue, int expectedPercent) { int percent = (int) Math.round(((ctValue - MIN_COLOR_TEMPERATURE) * 100.0) / COLOR_TEMPERATURE_RANGE); assertEquals(percent, expectedPercent); @@ -373,7 +397,8 @@ public class HueLightHandlerTest { long fadeTime = 400; - HueLightHandler hueLightHandler = new HueLightHandler(mockThing) { + HueLightHandler hueLightHandler = new HueLightHandler(mockThing, + new HueStateDescriptionOptionProvider(mock(ChannelTypeI18nLocalizationService.class))) { @Override protected synchronized HueClient getHueClient() { return mockClient;