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.*;
17 import java.math.BigDecimal;
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.List;
21 import java.util.Objects;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.stream.Collectors;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.hue.internal.FullGroup;
30 import org.openhab.binding.hue.internal.Scene;
31 import org.openhab.binding.hue.internal.State;
32 import org.openhab.binding.hue.internal.State.ColorMode;
33 import org.openhab.binding.hue.internal.StateUpdate;
34 import org.openhab.core.library.types.HSBType;
35 import org.openhab.core.library.types.IncreaseDecreaseType;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.PercentType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.ThingStatusInfo;
45 import org.openhab.core.thing.ThingTypeUID;
46 import org.openhab.core.thing.binding.BaseThingHandler;
47 import org.openhab.core.thing.binding.ThingHandler;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.StateOption;
50 import org.openhab.core.types.UnDefType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * {@link HueGroupHandler} is the handler for a hue group of lights. It uses the {@link HueClient} to execute the
58 * @author Laurent Garnier - Initial contribution
61 public class HueGroupHandler extends BaseThingHandler implements GroupStatusListener {
62 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_GROUP);
64 private final Logger logger = LoggerFactory.getLogger(HueGroupHandler.class);
65 private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider;
67 private @NonNullByDefault({}) String groupId;
69 private @Nullable Integer lastSentColorTemp;
70 private @Nullable Integer lastSentBrightness;
72 private long defaultFadeTime = 400;
74 private @Nullable HueClient hueClient;
76 private @Nullable ScheduledFuture<?> scheduledFuture;
77 private @Nullable FullGroup lastFullGroup;
79 private List<String> consoleScenesList = new ArrayList<>();
81 public HueGroupHandler(Thing thing, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) {
83 this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
87 public void initialize() {
88 logger.debug("Initializing hue group handler.");
89 Bridge bridge = getBridge();
90 initializeThing((bridge == null) ? null : bridge.getStatus());
94 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
95 logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
96 initializeThing(bridgeStatusInfo.getStatus());
99 private void initializeThing(@Nullable ThingStatus bridgeStatus) {
100 logger.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus);
101 final String configGroupId = (String) getConfig().get(GROUP_ID);
102 if (configGroupId != null) {
103 BigDecimal time = (BigDecimal) getConfig().get(FADETIME);
105 defaultFadeTime = time.longValueExact();
108 groupId = configGroupId;
109 // note: this call implicitly registers our handler as a listener on the bridge
110 if (getHueClient() != null) {
111 if (bridgeStatus == ThingStatus.ONLINE) {
112 updateStatus(ThingStatus.ONLINE);
114 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
120 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
121 "@text/offline.conf-error-no-group-id");
126 public void dispose() {
127 logger.debug("Hue group handler disposes. Unregistering listener.");
128 cancelScheduledFuture();
129 if (groupId != null) {
130 HueClient bridgeHandler = getHueClient();
131 if (bridgeHandler != null) {
132 bridgeHandler.unregisterGroupStatusListener(this);
139 protected synchronized @Nullable HueClient getHueClient() {
140 if (hueClient == null) {
141 Bridge bridge = getBridge();
142 if (bridge == null) {
145 ThingHandler handler = bridge.getHandler();
146 if (handler instanceof HueBridgeHandler) {
147 HueClient bridgeHandler = (HueClient) handler;
148 hueClient = bridgeHandler;
149 bridgeHandler.registerGroupStatusListener(this);
158 public void handleCommand(ChannelUID channelUID, Command command) {
159 handleCommand(channelUID.getId(), command, defaultFadeTime);
162 public void handleCommand(String channel, Command command, long fadeTime) {
163 HueClient bridgeHandler = getHueClient();
164 if (bridgeHandler == null) {
165 logger.debug("hue bridge handler not found. Cannot handle command without bridge.");
169 FullGroup group = bridgeHandler.getGroupById(groupId);
171 logger.debug("hue group not known on bridge. Cannot handle command.");
172 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
173 "@text/offline.conf-error-wrong-group-id");
177 Integer lastColorTemp;
178 StateUpdate newState = null;
181 if (command instanceof HSBType) {
182 HSBType hsbCommand = (HSBType) command;
183 if (hsbCommand.getBrightness().intValue() == 0) {
184 newState = LightStateConverter.toOnOffLightState(OnOffType.OFF);
186 newState = LightStateConverter.toColorLightState(hsbCommand, group.getState());
187 newState.setOn(true);
188 newState.setTransitionTime(fadeTime);
190 } else if (command instanceof PercentType) {
191 newState = LightStateConverter.toBrightnessLightState((PercentType) command);
192 newState.setTransitionTime(fadeTime);
193 } else if (command instanceof OnOffType) {
194 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
195 } else if (command instanceof IncreaseDecreaseType) {
196 newState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, group);
197 if (newState != null) {
198 newState.setTransitionTime(fadeTime);
202 case CHANNEL_COLORTEMPERATURE:
203 if (command instanceof PercentType) {
204 newState = LightStateConverter.toColorTemperatureLightState((PercentType) command);
205 newState.setTransitionTime(fadeTime);
206 } else if (command instanceof OnOffType) {
207 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
208 } else if (command instanceof IncreaseDecreaseType) {
209 newState = convertColorTempChangeToStateUpdate((IncreaseDecreaseType) command, group);
210 if (newState != null) {
211 newState.setTransitionTime(fadeTime);
215 case CHANNEL_BRIGHTNESS:
216 if (command instanceof PercentType) {
217 newState = LightStateConverter.toBrightnessLightState((PercentType) command);
218 newState.setTransitionTime(fadeTime);
219 } else if (command instanceof OnOffType) {
220 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
221 } else if (command instanceof IncreaseDecreaseType) {
222 newState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, group);
223 if (newState != null) {
224 newState.setTransitionTime(fadeTime);
227 lastColorTemp = lastSentColorTemp;
228 if (newState != null && lastColorTemp != null) {
229 // make sure that the light also has the latest color temp
230 // this might not have been yet set in the light, if it was off
231 newState.setColorTemperature(lastColorTemp);
232 newState.setTransitionTime(fadeTime);
236 if (command instanceof OnOffType) {
237 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
239 lastColorTemp = lastSentColorTemp;
240 if (newState != null && lastColorTemp != null) {
241 // make sure that the light also has the latest color temp
242 // this might not have been yet set in the light, if it was off
243 newState.setColorTemperature(lastColorTemp);
244 newState.setTransitionTime(fadeTime);
248 if (command instanceof StringType) {
249 newState = LightStateConverter.toAlertState((StringType) command);
250 if (newState == null) {
251 // Unsupported StringType is passed. Log a warning
252 // message and return.
253 logger.warn("Unsupported String command: {}. Supported commands are: {}, {}, {} ", command,
254 LightStateConverter.ALERT_MODE_NONE, LightStateConverter.ALERT_MODE_SELECT,
255 LightStateConverter.ALERT_MODE_LONG_SELECT);
258 scheduleAlertStateRestore(command);
263 if (command instanceof StringType) {
264 newState = new StateUpdate().setScene(command.toString());
270 if (newState != null) {
271 cacheNewState(newState);
272 bridgeHandler.updateGroupState(group, newState, fadeTime);
274 logger.debug("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel);
279 * Caches the new state that is sent to the bridge. This is necessary in case the lights are off when the values are
280 * sent. In this case, the values are not yet set in the lights.
282 * @param newState the state to be cached
284 private void cacheNewState(StateUpdate newState) {
285 Integer tmpBrightness = newState.getBrightness();
286 if (tmpBrightness != null) {
287 lastSentBrightness = tmpBrightness;
289 Integer tmpColorTemp = newState.getColorTemperature();
290 if (tmpColorTemp != null) {
291 lastSentColorTemp = tmpColorTemp;
295 private @Nullable StateUpdate convertColorTempChangeToStateUpdate(IncreaseDecreaseType command, FullGroup group) {
296 StateUpdate stateUpdate = null;
297 Integer currentColorTemp = getCurrentColorTemp(group.getState());
298 if (currentColorTemp != null) {
299 int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp);
300 stateUpdate = new StateUpdate().setColorTemperature(newColorTemp);
305 private @Nullable Integer getCurrentColorTemp(@Nullable State groupState) {
306 Integer colorTemp = lastSentColorTemp;
307 if (colorTemp == null && groupState != null) {
308 colorTemp = groupState.getColorTemperature();
313 private @Nullable StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullGroup group) {
314 Integer currentBrightness = getCurrentBrightness(group);
315 if (currentBrightness == null) {
318 int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness);
319 return createBrightnessStateUpdate(currentBrightness, newBrightness);
322 private @Nullable Integer getCurrentBrightness(FullGroup group) {
323 if (lastSentBrightness != null) {
324 return lastSentBrightness;
326 State currentState = group.getState();
327 if (currentState == null) {
330 return currentState.isOn() ? currentState.getBrightness() : 0;
333 private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) {
334 StateUpdate lightUpdate = new StateUpdate();
335 if (newBrightness == 0) {
336 lightUpdate.turnOff();
338 lightUpdate.setBrightness(newBrightness);
339 if (currentBrightness == 0) {
340 lightUpdate.turnOn();
347 public void channelLinked(ChannelUID channelUID) {
348 HueClient handler = getHueClient();
349 if (handler != null) {
350 FullGroup group = handler.getGroupById(groupId);
352 onGroupStateChanged(group);
358 public boolean onGroupStateChanged(FullGroup group) {
359 logger.trace("onGroupStateChanged() was called for group {}", group.getId());
361 State state = group.getState();
363 final FullGroup lastState = lastFullGroup;
364 if (lastState == null || !Objects.equals(lastState.getState(), state)) {
365 lastFullGroup = group;
370 logger.trace("New state for group {}", groupId);
372 lastSentColorTemp = null;
373 lastSentBrightness = null;
375 updateStatus(ThingStatus.ONLINE);
377 logger.debug("onGroupStateChanged Group {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}", group.getName(),
378 state.isOn(), state.getBrightness(), state.getHue(), state.getSaturation(), state.getColorTemperature(),
379 state.getColorMode(), state.getXY());
381 HSBType hsbType = LightStateConverter.toHSBType(state);
383 hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), new PercentType(0));
385 updateState(CHANNEL_COLOR, hsbType);
387 ColorMode colorMode = state.getColorMode();
388 if (ColorMode.CT.equals(colorMode)) {
389 PercentType colorTempPercentType = LightStateConverter.toColorTemperaturePercentType(state);
390 updateState(CHANNEL_COLORTEMPERATURE, colorTempPercentType);
392 updateState(CHANNEL_COLORTEMPERATURE, UnDefType.NULL);
395 PercentType brightnessPercentType = LightStateConverter.toBrightnessPercentType(state);
397 brightnessPercentType = new PercentType(0);
399 updateState(CHANNEL_BRIGHTNESS, brightnessPercentType);
401 updateState(CHANNEL_SWITCH, state.isOn() ? OnOffType.ON : OnOffType.OFF);
407 public void onGroupAdded(FullGroup group) {
408 onGroupStateChanged(group);
412 public void onGroupRemoved() {
413 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.group-removed");
417 public void onGroupGone() {
418 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.group-removed");
422 * Sets the state options for applicable scenes.
425 public void onScenesUpdated(List<Scene> updatedScenes) {
426 List<StateOption> stateOptions = Collections.emptyList();
427 consoleScenesList = new ArrayList<>();
428 HueClient handler = getHueClient();
429 if (handler != null) {
430 FullGroup group = handler.getGroupById(groupId);
432 stateOptions = updatedScenes.stream().filter(scene -> scene.isApplicableTo(group))
433 .map(Scene::toStateOption).collect(Collectors.toList());
434 consoleScenesList = updatedScenes
435 .stream().filter(scene -> scene.isApplicableTo(group)).map(scene -> "Id is \"" + scene.getId()
436 + "\" for scene \"" + scene.toStateOption().getLabel() + "\"")
437 .collect(Collectors.toList());
440 stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
445 * Schedules restoration of the alert item state to {@link LightStateConverter#ALERT_MODE_NONE} after a given time.
447 * Based on the initial command:
449 * <li>For {@link LightStateConverter#ALERT_MODE_SELECT} restoration will be triggered after <strong>2
451 * <li>For {@link LightStateConverter#ALERT_MODE_LONG_SELECT} restoration will be triggered after <strong>15
454 * This method also cancels any previously scheduled restoration.
456 * @param command The {@link Command} sent to the item
458 private void scheduleAlertStateRestore(Command command) {
459 cancelScheduledFuture();
460 int delay = getAlertDuration(command);
463 scheduledFuture = scheduler.schedule(() -> {
464 updateState(CHANNEL_ALERT, new StringType(LightStateConverter.ALERT_MODE_NONE));
465 }, delay, TimeUnit.MILLISECONDS);
470 * This method will cancel previously scheduled alert item state
473 private void cancelScheduledFuture() {
474 ScheduledFuture<?> scheduledJob = scheduledFuture;
475 if (scheduledJob != null) {
476 scheduledJob.cancel(true);
477 scheduledFuture = null;
482 * This method returns the time in <strong>milliseconds</strong> after
483 * which, the state of the alert item has to be restored to {@link LightStateConverter#ALERT_MODE_NONE}.
485 * @param command The initial command sent to the alert item.
486 * @return Based on the initial command will return:
488 * <li><strong>2000</strong> for {@link LightStateConverter#ALERT_MODE_SELECT}.
489 * <li><strong>15000</strong> for {@link LightStateConverter#ALERT_MODE_LONG_SELECT}.
490 * <li><strong>-1</strong> for any command different from the previous two.
493 private int getAlertDuration(Command command) {
495 switch (command.toString()) {
496 case LightStateConverter.ALERT_MODE_LONG_SELECT:
499 case LightStateConverter.ALERT_MODE_SELECT:
510 public List<String> listScenesForConsole() {
511 return consoleScenesList;
515 public String getGroupId() {