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