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