]> git.basschouten.com Git - openhab-addons.git/blob
7f72a6b3f01a491397858521ae6ae87a18c61304
[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.io.hueemulation.internal.rest;
14
15 import java.util.ArrayList;
16 import java.util.Arrays;
17 import java.util.Collections;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Objects;
21 import java.util.UUID;
22 import java.util.stream.Collectors;
23
24 import javax.ws.rs.Consumes;
25 import javax.ws.rs.DELETE;
26 import javax.ws.rs.GET;
27 import javax.ws.rs.POST;
28 import javax.ws.rs.PUT;
29 import javax.ws.rs.Path;
30 import javax.ws.rs.PathParam;
31 import javax.ws.rs.Produces;
32 import javax.ws.rs.core.Context;
33 import javax.ws.rs.core.MediaType;
34 import javax.ws.rs.core.Response;
35 import javax.ws.rs.core.UriInfo;
36
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.openhab.core.automation.Action;
39 import org.openhab.core.automation.Rule;
40 import org.openhab.core.automation.RuleRegistry;
41 import org.openhab.core.automation.util.ModuleBuilder;
42 import org.openhab.core.automation.util.RuleBuilder;
43 import org.openhab.core.common.registry.RegistryChangeListener;
44 import org.openhab.core.config.core.Configuration;
45 import org.openhab.core.items.GroupItem;
46 import org.openhab.core.items.Item;
47 import org.openhab.core.items.ItemNotFoundException;
48 import org.openhab.core.items.ItemRegistry;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.State;
51 import org.openhab.io.hueemulation.internal.ConfigStore;
52 import org.openhab.io.hueemulation.internal.HueEmulationService;
53 import org.openhab.io.hueemulation.internal.NetworkUtils;
54 import org.openhab.io.hueemulation.internal.StateUtils;
55 import org.openhab.io.hueemulation.internal.automation.dto.ItemCommandActionConfig;
56 import org.openhab.io.hueemulation.internal.dto.AbstractHueState;
57 import org.openhab.io.hueemulation.internal.dto.HueSceneEntry;
58 import org.openhab.io.hueemulation.internal.dto.HueSceneWithLightstates;
59 import org.openhab.io.hueemulation.internal.dto.changerequest.HueChangeSceneEntry;
60 import org.openhab.io.hueemulation.internal.dto.changerequest.HueStateChange;
61 import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
62 import org.openhab.io.hueemulation.internal.dto.response.HueSuccessGeneric;
63 import org.osgi.service.component.annotations.Activate;
64 import org.osgi.service.component.annotations.Component;
65 import org.osgi.service.component.annotations.Deactivate;
66 import org.osgi.service.component.annotations.Reference;
67 import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
68 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
69 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
72
73 import io.swagger.v3.oas.annotations.Operation;
74 import io.swagger.v3.oas.annotations.Parameter;
75 import io.swagger.v3.oas.annotations.responses.ApiResponse;
76
77 /**
78  * Handles Hue scenes via the automation subsystem and the corresponding REST interface
79  *
80  * @author David Graeff - Initial contribution
81  */
82 @Component(immediate = false, service = Scenes.class)
83 @JaxrsResource
84 @JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + HueEmulationService.REST_APP_NAME + ")")
85 @NonNullByDefault
86 @Path("")
87 @Produces(MediaType.APPLICATION_JSON)
88 @Consumes(MediaType.APPLICATION_JSON)
89 public class Scenes implements RegistryChangeListener<Rule> {
90     private final Logger logger = LoggerFactory.getLogger(Scenes.class);
91
92     @Reference
93     protected @NonNullByDefault({}) ConfigStore cs;
94     @Reference
95     protected @NonNullByDefault({}) UserManagement userManagement;
96     @Reference
97     protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
98     @Reference
99     protected @NonNullByDefault({}) ItemRegistry itemRegistry;
100
101     /**
102      * Registers to the {@link RuleRegistry} and enumerates currently existing rules.
103      */
104     @Activate
105     public void activate() {
106         ruleRegistry.removeRegistryChangeListener(this);
107         ruleRegistry.addRegistryChangeListener(this);
108
109         for (Rule item : ruleRegistry.getAll()) {
110             added(item);
111         }
112     }
113
114     @Deactivate
115     public void deactivate() {
116         ruleRegistry.removeRegistryChangeListener(this);
117     }
118
119     @Override
120     public void added(Rule scene) {
121         if (!scene.getTags().contains("scene")) {
122             return;
123         }
124         HueSceneEntry entry = new HueSceneEntry(scene.getName());
125         String desc = scene.getDescription();
126         if (desc != null) {
127             entry.description = desc;
128         }
129
130         List<String> items = new ArrayList<>();
131
132         for (Action a : scene.getActions()) {
133             if (!a.getTypeUID().equals("core.ItemCommandAction")) {
134                 continue;
135             }
136             ItemCommandActionConfig config = a.getConfiguration().as(ItemCommandActionConfig.class);
137             Item item;
138             try {
139                 item = itemRegistry.getItem(config.itemName);
140             } catch (ItemNotFoundException e) {
141                 logger.warn("Rule {} is referring to a non existing item {}", scene.getName(), config.itemName);
142                 continue;
143             }
144             if (scene.getActions().size() == 1 && item instanceof GroupItem) {
145                 entry.type = HueSceneEntry.TypeEnum.GroupScene;
146                 entry.group = cs.mapItemUIDtoHueID(item);
147             } else {
148                 items.add(cs.mapItemUIDtoHueID(item));
149             }
150         }
151
152         if (!items.isEmpty()) {
153             entry.lights = items;
154         }
155
156         cs.ds.scenes.put(scene.getUID(), entry);
157     }
158
159     @Override
160     public void removed(Rule element) {
161         cs.ds.scenes.remove(element.getUID());
162     }
163
164     @Override
165     public void updated(Rule oldElement, Rule element) {
166         removed(oldElement);
167         added(element);
168     }
169
170     @GET
171     @Path("{username}/scenes")
172     @Produces(MediaType.APPLICATION_JSON)
173     @Operation(summary = "Return all scenes", responses = { @ApiResponse(responseCode = "200", description = "OK") })
174     public Response getScenesApi(@Context UriInfo uri,
175             @PathParam("username") @Parameter(description = "username") String username) {
176         if (!userManagement.authorizeUser(username)) {
177             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
178         }
179         return Response.ok(cs.gson.toJson(cs.ds.scenes)).build();
180     }
181
182     @SuppressWarnings({ "unused", "null" })
183     @GET
184     @Path("{username}/scenes/{id}")
185     @Operation(summary = "Return a scene", responses = { @ApiResponse(responseCode = "200", description = "OK") })
186     public Response getSceneApi(@Context UriInfo uri, //
187             @PathParam("username") @Parameter(description = "username") String username,
188             @PathParam("id") @Parameter(description = "scene id") String id) {
189         if (!userManagement.authorizeUser(username)) {
190             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
191         }
192         HueSceneEntry sceneEntry = cs.ds.scenes.get(id);
193         if (sceneEntry == null) {
194             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Scene does not exist!");
195         }
196         HueSceneWithLightstates s = new HueSceneWithLightstates(sceneEntry);
197         for (String itemID : s.lights) {
198             Item item;
199             try {
200                 item = itemRegistry.getItem(itemID);
201             } catch (ItemNotFoundException e) {
202                 logger.warn("Scene {} is referring to a non existing item {}", sceneEntry.name, itemID);
203                 continue;
204             }
205             AbstractHueState state = StateUtils.colorStateFromItemState(item.getState(), null);
206             s.lightstates.put(cs.mapItemUIDtoHueID(item), state);
207         }
208
209         return Response.ok(cs.gson.toJson(s)).build();
210     }
211
212     @DELETE
213     @Path("{username}/scenes/{id}")
214     @Operation(summary = "Deletes a scene", responses = {
215             @ApiResponse(responseCode = "200", description = "The user got removed"),
216             @ApiResponse(responseCode = "403", description = "Access denied") })
217     public Response removeSceneApi(@Context UriInfo uri,
218             @PathParam("username") @Parameter(description = "username") String username,
219             @PathParam("id") @Parameter(description = "Scene to remove") String id) {
220         if (!userManagement.authorizeUser(username)) {
221             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
222         }
223
224         Rule rule = ruleRegistry.remove(id);
225         if (rule == null) {
226             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Scene does not exist!");
227         }
228
229         return NetworkUtils.singleSuccess(cs.gson, "/scenes/" + id + " deleted.");
230     }
231
232     protected static Action actionFromState(String itemID, State state) {
233         final Configuration actionConfig = new Configuration();
234         actionConfig.put("itemName", itemID);
235         actionConfig.put("command", StateUtils.commandByItemState(state).toFullString());
236         return ModuleBuilder.createAction().withId(itemID).withTypeUID("core.ItemCommandAction")
237                 .withConfiguration(actionConfig).build();
238     }
239
240     protected static Action actionFromState(String itemID, Command command) {
241         final Configuration actionConfig = new Configuration();
242         actionConfig.put("itemName", itemID);
243         actionConfig.put("command", command.toFullString());
244         return ModuleBuilder.createAction().withId(itemID).withTypeUID("core.ItemCommandAction")
245                 .withConfiguration(actionConfig).build();
246     }
247
248     /**
249      * Either assigns a new name, description, lights to a scene or directly assign
250      * a new light state for an entry to a scene
251      */
252     @PUT
253     @Path("{username}/scenes/{id}")
254     @Operation(summary = "Set scene attributes", responses = { @ApiResponse(responseCode = "200", description = "OK") })
255     public Response modifySceneApi(@Context UriInfo uri, //
256             @PathParam("username") @Parameter(description = "username") String username,
257             @PathParam("id") @Parameter(description = "scene id") String id, String body) {
258         if (!userManagement.authorizeUser(username)) {
259             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
260         }
261
262         final HueChangeSceneEntry changeRequest = cs.gson.fromJson(body, HueChangeSceneEntry.class);
263
264         Rule rule = ruleRegistry.remove(id);
265         if (rule == null) {
266             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Scene does not exist!");
267         }
268
269         RuleBuilder builder = RuleBuilder.create(rule);
270
271         String temp = changeRequest.name;
272         if (temp != null) {
273             builder.withName(temp);
274         }
275         temp = changeRequest.description;
276         if (temp != null) {
277             builder.withDescription(temp);
278         }
279
280         List<String> lights = changeRequest.lights;
281         if (changeRequest.storelightstate && lights != null) {
282             @SuppressWarnings("null")
283             @NonNullByDefault({})
284             List<Action> actions = lights.stream().map(itemID -> itemRegistry.get(itemID)).filter(Objects::nonNull)
285                     .map(item -> actionFromState(item.getUID(), item.getState())).collect(Collectors.toList());
286             builder.withActions(actions);
287         }
288         Map<String, HueStateChange> lightStates = changeRequest.lightstates;
289         if (changeRequest.storelightstate && lightStates != null) {
290             List<Action> actions = new ArrayList<>(rule.getActions());
291             for (Map.Entry<String, HueStateChange> entry : lightStates.entrySet()) {
292                 // Remove existing action
293                 actions.removeIf(action -> action.getId().equals(entry.getKey()));
294                 // Assign new action
295                 Command command = StateUtils.computeCommandByChangeRequest(entry.getValue());
296                 if (command == null) {
297                     logger.warn("Failed to compute command for {}", body);
298                     return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Cannot compute command!");
299                 }
300                 actions.add(actionFromState(entry.getKey(), command));
301             }
302             builder.withActions(actions);
303         }
304
305         try {
306             ruleRegistry.add(builder.build());
307         } catch (IllegalStateException e) {
308             return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
309         }
310
311         List<String> lightsList = changeRequest.lights;
312         return NetworkUtils.successList(cs.gson, Arrays.asList( //
313                 new HueSuccessGeneric(changeRequest.name, "/scenes/" + id + "/name"), //
314                 new HueSuccessGeneric(changeRequest.description, "/scenes/" + id + "/description"), //
315                 new HueSuccessGeneric(lightsList != null ? String.join(",", lightsList) : null,
316                         "/scenes/" + id + "/lights") //
317         ));
318     }
319
320     @SuppressWarnings({ "null" })
321     @POST
322     @Path("{username}/scenes")
323     @Operation(summary = "Create a new scene", responses = { @ApiResponse(responseCode = "200", description = "OK") })
324     public Response postNewScene(@Context UriInfo uri,
325             @PathParam("username") @Parameter(description = "username") String username, String body) {
326         if (!userManagement.authorizeUser(username)) {
327             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
328         }
329
330         HueSceneEntry newScheduleData = cs.gson.fromJson(body, HueSceneEntry.class);
331         if (newScheduleData == null || newScheduleData.name.isEmpty()) {
332             return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON,
333                     "Invalid request: No name or localtime!");
334         }
335
336         String uid = UUID.randomUUID().toString();
337         RuleBuilder builder = RuleBuilder.create(uid).withName(newScheduleData.name).withTags("scene");
338
339         if (!newScheduleData.description.isEmpty()) {
340             builder.withDescription(newScheduleData.description);
341         }
342
343         List<String> lights = newScheduleData.lights;
344         if (lights != null) {
345             List<Action> actions = new ArrayList<>();
346             for (String itemID : lights) {
347                 Item item = itemRegistry.get(itemID);
348                 if (item == null) {
349                     continue;
350                 }
351                 actions.add(actionFromState(cs.mapItemUIDtoHueID(item), item.getState()));
352             }
353             builder.withActions(actions);
354         }
355         String groupid = newScheduleData.group;
356         if (groupid != null) {
357             Item groupItem = itemRegistry.get(groupid);
358             if (groupItem == null) {
359                 return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, "Group does not exist!");
360             }
361             List<Action> actions = Collections
362                     .singletonList(actionFromState(cs.mapItemUIDtoHueID(groupItem), groupItem.getState()));
363             builder.withActions(actions);
364         }
365
366         try {
367             ruleRegistry.add(builder.build());
368         } catch (IllegalStateException e) {
369             return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
370         }
371
372         return NetworkUtils.singleSuccess(cs.gson, uid, "id");
373     }
374
375     @PUT
376     @Path("{username}/scenes/{id}/lightstates/{lightid}")
377     @Operation(summary = "Set scene attributes", responses = { @ApiResponse(responseCode = "200", description = "OK") })
378     public Response modifySceneLightStateApi(@Context UriInfo uri, //
379             @PathParam("username") @Parameter(description = "username") String username,
380             @PathParam("id") @Parameter(description = "scene id") String id,
381             @PathParam("lightid") @Parameter(description = "light id") String lightid, String body) {
382         if (!userManagement.authorizeUser(username)) {
383             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
384         }
385
386         final HueStateChange changeRequest = Objects.requireNonNull(cs.gson.fromJson(body, HueStateChange.class));
387
388         Rule rule = ruleRegistry.remove(id);
389         if (rule == null) {
390             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Scene does not exist!");
391         }
392
393         RuleBuilder builder = RuleBuilder.create(rule);
394
395         List<Action> actions = new ArrayList<>(rule.getActions());
396         // Remove existing action
397         actions.removeIf(action -> action.getId().equals(lightid));
398         // Assign new action
399         Command command = StateUtils.computeCommandByChangeRequest(changeRequest);
400         if (command == null) {
401             logger.warn("Failed to compute command for {}", body);
402             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Cannot compute command!");
403         }
404
405         actions.add(actionFromState(lightid, command));
406
407         builder.withActions(actions);
408
409         try {
410             ruleRegistry.add(builder.build());
411         } catch (IllegalStateException e) {
412             return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
413         }
414
415         return NetworkUtils.successList(cs.gson, Arrays.asList( //
416                 new HueSuccessGeneric(changeRequest.on, "/scenes/" + id + "/lightstates/" + lightid + "/on"), //
417                 new HueSuccessGeneric(changeRequest.hue, "/scenes/" + id + "/lightstates/" + lightid + "/hue"), //
418                 new HueSuccessGeneric(changeRequest.sat, "/scenes/" + id + "/lightstates/" + lightid + "/sat"), //
419                 new HueSuccessGeneric(changeRequest.bri, "/scenes/" + id + "/lightstates/" + lightid + "/bri"), //
420                 new HueSuccessGeneric(changeRequest.transitiontime,
421                         "/scenes/" + id + "/lightstates/" + lightid + "/transitiontime")));
422     }
423 }