]> git.basschouten.com Git - openhab-addons.git/blob
1e0985c5b9c52d2de67b2ab480efa24ef3e03353
[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.deconz.internal.handler;
14
15 import static org.openhab.binding.deconz.internal.BindingConstants.*;
16 import static org.openhab.binding.deconz.internal.Util.constrainToRange;
17 import static org.openhab.binding.deconz.internal.Util.kelvinToMired;
18
19 import java.util.Collection;
20 import java.util.Map;
21 import java.util.Set;
22 import java.util.concurrent.CompletableFuture;
23 import java.util.stream.Collectors;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.openhab.binding.deconz.internal.DeconzDynamicCommandDescriptionProvider;
27 import org.openhab.binding.deconz.internal.Util;
28 import org.openhab.binding.deconz.internal.action.GroupActions;
29 import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
30 import org.openhab.binding.deconz.internal.dto.GroupAction;
31 import org.openhab.binding.deconz.internal.dto.GroupMessage;
32 import org.openhab.binding.deconz.internal.dto.GroupState;
33 import org.openhab.binding.deconz.internal.dto.Scene;
34 import org.openhab.binding.deconz.internal.types.ResourceType;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.HSBType;
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.Channel;
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.ThingTypeUID;
45 import org.openhab.core.thing.binding.ThingHandlerService;
46 import org.openhab.core.types.Command;
47 import org.openhab.core.types.RefreshType;
48 import org.openhab.core.types.State;
49 import org.openhab.core.types.UnDefType;
50 import org.openhab.core.util.ColorUtil;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import com.google.gson.Gson;
55
56 /**
57  * This group thing doesn't establish any connections, that is done by the bridge Thing.
58  *
59  * It waits for the bridge to come online, grab the websocket connection and bridge configuration
60  * and registers to the websocket connection as a listener.
61  *
62  * @author Jan N. Klug - Initial contribution
63  */
64 @NonNullByDefault
65 public class GroupThingHandler extends DeconzBaseThingHandler {
66     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_LIGHTGROUP);
67     private final Logger logger = LoggerFactory.getLogger(GroupThingHandler.class);
68     private final DeconzDynamicCommandDescriptionProvider commandDescriptionProvider;
69
70     private Map<String, String> scenes = Map.of();
71     private GroupState groupStateCache = new GroupState();
72     private String colorMode = "";
73
74     public GroupThingHandler(Thing thing, Gson gson,
75             DeconzDynamicCommandDescriptionProvider commandDescriptionProvider) {
76         super(thing, gson, ResourceType.GROUPS);
77         this.commandDescriptionProvider = commandDescriptionProvider;
78     }
79
80     @Override
81     public void initialize() {
82         ThingConfig thingConfig = getConfigAs(ThingConfig.class);
83         colorMode = thingConfig.colormode;
84
85         super.initialize();
86     }
87
88     @Override
89     public void handleCommand(ChannelUID channelUID, Command command) {
90         String channelId = channelUID.getId();
91
92         GroupAction newGroupAction = new GroupAction();
93         switch (channelId) {
94             case CHANNEL_ALL_ON, CHANNEL_ANY_ON -> {
95                 if (command instanceof RefreshType) {
96                     valueUpdated(channelUID, groupStateCache);
97                     return;
98                 }
99             }
100             case CHANNEL_ALERT -> {
101                 if (command instanceof StringType) {
102                     newGroupAction.alert = command.toString();
103                 } else {
104                     return;
105                 }
106             }
107             case CHANNEL_COLOR -> {
108                 if (command instanceof OnOffType) {
109                     newGroupAction.on = (command == OnOffType.ON);
110                 } else if (command instanceof HSBType hsbCommand) {
111                     // XY color is the implicit default: Use XY color mode if i) no color mode is set or ii) if the bulb
112                     // is in CT mode or iii) already in XY mode. Only if the bulb is in HS mode, use this one.
113                     if ("hs".equals(colorMode)) {
114                         newGroupAction.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
115                         newGroupAction.sat = Util.fromPercentType(hsbCommand.getSaturation());
116                         newGroupAction.bri = Util.fromPercentType(hsbCommand.getBrightness());
117                     } else {
118                         double[] xy = ColorUtil.hsbToXY(hsbCommand);
119                         newGroupAction.xy = new double[] { xy[0], xy[1] };
120                         newGroupAction.bri = (int) (xy[2] * BRIGHTNESS_MAX);
121                     }
122                 } else if (command instanceof PercentType percentCommand) {
123                     newGroupAction.bri = Util.fromPercentType(percentCommand);
124                 } else if (command instanceof DecimalType decimalCommand) {
125                     newGroupAction.bri = decimalCommand.intValue();
126                 } else {
127                     return;
128                 }
129
130                 // send on/off state together with brightness if not already set or unknown
131                 Integer newBri = newGroupAction.bri;
132                 if (newBri != null) {
133                     newGroupAction.on = (newBri > 0);
134                 }
135                 Double transitiontime = config.transitiontime;
136                 if (transitiontime != null) {
137                     // value is in 1/10 seconds
138                     newGroupAction.transitiontime = (int) Math.round(10 * transitiontime);
139                 }
140             }
141             case CHANNEL_COLOR_TEMPERATURE -> {
142                 if (command instanceof DecimalType decimalCommand) {
143                     int miredValue = kelvinToMired(decimalCommand.intValue());
144                     newGroupAction.ct = constrainToRange(miredValue, ZCL_CT_MIN, ZCL_CT_MAX);
145                     newGroupAction.on = true;
146                 }
147             }
148             case CHANNEL_SCENE -> {
149                 if (command instanceof StringType) {
150                     getIdFromSceneName(command.toString())
151                             .thenAccept(id -> sendCommand(null, command, channelUID, "scenes/" + id + "/recall", null))
152                             .exceptionally(e -> {
153                                 logger.debug("Ignoring command {} for {}, scene is not found in available scenes {}.",
154                                         command, channelUID, scenes);
155                                 return null;
156                             });
157                 }
158                 return;
159             }
160             default -> {
161                 // no supported command
162                 return;
163             }
164         }
165
166         Boolean newOn = newGroupAction.on;
167         if (newOn != null && !newOn) {
168             // if light shall be off, no other commands are allowed, so reset the new light state
169             newGroupAction.clear();
170             newGroupAction.on = false;
171         }
172
173         sendCommand(newGroupAction, command, channelUID, null);
174     }
175
176     @Override
177     protected void processStateResponse(DeconzBaseMessage stateResponse) {
178         scenes = processScenes(stateResponse);
179         messageReceived(stateResponse);
180     }
181
182     private void valueUpdated(ChannelUID channelUID, GroupState newState) {
183         switch (channelUID.getId()) {
184             case CHANNEL_ALL_ON -> updateSwitchChannel(channelUID, newState.allOn);
185             case CHANNEL_ANY_ON -> updateSwitchChannel(channelUID, newState.anyOn);
186         }
187     }
188
189     @Override
190     public void messageReceived(DeconzBaseMessage message) {
191         if (message instanceof GroupMessage groupMessage) {
192             logger.trace("{} received {}", thing.getUID(), groupMessage);
193             GroupState groupState = groupMessage.state;
194             if (groupState != null) {
195                 updateStatus(ThingStatus.ONLINE);
196                 thing.getChannels().stream().map(Channel::getUID).forEach(c -> valueUpdated(c, groupState));
197                 groupStateCache = groupState;
198             }
199             GroupAction groupAction = groupMessage.action;
200             if (groupAction != null) {
201                 if (colorMode.isEmpty()) {
202                     String cmode = groupAction.colormode;
203                     if (cmode != null && ("hs".equals(cmode) || "xy".equals(cmode))) {
204                         // only set the color mode if it is hs or xy, not ct
205                         colorMode = cmode;
206                     }
207                 }
208             }
209         } else {
210             logger.trace("{} received {}", thing.getUID(), message);
211             getSceneNameFromId(message.scid).thenAccept(v -> updateState(CHANNEL_SCENE, v));
212         }
213     }
214
215     private CompletableFuture<String> getIdFromSceneName(String sceneName) {
216         CompletableFuture<String> f = new CompletableFuture<>();
217
218         Util.getKeysFromValue(scenes, sceneName).findAny().ifPresentOrElse(f::complete, () -> {
219             // we need to check if that is a new scene
220             logger.trace("Scene name {} not found in {}, refreshing scene list", sceneName, thing.getUID());
221             requestState(stateResponse -> {
222                 scenes = processScenes(stateResponse);
223                 Util.getKeysFromValue(scenes, sceneName).findAny().ifPresentOrElse(f::complete,
224                         () -> f.completeExceptionally(new IllegalArgumentException("Scene not found")));
225             });
226         });
227
228         return f;
229     }
230
231     private CompletableFuture<State> getSceneNameFromId(String sceneId) {
232         CompletableFuture<State> f = new CompletableFuture<>();
233
234         String sceneName = scenes.get(sceneId);
235         if (sceneName != null) {
236             // we already know that name, exit early
237             f.complete(new StringType(sceneName));
238         } else {
239             // we need to check if that is a new scene
240             logger.trace("Scene name for id {} not found in {}, refreshing scene list", sceneId, thing.getUID());
241             requestState(stateResponse -> {
242                 scenes = processScenes(stateResponse);
243                 String newSceneId = scenes.get(sceneId);
244                 if (newSceneId != null) {
245                     f.complete(new StringType(newSceneId));
246                 } else {
247                     logger.debug("Scene name for id {} not found in {} even after refreshing scene list.", sceneId,
248                             thing.getUID());
249                     f.complete(UnDefType.UNDEF);
250                 }
251             });
252         }
253
254         return f;
255     }
256
257     private Map<String, String> processScenes(DeconzBaseMessage stateResponse) {
258         if (stateResponse instanceof GroupMessage groupMessage) {
259             Map<String, String> scenes = groupMessage.scenes.stream()
260                     .collect(Collectors.toMap(scene -> scene.id, scene -> scene.name));
261             ChannelUID channelUID = new ChannelUID(thing.getUID(), CHANNEL_SCENE);
262             commandDescriptionProvider.setCommandOptions(channelUID,
263                     groupMessage.scenes.stream().map(Scene::toCommandOption).collect(Collectors.toList()));
264             return scenes;
265         }
266         return Map.of();
267     }
268
269     @Override
270     public Collection<Class<? extends ThingHandlerService>> getServices() {
271         return Set.of(GroupActions.class);
272     }
273 }