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: Pascal Larin <plarin@gmail.com>
  • Loading branch information
David Gräff authored and chaton78 committed Jan 1, 2019
1 parent 202005a commit 21ba75e
Show file tree
Hide file tree
Showing 16 changed files with 776 additions and 219 deletions.
Original file line number Diff line number Diff line change
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;
}
}
Original file line number Diff line number Diff line change
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 21ba75e

Please sign in to comment.