]> git.basschouten.com Git - openhab-addons.git/blob
16ae0d477c76c16311c9760ae626f7a98b79bcda
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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         return NetworkUtils.successList(cs.gson, Arrays.asList( //
312                 new HueSuccessGeneric(changeRequest.name, "/scenes/" + id + "/name"), //
313                 new HueSuccessGeneric(changeRequest.description, "/scenes/" + id + "/description"), //
314                 new HueSuccessGeneric(changeRequest.lights != null ? String.join(",", changeRequest.lights) : null,
315                         "/scenes/" + id + "/lights") //
316         ));
317     }
318
319     @SuppressWarnings({ "null" })
320     @POST
321     @Path("{username}/scenes")
322     @Operation(summary = "Create a new scene", responses = { @ApiResponse(responseCode = "200", description = "OK") })
323     public Response postNewScene(@Context UriInfo uri,
324             @PathParam("username") @Parameter(description = "username") String username, String body) {
325         if (!userManagement.authorizeUser(username)) {
326             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
327         }
328
329         HueSceneEntry newScheduleData = cs.gson.fromJson(body, HueSceneEntry.class);
330         if (newScheduleData == null || newScheduleData.name.isEmpty()) {
331             return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON,
332                     "Invalid request: No name or localtime!");
333         }
334
335         String uid = UUID.randomUUID().toString();
336         RuleBuilder builder = RuleBuilder.create(uid).withName(newScheduleData.name).withTags("scene");
337
338         if (!newScheduleData.description.isEmpty()) {
339             builder.withDescription(newScheduleData.description);
340         }
341
342         List<String> lights = newScheduleData.lights;
343         if (lights != null) {
344             List<Action> actions = new ArrayList<>();
345             for (String itemID : lights) {
346                 Item item = itemRegistry.get(itemID);
347                 if (item == null) {
348                     continue;
349                 }
350                 actions.add(actionFromState(cs.mapItemUIDtoHueID(item), item.getState()));
351             }
352             builder.withActions(actions);
353         }
354         String groupid = newScheduleData.group;
355         if (groupid != null) {
356             Item groupItem = itemRegistry.get(groupid);
357             if (groupItem == null) {
358                 return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, "Group does not exist!");
359             }
360             List<Action> actions = Collections
361                     .singletonList(actionFromState(cs.mapItemUIDtoHueID(groupItem), groupItem.getState()));
362             builder.withActions(actions);
363         }
364
365         try {
366             ruleRegistry.add(builder.build());
367         } catch (IllegalStateException e) {
368             return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
369         }
370
371         return NetworkUtils.singleSuccess(cs.gson, uid, "id");
372     }
373
374     @PUT
375     @Path("{username}/scenes/{id}/lightstates/{lightid}")
376     @Operation(summary = "Set scene attributes", responses = { @ApiResponse(responseCode = "200", description = "OK") })
377     public Response modifySceneLightStateApi(@Context UriInfo uri, //
378             @PathParam("username") @Parameter(description = "username") String username,
379             @PathParam("id") @Parameter(description = "scene id") String id,
380             @PathParam("lightid") @Parameter(description = "light id") String lightid, String body) {
381         if (!userManagement.authorizeUser(username)) {
382             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
383         }
384
385         final HueStateChange changeRequest = cs.gson.fromJson(body, HueStateChange.class);
386
387         Rule rule = ruleRegistry.remove(id);
388         if (rule == null) {
389             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Scene does not exist!");
390         }
391
392         RuleBuilder builder = RuleBuilder.create(rule);
393
394         List<Action> actions = new ArrayList<>(rule.getActions());
395         // Remove existing action
396         actions.removeIf(action -> action.getId().equals(lightid));
397         // Assign new action
398         Command command = StateUtils.computeCommandByChangeRequest(changeRequest);
399         if (command == null) {
400             logger.warn("Failed to compute command for {}", body);
401             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Cannot compute command!");
402         }
403
404         actions.add(actionFromState(lightid, command));
405
406         builder.withActions(actions);
407
408         try {
409             ruleRegistry.add(builder.build());
410         } catch (IllegalStateException e) {
411             return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
412         }
413
414         return NetworkUtils.successList(cs.gson, Arrays.asList( //
415                 new HueSuccessGeneric(changeRequest.on, "/scenes/" + id + "/lightstates/" + lightid + "/on"), //
416                 new HueSuccessGeneric(changeRequest.hue, "/scenes/" + id + "/lightstates/" + lightid + "/hue"), //
417                 new HueSuccessGeneric(changeRequest.sat, "/scenes/" + id + "/lightstates/" + lightid + "/sat"), //
418                 new HueSuccessGeneric(changeRequest.bri, "/scenes/" + id + "/lightstates/" + lightid + "/bri"), //
419                 new HueSuccessGeneric(changeRequest.transitiontime,
420                         "/scenes/" + id + "/lightstates/" + lightid + "/transitiontime")));
421     }
422 }