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.*;
17 import java.math.BigDecimal;
18 import java.util.List;
20 import java.util.Objects;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.stream.Collectors;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.hue.internal.FullGroup;
29 import org.openhab.binding.hue.internal.Scene;
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.dto.ColorTemperature;
34 import org.openhab.core.library.types.DecimalType;
35 import org.openhab.core.library.types.HSBType;
36 import org.openhab.core.library.types.IncreaseDecreaseType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.PercentType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.ThingStatusInfo;
46 import org.openhab.core.thing.ThingTypeUID;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.thing.binding.ThingHandler;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.StateOption;
51 import org.openhab.core.types.UnDefType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * {@link HueGroupHandler} is the handler for a hue group of lights. It uses the {@link HueClient} to execute the
59 * @author Laurent Garnier - Initial contribution
62 public class HueGroupHandler extends BaseThingHandler implements GroupStatusListener {
63 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GROUP);
64 public static final String PROPERTY_MEMBERS = "members";
66 private final Logger logger = LoggerFactory.getLogger(HueGroupHandler.class);
67 private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider;
69 private @NonNullByDefault({}) String groupId;
71 private @Nullable Integer lastSentColorTemp;
72 private @Nullable Integer lastSentBrightness;
74 private ColorTemperature colorTemperatureCapabilties = new ColorTemperature();
75 private long defaultFadeTime = 400;
77 private @Nullable HueClient hueClient;
79 private @Nullable ScheduledFuture<?> scheduledFuture;
80 private @Nullable FullGroup lastFullGroup;
82 private List<String> consoleScenesList = List.of();
84 public HueGroupHandler(Thing thing, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) {
86 this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
90 public void initialize() {
91 logger.debug("Initializing hue group handler.");
92 Bridge bridge = getBridge();
93 initializeThing((bridge == null) ? null : bridge.getStatus());
97 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
98 logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
99 initializeThing(bridgeStatusInfo.getStatus());
102 private void initializeThing(@Nullable ThingStatus bridgeStatus) {
103 logger.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus);
104 final String configGroupId = (String) getConfig().get(GROUP_ID);
105 if (configGroupId != null) {
106 BigDecimal time = (BigDecimal) getConfig().get(FADETIME);
108 defaultFadeTime = time.longValueExact();
111 groupId = configGroupId;
112 // note: this call implicitly registers our handler as a listener on the bridge
113 if (getHueClient() != null) {
114 if (bridgeStatus == ThingStatus.ONLINE) {
115 updateStatus(ThingStatus.ONLINE);
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
120 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
123 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
124 "@text/offline.conf-error-no-group-id");
128 private synchronized void initializeProperties(@Nullable FullGroup fullGroup) {
129 if (fullGroup != null) {
130 Map<String, String> properties = editProperties();
131 properties.put(PROPERTY_MEMBERS, fullGroup.getLightIds().stream().collect(Collectors.joining(",")));
132 updateProperties(properties);
137 public void dispose() {
138 logger.debug("Hue group handler disposes. Unregistering listener.");
139 cancelScheduledFuture();
140 if (groupId != null) {
141 HueClient bridgeHandler = getHueClient();
142 if (bridgeHandler != null) {
143 bridgeHandler.unregisterGroupStatusListener(this);
150 protected synchronized @Nullable HueClient getHueClient() {
151 if (hueClient == null) {
152 Bridge bridge = getBridge();
153 if (bridge == null) {
156 ThingHandler handler = bridge.getHandler();
157 if (handler instanceof HueBridgeHandler) {
158 HueClient bridgeHandler = (HueClient) handler;
159 hueClient = bridgeHandler;
160 bridgeHandler.registerGroupStatusListener(this);
169 public void handleCommand(ChannelUID channelUID, Command command) {
170 handleCommand(channelUID.getId(), command, defaultFadeTime);
173 public void handleCommand(String channel, Command command, long fadeTime) {
174 HueClient bridgeHandler = getHueClient();
175 if (bridgeHandler == null) {
176 logger.debug("hue bridge handler not found. Cannot handle command without bridge.");
180 FullGroup group = bridgeHandler.getGroupById(groupId);
182 logger.debug("hue group not known on bridge. Cannot handle command.");
183 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
184 "@text/offline.conf-error-wrong-group-id");
188 Integer lastColorTemp;
189 StateUpdate newState = null;
192 if (command instanceof HSBType) {
193 HSBType hsbCommand = (HSBType) command;
194 if (hsbCommand.getBrightness().intValue() == 0) {
195 newState = LightStateConverter.toOnOffLightState(OnOffType.OFF);
197 newState = LightStateConverter.toColorLightState(hsbCommand, group.getState());
198 newState.setOn(true);
199 newState.setTransitionTime(fadeTime);
201 } else if (command instanceof PercentType) {
202 newState = LightStateConverter.toBrightnessLightState((PercentType) command);
203 newState.setTransitionTime(fadeTime);
204 } else if (command instanceof OnOffType) {
205 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
206 } else if (command instanceof IncreaseDecreaseType) {
207 newState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, group);
208 if (newState != null) {
209 newState.setTransitionTime(fadeTime);
213 case CHANNEL_COLORTEMPERATURE:
214 if (command instanceof PercentType) {
215 newState = LightStateConverter.toColorTemperatureLightStateFromPercentType((PercentType) command,
216 colorTemperatureCapabilties);
217 newState.setTransitionTime(fadeTime);
218 } else if (command instanceof OnOffType) {
219 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
220 } else if (command instanceof IncreaseDecreaseType) {
221 newState = convertColorTempChangeToStateUpdate((IncreaseDecreaseType) command, group);
222 if (newState != null) {
223 newState.setTransitionTime(fadeTime);
227 case CHANNEL_COLORTEMPERATURE_ABS:
228 if (command instanceof DecimalType) {
229 newState = LightStateConverter.toColorTemperatureLightState((DecimalType) command,
230 colorTemperatureCapabilties);
231 newState.setTransitionTime(fadeTime);
234 case CHANNEL_BRIGHTNESS:
235 if (command instanceof PercentType) {
236 newState = LightStateConverter.toBrightnessLightState((PercentType) command);
237 newState.setTransitionTime(fadeTime);
238 } else if (command instanceof OnOffType) {
239 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
240 } else if (command instanceof IncreaseDecreaseType) {
241 newState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, group);
242 if (newState != null) {
243 newState.setTransitionTime(fadeTime);
246 lastColorTemp = lastSentColorTemp;
247 if (newState != null && lastColorTemp != null) {
248 // make sure that the light also has the latest color temp
249 // this might not have been yet set in the light, if it was off
250 newState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
251 newState.setTransitionTime(fadeTime);
255 if (command instanceof OnOffType) {
256 newState = LightStateConverter.toOnOffLightState((OnOffType) command);
258 lastColorTemp = lastSentColorTemp;
259 if (newState != null && lastColorTemp != null) {
260 // make sure that the light also has the latest color temp
261 // this might not have been yet set in the light, if it was off
262 newState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
263 newState.setTransitionTime(fadeTime);
267 if (command instanceof StringType) {
268 newState = LightStateConverter.toAlertState((StringType) command);
269 if (newState == null) {
270 // Unsupported StringType is passed. Log a warning
271 // message and return.
272 logger.warn("Unsupported String command: {}. Supported commands are: {}, {}, {} ", command,
273 LightStateConverter.ALERT_MODE_NONE, LightStateConverter.ALERT_MODE_SELECT,
274 LightStateConverter.ALERT_MODE_LONG_SELECT);
277 scheduleAlertStateRestore(command);
282 if (command instanceof StringType) {
283 newState = new StateUpdate().setScene(command.toString());
289 if (newState != null) {
290 cacheNewState(newState);
291 bridgeHandler.updateGroupState(group, newState, fadeTime);
293 logger.debug("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel);
298 * Caches the new state that is sent to the bridge. This is necessary in case the lights are off when the values are
299 * sent. In this case, the values are not yet set in the lights.
301 * @param newState the state to be cached
303 private void cacheNewState(StateUpdate newState) {
304 Integer tmpBrightness = newState.getBrightness();
305 if (tmpBrightness != null) {
306 lastSentBrightness = tmpBrightness;
308 Integer tmpColorTemp = newState.getColorTemperature();
309 if (tmpColorTemp != null) {
310 lastSentColorTemp = tmpColorTemp;
314 private @Nullable StateUpdate convertColorTempChangeToStateUpdate(IncreaseDecreaseType command, FullGroup group) {
315 StateUpdate stateUpdate = null;
316 Integer currentColorTemp = getCurrentColorTemp(group.getState());
317 if (currentColorTemp != null) {
318 int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp,
319 colorTemperatureCapabilties);
320 stateUpdate = new StateUpdate().setColorTemperature(newColorTemp, colorTemperatureCapabilties);
325 private @Nullable Integer getCurrentColorTemp(@Nullable State groupState) {
326 Integer colorTemp = lastSentColorTemp;
327 if (colorTemp == null && groupState != null) {
328 colorTemp = groupState.getColorTemperature();
333 private @Nullable StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullGroup group) {
334 Integer currentBrightness = getCurrentBrightness(group);
335 if (currentBrightness == null) {
338 int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness);
339 return createBrightnessStateUpdate(currentBrightness, newBrightness);
342 private @Nullable Integer getCurrentBrightness(FullGroup group) {
343 if (lastSentBrightness != null) {
344 return lastSentBrightness;
346 State currentState = group.getState();
347 if (currentState == null) {
350 return currentState.isOn() ? currentState.getBrightness() : 0;
353 private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) {
354 StateUpdate lightUpdate = new StateUpdate();
355 if (newBrightness == 0) {
356 lightUpdate.turnOff();
358 lightUpdate.setBrightness(newBrightness);
359 if (currentBrightness == 0) {
360 lightUpdate.turnOn();
367 public void channelLinked(ChannelUID channelUID) {
368 HueClient handler = getHueClient();
369 if (handler != null) {
370 FullGroup group = handler.getGroupById(groupId);
372 onGroupStateChanged(group);
378 public boolean onGroupStateChanged(FullGroup group) {
379 logger.trace("onGroupStateChanged() was called for group {}", group.getId());
381 State state = group.getState();
383 final FullGroup lastState = lastFullGroup;
384 if (lastState == null || !Objects.equals(lastState.getState(), state)) {
385 lastFullGroup = group;
390 logger.trace("New state for group {}", groupId);
392 initializeProperties(group);
394 lastSentColorTemp = null;
395 lastSentBrightness = null;
397 updateStatus(ThingStatus.ONLINE);
399 logger.debug("onGroupStateChanged Group {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}", group.getName(),
400 state.isOn(), state.getBrightness(), state.getHue(), state.getSaturation(), state.getColorTemperature(),
401 state.getColorMode(), state.getXY());
403 HSBType hsbType = LightStateConverter.toHSBType(state);
405 hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), PercentType.ZERO);
407 updateState(CHANNEL_COLOR, hsbType);
409 ColorMode colorMode = state.getColorMode();
410 if (ColorMode.CT.equals(colorMode)) {
411 updateState(CHANNEL_COLORTEMPERATURE,
412 LightStateConverter.toColorTemperaturePercentType(state, colorTemperatureCapabilties));
413 updateState(CHANNEL_COLORTEMPERATURE_ABS, LightStateConverter.toColorTemperature(state));
415 updateState(CHANNEL_COLORTEMPERATURE, UnDefType.UNDEF);
416 updateState(CHANNEL_COLORTEMPERATURE_ABS, UnDefType.UNDEF);
419 PercentType brightnessPercentType = LightStateConverter.toBrightnessPercentType(state);
421 brightnessPercentType = PercentType.ZERO;
423 updateState(CHANNEL_BRIGHTNESS, brightnessPercentType);
425 updateState(CHANNEL_SWITCH, OnOffType.from(state.isOn()));
431 public void onGroupAdded(FullGroup group) {
432 onGroupStateChanged(group);
436 public void onGroupRemoved() {
437 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.group-removed");
441 public void onGroupGone() {
442 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.group-removed");
446 * Sets the state options for applicable scenes.
449 public void onScenesUpdated(List<Scene> updatedScenes) {
450 List<StateOption> stateOptions = List.of();
451 consoleScenesList = List.of();
452 HueClient handler = getHueClient();
453 if (handler != null) {
454 FullGroup group = handler.getGroupById(groupId);
456 stateOptions = updatedScenes.stream().filter(scene -> scene.isApplicableTo(group))
457 .map(Scene::toStateOption).collect(Collectors.toList());
458 consoleScenesList = updatedScenes
459 .stream().filter(scene -> scene.isApplicableTo(group)).map(scene -> "Id is \"" + scene.getId()
460 + "\" for scene \"" + scene.toStateOption().getLabel() + "\"")
461 .collect(Collectors.toList());
464 stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
469 * Schedules restoration of the alert item state to {@link LightStateConverter#ALERT_MODE_NONE} after a given time.
471 * Based on the initial command:
473 * <li>For {@link LightStateConverter#ALERT_MODE_SELECT} restoration will be triggered after <strong>2
475 * <li>For {@link LightStateConverter#ALERT_MODE_LONG_SELECT} restoration will be triggered after <strong>15
478 * This method also cancels any previously scheduled restoration.
480 * @param command The {@link Command} sent to the item
482 private void scheduleAlertStateRestore(Command command) {
483 cancelScheduledFuture();
484 int delay = getAlertDuration(command);
487 scheduledFuture = scheduler.schedule(() -> {
488 updateState(CHANNEL_ALERT, new StringType(LightStateConverter.ALERT_MODE_NONE));
489 }, delay, TimeUnit.MILLISECONDS);
494 * This method will cancel previously scheduled alert item state
497 private void cancelScheduledFuture() {
498 ScheduledFuture<?> scheduledJob = scheduledFuture;
499 if (scheduledJob != null) {
500 scheduledJob.cancel(true);
501 scheduledFuture = null;
506 * This method returns the time in <strong>milliseconds</strong> after
507 * which, the state of the alert item has to be restored to {@link LightStateConverter#ALERT_MODE_NONE}.
509 * @param command The initial command sent to the alert item.
510 * @return Based on the initial command will return:
512 * <li><strong>2000</strong> for {@link LightStateConverter#ALERT_MODE_SELECT}.
513 * <li><strong>15000</strong> for {@link LightStateConverter#ALERT_MODE_LONG_SELECT}.
514 * <li><strong>-1</strong> for any command different from the previous two.
517 private int getAlertDuration(Command command) {
519 switch (command.toString()) {
520 case LightStateConverter.ALERT_MODE_LONG_SELECT:
523 case LightStateConverter.ALERT_MODE_SELECT:
534 public List<String> listScenesForConsole() {
535 return consoleScenesList;
539 public String getGroupId() {