]> git.basschouten.com Git - openhab-addons.git/blob
27cdb5f0af72e3c1f7abf9e5d7704b918cf15c5d
[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.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;
54
55 /**
56  * {@link HueGroupHandler} is the handler for a hue group of lights. It uses the {@link HueClient} to execute the
57  * actual command.
58  *
59  * @author Laurent Garnier - Initial contribution
60  */
61 @NonNullByDefault
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";
65
66     private final Logger logger = LoggerFactory.getLogger(HueGroupHandler.class);
67     private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider;
68
69     private @NonNullByDefault({}) String groupId;
70
71     private @Nullable Integer lastSentColorTemp;
72     private @Nullable Integer lastSentBrightness;
73
74     private ColorTemperature colorTemperatureCapabilties = new ColorTemperature();
75     private long defaultFadeTime = 400;
76
77     private @Nullable HueClient hueClient;
78
79     private @Nullable ScheduledFuture<?> scheduledFuture;
80     private @Nullable FullGroup lastFullGroup;
81
82     private List<String> consoleScenesList = List.of();
83
84     public HueGroupHandler(Thing thing, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) {
85         super(thing);
86         this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
87     }
88
89     @Override
90     public void initialize() {
91         logger.debug("Initializing hue group handler.");
92         Bridge bridge = getBridge();
93         initializeThing((bridge == null) ? null : bridge.getStatus());
94     }
95
96     @Override
97     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
98         logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
99         initializeThing(bridgeStatusInfo.getStatus());
100     }
101
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);
107             if (time != null) {
108                 defaultFadeTime = time.longValueExact();
109             }
110
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);
116                 } else {
117                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
118                 }
119             } else {
120                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
121             }
122         } else {
123             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
124                     "@text/offline.conf-error-no-group-id");
125         }
126     }
127
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);
133         }
134     }
135
136     @Override
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);
144                 hueClient = null;
145             }
146             groupId = null;
147         }
148     }
149
150     protected synchronized @Nullable HueClient getHueClient() {
151         if (hueClient == null) {
152             Bridge bridge = getBridge();
153             if (bridge == null) {
154                 return null;
155             }
156             ThingHandler handler = bridge.getHandler();
157             if (handler instanceof HueBridgeHandler) {
158                 HueClient bridgeHandler = (HueClient) handler;
159                 hueClient = bridgeHandler;
160                 bridgeHandler.registerGroupStatusListener(this);
161             } else {
162                 return null;
163             }
164         }
165         return hueClient;
166     }
167
168     @Override
169     public void handleCommand(ChannelUID channelUID, Command command) {
170         handleCommand(channelUID.getId(), command, defaultFadeTime);
171     }
172
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.");
177             return;
178         }
179
180         FullGroup group = bridgeHandler.getGroupById(groupId);
181         if (group == null) {
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");
185             return;
186         }
187
188         Integer lastColorTemp;
189         StateUpdate newState = null;
190         switch (channel) {
191             case CHANNEL_COLOR:
192                 if (command instanceof HSBType) {
193                     HSBType hsbCommand = (HSBType) command;
194                     if (hsbCommand.getBrightness().intValue() == 0) {
195                         newState = LightStateConverter.toOnOffLightState(OnOffType.OFF);
196                     } else {
197                         newState = LightStateConverter.toColorLightState(hsbCommand, group.getState());
198                         newState.setOn(true);
199                         newState.setTransitionTime(fadeTime);
200                     }
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);
210                     }
211                 }
212                 break;
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);
224                     }
225                 }
226                 break;
227             case CHANNEL_COLORTEMPERATURE_ABS:
228                 if (command instanceof DecimalType) {
229                     newState = LightStateConverter.toColorTemperatureLightState((DecimalType) command,
230                             colorTemperatureCapabilties);
231                     newState.setTransitionTime(fadeTime);
232                 }
233                 break;
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);
244                     }
245                 }
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);
252                 }
253                 break;
254             case CHANNEL_SWITCH:
255                 if (command instanceof OnOffType) {
256                     newState = LightStateConverter.toOnOffLightState((OnOffType) command);
257                 }
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);
264                 }
265                 break;
266             case CHANNEL_ALERT:
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);
275                         return;
276                     } else {
277                         scheduleAlertStateRestore(command);
278                     }
279                 }
280                 break;
281             case CHANNEL_SCENE:
282                 if (command instanceof StringType) {
283                     newState = new StateUpdate().setScene(command.toString());
284                 }
285                 break;
286             default:
287                 break;
288         }
289         if (newState != null) {
290             cacheNewState(newState);
291             bridgeHandler.updateGroupState(group, newState, fadeTime);
292         } else {
293             logger.debug("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel);
294         }
295     }
296
297     /**
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.
300      *
301      * @param newState the state to be cached
302      */
303     private void cacheNewState(StateUpdate newState) {
304         Integer tmpBrightness = newState.getBrightness();
305         if (tmpBrightness != null) {
306             lastSentBrightness = tmpBrightness;
307         }
308         Integer tmpColorTemp = newState.getColorTemperature();
309         if (tmpColorTemp != null) {
310             lastSentColorTemp = tmpColorTemp;
311         }
312     }
313
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);
321         }
322         return stateUpdate;
323     }
324
325     private @Nullable Integer getCurrentColorTemp(@Nullable State groupState) {
326         Integer colorTemp = lastSentColorTemp;
327         if (colorTemp == null && groupState != null) {
328             colorTemp = groupState.getColorTemperature();
329         }
330         return colorTemp;
331     }
332
333     private @Nullable StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullGroup group) {
334         Integer currentBrightness = getCurrentBrightness(group);
335         if (currentBrightness == null) {
336             return null;
337         }
338         int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness);
339         return createBrightnessStateUpdate(currentBrightness, newBrightness);
340     }
341
342     private @Nullable Integer getCurrentBrightness(FullGroup group) {
343         if (lastSentBrightness != null) {
344             return lastSentBrightness;
345         }
346         State currentState = group.getState();
347         if (currentState == null) {
348             return null;
349         }
350         return currentState.isOn() ? currentState.getBrightness() : 0;
351     }
352
353     private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) {
354         StateUpdate lightUpdate = new StateUpdate();
355         if (newBrightness == 0) {
356             lightUpdate.turnOff();
357         } else {
358             lightUpdate.setBrightness(newBrightness);
359             if (currentBrightness == 0) {
360                 lightUpdate.turnOn();
361             }
362         }
363         return lightUpdate;
364     }
365
366     @Override
367     public void channelLinked(ChannelUID channelUID) {
368         HueClient handler = getHueClient();
369         if (handler != null) {
370             FullGroup group = handler.getGroupById(groupId);
371             if (group != null) {
372                 onGroupStateChanged(group);
373             }
374         }
375     }
376
377     @Override
378     public boolean onGroupStateChanged(FullGroup group) {
379         logger.trace("onGroupStateChanged() was called for group {}", group.getId());
380
381         State state = group.getState();
382
383         final FullGroup lastState = lastFullGroup;
384         if (lastState == null || !Objects.equals(lastState.getState(), state)) {
385             lastFullGroup = group;
386         } else {
387             return true;
388         }
389
390         logger.trace("New state for group {}", groupId);
391
392         initializeProperties(group);
393
394         lastSentColorTemp = null;
395         lastSentBrightness = null;
396
397         updateStatus(ThingStatus.ONLINE);
398
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());
402
403         HSBType hsbType = LightStateConverter.toHSBType(state);
404         if (!state.isOn()) {
405             hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), PercentType.ZERO);
406         }
407         updateState(CHANNEL_COLOR, hsbType);
408
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));
414         } else {
415             updateState(CHANNEL_COLORTEMPERATURE, UnDefType.UNDEF);
416             updateState(CHANNEL_COLORTEMPERATURE_ABS, UnDefType.UNDEF);
417         }
418
419         PercentType brightnessPercentType = LightStateConverter.toBrightnessPercentType(state);
420         if (!state.isOn()) {
421             brightnessPercentType = PercentType.ZERO;
422         }
423         updateState(CHANNEL_BRIGHTNESS, brightnessPercentType);
424
425         updateState(CHANNEL_SWITCH, OnOffType.from(state.isOn()));
426
427         return true;
428     }
429
430     @Override
431     public void onGroupAdded(FullGroup group) {
432         onGroupStateChanged(group);
433     }
434
435     @Override
436     public void onGroupRemoved() {
437         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.group-removed");
438     }
439
440     @Override
441     public void onGroupGone() {
442         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.group-removed");
443     }
444
445     /**
446      * Sets the state options for applicable scenes.
447      */
448     @Override
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);
455             if (group != null) {
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());
462             }
463         }
464         stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE),
465                 stateOptions);
466     }
467
468     /**
469      * Schedules restoration of the alert item state to {@link LightStateConverter#ALERT_MODE_NONE} after a given time.
470      * <br>
471      * Based on the initial command:
472      * <ul>
473      * <li>For {@link LightStateConverter#ALERT_MODE_SELECT} restoration will be triggered after <strong>2
474      * seconds</strong>.
475      * <li>For {@link LightStateConverter#ALERT_MODE_LONG_SELECT} restoration will be triggered after <strong>15
476      * seconds</strong>.
477      * </ul>
478      * This method also cancels any previously scheduled restoration.
479      *
480      * @param command The {@link Command} sent to the item
481      */
482     private void scheduleAlertStateRestore(Command command) {
483         cancelScheduledFuture();
484         int delay = getAlertDuration(command);
485
486         if (delay > 0) {
487             scheduledFuture = scheduler.schedule(() -> {
488                 updateState(CHANNEL_ALERT, new StringType(LightStateConverter.ALERT_MODE_NONE));
489             }, delay, TimeUnit.MILLISECONDS);
490         }
491     }
492
493     /**
494      * This method will cancel previously scheduled alert item state
495      * restoration.
496      */
497     private void cancelScheduledFuture() {
498         ScheduledFuture<?> scheduledJob = scheduledFuture;
499         if (scheduledJob != null) {
500             scheduledJob.cancel(true);
501             scheduledFuture = null;
502         }
503     }
504
505     /**
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}.
508      *
509      * @param command The initial command sent to the alert item.
510      * @return Based on the initial command will return:
511      *         <ul>
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.
515      *         </ul>
516      */
517     private int getAlertDuration(Command command) {
518         int delay;
519         switch (command.toString()) {
520             case LightStateConverter.ALERT_MODE_LONG_SELECT:
521                 delay = 15000;
522                 break;
523             case LightStateConverter.ALERT_MODE_SELECT:
524                 delay = 2000;
525                 break;
526             default:
527                 delay = -1;
528                 break;
529         }
530
531         return delay;
532     }
533
534     public List<String> listScenesForConsole() {
535         return consoleScenesList;
536     }
537
538     @Override
539     public String getGroupId() {
540         return groupId;
541     }
542 }