]> git.basschouten.com Git - openhab-addons.git/blob
8d70da97d82851c4f750d754196e310df9a8e683
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.hue.internal.handler;
14
15 import static org.openhab.binding.hue.internal.HueBindingConstants.*;
16
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;
22 import java.util.Set;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.stream.Collectors;
26
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;
53
54 /**
55  * {@link HueGroupHandler} is the handler for a hue group of lights. It uses the {@link HueClient} to execute the
56  * actual command.
57  *
58  * @author Laurent Garnier - Initial contribution
59  */
60 @NonNullByDefault
61 public class HueGroupHandler extends BaseThingHandler implements GroupStatusListener {
62     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_GROUP);
63
64     private final Logger logger = LoggerFactory.getLogger(HueGroupHandler.class);
65     private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider;
66
67     private @NonNullByDefault({}) String groupId;
68
69     private @Nullable Integer lastSentColorTemp;
70     private @Nullable Integer lastSentBrightness;
71
72     private long defaultFadeTime = 400;
73
74     private @Nullable HueClient hueClient;
75
76     private @Nullable ScheduledFuture<?> scheduledFuture;
77     private @Nullable FullGroup lastFullGroup;
78
79     private List<String> consoleScenesList = new ArrayList<>();
80
81     public HueGroupHandler(Thing thing, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) {
82         super(thing);
83         this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
84     }
85
86     @Override
87     public void initialize() {
88         logger.debug("Initializing hue group handler.");
89         Bridge bridge = getBridge();
90         initializeThing((bridge == null) ? null : bridge.getStatus());
91     }
92
93     @Override
94     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
95         logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
96         initializeThing(bridgeStatusInfo.getStatus());
97     }
98
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);
104             if (time != null) {
105                 defaultFadeTime = time.longValueExact();
106             }
107
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);
113                 } else {
114                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
115                 }
116             } else {
117                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
118             }
119         } else {
120             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
121                     "@text/offline.conf-error-no-group-id");
122         }
123     }
124
125     @Override
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);
133                 hueClient = null;
134             }
135             groupId = null;
136         }
137     }
138
139     protected synchronized @Nullable HueClient getHueClient() {
140         if (hueClient == null) {
141             Bridge bridge = getBridge();
142             if (bridge == null) {
143                 return null;
144             }
145             ThingHandler handler = bridge.getHandler();
146             if (handler instanceof HueBridgeHandler) {
147                 HueClient bridgeHandler = (HueClient) handler;
148                 hueClient = bridgeHandler;
149                 bridgeHandler.registerGroupStatusListener(this);
150             } else {
151                 return null;
152             }
153         }
154         return hueClient;
155     }
156
157     @Override
158     public void handleCommand(ChannelUID channelUID, Command command) {
159         handleCommand(channelUID.getId(), command, defaultFadeTime);
160     }
161
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.");
166             return;
167         }
168
169         FullGroup group = bridgeHandler.getGroupById(groupId);
170         if (group == null) {
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");
174             return;
175         }
176
177         Integer lastColorTemp;
178         StateUpdate newState = null;
179         switch (channel) {
180             case CHANNEL_COLOR:
181                 if (command instanceof HSBType) {
182                     HSBType hsbCommand = (HSBType) command;
183                     if (hsbCommand.getBrightness().intValue() == 0) {
184                         newState = LightStateConverter.toOnOffLightState(OnOffType.OFF);
185                     } else {
186                         newState = LightStateConverter.toColorLightState(hsbCommand, group.getState());
187                         newState.setOn(true);
188                         newState.setTransitionTime(fadeTime);
189                     }
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);
199                     }
200                 }
201                 break;
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);
212                     }
213                 }
214                 break;
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);
225                     }
226                 }
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);
233                 }
234                 break;
235             case CHANNEL_SWITCH:
236                 if (command instanceof OnOffType) {
237                     newState = LightStateConverter.toOnOffLightState((OnOffType) command);
238                 }
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);
245                 }
246                 break;
247             case CHANNEL_ALERT:
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);
256                         return;
257                     } else {
258                         scheduleAlertStateRestore(command);
259                     }
260                 }
261                 break;
262             case CHANNEL_SCENE:
263                 if (command instanceof StringType) {
264                     newState = new StateUpdate().setScene(command.toString());
265                 }
266                 break;
267             default:
268                 break;
269         }
270         if (newState != null) {
271             cacheNewState(newState);
272             bridgeHandler.updateGroupState(group, newState, fadeTime);
273         } else {
274             logger.debug("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel);
275         }
276     }
277
278     /**
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.
281      *
282      * @param newState the state to be cached
283      */
284     private void cacheNewState(StateUpdate newState) {
285         Integer tmpBrightness = newState.getBrightness();
286         if (tmpBrightness != null) {
287             lastSentBrightness = tmpBrightness;
288         }
289         Integer tmpColorTemp = newState.getColorTemperature();
290         if (tmpColorTemp != null) {
291             lastSentColorTemp = tmpColorTemp;
292         }
293     }
294
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);
301         }
302         return stateUpdate;
303     }
304
305     private @Nullable Integer getCurrentColorTemp(@Nullable State groupState) {
306         Integer colorTemp = lastSentColorTemp;
307         if (colorTemp == null && groupState != null) {
308             colorTemp = groupState.getColorTemperature();
309         }
310         return colorTemp;
311     }
312
313     private @Nullable StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullGroup group) {
314         Integer currentBrightness = getCurrentBrightness(group);
315         if (currentBrightness == null) {
316             return null;
317         }
318         int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness);
319         return createBrightnessStateUpdate(currentBrightness, newBrightness);
320     }
321
322     private @Nullable Integer getCurrentBrightness(FullGroup group) {
323         if (lastSentBrightness != null) {
324             return lastSentBrightness;
325         }
326         State currentState = group.getState();
327         if (currentState == null) {
328             return null;
329         }
330         return currentState.isOn() ? currentState.getBrightness() : 0;
331     }
332
333     private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) {
334         StateUpdate lightUpdate = new StateUpdate();
335         if (newBrightness == 0) {
336             lightUpdate.turnOff();
337         } else {
338             lightUpdate.setBrightness(newBrightness);
339             if (currentBrightness == 0) {
340                 lightUpdate.turnOn();
341             }
342         }
343         return lightUpdate;
344     }
345
346     @Override
347     public void channelLinked(ChannelUID channelUID) {
348         HueClient handler = getHueClient();
349         if (handler != null) {
350             FullGroup group = handler.getGroupById(groupId);
351             if (group != null) {
352                 onGroupStateChanged(group);
353             }
354         }
355     }
356
357     @Override
358     public boolean onGroupStateChanged(FullGroup group) {
359         logger.trace("onGroupStateChanged() was called for group {}", group.getId());
360
361         State state = group.getState();
362
363         final FullGroup lastState = lastFullGroup;
364         if (lastState == null || !Objects.equals(lastState.getState(), state)) {
365             lastFullGroup = group;
366         } else {
367             return true;
368         }
369
370         logger.trace("New state for group {}", groupId);
371
372         lastSentColorTemp = null;
373         lastSentBrightness = null;
374
375         updateStatus(ThingStatus.ONLINE);
376
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());
380
381         HSBType hsbType = LightStateConverter.toHSBType(state);
382         if (!state.isOn()) {
383             hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), new PercentType(0));
384         }
385         updateState(CHANNEL_COLOR, hsbType);
386
387         ColorMode colorMode = state.getColorMode();
388         if (ColorMode.CT.equals(colorMode)) {
389             PercentType colorTempPercentType = LightStateConverter.toColorTemperaturePercentType(state);
390             updateState(CHANNEL_COLORTEMPERATURE, colorTempPercentType);
391         } else {
392             updateState(CHANNEL_COLORTEMPERATURE, UnDefType.NULL);
393         }
394
395         PercentType brightnessPercentType = LightStateConverter.toBrightnessPercentType(state);
396         if (!state.isOn()) {
397             brightnessPercentType = new PercentType(0);
398         }
399         updateState(CHANNEL_BRIGHTNESS, brightnessPercentType);
400
401         updateState(CHANNEL_SWITCH, state.isOn() ? OnOffType.ON : OnOffType.OFF);
402
403         return true;
404     }
405
406     @Override
407     public void onGroupAdded(FullGroup group) {
408         onGroupStateChanged(group);
409     }
410
411     @Override
412     public void onGroupRemoved() {
413         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.group-removed");
414     }
415
416     @Override
417     public void onGroupGone() {
418         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.group-removed");
419     }
420
421     /**
422      * Sets the state options for applicable scenes.
423      */
424     @Override
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);
431             if (group != null) {
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());
438             }
439         }
440         stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
441                 stateOptions);
442     }
443
444     /**
445      * Schedules restoration of the alert item state to {@link LightStateConverter#ALERT_MODE_NONE} after a given time.
446      * <br>
447      * Based on the initial command:
448      * <ul>
449      * <li>For {@link LightStateConverter#ALERT_MODE_SELECT} restoration will be triggered after <strong>2
450      * seconds</strong>.
451      * <li>For {@link LightStateConverter#ALERT_MODE_LONG_SELECT} restoration will be triggered after <strong>15
452      * seconds</strong>.
453      * </ul>
454      * This method also cancels any previously scheduled restoration.
455      *
456      * @param command The {@link Command} sent to the item
457      */
458     private void scheduleAlertStateRestore(Command command) {
459         cancelScheduledFuture();
460         int delay = getAlertDuration(command);
461
462         if (delay > 0) {
463             scheduledFuture = scheduler.schedule(() -> {
464                 updateState(CHANNEL_ALERT, new StringType(LightStateConverter.ALERT_MODE_NONE));
465             }, delay, TimeUnit.MILLISECONDS);
466         }
467     }
468
469     /**
470      * This method will cancel previously scheduled alert item state
471      * restoration.
472      */
473     private void cancelScheduledFuture() {
474         ScheduledFuture<?> scheduledJob = scheduledFuture;
475         if (scheduledJob != null) {
476             scheduledJob.cancel(true);
477             scheduledFuture = null;
478         }
479     }
480
481     /**
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}.
484      *
485      * @param command The initial command sent to the alert item.
486      * @return Based on the initial command will return:
487      *         <ul>
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.
491      *         </ul>
492      */
493     private int getAlertDuration(Command command) {
494         int delay;
495         switch (command.toString()) {
496             case LightStateConverter.ALERT_MODE_LONG_SELECT:
497                 delay = 15000;
498                 break;
499             case LightStateConverter.ALERT_MODE_SELECT:
500                 delay = 2000;
501                 break;
502             default:
503                 delay = -1;
504                 break;
505         }
506
507         return delay;
508     }
509
510     public List<String> listScenesForConsole() {
511         return consoleScenesList;
512     }
513
514     @Override
515     public String getGroupId() {
516         return groupId;
517     }
518 }