]> git.basschouten.com Git - openhab-addons.git/blob
67039626debc94f592500df8b7a7da84216d2ed2
[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.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.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;
62
63 import io.swagger.annotations.ApiOperation;
64 import io.swagger.annotations.ApiParam;
65 import io.swagger.annotations.ApiResponse;
66 import io.swagger.annotations.ApiResponses;
67
68 /**
69  * Handles Hue rules via the automation subsystem and the corresponding REST interface
70  *
71  * @author David Graeff - Initial contribution
72  */
73 @Component(immediate = false, service = { Rules.class }, property = "com.eclipsesource.jaxrs.publish=false")
74 @NonNullByDefault
75 @Path("")
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";
80
81     @Reference
82     protected @NonNullByDefault({}) ConfigStore cs;
83     @Reference
84     protected @NonNullByDefault({}) UserManagement userManagement;
85     @Reference
86     protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
87     @Reference
88     protected @NonNullByDefault({}) ItemRegistry itemRegistry;
89
90     /**
91      * Registers to the {@link RuleRegistry} and enumerates currently existing rules.
92      */
93     @Activate
94     public void activate() {
95         ruleRegistry.removeRegistryChangeListener(this);
96         ruleRegistry.addRegistryChangeListener(this);
97
98         for (Rule item : ruleRegistry.getAll()) {
99             added(item);
100         }
101     }
102
103     @Deactivate
104     public void deactivate() {
105         ruleRegistry.removeRegistryChangeListener(this);
106     }
107
108     @Override
109     public void added(Rule rule) {
110         if (!rule.getTags().contains(RULES_TAG)) {
111             return;
112         }
113         HueRuleEntry entry = new HueRuleEntry(rule.getName());
114         String desc = rule.getDescription();
115         if (desc != null) {
116             entry.description = desc;
117         }
118
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) {
124                 return;
125             }
126
127             entry.conditions.add(condition);
128         });
129
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) {
133                 return;
134             }
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);
139         });
140
141         cs.ds.rules.put(rule.getUID(), entry);
142     }
143
144     @Override
145     public void removed(Rule element) {
146         cs.ds.rules.remove(element.getUID());
147     }
148
149     @Override
150     public void updated(Rule oldElement, Rule element) {
151         removed(oldElement);
152         added(element);
153     }
154
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);
161         }
162
163         final Configuration triggerConfig = new Configuration();
164
165         String itemName = parts[2];
166
167         Item item = itemRegistry.get(itemName);
168         if (item == null) {
169             throw new IllegalStateException("Item of address does not exist: " + itemName);
170         }
171
172         triggerConfig.put("itemName", itemName);
173
174         // There might be multiple triggers for the same item. Due to the map, we are only creating one though
175
176         Trigger trigger = ModuleBuilder.createTrigger().withId(id).withTypeUID("core.ItemStateChangeTrigger")
177                 .withConfiguration(triggerConfig).build();
178
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);
183
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;
189         if (value != null) {
190             conditionConfig.put("value", value);
191         }
192
193         Condition conditon = ModuleBuilder.createCondition().withId(id + "-condition").withTypeUID("hue.ruleCondition")
194                 .withConfiguration(conditionConfig).withInputs(inputs).build();
195
196         return new AbstractMap.SimpleEntry<>(trigger, conditon);
197     }
198
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)));
205
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)));
209
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());
215         }
216
217         builder.withTriggers(new ArrayList<>(triggers.values())).withConditions(new ArrayList<>(conditions.values()));
218         return builder;
219     }
220
221     protected static List<Action> createActions(String uid, List<HueCommand> hueActions, List<Action> oldActions,
222             String apikey) {
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"));
226
227         for (HueCommand command : hueActions) {
228             command.address = "/api/" + apikey + command.address;
229             actions.add(RuleUtils.createHttpAction(command, command.address.replace("/", "-")));
230         }
231         return actions;
232     }
233
234     @GET
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");
243         }
244         return Response.ok(cs.gson.toJson(cs.ds.rules)).build();
245     }
246
247     @GET
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");
256         }
257         return Response.ok(cs.gson.toJson(cs.ds.rules.get(id))).build();
258     }
259
260     @DELETE
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");
270         }
271
272         Rule rule = ruleRegistry.remove(id);
273         if (rule == null) {
274             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Rule does not exist!");
275         }
276
277         return NetworkUtils.singleSuccess(cs.gson, "/rules/" + id + " deleted.");
278     }
279
280     @PUT
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");
289         }
290
291         final HueRuleEntry changeRequest = cs.gson.fromJson(body, HueRuleEntry.class);
292
293         Rule rule = ruleRegistry.remove(id);
294         if (rule == null) {
295             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Rule does not exist!");
296         }
297
298         RuleBuilder builder = RuleBuilder.create(rule);
299
300         String temp;
301
302         temp = changeRequest.name;
303         if (!temp.isEmpty()) {
304             builder.withName(changeRequest.name);
305         }
306
307         temp = changeRequest.description;
308         if (!temp.isEmpty()) {
309             builder.withDescription(temp);
310         }
311
312         try {
313             if (!changeRequest.actions.isEmpty()) {
314                 builder.withActions(createActions(rule.getUID(), changeRequest.actions, rule.getActions(), username));
315             }
316             if (!changeRequest.conditions.isEmpty()) {
317                 builder = createHueRuleConditions(changeRequest.conditions, builder, rule.getTriggers(),
318                         rule.getConditions(), itemRegistry);
319             }
320
321             ruleRegistry.add(builder.build());
322         } catch (IllegalStateException e) {
323             return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
324         }
325
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") //
331         ));
332     }
333
334     @SuppressWarnings({ "null" })
335     @POST
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");
343         }
344
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!");
350         }
351
352         String uid = UUID.randomUUID().toString();
353         RuleBuilder builder = RuleBuilder.create(uid).withName(newRuleData.name);
354
355         String description = newRuleData.description;
356         if (description != null) {
357             builder.withDescription(description);
358         }
359
360         try {
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());
367         }
368
369         return NetworkUtils.singleSuccess(cs.gson, uid, "id");
370     }
371 }