2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.hueemulation.internal.rest;
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;
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;
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;
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;
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.
74 * If the scheduled task should remove itself after completion, a RemoveRuleAction is used in the rule.
76 * The actual command execution uses HttpAction.
78 * @author David Graeff - Initial contribution
80 @Component(immediate = false, service = Schedules.class)
82 @JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + HueEmulationService.REST_APP_NAME + ")")
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);
92 protected @NonNullByDefault({}) ConfigStore cs;
94 protected @NonNullByDefault({}) UserManagement userManagement;
97 protected @NonNullByDefault({}) RuleManager ruleManager;
99 protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
102 * Registers to the {@link RuleRegistry} and enumerates currently existing rules.
105 public void activate() {
106 ruleRegistry.removeRegistryChangeListener(this);
107 ruleRegistry.addRegistryChangeListener(this);
109 for (Rule item : ruleRegistry.getAll()) {
115 public void deactivate() {
116 ruleRegistry.removeRegistryChangeListener(this);
120 * Called by the registry when a rule got added (and when a rule got modified).
122 * Converts the rule into a {@link HueScheduleEntry} object and add that to the hue datastore.
125 public void added(Rule rule) {
126 if (!rule.getTags().contains(SCHEDULE_TAG)) {
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";
135 String timeStringFromTrigger = RuleUtils.timeStringFromTrigger(rule.getTriggers());
136 if (timeStringFromTrigger == null) {
137 logger.warn("Schedule from rule '{}' invalid!", rule.getName());
141 entry.localtime = timeStringFromTrigger;
143 for (Action a : rule.getActions()) {
144 if (!a.getTypeUID().equals("rules.HttpAction")) {
147 HueCommand command = RuleUtils.httpActionToHueCommand(cs.ds, a, rule.getName());
148 if (command == null) {
151 entry.command = command;
154 cs.ds.schedules.put(rule.getUID(), entry);
158 public void removed(Rule element) {
159 cs.ds.schedules.remove(element.getUID());
163 public void updated(Rule oldElement, Rule element) {
169 * Creates a new rule that executes a http rule action, triggered by the scheduled time
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
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
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;
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!");
194 temp = data.description;
196 builder.withDescription(temp);
199 temp = data.localtime;
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!");
206 List<Action> actions = new ArrayList<>(oldActions);
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!");
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());
228 builder.withActions(actions);
230 return builder.withVisibility(Visibility.VISIBLE).withTags(SCHEDULE_TAG).build();
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");
242 return Response.ok(cs.gson.toJson(cs.ds.schedules)).build();
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");
254 return Response.ok(cs.gson.toJson(cs.ds.schedules.get(id))).build();
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");
269 Rule rule = ruleRegistry.remove(id);
271 return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Schedule does not exist!");
274 return NetworkUtils.singleSuccess(cs.gson, "/schedules/" + id + " deleted.");
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");
288 final HueChangeScheduleEntry changeRequest = cs.gson.fromJson(body, HueChangeScheduleEntry.class);
290 Rule rule = ruleRegistry.remove(id);
292 return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Schedule does not exist!");
295 RuleBuilder builder = RuleBuilder.create(rule);
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());
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") //
314 @SuppressWarnings({ "null" })
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");
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!");
331 String uid = UUID.randomUUID().toString();
332 RuleBuilder builder = RuleBuilder.create(uid);
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());
341 ruleRegistry.add(rule);
343 return NetworkUtils.singleSuccess(cs.gson, uid, "id");