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