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;
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.FullLight;
28 import org.openhab.binding.hue.internal.State;
29 import org.openhab.binding.hue.internal.StateUpdate;
30 import org.openhab.binding.hue.internal.dto.Capabilities;
31 import org.openhab.binding.hue.internal.dto.ColorTemperature;
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
72 public class HueLightHandler extends BaseThingHandler implements HueLightActionsHandler, LightStatusListener {
74 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_COLOR_LIGHT,
75 THING_TYPE_COLOR_TEMPERATURE_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT,
76 THING_TYPE_ON_OFF_LIGHT, THING_TYPE_ON_OFF_PLUG, THING_TYPE_DIMMABLE_PLUG);
78 public static final String OSRAM_PAR16_50_TW_MODEL_ID = "PAR16_50_TW";
80 private final Logger logger = LoggerFactory.getLogger(HueLightHandler.class);
82 private final HueStateDescriptionProvider stateDescriptionProvider;
84 private @NonNullByDefault({}) String lightId;
86 private @Nullable FullLight lastFullLight;
87 private long endBypassTime = 0L;
89 private @Nullable Integer lastSentColorTemp;
90 private @Nullable Integer lastSentBrightness;
92 // Flag to indicate whether the bulb is of type Osram par16 50 TW or not
93 private boolean isOsramPar16 = false;
95 private boolean propertiesInitializedSuccessfully = false;
96 private boolean capabilitiesInitializedSuccessfully = false;
97 private ColorTemperature colorTemperatureCapabilties = new ColorTemperature();
98 private long defaultFadeTime = 400;
100 private @Nullable HueClient hueClient;
102 private @Nullable ScheduledFuture<?> scheduledFuture;
104 public HueLightHandler(Thing hueLight, HueStateDescriptionProvider stateDescriptionProvider) {
106 this.stateDescriptionProvider = stateDescriptionProvider;
110 public void initialize() {
111 logger.debug("Initializing hue light handler.");
112 Bridge bridge = getBridge();
113 initializeThing((bridge == null) ? null : bridge.getStatus());
117 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
118 logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
119 initializeThing(bridgeStatusInfo.getStatus());
122 private void initializeThing(@Nullable ThingStatus bridgeStatus) {
123 logger.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus);
124 final String configLightId = (String) getConfig().get(LIGHT_ID);
125 if (configLightId != null) {
126 BigDecimal time = (BigDecimal) getConfig().get(FADETIME);
128 defaultFadeTime = time.longValueExact();
131 lightId = configLightId;
132 // note: this call implicitly registers our handler as a listener on the bridge
133 HueClient bridgeHandler = getHueClient();
134 if (bridgeHandler != null) {
135 if (bridgeStatus == ThingStatus.ONLINE) {
136 FullLight fullLight = bridgeHandler.getLightById(lightId);
137 initializeProperties(fullLight);
138 initializeCapabilities(fullLight);
139 updateStatus(ThingStatus.ONLINE);
141 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
147 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
148 "@text/offline.conf-error-no-light-id");
152 private synchronized void initializeProperties(@Nullable FullLight fullLight) {
153 if (!propertiesInitializedSuccessfully && fullLight != null) {
154 Map<String, String> properties = editProperties();
155 String softwareVersion = fullLight.getSoftwareVersion();
156 if (softwareVersion != null) {
157 properties.put(PROPERTY_FIRMWARE_VERSION, softwareVersion);
159 String modelId = fullLight.getNormalizedModelID();
160 if (modelId != null) {
161 properties.put(PROPERTY_MODEL_ID, modelId);
163 properties.put(PROPERTY_VENDOR, fullLight.getManufacturerName());
164 properties.put(PRODUCT_NAME, fullLight.getProductName());
165 String uniqueID = fullLight.getUniqueID();
166 if (uniqueID != null) {
167 properties.put(UNIQUE_ID, uniqueID);
169 updateProperties(properties);
170 isOsramPar16 = OSRAM_PAR16_50_TW_MODEL_ID.equals(modelId);
171 propertiesInitializedSuccessfully = true;
175 private void initializeCapabilities(@Nullable FullLight fullLight) {
176 if (!capabilitiesInitializedSuccessfully && fullLight != null) {
177 Capabilities capabilities = fullLight.capabilities;
178 if (capabilities != null) {
179 ColorTemperature ct = capabilities.control.ct;
181 colorTemperatureCapabilties = ct;
183 // minimum and maximum are inverted due to mired/Kelvin conversion!
184 StateDescriptionFragment stateDescriptionFragment = StateDescriptionFragmentBuilder.create()
185 .withMinimum(new BigDecimal(LightStateConverter.miredToKelvin(ct.max))) //
186 .withMaximum(new BigDecimal(LightStateConverter.miredToKelvin(ct.min))) //
187 .withStep(new BigDecimal(100)) //
188 .withPattern("%.0f K") //
190 stateDescriptionProvider.setStateDescriptionFragment(
191 new ChannelUID(thing.getUID(), CHANNEL_COLORTEMPERATURE_ABS), stateDescriptionFragment);
194 capabilitiesInitializedSuccessfully = true;
199 public void dispose() {
200 logger.debug("Hue light handler disposes. Unregistering listener.");
201 cancelScheduledFuture();
202 if (lightId != null) {
203 HueClient bridgeHandler = getHueClient();
204 if (bridgeHandler != null) {
205 bridgeHandler.unregisterLightStatusListener(this);
213 public void handleCommand(ChannelUID channelUID, Command command) {
214 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 newState = null;
236 case CHANNEL_COLORTEMPERATURE:
237 if (command instanceof PercentType) {
238 newState = LightStateConverter.toColorTemperatureLightStateFromPercentType((PercentType) command,
239 colorTemperatureCapabilties);
240 newState.setTransitionTime(fadeTime);
241 } else if (command instanceof OnOffType) {
242 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
244 newState = addOsramSpecificCommands(newState, (OnOffType) command);
246 } else if (command instanceof IncreaseDecreaseType) {
247 newState = convertColorTempChangeToStateUpdate((IncreaseDecreaseType) command, light);
248 if (newState != null) {
249 newState.setTransitionTime(fadeTime);
253 case CHANNEL_COLORTEMPERATURE_ABS:
254 if (command instanceof DecimalType) {
255 newState = LightStateConverter.toColorTemperatureLightState((DecimalType) command,
256 colorTemperatureCapabilties);
257 newState.setTransitionTime(fadeTime);
260 case CHANNEL_BRIGHTNESS:
261 if (command instanceof PercentType) {
262 newState = LightStateConverter.toBrightnessLightState((PercentType) command);
263 newState.setTransitionTime(fadeTime);
264 } else if (command instanceof OnOffType) {
265 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
267 newState = addOsramSpecificCommands(newState, (OnOffType) command);
269 } else if (command instanceof IncreaseDecreaseType) {
270 newState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
271 if (newState != null) {
272 newState.setTransitionTime(fadeTime);
275 lastColorTemp = lastSentColorTemp;
276 if (newState != null && lastColorTemp != null) {
277 // make sure that the light also has the latest color temp
278 // this might not have been yet set in the light, if it was off
279 newState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
280 newState.setTransitionTime(fadeTime);
284 if (command instanceof OnOffType) {
285 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
287 newState = addOsramSpecificCommands(newState, (OnOffType) command);
290 lastColorTemp = lastSentColorTemp;
291 if (newState != null && lastColorTemp != null) {
292 // make sure that the light also has the latest color temp
293 // this might not have been yet set in the light, if it was off
294 newState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
295 newState.setTransitionTime(fadeTime);
299 if (command instanceof HSBType) {
300 HSBType hsbCommand = (HSBType) command;
301 if (hsbCommand.getBrightness().intValue() == 0) {
302 newState = LightStateConverter.toOnOffLightState(OnOffType.OFF);
304 newState = LightStateConverter.toColorLightState(hsbCommand, light.getState());
305 newState.setTransitionTime(fadeTime);
307 } else if (command instanceof PercentType) {
308 newState = LightStateConverter.toBrightnessLightState((PercentType) command);
309 newState.setTransitionTime(fadeTime);
310 } else if (command instanceof OnOffType) {
311 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
312 } else if (command instanceof IncreaseDecreaseType) {
313 newState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
314 if (newState != null) {
315 newState.setTransitionTime(fadeTime);
320 if (command instanceof StringType) {
321 newState = LightStateConverter.toAlertState((StringType) command);
322 if (newState == null) {
323 // Unsupported StringType is passed. Log a warning
324 // message and return.
325 logger.warn("Unsupported String command: {}. Supported commands are: {}, {}, {} ", command,
326 LightStateConverter.ALERT_MODE_NONE, LightStateConverter.ALERT_MODE_SELECT,
327 LightStateConverter.ALERT_MODE_LONG_SELECT);
330 scheduleAlertStateRestore(command);
335 if (command instanceof OnOffType) {
336 newState = LightStateConverter.toOnOffEffectState((OnOffType) command);
340 if (newState != null) {
341 // Cache values which we have sent
342 Integer tmpBrightness = newState.getBrightness();
343 if (tmpBrightness != null) {
344 lastSentBrightness = tmpBrightness;
346 Integer tmpColorTemp = newState.getColorTemperature();
347 if (tmpColorTemp != null) {
348 lastSentColorTemp = tmpColorTemp;
350 bridgeHandler.updateLightState(this, light, newState, fadeTime);
352 logger.warn("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel);
357 * Applies additional {@link StateUpdate} commands as a workaround for Osram
358 * Lightify PAR16 TW firmware bug. Also see
359 * http://www.everyhue.com/vanilla/discussion/1756/solved-lightify-turning-off
361 private StateUpdate addOsramSpecificCommands(StateUpdate lightState, OnOffType actionType) {
362 if (actionType.equals(OnOffType.ON)) {
363 lightState.setBrightness(254);
365 lightState.setTransitionTime(0);
370 private @Nullable StateUpdate convertColorTempChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
371 StateUpdate stateUpdate = null;
372 Integer currentColorTemp = getCurrentColorTemp(light.getState());
373 if (currentColorTemp != null) {
374 int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp,
375 colorTemperatureCapabilties);
376 stateUpdate = new StateUpdate().setColorTemperature(newColorTemp, colorTemperatureCapabilties);
381 private @Nullable Integer getCurrentColorTemp(@Nullable State lightState) {
382 Integer colorTemp = lastSentColorTemp;
383 if (colorTemp == null && lightState != null) {
384 return lightState.getColorTemperature();
389 private @Nullable StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
390 Integer currentBrightness = getCurrentBrightness(light.getState());
391 if (currentBrightness == null) {
394 int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness);
395 return createBrightnessStateUpdate(currentBrightness, newBrightness);
398 private @Nullable Integer getCurrentBrightness(@Nullable State lightState) {
399 if (lastSentBrightness == null && lightState != null) {
400 return lightState.isOn() ? lightState.getBrightness() : 0;
402 return lastSentBrightness;
405 private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) {
406 StateUpdate lightUpdate = new StateUpdate();
407 if (newBrightness == 0) {
408 lightUpdate.turnOff();
410 lightUpdate.setBrightness(newBrightness);
411 if (currentBrightness == 0) {
412 lightUpdate.turnOn();
418 protected synchronized @Nullable HueClient getHueClient() {
419 if (hueClient == null) {
420 Bridge bridge = getBridge();
421 if (bridge == null) {
424 ThingHandler handler = bridge.getHandler();
425 if (handler instanceof HueClient) {
426 HueClient bridgeHandler = (HueClient) handler;
427 hueClient = bridgeHandler;
428 bridgeHandler.registerLightStatusListener(this);
437 public void setPollBypass(long bypassTime) {
438 endBypassTime = System.currentTimeMillis() + bypassTime;
442 public void unsetPollBypass() {
447 public boolean onLightStateChanged(FullLight fullLight) {
448 logger.trace("onLightStateChanged() was called");
450 if (System.currentTimeMillis() <= endBypassTime) {
451 logger.debug("Bypass light update after command ({}).", lightId);
455 State state = fullLight.getState();
457 final FullLight lastState = lastFullLight;
458 if (lastState == null || !Objects.equals(lastState.getState(), state)) {
459 lastFullLight = fullLight;
464 logger.trace("New state for light {}", lightId);
466 initializeProperties(fullLight);
468 lastSentColorTemp = null;
469 lastSentBrightness = null;
471 // update status (ONLINE, OFFLINE)
472 if (state.isReachable()) {
473 updateStatus(ThingStatus.ONLINE);
475 // we assume OFFLINE without any error (NONE), as this is an
476 // expected state (when bulb powered off)
477 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-not-reachable");
480 logger.debug("onLightStateChanged Light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
481 fullLight.getName(), state.isOn(), state.getBrightness(), state.getHue(), state.getSaturation(),
482 state.getColorTemperature(), state.getColorMode(), state.getXY());
484 HSBType hsbType = LightStateConverter.toHSBType(state);
486 hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), PercentType.ZERO);
488 updateState(CHANNEL_COLOR, hsbType);
490 PercentType brightnessPercentType = state.isOn() ? LightStateConverter.toBrightnessPercentType(state)
492 updateState(CHANNEL_BRIGHTNESS, brightnessPercentType);
494 updateState(CHANNEL_SWITCH, OnOffType.from(state.isOn()));
496 updateState(CHANNEL_COLORTEMPERATURE,
497 LightStateConverter.toColorTemperaturePercentType(state, colorTemperatureCapabilties));
498 updateState(CHANNEL_COLORTEMPERATURE_ABS, LightStateConverter.toColorTemperature(state));
500 StringType stringType = LightStateConverter.toAlertStringType(state);
501 if (!"NULL".equals(stringType.toString())) {
502 updateState(CHANNEL_ALERT, stringType);
503 scheduleAlertStateRestore(stringType);
510 public void channelLinked(ChannelUID channelUID) {
511 HueClient handler = getHueClient();
512 if (handler != null) {
513 FullLight light = handler.getLightById(lightId);
515 onLightStateChanged(light);
521 public void onLightRemoved() {
522 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-removed");
526 public void onLightGone() {
527 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.light-not-reachable");
531 public void onLightAdded(FullLight light) {
532 onLightStateChanged(light);
536 * Schedules restoration of the alert item state to {@link LightStateConverter#ALERT_MODE_NONE} after a given time.
538 * Based on the initial command:
540 * <li>For {@link LightStateConverter#ALERT_MODE_SELECT} restoration will be triggered after <strong>2
542 * <li>For {@link LightStateConverter#ALERT_MODE_LONG_SELECT} restoration will be triggered after <strong>15
545 * This method also cancels any previously scheduled restoration.
547 * @param command The {@link Command} sent to the item
549 private void scheduleAlertStateRestore(Command command) {
550 cancelScheduledFuture();
551 int delay = getAlertDuration(command);
554 scheduledFuture = scheduler.schedule(() -> {
555 updateState(CHANNEL_ALERT, new StringType(LightStateConverter.ALERT_MODE_NONE));
556 }, delay, TimeUnit.MILLISECONDS);
561 * This method will cancel previously scheduled alert item state
564 private void cancelScheduledFuture() {
565 ScheduledFuture<?> scheduledJob = scheduledFuture;
566 if (scheduledJob != null) {
567 scheduledJob.cancel(true);
568 scheduledFuture = null;
573 * This method returns the time in <strong>milliseconds</strong> after
574 * which, the state of the alert item has to be restored to {@link LightStateConverter#ALERT_MODE_NONE}.
576 * @param command The initial command sent to the alert item.
577 * @return Based on the initial command will return:
579 * <li><strong>2000</strong> for {@link LightStateConverter#ALERT_MODE_SELECT}.
580 * <li><strong>15000</strong> for {@link LightStateConverter#ALERT_MODE_LONG_SELECT}.
581 * <li><strong>-1</strong> for any command different from the previous two.
584 private int getAlertDuration(Command command) {
586 switch (command.toString()) {
587 case LightStateConverter.ALERT_MODE_LONG_SELECT:
590 case LightStateConverter.ALERT_MODE_SELECT:
602 public String getLightId() {