2 * Copyright (c) 2010-2022 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.handler;
15 import static org.openhab.binding.hue.internal.HueBindingConstants.*;
16 import static org.openhab.core.thing.Thing.*;
18 import java.math.BigDecimal;
20 import java.util.Objects;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.hue.internal.dto.Capabilities;
28 import org.openhab.binding.hue.internal.dto.ColorTemperature;
29 import org.openhab.binding.hue.internal.dto.FullLight;
30 import org.openhab.binding.hue.internal.dto.State;
31 import org.openhab.binding.hue.internal.dto.StateUpdate;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.HSBType;
34 import org.openhab.core.library.types.IncreaseDecreaseType;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.PercentType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.thing.Bridge;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.ThingStatusInfo;
44 import org.openhab.core.thing.ThingTypeUID;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.thing.binding.ThingHandler;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.StateDescriptionFragment;
49 import org.openhab.core.types.StateDescriptionFragmentBuilder;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * {@link HueLightHandler} is the handler for a Hue light. It uses the {@link HueClient} to execute the actual
57 * @author Dennis Nobel - Initial contribution
58 * @author Oliver Libutzki - Adjustments
59 * @author Kai Kreuzer - stabilized code
60 * @author Andre Fuechsel - implemented switch off when brightness == 0, changed to support generic thing types, changed
61 * the initialization of properties
62 * @author Thomas Höfer - added thing properties
63 * @author Jochen Hiller - fixed status updates for reachable=true/false
64 * @author Markus Mazurczak - added code for command handling of OSRAM PAR16 50
66 * @author Yordan Zhelev - added alert and effect functions
67 * @author Denis Dudnik - switched to internally integrated source of Jue library
68 * @author Christoph Weitkamp - Added support for bulbs using CIE XY colormode only
69 * @author Jochen Leopold - Added support for custom fade times
70 * @author Jacob Laursen - Add workaround for LK Wiser products
73 public class HueLightHandler extends BaseThingHandler implements HueLightActionsHandler, LightStatusListener {
75 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_COLOR_LIGHT,
76 THING_TYPE_COLOR_TEMPERATURE_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT,
77 THING_TYPE_ON_OFF_LIGHT, THING_TYPE_ON_OFF_PLUG, THING_TYPE_DIMMABLE_PLUG);
79 public static final String OSRAM_PAR16_50_TW_MODEL_ID = "PAR16_50_TW";
80 public static final String LK_WISER_MODEL_ID = "LK_Dimmer";
82 private final Logger logger = LoggerFactory.getLogger(HueLightHandler.class);
84 private final HueStateDescriptionProvider stateDescriptionProvider;
86 private @NonNullByDefault({}) String lightId;
88 private @Nullable FullLight lastFullLight;
89 private long endBypassTime = 0L;
91 private @Nullable Integer lastSentColorTemp;
92 private @Nullable Integer lastSentBrightness;
95 * Flag to indicate whether the bulb is of type Osram par16 50 TW
97 private boolean isOsramPar16 = false;
99 * Flag to indicate whether the dimmer/relay is of type LK Wiser by Schneider Electric
101 private boolean isLkWiser = false;
103 private boolean propertiesInitializedSuccessfully = false;
104 private boolean capabilitiesInitializedSuccessfully = false;
105 private ColorTemperature colorTemperatureCapabilties = new ColorTemperature();
106 private long defaultFadeTime = 400;
108 private @Nullable HueClient hueClient;
110 private @Nullable ScheduledFuture<?> scheduledFuture;
112 public HueLightHandler(Thing hueLight, HueStateDescriptionProvider stateDescriptionProvider) {
114 this.stateDescriptionProvider = stateDescriptionProvider;
118 public void initialize() {
119 logger.debug("Initializing Hue light handler.");
120 Bridge bridge = getBridge();
121 initializeThing((bridge == null) ? null : bridge.getStatus());
125 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
126 logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
127 initializeThing(bridgeStatusInfo.getStatus());
130 private void initializeThing(@Nullable ThingStatus bridgeStatus) {
131 logger.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus);
132 final String configLightId = (String) getConfig().get(LIGHT_ID);
133 if (configLightId != null) {
134 BigDecimal time = (BigDecimal) getConfig().get(FADETIME);
136 defaultFadeTime = time.longValueExact();
139 lightId = configLightId;
140 // note: this call implicitly registers our handler as a listener on the bridge
141 HueClient bridgeHandler = getHueClient();
142 if (bridgeHandler != null) {
143 if (bridgeStatus == ThingStatus.ONLINE) {
144 FullLight fullLight = bridgeHandler.getLightById(lightId);
145 initializeProperties(fullLight);
146 initializeCapabilities(fullLight);
147 updateStatus(ThingStatus.ONLINE);
149 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
155 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
156 "@text/offline.conf-error-no-light-id");
160 private synchronized void initializeProperties(@Nullable FullLight fullLight) {
161 if (!propertiesInitializedSuccessfully && fullLight != null) {
162 Map<String, String> properties = editProperties();
163 String softwareVersion = fullLight.getSoftwareVersion();
164 if (softwareVersion != null) {
165 properties.put(PROPERTY_FIRMWARE_VERSION, softwareVersion);
167 String modelId = fullLight.getNormalizedModelID();
168 if (modelId != null) {
169 properties.put(PROPERTY_MODEL_ID, modelId);
172 case OSRAM_PAR16_50_TW_MODEL_ID:
175 case LK_WISER_MODEL_ID:
180 properties.put(PROPERTY_VENDOR, fullLight.getManufacturerName());
181 properties.put(PRODUCT_NAME, fullLight.getProductName());
182 String uniqueID = fullLight.getUniqueID();
183 if (uniqueID != null) {
184 properties.put(UNIQUE_ID, uniqueID);
186 updateProperties(properties);
187 propertiesInitializedSuccessfully = true;
191 private void initializeCapabilities(@Nullable FullLight fullLight) {
192 if (!capabilitiesInitializedSuccessfully && fullLight != null) {
193 Capabilities capabilities = fullLight.capabilities;
194 if (capabilities != null) {
195 ColorTemperature ct = capabilities.control.ct;
197 colorTemperatureCapabilties = ct;
199 // minimum and maximum are inverted due to mired/Kelvin conversion!
200 StateDescriptionFragment stateDescriptionFragment = StateDescriptionFragmentBuilder.create()
201 .withMinimum(new BigDecimal(LightStateConverter.miredToKelvin(ct.max))) //
202 .withMaximum(new BigDecimal(LightStateConverter.miredToKelvin(ct.min))) //
203 .withStep(new BigDecimal(100)) //
204 .withPattern("%.0f K") //
206 stateDescriptionProvider.setStateDescriptionFragment(
207 new ChannelUID(thing.getUID(), CHANNEL_COLORTEMPERATURE_ABS), stateDescriptionFragment);
210 capabilitiesInitializedSuccessfully = true;
215 public void dispose() {
216 logger.debug("Hue light handler disposes. Unregistering listener.");
217 cancelScheduledFuture();
218 if (lightId != null) {
219 HueClient bridgeHandler = getHueClient();
220 if (bridgeHandler != null) {
221 bridgeHandler.unregisterLightStatusListener(this);
229 public void handleCommand(ChannelUID channelUID, Command command) {
230 handleCommand(channelUID.getId(), command, defaultFadeTime);
234 public void handleCommand(String channel, Command command, long fadeTime) {
235 HueClient bridgeHandler = getHueClient();
236 if (bridgeHandler == null) {
237 logger.warn("Hue Bridge handler not found. Cannot handle command without bridge.");
241 final FullLight light = lastFullLight == null ? bridgeHandler.getLightById(lightId) : lastFullLight;
243 logger.debug("Hue light not known on bridge. Cannot handle command.");
244 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
245 "@text/offline.conf-error-wrong-light-id");
249 Integer lastColorTemp;
250 StateUpdate newState = null;
252 case CHANNEL_COLORTEMPERATURE:
253 if (command instanceof PercentType) {
254 newState = LightStateConverter.toColorTemperatureLightStateFromPercentType((PercentType) command,
255 colorTemperatureCapabilties);
256 newState.setTransitionTime(fadeTime);
257 } else if (command instanceof OnOffType) {
258 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
260 newState = addOsramSpecificCommands(newState, (OnOffType) command);
262 } else if (command instanceof IncreaseDecreaseType) {
263 newState = convertColorTempChangeToStateUpdate((IncreaseDecreaseType) command, light);
264 if (newState != null) {
265 newState.setTransitionTime(fadeTime);
269 case CHANNEL_COLORTEMPERATURE_ABS:
270 if (command instanceof DecimalType) {
271 newState = LightStateConverter.toColorTemperatureLightState((DecimalType) command,
272 colorTemperatureCapabilties);
273 newState.setTransitionTime(fadeTime);
276 case CHANNEL_BRIGHTNESS:
277 if (command instanceof PercentType) {
278 newState = LightStateConverter.toBrightnessLightState((PercentType) command);
279 newState.setTransitionTime(fadeTime);
280 } else if (command instanceof OnOffType) {
281 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
283 newState = addOsramSpecificCommands(newState, (OnOffType) command);
284 } else if (isLkWiser) {
285 newState = addLkWiserSpecificCommands(newState, (OnOffType) command);
287 } else if (command instanceof IncreaseDecreaseType) {
288 newState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
289 if (newState != null) {
290 newState.setTransitionTime(fadeTime);
293 lastColorTemp = lastSentColorTemp;
294 if (newState != null && lastColorTemp != null) {
295 // make sure that the light also has the latest color temp
296 // this might not have been yet set in the light, if it was off
297 newState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
298 newState.setTransitionTime(fadeTime);
302 if (command instanceof OnOffType) {
303 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
305 newState = addOsramSpecificCommands(newState, (OnOffType) command);
306 } else if (isLkWiser) {
307 newState = addLkWiserSpecificCommands(newState, (OnOffType) command);
310 lastColorTemp = lastSentColorTemp;
311 if (newState != null && lastColorTemp != null) {
312 // make sure that the light also has the latest color temp
313 // this might not have been yet set in the light, if it was off
314 newState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
315 newState.setTransitionTime(fadeTime);
319 if (command instanceof HSBType) {
320 HSBType hsbCommand = (HSBType) command;
321 if (hsbCommand.getBrightness().intValue() == 0) {
322 newState = LightStateConverter.toOnOffLightState(OnOffType.OFF);
324 newState = LightStateConverter.toColorLightState(hsbCommand, light.getState());
325 newState.setTransitionTime(fadeTime);
327 } else if (command instanceof PercentType) {
328 newState = LightStateConverter.toBrightnessLightState((PercentType) command);
329 newState.setTransitionTime(fadeTime);
330 } else if (command instanceof OnOffType) {
331 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
332 } else if (command instanceof IncreaseDecreaseType) {
333 newState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
334 if (newState != null) {
335 newState.setTransitionTime(fadeTime);
340 if (command instanceof StringType) {
341 newState = LightStateConverter.toAlertState((StringType) command);
342 if (newState == null) {
343 // Unsupported StringType is passed. Log a warning
344 // message and return.
345 logger.warn("Unsupported String command: {}. Supported commands are: {}, {}, {} ", command,
346 LightStateConverter.ALERT_MODE_NONE, LightStateConverter.ALERT_MODE_SELECT,
347 LightStateConverter.ALERT_MODE_LONG_SELECT);
350 scheduleAlertStateRestore(command);
355 if (command instanceof OnOffType) {
356 newState = LightStateConverter.toOnOffEffectState((OnOffType) command);
360 if (newState != null) {
361 // Cache values which we have sent
362 Integer tmpBrightness = newState.getBrightness();
363 if (tmpBrightness != null) {
364 lastSentBrightness = tmpBrightness;
366 Integer tmpColorTemp = newState.getColorTemperature();
367 if (tmpColorTemp != null) {
368 lastSentColorTemp = tmpColorTemp;
370 bridgeHandler.updateLightState(this, light, newState, fadeTime);
372 logger.warn("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel);
377 * Applies additional {@link StateUpdate} commands as a workaround for Osram
378 * Lightify PAR16 TW firmware bug. Also see
379 * http://www.everyhue.com/vanilla/discussion/1756/solved-lightify-turning-off
381 private StateUpdate addOsramSpecificCommands(StateUpdate lightState, OnOffType actionType) {
382 if (actionType.equals(OnOffType.ON)) {
383 lightState.setBrightness(254);
385 lightState.setTransitionTime(0);
391 * Applies additional {@link StateUpdate} commands as a workaround for LK Wiser
392 * Dimmer/Relay firmware bug. Additional details here:
393 * https://techblog.vindvejr.dk/?p=455
395 private StateUpdate addLkWiserSpecificCommands(StateUpdate lightState, OnOffType actionType) {
396 if (actionType.equals(OnOffType.OFF)) {
397 lightState.setTransitionTime(0);
402 private @Nullable StateUpdate convertColorTempChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
403 StateUpdate stateUpdate = null;
404 Integer currentColorTemp = getCurrentColorTemp(light.getState());
405 if (currentColorTemp != null) {
406 int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp,
407 colorTemperatureCapabilties);
408 stateUpdate = new StateUpdate().setColorTemperature(newColorTemp, colorTemperatureCapabilties);
413 private @Nullable Integer getCurrentColorTemp(@Nullable State lightState) {
414 Integer colorTemp = lastSentColorTemp;
415 if (colorTemp == null && lightState != null) {
416 return lightState.getColorTemperature();
421 private @Nullable StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
422 Integer currentBrightness = getCurrentBrightness(light.getState());
423 if (currentBrightness == null) {
426 int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness);
427 return createBrightnessStateUpdate(currentBrightness, newBrightness);
430 private @Nullable Integer getCurrentBrightness(@Nullable State lightState) {
431 if (lastSentBrightness == null && lightState != null) {
432 return lightState.isOn() ? lightState.getBrightness() : 0;
434 return lastSentBrightness;
437 private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) {
438 StateUpdate lightUpdate = new StateUpdate();
439 if (newBrightness == 0) {
440 lightUpdate.turnOff();
442 lightUpdate.setBrightness(newBrightness);
443 if (currentBrightness == 0) {
444 lightUpdate.turnOn();
450 protected synchronized @Nullable HueClient getHueClient() {
451 if (hueClient == null) {
452 Bridge bridge = getBridge();
453 if (bridge == null) {
456 ThingHandler handler = bridge.getHandler();
457 if (handler instanceof HueClient) {
458 HueClient bridgeHandler = (HueClient) handler;
459 hueClient = bridgeHandler;
460 bridgeHandler.registerLightStatusListener(this);
469 public void setPollBypass(long bypassTime) {
470 endBypassTime = System.currentTimeMillis() + bypassTime;
474 public void unsetPollBypass() {
479 public boolean onLightStateChanged(FullLight fullLight) {
480 logger.trace("onLightStateChanged() was called");
482 if (System.currentTimeMillis() <= endBypassTime) {
483 logger.debug("Bypass light update after command ({}).", lightId);
487 State state = fullLight.getState();
489 final FullLight lastState = lastFullLight;
490 if (lastState == null || !Objects.equals(lastState.getState(), state)) {
491 lastFullLight = fullLight;
496 logger.trace("New state for light {}", lightId);
498 initializeProperties(fullLight);
500 lastSentColorTemp = null;
501 lastSentBrightness = null;
503 // update status (ONLINE, OFFLINE)
504 if (state.isReachable()) {
505 updateStatus(ThingStatus.ONLINE);
507 // we assume OFFLINE without any error (NONE), as this is an
508 // expected state (when bulb powered off)
509 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-not-reachable");
512 logger.debug("onLightStateChanged Light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
513 fullLight.getName(), state.isOn(), state.getBrightness(), state.getHue(), state.getSaturation(),
514 state.getColorTemperature(), state.getColorMode(), state.getXY());
516 HSBType hsbType = LightStateConverter.toHSBType(state);
518 hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), PercentType.ZERO);
520 updateState(CHANNEL_COLOR, hsbType);
522 PercentType brightnessPercentType = state.isOn() ? LightStateConverter.toBrightnessPercentType(state)
524 updateState(CHANNEL_BRIGHTNESS, brightnessPercentType);
526 updateState(CHANNEL_SWITCH, OnOffType.from(state.isOn()));
528 updateState(CHANNEL_COLORTEMPERATURE,
529 LightStateConverter.toColorTemperaturePercentType(state, colorTemperatureCapabilties));
530 updateState(CHANNEL_COLORTEMPERATURE_ABS, LightStateConverter.toColorTemperature(state));
532 StringType stringType = LightStateConverter.toAlertStringType(state);
533 if (!"NULL".equals(stringType.toString())) {
534 updateState(CHANNEL_ALERT, stringType);
535 scheduleAlertStateRestore(stringType);
542 public void channelLinked(ChannelUID channelUID) {
543 HueClient handler = getHueClient();
544 if (handler != null) {
545 FullLight light = handler.getLightById(lightId);
547 onLightStateChanged(light);
553 public void onLightRemoved() {
554 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-removed");
558 public void onLightGone() {
559 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.light-not-reachable");
563 public void onLightAdded(FullLight light) {
564 onLightStateChanged(light);
568 * Schedules restoration of the alert item state to {@link LightStateConverter#ALERT_MODE_NONE} after a given time.
570 * Based on the initial command:
572 * <li>For {@link LightStateConverter#ALERT_MODE_SELECT} restoration will be triggered after <strong>2
574 * <li>For {@link LightStateConverter#ALERT_MODE_LONG_SELECT} restoration will be triggered after <strong>15
577 * This method also cancels any previously scheduled restoration.
579 * @param command The {@link Command} sent to the item
581 private void scheduleAlertStateRestore(Command command) {
582 cancelScheduledFuture();
583 int delay = getAlertDuration(command);
586 scheduledFuture = scheduler.schedule(() -> {
587 updateState(CHANNEL_ALERT, new StringType(LightStateConverter.ALERT_MODE_NONE));
588 }, delay, TimeUnit.MILLISECONDS);
593 * This method will cancel previously scheduled alert item state
596 private void cancelScheduledFuture() {
597 ScheduledFuture<?> scheduledJob = scheduledFuture;
598 if (scheduledJob != null) {
599 scheduledJob.cancel(true);
600 scheduledFuture = null;
605 * This method returns the time in <strong>milliseconds</strong> after
606 * which, the state of the alert item has to be restored to {@link LightStateConverter#ALERT_MODE_NONE}.
608 * @param command The initial command sent to the alert item.
609 * @return Based on the initial command will return:
611 * <li><strong>2000</strong> for {@link LightStateConverter#ALERT_MODE_SELECT}.
612 * <li><strong>15000</strong> for {@link LightStateConverter#ALERT_MODE_LONG_SELECT}.
613 * <li><strong>-1</strong> for any command different from the previous two.
616 private int getAlertDuration(Command command) {
618 switch (command.toString()) {
619 case LightStateConverter.ALERT_MODE_LONG_SELECT:
622 case LightStateConverter.ALERT_MODE_SELECT:
634 public String getLightId() {