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.AbstractMap;
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.Collections;
19 import java.util.List;
21 import java.util.Map.Entry;
22 import java.util.TreeMap;
23 import java.util.UUID;
24 import java.util.stream.Collectors;
26 import javax.ws.rs.Consumes;
27 import javax.ws.rs.DELETE;
28 import javax.ws.rs.GET;
29 import javax.ws.rs.POST;
30 import javax.ws.rs.PUT;
31 import javax.ws.rs.Path;
32 import javax.ws.rs.PathParam;
33 import javax.ws.rs.Produces;
34 import javax.ws.rs.core.Context;
35 import javax.ws.rs.core.MediaType;
36 import javax.ws.rs.core.Response;
37 import javax.ws.rs.core.UriInfo;
39 import org.eclipse.jdt.annotation.NonNullByDefault;
40 import org.openhab.core.config.core.Configuration;
41 import org.openhab.core.common.registry.RegistryChangeListener;
42 import org.openhab.core.items.Item;
43 import org.openhab.core.items.ItemRegistry;
44 import org.openhab.core.automation.Action;
45 import org.openhab.core.automation.Condition;
46 import org.openhab.core.automation.Rule;
47 import org.openhab.core.automation.RuleRegistry;
48 import org.openhab.core.automation.Trigger;
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.RuleUtils;
54 import org.openhab.io.hueemulation.internal.dto.HueRuleEntry;
55 import org.openhab.io.hueemulation.internal.dto.changerequest.HueCommand;
56 import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
57 import org.openhab.io.hueemulation.internal.dto.response.HueSuccessGeneric;
58 import org.osgi.service.component.annotations.Activate;
59 import org.osgi.service.component.annotations.Component;
60 import org.osgi.service.component.annotations.Deactivate;
61 import org.osgi.service.component.annotations.Reference;
63 import io.swagger.annotations.ApiOperation;
64 import io.swagger.annotations.ApiParam;
65 import io.swagger.annotations.ApiResponse;
66 import io.swagger.annotations.ApiResponses;
69 * Handles Hue rules via the automation subsystem and the corresponding REST interface
71 * @author David Graeff - Initial contribution
73 @Component(immediate = false, service = { Rules.class }, property = "com.eclipsesource.jaxrs.publish=false")
76 @Produces(MediaType.APPLICATION_JSON)
77 @Consumes(MediaType.APPLICATION_JSON)
78 public class Rules implements RegistryChangeListener<Rule> {
79 public static final String RULES_TAG = "hueemulation_rule";
82 protected @NonNullByDefault({}) ConfigStore cs;
84 protected @NonNullByDefault({}) UserManagement userManagement;
86 protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
88 protected @NonNullByDefault({}) ItemRegistry itemRegistry;
91 * Registers to the {@link RuleRegistry} and enumerates currently existing rules.
94 public void activate() {
95 ruleRegistry.removeRegistryChangeListener(this);
96 ruleRegistry.addRegistryChangeListener(this);
98 for (Rule item : ruleRegistry.getAll()) {
104 public void deactivate() {
105 ruleRegistry.removeRegistryChangeListener(this);
109 public void added(Rule rule) {
110 if (!rule.getTags().contains(RULES_TAG)) {
113 HueRuleEntry entry = new HueRuleEntry(rule.getName());
114 String desc = rule.getDescription();
116 entry.description = desc;
119 rule.getConditions().stream().filter(c -> c.getTypeUID().equals("hue.ruleCondition")).forEach(c -> {
120 HueRuleEntry.Condition condition = c.getConfiguration().as(HueRuleEntry.Condition.class);
121 // address with pattern "/sensors/2/state/buttonevent"
122 String[] parts = condition.address.split("/");
123 if (parts.length < 3) {
127 entry.conditions.add(condition);
130 rule.getActions().stream().filter(a -> a.getTypeUID().equals("rules.HttpAction")).forEach(a -> {
131 HueCommand command = RuleUtils.httpActionToHueCommand(cs.ds, a, rule.getName());
132 if (command == null) {
135 // Remove the "/api/{user}" part
136 String[] parts = command.address.split("/");
137 command.address = "/" + String.join("/", Arrays.copyOfRange(parts, 3, parts.length));
138 entry.actions.add(command);
141 cs.ds.rules.put(rule.getUID(), entry);
145 public void removed(Rule element) {
146 cs.ds.rules.remove(element.getUID());
150 public void updated(Rule oldElement, Rule element) {
155 protected static Map.Entry<Trigger, Condition> hueConditionToAutomation(String id, HueRuleEntry.Condition condition,
156 ItemRegistry itemRegistry) {
157 // pattern: "/sensors/2/state/buttonevent"
158 String[] parts = condition.address.split("/");
159 if (parts.length < 3) {
160 throw new IllegalStateException("Condition address invalid: " + condition.address);
163 final Configuration triggerConfig = new Configuration();
165 String itemName = parts[2];
167 Item item = itemRegistry.get(itemName);
169 throw new IllegalStateException("Item of address does not exist: " + itemName);
172 triggerConfig.put("itemName", itemName);
174 // There might be multiple triggers for the same item. Due to the map, we are only creating one though
176 Trigger trigger = ModuleBuilder.createTrigger().withId(id).withTypeUID("core.ItemStateChangeTrigger")
177 .withConfiguration(triggerConfig).build();
179 // Connect the outputs of the trigger with the inputs of the condition
180 Map<String, String> inputs = new TreeMap<>();
181 inputs.put("newState", id);
182 inputs.put("oldState", id);
184 // Config for condition
185 final Configuration conditionConfig = new Configuration();
186 conditionConfig.put("operator", condition.operator.name());
187 conditionConfig.put("address", condition.address);
188 String value = condition.value;
190 conditionConfig.put("value", value);
193 Condition conditon = ModuleBuilder.createCondition().withId(id + "-condition").withTypeUID("hue.ruleCondition")
194 .withConfiguration(conditionConfig).withInputs(inputs).build();
196 return new AbstractMap.SimpleEntry<>(trigger, conditon);
199 protected static RuleBuilder createHueRuleConditions(List<HueRuleEntry.Condition> hueConditions,
200 RuleBuilder builder, List<Trigger> oldTriggers, List<Condition> oldConditions, ItemRegistry itemRegistry) {
201 // Preserve all triggers, conditions that are not part of hue rules
202 Map<String, Trigger> triggers = new TreeMap<>();
203 triggers.putAll(oldTriggers.stream().filter(a -> !a.getTypeUID().equals("core.ItemStateChangeTrigger"))
204 .collect(Collectors.toMap(e -> e.getId(), e -> e)));
206 Map<String, Condition> conditions = new TreeMap<>();
207 conditions.putAll(oldConditions.stream().filter(a -> !a.getTypeUID().equals("hue.ruleCondition"))
208 .collect(Collectors.toMap(e -> e.getId(), e -> e)));
210 for (HueRuleEntry.Condition condition : hueConditions) {
211 String id = condition.address.replace("/", "-");
212 Entry<Trigger, Condition> entry = hueConditionToAutomation(id, condition, itemRegistry);
213 triggers.put(id, entry.getKey());
214 conditions.put(id, entry.getValue());
217 builder.withTriggers(new ArrayList<>(triggers.values())).withConditions(new ArrayList<>(conditions.values()));
221 protected static List<Action> createActions(String uid, List<HueCommand> hueActions, List<Action> oldActions,
223 // Preserve all actions that are not "rules.HttpAction"
224 List<Action> actions = new ArrayList<>(oldActions);
225 actions.removeIf(a -> a.getTypeUID().equals("rules.HttpAction"));
227 for (HueCommand command : hueActions) {
228 command.address = "/api/" + apikey + command.address;
229 actions.add(RuleUtils.createHttpAction(command, command.address.replace("/", "-")));
235 @Path("{username}/rules")
236 @Produces(MediaType.APPLICATION_JSON)
237 @ApiOperation(value = "Return all rules")
238 @ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
239 public Response getRulesApi(@Context UriInfo uri,
240 @PathParam("username") @ApiParam(value = "username") String username) {
241 if (!userManagement.authorizeUser(username)) {
242 return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
244 return Response.ok(cs.gson.toJson(cs.ds.rules)).build();
248 @Path("{username}/rules/{id}")
249 @ApiOperation(value = "Return a rule")
250 @ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
251 public Response getRuleApi(@Context UriInfo uri, //
252 @PathParam("username") @ApiParam(value = "username") String username,
253 @PathParam("id") @ApiParam(value = "rule id") String id) {
254 if (!userManagement.authorizeUser(username)) {
255 return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
257 return Response.ok(cs.gson.toJson(cs.ds.rules.get(id))).build();
261 @Path("{username}/rules/{id}")
262 @ApiOperation(value = "Deletes a rule")
263 @ApiResponses(value = { @ApiResponse(code = 200, message = "The user got removed"),
264 @ApiResponse(code = 403, message = "Access denied") })
265 public Response removeRuleApi(@Context UriInfo uri,
266 @PathParam("username") @ApiParam(value = "username") String username,
267 @PathParam("id") @ApiParam(value = "Rule to remove") String id) {
268 if (!userManagement.authorizeUser(username)) {
269 return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
272 Rule rule = ruleRegistry.remove(id);
274 return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Rule does not exist!");
277 return NetworkUtils.singleSuccess(cs.gson, "/rules/" + id + " deleted.");
281 @Path("{username}/rules/{id}")
282 @ApiOperation(value = "Set rule attributes")
283 @ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
284 public Response modifyRuleApi(@Context UriInfo uri, //
285 @PathParam("username") @ApiParam(value = "username") String username,
286 @PathParam("id") @ApiParam(value = "rule id") String id, String body) {
287 if (!userManagement.authorizeUser(username)) {
288 return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
291 final HueRuleEntry changeRequest = cs.gson.fromJson(body, HueRuleEntry.class);
293 Rule rule = ruleRegistry.remove(id);
295 return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Rule does not exist!");
298 RuleBuilder builder = RuleBuilder.create(rule);
302 temp = changeRequest.name;
303 if (!temp.isEmpty()) {
304 builder.withName(changeRequest.name);
307 temp = changeRequest.description;
308 if (!temp.isEmpty()) {
309 builder.withDescription(temp);
313 if (!changeRequest.actions.isEmpty()) {
314 builder.withActions(createActions(rule.getUID(), changeRequest.actions, rule.getActions(), username));
316 if (!changeRequest.conditions.isEmpty()) {
317 builder = createHueRuleConditions(changeRequest.conditions, builder, rule.getTriggers(),
318 rule.getConditions(), itemRegistry);
321 ruleRegistry.add(builder.build());
322 } catch (IllegalStateException e) {
323 return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
326 return NetworkUtils.successList(cs.gson, Arrays.asList( //
327 new HueSuccessGeneric(changeRequest.name, "/rules/" + id + "/name"), //
328 new HueSuccessGeneric(changeRequest.description, "/rules/" + id + "/description"), //
329 new HueSuccessGeneric(changeRequest.actions.toString(), "/rules/" + id + "/actions"), //
330 new HueSuccessGeneric(changeRequest.conditions.toString(), "/rules/" + id + "/conditions") //
334 @SuppressWarnings({ "null" })
336 @Path("{username}/rules")
337 @ApiOperation(value = "Create a new rule")
338 @ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
339 public Response postNewRule(@Context UriInfo uri,
340 @PathParam("username") @ApiParam(value = "username") String username, String body) {
341 if (!userManagement.authorizeUser(username)) {
342 return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
345 HueRuleEntry newRuleData = cs.gson.fromJson(body, HueRuleEntry.class);
346 if (newRuleData == null || newRuleData.name.isEmpty() || newRuleData.actions.isEmpty()
347 || newRuleData.conditions.isEmpty()) {
348 return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON,
349 "Invalid request: No name or actions or conditons!");
352 String uid = UUID.randomUUID().toString();
353 RuleBuilder builder = RuleBuilder.create(uid).withName(newRuleData.name);
355 String description = newRuleData.description;
356 if (description != null) {
357 builder.withDescription(description);
361 builder.withActions(createActions(uid, newRuleData.actions, Collections.emptyList(), username));
362 builder = createHueRuleConditions(newRuleData.conditions, builder, Collections.emptyList(),
363 Collections.emptyList(), itemRegistry);
364 ruleRegistry.add(builder.withTags(RULES_TAG).build());
365 } catch (IllegalStateException e) {
366 return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
369 return NetworkUtils.singleSuccess(cs.gson, uid, "id");