2 * Copyright (c) 2010-2020 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.AbstractMap.SimpleEntry;
20 import java.util.Arrays;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.List;
25 import java.util.Objects;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.stream.Collectors;
30 import java.util.stream.Stream;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.hue.internal.FullLight;
35 import org.openhab.binding.hue.internal.State;
36 import org.openhab.binding.hue.internal.State.ColorMode;
37 import org.openhab.binding.hue.internal.StateUpdate;
38 import org.openhab.binding.hue.internal.action.LightActions;
39 import org.openhab.core.library.types.HSBType;
40 import org.openhab.core.library.types.IncreaseDecreaseType;
41 import org.openhab.core.library.types.OnOffType;
42 import org.openhab.core.library.types.PercentType;
43 import org.openhab.core.library.types.StringType;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.Thing;
47 import org.openhab.core.thing.ThingStatus;
48 import org.openhab.core.thing.ThingStatusDetail;
49 import org.openhab.core.thing.ThingStatusInfo;
50 import org.openhab.core.thing.ThingTypeUID;
51 import org.openhab.core.thing.binding.BaseThingHandler;
52 import org.openhab.core.thing.binding.ThingHandler;
53 import org.openhab.core.thing.binding.ThingHandlerService;
54 import org.openhab.core.types.Command;
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 = Stream.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).collect(Collectors.toSet());
85 private static final Map<String, List<String>> VENDOR_MODEL_MAP = Stream.of(
86 new SimpleEntry<>("Philips",
87 Arrays.asList("LCT001", "LCT002", "LCT003", "LCT007", "LLC001", "LLC006", "LLC007", "LLC010",
88 "LLC011", "LLC012", "LLC013", "LLC020", "LST001", "LST002", "LWB004", "LWB006", "LWB007",
90 new SimpleEntry<>("OSRAM",
91 Arrays.asList("Classic_A60_RGBW", "PAR16_50_TW", "Surface_Light_TW", "Plug_01")))
92 .collect(Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue()));
95 private static final String OSRAM_PAR16_50_TW_MODEL_ID = "PAR16_50_TW";
97 private final Logger logger = LoggerFactory.getLogger(HueLightHandler.class);
99 private @NonNullByDefault({}) String lightId;
101 private @Nullable FullLight lastFullLight;
102 private long endBypassTime = 0L;
104 private @Nullable Integer lastSentColorTemp;
105 private @Nullable Integer lastSentBrightness;
107 // Flag to indicate whether the bulb is of type Osram par16 50 TW or not
108 private boolean isOsramPar16 = false;
110 private boolean propertiesInitializedSuccessfully = false;
111 private long defaultFadeTime = 400;
113 private @Nullable HueClient hueClient;
115 private @Nullable ScheduledFuture<?> scheduledFuture;
117 public HueLightHandler(Thing hueLight) {
122 public void initialize() {
123 logger.debug("Initializing hue light handler.");
124 Bridge bridge = getBridge();
125 initializeThing((bridge == null) ? null : bridge.getStatus());
129 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
130 logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
131 initializeThing(bridgeStatusInfo.getStatus());
134 private void initializeThing(@Nullable ThingStatus bridgeStatus) {
135 logger.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus);
136 final String configLightId = (String) getConfig().get(LIGHT_ID);
137 if (configLightId != null) {
138 BigDecimal time = (BigDecimal) getConfig().get(FADETIME);
140 defaultFadeTime = time.longValueExact();
143 lightId = configLightId;
144 // note: this call implicitly registers our handler as a listener on the bridge
145 HueClient bridgeHandler = getHueClient();
146 if (bridgeHandler != null) {
147 if (bridgeStatus == ThingStatus.ONLINE) {
148 initializeProperties(bridgeHandler.getLightById(lightId));
149 updateStatus(ThingStatus.ONLINE);
151 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
154 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
157 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
158 "@text/offline.conf-error-no-light-id");
162 private synchronized void initializeProperties(@Nullable FullLight fullLight) {
163 if (!propertiesInitializedSuccessfully && fullLight != null) {
164 Map<String, String> properties = editProperties();
165 String softwareVersion = fullLight.getSoftwareVersion();
166 if (softwareVersion != null) {
167 properties.put(PROPERTY_FIRMWARE_VERSION, softwareVersion);
169 String modelId = fullLight.getNormalizedModelID();
170 if (modelId != null) {
171 properties.put(PROPERTY_MODEL_ID, modelId);
172 String vendor = getVendor(modelId);
173 if (vendor != null) {
174 properties.put(PROPERTY_VENDOR, vendor);
177 properties.put(PROPERTY_VENDOR, fullLight.getManufacturerName());
179 properties.put(PRODUCT_NAME, fullLight.getProductName());
180 String uniqueID = fullLight.getUniqueID();
181 if (uniqueID != null) {
182 properties.put(UNIQUE_ID, uniqueID);
184 updateProperties(properties);
185 isOsramPar16 = OSRAM_PAR16_50_TW_MODEL_ID.equals(modelId);
186 propertiesInitializedSuccessfully = true;
190 private @Nullable String getVendor(String modelId) {
191 for (String vendor : VENDOR_MODEL_MAP.keySet()) {
192 if (VENDOR_MODEL_MAP.get(vendor).contains(modelId)) {
200 public void dispose() {
201 logger.debug("Hue light handler disposes. Unregistering listener.");
202 cancelScheduledFuture();
203 if (lightId != null) {
204 HueClient bridgeHandler = getHueClient();
205 if (bridgeHandler != null) {
206 bridgeHandler.unregisterLightStatusListener(this);
214 public void handleCommand(ChannelUID channelUID, Command command) {
215 handleCommand(channelUID.getId(), command, defaultFadeTime);
218 public void handleCommand(String channel, Command command, long fadeTime) {
219 HueClient bridgeHandler = getHueClient();
220 if (bridgeHandler == null) {
221 logger.warn("hue bridge handler not found. Cannot handle command without bridge.");
225 final FullLight light = lastFullLight == null ? bridgeHandler.getLightById(lightId) : lastFullLight;
227 logger.debug("hue light not known on bridge. Cannot handle command.");
228 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
229 "@text/offline.conf-error-wrong-light-id");
233 Integer lastColorTemp;
234 StateUpdate lightState = null;
236 case CHANNEL_COLORTEMPERATURE:
237 if (command instanceof PercentType) {
238 lightState = LightStateConverter.toColorTemperatureLightState((PercentType) command);
239 lightState.setTransitionTime(fadeTime);
240 } else if (command instanceof OnOffType) {
241 lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
243 lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
245 } else if (command instanceof IncreaseDecreaseType) {
246 lightState = convertColorTempChangeToStateUpdate((IncreaseDecreaseType) command, light);
247 if (lightState != null) {
248 lightState.setTransitionTime(fadeTime);
253 case CHANNEL_BRIGHTNESS:
254 if (command instanceof PercentType) {
255 lightState = LightStateConverter.toBrightnessLightState((PercentType) command);
256 lightState.setTransitionTime(fadeTime);
257 } else if (command instanceof OnOffType) {
258 lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
260 lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
262 } else if (command instanceof IncreaseDecreaseType) {
263 lightState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
264 if (lightState != null) {
265 lightState.setTransitionTime(fadeTime);
268 lastColorTemp = lastSentColorTemp;
269 if (lightState != null && lastColorTemp != null) {
270 // make sure that the light also has the latest color temp
271 // this might not have been yet set in the light, if it was off
272 lightState.setColorTemperature(lastColorTemp);
273 lightState.setTransitionTime(fadeTime);
277 logger.trace("CHANNEL_SWITCH handling command {}", command);
278 if (command instanceof OnOffType) {
279 lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
281 lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
284 lastColorTemp = lastSentColorTemp;
285 if (lightState != null && lastColorTemp != null) {
286 // make sure that the light also has the latest color temp
287 // this might not have been yet set in the light, if it was off
288 lightState.setColorTemperature(lastColorTemp);
289 lightState.setTransitionTime(fadeTime);
293 if (command instanceof HSBType) {
294 HSBType hsbCommand = (HSBType) command;
295 if (hsbCommand.getBrightness().intValue() == 0) {
296 lightState = LightStateConverter.toOnOffLightState(OnOffType.OFF);
298 lightState = LightStateConverter.toColorLightState(hsbCommand, light.getState());
299 lightState.setTransitionTime(fadeTime);
301 } else if (command instanceof PercentType) {
302 lightState = LightStateConverter.toBrightnessLightState((PercentType) command);
303 lightState.setTransitionTime(fadeTime);
304 } else if (command instanceof OnOffType) {
305 lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
306 } else if (command instanceof IncreaseDecreaseType) {
307 lightState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
308 if (lightState != null) {
309 lightState.setTransitionTime(fadeTime);
314 if (command instanceof StringType) {
315 lightState = LightStateConverter.toAlertState((StringType) command);
316 if (lightState == null) {
317 // Unsupported StringType is passed. Log a warning
318 // message and return.
319 logger.warn("Unsupported String command: {}. Supported commands are: {}, {}, {} ", command,
320 LightStateConverter.ALERT_MODE_NONE, LightStateConverter.ALERT_MODE_SELECT,
321 LightStateConverter.ALERT_MODE_LONG_SELECT);
324 scheduleAlertStateRestore(command);
329 if (command instanceof OnOffType) {
330 lightState = LightStateConverter.toOnOffEffectState((OnOffType) command);
334 if (lightState != null) {
335 // Cache values which we have sent
336 Integer tmpBrightness = lightState.getBrightness();
337 if (tmpBrightness != null) {
338 lastSentBrightness = tmpBrightness;
340 Integer tmpColorTemp = lightState.getColorTemperature();
341 if (tmpColorTemp != null) {
342 lastSentColorTemp = tmpColorTemp;
344 bridgeHandler.updateLightState(this, light, lightState, fadeTime);
346 logger.warn("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel);
351 * Applies additional {@link StateUpdate} commands as a workaround for Osram
352 * Lightify PAR16 TW firmware bug. Also see
353 * http://www.everyhue.com/vanilla/discussion/1756/solved-lightify-turning-off
355 private StateUpdate addOsramSpecificCommands(StateUpdate lightState, OnOffType actionType) {
356 if (actionType.equals(OnOffType.ON)) {
357 lightState.setBrightness(254);
359 lightState.setTransitionTime(0);
364 private @Nullable StateUpdate convertColorTempChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
365 StateUpdate stateUpdate = null;
366 Integer currentColorTemp = getCurrentColorTemp(light.getState());
367 if (currentColorTemp != null) {
368 int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp);
369 stateUpdate = new StateUpdate().setColorTemperature(newColorTemp);
374 private @Nullable Integer getCurrentColorTemp(@Nullable State lightState) {
375 Integer colorTemp = lastSentColorTemp;
376 if (colorTemp == null && lightState != null) {
377 colorTemp = lightState.getColorTemperature();
382 private @Nullable StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
383 StateUpdate stateUpdate = null;
384 Integer currentBrightness = getCurrentBrightness(light.getState());
385 if (currentBrightness != null) {
386 int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness);
387 stateUpdate = createBrightnessStateUpdate(currentBrightness, newBrightness);
392 private @Nullable Integer getCurrentBrightness(@Nullable State lightState) {
393 Integer brightness = lastSentBrightness;
394 if (brightness == null && lightState != null) {
395 if (!lightState.isOn()) {
398 brightness = lightState.getBrightness();
404 private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) {
405 StateUpdate lightUpdate = new StateUpdate();
406 if (newBrightness == 0) {
407 lightUpdate.turnOff();
409 lightUpdate.setBrightness(newBrightness);
410 if (currentBrightness == 0) {
411 lightUpdate.turnOn();
417 protected synchronized @Nullable HueClient getHueClient() {
418 if (hueClient == null) {
419 Bridge bridge = getBridge();
420 if (bridge == null) {
423 ThingHandler handler = bridge.getHandler();
424 if (handler instanceof HueClient) {
425 HueClient bridgeHandler = (HueClient) handler;
426 hueClient = bridgeHandler;
427 bridgeHandler.registerLightStatusListener(this);
436 public void setPollBypass(long bypassTime) {
437 endBypassTime = System.currentTimeMillis() + bypassTime;
441 public void unsetPollBypass() {
446 public boolean onLightStateChanged(FullLight fullLight) {
447 logger.trace("onLightStateChanged() was called");
449 if (System.currentTimeMillis() <= endBypassTime) {
450 logger.debug("Bypass light update after command ({}).", lightId);
454 State state = fullLight.getState();
456 final FullLight lastState = lastFullLight;
457 if (lastState == null || !Objects.equals(lastState.getState(), state)) {
458 lastFullLight = fullLight;
463 logger.trace("New state for light {}", lightId);
465 initializeProperties(fullLight);
467 lastSentColorTemp = null;
468 lastSentBrightness = null;
470 // update status (ONLINE, OFFLINE)
471 if (state.isReachable()) {
472 updateStatus(ThingStatus.ONLINE);
474 // we assume OFFLINE without any error (NONE), as this is an
475 // expected state (when bulb powered off)
476 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-not-reachable");
479 logger.debug("onLightStateChanged Light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
480 fullLight.getName(), state.isOn(), state.getBrightness(), state.getHue(), state.getSaturation(),
481 state.getColorTemperature(), state.getColorMode(), state.getXY());
483 HSBType hsbType = LightStateConverter.toHSBType(state);
485 hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), new PercentType(0));
487 updateState(CHANNEL_COLOR, hsbType);
489 ColorMode colorMode = state.getColorMode();
490 if (ColorMode.CT.equals(colorMode)) {
491 PercentType colorTempPercentType = LightStateConverter.toColorTemperaturePercentType(state);
492 updateState(CHANNEL_COLORTEMPERATURE, colorTempPercentType);
494 updateState(CHANNEL_COLORTEMPERATURE, UnDefType.NULL);
497 PercentType brightnessPercentType = LightStateConverter.toBrightnessPercentType(state);
499 brightnessPercentType = new PercentType(0);
501 updateState(CHANNEL_BRIGHTNESS, brightnessPercentType);
504 updateState(CHANNEL_SWITCH, OnOffType.ON);
506 updateState(CHANNEL_SWITCH, OnOffType.OFF);
509 StringType stringType = LightStateConverter.toAlertStringType(state);
510 if (!"NULL".equals(stringType.toString())) {
511 updateState(CHANNEL_ALERT, stringType);
512 scheduleAlertStateRestore(stringType);
519 public void channelLinked(ChannelUID channelUID) {
520 HueClient handler = getHueClient();
521 if (handler != null) {
522 FullLight light = handler.getLightById(lightId);
524 onLightStateChanged(light);
530 public void onLightRemoved() {
531 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-removed");
535 public void onLightGone() {
536 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.light-not-reachable");
540 public void onLightAdded(FullLight light) {
541 onLightStateChanged(light);
545 * Schedules restoration of the alert item state to {@link LightStateConverter#ALERT_MODE_NONE} after a given time.
547 * Based on the initial command:
549 * <li>For {@link LightStateConverter#ALERT_MODE_SELECT} restoration will be triggered after <strong>2
551 * <li>For {@link LightStateConverter#ALERT_MODE_LONG_SELECT} restoration will be triggered after <strong>15
554 * This method also cancels any previously scheduled restoration.
556 * @param command The {@link Command} sent to the item
558 private void scheduleAlertStateRestore(Command command) {
559 cancelScheduledFuture();
560 int delay = getAlertDuration(command);
563 scheduledFuture = scheduler.schedule(() -> {
564 updateState(CHANNEL_ALERT, new StringType(LightStateConverter.ALERT_MODE_NONE));
565 }, delay, TimeUnit.MILLISECONDS);
570 * This method will cancel previously scheduled alert item state
573 private void cancelScheduledFuture() {
574 ScheduledFuture<?> scheduledJob = scheduledFuture;
575 if (scheduledJob != null) {
576 scheduledJob.cancel(true);
577 scheduledFuture = null;
582 * This method returns the time in <strong>milliseconds</strong> after
583 * which, the state of the alert item has to be restored to {@link LightStateConverter#ALERT_MODE_NONE}.
585 * @param command The initial command sent to the alert item.
586 * @return Based on the initial command will return:
588 * <li><strong>2000</strong> for {@link LightStateConverter#ALERT_MODE_SELECT}.
589 * <li><strong>15000</strong> for {@link LightStateConverter#ALERT_MODE_LONG_SELECT}.
590 * <li><strong>-1</strong> for any command different from the previous two.
593 private int getAlertDuration(Command command) {
595 switch (command.toString()) {
596 case LightStateConverter.ALERT_MODE_LONG_SELECT:
599 case LightStateConverter.ALERT_MODE_SELECT:
611 public Collection<Class<? extends ThingHandlerService>> getServices() {
612 return Collections.singletonList(LightActions.class);
616 public String getLightId() {