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