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