]> git.basschouten.com Git - openhab-addons.git/blob
a8e87730afe9b50958c9e97235a8d87cc6516e1a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.List;
19 import java.util.Objects;
20 import java.util.Set;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.stream.Collectors;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.hue.internal.FullGroup;
28 import org.openhab.binding.hue.internal.Scene;
29 import org.openhab.binding.hue.internal.State;
30 import org.openhab.binding.hue.internal.State.ColorMode;
31 import org.openhab.binding.hue.internal.StateUpdate;
32 import org.openhab.binding.hue.internal.dto.ColorTemperature;
33 import org.openhab.core.library.types.DecimalType;
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 = Set.of(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 ColorTemperature colorTemperatureCapabilties = new ColorTemperature();
73     private long defaultFadeTime = 400;
74
75     private @Nullable HueClient hueClient;
76
77     private @Nullable ScheduledFuture<?> scheduledFuture;
78     private @Nullable FullGroup lastFullGroup;
79
80     private List<String> consoleScenesList = List.of();
81
82     public HueGroupHandler(Thing thing, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) {
83         super(thing);
84         this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
85     }
86
87     @Override
88     public void initialize() {
89         logger.debug("Initializing hue group handler.");
90         Bridge bridge = getBridge();
91         initializeThing((bridge == null) ? null : bridge.getStatus());
92     }
93
94     @Override
95     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
96         logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
97         initializeThing(bridgeStatusInfo.getStatus());
98     }
99
100     private void initializeThing(@Nullable ThingStatus bridgeStatus) {
101         logger.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus);
102         final String configGroupId = (String) getConfig().get(GROUP_ID);
103         if (configGroupId != null) {
104             BigDecimal time = (BigDecimal) getConfig().get(FADETIME);
105             if (time != null) {
106                 defaultFadeTime = time.longValueExact();
107             }
108
109             groupId = configGroupId;
110             // note: this call implicitly registers our handler as a listener on the bridge
111             if (getHueClient() != null) {
112                 if (bridgeStatus == ThingStatus.ONLINE) {
113                     updateStatus(ThingStatus.ONLINE);
114                 } else {
115                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
116                 }
117             } else {
118                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
119             }
120         } else {
121             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
122                     "@text/offline.conf-error-no-group-id");
123         }
124     }
125
126     @Override
127     public void dispose() {
128         logger.debug("Hue group handler disposes. Unregistering listener.");
129         cancelScheduledFuture();
130         if (groupId != null) {
131             HueClient bridgeHandler = getHueClient();
132             if (bridgeHandler != null) {
133                 bridgeHandler.unregisterGroupStatusListener(this);
134                 hueClient = null;
135             }
136             groupId = null;
137         }
138     }
139
140     protected synchronized @Nullable HueClient getHueClient() {
141         if (hueClient == null) {
142             Bridge bridge = getBridge();
143             if (bridge == null) {
144                 return null;
145             }
146             ThingHandler handler = bridge.getHandler();
147             if (handler instanceof HueBridgeHandler) {
148                 HueClient bridgeHandler = (HueClient) handler;
149                 hueClient = bridgeHandler;
150                 bridgeHandler.registerGroupStatusListener(this);
151             } else {
152                 return null;
153             }
154         }
155         return hueClient;
156     }
157
158     @Override
159     public void handleCommand(ChannelUID channelUID, Command command) {
160         handleCommand(channelUID.getId(), command, defaultFadeTime);
161     }
162
163     public void handleCommand(String channel, Command command, long fadeTime) {
164         HueClient bridgeHandler = getHueClient();
165         if (bridgeHandler == null) {
166             logger.debug("hue bridge handler not found. Cannot handle command without bridge.");
167             return;
168         }
169
170         FullGroup group = bridgeHandler.getGroupById(groupId);
171         if (group == null) {
172             logger.debug("hue group not known on bridge. Cannot handle command.");
173             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
174                     "@text/offline.conf-error-wrong-group-id");
175             return;
176         }
177
178         Integer lastColorTemp;
179         StateUpdate newState = null;
180         switch (channel) {
181             case CHANNEL_COLOR:
182                 if (command instanceof HSBType) {
183                     HSBType hsbCommand = (HSBType) command;
184                     if (hsbCommand.getBrightness().intValue() == 0) {
185                         newState = LightStateConverter.toOnOffLightState(OnOffType.OFF);
186                     } else {
187                         newState = LightStateConverter.toColorLightState(hsbCommand, group.getState());
188                         newState.setOn(true);
189                         newState.setTransitionTime(fadeTime);
190                     }
191                 } else if (command instanceof PercentType) {
192                     newState = LightStateConverter.toBrightnessLightState((PercentType) command);
193                     newState.setTransitionTime(fadeTime);
194                 } else if (command instanceof OnOffType) {
195                     newState = LightStateConverter.toOnOffLightState((OnOffType) command);
196                 } else if (command instanceof IncreaseDecreaseType) {
197                     newState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, group);
198                     if (newState != null) {
199                         newState.setTransitionTime(fadeTime);
200                     }
201                 }
202                 break;
203             case CHANNEL_COLORTEMPERATURE:
204                 if (command instanceof PercentType) {
205                     newState = LightStateConverter.toColorTemperatureLightStateFromPercentType((PercentType) command,
206                             colorTemperatureCapabilties);
207                     newState.setTransitionTime(fadeTime);
208                 } else if (command instanceof OnOffType) {
209                     newState = LightStateConverter.toOnOffLightState((OnOffType) command);
210                 } else if (command instanceof IncreaseDecreaseType) {
211                     newState = convertColorTempChangeToStateUpdate((IncreaseDecreaseType) command, group);
212                     if (newState != null) {
213                         newState.setTransitionTime(fadeTime);
214                     }
215                 }
216                 break;
217             case CHANNEL_COLORTEMPERATURE_ABS:
218                 if (command instanceof DecimalType) {
219                     newState = LightStateConverter.toColorTemperatureLightState((DecimalType) command,
220                             colorTemperatureCapabilties);
221                     newState.setTransitionTime(fadeTime);
222                 }
223                 break;
224             case CHANNEL_BRIGHTNESS:
225                 if (command instanceof PercentType) {
226                     newState = LightStateConverter.toBrightnessLightState((PercentType) command);
227                     newState.setTransitionTime(fadeTime);
228                 } else if (command instanceof OnOffType) {
229                     newState = LightStateConverter.toOnOffLightState((OnOffType) command);
230                 } else if (command instanceof IncreaseDecreaseType) {
231                     newState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, group);
232                     if (newState != null) {
233                         newState.setTransitionTime(fadeTime);
234                     }
235                 }
236                 lastColorTemp = lastSentColorTemp;
237                 if (newState != null && lastColorTemp != null) {
238                     // make sure that the light also has the latest color temp
239                     // this might not have been yet set in the light, if it was off
240                     newState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
241                     newState.setTransitionTime(fadeTime);
242                 }
243                 break;
244             case CHANNEL_SWITCH:
245                 if (command instanceof OnOffType) {
246                     newState = LightStateConverter.toOnOffLightState((OnOffType) command);
247                 }
248                 lastColorTemp = lastSentColorTemp;
249                 if (newState != null && lastColorTemp != null) {
250                     // make sure that the light also has the latest color temp
251                     // this might not have been yet set in the light, if it was off
252                     newState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
253                     newState.setTransitionTime(fadeTime);
254                 }
255                 break;
256             case CHANNEL_ALERT:
257                 if (command instanceof StringType) {
258                     newState = LightStateConverter.toAlertState((StringType) command);
259                     if (newState == null) {
260                         // Unsupported StringType is passed. Log a warning
261                         // message and return.
262                         logger.warn("Unsupported String command: {}. Supported commands are: {}, {}, {} ", command,
263                                 LightStateConverter.ALERT_MODE_NONE, LightStateConverter.ALERT_MODE_SELECT,
264                                 LightStateConverter.ALERT_MODE_LONG_SELECT);
265                         return;
266                     } else {
267                         scheduleAlertStateRestore(command);
268                     }
269                 }
270                 break;
271             case CHANNEL_SCENE:
272                 if (command instanceof StringType) {
273                     newState = new StateUpdate().setScene(command.toString());
274                 }
275                 break;
276             default:
277                 break;
278         }
279         if (newState != null) {
280             cacheNewState(newState);
281             bridgeHandler.updateGroupState(group, newState, fadeTime);
282         } else {
283             logger.debug("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel);
284         }
285     }
286
287     /**
288      * Caches the new state that is sent to the bridge. This is necessary in case the lights are off when the values are
289      * sent. In this case, the values are not yet set in the lights.
290      *
291      * @param newState the state to be cached
292      */
293     private void cacheNewState(StateUpdate newState) {
294         Integer tmpBrightness = newState.getBrightness();
295         if (tmpBrightness != null) {
296             lastSentBrightness = tmpBrightness;
297         }
298         Integer tmpColorTemp = newState.getColorTemperature();
299         if (tmpColorTemp != null) {
300             lastSentColorTemp = tmpColorTemp;
301         }
302     }
303
304     private @Nullable StateUpdate convertColorTempChangeToStateUpdate(IncreaseDecreaseType command, FullGroup group) {
305         StateUpdate stateUpdate = null;
306         Integer currentColorTemp = getCurrentColorTemp(group.getState());
307         if (currentColorTemp != null) {
308             int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp,
309                     colorTemperatureCapabilties);
310             stateUpdate = new StateUpdate().setColorTemperature(newColorTemp, colorTemperatureCapabilties);
311         }
312         return stateUpdate;
313     }
314
315     private @Nullable Integer getCurrentColorTemp(@Nullable State groupState) {
316         Integer colorTemp = lastSentColorTemp;
317         if (colorTemp == null && groupState != null) {
318             colorTemp = groupState.getColorTemperature();
319         }
320         return colorTemp;
321     }
322
323     private @Nullable StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullGroup group) {
324         Integer currentBrightness = getCurrentBrightness(group);
325         if (currentBrightness == null) {
326             return null;
327         }
328         int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness);
329         return createBrightnessStateUpdate(currentBrightness, newBrightness);
330     }
331
332     private @Nullable Integer getCurrentBrightness(FullGroup group) {
333         if (lastSentBrightness != null) {
334             return lastSentBrightness;
335         }
336         State currentState = group.getState();
337         if (currentState == null) {
338             return null;
339         }
340         return currentState.isOn() ? currentState.getBrightness() : 0;
341     }
342
343     private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) {
344         StateUpdate lightUpdate = new StateUpdate();
345         if (newBrightness == 0) {
346             lightUpdate.turnOff();
347         } else {
348             lightUpdate.setBrightness(newBrightness);
349             if (currentBrightness == 0) {
350                 lightUpdate.turnOn();
351             }
352         }
353         return lightUpdate;
354     }
355
356     @Override
357     public void channelLinked(ChannelUID channelUID) {
358         HueClient handler = getHueClient();
359         if (handler != null) {
360             FullGroup group = handler.getGroupById(groupId);
361             if (group != null) {
362                 onGroupStateChanged(group);
363             }
364         }
365     }
366
367     @Override
368     public boolean onGroupStateChanged(FullGroup group) {
369         logger.trace("onGroupStateChanged() was called for group {}", group.getId());
370
371         State state = group.getState();
372
373         final FullGroup lastState = lastFullGroup;
374         if (lastState == null || !Objects.equals(lastState.getState(), state)) {
375             lastFullGroup = group;
376         } else {
377             return true;
378         }
379
380         logger.trace("New state for group {}", groupId);
381
382         lastSentColorTemp = null;
383         lastSentBrightness = null;
384
385         updateStatus(ThingStatus.ONLINE);
386
387         logger.debug("onGroupStateChanged Group {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}", group.getName(),
388                 state.isOn(), state.getBrightness(), state.getHue(), state.getSaturation(), state.getColorTemperature(),
389                 state.getColorMode(), state.getXY());
390
391         HSBType hsbType = LightStateConverter.toHSBType(state);
392         if (!state.isOn()) {
393             hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), PercentType.ZERO);
394         }
395         updateState(CHANNEL_COLOR, hsbType);
396
397         ColorMode colorMode = state.getColorMode();
398         if (ColorMode.CT.equals(colorMode)) {
399             updateState(CHANNEL_COLORTEMPERATURE,
400                     LightStateConverter.toColorTemperaturePercentType(state, colorTemperatureCapabilties));
401             updateState(CHANNEL_COLORTEMPERATURE_ABS, LightStateConverter.toColorTemperature(state));
402         } else {
403             updateState(CHANNEL_COLORTEMPERATURE, UnDefType.UNDEF);
404             updateState(CHANNEL_COLORTEMPERATURE_ABS, UnDefType.UNDEF);
405         }
406
407         PercentType brightnessPercentType = LightStateConverter.toBrightnessPercentType(state);
408         if (!state.isOn()) {
409             brightnessPercentType = PercentType.ZERO;
410         }
411         updateState(CHANNEL_BRIGHTNESS, brightnessPercentType);
412
413         updateState(CHANNEL_SWITCH, OnOffType.from(state.isOn()));
414
415         return true;
416     }
417
418     @Override
419     public void onGroupAdded(FullGroup group) {
420         onGroupStateChanged(group);
421     }
422
423     @Override
424     public void onGroupRemoved() {
425         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.group-removed");
426     }
427
428     @Override
429     public void onGroupGone() {
430         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.group-removed");
431     }
432
433     /**
434      * Sets the state options for applicable scenes.
435      */
436     @Override
437     public void onScenesUpdated(List<Scene> updatedScenes) {
438         List<StateOption> stateOptions = List.of();
439         consoleScenesList = List.of();
440         HueClient handler = getHueClient();
441         if (handler != null) {
442             FullGroup group = handler.getGroupById(groupId);
443             if (group != null) {
444                 stateOptions = updatedScenes.stream().filter(scene -> scene.isApplicableTo(group))
445                         .map(Scene::toStateOption).collect(Collectors.toList());
446                 consoleScenesList = updatedScenes
447                         .stream().filter(scene -> scene.isApplicableTo(group)).map(scene -> "Id is \"" + scene.getId()
448                                 + "\" for scene \"" + scene.toStateOption().getLabel() + "\"")
449                         .collect(Collectors.toList());
450             }
451         }
452         stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
453                 stateOptions);
454     }
455
456     /**
457      * Schedules restoration of the alert item state to {@link LightStateConverter#ALERT_MODE_NONE} after a given time.
458      * <br>
459      * Based on the initial command:
460      * <ul>
461      * <li>For {@link LightStateConverter#ALERT_MODE_SELECT} restoration will be triggered after <strong>2
462      * seconds</strong>.
463      * <li>For {@link LightStateConverter#ALERT_MODE_LONG_SELECT} restoration will be triggered after <strong>15
464      * seconds</strong>.
465      * </ul>
466      * This method also cancels any previously scheduled restoration.
467      *
468      * @param command The {@link Command} sent to the item
469      */
470     private void scheduleAlertStateRestore(Command command) {
471         cancelScheduledFuture();
472         int delay = getAlertDuration(command);
473
474         if (delay > 0) {
475             scheduledFuture = scheduler.schedule(() -> {
476                 updateState(CHANNEL_ALERT, new StringType(LightStateConverter.ALERT_MODE_NONE));
477             }, delay, TimeUnit.MILLISECONDS);
478         }
479     }
480
481     /**
482      * This method will cancel previously scheduled alert item state
483      * restoration.
484      */
485     private void cancelScheduledFuture() {
486         ScheduledFuture<?> scheduledJob = scheduledFuture;
487         if (scheduledJob != null) {
488             scheduledJob.cancel(true);
489             scheduledFuture = null;
490         }
491     }
492
493     /**
494      * This method returns the time in <strong>milliseconds</strong> after
495      * which, the state of the alert item has to be restored to {@link LightStateConverter#ALERT_MODE_NONE}.
496      *
497      * @param command The initial command sent to the alert item.
498      * @return Based on the initial command will return:
499      *         <ul>
500      *         <li><strong>2000</strong> for {@link LightStateConverter#ALERT_MODE_SELECT}.
501      *         <li><strong>15000</strong> for {@link LightStateConverter#ALERT_MODE_LONG_SELECT}.
502      *         <li><strong>-1</strong> for any command different from the previous two.
503      *         </ul>
504      */
505     private int getAlertDuration(Command command) {
506         int delay;
507         switch (command.toString()) {
508             case LightStateConverter.ALERT_MODE_LONG_SELECT:
509                 delay = 15000;
510                 break;
511             case LightStateConverter.ALERT_MODE_SELECT:
512                 delay = 2000;
513                 break;
514             default:
515                 delay = -1;
516                 break;
517         }
518
519         return delay;
520     }
521
522     public List<String> listScenesForConsole() {
523         return consoleScenesList;
524     }
525
526     @Override
527     public String getGroupId() {
528         return groupId;
529     }
530 }