2 * Copyright (c) 2010-2022 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 static org.hamcrest.CoreMatchers.*;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.junit.jupiter.api.Assertions.*;
18 import static org.mockito.ArgumentMatchers.*;
19 import static org.mockito.Mockito.*;
21 import java.io.IOException;
22 import java.lang.reflect.Type;
23 import java.util.Collections;
25 import java.util.Map.Entry;
26 import java.util.Random;
28 import javax.ws.rs.client.Entity;
29 import javax.ws.rs.core.Response;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.glassfish.jersey.server.ResourceConfig;
33 import org.junit.jupiter.api.AfterEach;
34 import org.junit.jupiter.api.BeforeEach;
35 import org.junit.jupiter.api.Test;
36 import org.openhab.core.automation.Rule;
37 import org.openhab.core.automation.RuleManager;
38 import org.openhab.core.automation.RuleRegistry;
39 import org.openhab.core.automation.Trigger;
40 import org.openhab.core.automation.util.RuleBuilder;
41 import org.openhab.core.automation.util.TriggerBuilder;
42 import org.openhab.core.config.core.Configuration;
43 import org.openhab.core.library.items.ColorItem;
44 import org.openhab.core.library.items.SwitchItem;
45 import org.openhab.io.hueemulation.internal.ConfigStore;
46 import org.openhab.io.hueemulation.internal.DeviceType;
47 import org.openhab.io.hueemulation.internal.RuleUtils;
48 import org.openhab.io.hueemulation.internal.dto.HueLightEntry;
49 import org.openhab.io.hueemulation.internal.dto.HueSceneEntry;
50 import org.openhab.io.hueemulation.internal.dto.HueScheduleEntry;
51 import org.openhab.io.hueemulation.internal.dto.changerequest.HueCommand;
52 import org.openhab.io.hueemulation.internal.rest.mocks.DummyRuleRegistry;
54 import com.google.gson.Gson;
55 import com.google.gson.reflect.TypeToken;
58 * Tests for various schedule API endpoints.
60 * @author David Graeff - Initial contribution
63 public class ScheduleTests {
64 protected @NonNullByDefault({}) CommonSetup commonSetup;
65 protected @NonNullByDefault({}) ConfigStore cs;
66 protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
68 Schedules subject = new Schedules();
71 public void setUp() throws IOException {
72 commonSetup = new CommonSetup(false);
73 this.cs = commonSetup.cs;
75 ruleRegistry = new DummyRuleRegistry();
77 subject.cs = commonSetup.cs;
78 subject.userManagement = commonSetup.userManagement;
79 subject.ruleManager = mock(RuleManager.class);
80 when(subject.ruleManager.isEnabled(anyString())).thenReturn(true);
81 subject.ruleRegistry = ruleRegistry;
84 // Add simulated lights
85 cs.ds.lights.put("1", new HueLightEntry(new SwitchItem("switch"), "switch", DeviceType.SwitchType));
86 cs.ds.lights.put("2", new HueLightEntry(new ColorItem("color"), "color", DeviceType.ColorType));
87 cs.ds.lights.put("3", new HueLightEntry(new ColorItem("white"), "white", DeviceType.WhiteTemperatureType));
89 commonSetup.start(new ResourceConfig().registerInstances(subject));
91 // Mock random -> always return int=10 or the highest possible int if bounded
92 Random random = mock(Random.class, withSettings().withoutAnnotations());
93 doReturn(10).when(random).nextInt();
95 Integer bound = a.getArgument(0);
97 }).when(random).nextInt(anyInt());
98 RuleUtils.random = random;
102 public void tearDown() throws Exception {
103 RuleUtils.random = new Random();
104 commonSetup.dispose();
107 @SuppressWarnings("null")
109 public void addUpdateRemoveScheduleToRegistry() {
110 HueCommand command = new HueCommand("/api/testuser/lights/1/state", "PUT", "{'on':true}");
111 String localtime = "2020-02-01T12:12:00";
113 Rule rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
114 .withActions(RuleUtils.createHttpAction(command, "command")) //
115 .withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
117 ruleRegistry.add(rule);
120 HueScheduleEntry sceneEntry = cs.ds.schedules.get("demo1");
121 assertThat(sceneEntry.command.address, is("/api/testuser/lights/1/state"));
122 assertThat(sceneEntry.command.method, is("PUT"));
123 assertThat(sceneEntry.command.body, is("{'on':true}"));
124 assertThat(sceneEntry.localtime, is(localtime));
127 localtime = "2021-03-01T17:12:00";
128 rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
129 .withActions(RuleUtils.createHttpAction(command, "command")) //
130 .withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
131 ruleRegistry.update(rule);
133 sceneEntry = cs.ds.schedules.get("demo1");
134 assertThat(sceneEntry.command.address, is("/api/testuser/lights/1/state"));
135 assertThat(sceneEntry.localtime, is(localtime));
139 ruleRegistry.remove("demo1");
140 sceneEntry = cs.ds.schedules.get("demo1");
141 assertThat(sceneEntry, nullValue());
144 @SuppressWarnings("null")
146 public void addGetRemoveScheduleViaRest() {
148 String body = "{ 'name':'Wake up', 'description':'My wake up alarm', 'localtime':'2015-06-30T14:24:40'," + //
149 "'command':{'address':'/api/testuser/lights/1/state','method':'PUT','body':'{\"on\":true}'} }";
150 Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules").request()
151 .post(Entity.json(body));
152 assertEquals(200, response.getStatus());
153 assertThat(response.readEntity(String.class), containsString("success"));
155 // 1.1 Check for entry
156 Entry<String, HueScheduleEntry> entry = cs.ds.schedules.entrySet().stream().findAny().get();
157 assertThat(entry.getValue().name, is("Wake up"));
158 assertThat(entry.getValue().command.address, is("/api/testuser/lights/1/state"));
159 assertThat(entry.getValue().command.method, is("PUT"));
160 assertThat(entry.getValue().command.body, is("{\"on\":true}"));
161 assertThat(entry.getValue().localtime, is("2015-06-30T14:24:40"));
163 // 1.2 Check for rule
164 Rule rule = ruleRegistry.get(entry.getKey());
165 assertThat(rule.getName(), is("Wake up"));
166 assertThat(rule.getActions().get(0).getId(), is("command"));
167 assertThat(rule.getActions().get(0).getTypeUID(), is("rules.HttpAction"));
170 response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules/" + entry.getKey()).request()
172 assertEquals(200, response.getStatus());
173 HueSceneEntry fromJson = new Gson().fromJson(response.readEntity(String.class), HueSceneEntry.class);
174 assertThat(fromJson.name, is(entry.getValue().name));
177 response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules/" + entry.getKey()).request()
179 assertEquals(200, response.getStatus());
180 assertTrue(cs.ds.schedules.isEmpty());
183 @SuppressWarnings("null")
185 public void updateScheduleViaRest() {
186 HueCommand command = new HueCommand("/api/testuser/lights/1/state", "PUT", "{'on':true}");
187 String localtime = "2020-02-01T12:12:00";
189 Rule rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
190 .withActions(RuleUtils.createHttpAction(command, "command")) //
191 .withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
193 ruleRegistry.add(rule);
195 // Modify (just the name)
196 String body = "{ 'name':'A new name'}";
197 Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules/demo1").request()
198 .put(Entity.json(body));
199 assertEquals(200, response.getStatus());
200 assertThat(response.readEntity(String.class), containsString("name"));
202 Entry<String, HueScheduleEntry> entry = cs.ds.schedules.entrySet().stream().findAny().get();
203 assertThat(entry.getValue().name, is("A new name"));
204 assertThat(entry.getValue().command.address, is("/api/testuser/lights/1/state")); // nothing else should have
206 assertThat(entry.getValue().localtime, is(localtime));
209 rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
210 .withActions(RuleUtils.createHttpAction(command, "command")) //
211 .withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
213 ruleRegistry.update(rule); // Reset rule
215 entry = cs.ds.schedules.entrySet().stream().findAny().get();
216 String uid = entry.getKey();
218 // Modify (Change time)
219 body = "{ 'localtime':'2015-06-30T14:24:40'}";
220 response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules/demo1").request()
221 .put(Entity.json(body));
222 assertEquals(200, response.getStatus());
223 assertThat(response.readEntity(String.class), containsString("localtime"));
225 entry = cs.ds.schedules.entrySet().stream().findAny().get();
226 assertThat(entry.getValue().name, is("test name")); // should not have changed
227 assertThat(entry.getKey(), is(uid));
228 assertThat(entry.getValue().localtime, is("2015-06-30T14:24:40"));
230 // Modify (Change command)
231 body = "{ 'command':{'address':'/api/testuser/lights/2/state','method':'PUT','body':'{\"on\":true}'} }";
232 response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules/demo1").request()
233 .put(Entity.json(body));
234 assertEquals(200, response.getStatus());
235 assertThat(response.readEntity(String.class), containsString("command"));
237 entry = cs.ds.schedules.entrySet().stream().findAny().get();
238 assertThat(entry.getValue().name, is("test name")); // should not have changed
239 assertThat(entry.getKey(), is(uid));
240 assertThat(entry.getValue().command.address, is("/api/testuser/lights/2/state"));
244 public void getAll() {
245 HueCommand command = new HueCommand("/api/testuser/lights/1/state", "POST", "{'on':true}");
246 String localtime = "2020-02-01T12:12:00";
248 Rule rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
249 .withActions(RuleUtils.createHttpAction(command, "command")) //
250 .withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
252 ruleRegistry.add(rule);
254 Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules").request().get();
255 Type type = new TypeToken<Map<String, HueSceneEntry>>() {
257 Map<String, HueSceneEntry> fromJson = new Gson().fromJson(response.readEntity(String.class), type);
258 assertTrue(fromJson.containsKey("demo1"));
262 public void timeStringToTrigger() {
265 Configuration configuration;
268 timeString = "2020-02-01T12:12:00";
269 trigger = RuleUtils.createTriggerForTimeString(timeString);
270 configuration = trigger.getConfiguration();
272 assertThat(trigger.getTypeUID(), is("timer.AbsoluteDateTimeTrigger"));
273 assertThat(configuration.get("date"), is("2020-02-01"));
274 assertThat(configuration.get("time"), is("12:12:00"));
276 // absolute randomized time
277 timeString = "2020-02-01T12:12:00A14:12:34";
278 trigger = RuleUtils.createTriggerForTimeString(timeString);
279 configuration = trigger.getConfiguration();
281 assertThat(trigger.getTypeUID(), is("timer.AbsoluteDateTimeTrigger"));
282 assertThat(configuration.get("date"), is("2020-02-01"));
283 assertThat(configuration.get("time"), is("12:12:00"));
284 assertThat(configuration.get("randomizeTime"), is("14:12:34"));
286 // Recurring times,Monday = 64, Tuesday = 32, Wednesday = 16, Thursday = 8, Friday = 4, Saturday = 2, Sunday= 1
287 // Cron expression: min hour day month weekdays
288 timeString = "W3/T12:15:17";
289 trigger = RuleUtils.createTriggerForTimeString(timeString);
290 configuration = trigger.getConfiguration();
292 assertThat(trigger.getTypeUID(), is("timer.GenericCronTrigger"));
293 assertThat(configuration.get("cronExpression"), is("15 12 * * 6,7"));
295 // Recurring randomized times
296 timeString = "W127/T12:15:17A14:12:34";
297 trigger = RuleUtils.createTriggerForTimeString(timeString);
298 configuration = trigger.getConfiguration();
300 assertThat(trigger.getTypeUID(), is("timer.GenericCronTrigger"));
301 assertThat(configuration.get("cronExpression"), is("15 14 * * 1,2,3,4,5,6,7"));
303 // Timer, expiring after given time
304 timeString = "PT12:12:00";
305 trigger = RuleUtils.createTriggerForTimeString(timeString);
306 configuration = trigger.getConfiguration();
308 assertThat(trigger.getTypeUID(), is("timer.TimerTrigger"));
309 assertThat(configuration.get("time"), is("12:12:00"));
311 // Timer with random element
312 timeString = "PT12:12:00A14:12:34";
313 trigger = RuleUtils.createTriggerForTimeString(timeString);
314 configuration = trigger.getConfiguration();
316 assertThat(trigger.getTypeUID(), is("timer.TimerTrigger"));
317 assertThat(configuration.get("time"), is("12:12:00"));
318 assertThat(configuration.get("randomizeTime"), is("14:12:34"));
320 // Timers, Recurring timer
321 timeString = "R/PT12:12:00";
322 trigger = RuleUtils.createTriggerForTimeString(timeString);
323 configuration = trigger.getConfiguration();
325 assertThat(trigger.getTypeUID(), is("timer.TimerTrigger"));
326 assertThat(configuration.get("time"), is("12:12:00"));
327 assertThat(configuration.get("repeat"), is("-1"));
329 // Recurring timer with random element
330 timeString = "R12/PT12:12:00A14:12:34";
331 trigger = RuleUtils.createTriggerForTimeString(timeString);
332 configuration = trigger.getConfiguration();
334 assertThat(trigger.getTypeUID(), is("timer.TimerTrigger"));
335 assertThat(configuration.get("time"), is("12:12:00"));
336 assertThat(configuration.get("randomizeTime"), is("14:12:34"));
337 assertThat(configuration.get("repeat"), is("12"));
341 public void triggerToTimestring() {
344 Configuration configuration;
347 configuration = new Configuration();
348 configuration.put("date", "2020-02-01");
349 configuration.put("time", "12:12:00");
350 trigger = TriggerBuilder.create().withId("absolutetrigger").withTypeUID("timer.AbsoluteDateTimeTrigger")
351 .withConfiguration(configuration).build();
352 timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
354 assertThat(timeString, is("2020-02-01T12:12:00"));
356 // absolute randomized time
357 configuration = new Configuration();
358 configuration.put("date", "2020-02-01");
359 configuration.put("time", "12:12:00");
360 configuration.put("randomizeTime", "14:12:34");
361 trigger = TriggerBuilder.create().withId("absolutetrigger").withTypeUID("timer.AbsoluteDateTimeTrigger")
362 .withConfiguration(configuration).build();
363 timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
365 assertThat(timeString, is("2020-02-01T12:12:00A14:12:34"));
367 // Recurring times,Monday = 64, Tuesday = 32, Wednesday = 16, Thursday = 8, Friday = 4, Saturday = 2, Sunday= 1
368 // Cron expression: min hour day month weekdays
369 configuration = new Configuration();
370 configuration.put("cronExpression", "15 12 * * 6,7");
371 trigger = TriggerBuilder.create().withId("crontrigger").withTypeUID("timer.GenericCronTrigger")
372 .withConfiguration(configuration).build();
373 timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
375 assertThat(timeString, is("W3/T12:15:00"));
377 // Recurring randomized times (not possible, the cron rule has no way to store that info)
378 configuration = new Configuration();
379 configuration.put("cronExpression", "15 14 * * 1,2,3,4,5,6,7");
380 trigger = TriggerBuilder.create().withId("crontrigger").withTypeUID("timer.GenericCronTrigger")
381 .withConfiguration(configuration).build();
382 timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
384 assertThat(timeString, is("W127/T14:15:00"));
386 // Timer, expiring after given time
387 configuration = new Configuration();
388 configuration.put("time", "12:12:00");
389 trigger = TriggerBuilder.create().withId("timertrigger").withTypeUID("timer.TimerTrigger")
390 .withConfiguration(configuration).build();
391 timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
393 assertThat(timeString, is("PT12:12:00"));
395 // Timer with random element
396 configuration = new Configuration();
397 configuration.put("time", "12:12:00");
398 configuration.put("randomizeTime", "14:12:34");
399 trigger = TriggerBuilder.create().withId("timertrigger").withTypeUID("timer.TimerTrigger")
400 .withConfiguration(configuration).build();
401 timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
403 assertThat(timeString, is("PT12:12:00A14:12:34"));
405 // Timers, Recurring timer
406 configuration = new Configuration();
407 configuration.put("time", "12:12:00");
408 configuration.put("repeat", -1);
409 trigger = TriggerBuilder.create().withId("timertrigger").withTypeUID("timer.TimerTrigger")
410 .withConfiguration(configuration).build();
411 timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
413 assertThat(timeString, is("R/PT12:12:00"));
415 // Recurring timer with random element
416 configuration = new Configuration();
417 configuration.put("time", "12:12:00");
418 configuration.put("randomizeTime", "14:12:34");
419 configuration.put("repeat", 12);
420 trigger = TriggerBuilder.create().withId("timertrigger").withTypeUID("timer.TimerTrigger")
421 .withConfiguration(configuration).build();
422 timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
424 assertThat(timeString, is("R12/PT12:12:00A14:12:34"));