2 * Copyright (c) 2010-2023 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.automation.Action;
41 import org.openhab.core.automation.Condition;
42 import org.openhab.core.automation.Rule;
43 import org.openhab.core.automation.RuleRegistry;
44 import org.openhab.core.automation.Trigger;
45 import org.openhab.core.automation.util.ModuleBuilder;
46 import org.openhab.core.automation.util.RuleBuilder;
47 import org.openhab.core.common.registry.RegistryChangeListener;
48 import org.openhab.core.config.core.Configuration;
49 import org.openhab.core.items.Item;
50 import org.openhab.core.items.ItemRegistry;
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.RuleUtils;
55 import org.openhab.io.hueemulation.internal.dto.HueRuleEntry;
56 import org.openhab.io.hueemulation.internal.dto.changerequest.HueCommand;
57 import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
58 import org.openhab.io.hueemulation.internal.dto.response.HueSuccessGeneric;
59 import org.osgi.service.component.annotations.Activate;
60 import org.osgi.service.component.annotations.Component;
61 import org.osgi.service.component.annotations.Deactivate;
62 import org.osgi.service.component.annotations.Reference;
63 import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
64 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
65 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
67 import io.swagger.v3.oas.annotations.Operation;
68 import io.swagger.v3.oas.annotations.Parameter;
69 import io.swagger.v3.oas.annotations.responses.ApiResponse;
72 * Handles Hue rules via the automation subsystem and the corresponding REST interface
74 * @author David Graeff - Initial contribution
76 @Component(immediate = false, service = Rules.class)
78 @JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + HueEmulationService.REST_APP_NAME + ")")
81 @Produces(MediaType.APPLICATION_JSON)
82 @Consumes(MediaType.APPLICATION_JSON)
83 public class Rules implements RegistryChangeListener<Rule> {
84 public static final String RULES_TAG = "hueemulation_rule";
87 protected @NonNullByDefault({}) ConfigStore cs;
89 protected @NonNullByDefault({}) UserManagement userManagement;
91 protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
93 protected @NonNullByDefault({}) ItemRegistry itemRegistry;
96 * Registers to the {@link RuleRegistry} and enumerates currently existing rules.
99 public void activate() {
100 ruleRegistry.removeRegistryChangeListener(this);
101 ruleRegistry.addRegistryChangeListener(this);
103 for (Rule item : ruleRegistry.getAll()) {
109 public void deactivate() {
110 ruleRegistry.removeRegistryChangeListener(this);
114 public void added(Rule rule) {
115 if (!rule.getTags().contains(RULES_TAG)) {
118 HueRuleEntry entry = new HueRuleEntry(rule.getName());
119 String desc = rule.getDescription();
121 entry.description = desc;
124 rule.getConditions().stream().filter(c -> c.getTypeUID().equals("hue.ruleCondition")).forEach(c -> {
125 HueRuleEntry.Condition condition = c.getConfiguration().as(HueRuleEntry.Condition.class);
126 // address with pattern "/sensors/2/state/buttonevent"
127 String[] parts = condition.address.split("/");
128 if (parts.length < 3) {
132 entry.conditions.add(condition);
135 rule.getActions().stream().filter(a -> a.getTypeUID().equals("rules.HttpAction")).forEach(a -> {
136 HueCommand command = RuleUtils.httpActionToHueCommand(cs.ds, a, rule.getName());
137 if (command == null) {
140 // Remove the "/api/{user}" part
141 String[] parts = command.address.split("/");
142 command.address = "/" + String.join("/", Arrays.copyOfRange(parts, 3, parts.length));
143 entry.actions.add(command);
146 cs.ds.rules.put(rule.getUID(), entry);
150 public void removed(Rule element) {
151 cs.ds.rules.remove(element.getUID());
155 public void updated(Rule oldElement, Rule element) {
160 protected static Map.Entry<Trigger, Condition> hueConditionToAutomation(String id, HueRuleEntry.Condition condition,
161 ItemRegistry itemRegistry) {
162 // pattern: "/sensors/2/state/buttonevent"
163 String[] parts = condition.address.split("/");
164 if (parts.length < 3) {
165 throw new IllegalStateException("Condition address invalid: " + condition.address);
168 final Configuration triggerConfig = new Configuration();
170 String itemName = parts[2];
172 Item item = itemRegistry.get(itemName);
174 throw new IllegalStateException("Item of address does not exist: " + itemName);
177 triggerConfig.put("itemName", itemName);
179 // There might be multiple triggers for the same item. Due to the map, we are only creating one though
181 Trigger trigger = ModuleBuilder.createTrigger().withId(id).withTypeUID("core.ItemStateChangeTrigger")
182 .withConfiguration(triggerConfig).build();
184 // Connect the outputs of the trigger with the inputs of the condition
185 Map<String, String> inputs = new TreeMap<>();
186 inputs.put("newState", id);
187 inputs.put("oldState", id);
189 // Config for condition
190 final Configuration conditionConfig = new Configuration();
191 conditionConfig.put("operator", condition.operator.name());
192 conditionConfig.put("address", condition.address);
193 String value = condition.value;
195 conditionConfig.put("value", value);
198 Condition conditon = ModuleBuilder.createCondition().withId(id + "-condition").withTypeUID("hue.ruleCondition")
199 .withConfiguration(conditionConfig).withInputs(inputs).build();
201 return new AbstractMap.SimpleEntry<>(trigger, conditon);
204 protected static RuleBuilder createHueRuleConditions(List<HueRuleEntry.Condition> hueConditions,
205 RuleBuilder builder, List<Trigger> oldTriggers, List<Condition> oldConditions, ItemRegistry itemRegistry) {
206 // Preserve all triggers, conditions that are not part of hue rules
207 Map<String, Trigger> triggers = new TreeMap<>();
208 triggers.putAll(oldTriggers.stream().filter(a -> !a.getTypeUID().equals("core.ItemStateChangeTrigger"))
209 .collect(Collectors.toMap(e -> e.getId(), e -> e)));
211 Map<String, Condition> conditions = new TreeMap<>();
212 conditions.putAll(oldConditions.stream().filter(a -> !a.getTypeUID().equals("hue.ruleCondition"))
213 .collect(Collectors.toMap(e -> e.getId(), e -> e)));
215 for (HueRuleEntry.Condition condition : hueConditions) {
216 String id = condition.address.replace("/", "-");
217 Entry<Trigger, Condition> entry = hueConditionToAutomation(id, condition, itemRegistry);
218 triggers.put(id, entry.getKey());
219 conditions.put(id, entry.getValue());
222 builder.withTriggers(new ArrayList<>(triggers.values())).withConditions(new ArrayList<>(conditions.values()));
226 protected static List<Action> createActions(String uid, List<HueCommand> hueActions, List<Action> oldActions,
228 // Preserve all actions that are not "rules.HttpAction"
229 List<Action> actions = new ArrayList<>(oldActions);
230 actions.removeIf(a -> a.getTypeUID().equals("rules.HttpAction"));
232 for (HueCommand command : hueActions) {
233 command.address = "/api/" + apikey + command.address;
234 actions.add(RuleUtils.createHttpAction(command, command.address.replace("/", "-")));
240 @Path("{username}/rules")
241 @Produces(MediaType.APPLICATION_JSON)
242 @Operation(summary = "Return all rules", responses = { @ApiResponse(responseCode = "200", description = "OK") })
243 public Response getRulesApi(@Context UriInfo uri,
244 @PathParam("username") @Parameter(description = "username") String username) {
245 if (!userManagement.authorizeUser(username)) {
246 return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
248 return Response.ok(cs.gson.toJson(cs.ds.rules)).build();
252 @Path("{username}/rules/{id}")
253 @Operation(summary = "Return a rule", responses = { @ApiResponse(responseCode = "200", description = "OK") })
254 public Response getRuleApi(@Context UriInfo uri, //
255 @PathParam("username") @Parameter(description = "username") String username,
256 @PathParam("id") @Parameter(description = "rule id") String id) {
257 if (!userManagement.authorizeUser(username)) {
258 return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
260 return Response.ok(cs.gson.toJson(cs.ds.rules.get(id))).build();
264 @Path("{username}/rules/{id}")
265 @Operation(summary = "Deletes a rule", responses = {
266 @ApiResponse(responseCode = "200", description = "The user got removed"),
267 @ApiResponse(responseCode = "403", description = "Access denied") })
268 public Response removeRuleApi(@Context UriInfo uri,
269 @PathParam("username") @Parameter(description = "username") String username,
270 @PathParam("id") @Parameter(description = "Rule to remove") String id) {
271 if (!userManagement.authorizeUser(username)) {
272 return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
275 Rule rule = ruleRegistry.remove(id);
277 return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Rule does not exist!");
280 return NetworkUtils.singleSuccess(cs.gson, "/rules/" + id + " deleted.");
284 @Path("{username}/rules/{id}")
285 @Operation(summary = "Set rule attributes", responses = { @ApiResponse(responseCode = "200", description = "OK") })
286 public Response modifyRuleApi(@Context UriInfo uri, //
287 @PathParam("username") @Parameter(description = "username") String username,
288 @PathParam("id") @Parameter(description = "rule id") String id, String body) {
289 if (!userManagement.authorizeUser(username)) {
290 return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
293 final HueRuleEntry changeRequest = cs.gson.fromJson(body, HueRuleEntry.class);
295 Rule rule = ruleRegistry.remove(id);
297 return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Rule does not exist!");
300 RuleBuilder builder = RuleBuilder.create(rule);
304 temp = changeRequest.name;
305 if (!temp.isEmpty()) {
306 builder.withName(changeRequest.name);
309 temp = changeRequest.description;
310 if (!temp.isEmpty()) {
311 builder.withDescription(temp);
315 if (!changeRequest.actions.isEmpty()) {
316 builder.withActions(createActions(rule.getUID(), changeRequest.actions, rule.getActions(), username));
318 if (!changeRequest.conditions.isEmpty()) {
319 builder = createHueRuleConditions(changeRequest.conditions, builder, rule.getTriggers(),
320 rule.getConditions(), itemRegistry);
323 ruleRegistry.add(builder.build());
324 } catch (IllegalStateException e) {
325 return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
328 return NetworkUtils.successList(cs.gson, Arrays.asList( //
329 new HueSuccessGeneric(changeRequest.name, "/rules/" + id + "/name"), //
330 new HueSuccessGeneric(changeRequest.description, "/rules/" + id + "/description"), //
331 new HueSuccessGeneric(changeRequest.actions.toString(), "/rules/" + id + "/actions"), //
332 new HueSuccessGeneric(changeRequest.conditions.toString(), "/rules/" + id + "/conditions") //
336 @SuppressWarnings({ "null" })
338 @Path("{username}/rules")
339 @Operation(summary = "Create a new rule", responses = { @ApiResponse(responseCode = "200", description = "OK") })
340 public Response postNewRule(@Context UriInfo uri,
341 @PathParam("username") @Parameter(description = "username") String username, String body) {
342 if (!userManagement.authorizeUser(username)) {
343 return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
346 HueRuleEntry newRuleData = cs.gson.fromJson(body, HueRuleEntry.class);
347 if (newRuleData == null || newRuleData.name.isEmpty() || newRuleData.actions.isEmpty()
348 || newRuleData.conditions.isEmpty()) {
349 return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON,
350 "Invalid request: No name or actions or conditons!");
353 String uid = UUID.randomUUID().toString();
354 RuleBuilder builder = RuleBuilder.create(uid).withName(newRuleData.name);
356 String description = newRuleData.description;
357 if (description != null) {
358 builder.withDescription(description);
362 builder.withActions(createActions(uid, newRuleData.actions, Collections.emptyList(), username));
363 builder = createHueRuleConditions(newRuleData.conditions, builder, Collections.emptyList(),
364 Collections.emptyList(), itemRegistry);
365 ruleRegistry.add(builder.withTags(RULES_TAG).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");