2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.hueemulation.internal.rest;
15 import java.util.ArrayList;
16 import java.util.Arrays;
17 import java.util.Collections;
18 import java.util.List;
20 import java.util.Objects;
21 import java.util.UUID;
22 import java.util.stream.Collectors;
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;
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;
69 import io.swagger.annotations.ApiOperation;
70 import io.swagger.annotations.ApiParam;
71 import io.swagger.annotations.ApiResponse;
72 import io.swagger.annotations.ApiResponses;
75 * Handles Hue scenes via the automation subsystem and the corresponding REST interface
77 * @author David Graeff - Initial contribution
79 @Component(immediate = false, service = { Scenes.class }, property = "com.eclipsesource.jaxrs.publish=false")
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);
88 protected @NonNullByDefault({}) ConfigStore cs;
90 protected @NonNullByDefault({}) UserManagement userManagement;
92 protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
94 protected @NonNullByDefault({}) ItemRegistry itemRegistry;
97 * Registers to the {@link RuleRegistry} and enumerates currently existing rules.
100 public void activate() {
101 ruleRegistry.removeRegistryChangeListener(this);
102 ruleRegistry.addRegistryChangeListener(this);
104 for (Rule item : ruleRegistry.getAll()) {
110 public void deactivate() {
111 ruleRegistry.removeRegistryChangeListener(this);
115 public void added(Rule scene) {
116 if (!scene.getTags().contains("scene")) {
119 HueSceneEntry entry = new HueSceneEntry(scene.getName());
120 String desc = scene.getDescription();
122 entry.description = desc;
125 List<String> items = new ArrayList<>();
127 for (Action a : scene.getActions()) {
128 if (!a.getTypeUID().equals("core.ItemCommandAction")) {
131 ItemCommandActionConfig config = a.getConfiguration().as(ItemCommandActionConfig.class);
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);
139 if (scene.getActions().size() == 1 && item instanceof GroupItem) {
140 entry.type = HueSceneEntry.TypeEnum.GroupScene;
141 entry.group = cs.mapItemUIDtoHueID(item);
143 items.add(cs.mapItemUIDtoHueID(item));
147 if (!items.isEmpty()) {
148 entry.lights = items;
151 cs.ds.scenes.put(scene.getUID(), entry);
155 public void removed(Rule element) {
156 cs.ds.scenes.remove(element.getUID());
160 public void updated(Rule oldElement, Rule element) {
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");
175 return Response.ok(cs.gson.toJson(cs.ds.scenes)).build();
178 @SuppressWarnings({ "unused", "null" })
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");
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!");
193 HueSceneWithLightstates s = new HueSceneWithLightstates(sceneEntry);
194 for (String itemID : s.lights) {
197 item = itemRegistry.getItem(itemID);
198 } catch (ItemNotFoundException e) {
199 logger.warn("Scene {} is referring to a non existing item {}", sceneEntry.name, itemID);
202 AbstractHueState state = StateUtils.colorStateFromItemState(item.getState(), null);
203 s.lightstates.put(cs.mapItemUIDtoHueID(item), state);
206 return Response.ok(cs.gson.toJson(s)).build();
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");
221 Rule rule = ruleRegistry.remove(id);
223 return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Scene does not exist!");
226 return NetworkUtils.singleSuccess(cs.gson, "/scenes/" + id + " deleted.");
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();
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();
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
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");
260 final HueChangeSceneEntry changeRequest = cs.gson.fromJson(body, HueChangeSceneEntry.class);
262 Rule rule = ruleRegistry.remove(id);
264 return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Scene does not exist!");
267 RuleBuilder builder = RuleBuilder.create(rule);
269 String temp = changeRequest.name;
271 builder.withName(temp);
273 temp = changeRequest.description;
275 builder.withDescription(temp);
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);
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()));
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!");
298 actions.add(actionFromState(entry.getKey(), command));
300 builder.withActions(actions);
304 ruleRegistry.add(builder.build());
305 } catch (IllegalStateException e) {
306 return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
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") //
317 @SuppressWarnings({ "null" })
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");
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!");
334 String uid = UUID.randomUUID().toString();
335 RuleBuilder builder = RuleBuilder.create(uid).withName(newScheduleData.name).withTags("scene");
337 if (!newScheduleData.description.isEmpty()) {
338 builder.withDescription(newScheduleData.description);
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);
349 actions.add(actionFromState(cs.mapItemUIDtoHueID(item), item.getState()));
351 builder.withActions(actions);
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!");
359 List<Action> actions = Collections
360 .singletonList(actionFromState(cs.mapItemUIDtoHueID(groupItem), groupItem.getState()));
361 builder.withActions(actions);
365 ruleRegistry.add(builder.build());
366 } catch (IllegalStateException e) {
367 return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
370 return NetworkUtils.singleSuccess(cs.gson, uid, "id");
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");
385 final HueStateChange changeRequest = cs.gson.fromJson(body, HueStateChange.class);
387 Rule rule = ruleRegistry.remove(id);
389 return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Scene does not exist!");
392 RuleBuilder builder = RuleBuilder.create(rule);
394 List<Action> actions = new ArrayList<>(rule.getActions());
395 // Remove existing action
396 actions.removeIf(action -> action.getId().equals(lightid));
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!");
404 actions.add(actionFromState(lightid, command));
406 builder.withActions(actions);
409 ruleRegistry.add(builder.build());
410 } catch (IllegalStateException e) {
411 return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
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")));