2 * Copyright (c) 2010-2023 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.List;
25 import java.util.Map.Entry;
26 import java.util.Random;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jetty.client.api.ContentResponse;
30 import org.glassfish.jersey.server.ResourceConfig;
31 import org.junit.jupiter.api.AfterEach;
32 import org.junit.jupiter.api.BeforeEach;
33 import org.junit.jupiter.api.Test;
34 import org.openhab.core.automation.Rule;
35 import org.openhab.core.automation.RuleManager;
36 import org.openhab.core.automation.RuleRegistry;
37 import org.openhab.core.automation.Trigger;
38 import org.openhab.core.automation.util.RuleBuilder;
39 import org.openhab.core.automation.util.TriggerBuilder;
40 import org.openhab.core.config.core.Configuration;
41 import org.openhab.core.library.items.ColorItem;
42 import org.openhab.core.library.items.SwitchItem;
43 import org.openhab.io.hueemulation.internal.ConfigStore;
44 import org.openhab.io.hueemulation.internal.DeviceType;
45 import org.openhab.io.hueemulation.internal.RuleUtils;
46 import org.openhab.io.hueemulation.internal.dto.HueLightEntry;
47 import org.openhab.io.hueemulation.internal.dto.HueSceneEntry;
48 import org.openhab.io.hueemulation.internal.dto.HueScheduleEntry;
49 import org.openhab.io.hueemulation.internal.dto.changerequest.HueCommand;
50 import org.openhab.io.hueemulation.internal.rest.mocks.DummyRuleRegistry;
52 import com.google.gson.Gson;
53 import com.google.gson.reflect.TypeToken;
56 * Tests for various schedule API endpoints.
58 * @author David Graeff - Initial contribution
61 public class ScheduleTests {
62 protected @NonNullByDefault({}) CommonSetup commonSetup;
63 protected @NonNullByDefault({}) ConfigStore cs;
64 protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
66 Schedules subject = new Schedules();
69 public void setUp() throws IOException {
70 commonSetup = new CommonSetup(false);
71 this.cs = commonSetup.cs;
73 ruleRegistry = new DummyRuleRegistry();
75 subject.cs = commonSetup.cs;
76 subject.userManagement = commonSetup.userManagement;
77 subject.ruleManager = mock(RuleManager.class);
78 when(subject.ruleManager.isEnabled(anyString())).thenReturn(true);
79 subject.ruleRegistry = ruleRegistry;
82 // Add simulated lights
83 cs.ds.lights.put("1", new HueLightEntry(new SwitchItem("switch"), "switch", DeviceType.SwitchType));
84 cs.ds.lights.put("2", new HueLightEntry(new ColorItem("color"), "color", DeviceType.ColorType));
85 cs.ds.lights.put("3", new HueLightEntry(new ColorItem("white"), "white", DeviceType.WhiteTemperatureType));
87 commonSetup.start(new ResourceConfig().registerInstances(subject));
89 // Mock random -> always return int=10 or the highest possible int if bounded
90 Random random = mock(Random.class, withSettings().withoutAnnotations());
91 doReturn(10).when(random).nextInt();
93 Integer bound = a.getArgument(0);
95 }).when(random).nextInt(anyInt());
96 RuleUtils.random = random;
100 public void tearDown() throws Exception {
101 RuleUtils.random = new Random();
102 commonSetup.dispose();
105 @SuppressWarnings("null")
107 public void addUpdateRemoveScheduleToRegistry() {
108 HueCommand command = new HueCommand("/api/testuser/lights/1/state", "PUT", "{'on':true}");
109 String localtime = "2020-02-01T12:12:00";
111 Rule rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
112 .withActions(RuleUtils.createHttpAction(command, "command")) //
113 .withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
115 ruleRegistry.add(rule);
118 HueScheduleEntry sceneEntry = cs.ds.schedules.get("demo1");
119 assertThat(sceneEntry.command.address, is("/api/testuser/lights/1/state"));
120 assertThat(sceneEntry.command.method, is("PUT"));
121 assertThat(sceneEntry.command.body, is("{'on':true}"));
122 assertThat(sceneEntry.localtime, is(localtime));
125 localtime = "2021-03-01T17:12:00";
126 rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
127 .withActions(RuleUtils.createHttpAction(command, "command")) //
128 .withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
129 ruleRegistry.update(rule);
131 sceneEntry = cs.ds.schedules.get("demo1");
132 assertThat(sceneEntry.command.address, is("/api/testuser/lights/1/state"));
133 assertThat(sceneEntry.localtime, is(localtime));
137 ruleRegistry.remove("demo1");
138 sceneEntry = cs.ds.schedules.get("demo1");
139 assertThat(sceneEntry, nullValue());
142 @SuppressWarnings("null")
144 public void addGetRemoveScheduleViaRest() throws Exception {
146 String body = "{ 'name':'Wake up', 'description':'My wake up alarm', 'localtime':'2015-06-30T14:24:40'," + //
147 "'command':{'address':'/api/testuser/lights/1/state','method':'PUT','body':'{\"on\":true}'} }";
148 ContentResponse response = commonSetup.sendPost("/testuser/schedules", body);
149 assertEquals(200, response.getStatus());
150 assertThat(response.getContentAsString(), containsString("success"));
152 // 1.1 Check for entry
153 Entry<String, HueScheduleEntry> entry = cs.ds.schedules.entrySet().stream().findAny().get();
154 assertThat(entry.getValue().name, is("Wake up"));
155 assertThat(entry.getValue().command.address, is("/api/testuser/lights/1/state"));
156 assertThat(entry.getValue().command.method, is("PUT"));
157 assertThat(entry.getValue().command.body, is("{\"on\":true}"));
158 assertThat(entry.getValue().localtime, is("2015-06-30T14:24:40"));
160 // 1.2 Check for rule
161 Rule rule = ruleRegistry.get(entry.getKey());
162 assertThat(rule.getName(), is("Wake up"));
163 assertThat(rule.getActions().get(0).getId(), is("command"));
164 assertThat(rule.getActions().get(0).getTypeUID(), is("rules.HttpAction"));
167 response = commonSetup.sendGet("/testuser/schedules/" + entry.getKey());
168 assertEquals(200, response.getStatus());
169 HueSceneEntry fromJson = new Gson().fromJson(response.getContentAsString(), HueSceneEntry.class);
170 assertThat(fromJson.name, is(entry.getValue().name));
173 response = commonSetup.sendDelete("/testuser/schedules/" + entry.getKey());
174 assertEquals(200, response.getStatus());
175 assertTrue(cs.ds.schedules.isEmpty());
178 @SuppressWarnings("null")
180 public void updateScheduleViaRest() throws Exception {
181 HueCommand command = new HueCommand("/api/testuser/lights/1/state", "PUT", "{'on':true}");
182 String localtime = "2020-02-01T12:12:00";
184 Rule rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
185 .withActions(RuleUtils.createHttpAction(command, "command")) //
186 .withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
188 ruleRegistry.add(rule);
190 // Modify (just the name)
191 String body = "{ 'name':'A new name'}";
192 ContentResponse response = commonSetup.sendPut("/testuser/schedules/demo1", body);
193 assertEquals(200, response.getStatus());
194 assertThat(response.getContentAsString(), containsString("name"));
196 Entry<String, HueScheduleEntry> entry = cs.ds.schedules.entrySet().stream().findAny().get();
197 assertThat(entry.getValue().name, is("A new name"));
198 assertThat(entry.getValue().command.address, is("/api/testuser/lights/1/state")); // nothing else should have
200 assertThat(entry.getValue().localtime, is(localtime));
203 rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
204 .withActions(RuleUtils.createHttpAction(command, "command")) //
205 .withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
207 ruleRegistry.update(rule); // Reset rule
209 entry = cs.ds.schedules.entrySet().stream().findAny().get();
210 String uid = entry.getKey();
212 // Modify (Change time)
213 body = "{ 'localtime':'2015-06-30T14:24:40'}";
214 response = commonSetup.sendPut("/testuser/schedules/demo1", body);
215 assertEquals(200, response.getStatus());
216 assertThat(response.getContentAsString(), containsString("localtime"));
218 entry = cs.ds.schedules.entrySet().stream().findAny().get();
219 assertThat(entry.getValue().name, is("test name")); // should not have changed
220 assertThat(entry.getKey(), is(uid));
221 assertThat(entry.getValue().localtime, is("2015-06-30T14:24:40"));
223 // Modify (Change command)
224 body = "{ 'command':{'address':'/api/testuser/lights/2/state','method':'PUT','body':'{\"on\":true}'} }";
225 response = commonSetup.sendPut("/testuser/schedules/demo1", body);
226 assertEquals(200, response.getStatus());
227 assertThat(response.getContentAsString(), containsString("command"));
229 entry = cs.ds.schedules.entrySet().stream().findAny().get();
230 assertThat(entry.getValue().name, is("test name")); // should not have changed
231 assertThat(entry.getKey(), is(uid));
232 assertThat(entry.getValue().command.address, is("/api/testuser/lights/2/state"));
236 public void getAll() throws Exception {
237 HueCommand command = new HueCommand("/api/testuser/lights/1/state", "POST", "{'on':true}");
238 String localtime = "2020-02-01T12:12:00";
240 Rule rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
241 .withActions(RuleUtils.createHttpAction(command, "command")) //
242 .withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
244 ruleRegistry.add(rule);
246 ContentResponse response = commonSetup.sendGet("/testuser/schedules");
247 Type type = new TypeToken<Map<String, HueSceneEntry>>() {
249 Map<String, HueSceneEntry> fromJson = new Gson().fromJson(response.getContentAsString(), type);
250 assertTrue(fromJson.containsKey("demo1"));
254 public void timeStringToTrigger() {
257 Configuration configuration;
260 timeString = "2020-02-01T12:12:00";
261 trigger = RuleUtils.createTriggerForTimeString(timeString);
262 configuration = trigger.getConfiguration();
264 assertThat(trigger.getTypeUID(), is("timer.AbsoluteDateTimeTrigger"));
265 assertThat(configuration.get("date"), is("2020-02-01"));
266 assertThat(configuration.get("time"), is("12:12:00"));
268 // absolute randomized time
269 timeString = "2020-02-01T12:12:00A14:12:34";
270 trigger = RuleUtils.createTriggerForTimeString(timeString);
271 configuration = trigger.getConfiguration();
273 assertThat(trigger.getTypeUID(), is("timer.AbsoluteDateTimeTrigger"));
274 assertThat(configuration.get("date"), is("2020-02-01"));
275 assertThat(configuration.get("time"), is("12:12:00"));
276 assertThat(configuration.get("randomizeTime"), is("14:12:34"));
278 // Recurring times,Monday = 64, Tuesday = 32, Wednesday = 16, Thursday = 8, Friday = 4, Saturday = 2, Sunday= 1
279 // Cron expression: min hour day month weekdays
280 timeString = "W3/T12:15:17";
281 trigger = RuleUtils.createTriggerForTimeString(timeString);
282 configuration = trigger.getConfiguration();
284 assertThat(trigger.getTypeUID(), is("timer.GenericCronTrigger"));
285 assertThat(configuration.get("cronExpression"), is("15 12 * * 6,7"));
287 // Recurring randomized times
288 timeString = "W127/T12:15:17A14:12:34";
289 trigger = RuleUtils.createTriggerForTimeString(timeString);
290 configuration = trigger.getConfiguration();
292 assertThat(trigger.getTypeUID(), is("timer.GenericCronTrigger"));
293 assertThat(configuration.get("cronExpression"), is("15 14 * * 1,2,3,4,5,6,7"));
295 // Timer, expiring after given time
296 timeString = "PT12:12:00";
297 trigger = RuleUtils.createTriggerForTimeString(timeString);
298 configuration = trigger.getConfiguration();
300 assertThat(trigger.getTypeUID(), is("timer.TimerTrigger"));
301 assertThat(configuration.get("time"), is("12:12:00"));
303 // Timer with random element
304 timeString = "PT12:12:00A14:12:34";
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"));
310 assertThat(configuration.get("randomizeTime"), is("14:12:34"));
312 // Timers, Recurring timer
313 timeString = "R/PT12:12:00";
314 trigger = RuleUtils.createTriggerForTimeString(timeString);
315 configuration = trigger.getConfiguration();
317 assertThat(trigger.getTypeUID(), is("timer.TimerTrigger"));
318 assertThat(configuration.get("time"), is("12:12:00"));
319 assertThat(configuration.get("repeat"), is("-1"));
321 // Recurring timer with random element
322 timeString = "R12/PT12:12:00A14:12:34";
323 trigger = RuleUtils.createTriggerForTimeString(timeString);
324 configuration = trigger.getConfiguration();
326 assertThat(trigger.getTypeUID(), is("timer.TimerTrigger"));
327 assertThat(configuration.get("time"), is("12:12:00"));
328 assertThat(configuration.get("randomizeTime"), is("14:12:34"));
329 assertThat(configuration.get("repeat"), is("12"));
333 public void triggerToTimestring() {
336 Configuration configuration;
339 configuration = new Configuration();
340 configuration.put("date", "2020-02-01");
341 configuration.put("time", "12:12:00");
342 trigger = TriggerBuilder.create().withId("absolutetrigger").withTypeUID("timer.AbsoluteDateTimeTrigger")
343 .withConfiguration(configuration).build();
344 timeString = RuleUtils.timeStringFromTrigger(List.of(trigger));
346 assertThat(timeString, is("2020-02-01T12:12:00"));
348 // absolute randomized time
349 configuration = new Configuration();
350 configuration.put("date", "2020-02-01");
351 configuration.put("time", "12:12:00");
352 configuration.put("randomizeTime", "14:12:34");
353 trigger = TriggerBuilder.create().withId("absolutetrigger").withTypeUID("timer.AbsoluteDateTimeTrigger")
354 .withConfiguration(configuration).build();
355 timeString = RuleUtils.timeStringFromTrigger(List.of(trigger));
357 assertThat(timeString, is("2020-02-01T12:12:00A14:12:34"));
359 // Recurring times,Monday = 64, Tuesday = 32, Wednesday = 16, Thursday = 8, Friday = 4, Saturday = 2, Sunday= 1
360 // Cron expression: min hour day month weekdays
361 configuration = new Configuration();
362 configuration.put("cronExpression", "15 12 * * 6,7");
363 trigger = TriggerBuilder.create().withId("crontrigger").withTypeUID("timer.GenericCronTrigger")
364 .withConfiguration(configuration).build();
365 timeString = RuleUtils.timeStringFromTrigger(List.of(trigger));
367 assertThat(timeString, is("W3/T12:15:00"));
369 // Recurring randomized times (not possible, the cron rule has no way to store that info)
370 configuration = new Configuration();
371 configuration.put("cronExpression", "15 14 * * 1,2,3,4,5,6,7");
372 trigger = TriggerBuilder.create().withId("crontrigger").withTypeUID("timer.GenericCronTrigger")
373 .withConfiguration(configuration).build();
374 timeString = RuleUtils.timeStringFromTrigger(List.of(trigger));
376 assertThat(timeString, is("W127/T14:15:00"));
378 // Timer, expiring after given time
379 configuration = new Configuration();
380 configuration.put("time", "12:12:00");
381 trigger = TriggerBuilder.create().withId("timertrigger").withTypeUID("timer.TimerTrigger")
382 .withConfiguration(configuration).build();
383 timeString = RuleUtils.timeStringFromTrigger(List.of(trigger));
385 assertThat(timeString, is("PT12:12:00"));
387 // Timer with random element
388 configuration = new Configuration();
389 configuration.put("time", "12:12:00");
390 configuration.put("randomizeTime", "14:12:34");
391 trigger = TriggerBuilder.create().withId("timertrigger").withTypeUID("timer.TimerTrigger")
392 .withConfiguration(configuration).build();
393 timeString = RuleUtils.timeStringFromTrigger(List.of(trigger));
395 assertThat(timeString, is("PT12:12:00A14:12:34"));
397 // Timers, Recurring timer
398 configuration = new Configuration();
399 configuration.put("time", "12:12:00");
400 configuration.put("repeat", -1);
401 trigger = TriggerBuilder.create().withId("timertrigger").withTypeUID("timer.TimerTrigger")
402 .withConfiguration(configuration).build();
403 timeString = RuleUtils.timeStringFromTrigger(List.of(trigger));
405 assertThat(timeString, is("R/PT12:12:00"));
407 // Recurring timer with random element
408 configuration = new Configuration();
409 configuration.put("time", "12:12:00");
410 configuration.put("randomizeTime", "14:12:34");
411 configuration.put("repeat", 12);
412 trigger = TriggerBuilder.create().withId("timertrigger").withTypeUID("timer.TimerTrigger")
413 .withConfiguration(configuration).build();
414 timeString = RuleUtils.timeStringFromTrigger(List.of(trigger));
416 assertThat(timeString, is("R12/PT12:12:00A14:12:34"));