2 * Copyright (c) 2010-2021 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;
19 import java.util.Collection;
20 import java.util.List;
22 import java.util.Objects;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.hue.internal.FullLight;
30 import org.openhab.binding.hue.internal.State;
31 import org.openhab.binding.hue.internal.State.ColorMode;
32 import org.openhab.binding.hue.internal.StateUpdate;
33 import org.openhab.binding.hue.internal.action.LightActions;
34 import org.openhab.binding.hue.internal.dto.Capabilities;
35 import org.openhab.binding.hue.internal.dto.ColorTemperature;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.HSBType;
38 import org.openhab.core.library.types.IncreaseDecreaseType;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.PercentType;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.ThingStatusInfo;
48 import org.openhab.core.thing.ThingTypeUID;
49 import org.openhab.core.thing.binding.BaseThingHandler;
50 import org.openhab.core.thing.binding.ThingHandler;
51 import org.openhab.core.thing.binding.ThingHandlerService;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.StateDescription;
54 import org.openhab.core.types.StateDescriptionFragmentBuilder;
55 import org.openhab.core.types.UnDefType;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
60 * {@link HueLightHandler} is the handler for a hue light. It uses the {@link HueClient} to execute the actual
63 * @author Dennis Nobel - Initial contribution
64 * @author Oliver Libutzki - Adjustments
65 * @author Kai Kreuzer - stabilized code
66 * @author Andre Fuechsel - implemented switch off when brightness == 0, changed to support generic thing types, changed
67 * the initialization of properties
68 * @author Thomas Höfer - added thing properties
69 * @author Jochen Hiller - fixed status updates for reachable=true/false
70 * @author Markus Mazurczak - added code for command handling of OSRAM PAR16 50
72 * @author Yordan Zhelev - added alert and effect functions
73 * @author Denis Dudnik - switched to internally integrated source of Jue library
74 * @author Christoph Weitkamp - Added support for bulbs using CIE XY colormode only
75 * @author Jochen Leopold - Added support for custom fade times
78 public class HueLightHandler extends BaseThingHandler implements LightStatusListener {
80 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_COLOR_LIGHT,
81 THING_TYPE_COLOR_TEMPERATURE_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT,
82 THING_TYPE_ON_OFF_LIGHT, THING_TYPE_ON_OFF_PLUG, THING_TYPE_DIMMABLE_PLUG);
84 private static final Map<String, List<String>> VENDOR_MODEL_MAP = Map.of( //
85 "Philips", List.of("LCT001", "LCT002", "LCT003", "LCT007", "LLC001", "LLC006", "LLC007", "LLC010", //
86 "LLC011", "LLC012", "LLC013", "LLC020", "LST001", "LST002", "LWB004", "LWB006", "LWB007", //
88 "OSRAM", List.of("Classic_A60_RGBW", "PAR16_50_TW", "Surface_Light_TW", "Plug_01"));
90 private static final String OSRAM_PAR16_50_TW_MODEL_ID = "PAR16_50_TW";
92 private final Logger logger = LoggerFactory.getLogger(HueLightHandler.class);
93 private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider;
95 private @NonNullByDefault({}) String lightId;
97 private @Nullable FullLight lastFullLight;
98 private long endBypassTime = 0L;
100 private @Nullable Integer lastSentColorTemp;
101 private @Nullable Integer lastSentBrightness;
103 // Flag to indicate whether the bulb is of type Osram par16 50 TW or not
104 private boolean isOsramPar16 = false;
106 private boolean propertiesInitializedSuccessfully = false;
107 private boolean capabilitiesInitializedSuccessfully = false;
108 private ColorTemperature colorTemperatureCapabilties = new ColorTemperature();
109 private long defaultFadeTime = 400;
111 private @Nullable HueClient hueClient;
113 private @Nullable ScheduledFuture<?> scheduledFuture;
115 public HueLightHandler(Thing hueLight, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) {
117 this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
121 public void initialize() {
122 logger.debug("Initializing hue light handler.");
123 Bridge bridge = getBridge();
124 initializeThing((bridge == null) ? null : bridge.getStatus());
128 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
129 logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
130 initializeThing(bridgeStatusInfo.getStatus());
133 private void initializeThing(@Nullable ThingStatus bridgeStatus) {
134 logger.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus);
135 final String configLightId = (String) getConfig().get(LIGHT_ID);
136 if (configLightId != null) {
137 BigDecimal time = (BigDecimal) getConfig().get(FADETIME);
139 defaultFadeTime = time.longValueExact();
142 lightId = configLightId;
143 // note: this call implicitly registers our handler as a listener on the bridge
144 HueClient bridgeHandler = getHueClient();
145 if (bridgeHandler != null) {
146 if (bridgeStatus == ThingStatus.ONLINE) {
147 FullLight fullLight = bridgeHandler.getLightById(lightId);
148 initializeProperties(fullLight);
149 initializeCapabilities(fullLight);
150 updateStatus(ThingStatus.ONLINE);
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
155 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
158 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
159 "@text/offline.conf-error-no-light-id");
163 private synchronized void initializeProperties(@Nullable FullLight fullLight) {
164 if (!propertiesInitializedSuccessfully && fullLight != null) {
165 Map<String, String> properties = editProperties();
166 String softwareVersion = fullLight.getSoftwareVersion();
167 if (softwareVersion != null) {
168 properties.put(PROPERTY_FIRMWARE_VERSION, softwareVersion);
170 String modelId = fullLight.getNormalizedModelID();
171 if (modelId != null) {
172 properties.put(PROPERTY_MODEL_ID, modelId);
173 String vendor = getVendor(modelId);
174 if (vendor != null) {
175 properties.put(PROPERTY_VENDOR, vendor);
178 properties.put(PROPERTY_VENDOR, fullLight.getManufacturerName());
180 properties.put(PRODUCT_NAME, fullLight.getProductName());
181 String uniqueID = fullLight.getUniqueID();
182 if (uniqueID != null) {
183 properties.put(UNIQUE_ID, uniqueID);
185 updateProperties(properties);
186 isOsramPar16 = OSRAM_PAR16_50_TW_MODEL_ID.equals(modelId);
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 StateDescription stateDescription = 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") //
205 .build().toStateDescription();
206 if (stateDescription != null) {
207 stateDescriptionOptionProvider.setDescription(
208 new ChannelUID(thing.getUID(), CHANNEL_COLORTEMPERATURE_ABS), stateDescription);
210 logger.warn("Failed to create state description in thing {}", thing.getUID());
214 capabilitiesInitializedSuccessfully = true;
218 private @Nullable String getVendor(String modelId) {
219 for (String vendor : VENDOR_MODEL_MAP.keySet()) {
220 if (VENDOR_MODEL_MAP.get(vendor).contains(modelId)) {
228 public void dispose() {
229 logger.debug("Hue light handler disposes. Unregistering listener.");
230 cancelScheduledFuture();
231 if (lightId != null) {
232 HueClient bridgeHandler = getHueClient();
233 if (bridgeHandler != null) {
234 bridgeHandler.unregisterLightStatusListener(this);
242 public void handleCommand(ChannelUID channelUID, Command command) {
243 handleCommand(channelUID.getId(), command, defaultFadeTime);
246 public void handleCommand(String channel, Command command, long fadeTime) {
247 HueClient bridgeHandler = getHueClient();
248 if (bridgeHandler == null) {
249 logger.warn("hue bridge handler not found. Cannot handle command without bridge.");
253 final FullLight light = lastFullLight == null ? bridgeHandler.getLightById(lightId) : lastFullLight;
255 logger.debug("hue light not known on bridge. Cannot handle command.");
256 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
257 "@text/offline.conf-error-wrong-light-id");
261 Integer lastColorTemp;
262 StateUpdate lightState = null;
264 case CHANNEL_COLORTEMPERATURE:
265 if (command instanceof PercentType) {
266 lightState = LightStateConverter.toColorTemperatureLightStateFromPercentType((PercentType) command,
267 colorTemperatureCapabilties);
268 lightState.setTransitionTime(fadeTime);
269 } else if (command instanceof OnOffType) {
270 lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
272 lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
274 } else if (command instanceof IncreaseDecreaseType) {
275 lightState = convertColorTempChangeToStateUpdate((IncreaseDecreaseType) command, light);
276 if (lightState != null) {
277 lightState.setTransitionTime(fadeTime);
281 case CHANNEL_COLORTEMPERATURE_ABS:
282 if (command instanceof DecimalType) {
283 lightState = LightStateConverter.toColorTemperatureLightState((DecimalType) command,
284 colorTemperatureCapabilties);
285 lightState.setTransitionTime(fadeTime);
288 case CHANNEL_BRIGHTNESS:
289 if (command instanceof PercentType) {
290 lightState = LightStateConverter.toBrightnessLightState((PercentType) command);
291 lightState.setTransitionTime(fadeTime);
292 } else if (command instanceof OnOffType) {
293 lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
295 lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
297 } else if (command instanceof IncreaseDecreaseType) {
298 lightState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
299 if (lightState != null) {
300 lightState.setTransitionTime(fadeTime);
303 lastColorTemp = lastSentColorTemp;
304 if (lightState != null && lastColorTemp != null) {
305 // make sure that the light also has the latest color temp
306 // this might not have been yet set in the light, if it was off
307 lightState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
308 lightState.setTransitionTime(fadeTime);
312 logger.trace("CHANNEL_SWITCH handling command {}", command);
313 if (command instanceof OnOffType) {
314 lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
316 lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
319 lastColorTemp = lastSentColorTemp;
320 if (lightState != null && lastColorTemp != null) {
321 // make sure that the light also has the latest color temp
322 // this might not have been yet set in the light, if it was off
323 lightState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
324 lightState.setTransitionTime(fadeTime);
328 if (command instanceof HSBType) {
329 HSBType hsbCommand = (HSBType) command;
330 if (hsbCommand.getBrightness().intValue() == 0) {
331 lightState = LightStateConverter.toOnOffLightState(OnOffType.OFF);
333 lightState = LightStateConverter.toColorLightState(hsbCommand, light.getState());
334 lightState.setTransitionTime(fadeTime);
336 } else if (command instanceof PercentType) {
337 lightState = LightStateConverter.toBrightnessLightState((PercentType) command);
338 lightState.setTransitionTime(fadeTime);
339 } else if (command instanceof OnOffType) {
340 lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
341 } else if (command instanceof IncreaseDecreaseType) {
342 lightState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
343 if (lightState != null) {
344 lightState.setTransitionTime(fadeTime);
349 if (command instanceof StringType) {
350 lightState = LightStateConverter.toAlertState((StringType) command);
351 if (lightState == null) {
352 // Unsupported StringType is passed. Log a warning
353 // message and return.
354 logger.warn("Unsupported String command: {}. Supported commands are: {}, {}, {} ", command,
355 LightStateConverter.ALERT_MODE_NONE, LightStateConverter.ALERT_MODE_SELECT,
356 LightStateConverter.ALERT_MODE_LONG_SELECT);
359 scheduleAlertStateRestore(command);
364 if (command instanceof OnOffType) {
365 lightState = LightStateConverter.toOnOffEffectState((OnOffType) command);
369 if (lightState != null) {
370 // Cache values which we have sent
371 Integer tmpBrightness = lightState.getBrightness();
372 if (tmpBrightness != null) {
373 lastSentBrightness = tmpBrightness;
375 Integer tmpColorTemp = lightState.getColorTemperature();
376 if (tmpColorTemp != null) {
377 lastSentColorTemp = tmpColorTemp;
379 bridgeHandler.updateLightState(this, light, lightState, fadeTime);
381 logger.warn("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel);
386 * Applies additional {@link StateUpdate} commands as a workaround for Osram
387 * Lightify PAR16 TW firmware bug. Also see
388 * http://www.everyhue.com/vanilla/discussion/1756/solved-lightify-turning-off
390 private StateUpdate addOsramSpecificCommands(StateUpdate lightState, OnOffType actionType) {
391 if (actionType.equals(OnOffType.ON)) {
392 lightState.setBrightness(254);
394 lightState.setTransitionTime(0);
399 private @Nullable StateUpdate convertColorTempChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
400 StateUpdate stateUpdate = null;
401 Integer currentColorTemp = getCurrentColorTemp(light.getState());
402 if (currentColorTemp != null) {
403 int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp,
404 colorTemperatureCapabilties);
405 stateUpdate = new StateUpdate().setColorTemperature(newColorTemp, colorTemperatureCapabilties);
410 private @Nullable Integer getCurrentColorTemp(@Nullable State lightState) {
411 Integer colorTemp = lastSentColorTemp;
412 if (colorTemp == null && lightState != null) {
413 colorTemp = lightState.getColorTemperature();
418 private @Nullable StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
419 StateUpdate stateUpdate = null;
420 Integer currentBrightness = getCurrentBrightness(light.getState());
421 if (currentBrightness != null) {
422 int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness);
423 stateUpdate = createBrightnessStateUpdate(currentBrightness, newBrightness);
428 private @Nullable Integer getCurrentBrightness(@Nullable State lightState) {
429 Integer brightness = lastSentBrightness;
430 if (brightness == null && lightState != null) {
431 if (!lightState.isOn()) {
434 brightness = lightState.getBrightness();
440 private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) {
441 StateUpdate lightUpdate = new StateUpdate();
442 if (newBrightness == 0) {
443 lightUpdate.turnOff();
445 lightUpdate.setBrightness(newBrightness);
446 if (currentBrightness == 0) {
447 lightUpdate.turnOn();
453 protected synchronized @Nullable HueClient getHueClient() {
454 if (hueClient == null) {
455 Bridge bridge = getBridge();
456 if (bridge == null) {
459 ThingHandler handler = bridge.getHandler();
460 if (handler instanceof HueClient) {
461 HueClient bridgeHandler = (HueClient) handler;
462 hueClient = bridgeHandler;
463 bridgeHandler.registerLightStatusListener(this);
472 public void setPollBypass(long bypassTime) {
473 endBypassTime = System.currentTimeMillis() + bypassTime;
477 public void unsetPollBypass() {
482 public boolean onLightStateChanged(FullLight fullLight) {
483 logger.trace("onLightStateChanged() was called");
485 if (System.currentTimeMillis() <= endBypassTime) {
486 logger.debug("Bypass light update after command ({}).", lightId);
490 State state = fullLight.getState();
492 final FullLight lastState = lastFullLight;
493 if (lastState == null || !Objects.equals(lastState.getState(), state)) {
494 lastFullLight = fullLight;
499 logger.trace("New state for light {}", lightId);
501 initializeProperties(fullLight);
503 lastSentColorTemp = null;
504 lastSentBrightness = null;
506 // update status (ONLINE, OFFLINE)
507 if (state.isReachable()) {
508 updateStatus(ThingStatus.ONLINE);
510 // we assume OFFLINE without any error (NONE), as this is an
511 // expected state (when bulb powered off)
512 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-not-reachable");
515 logger.debug("onLightStateChanged Light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
516 fullLight.getName(), state.isOn(), state.getBrightness(), state.getHue(), state.getSaturation(),
517 state.getColorTemperature(), state.getColorMode(), state.getXY());
519 HSBType hsbType = LightStateConverter.toHSBType(state);
521 hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), PercentType.ZERO);
523 updateState(CHANNEL_COLOR, hsbType);
525 ColorMode colorMode = state.getColorMode();
526 if (ColorMode.CT.equals(colorMode)) {
527 updateState(CHANNEL_COLORTEMPERATURE,
528 LightStateConverter.toColorTemperaturePercentType(state, colorTemperatureCapabilties));
529 updateState(CHANNEL_COLORTEMPERATURE_ABS, LightStateConverter.toColorTemperature(state));
531 updateState(CHANNEL_COLORTEMPERATURE, UnDefType.UNDEF);
532 updateState(CHANNEL_COLORTEMPERATURE_ABS, UnDefType.UNDEF);
535 PercentType brightnessPercentType = LightStateConverter.toBrightnessPercentType(state);
537 brightnessPercentType = PercentType.ZERO;
539 updateState(CHANNEL_BRIGHTNESS, brightnessPercentType);
541 updateState(CHANNEL_SWITCH, OnOffType.from(state.isOn()));
543 StringType stringType = LightStateConverter.toAlertStringType(state);
544 if (!"NULL".equals(stringType.toString())) {
545 updateState(CHANNEL_ALERT, stringType);
546 scheduleAlertStateRestore(stringType);
553 public void channelLinked(ChannelUID channelUID) {
554 HueClient handler = getHueClient();
555 if (handler != null) {
556 FullLight light = handler.getLightById(lightId);
558 onLightStateChanged(light);
564 public void onLightRemoved() {
565 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-removed");
569 public void onLightGone() {
570 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.light-not-reachable");
574 public void onLightAdded(FullLight light) {
575 onLightStateChanged(light);
579 * Schedules restoration of the alert item state to {@link LightStateConverter#ALERT_MODE_NONE} after a given time.
581 * Based on the initial command:
583 * <li>For {@link LightStateConverter#ALERT_MODE_SELECT} restoration will be triggered after <strong>2
585 * <li>For {@link LightStateConverter#ALERT_MODE_LONG_SELECT} restoration will be triggered after <strong>15
588 * This method also cancels any previously scheduled restoration.
590 * @param command The {@link Command} sent to the item
592 private void scheduleAlertStateRestore(Command command) {
593 cancelScheduledFuture();
594 int delay = getAlertDuration(command);
597 scheduledFuture = scheduler.schedule(() -> {
598 updateState(CHANNEL_ALERT, new StringType(LightStateConverter.ALERT_MODE_NONE));
599 }, delay, TimeUnit.MILLISECONDS);
604 * This method will cancel previously scheduled alert item state
607 private void cancelScheduledFuture() {
608 ScheduledFuture<?> scheduledJob = scheduledFuture;
609 if (scheduledJob != null) {
610 scheduledJob.cancel(true);
611 scheduledFuture = null;
616 * This method returns the time in <strong>milliseconds</strong> after
617 * which, the state of the alert item has to be restored to {@link LightStateConverter#ALERT_MODE_NONE}.
619 * @param command The initial command sent to the alert item.
620 * @return Based on the initial command will return:
622 * <li><strong>2000</strong> for {@link LightStateConverter#ALERT_MODE_SELECT}.
623 * <li><strong>15000</strong> for {@link LightStateConverter#ALERT_MODE_LONG_SELECT}.
624 * <li><strong>-1</strong> for any command different from the previous two.
627 private int getAlertDuration(Command command) {
629 switch (command.toString()) {
630 case LightStateConverter.ALERT_MODE_LONG_SELECT:
633 case LightStateConverter.ALERT_MODE_SELECT:
645 public Collection<Class<? extends ThingHandlerService>> getServices() {
646 return List.of(LightActions.class);
650 public String getLightId() {