Skip to content

Commit

Permalink
Hue emulation: User creation fix, storage service fix, turn white fix…
Browse files Browse the repository at this point in the history
…, group items (openhab#4339)

Fixes:
* Fix user creation without a proposed username in the request. Incl. test
* Fix: The hue value was wrongly applied to the saturation field. Incl. test
* Fix usage of StorageService: Set the classloader
* Set the saturation to 0 if a ct (color temperature) value is set.
  This is because Alexa only sets "ct" if you command her to turn the light white.
* Only call writeToFile in LightItems once, after all items have been
  loaded up from the registry.
* Don't load items twice from the registry.
* Reload items whenever the tags configuration has changed.
* Allow group items

Features:
* Add troubleshoot section to readme.
  Allow a pretty printed output for /api/{username}/lights?debug=true.
* Add REST API POST support on /api/{username}/groups.

Tests:
* Add LightItems class unit tests for adding/updating items and group items
  by category and tags.
* Add tests for setting the hue and saturation and turn a light from color to white

Refactor:
* Move UserAuth class out of DataStore into own HueUserAuth class

Fixes openhab#4293
Fixes openhab#4307

Signed-off-by: David Graeff <david.graeff@web.de>
Signed-off-by: Maximilian Hess <mail@ne0h.de>
  • Loading branch information
David Gräff authored and ne0h committed Sep 15, 2019
1 parent 87a92e6 commit 6813a3a
Show file tree
Hide file tree
Showing 16 changed files with 776 additions and 219 deletions.
Expand Up @@ -14,7 +14,8 @@

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.*;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
Expand All @@ -27,8 +28,13 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

import org.eclipse.smarthome.core.events.EventPublisher;
import org.eclipse.smarthome.core.items.GroupItem;
import org.eclipse.smarthome.core.items.Item;
import org.eclipse.smarthome.core.items.ItemRegistry;
import org.eclipse.smarthome.core.items.events.ItemCommandEvent;
import org.eclipse.smarthome.core.library.types.HSBType;
import org.eclipse.smarthome.core.library.types.OnOffType;
import org.eclipse.smarthome.core.service.ReadyMarker;
import org.eclipse.smarthome.core.service.ReadyService;
import org.eclipse.smarthome.test.java.JavaOSGiTest;
Expand All @@ -38,9 +44,10 @@
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.openhab.io.hueemulation.internal.dto.HueDataStore.UserAuth;
import org.openhab.io.hueemulation.internal.dto.HueDevice;
import org.openhab.io.hueemulation.internal.dto.HueStateColorBulb;
import org.openhab.io.hueemulation.internal.dto.HueUnauthorizedConfig;
import org.openhab.io.hueemulation.internal.dto.HueUserAuth;
import org.osgi.service.cm.ConfigurationAdmin;

import com.google.gson.Gson;
Expand All @@ -58,6 +65,12 @@ public class HueEmulationServiceOSGiTest extends JavaOSGiTest {
@Mock
ConfigurationAdmin configurationAdmin;

@Mock
EventPublisher eventPublisher;

@Mock
Item item;

String host;

@SuppressWarnings("null")
Expand All @@ -74,6 +87,10 @@ public void setUp() {
hueService = getService(HueEmulationService.class, HueEmulationService.class);
assertThat(hueService, notNullValue());

when(item.getName()).thenReturn("itemname");

hueService.setEventPublisher(eventPublisher);

readyService.markReady(new ReadyMarker("fake", "org.eclipse.smarthome.model.core"));
waitFor(() -> hueService.discovery != null, 5000, 100);
assertThat(hueService.started, is(true));
Expand Down Expand Up @@ -154,21 +171,33 @@ public void UnauthorizedAccessTest()
body = read(c);
assertThat(body, containsString("success"));
assertThat(hueService.ds.config.whitelist.get("testuser").name, is("label"));
hueService.ds.config.whitelist.clear();

// Add user name without proposing one (the bridge generates one)
body = "{'devicetype':'label'}";
c = (HttpURLConnection) new URL(host + "/api").openConnection();
c.setRequestProperty("Content-Type", "application/json");
c.setRequestMethod("POST");
c.setDoOutput(true);
c.getOutputStream().write(body.getBytes(), 0, body.getBytes().length);
assertThat(c.getResponseCode(), is(200));
body = read(c);
assertThat(body, containsString("success"));
assertThat(body, containsString(hueService.ds.config.whitelist.keySet().iterator().next()));
}

@Test
public void LightsTest() throws InterruptedException, ExecutionException, TimeoutException, IOException {
HttpURLConnection c;
String body;

hueService.ds.config.whitelist.put("testuser", new UserAuth("testUserLabel"));
hueService.ds.config.whitelist.put("testuser", new HueUserAuth("testUserLabel"));

c = (HttpURLConnection) new URL(host + "/api/testuser/lights").openConnection();
assertThat(c.getResponseCode(), is(200));
body = read(c);
assertThat(body, containsString("{}"));

Item item = mock(Item.class);
hueService.ds.lights.put(1, new HueDevice(item, "switch", DeviceType.SwitchType));
hueService.ds.lights.put(2, new HueDevice(item, "color", DeviceType.ColorType));
hueService.ds.lights.put(3, new HueDevice(item, "white", DeviceType.WhiteTemperatureType));
Expand All @@ -187,4 +216,117 @@ public void LightsTest() throws InterruptedException, ExecutionException, Timeou
body = read(c);
assertThat(body, containsString("color"));
}

@Test
public void LightGroupItemSwitchTest()
throws InterruptedException, ExecutionException, TimeoutException, IOException {
HttpURLConnection c;
String body;

GroupItem gitem = new GroupItem("group", item);
hueService.ds.config.whitelist.put("testuser", new HueUserAuth("testUserLabel"));
hueService.ds.lights.put(7, new HueDevice(gitem, "switch", DeviceType.SwitchType));

body = "{'on':true}";
c = (HttpURLConnection) new URL(host + "/api/testuser/lights/7/state").openConnection();
c.setRequestProperty("Content-Type", "application/json");
c.setRequestMethod("PUT");
c.setDoOutput(true);
c.getOutputStream().write(body.getBytes(), 0, body.getBytes().length);
assertThat(c.getResponseCode(), is(200));
body = read(c);
assertThat(body, containsString("success"));
assertThat(body, containsString("on"));

verify(eventPublisher).post(argThat(ce -> assertOnValue((ItemCommandEvent) ce, true)));
}

@Test
public void LightHueTest() throws InterruptedException, ExecutionException, TimeoutException, IOException {
HttpURLConnection c;
String body;

hueService.ds.config.whitelist.put("testuser", new HueUserAuth("testUserLabel"));
hueService.ds.lights.put(2, new HueDevice(item, "color", DeviceType.ColorType));

body = "{'hue':1000}";
c = (HttpURLConnection) new URL(host + "/api/testuser/lights/2/state").openConnection();
c.setRequestProperty("Content-Type", "application/json");
c.setRequestMethod("PUT");
c.setDoOutput(true);
c.getOutputStream().write(body.getBytes(), 0, body.getBytes().length);
assertThat(c.getResponseCode(), is(200));
body = read(c);
assertThat(body, containsString("success"));
assertThat(body, containsString("hue"));

verify(eventPublisher).post(argThat(ce -> assertHueValue((ItemCommandEvent) ce, 1000)));
}

@Test
public void LightSaturationTest() throws InterruptedException, ExecutionException, TimeoutException, IOException {
HttpURLConnection c;
String body;

hueService.ds.config.whitelist.put("testuser", new HueUserAuth("testUserLabel"));
hueService.ds.lights.put(2, new HueDevice(item, "color", DeviceType.ColorType));

body = "{'sat':50}";
c = (HttpURLConnection) new URL(host + "/api/testuser/lights/2/state").openConnection();
c.setRequestProperty("Content-Type", "application/json");
c.setRequestMethod("PUT");
c.setDoOutput(true);
c.getOutputStream().write(body.getBytes(), 0, body.getBytes().length);
assertThat(c.getResponseCode(), is(200));
body = read(c);
assertThat(body, containsString("success"));
assertThat(body, containsString("sat"));

verify(eventPublisher).post(argThat(ce -> assertSatValue((ItemCommandEvent) ce, 50)));
}

/**
* Amazon echos are setting ct only, if commanded to turn a light white.
*/
@Test
public void LightToWhiteTest() throws InterruptedException, ExecutionException, TimeoutException, IOException {
HttpURLConnection c;
String body;

// We start with a coloured state
when(item.getState()).thenReturn(new HSBType("100,100,100"));
hueService.ds.config.whitelist.put("testuser", new HueUserAuth("testUserLabel"));
hueService.ds.lights.put(2, new HueDevice(item, "color", DeviceType.ColorType));

body = "{'ct':500}";
c = (HttpURLConnection) new URL(host + "/api/testuser/lights/2/state").openConnection();
c.setRequestProperty("Content-Type", "application/json");
c.setRequestMethod("PUT");
c.setDoOutput(true);
c.getOutputStream().write(body.getBytes(), 0, body.getBytes().length);
assertThat(c.getResponseCode(), is(200));
body = read(c);
assertThat(body, containsString("success"));
assertThat(body, containsString("sat"));
assertThat(body, containsString("ct"));

// Saturation is expected to be 0 -> white light
verify(eventPublisher).post(argThat(ce -> assertSatValue((ItemCommandEvent) ce, 0)));
}

private boolean assertHueValue(ItemCommandEvent ce, int hueValue) {
assertThat(((HSBType) ce.getItemCommand()).getHue().intValue(), is(hueValue * 360 / HueStateColorBulb.MAX_HUE));
return true;
}

private boolean assertSatValue(ItemCommandEvent ce, int satValue) {
assertThat(((HSBType) ce.getItemCommand()).getSaturation().intValue(),
is(satValue * 100 / HueStateColorBulb.MAX_SAT));
return true;
}

private boolean assertOnValue(ItemCommandEvent ce, boolean value) {
assertThat(ce.getItemCommand(), is(OnOffType.from(value)));
return true;
}
}
Expand Up @@ -17,28 +17,28 @@
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Paths;

import javax.servlet.http.HttpServletRequest;

import org.eclipse.smarthome.core.events.Event;
import org.eclipse.smarthome.core.events.EventPublisher;
import org.eclipse.smarthome.core.items.GroupItem;
import org.eclipse.smarthome.core.library.items.ColorItem;
import org.eclipse.smarthome.core.library.items.SwitchItem;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.openhab.io.hueemulation.internal.RESTApi.HttpMethod;
import org.openhab.io.hueemulation.internal.dto.HueDataStore;
import org.openhab.io.hueemulation.internal.dto.HueDataStore.UserAuth;
import org.openhab.io.hueemulation.internal.dto.HueDevice;
import org.openhab.io.hueemulation.internal.dto.HueStateColorBulb;
import org.openhab.io.hueemulation.internal.dto.HueStatePlug;
import org.openhab.io.hueemulation.internal.dto.HueUserAuth;

import com.google.gson.Gson;

Expand Down Expand Up @@ -66,24 +66,29 @@ public void setUp() {
configManagement = spy(new ConfigManagement(ds));
restAPI = spy(new RESTApi(ds, userManagement, configManagement, gson));
restAPI.setEventPublisher(eventPublisher);

// Add simulated lights
ds.lights.put(1, new HueDevice(new SwitchItem("switch"), "switch", DeviceType.SwitchType));
ds.lights.put(2, new HueDevice(new ColorItem("color"), "color", DeviceType.ColorType));
ds.lights.put(3, new HueDevice(new ColorItem("white"), "white", DeviceType.WhiteTemperatureType));

// Add group item
ds.lights.put(10,
new HueDevice(new GroupItem("white", new SwitchItem("switch")), "white", DeviceType.SwitchType));
}

@Test
public void invalidUser() throws IOException {
PrintWriter out = mock(PrintWriter.class);
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getMethod()).thenReturn("GET");
int result = restAPI.handleUser(req, out, "testuser", Paths.get(""));
int result = restAPI.handleUser(HttpMethod.GET, "", out, "testuser", Paths.get(""), Paths.get(""), false);
assertEquals(403, result);
}

@Test
public void validUser() throws IOException {
PrintWriter out = mock(PrintWriter.class);
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getMethod()).thenReturn("GET");
ds.config.whitelist.put("testuser", new UserAuth("testuser"));
int result = restAPI.handleUser(req, out, "testuser", Paths.get("/"));
ds.config.whitelist.put("testuser", new HueUserAuth("testuser"));
int result = restAPI.handleUser(HttpMethod.GET, "", out, "testuser", Paths.get("/"), Paths.get(""), false);
assertEquals(200, result);
}

Expand All @@ -93,68 +98,71 @@ public void addUser() throws IOException {
HttpServletRequest req = mock(HttpServletRequest.class);

// GET should fail
when(req.getMethod()).thenReturn("GET");
int result = restAPI.handle(req, out, Paths.get("/api"));
int result = restAPI.handle(HttpMethod.GET, "", out, Paths.get("/api"), false);
assertEquals(405, result);

// Post should create a user, except: if linkbutton not enabled
when(req.getMethod()).thenReturn("POST");
result = restAPI.handle(req, out, Paths.get("/api"));
result = restAPI.handle(HttpMethod.POST, "", out, Paths.get("/api"), false);
assertEquals(10403, result);

// Post should create a user
ds.config.linkbutton = true;
when(req.getMethod()).thenReturn("POST");
BufferedReader r = new BufferedReader(new StringReader("{'username':'testuser','devicetype':'user-label'}"));
when(req.getReader()).thenReturn(r);
when(req.getContentType()).thenReturn("application/json");
result = restAPI.handle(req, out, Paths.get("/api"));
String body = "{'username':'testuser','devicetype':'user-label'}";
result = restAPI.handle(HttpMethod.POST, body, out, Paths.get("/api"), false);
assertEquals(result, 200);
assertThat(ds.config.whitelist.get("testuser").name, is("user-label"));
}

@Test
public void changeLightState() throws IOException {
StringWriter out = new StringWriter();
HttpServletRequest req = mock(HttpServletRequest.class);
// Prepare request mock to POST a json
when(req.getMethod()).thenReturn("PUT");
when(req.getContentType()).thenReturn("application/json");
public void changeSwitchState() throws IOException {
ds.config.whitelist.put("testuser", new HueUserAuth("testuser"));

// Add simulated lights
ds.lights.put(1, new HueDevice(new SwitchItem("switch"), "switch", DeviceType.SwitchType));
ds.lights.put(2, new HueDevice(new ColorItem("color"), "color", DeviceType.ColorType));
ds.lights.put(3, new HueDevice(new ColorItem("white"), "white", DeviceType.WhiteTemperatureType));
assertThat(((HueStatePlug) ds.lights.get(1).state).on, is(false));

// Add simulated api-key
ds.config.whitelist.put("testuser", new UserAuth("testuser"));
StringWriter out = new StringWriter();
String body = "{'on':true}";
int result = restAPI.handle(HttpMethod.PUT, body, out, Paths.get("/api/testuser/lights/1/state"), false);
assertEquals(200, result);
assertThat(out.toString(), containsString("success"));
assertThat(((HueStatePlug) ds.lights.get(1).state).on, is(true));
verify(eventPublisher).post(argThat((Event t) -> {
assertThat(t.getPayload(), is("{\"type\":\"OnOff\",\"value\":\"ON\"}"));
return true;
}));
}

@Test
public void changeGroupItemSwitchState() throws IOException {
ds.config.whitelist.put("testuser", new HueUserAuth("testuser"));

int result;
assertThat(((HueStatePlug) ds.lights.get(10).state).on, is(false));

// Post new state to a switch
assertThat(((HueStatePlug) ds.lights.get(1).state).on, is(false));
when(req.getReader()).thenReturn(new BufferedReader(new StringReader("{'on':true}")));
when(req.getRequestURI()).thenReturn("/api/testuser/lights/1/state");
result = restAPI.handle(req, out, Paths.get(req.getRequestURI()));
StringWriter out = new StringWriter();
String body = "{'on':true}";
int result = restAPI.handle(HttpMethod.PUT, body, out, Paths.get("/api/testuser/lights/10/state"), false);
assertEquals(200, result);
assertThat(out.toString(), containsString("success"));
assertThat(((HueStatePlug) ds.lights.get(1).state).on, is(true));
assertThat(((HueStatePlug) ds.lights.get(10).state).on, is(true));
verify(eventPublisher).post(argThat((Event t) -> {
assertThat(t.getPayload(), is("{\"type\":\"OnOff\",\"value\":\"ON\"}"));
return true;
}));
}

@Test
public void changeOnAndBriValues() throws IOException {
ds.config.whitelist.put("testuser", new HueUserAuth("testuser"));

// Post new state to a light
assertThat(((HueStateColorBulb) ds.lights.get(2).state).on, is(false));
assertThat(((HueStateColorBulb) ds.lights.get(2).state).bri, is(0));
when(req.getReader()).thenReturn(new BufferedReader(new StringReader("{'on':true,'bri':200}")));
when(req.getRequestURI()).thenReturn("/api/testuser/lights/2/state");
result = restAPI.handle(req, out, Paths.get(req.getRequestURI()));

String body = "{'on':true,'bri':200}";
StringWriter out = new StringWriter();
int result = restAPI.handle(HttpMethod.PUT, body, out, Paths.get("/api/testuser/lights/2/state"), false);
assertEquals(200, result);
assertThat(out.toString(), containsString("success"));
assertThat(((HueStateColorBulb) ds.lights.get(2).state).on, is(true));
assertThat(((HueStateColorBulb) ds.lights.get(2).state).bri, is(200));

}

}

0 comments on commit 6813a3a

Please sign in to comment.