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.StateUpdate;
32 import org.openhab.binding.hue.internal.action.LightActions;
33 import org.openhab.binding.hue.internal.dto.Capabilities;
34 import org.openhab.binding.hue.internal.dto.ColorTemperature;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.HSBType;
37 import org.openhab.core.library.types.IncreaseDecreaseType;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.PercentType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.ThingStatusInfo;
47 import org.openhab.core.thing.ThingTypeUID;
48 import org.openhab.core.thing.binding.BaseThingHandler;
49 import org.openhab.core.thing.binding.ThingHandler;
50 import org.openhab.core.thing.binding.ThingHandlerService;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.StateDescription;
53 import org.openhab.core.types.StateDescriptionFragmentBuilder;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
58 * {@link HueLightHandler} is the handler for a hue light. It uses the {@link HueClient} to execute the actual
61 * @author Dennis Nobel - Initial contribution
62 * @author Oliver Libutzki - Adjustments
63 * @author Kai Kreuzer - stabilized code
64 * @author Andre Fuechsel - implemented switch off when brightness == 0, changed to support generic thing types, changed
65 * the initialization of properties
66 * @author Thomas Höfer - added thing properties
67 * @author Jochen Hiller - fixed status updates for reachable=true/false
68 * @author Markus Mazurczak - added code for command handling of OSRAM PAR16 50
70 * @author Yordan Zhelev - added alert and effect functions
71 * @author Denis Dudnik - switched to internally integrated source of Jue library
72 * @author Christoph Weitkamp - Added support for bulbs using CIE XY colormode only
73 * @author Jochen Leopold - Added support for custom fade times
76 public class HueLightHandler extends BaseThingHandler implements LightStatusListener {
78 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_COLOR_LIGHT,
79 THING_TYPE_COLOR_TEMPERATURE_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT,
80 THING_TYPE_ON_OFF_LIGHT, THING_TYPE_ON_OFF_PLUG, THING_TYPE_DIMMABLE_PLUG);
82 public static final String OSRAM_PAR16_50_TW_MODEL_ID = "PAR16_50_TW";
84 private final Logger logger = LoggerFactory.getLogger(HueLightHandler.class);
86 private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider;
88 private @NonNullByDefault({}) String lightId;
90 private @Nullable FullLight lastFullLight;
91 private long endBypassTime = 0L;
93 private @Nullable Integer lastSentColorTemp;
94 private @Nullable Integer lastSentBrightness;
96 // Flag to indicate whether the bulb is of type Osram par16 50 TW or not
97 private boolean isOsramPar16 = false;
99 private boolean propertiesInitializedSuccessfully = false;
100 private boolean capabilitiesInitializedSuccessfully = false;
101 private ColorTemperature colorTemperatureCapabilties = new ColorTemperature();
102 private long defaultFadeTime = 400;
104 private @Nullable HueClient hueClient;
106 private @Nullable ScheduledFuture<?> scheduledFuture;
108 public HueLightHandler(Thing hueLight, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) {
110 this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
114 public void initialize() {
115 logger.debug("Initializing hue light handler.");
116 Bridge bridge = getBridge();
117 initializeThing((bridge == null) ? null : bridge.getStatus());
121 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
122 logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
123 initializeThing(bridgeStatusInfo.getStatus());
126 private void initializeThing(@Nullable ThingStatus bridgeStatus) {
127 logger.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus);
128 final String configLightId = (String) getConfig().get(LIGHT_ID);
129 if (configLightId != null) {
130 BigDecimal time = (BigDecimal) getConfig().get(FADETIME);
132 defaultFadeTime = time.longValueExact();
135 lightId = configLightId;
136 // note: this call implicitly registers our handler as a listener on the bridge
137 HueClient bridgeHandler = getHueClient();
138 if (bridgeHandler != null) {
139 if (bridgeStatus == ThingStatus.ONLINE) {
140 FullLight fullLight = bridgeHandler.getLightById(lightId);
141 initializeProperties(fullLight);
142 initializeCapabilities(fullLight);
143 updateStatus(ThingStatus.ONLINE);
145 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
148 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
151 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
152 "@text/offline.conf-error-no-light-id");
156 private synchronized void initializeProperties(@Nullable FullLight fullLight) {
157 if (!propertiesInitializedSuccessfully && fullLight != null) {
158 Map<String, String> properties = editProperties();
159 String softwareVersion = fullLight.getSoftwareVersion();
160 if (softwareVersion != null) {
161 properties.put(PROPERTY_FIRMWARE_VERSION, softwareVersion);
163 String modelId = fullLight.getNormalizedModelID();
164 if (modelId != null) {
165 properties.put(PROPERTY_MODEL_ID, modelId);
167 properties.put(PROPERTY_VENDOR, fullLight.getManufacturerName());
168 properties.put(PRODUCT_NAME, fullLight.getProductName());
169 String uniqueID = fullLight.getUniqueID();
170 if (uniqueID != null) {
171 properties.put(UNIQUE_ID, uniqueID);
173 updateProperties(properties);
174 isOsramPar16 = OSRAM_PAR16_50_TW_MODEL_ID.equals(modelId);
175 propertiesInitializedSuccessfully = true;
179 private void initializeCapabilities(@Nullable FullLight fullLight) {
180 if (!capabilitiesInitializedSuccessfully && fullLight != null) {
181 Capabilities capabilities = fullLight.capabilities;
182 if (capabilities != null) {
183 ColorTemperature ct = capabilities.control.ct;
185 colorTemperatureCapabilties = ct;
187 // minimum and maximum are inverted due to mired/Kelvin conversion!
188 StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
189 .withMinimum(new BigDecimal(LightStateConverter.miredToKelvin(ct.max))) //
190 .withMaximum(new BigDecimal(LightStateConverter.miredToKelvin(ct.min))) //
191 .withStep(new BigDecimal(100)) //
192 .withPattern("%.0f K") //
193 .build().toStateDescription();
194 if (stateDescription != null) {
195 stateDescriptionOptionProvider.setDescription(
196 new ChannelUID(thing.getUID(), CHANNEL_COLORTEMPERATURE_ABS), stateDescription);
198 logger.warn("Failed to create state description in thing {}", thing.getUID());
202 capabilitiesInitializedSuccessfully = true;
207 public void dispose() {
208 logger.debug("Hue light handler disposes. Unregistering listener.");
209 cancelScheduledFuture();
210 if (lightId != null) {
211 HueClient bridgeHandler = getHueClient();
212 if (bridgeHandler != null) {
213 bridgeHandler.unregisterLightStatusListener(this);
221 public void handleCommand(ChannelUID channelUID, Command command) {
222 handleCommand(channelUID.getId(), command, defaultFadeTime);
225 public void handleCommand(String channel, Command command, long fadeTime) {
226 HueClient bridgeHandler = getHueClient();
227 if (bridgeHandler == null) {
228 logger.warn("hue bridge handler not found. Cannot handle command without bridge.");
232 final FullLight light = lastFullLight == null ? bridgeHandler.getLightById(lightId) : lastFullLight;
234 logger.debug("hue light not known on bridge. Cannot handle command.");
235 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
236 "@text/offline.conf-error-wrong-light-id");
240 Integer lastColorTemp;
241 StateUpdate lightState = null;
243 case CHANNEL_COLORTEMPERATURE:
244 if (command instanceof PercentType) {
245 lightState = LightStateConverter.toColorTemperatureLightStateFromPercentType((PercentType) command,
246 colorTemperatureCapabilties);
247 lightState.setTransitionTime(fadeTime);
248 } else if (command instanceof OnOffType) {
249 lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
251 lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
253 } else if (command instanceof IncreaseDecreaseType) {
254 lightState = convertColorTempChangeToStateUpdate((IncreaseDecreaseType) command, light);
255 if (lightState != null) {
256 lightState.setTransitionTime(fadeTime);
260 case CHANNEL_COLORTEMPERATURE_ABS:
261 if (command instanceof DecimalType) {
262 lightState = LightStateConverter.toColorTemperatureLightState((DecimalType) command,
263 colorTemperatureCapabilties);
264 lightState.setTransitionTime(fadeTime);
267 case CHANNEL_BRIGHTNESS:
268 if (command instanceof PercentType) {
269 lightState = LightStateConverter.toBrightnessLightState((PercentType) command);
270 lightState.setTransitionTime(fadeTime);
271 } else if (command instanceof OnOffType) {
272 lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
274 lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
276 } else if (command instanceof IncreaseDecreaseType) {
277 lightState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
278 if (lightState != null) {
279 lightState.setTransitionTime(fadeTime);
282 lastColorTemp = lastSentColorTemp;
283 if (lightState != null && lastColorTemp != null) {
284 // make sure that the light also has the latest color temp
285 // this might not have been yet set in the light, if it was off
286 lightState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
287 lightState.setTransitionTime(fadeTime);
291 if (command instanceof OnOffType) {
292 lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
294 lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
297 lastColorTemp = lastSentColorTemp;
298 if (lightState != null && lastColorTemp != null) {
299 // make sure that the light also has the latest color temp
300 // this might not have been yet set in the light, if it was off
301 lightState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
302 lightState.setTransitionTime(fadeTime);
306 if (command instanceof HSBType) {
307 HSBType hsbCommand = (HSBType) command;
308 if (hsbCommand.getBrightness().intValue() == 0) {
309 lightState = LightStateConverter.toOnOffLightState(OnOffType.OFF);
311 lightState = LightStateConverter.toColorLightState(hsbCommand, light.getState());
312 lightState.setTransitionTime(fadeTime);
314 } else if (command instanceof PercentType) {
315 lightState = LightStateConverter.toBrightnessLightState((PercentType) command);
316 lightState.setTransitionTime(fadeTime);
317 } else if (command instanceof OnOffType) {
318 lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
319 } else if (command instanceof IncreaseDecreaseType) {
320 lightState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
321 if (lightState != null) {
322 lightState.setTransitionTime(fadeTime);
327 if (command instanceof StringType) {
328 lightState = LightStateConverter.toAlertState((StringType) command);
329 if (lightState == null) {
330 // Unsupported StringType is passed. Log a warning
331 // message and return.
332 logger.warn("Unsupported String command: {}. Supported commands are: {}, {}, {} ", command,
333 LightStateConverter.ALERT_MODE_NONE, LightStateConverter.ALERT_MODE_SELECT,
334 LightStateConverter.ALERT_MODE_LONG_SELECT);
337 scheduleAlertStateRestore(command);
342 if (command instanceof OnOffType) {
343 lightState = LightStateConverter.toOnOffEffectState((OnOffType) command);
347 if (lightState != null) {
348 // Cache values which we have sent
349 Integer tmpBrightness = lightState.getBrightness();
350 if (tmpBrightness != null) {
351 lastSentBrightness = tmpBrightness;
353 Integer tmpColorTemp = lightState.getColorTemperature();
354 if (tmpColorTemp != null) {
355 lastSentColorTemp = tmpColorTemp;
357 bridgeHandler.updateLightState(this, light, lightState, fadeTime);
359 logger.warn("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel);
364 * Applies additional {@link StateUpdate} commands as a workaround for Osram
365 * Lightify PAR16 TW firmware bug. Also see
366 * http://www.everyhue.com/vanilla/discussion/1756/solved-lightify-turning-off
368 private StateUpdate addOsramSpecificCommands(StateUpdate lightState, OnOffType actionType) {
369 if (actionType.equals(OnOffType.ON)) {
370 lightState.setBrightness(254);
372 lightState.setTransitionTime(0);
377 private @Nullable StateUpdate convertColorTempChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
378 StateUpdate stateUpdate = null;
379 Integer currentColorTemp = getCurrentColorTemp(light.getState());
380 if (currentColorTemp != null) {
381 int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp,
382 colorTemperatureCapabilties);
383 stateUpdate = new StateUpdate().setColorTemperature(newColorTemp, colorTemperatureCapabilties);
388 private @Nullable Integer getCurrentColorTemp(@Nullable State lightState) {
389 Integer colorTemp = lastSentColorTemp;
390 if (colorTemp == null && lightState != null) {
391 return lightState.getColorTemperature();
396 private @Nullable StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
397 Integer currentBrightness = getCurrentBrightness(light.getState());
398 if (currentBrightness == null) {
401 int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness);
402 return createBrightnessStateUpdate(currentBrightness, newBrightness);
405 private @Nullable Integer getCurrentBrightness(@Nullable State lightState) {
406 if (lastSentBrightness == null && lightState != null) {
407 return lightState.isOn() ? lightState.getBrightness() : 0;
409 return lastSentBrightness;
412 private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) {
413 StateUpdate lightUpdate = new StateUpdate();
414 if (newBrightness == 0) {
415 lightUpdate.turnOff();
417 lightUpdate.setBrightness(newBrightness);
418 if (currentBrightness == 0) {
419 lightUpdate.turnOn();
425 protected synchronized @Nullable HueClient getHueClient() {
426 if (hueClient == null) {
427 Bridge bridge = getBridge();
428 if (bridge == null) {
431 ThingHandler handler = bridge.getHandler();
432 if (handler instanceof HueClient) {
433 HueClient bridgeHandler = (HueClient) handler;
434 hueClient = bridgeHandler;
435 bridgeHandler.registerLightStatusListener(this);
444 public void setPollBypass(long bypassTime) {
445 endBypassTime = System.currentTimeMillis() + bypassTime;
449 public void unsetPollBypass() {
454 public boolean onLightStateChanged(FullLight fullLight) {
455 logger.trace("onLightStateChanged() was called");
457 if (System.currentTimeMillis() <= endBypassTime) {
458 logger.debug("Bypass light update after command ({}).", lightId);
462 State state = fullLight.getState();
464 final FullLight lastState = lastFullLight;
465 if (lastState == null || !Objects.equals(lastState.getState(), state)) {
466 lastFullLight = fullLight;
471 logger.trace("New state for light {}", lightId);
473 initializeProperties(fullLight);
475 lastSentColorTemp = null;
476 lastSentBrightness = null;
478 // update status (ONLINE, OFFLINE)
479 if (state.isReachable()) {
480 updateStatus(ThingStatus.ONLINE);
482 // we assume OFFLINE without any error (NONE), as this is an
483 // expected state (when bulb powered off)
484 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-not-reachable");
487 logger.debug("onLightStateChanged Light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
488 fullLight.getName(), state.isOn(), state.getBrightness(), state.getHue(), state.getSaturation(),
489 state.getColorTemperature(), state.getColorMode(), state.getXY());
491 HSBType hsbType = LightStateConverter.toHSBType(state);
493 hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), PercentType.ZERO);
495 updateState(CHANNEL_COLOR, hsbType);
497 PercentType brightnessPercentType = state.isOn() ? LightStateConverter.toBrightnessPercentType(state)
499 updateState(CHANNEL_BRIGHTNESS, brightnessPercentType);
501 updateState(CHANNEL_SWITCH, OnOffType.from(state.isOn()));
503 updateState(CHANNEL_COLORTEMPERATURE,
504 LightStateConverter.toColorTemperaturePercentType(state, colorTemperatureCapabilties));
505 updateState(CHANNEL_COLORTEMPERATURE_ABS, LightStateConverter.toColorTemperature(state));
507 StringType stringType = LightStateConverter.toAlertStringType(state);
508 if (!"NULL".equals(stringType.toString())) {
509 updateState(CHANNEL_ALERT, stringType);
510 scheduleAlertStateRestore(stringType);
517 public void channelLinked(ChannelUID channelUID) {
518 HueClient handler = getHueClient();
519 if (handler != null) {
520 FullLight light = handler.getLightById(lightId);
522 onLightStateChanged(light);
528 public void onLightRemoved() {
529 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-removed");
533 public void onLightGone() {
534 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.light-not-reachable");
538 public void onLightAdded(FullLight light) {
539 onLightStateChanged(light);
543 * Schedules restoration of the alert item state to {@link LightStateConverter#ALERT_MODE_NONE} after a given time.
545 * Based on the initial command:
547 * <li>For {@link LightStateConverter#ALERT_MODE_SELECT} restoration will be triggered after <strong>2
549 * <li>For {@link LightStateConverter#ALERT_MODE_LONG_SELECT} restoration will be triggered after <strong>15
552 * This method also cancels any previously scheduled restoration.
554 * @param command The {@link Command} sent to the item
556 private void scheduleAlertStateRestore(Command command) {
557 cancelScheduledFuture();
558 int delay = getAlertDuration(command);
561 scheduledFuture = scheduler.schedule(() -> {
562 updateState(CHANNEL_ALERT, new StringType(LightStateConverter.ALERT_MODE_NONE));
563 }, delay, TimeUnit.MILLISECONDS);
568 * This method will cancel previously scheduled alert item state
571 private void cancelScheduledFuture() {
572 ScheduledFuture<?> scheduledJob = scheduledFuture;
573 if (scheduledJob != null) {
574 scheduledJob.cancel(true);
575 scheduledFuture = null;
580 * This method returns the time in <strong>milliseconds</strong> after
581 * which, the state of the alert item has to be restored to {@link LightStateConverter#ALERT_MODE_NONE}.
583 * @param command The initial command sent to the alert item.
584 * @return Based on the initial command will return:
586 * <li><strong>2000</strong> for {@link LightStateConverter#ALERT_MODE_SELECT}.
587 * <li><strong>15000</strong> for {@link LightStateConverter#ALERT_MODE_LONG_SELECT}.
588 * <li><strong>-1</strong> for any command different from the previous two.
591 private int getAlertDuration(Command command) {
593 switch (command.toString()) {
594 case LightStateConverter.ALERT_MODE_LONG_SELECT:
597 case LightStateConverter.ALERT_MODE_SELECT:
609 public Collection<Class<? extends ThingHandlerService>> getServices() {
610 return List.of(LightActions.class);
614 public String getLightId() {