]> git.basschouten.com Git - openhab-addons.git/blob
ceb9fb0a0a58df9bb531347dbad311e1427ebe41
[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.ArrayList;
16 import java.util.Arrays;
17 import java.util.Collections;
18 import java.util.List;
19 import java.util.UUID;
20 import java.util.stream.Collectors;
21
22 import javax.ws.rs.Consumes;
23 import javax.ws.rs.DELETE;
24 import javax.ws.rs.GET;
25 import javax.ws.rs.POST;
26 import javax.ws.rs.PUT;
27 import javax.ws.rs.Path;
28 import javax.ws.rs.PathParam;
29 import javax.ws.rs.Produces;
30 import javax.ws.rs.core.Context;
31 import javax.ws.rs.core.MediaType;
32 import javax.ws.rs.core.Response;
33 import javax.ws.rs.core.UriInfo;
34
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.openhab.core.config.core.Configuration;
37 import org.openhab.core.common.registry.RegistryChangeListener;
38 import org.openhab.core.automation.Action;
39 import org.openhab.core.automation.Rule;
40 import org.openhab.core.automation.RuleManager;
41 import org.openhab.core.automation.RuleRegistry;
42 import org.openhab.core.automation.Trigger;
43 import org.openhab.core.automation.Visibility;
44 import org.openhab.core.automation.util.ModuleBuilder;
45 import org.openhab.core.automation.util.RuleBuilder;
46 import org.openhab.io.hueemulation.internal.ConfigStore;
47 import org.openhab.io.hueemulation.internal.NetworkUtils;
48 import org.openhab.io.hueemulation.internal.RuleUtils;
49 import org.openhab.io.hueemulation.internal.dto.HueDataStore;
50 import org.openhab.io.hueemulation.internal.dto.HueScheduleEntry;
51 import org.openhab.io.hueemulation.internal.dto.changerequest.HueChangeScheduleEntry;
52 import org.openhab.io.hueemulation.internal.dto.changerequest.HueCommand;
53 import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
54 import org.openhab.io.hueemulation.internal.dto.response.HueSuccessGeneric;
55 import org.osgi.service.component.annotations.Activate;
56 import org.osgi.service.component.annotations.Component;
57 import org.osgi.service.component.annotations.Deactivate;
58 import org.osgi.service.component.annotations.Reference;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 import io.swagger.annotations.ApiOperation;
63 import io.swagger.annotations.ApiParam;
64 import io.swagger.annotations.ApiResponse;
65 import io.swagger.annotations.ApiResponses;
66
67 /**
68  * Enables the schedule part of the Hue REST API. Uses automation rules with GenericCronTrigger, TimerTrigger and
69  * AbsoluteDateTimeTrigger depending on the schedule time pattern.
70  * <p>
71  * If the scheduled task should remove itself after completion, a RemoveRuleAction is used in the rule.
72  * <p>
73  * The actual command execution uses HttpAction.
74  *
75  * @author David Graeff - Initial contribution
76  */
77 @Component(immediate = false, service = { Schedules.class }, property = "com.eclipsesource.jaxrs.publish=false")
78 @NonNullByDefault
79 @Path("")
80 @Produces(MediaType.APPLICATION_JSON)
81 @Consumes(MediaType.APPLICATION_JSON)
82 public class Schedules implements RegistryChangeListener<Rule> {
83     public static final String SCHEDULE_TAG = "hueemulation_schedule";
84     private final Logger logger = LoggerFactory.getLogger(Schedules.class);
85
86     @Reference
87     protected @NonNullByDefault({}) ConfigStore cs;
88     @Reference
89     protected @NonNullByDefault({}) UserManagement userManagement;
90
91     @Reference
92     protected @NonNullByDefault({}) RuleManager ruleManager;
93     @Reference
94     protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
95
96     /**
97      * Registers to the {@link RuleRegistry} and enumerates currently existing rules.
98      */
99     @Activate
100     public void activate() {
101         ruleRegistry.removeRegistryChangeListener(this);
102         ruleRegistry.addRegistryChangeListener(this);
103
104         for (Rule item : ruleRegistry.getAll()) {
105             added(item);
106         }
107     }
108
109     @Deactivate
110     public void deactivate() {
111         ruleRegistry.removeRegistryChangeListener(this);
112     }
113
114     /**
115      * Called by the registry when a rule got added (and when a rule got modified).
116      * <p>
117      * Converts the rule into a {@link HueScheduleEntry} object and add that to the hue datastore.
118      */
119     @Override
120     public void added(Rule rule) {
121         if (!rule.getTags().contains(SCHEDULE_TAG)) {
122             return;
123         }
124         HueScheduleEntry entry = new HueScheduleEntry();
125         entry.name = rule.getName();
126         entry.description = rule.getDescription();
127         entry.autodelete = rule.getActions().stream().anyMatch(p -> p.getId().equals("autodelete"));
128         entry.status = ruleManager.isEnabled(rule.getUID()) ? "enabled" : "disabled";
129
130         String timeStringFromTrigger = RuleUtils.timeStringFromTrigger(rule.getTriggers());
131         if (timeStringFromTrigger == null) {
132             logger.warn("Schedule from rule '{}' invalid!", rule.getName());
133             return;
134         }
135
136         entry.localtime = timeStringFromTrigger;
137
138         for (Action a : rule.getActions()) {
139             if (!a.getTypeUID().equals("rules.HttpAction")) {
140                 continue;
141             }
142             HueCommand command = RuleUtils.httpActionToHueCommand(cs.ds, a, rule.getName());
143             if (command == null) {
144                 continue;
145             }
146             entry.command = command;
147         }
148
149         cs.ds.schedules.put(rule.getUID(), entry);
150     }
151
152     @Override
153     public void removed(Rule element) {
154         cs.ds.schedules.remove(element.getUID());
155     }
156
157     @Override
158     public void updated(Rule oldElement, Rule element) {
159         removed(oldElement);
160         added(element);
161     }
162
163     /**
164      * Creates a new rule that executes a http rule action, triggered by the scheduled time
165      *
166      * @param uid A rule unique id.
167      * @param builder A rule builder that will be used for creating the rule. It must have been created with the given
168      *            uid.
169      * @param oldActions Old actions. Useful if `data` is only partially set and old actions should be preserved
170      * @param data The configuration for the http action and trigger time is in here
171      * @return A new rule with the given uid
172      * @throws IllegalStateException If a required parameter is not set or if a light / group that is referred to is not
173      *             existing
174      */
175     protected static Rule createRule(String uid, RuleBuilder builder, List<Action> oldActions,
176             List<Trigger> oldTriggers, HueChangeScheduleEntry data, HueDataStore ds) throws IllegalStateException {
177         HueCommand command = data.command;
178         Boolean autodelete = data.autodelete;
179
180         String temp;
181
182         temp = data.name;
183         if (temp != null) {
184             builder.withName(temp);
185         } else if (oldActions.isEmpty()) { // This is a new rule without a name yet
186             throw new IllegalStateException("Name not set!");
187         }
188
189         temp = data.description;
190         if (temp != null) {
191             builder.withDescription(temp);
192         }
193
194         temp = data.localtime;
195         if (temp != null) {
196             builder.withTriggers(RuleUtils.createTriggerForTimeString(temp));
197         } else if (oldTriggers.isEmpty()) { // This is a new rule without triggers yet
198             throw new IllegalStateException("localtime not set!");
199         }
200
201         List<Action> actions = new ArrayList<>(oldActions);
202
203         if (command != null) {
204             RuleUtils.validateHueHttpAddress(ds, command.address);
205             actions.removeIf(a -> a.getId().equals("command")); // Remove old command action if any and add new one
206             actions.add(RuleUtils.createHttpAction(command, "command"));
207         } else if (oldActions.isEmpty()) { // This is a new rule without an action yet
208             throw new IllegalStateException("No command set!");
209         }
210
211         if (autodelete != null) {
212             // Remove action to remove rule after execution
213             actions = actions.stream().filter(e -> !e.getId().equals("autodelete"))
214                     .collect(Collectors.toCollection(() -> new ArrayList<>()));
215             if (autodelete) { // Add action to remove this rule again after execution
216                 final Configuration actionConfig = new Configuration();
217                 actionConfig.put("removeuid", uid);
218                 actions.add(ModuleBuilder.createAction().withId("autodelete").withTypeUID("rules.RemoveRuleAction")
219                         .withConfiguration(actionConfig).build());
220             }
221         }
222
223         builder.withActions(actions);
224
225         return builder.withVisibility(Visibility.VISIBLE).withTags(SCHEDULE_TAG).build();
226     }
227
228     @GET
229     @Path("{username}/schedules")
230     @Produces(MediaType.APPLICATION_JSON)
231     @ApiOperation(value = "Return all schedules")
232     @ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
233     public Response getSchedulesApi(@Context UriInfo uri,
234             @PathParam("username") @ApiParam(value = "username") String username) {
235         if (!userManagement.authorizeUser(username)) {
236             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
237         }
238         return Response.ok(cs.gson.toJson(cs.ds.schedules)).build();
239     }
240
241     @GET
242     @Path("{username}/schedules/{id}")
243     @ApiOperation(value = "Return a schedule")
244     @ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
245     public Response getScheduleApi(@Context UriInfo uri, //
246             @PathParam("username") @ApiParam(value = "username") String username,
247             @PathParam("id") @ApiParam(value = "schedule id") String id) {
248         if (!userManagement.authorizeUser(username)) {
249             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
250         }
251         return Response.ok(cs.gson.toJson(cs.ds.schedules.get(id))).build();
252     }
253
254     @DELETE
255     @Path("{username}/schedules/{id}")
256     @ApiOperation(value = "Deletes a schedule")
257     @ApiResponses(value = { @ApiResponse(code = 200, message = "The user got removed"),
258             @ApiResponse(code = 403, message = "Access denied") })
259     public Response removeScheduleApi(@Context UriInfo uri,
260             @PathParam("username") @ApiParam(value = "username") String username,
261             @PathParam("id") @ApiParam(value = "Schedule to remove") String id) {
262         if (!userManagement.authorizeUser(username)) {
263             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
264         }
265
266         Rule rule = ruleRegistry.remove(id);
267         if (rule == null) {
268             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Schedule does not exist!");
269         }
270
271         return NetworkUtils.singleSuccess(cs.gson, "/schedules/" + id + " deleted.");
272     }
273
274     @PUT
275     @Path("{username}/schedules/{id}")
276     @ApiOperation(value = "Set schedule attributes")
277     @ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
278     public Response modifyScheduleApi(@Context UriInfo uri, //
279             @PathParam("username") @ApiParam(value = "username") String username,
280             @PathParam("id") @ApiParam(value = "schedule id") String id, String body) {
281         if (!userManagement.authorizeUser(username)) {
282             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
283         }
284
285         final HueChangeScheduleEntry changeRequest = cs.gson.fromJson(body, HueChangeScheduleEntry.class);
286
287         Rule rule = ruleRegistry.remove(id);
288         if (rule == null) {
289             return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Schedule does not exist!");
290         }
291
292         RuleBuilder builder = RuleBuilder.create(rule);
293
294         try {
295             ruleRegistry.add(
296                     createRule(rule.getUID(), builder, rule.getActions(), rule.getTriggers(), changeRequest, cs.ds));
297         } catch (IllegalStateException e) {
298             return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
299         }
300
301         return NetworkUtils.successList(cs.gson, Arrays.asList( //
302                 new HueSuccessGeneric(changeRequest.name, "/schedules/" + id + "/name"), //
303                 new HueSuccessGeneric(changeRequest.description, "/schedules/" + id + "/description"), //
304                 new HueSuccessGeneric(changeRequest.localtime, "/schedules/" + id + "/localtime"), //
305                 new HueSuccessGeneric(changeRequest.status, "/schedules/" + id + "/status"), //
306                 new HueSuccessGeneric(changeRequest.autodelete, "/schedules/1/autodelete"), //
307                 new HueSuccessGeneric(changeRequest.command, "/schedules/1/command") //
308         ));
309     }
310
311     @SuppressWarnings({ "null" })
312     @POST
313     @Path("{username}/schedules")
314     @ApiOperation(value = "Create a new schedule")
315     @ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
316     public Response postNewSchedule(@Context UriInfo uri,
317             @PathParam("username") @ApiParam(value = "username") String username, String body) {
318         if (!userManagement.authorizeUser(username)) {
319             return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
320         }
321
322         HueScheduleEntry newScheduleData = cs.gson.fromJson(body, HueScheduleEntry.class);
323         if (newScheduleData == null || newScheduleData.name.isEmpty() || newScheduleData.localtime.isEmpty()) {
324             return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON,
325                     "Invalid request: No name or localtime!");
326         }
327
328         String uid = UUID.randomUUID().toString();
329         RuleBuilder builder = RuleBuilder.create(uid);
330
331         Rule rule;
332         try {
333             rule = createRule(uid, builder, Collections.emptyList(), Collections.emptyList(), newScheduleData, cs.ds);
334         } catch (IllegalStateException e) { // No stacktrace required, we just need the exception message
335             return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
336         }
337
338         ruleRegistry.add(rule);
339
340         return NetworkUtils.singleSuccess(cs.gson, uid, "id");
341     }
342 }