]> git.basschouten.com Git - openhab-addons.git/blob
a5851dc85e99d0e1aa98c6ef81585730a26f96bb
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.AbstractMap;
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.Collections;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Map.Entry;
22 import java.util.TreeMap;
23 import java.util.UUID;
24 import java.util.stream.Collectors;
25
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;
38
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;
66
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;
70
71 /**
72  * Handles Hue rules via the automation subsystem and the corresponding REST interface
73  *
74  * @author David Graeff - Initial contribution
75  */
76 @Component(immediate = false, service = Rules.class)
77 @JaxrsResource
78 @JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + HueEmulationService.REST_APP_NAME + ")")
79 @NonNullByDefault
80 @Path("")
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";
85
86     @Reference
87     protected @NonNullByDefault({}) ConfigStore cs;
88     @Reference
89     protected @NonNullByDefault({}) UserManagement userManagement;
90     @Reference
91     protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
92     @Reference
93     protected @NonNullByDefault({}) ItemRegistry itemRegistry;
94
95     /**
96      * Registers to the {@link RuleRegistry} and enumerates currently existing rules.
97      */
98     @Activate
99     public void activate() {
100         ruleRegistry.removeRegistryChangeListener(this);
101         ruleRegistry.addRegistryChangeListener(this);
102
103         for (Rule item : ruleRegistry.getAll()) {
104             added(item);
105         }
106     }
107
108     @Deactivate
109     public void deactivate() {
110         ruleRegistry.removeRegistryChangeListener(this);
111     }
112
113     @Override
114     public void added(Rule rule) {
115         if (!rule.getTags().contains(RULES_TAG)) {
116             return;
117         }
118         HueRuleEntry entry = new HueRuleEntry(rule.getName());
119         String desc = rule.getDescription();
120         if (desc != null) {
121             entry.description = desc;
122         }
123
124         rule.getConditions().stream().filter(c -> "hue.ruleCondition".equals(c.getTypeUID())).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) {
129                 return;
130             }
131
132             entry.conditions.add(condition);
133         });
134
135         rule.getActions().stream().filter(a -> "rules.HttpAction".equals(a.getTypeUID())).forEach(a -> {
136             HueCommand command = RuleUtils.httpActionToHueCommand(cs.ds, a, rule.getName());
137             if (command == null) {
138                 return;
139             }
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);
144         });
145
146         cs.ds.rules.put(rule.getUID(), entry);
147     }
148
149     @Override
150     public void removed(Rule element) {
151         cs.ds.rules.remove(element.getUID());
152     }
153
154     @Override
155     public void updated(Rule oldElement, Rule element) {
156         removed(oldElement);
157         added(element);
158     }
159
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);
166         }
167
168         final Configuration triggerConfig = new Configuration();
169
170         String itemName = parts[2];
171
172         Item item = itemRegistry.get(itemName);
173         if (item == null) {
174             throw new IllegalStateException("Item of address does not exist: " + itemName);
175         }
176
177         triggerConfig.put("itemName", itemName);
178
179         // There might be multiple triggers for the same item. Due to the map, we are only creating one though
180
181         Trigger trigger = ModuleBuilder.createTrigger().withId(id).withTypeUID("core.ItemStateChangeTrigger")
182                 .withConfiguration(triggerConfig).build();
183
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);
188
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;
194         if (value != null) {
195             conditionConfig.put("value", value);
196         }
197
198         Condition conditon = ModuleBuilder.createCondition().withId(id + "-condition").withTypeUID("hue.ruleCondition")
199                 .withConfiguration(conditionConfig).withInputs(inputs).build();
200
201         return new AbstractMap.SimpleEntry<>(trigger, conditon);
202     }
203
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 -> !"core.ItemStateChangeTrigger".equals(a.getTypeUID()))
209                 .collect(Collectors.toMap(e -> e.getId(), e -> e)));
210
211         Map<String, Condition> conditions = new TreeMap<>();
212         conditions.putAll(oldConditions.stream().filter(a -> !"hue.ruleCondition".equals(a.getTypeUID()))
213                 .collect(Collectors.toMap(e -> e.getId(), e -> e)));
214
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());
220         }
221
222         builder.withTriggers(new ArrayList<>(triggers.values())).withConditions(new ArrayList<>(conditions.values()));
223         return builder;
224     }
225
226     protected static List<Action> createActions(String uid, List<HueCommand> hueActions, List<Action> oldActions,
227             String apikey) {
228         // Preserve all actions that are not "rules.HttpAction"
229         List<Action> actions = new ArrayList<>(oldActions);
230         actions.removeIf(a -> "rules.HttpAction".equals(a.getTypeUID()));
231
232         for (HueCommand command : hueActions) {
233             command.address = "/api/" + apikey + command.address;
234             actions.add(RuleUtils.createHttpAction(command, command.address.replace("/", "-")));
235         }
236         return actions;
237     }
238
239     @GET
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");
247         }
248         return Response.ok(cs.gson.toJson(cs.ds.rules)).build();
249     }
250
251     @GET
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");
259         }
260         return Response.ok(cs.gson.toJson(cs.ds.rules.get(id))).build();
261     }
262
263     @DELETE
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");
273         }
274
275         Rule rule = ruleRegistry.remove(id);
276         if (rule == null) {
277             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Rule does not exist!");
278         }
279
280         return NetworkUtils.singleSuccess(cs.gson, "/rules/" + id + " deleted.");
281     }
282
283     @PUT
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");
291         }
292
293         final HueRuleEntry changeRequest = cs.gson.fromJson(body, HueRuleEntry.class);
294
295         Rule rule = ruleRegistry.remove(id);
296         if (rule == null) {
297             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Rule does not exist!");
298         }
299
300         RuleBuilder builder = RuleBuilder.create(rule);
301
302         String temp;
303
304         temp = changeRequest.name;
305         if (!temp.isEmpty()) {
306             builder.withName(changeRequest.name);
307         }
308
309         temp = changeRequest.description;
310         if (!temp.isEmpty()) {
311             builder.withDescription(temp);
312         }
313
314         try {
315             if (!changeRequest.actions.isEmpty()) {
316                 builder.withActions(createActions(rule.getUID(), changeRequest.actions, rule.getActions(), username));
317             }
318             if (!changeRequest.conditions.isEmpty()) {
319                 builder = createHueRuleConditions(changeRequest.conditions, builder, rule.getTriggers(),
320                         rule.getConditions(), itemRegistry);
321             }
322
323             ruleRegistry.add(builder.build());
324         } catch (IllegalStateException e) {
325             return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
326         }
327
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") //
333         ));
334     }
335
336     @SuppressWarnings({ "null" })
337     @POST
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");
344         }
345
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!");
351         }
352
353         String uid = UUID.randomUUID().toString();
354         RuleBuilder builder = RuleBuilder.create(uid).withName(newRuleData.name);
355
356         String description = newRuleData.description;
357         if (description != null) {
358             builder.withDescription(description);
359         }
360
361         try {
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());
368         }
369
370         return NetworkUtils.singleSuccess(cs.gson, uid, "id");
371     }
372 }