diff --git a/addons/io/org.openhab.io.hueemulation.test/src/test/java/org/openhab/io/hueemulation/internal/HueEmulationServiceOSGiTest.java b/addons/io/org.openhab.io.hueemulation.test/src/test/java/org/openhab/io/hueemulation/internal/HueEmulationServiceOSGiTest.java index 2e77b6044c58..ce5c10bce17a 100644 --- a/addons/io/org.openhab.io.hueemulation.test/src/test/java/org/openhab/io/hueemulation/internal/HueEmulationServiceOSGiTest.java +++ b/addons/io/org.openhab.io.hueemulation.test/src/test/java/org/openhab/io/hueemulation/internal/HueEmulationServiceOSGiTest.java @@ -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; @@ -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; @@ -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; @@ -58,6 +65,12 @@ public class HueEmulationServiceOSGiTest extends JavaOSGiTest { @Mock ConfigurationAdmin configurationAdmin; + @Mock + EventPublisher eventPublisher; + + @Mock + Item item; + String host; @SuppressWarnings("null") @@ -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)); @@ -154,6 +171,19 @@ 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 @@ -161,14 +191,13 @@ public void LightsTest() throws InterruptedException, ExecutionException, Timeou 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)); @@ -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; + } } diff --git a/addons/io/org.openhab.io.hueemulation.test/src/test/java/org/openhab/io/hueemulation/internal/HueRestAPITest.java b/addons/io/org.openhab.io.hueemulation.test/src/test/java/org/openhab/io/hueemulation/internal/HueRestAPITest.java index 1d13a324ddc4..902c8f13b537 100644 --- a/addons/io/org.openhab.io.hueemulation.test/src/test/java/org/openhab/io/hueemulation/internal/HueRestAPITest.java +++ b/addons/io/org.openhab.io.hueemulation.test/src/test/java/org/openhab/io/hueemulation/internal/HueRestAPITest.java @@ -17,10 +17,8 @@ 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; @@ -28,17 +26,19 @@ 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; @@ -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); } @@ -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)); - } - } diff --git a/addons/io/org.openhab.io.hueemulation.test/src/test/java/org/openhab/io/hueemulation/internal/LightItemsTest.java b/addons/io/org.openhab.io.hueemulation.test/src/test/java/org/openhab/io/hueemulation/internal/LightItemsTest.java new file mode 100644 index 000000000000..6f16d6fb2d92 --- /dev/null +++ b/addons/io/org.openhab.io.hueemulation.test/src/test/java/org/openhab/io/hueemulation/internal/LightItemsTest.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.io.hueemulation.internal; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.smarthome.core.items.GroupItem; +import org.eclipse.smarthome.core.items.ItemRegistry; +import org.eclipse.smarthome.core.library.items.SwitchItem; +import org.eclipse.smarthome.core.storage.Storage; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.openhab.io.hueemulation.internal.dto.HueDataStore; +import org.openhab.io.hueemulation.internal.dto.HueDevice; +import org.openhab.io.hueemulation.internal.dto.HueStatePlug; + +import com.google.gson.Gson; + +/** + * Tests for {@link LightItems}. + * + * @author David Graeff - Initial contribution + */ +public class LightItemsTest { + private Gson gson; + private HueDataStore ds; + private LightItems lightItems; + + @Mock + private ItemRegistry itemRegistry; + + @Mock + Storage storage; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(itemRegistry.getItems()).thenReturn(Collections.emptyList()); + gson = new Gson(); + ds = new HueDataStore(); + lightItems = spy(new LightItems(ds)); + lightItems.setItemRegistry(itemRegistry); + lightItems.setFilterTags(Collections.singleton("Switchable"), Collections.singleton("ColorLighting"), + Collections.singleton("Lighting")); + verify(itemRegistry).getItems(); + } + + @Test + public void loadStorage() throws IOException { + Map itemUIDtoHueID = new TreeMap<>(); + itemUIDtoHueID.put("switch1", 12); + when(storage.getKeys()).thenReturn(itemUIDtoHueID.keySet()); + when(storage.get(eq("switch1"))).thenReturn(itemUIDtoHueID.get("switch1")); + lightItems.loadMappingFromFile(storage); + } + + @Test + public void addSwitchableByCategory() throws IOException { + SwitchItem item = new SwitchItem("switch1"); + item.setCategory("Light"); + lightItems.added(item); + HueDevice device = ds.lights.get(lightItems.itemUIDtoHueID.get("switch1")); + assertThat(device.item, is(item)); + assertThat(device.state, is(instanceOf(HueStatePlug.class))); + + } + + @Test + public void addSwitchableByTag() throws IOException { + SwitchItem item = new SwitchItem("switch1"); + item.addTag("Switchable"); + lightItems.added(item); + HueDevice device = ds.lights.get(lightItems.itemUIDtoHueID.get("switch1")); + assertThat(device.item, is(item)); + assertThat(device.state, is(instanceOf(HueStatePlug.class))); + } + + @Test + public void addGroupSwitchableByTag() throws IOException { + GroupItem item = new GroupItem("group1", new SwitchItem("switch1")); + item.addTag("Switchable"); + lightItems.added(item); + HueDevice device = ds.lights.get(lightItems.itemUIDtoHueID.get("group1")); + assertThat(device.item, is(item)); + assertThat(device.state, is(instanceOf(HueStatePlug.class))); + } + + @Test + public void updateSwitchable() throws IOException { + SwitchItem item = new SwitchItem("switch1"); + item.setLabel("labelOld"); + item.addTag("Switchable"); + lightItems.added(item); + Integer hueID = lightItems.itemUIDtoHueID.get("switch1"); + HueDevice device = ds.lights.get(hueID); + assertThat(device.item, is(item)); + assertThat(device.state, is(instanceOf(HueStatePlug.class))); + assertThat(device.name, is("labelOld")); + + SwitchItem newitem = new SwitchItem("switch1"); + newitem.setLabel("labelNew"); + newitem.addTag("Switchable"); + lightItems.updated(item, newitem); + device = ds.lights.get(hueID); + assertThat(device.item, is(newitem)); + assertThat(device.state, is(instanceOf(HueStatePlug.class))); + assertThat(device.name, is("labelNew")); + + // Update with an item that has no tags anymore -> should be removed + SwitchItem newitemWithoutTag = new SwitchItem("switch1"); + newitemWithoutTag.setLabel("labelNew2"); + lightItems.updated(newitem, newitemWithoutTag); + + device = ds.lights.get(hueID); + assertThat(device, nullValue()); + assertThat(lightItems.itemUIDtoHueID.get("switch1"), nullValue()); + } +} diff --git a/addons/io/org.openhab.io.hueemulation/README.md b/addons/io/org.openhab.io.hueemulation/README.md index 2d7c4b03b2cf..18e839d508a2 100644 --- a/addons/io/org.openhab.io.hueemulation/README.md +++ b/addons/io/org.openhab.io.hueemulation/README.md @@ -1,6 +1,10 @@ # openHAB Hue Emulation Service -Hue Emulation exposes openHAB items as Hue devices to other Hue HTTP API compatible applications like an Amazon Echo. +Hue Emulation exposes openHAB items as Hue devices to other Hue HTTP API compatible applications like an Amazon Echo, Google Home or +any Hue compatible application. + +Because Amazon Echo and Google Home control openHAB locally this way, it is a fast and reliable way +to voice control your installation. See the Troubleshoot section down below though. ## Discovery: @@ -8,6 +12,7 @@ As soon as the binding is enabled, it will announce the presence of an (emulated Hue bridges are using the Universal Plug and Play (UPnP) protocol for discovery. Like the real HUE bridge the service must be put into pairing mode before other applications can access it. +By default the pairing mode disables itself after 1 minute (can be configured). ## Exposed devices @@ -23,24 +28,26 @@ This service can emulate 3 different devices: The exposed Hue-type depends on some criteria: * If the item has the category "ColorLight": It will be exposed as a color bulb -* If the item has the category "Light": It will be exposed as a dimmable white bulb. +* If the item has the category "Light": It will be exposed as a switch. -This initial type determination is overriden if the item is tagged. +This initial type determination is overridden if the item is tagged. Tags can be configured in Paper UI, please refer to the next section. The following default tags are setup: -* "Switchable": Item will be exposed as a switchable +* "Switchable": Item will be exposed as an OSRAM SMART+ Plug * "Lighting": Item will be exposed as a dimmable white bulb * "ColorLighting": Item will be exposed as a color bulb It is the responsibility of binding developers to categories and default tag their available *Channels*, so that linked Items are automatically exposed with this service. +You can tag items manually though as well. + ## Exposed names Your items labels are used for exposing! The default naming schema in Paper UI for automatically linked items unfortunately names *Items* like their Channel names, -so usually "Brightness" or "Color". You want to rename those! +so usually "Brightness" or "Color". You want to rename those. ## Configuration: @@ -59,14 +66,26 @@ After that timeout, the `pairingEnabled` is automatically set to `false`. org.openhab.hueemulation:pairingTimeout=60 ``` -For systems with multiple IP addresses the IP to use for UPNP may optionally be specified. -Otherwise the first non loopback address will be used. +To create an api key on the fly, you can set the following option. + +Necessary for Amazon Echos and other devices where the API key cannot be reset. +After a new installation of openHAB or a configuration pruning the old +API keys are gone but the Echos will keep trying with their invalid keys. + +``` +org.openhab.hueemulation:createNewUserOnEveryEndpoint=false +``` + +For systems with multiple IP addresses the IP to expose via UPNP may optionally be specified. +Otherwise the openHAB configured primary address will be used. +Usually you do not want to set this option, but change the primary address configuration of openHAB. ``` org.openhab.hueemulation:discoveryIp=192.168.1.100 ``` -One of the comma separated tags must match for the item to be exposed. Can be empty to match every item. +One of the comma separated tags must match for the item to be exposed. +Can be empty to match an item based on the other criteria. ``` org.openhab.hueemulation:restrictToTagsSwitches=Switchable @@ -74,6 +93,19 @@ org.openhab.hueemulation:restrictToTagsWhiteLights=Lighting org.openhab.hueemulation:restrictToTagsColorLights=ColorLighting ``` +## Troubleshooting + +Some devices like the Amazon Echo, Google Home and all Philips devices expect a Hue bridge to +run on port 80. You must either port forward your openHAB installation to port 80, install +a reverse proxy on port 80 or let openHAB run on port 80. + +You can test if the hue emulation does its job by enabling pairing mode including the option +"Amazon Echo device discovery fix". + +1. Navigate with your browser to "http://your-openhab-ip/description.xml" to check the discovery + response. Check the IP address in there. +2. Navigate with your browser to "http://your-openhab-ip/api/testuser/lights?debug=true" + to check all exposed lights and switches. ## Text configuration example diff --git a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/HueEmulationService.java b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/HueEmulationService.java index 593f731b1ba0..5e1796594c43 100644 --- a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/HueEmulationService.java +++ b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/HueEmulationService.java @@ -42,6 +42,7 @@ import org.eclipse.smarthome.core.service.ReadyService; import org.eclipse.smarthome.core.service.ReadyService.ReadyTracker; import org.eclipse.smarthome.core.storage.StorageService; +import org.openhab.io.hueemulation.internal.RESTApi.HttpMethod; import org.openhab.io.hueemulation.internal.dto.HueDataStore; import org.openhab.io.hueemulation.internal.dto.HueGroup; import org.openhab.io.hueemulation.internal.dto.response.HueResponse; @@ -102,7 +103,6 @@ public class HueEmulationService implements ReadyTracker { //// Required services //// private @NonNullByDefault({}) HttpService httpService; - private @NonNullByDefault({}) ItemRegistry itemRegistry; private @NonNullByDefault({}) NetworkAddressService networkAddressService; private @NonNullByDefault({}) ReadyService readyService; protected @NonNullByDefault({}) HueEmulationUpnpServer discovery; @@ -127,6 +127,9 @@ public class HueEmulationService implements ReadyTracker { protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Utils.setHeaders(resp); final Path path = Paths.get(req.getRequestURI()); + final boolean isDebug = req.getParameter("debug") != null; + String postBody; + final HttpMethod method; try (PrintWriter httpOut = resp.getWriter()) { // UPNP discovery document @@ -137,10 +140,43 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws StringWriter out = new StringWriter(); - resp.setContentType("application/json"); + if (!isDebug) { + resp.setContentType("application/json"); + } else { + resp.setContentType("text/plain"); + } + + try { + method = HttpMethod.valueOf(req.getMethod()); + } catch (Exception e) { + resp.setStatus(405); + apiServerError(req, out, HueResponse.METHOD_NOT_ALLOWED, + req.getMethod() + " not allowed for this resource"); + httpOut.print(out.toString()); + return; + } + + if (method == HttpMethod.POST || method == HttpMethod.PUT) { + try { + postBody = req.getReader().lines().collect(Collectors.joining(System.lineSeparator())); + } catch (IllegalStateException e) { + try { + postBody = new BufferedReader(new InputStreamReader(req.getInputStream())).lines() + .collect(Collectors.joining("\n")); + } catch (IllegalStateException e2) { + apiServerError(req, out, HueResponse.INTERNAL_ERROR, + "Could not read http body. Jetty failure."); + resp.setStatus(500); + return; + } + } + } else { + postBody = ""; + } + int statuscode = 0; try { - statuscode = restAPI.handle(req, out, path); + statuscode = restAPI.handle(method, postBody, out, path, isDebug); } catch (IllegalStateException e) { logger.warn("Unexpected multiple stream access", e); resp.setStatus(500); @@ -235,7 +271,7 @@ protected void modified(Map properties) { protected void deactivate() { readyService.unregisterTracker(this); configManagement.stopPairingTimeoutThread(); - lightItems.close(itemRegistry); + lightItems.close(); userManagement.writeToFile(); configManagement.writeToFile(); @@ -275,8 +311,6 @@ public synchronized void onReadyMarkerAdded(ReadyMarker readyMarker) { logger.debug("Hue Emulation: Cannot register /description.xml"); } - lightItems.fetchItemsAndWatchRegistry(itemRegistry); - configManagement.checkPairingTimeout(); restartDiscovery(); @@ -314,11 +348,11 @@ protected void unsetNetworkAddressService(NetworkAddressService netUtils) { @Reference protected void setItemRegistry(ItemRegistry itemRegistry) { - this.itemRegistry = itemRegistry; + lightItems.setItemRegistry(itemRegistry); } protected void unsetItemRegistry(ItemRegistry itemRegistry) { - this.itemRegistry = null; + lightItems.setItemRegistry(null); } @Reference @@ -341,9 +375,10 @@ protected void unsetHttpService(HttpService httpService) { @Reference(policy = ReferencePolicy.DYNAMIC) protected void setStorageService(StorageService storageService) { - userManagement.loadUsersFromFile(storageService.getStorage("hue.emulation.users")); - lightItems.loadMappingFromFile(storageService.getStorage("hue.emulation.lights")); - configManagement.loadConfigFromFile(storageService.getStorage("hue.emulation.config")); + ClassLoader loader = this.getClass().getClassLoader(); + userManagement.loadUsersFromFile(storageService.getStorage("hue.emulation.users", loader)); + lightItems.loadMappingFromFile(storageService.getStorage("hue.emulation.lights", loader)); + configManagement.loadConfigFromFile(storageService.getStorage("hue.emulation.config", loader)); } protected void unsetStorageService(StorageService storageService) { diff --git a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/LightItems.java b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/LightItems.java index 9ea779bfb046..16931429a75d 100644 --- a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/LightItems.java +++ b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/LightItems.java @@ -22,7 +22,6 @@ import org.eclipse.smarthome.core.items.ItemRegistry; import org.eclipse.smarthome.core.library.CoreItemFactory; import org.eclipse.smarthome.core.storage.Storage; -import org.eclipse.smarthome.core.types.StateDescription; import org.openhab.io.hueemulation.internal.dto.HueDataStore; import org.openhab.io.hueemulation.internal.dto.HueDevice; import org.openhab.io.hueemulation.internal.dto.HueGroup; @@ -30,20 +29,30 @@ import org.slf4j.LoggerFactory; /** - * Listens to the ItemRegistry for items that fulfill the criteria - * (type is any of SWITCH, DIMMER, COLOR and it is tagged or has a category) - * and creates {@link HueDevice} instances for every found item. + * Listens to the ItemRegistry for items that fulfill one of these criteria: + *
    + *
  • Type is any of SWITCH, DIMMER, COLOR (or Group type with one of the mentioned base item types) + *
  • The category is "ColorLight" for coloured lights or "Light" for switchables. + *
  • The item is tagged, according to what is set with {@link #setFilterTags(Set, Set, Set)}. + *
* *

- * The {@link HueDevice} instances are kept in the given {@link HueDataStore}. + * A {@link HueDevice} instances is created for each found item. + * Those are kept in the given {@link HueDataStore}. *

* *

- * Implementing groups or scenes should be done here as well by filtering for GroupItems. - * At the moment only the artificial Group 0 is provided. + * Implementing scenes should be done here as well. *

* - * The HUE Rest API requires a unique integer ID for every listed device. + *

+ * The HUE Rest API requires a unique integer ID for every listed device. A storage service + * is used to store and load this mapping. A storage is not required for this class to work, + * but without it the mapping will be temporary only and ids may change on every boot up. + *

+ * + *

+ *

* * @author David Graeff - Initial contribution */ @@ -54,19 +63,25 @@ public class LightItems implements RegistryChangeListener { .of(CoreItemFactory.COLOR, CoreItemFactory.DIMMER, CoreItemFactory.SWITCH).collect(Collectors.toSet()); // deviceMap maps a unique Item id to a Hue numeric id - private final TreeMap itemUIDtoHueID = new TreeMap<>(); + final TreeMap itemUIDtoHueID = new TreeMap<>(); private final HueDataStore dataStore; private Set switchFilter = Collections.emptySet(); private Set colorFilter = Collections.emptySet(); private Set whiteFilter = Collections.emptySet(); private @Nullable Storage storage; + private boolean initDone = false; + private @NonNullByDefault({}) ItemRegistry itemRegistry; public LightItems(HueDataStore ds) { dataStore = ds; } /** - * Set filter tags. Empty sets are allowed, items will not be filtered then. + * Set filter tags. Empty sets are allowed, items will not be filtered by tags but other criteria then. + * + *

+ * Calling this method will reset the {@link HueDataStore} and parse items from the item registry again. + *

* * @param switchFilter The switch filter tags * @param colorFilter The color filter tags @@ -76,14 +91,38 @@ public void setFilterTags(Set switchFilter, Set colorFilter, Set this.switchFilter = switchFilter; this.colorFilter = colorFilter; this.whiteFilter = whiteFilter; + fetchItems(); + } + + /** + * Sets the item registry. Used to load up items and register to changes + * + * @param itemRegistry The item registry + */ + public void setItemRegistry(@Nullable ItemRegistry itemRegistry) { + this.itemRegistry = itemRegistry; } /** - * Load the {@link #itemUIDtoHueID} mapping. + * Load the {@link #itemUIDtoHueID} mapping from the given storage. + * This method can also be called when the storage service changes. + * A changed storage causes an immediately write request. + *

+ * Because storage services are dynamically bound and may appear late, + * it may happen that the mapping is only loaded after items have been parsed already. + * In that case the {@link HueDataStore} is reset and items from the item registry are parsed again. + * It is important to keep the once exposed ids though. + *

+ * + * @param storage A storage service */ - public void loadMappingFromFile(Storage storage) { + public void loadMappingFromFile(@Nullable Storage storage) { boolean storageChanged = this.storage != null && this.storage != storage; this.storage = storage; + if (storage == null) { + return; + } + for (String itemUID : storage.getKeys()) { Integer hueID = storage.get(itemUID); if (hueID == null) { @@ -91,8 +130,11 @@ public void loadMappingFromFile(Storage storage) { } itemUIDtoHueID.put(itemUID, hueID); } + if (storageChanged) { writeToFile(); + } else if (initDone) { // storage comes late to the game -> reassign all items + fetchItems(); } } @@ -101,19 +143,26 @@ public void loadMappingFromFile(Storage storage) { * Call {@link #close(ItemRegistry)} when you are done with this object. * * Only call this after you have set the filter tags with {@link #setFilterTags(Set, Set, Set)}. - * - * @param itemRegistry The item registry */ - public void fetchItemsAndWatchRegistry(ItemRegistry itemRegistry) { + public synchronized void fetchItems() { + initDone = false; + + dataStore.resetGroupsAndLights(); + + itemRegistry.removeRegistryChangeListener(this); itemRegistry.addRegistryChangeListener(this); + boolean changed = false; for (Item item : itemRegistry.getItems()) { - added(item); + changed |= addItem(item); } - } + initDone = true; - private int generateNextHueID() { - return dataStore.lights.size() == 0 ? 1 : new Integer(dataStore.lights.lastKey().intValue() + 1); + logger.debug("Added items: {}", + dataStore.lights.values().stream().map(l -> l.name).collect(Collectors.joining(", "))); + if (changed) { + writeToFile(); + } } /** @@ -124,6 +173,7 @@ private void writeToFile() { if (storage == null) { return; } + storage.getKeys().forEach(key -> storage.remove(key)); itemUIDtoHueID.forEach((itemUID, hueID) -> storage.put(itemUID, hueID)); } @@ -134,23 +184,16 @@ public void resetStorage() { /** * Unregisters from the {@link ItemRegistry}. */ - public void close(ItemRegistry itemRegistry) { + public void close() { writeToFile(); itemRegistry.removeRegistryChangeListener(this); } - private @Nullable DeviceType determineTargetType(Item element) { + private @Nullable DeviceType determineTargetType(@Nullable String category, String type, Set tags) { // Determine type, heuristically DeviceType t = null; - // No read only states - StateDescription stateDescription = element.getStateDescription(); - if (stateDescription != null && stateDescription.isReadOnly()) { - return t; - } - // First consider the category - String category = element.getCategory(); if (category != null) { switch (category) { case "ColorLight": @@ -162,19 +205,19 @@ public void close(ItemRegistry itemRegistry) { } // Then the tags - if (switchFilter.stream().anyMatch(element.getTags()::contains)) { + if (switchFilter.stream().anyMatch(tags::contains)) { t = DeviceType.SwitchType; } - if (whiteFilter.stream().anyMatch(element.getTags()::contains)) { + if (whiteFilter.stream().anyMatch(tags::contains)) { t = DeviceType.WhiteTemperatureType; } - if (colorFilter.stream().anyMatch(element.getTags()::contains)) { + if (colorFilter.stream().anyMatch(tags::contains)) { t = DeviceType.ColorType; } // Last but not least, the item type if (t == null) { - switch (element.getType()) { + switch (type) { case CoreItemFactory.COLOR: if (colorFilter.size() == 0) { t = DeviceType.ColorType; @@ -195,26 +238,43 @@ public void close(ItemRegistry itemRegistry) { return t; } - @SuppressWarnings({ "unused", "null" }) @Override - public void added(Item element) { + public synchronized void added(Item element) { + addItem(element); + } + + String getType(Item element) { + if (element instanceof GroupItem) { + Item baseItem = ((GroupItem) element).getBaseItem(); + if (baseItem != null) { + return baseItem.getType(); + } else { + return ""; + } + } else { + return element.getType(); + } + } + + @SuppressWarnings({ "unused", "null" }) + public boolean addItem(Item element) { // Only allowed types - if (!ALLOWED_ITEM_TYPES.contains(element.getType())) { - return; + String type = getType(element); + + if (!ALLOWED_ITEM_TYPES.contains(type)) { + return false; } - DeviceType t = determineTargetType(element); + DeviceType t = determineTargetType(element.getCategory(), type, element.getTags()); if (t == null) { - return; + return false; } - logger.debug("Add item {}", element.getUID()); - Integer hueID = itemUIDtoHueID.get(element.getUID()); boolean itemAssociationCreated = false; if (hueID == null) { - hueID = generateNextHueID(); + hueID = dataStore.generateNextLightHueID(); itemAssociationCreated = true; } @@ -229,9 +289,13 @@ public void added(Item element) { } updateGroup0(); itemUIDtoHueID.put(element.getUID(), hueID); - if (itemAssociationCreated) { - writeToFile(); + if (initDone) { + logger.debug("Add item {}", element.getUID()); + if (itemAssociationCreated) { + writeToFile(); + } } + return itemAssociationCreated; } /** @@ -244,7 +308,7 @@ private void updateGroup0() { @SuppressWarnings({ "null", "unused" }) @Override - public void removed(Item element) { + public synchronized void removed(Item element) { Integer hueID = itemUIDtoHueID.get(element.getUID()); if (hueID == null) { return; @@ -262,7 +326,7 @@ public void removed(Item element) { */ @SuppressWarnings({ "null", "unused" }) @Override - public void updated(Item oldElement, Item element) { + public synchronized void updated(Item oldElement, Item element) { Integer hueID = itemUIDtoHueID.get(element.getUID()); if (hueID == null) { // If the correct tags got added -> use the logic within added() @@ -287,7 +351,7 @@ public void updated(Item oldElement, Item element) { } // Check if type can still be determined (tags and category is still sufficient) - DeviceType t = determineTargetType(element); + DeviceType t = determineTargetType(element.getCategory(), getType(element), element.getTags()); if (t == null) { removed(element); return; diff --git a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/RESTApi.java b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/RESTApi.java index 055b5d9dac18..d91ee449e892 100644 --- a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/RESTApi.java +++ b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/RESTApi.java @@ -22,8 +22,6 @@ import java.util.TreeMap; import java.util.UUID; -import javax.servlet.http.HttpServletRequest; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.events.EventPublisher; @@ -38,6 +36,7 @@ import org.openhab.io.hueemulation.internal.dto.changerequest.HueCreateUser; import org.openhab.io.hueemulation.internal.dto.response.HueResponse; import org.openhab.io.hueemulation.internal.dto.response.HueResponse.HueErrorMessage; +import org.openhab.io.hueemulation.internal.dto.response.HueSuccessCreateGroup; import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseCreateUser; import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseStartSearchLights; import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseStateChanged; @@ -64,6 +63,13 @@ public class RESTApi { private final ConfigManagement configManagement; private @NonNullByDefault({}) EventPublisher eventPublisher; + public static enum HttpMethod { + GET, + POST, + PUT, + DELETE + } + public RESTApi(HueDataStore ds, UserManagement userManagement, ConfigManagement configManagement, Gson gson) { this.ds = ds; this.userManagement = userManagement; @@ -88,21 +94,23 @@ private Path remaining(Path path) { /** * Handles /api and forwards any deeper path + * + * @param isDebug */ @SuppressWarnings("null") - public int handle(HttpServletRequest req, Writer out, Path path) throws IOException { + public int handle(HttpMethod method, String body, Writer out, Path path, boolean isDebug) throws IOException { if (!"api".equals(path.getName(0).toString())) { return 404; } if (path.getNameCount() == 1) { // request for API key - if (!"POST".equals(req.getMethod())) { + if (method != HttpMethod.POST) { return 405; } if (ds.config.linkbutton) { final HueCreateUser userRequest; try { - userRequest = gson.fromJson(req.getReader(), HueCreateUser.class); + userRequest = gson.fromJson(body, HueCreateUser.class); } catch (JsonParseException e) { return 400; } @@ -117,7 +125,7 @@ public int handle(HttpServletRequest req, Writer out, Path path) throws IOExcept userManagement.addUser(apiKey, userRequest.devicetype); try (JsonWriter writer = new JsonWriter(out)) { - HueSuccessResponseCreateUser h = new HueSuccessResponseCreateUser(userRequest.username); + HueSuccessResponseCreateUser h = new HueSuccessResponseCreateUser(apiKey); gson.toJson(Collections.singleton(new HueResponse(h)), new TypeToken>() { }.getType(), writer); } @@ -131,13 +139,14 @@ public int handle(HttpServletRequest req, Writer out, Path path) throws IOExcept Path userPath = remaining(path); - return handleUser(req, out, userPath.getName(0).toString(), remaining(userPath)); + return handleUser(method, body, out, userPath.getName(0).toString(), remaining(userPath), path, isDebug); } /** * Handles /api/config and /api/{user-name} and forwards any deeper path */ - public int handleUser(HttpServletRequest req, Writer out, String userName, Path remainingPath) throws IOException { + public int handleUser(HttpMethod method, String body, Writer out, String userName, Path remainingPath, Path fullURI, + boolean isDebug) throws IOException { if ("config".equals(userName)) { // Reduced config try (JsonWriter writer = new JsonWriter(out)) { @@ -156,7 +165,7 @@ public int handleUser(HttpServletRequest req, Writer out, String userName, Path } if (remainingPath.getNameCount() == 0) { /** /api/{username} */ - if (req.getMethod().equals("GET")) { + if (method == HttpMethod.GET) { out.write(gson.toJson(ds)); return 200; } else { @@ -169,11 +178,11 @@ public int handleUser(HttpServletRequest req, Writer out, String userName, Path switch (function) { case "lights": - return handleLights(req, out, remaining(remainingPath)); + return handleLights(method, body, out, remaining(remainingPath), fullURI, isDebug); case "groups": - return handleGroups(req, out, remaining(remainingPath)); + return handleGroups(method, body, out, remaining(remainingPath)); case "config": - return handleConfig(req, out, remaining(remainingPath), userName); + return handleConfig(method, body, out, remaining(remainingPath), userName); default: return 404; } @@ -183,16 +192,16 @@ public int handleUser(HttpServletRequest req, Writer out, String userName, Path * Handles /api/{user-name}/config and /api/{user-name}/config/whitelist * The own whitelisted user can remove itself with a DELETE */ - public int handleConfig(HttpServletRequest req, Writer out, Path remainingPath, String authorizedUser) + public int handleConfig(HttpMethod method, String body, Writer out, Path remainingPath, String authorizedUser) throws IOException { if (remainingPath.getNameCount() == 0) { - if (req.getMethod().equals("GET")) { + if (method == HttpMethod.GET) { out.write(gson.toJson(ds.config)); return 200; - } else if (req.getMethod().equals("PUT")) { + } else if (method == HttpMethod.PUT) { final HueChangeRequest changes; try { - changes = gson.fromJson(req.getReader(), HueChangeRequest.class); + changes = gson.fromJson(body, HueChangeRequest.class); } catch (com.google.gson.JsonParseException e) { return 400; } @@ -212,16 +221,16 @@ public int handleConfig(HttpServletRequest req, Writer out, Path remainingPath, return 405; } } else if (remainingPath.getNameCount() >= 1 && "whitelist".equals(remainingPath.getName(0).toString())) { - return handleConfigWhitelist(req, out, remaining(remainingPath), authorizedUser); + return handleConfigWhitelist(method, out, remaining(remainingPath), authorizedUser); } else { return 404; } } - public int handleConfigWhitelist(HttpServletRequest req, Writer out, Path remainingPath, String authorizedUser) + public int handleConfigWhitelist(HttpMethod method, Writer out, Path remainingPath, String authorizedUser) throws IOException { if (remainingPath.getNameCount() == 0) { - if (req.getMethod().equals("GET")) { + if (method == HttpMethod.GET) { out.write(gson.toJson(ds.config.whitelist)); return 200; } else { @@ -229,11 +238,11 @@ public int handleConfigWhitelist(HttpServletRequest req, Writer out, Path remain } } else if (remainingPath.getNameCount() == 1) { String username = remainingPath.getName(0).toString(); - if (req.getMethod().equals("GET")) { + if (method == HttpMethod.GET) { ds.config.whitelist.get(username); out.write(gson.toJson(ds.config.whitelist)); return 200; - } else if (req.getMethod().equals("DELETE")) { + } else if (method == HttpMethod.DELETE) { // Only own user can be removed if (username.equals(authorizedUser)) { userManagement.removeUser(authorizedUser); @@ -250,13 +259,23 @@ public int handleConfigWhitelist(HttpServletRequest req, Writer out, Path remain } @SuppressWarnings({ "null", "unused" }) - public int handleLights(HttpServletRequest req, Writer out, Path remainingPath) throws IOException { + public int handleLights(HttpMethod method, String body, Writer out, Path remainingPath, Path fullURI, + boolean isDebug) throws IOException { /** /api/{username}/lights */ if (remainingPath.getNameCount() == 0) { - if (req.getMethod().equals("GET")) { // Return complete object - out.write(gson.toJson(ds.lights)); + if (method == HttpMethod.GET) { // Return complete object + if (isDebug) { + out.write("Exposed lights:\n\n"); + for (HueDevice hueDevice : ds.lights.values()) { + out.write(hueDevice.toString()); + out.write("\n"); + } + } else { + ds.lights.values().forEach(v -> v.updateState()); + out.write(gson.toJson(ds.lights)); + } return 200; - } else if (req.getMethod().equals("POST")) { // Starts a search for new lights + } else if (method == HttpMethod.POST) { // Starts a search for new lights try (JsonWriter writer = new JsonWriter(out)) { List responses = new ArrayList<>(); responses.add(new HueResponse(new HueSuccessResponseStartSearchLights())); @@ -273,7 +292,7 @@ public int handleLights(HttpServletRequest req, Writer out, Path remainingPath) /** /api/{username}/lights/new */ if ("new".equals(id)) { - if (req.getMethod().equals("GET")) { + if (method == HttpMethod.GET) { out.write(gson.toJson(new HueNewLights())); return 200; } else { @@ -295,14 +314,15 @@ public int handleLights(HttpServletRequest req, Writer out, Path remainingPath) /** /api/{username}/lights/{id} */ if (remainingPath.getNameCount() == 1) { + hueDevice.updateState(); out.write(gson.toJson(hueDevice)); return 200; } if (remainingPath.getNameCount() == 2) { // Only lights allowed for /state so far - if (req.getMethod().equals("PUT")) { - return handleLightChangeState(req, out, hueID, hueDevice); + if (method == HttpMethod.PUT) { + return handleLightChangeState(fullURI, method, body, out, hueID, hueDevice); } else { return 405; } @@ -312,12 +332,21 @@ public int handleLights(HttpServletRequest req, Writer out, Path remainingPath) } @SuppressWarnings({ "null", "unused" }) - public int handleGroups(HttpServletRequest req, Writer out, Path remainingPath) throws IOException { + public int handleGroups(HttpMethod method, String body, Writer out, Path remainingPath) throws IOException { /** /api/{username}/groups */ if (remainingPath.getNameCount() == 0) { - if (req.getMethod().equals("GET")) { // Return complete object + if (method == HttpMethod.GET) { // Return complete object out.write(gson.toJson(ds.groups)); return 200; + } else if (method == HttpMethod.POST) { // Starts a search for new lights + int hueid = ds.generateNextGroupHueID(); + try (JsonWriter writer = new JsonWriter(out)) { + List responses = new ArrayList<>(); + responses.add(new HueResponse(new HueSuccessCreateGroup(hueid))); + gson.toJson(responses, new TypeToken>() { + }.getType(), writer); + } + return 200; } else { return 405; } @@ -350,11 +379,11 @@ public int handleGroups(HttpServletRequest req, Writer out, Path remainingPath) * Enpoint: /api/{username}/lights/{id}/state */ @SuppressWarnings({ "null", "unused" }) - private int handleLightChangeState(HttpServletRequest req, Writer out, int hueID, HueDevice hueDevice) - throws IOException { + private int handleLightChangeState(Path fullURI, HttpMethod method, String body, Writer out, int hueID, + HueDevice hueDevice) throws IOException { HueStateChange state; try { - state = gson.fromJson(req.getReader(), HueStateChange.class); + state = gson.fromJson(body, HueStateChange.class); } catch (com.google.gson.JsonParseException e) { return 400; } @@ -362,6 +391,8 @@ private int handleLightChangeState(HttpServletRequest req, Writer out, int hueID return 400; } + // logger.debug("Received state change: {}", gson.toJson(state)); + // Apply new state and collect success, error items Map successApplied = new TreeMap<>(); List errorApplied = new ArrayList<>(); @@ -376,8 +407,7 @@ private int handleLightChangeState(HttpServletRequest req, Writer out, int hueID // Generate the response. The response consists of a list with an entry each for all // submitted change requests. If for example "on" and "bri" was send, 2 entries in the response are // expected. - final Path path = Paths.get(req.getRequestURI()); - Path contextPath = path.subpath(2, path.getNameCount() - 1); + Path contextPath = fullURI.subpath(2, fullURI.getNameCount() - 1); List responses = new ArrayList<>(); successApplied.forEach((t, v) -> { responses.add(new HueResponse(new HueSuccessResponseStateChanged(contextPath.resolve(t).toString(), v))); diff --git a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/UserManagement.java b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/UserManagement.java index e3d1debe11ea..a86e2d100f54 100644 --- a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/UserManagement.java +++ b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/UserManagement.java @@ -16,7 +16,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.storage.Storage; import org.openhab.io.hueemulation.internal.dto.HueDataStore; -import org.openhab.io.hueemulation.internal.dto.HueDataStore.UserAuth; +import org.openhab.io.hueemulation.internal.dto.HueUserAuth; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,7 +29,7 @@ public class UserManagement { private final Logger logger = LoggerFactory.getLogger(UserManagement.class); private final HueDataStore dataStore; - private @Nullable Storage storage; + private @Nullable Storage storage; public UserManagement(HueDataStore ds) { dataStore = ds; @@ -38,11 +38,11 @@ public UserManagement(HueDataStore ds) { /** * Load users from disk */ - public void loadUsersFromFile(Storage storage) { + public void loadUsersFromFile(Storage storage) { boolean storageChanged = this.storage != null && this.storage != storage; this.storage = storage; for (String id : storage.getKeys()) { - UserAuth userAuth = storage.get(id); + HueUserAuth userAuth = storage.get(id); if (userAuth == null) { continue; } @@ -58,7 +58,7 @@ public void loadUsersFromFile(Storage storage) { */ @SuppressWarnings("null") public boolean authorizeUser(String userName) throws IOException { - UserAuth userAuth = dataStore.config.whitelist.get(userName); + HueUserAuth userAuth = dataStore.config.whitelist.get(userName); if (userAuth != null) { userAuth.lastUseDate = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); } @@ -72,14 +72,14 @@ public boolean authorizeUser(String userName) throws IOException { public synchronized void addUser(String apiKey, String label) throws IOException { if (!dataStore.config.whitelist.containsKey(apiKey)) { logger.debug("APIKey {} added", apiKey); - dataStore.config.whitelist.put(apiKey, new UserAuth(label)); + dataStore.config.whitelist.put(apiKey, new HueUserAuth(label)); writeToFile(); } } @SuppressWarnings("null") public synchronized void removeUser(String apiKey) { - UserAuth userAuth = dataStore.config.whitelist.remove(apiKey); + HueUserAuth userAuth = dataStore.config.whitelist.remove(apiKey); if (userAuth != null) { logger.debug("APIKey {} removed", apiKey); writeToFile(); @@ -90,10 +90,11 @@ public synchronized void removeUser(String apiKey) { * Persist users to storage. */ void writeToFile() { - Storage storage = this.storage; + Storage storage = this.storage; if (storage == null) { return; } + storage.getKeys().forEach(key -> storage.remove(key)); dataStore.config.whitelist.forEach((id, userAuth) -> storage.put(id, userAuth)); } diff --git a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueAuthorizedConfig.java b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueAuthorizedConfig.java index aa60867eab55..144bfa3a52ef 100644 --- a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueAuthorizedConfig.java +++ b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueAuthorizedConfig.java @@ -13,7 +13,6 @@ import java.util.TreeMap; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.io.hueemulation.internal.dto.HueDataStore.UserAuth; /** * Hue API config object @@ -48,5 +47,5 @@ public class HueAuthorizedConfig extends HueUnauthorizedConfig { public String proxyaddress = "none"; public int proxyport = 0; - public final Map whitelist = new TreeMap<>(); + public final Map whitelist = new TreeMap<>(); } diff --git a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueDataStore.java b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueDataStore.java index cbf1eb3eb3b7..c3ddc6572d1c 100644 --- a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueDataStore.java +++ b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueDataStore.java @@ -8,8 +8,6 @@ */ package org.openhab.io.hueemulation.internal.dto; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.Collections; import java.util.Map; import java.util.TreeMap; @@ -28,7 +26,7 @@ public class HueDataStore { public HueAuthorizedConfig config = new HueAuthorizedConfig(); public TreeMap lights = new TreeMap<>(); - public Map groups = new TreeMap<>(); + public TreeMap groups = new TreeMap<>(); public Map scenes = new TreeMap<>(); public Map rules = new TreeMap<>(); public Map sensors = new TreeMap<>(); @@ -36,32 +34,24 @@ public class HueDataStore { public Map resourcelinks = Collections.emptyMap(); public HueDataStore() { + resetGroupsAndLights(); + } + + public void resetGroupsAndLights() { + groups.clear(); + lights.clear(); // There must be a group 0 all the time! groups.put(0, new HueGroup("All lights", null, Collections.emptyMap())); } - public static class Dummy { + public int generateNextLightHueID() { + return lights.size() == 0 ? 1 : new Integer(lights.lastKey().intValue() + 1); } - public static class UserAuth { - public String name = ""; - public String createDate = ""; - public String lastUseDate = ""; - - /** - * For de-serialization. - */ - public UserAuth() { - } + public int generateNextGroupHueID() { + return groups.size() == 0 ? 1 : new Integer(groups.lastKey().intValue() + 1); + } - /** - * Create a new user - * - * @param name Visible name - */ - public UserAuth(String name) { - this.name = name; - this.createDate = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); - } + public static class Dummy { } } diff --git a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueDevice.java b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueDevice.java index 93e46332aef7..7bfb3020e36f 100644 --- a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueDevice.java +++ b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueDevice.java @@ -20,6 +20,7 @@ import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.State; import org.openhab.io.hueemulation.internal.DeviceType; +import org.openhab.io.hueemulation.internal.dto.HueStateColorBulb.ColorMode; /** * Hue API device object @@ -39,7 +40,7 @@ public class HueDevice { public final @Nullable String luminaireuniqueid = null; public final @Nullable String swconfigid; public final @Nullable String productid; - public Boolean friendsOfHue = true; + public @Nullable Boolean friendsOfHue = true; public final @Nullable String colorGamut; public @Nullable Boolean hascolor = null; @@ -139,6 +140,8 @@ public HueDevice(Item item, String uniqueid, DeviceType deviceType) { this.swconfigid = null; this.swversion = "V1.04.12"; this.productid = null; + this.hascolor = false; + this.friendsOfHue = null; break; } @@ -192,9 +195,9 @@ private T as(Class type) throws ClassCastExcepti /** * Apply the new received state from the REST PUT request. * - * @param newState New state + * @param newState New state * @param successApplied Output map "state-name"->value: All successfully applied items are added in here - * @param errorApplied Output: All erroneous items are added in here + * @param errorApplied Output: All erroneous items are added in here * @return Return a command computed via the incoming state object. */ public @Nullable Command applyState(HueStateChange newState, Map successApplied, @@ -244,29 +247,15 @@ private T as(Class type) throws ClassCastExcepti try { HueStateColorBulb c = as(HueStateColorBulb.class); - if (c.sat != newState.sat) { - c.sat = newState.sat; - command = c.toHSBType(); - } + c.sat = newState.sat; + c.colormode = ColorMode.hs; + command = c.toHSBType(); successApplied.put("sat", newState.sat); } catch (ClassCastException e) { errorApplied.add("sat"); } } - if (newState.hue != null) { - try { - HueStateColorBulb c = as(HueStateColorBulb.class); - if (c.hue != newState.hue) { - c.hue = newState.hue; - command = c.toHSBType(); - } - successApplied.put("hue", newState.hue); - } catch (ClassCastException e) { - errorApplied.add("hue"); - } - } - if (newState.sat_inc != null) { try { HueStateColorBulb c = as(HueStateColorBulb.class); @@ -274,6 +263,7 @@ private T as(Class type) throws ClassCastExcepti if (newV < 0 || newV > HueStateColorBulb.MAX_SAT) { throw new ClassCastException(); } + c.colormode = ColorMode.hs; c.sat = newV; command = c.toHSBType(); successApplied.put("sat", newState.sat); @@ -282,6 +272,18 @@ private T as(Class type) throws ClassCastExcepti } } + if (newState.hue != null) { + try { + HueStateColorBulb c = as(HueStateColorBulb.class); + c.colormode = ColorMode.hs; + c.hue = newState.hue; + command = c.toHSBType(); + successApplied.put("hue", newState.hue); + } catch (ClassCastException e) { + errorApplied.add("hue"); + } + } + if (newState.hue_inc != null) { try { HueStateColorBulb c = as(HueStateColorBulb.class); @@ -289,6 +291,7 @@ private T as(Class type) throws ClassCastExcepti if (newV < 0 || newV > HueStateColorBulb.MAX_HUE) { throw new ClassCastException(); } + c.colormode = ColorMode.hs; c.hue = newV; command = c.toHSBType(); successApplied.put("hue", newState.hue); @@ -299,11 +302,20 @@ private T as(Class type) throws ClassCastExcepti if (newState.ct != null) { try { - if (as(HueStateBulb.class).ct != newState.ct) { - as(HueStateBulb.class).ct = newState.ct; - // We can't do anything here with a white color temperature. - // The core ESH color type does not support setting it. + // We can't do anything here with a white color temperature. + // The core ESH color type does not support setting it. + + // Adjusting the color temperature implies setting the mode to ct + if (state instanceof HueStateColorBulb) { + HueStateColorBulb c = as(HueStateColorBulb.class); + if (c.colormode != ColorMode.ct || c.sat > 0) { + c.sat = 0; + c.colormode = ColorMode.ct; + command = c.toHSBType(); + } } + successApplied.put("colormode", ColorMode.ct); + successApplied.put("sat", 0); successApplied.put("ct", newState.ct); } catch (ClassCastException e) { errorApplied.add("ct"); @@ -312,12 +324,18 @@ private T as(Class type) throws ClassCastExcepti if (newState.ct_inc != null) { try { - HueStateColorBulb c = as(HueStateColorBulb.class); - int newV = c.ct + newState.ct_inc; - if (newV < 0 || newV > HueStateBulb.MAX_CT) { - throw new ClassCastException(); + // We can't do anything here with a white color temperature. + // The core ESH color type does not support setting it. + + // Adjusting the color temperature implies setting the mode to ct + if (state instanceof HueStateColorBulb) { + HueStateColorBulb c = as(HueStateColorBulb.class); + if (c.colormode != ColorMode.ct) { + c.sat = 0; + command = c.toHSBType(); + successApplied.put("colormode", c.colormode); + } } - c.ct = newV; successApplied.put("ct", newState.ct); } catch (ClassCastException e) { errorApplied.add("ct_inc"); @@ -346,10 +364,27 @@ private T as(Class type) throws ClassCastExcepti public void updateItem(Item element) { item = element; setState(item.getState()); + // Just update the item label and item reference String label = element.getLabel(); if (label != null) { name = label; } } + + /** + * Synchronizes the item state with the hue state object + */ + public void updateState() { + setState(item.getState()); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + b.append(name).append(": ").append(type).append("\n\t"); + b.append("State: ").append(state.toString()); + + return b.toString(); + } } diff --git a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueStateBulb.java b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueStateBulb.java index 28be008f7402..db48e25e27ed 100644 --- a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueStateBulb.java +++ b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueStateBulb.java @@ -39,7 +39,7 @@ public HueStateBulb(boolean on) { * Create a hue state with the given brightness percentage * * @param brightness Brightness percentage - * @param on On value + * @param on On value */ public HueStateBulb(PercentType brightness, boolean on) { super(on); @@ -48,6 +48,6 @@ public HueStateBulb(PercentType brightness, boolean on) { @Override public String toString() { - return "[on: " + on + " bri: " + bri + " reachable: " + reachable; + return "on: " + on + ", brightness: " + bri + ", reachable: " + reachable; } } diff --git a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueStateColorBulb.java b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueStateColorBulb.java index 9fda703e90c4..f8829fc09088 100644 --- a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueStateColorBulb.java +++ b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueStateColorBulb.java @@ -32,7 +32,14 @@ public class HueStateColorBulb extends HueStateBulb { /** time for transition in centiseconds. */ public int transitiontime; - public String colormode = "ct"; + + public static enum ColorMode { + ct, + hs, + xy + } + + public ColorMode colormode = ColorMode.ct; protected HueStateColorBulb() { } @@ -40,6 +47,7 @@ protected HueStateColorBulb() { public HueStateColorBulb(boolean on) { super(on); this.bri = on ? MAX_BRI : 0; + colormode = ColorMode.ct; } /** @@ -50,6 +58,7 @@ public HueStateColorBulb(boolean on) { */ public HueStateColorBulb(PercentType brightness, boolean on) { super(brightness, on); + colormode = ColorMode.ct; } /** @@ -62,6 +71,7 @@ public HueStateColorBulb(HSBType hsb) { this.hue = hsb.getHue().intValue() * MAX_HUE / 360; this.sat = hsb.getSaturation().intValue() * MAX_SAT / 100; this.bri = hsb.getBrightness().intValue() * MAX_BRI / 100; + colormode = this.sat > 0 ? ColorMode.hs : ColorMode.ct; } /** @@ -70,13 +80,15 @@ public HueStateColorBulb(HSBType hsb) { public HSBType toHSBType() { int bri = this.bri * 100 / MAX_BRI; int sat = this.sat * 100 / MAX_SAT; - int hue = this.sat * 360 / MAX_HUE; + int hue = this.hue * 360 / MAX_HUE; if (!this.on) { - return new HSBType(new DecimalType(hue), new PercentType(sat), new PercentType(0)); - } else { - return new HSBType(new DecimalType(hue), new PercentType(sat), new PercentType(bri)); + bri = 0; + } + if (colormode == ColorMode.ct) { + sat = 0; } + return new HSBType(new DecimalType(hue), new PercentType(sat), new PercentType(bri)); } @Override @@ -86,7 +98,8 @@ public String toString() { xyString += d + " "; } xyString += "}"; - return "[on: " + on + " bri: " + bri + " hue: " + hue + " sat: " + sat + " xy: " + xyString + " ct: " + ct - + " alert: " + alert + " effect: " + effect + " colormode: " + colormode + " reachable: " + reachable; + return "on: " + on + ", brightness: " + bri + ", hue: " + hue + ", sat: " + sat + ", xy: " + xyString + ", ct: " + + ct + ", alert: " + alert + ", effect: " + effect + ", colormode: " + colormode + ", reachable: " + + reachable; } } diff --git a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueStatePlug.java b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueStatePlug.java index 042b74576c3a..3b3c912c6cb7 100644 --- a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueStatePlug.java +++ b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueStatePlug.java @@ -26,6 +26,6 @@ public HueStatePlug(boolean on) { @Override public String toString() { - return "[on: " + on + " reachable: " + reachable; + return "on: " + on + ", reachable: " + reachable; } } diff --git a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueUserAuth.java b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueUserAuth.java new file mode 100644 index 000000000000..893258d0a0f1 --- /dev/null +++ b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/HueUserAuth.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.io.hueemulation.internal.dto; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Hue user object. Used by {@link HueAuthorizedConfig}. + * + * @author David Graeff - Initial contribution + */ +public class HueUserAuth { + public String name = ""; + public String createDate = ""; + public String lastUseDate = ""; + + /** + * For de-serialization. + */ + public HueUserAuth() { + } + + /** + * Create a new user + * + * @param name Visible name + */ + public HueUserAuth(String name) { + this.name = name; + this.createDate = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } +} \ No newline at end of file diff --git a/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/response/HueSuccessCreateGroup.java b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/response/HueSuccessCreateGroup.java new file mode 100644 index 000000000000..a8657b6bf15a --- /dev/null +++ b/addons/io/org.openhab.io.hueemulation/src/main/java/org/openhab/io/hueemulation/internal/dto/response/HueSuccessCreateGroup.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2018 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.io.hueemulation.internal.dto.response; + +/** + * This object describes the right hand side of "success". + * The response looks like this: + * + *
+ * {
+ *   "success":{
+ *      "id": "-the-id-"
+ *   }
+ * }
+ * 
+ * + * @author David Graeff - Initial contribution + */ +public class HueSuccessCreateGroup implements HueSuccessResponse { + public int id; + + public HueSuccessCreateGroup(int id) { + this.id = id; + } +}