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.assertEquals;
18 import static org.mockito.ArgumentMatchers.argThat;
19 import static org.mockito.Mockito.verify;
21 import java.io.IOException;
23 import javax.ws.rs.client.Entity;
24 import javax.ws.rs.core.Response;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.glassfish.jersey.server.ResourceConfig;
28 import org.junit.jupiter.api.AfterEach;
29 import org.junit.jupiter.api.BeforeEach;
30 import org.junit.jupiter.api.Test;
31 import org.openhab.core.events.Event;
32 import org.openhab.core.items.GroupItem;
33 import org.openhab.core.items.ItemRegistry;
34 import org.openhab.core.items.events.ItemCommandEvent;
35 import org.openhab.core.library.items.ColorItem;
36 import org.openhab.core.library.items.SwitchItem;
37 import org.openhab.core.library.types.HSBType;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.io.hueemulation.internal.ConfigStore;
40 import org.openhab.io.hueemulation.internal.DeviceType;
41 import org.openhab.io.hueemulation.internal.dto.HueGroupEntry;
42 import org.openhab.io.hueemulation.internal.dto.HueLightEntry;
43 import org.openhab.io.hueemulation.internal.dto.HueStateColorBulb;
44 import org.openhab.io.hueemulation.internal.dto.HueStatePlug;
45 import org.openhab.io.hueemulation.internal.rest.mocks.DummyItemRegistry;
48 * Tests for {@link LightsAndGroups}.
50 * @author David Graeff - Initial contribution
53 public class LightsAndGroupsTests {
54 protected @NonNullByDefault({}) CommonSetup commonSetup;
55 protected @NonNullByDefault({}) ItemRegistry itemRegistry;
56 protected @NonNullByDefault({}) ConfigStore cs;
58 LightsAndGroups subject = new LightsAndGroups();
61 public void setUp() throws IOException {
62 commonSetup = new CommonSetup(false);
63 itemRegistry = new DummyItemRegistry();
65 this.cs = commonSetup.cs;
68 subject.eventPublisher = commonSetup.eventPublisher;
69 subject.userManagement = commonSetup.userManagement;
70 subject.itemRegistry = itemRegistry;
73 // Add simulated lights
74 cs.ds.lights.put("1", new HueLightEntry(new SwitchItem("switch"), "switch", DeviceType.SwitchType));
75 cs.ds.lights.put("2", new HueLightEntry(new ColorItem("color"), "color", DeviceType.ColorType));
76 cs.ds.lights.put("3", new HueLightEntry(new ColorItem("white"), "white", DeviceType.WhiteTemperatureType));
79 cs.ds.groups.put("10",
80 new HueGroupEntry("name", new GroupItem("white", new SwitchItem("switch")), DeviceType.SwitchType));
82 commonSetup.start(new ResourceConfig().registerInstances(subject));
86 public void tearDown() throws Exception {
87 commonSetup.dispose();
91 public void addSwitchableByCategory() {
92 SwitchItem item = new SwitchItem("switch1");
93 item.setCategory("Light");
94 itemRegistry.add(item);
95 HueLightEntry device = cs.ds.lights.get(cs.mapItemUIDtoHueID(item));
96 assertThat(device.item, is(item));
97 assertThat(device.state, is(instanceOf(HueStatePlug.class)));
101 public void addSwitchableByTag() {
102 SwitchItem item = new SwitchItem("switch1");
103 item.addTag("Switchable");
104 itemRegistry.add(item);
105 HueLightEntry device = cs.ds.lights.get(cs.mapItemUIDtoHueID(item));
106 assertThat(device.item, is(item));
107 assertThat(device.state, is(instanceOf(HueStatePlug.class)));
111 public void ignoreByTag() {
112 SwitchItem item = new SwitchItem("switch1");
113 item.addTags("Switchable", "internal"); // The ignore tag will win
114 itemRegistry.add(item);
115 HueLightEntry device = cs.ds.lights.get(cs.mapItemUIDtoHueID(item));
116 assertThat(device, is(nullValue()));
120 public void addGroupSwitchableByTag() {
121 GroupItem item = new GroupItem("group1", new SwitchItem("switch1"));
122 item.addTag("Switchable");
123 itemRegistry.add(item);
124 HueGroupEntry device = cs.ds.groups.get(cs.mapItemUIDtoHueID(item));
125 assertThat(device.groupItem, is(item));
126 assertThat(device.action, is(instanceOf(HueStatePlug.class)));
130 public void addDeviceAsGroupSwitchableByTag() {
131 GroupItem item = new GroupItem("group1", new SwitchItem("switch1"));
132 item.addTag("Switchable");
133 item.addTag("Huelight");
134 itemRegistry.add(item);
135 HueLightEntry device = cs.ds.lights.get(cs.mapItemUIDtoHueID(item));
136 assertThat(device.item, is(item));
137 assertThat(device.state, is(instanceOf(HueStatePlug.class)));
141 public void addGroupWithoutTypeByTag() {
142 GroupItem item = new GroupItem("group1", null);
143 item.addTag("Switchable");
145 itemRegistry.add(item);
147 HueGroupEntry device = cs.ds.groups.get(cs.mapItemUIDtoHueID(item));
148 assertThat(device.groupItem, is(item));
149 assertThat(device.action, is(instanceOf(HueStatePlug.class)));
150 assertThat(cs.ds.groups.get(cs.mapItemUIDtoHueID(item)).groupItem, is(item));
154 public void removeGroupWithoutTypeAndTag() {
155 String groupName = "group1";
156 GroupItem item = new GroupItem(groupName, null);
157 item.addTag("Switchable");
158 itemRegistry.add(item);
160 String hueID = cs.mapItemUIDtoHueID(item);
161 assertThat(cs.ds.groups.get(hueID), notNullValue());
163 subject.updated(item, new GroupItem(groupName, null));
165 assertThat(cs.ds.groups.get(hueID), nullValue());
169 public void updateSwitchable() {
170 SwitchItem item = new SwitchItem("switch1");
171 item.setLabel("labelOld");
172 item.addTag("Switchable");
173 itemRegistry.add(item);
174 String hueID = cs.mapItemUIDtoHueID(item);
175 HueLightEntry device = cs.ds.lights.get(hueID);
176 assertThat(device.item, is(item));
177 assertThat(device.state, is(instanceOf(HueStatePlug.class)));
178 assertThat(device.name, is("labelOld"));
180 SwitchItem newitem = new SwitchItem("switch1");
181 newitem.setLabel("labelNew");
182 newitem.addTag("Switchable");
183 subject.updated(item, newitem);
184 device = cs.ds.lights.get(hueID);
185 assertThat(device.item, is(newitem));
186 assertThat(device.state, is(instanceOf(HueStatePlug.class)));
187 assertThat(device.name, is("labelNew"));
189 // Update with an item that has no tags anymore -> should be removed
190 SwitchItem newitemWithoutTag = new SwitchItem("switch1");
191 newitemWithoutTag.setLabel("labelNew2");
192 subject.updated(newitem, newitemWithoutTag);
194 device = cs.ds.lights.get(hueID);
195 assertThat(device, nullValue());
199 public void changeSwitchState() {
200 assertThat(((HueStatePlug) cs.ds.lights.get("1").state).on, is(false));
202 String body = "{'on':true}";
203 Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/1/state").request()
204 .put(Entity.json(body));
205 assertEquals(200, response.getStatus());
206 assertThat(response.readEntity(String.class), containsString("success"));
207 assertThat(((HueStatePlug) cs.ds.lights.get("1").state).on, is(true));
208 verify(commonSetup.eventPublisher).post(argThat((Event t) -> {
209 assertThat(t.getPayload(), is("{\"type\":\"OnOff\",\"value\":\"ON\"}"));
215 public void changeGroupItemSwitchState() {
216 assertThat(((HueStatePlug) cs.ds.groups.get("10").action).on, is(false));
218 String body = "{'on':true}";
219 Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/groups/10/action").request()
220 .put(Entity.json(body));
221 assertEquals(200, response.getStatus());
222 assertThat(response.readEntity(String.class), containsString("success"));
223 assertThat(((HueStatePlug) cs.ds.groups.get("10").action).on, is(true));
224 verify(commonSetup.eventPublisher).post(argThat((Event t) -> {
225 assertThat(t.getPayload(), is("{\"type\":\"OnOff\",\"value\":\"ON\"}"));
231 public void changeOnValue() {
232 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(false));
234 String body = "{'on':true}";
235 Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/2/state").request()
236 .put(Entity.json(body));
237 assertEquals(200, response.getStatus());
238 String entity = response.readEntity(String.class);
239 assertThat(entity, is("[{\"success\":{\"/lights/2/state/on\":true}}]"));
240 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(true));
244 public void changeOnAndBriValues() {
245 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(false));
246 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).bri, is(1));
248 String body = "{'on':true,'bri':200}";
249 Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/2/state").request()
250 .put(Entity.json(body));
251 assertEquals(200, response.getStatus());
252 assertThat(response.readEntity(String.class), containsString("success"));
253 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(true));
254 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).bri, is(200));
258 public void changeHueSatValues() {
259 HueLightEntry hueDevice = cs.ds.lights.get("2");
260 hueDevice.item.setState(OnOffType.ON);
261 hueDevice.state.as(HueStateColorBulb.class).on = true;
263 String body = "{'hue':1000,'sat':50}";
264 Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/2/state").request()
265 .put(Entity.json(body));
266 assertEquals(200, response.getStatus());
267 assertThat(response.readEntity(String.class), containsString("success"));
268 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(true));
269 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).hue, is(1000));
270 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).sat, is(50));
272 verify(commonSetup.eventPublisher).post(argThat(ce -> assertHueValue((ItemCommandEvent) ce, 1000)));
276 * Amazon echos are setting ct only, if commanded to turn a light white.
279 public void changeCtValue() {
280 HueLightEntry hueDevice = cs.ds.lights.get("2");
281 hueDevice.item.setState(OnOffType.ON);
282 hueDevice.state.as(HueStateColorBulb.class).on = true;
284 String body = "{'ct':500}";
285 Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/2/state").request()
286 .put(Entity.json(body));
287 assertEquals(200, response.getStatus());
288 body = response.readEntity(String.class);
289 assertThat(body, containsString("success"));
290 assertThat(body, containsString("ct"));
291 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(true));
292 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).ct, is(500));
293 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).sat, is(0));
295 // Saturation is expected to be 0 -> white light
296 verify(commonSetup.eventPublisher).post(argThat(ce -> assertSatValue((ItemCommandEvent) ce, 0)));
300 public void switchOnWithXY() {
301 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(false));
302 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).bri, is(1));
304 String body = "{'on':true,'bri':200,'xy':[0.5119,0.4147]}";
305 Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/2/state").request()
306 .put(Entity.json(body));
307 assertEquals(200, response.getStatus());
308 assertThat(response.readEntity(String.class), containsString("success"));
309 assertThat(response.readEntity(String.class), containsString("xy"));
310 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(true));
311 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).bri, is(200));
312 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).xy[0], is(0.5119));
313 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).xy[1], is(0.4147));
314 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).colormode, is(HueStateColorBulb.ColorMode.xy));
315 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).toHSBType().getHue().intValue(),
316 is((int) 27.47722590981918));
317 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).toHSBType().getSaturation().intValue(), is(88));
318 assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).toHSBType().getBrightness().intValue(), is(78));
322 public void allLightsAndSingleLight() {
323 Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights").request().get();
324 assertEquals(200, response.getStatus());
326 String body = response.readEntity(String.class);
328 assertThat(body, containsString("switch"));
329 assertThat(body, containsString("color"));
330 assertThat(body, containsString("white"));
332 // Single light access test
333 response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/2").request().get();
334 assertEquals(200, response.getStatus());
335 body = response.readEntity(String.class);
336 assertThat(body, containsString("color"));
339 private boolean assertHueValue(ItemCommandEvent ce, int hueValue) {
340 assertThat(((HSBType) ce.getItemCommand()).getHue().intValue(), is(hueValue * 360 / HueStateColorBulb.MAX_HUE));
344 private boolean assertSatValue(ItemCommandEvent ce, int satValue) {
345 assertThat(((HSBType) ce.getItemCommand()).getSaturation().intValue(),
346 is(satValue * 100 / HueStateColorBulb.MAX_SAT));