Updated the Hue API v 1.11 and tested.

Testing the upnp changes, ongoing.
This commit is contained in:
Admin
2016-06-03 16:25:00 -05:00
parent 39782fa339
commit 6c15ca2c3b
3 changed files with 166 additions and 81 deletions

10
pom.xml
View File

@@ -91,16 +91,6 @@
<artifactId>gson</artifactId> <artifactId>gson</artifactId>
<version>2.6.2</version> <version>2.6.2</version>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>com.cedarsoftware</groupId>
<artifactId>json-io</artifactId>
<version>4.1.6</version>
</dependency>
<dependency> <dependency>
<groupId>net.java.dev.eval</groupId> <groupId>net.java.dev.eval</groupId>
<artifactId>eval</artifactId> <artifactId>eval</artifactId>

View File

@@ -21,8 +21,6 @@ import com.bwssystems.hue.HueHome;
import com.bwssystems.hue.HueUtil; import com.bwssystems.hue.HueUtil;
import com.bwssystems.nest.controller.Nest; import com.bwssystems.nest.controller.Nest;
import com.bwssystems.util.JsonTransformer; import com.bwssystems.util.JsonTransformer;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson; import com.google.gson.Gson;
import net.java.dev.eval.Expression; import net.java.dev.eval.Expression;
@@ -90,7 +88,6 @@ public class HueMulator implements HueErrorStringSet {
private SSLContext sslcontext; private SSLContext sslcontext;
private SSLConnectionSocketFactory sslsf; private SSLConnectionSocketFactory sslsf;
private RequestConfig globalConfig; private RequestConfig globalConfig;
private ObjectMapper mapper;
private BridgeSettingsDescriptor bridgeSettings; private BridgeSettingsDescriptor bridgeSettings;
private byte[] sendData; private byte[] sendData;
private String hueUser; private String hueUser;
@@ -115,8 +112,6 @@ public class HueMulator implements HueErrorStringSet {
.setDefaultRequestConfig(globalConfig) .setDefaultRequestConfig(globalConfig)
.build(); .build();
mapper = new ObjectMapper(); //armzilla: work around Echo incorrect content type and breaking mapping. Map manually
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
repository = aDeviceRepository; repository = aDeviceRepository;
if(theBridgeSettings.isValidHarmony()) if(theBridgeSettings.isValidHarmony())
this.myHarmonyHome = theHarmonyHome; this.myHarmonyHome = theHarmonyHome;
@@ -317,30 +312,59 @@ public class HueMulator implements HueErrorStringSet {
String userId = request.params(":userid"); String userId = request.params(":userid");
String lightId = request.params(":id"); String lightId = request.params(":id");
String responseString = null; String responseString = null;
StateChangeBody theStateChanges = null;
DeviceState state = null; DeviceState state = null;
boolean stateHasOn = false; boolean stateHasBri = false;
boolean stateHasBriInc = false;
log.debug("Update state requested: " + userId + " from " + request.ip() + " body: " + request.body()); log.debug("Update state requested: " + userId + " from " + request.ip() + " body: " + request.body());
response.header("Access-Control-Allow-Origin", request.headers("Origin")); response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.type("application/json; charset=utf-8"); response.type("application/json; charset=utf-8");
response.status(HttpStatus.SC_OK); response.status(HttpStatus.SC_OK);
try { theStateChanges = new Gson().fromJson(request.body(), StateChangeBody.class);
state = mapper.readValue(request.body(), DeviceState.class); if (theStateChanges == null) {
if(request.body().contains("\"on\"")) log.warn("Could not parse state change body. Light state not changed.");
stateHasOn = true; responseString = "[{\"error\":{\"type\": 2, \"address\": \"/lights/" + lightId
} catch (IOException e) { + "\",\"description\": \"Could not parse state change body.\"}}]";
log.warn("Object mapper barfed on input of body.", e); return responseString;
responseString = "[{\"error\":{\"type\": 2, \"address\": \"/lights/" + lightId + "\",\"description\": \"Object mapper barfed on input of body.\"}}]"; }
return responseString;
} if (request.body().contains("\"bri\""))
stateHasBri = true;
if (request.body().contains("\"bri_inc\""))
stateHasBriInc = true;
DeviceDescriptor device = repository.findOne(lightId); DeviceDescriptor device = repository.findOne(lightId);
if (device == null) { if (device == null) {
log.warn("Could not find device: " + lightId + " for hue state change request: " + userId + " from " + request.ip() + " body: " + request.body()); log.warn("Could not find device: " + lightId + " for hue state change request: " + userId + " from " + request.ip() + " body: " + request.body());
responseString = "[{\"error\":{\"type\": 3, \"address\": \"/lights/" + lightId + "\",\"description\": \"Could not find device\", \"resource\": \"/lights/" + lightId + "\"}}]"; responseString = "[{\"error\":{\"type\": 3, \"address\": \"/lights/" + lightId + "\",\"description\": \"Could not find device\", \"resource\": \"/lights/" + lightId + "\"}}]";
return responseString; return responseString;
} }
state = device.getDeviceState();
responseString = this.formatSuccessHueResponse(state, request.body(), stateHasOn, lightId); if(state == null)
device.setDeviceState(state); state = DeviceState.createDeviceState();
state.fillIn();
if(stateHasBri)
{
if(theStateChanges.getBri() > 0 && !state.isOn())
state.setOn(true);
}
else if(stateHasBriInc) {
if((state.getBri() + theStateChanges.getBri_inc()) > 0 && !state.isOn())
state.setOn(true);
else if((state.getBri() + theStateChanges.getBri_inc()) <= 0 && state.isOn())
state.setOn(false);
}
else
{
if (theStateChanges.isOn()) {
if(state.getBri() <= 0)
state.setBri(255);
} else {
state.setBri(0);
}
}
device.getDeviceState().setBri(calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc));
responseString = this.formatSuccessHueResponse(theStateChanges, request.body(), lightId);
return responseString; return responseString;
}); });
@@ -369,7 +393,6 @@ public class HueMulator implements HueErrorStringSet {
DeviceState state = null; DeviceState state = null;
boolean stateHasBri = false; boolean stateHasBri = false;
boolean stateHasBriInc = false; boolean stateHasBriInc = false;
boolean stateHasOn = false;
log.debug("hue state change requested: " + userId + " from " + request.ip() + " body: " + request.body()); log.debug("hue state change requested: " + userId + " from " + request.ip() + " body: " + request.body());
response.header("Access-Control-Allow-Origin", request.headers("Origin")); response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.type("application/json; charset=utf-8"); response.type("application/json; charset=utf-8");
@@ -387,8 +410,6 @@ public class HueMulator implements HueErrorStringSet {
stateHasBri = true; stateHasBri = true;
if (request.body().contains("\"bri_inc\"")) if (request.body().contains("\"bri_inc\""))
stateHasBriInc = true; stateHasBriInc = true;
if (request.body().contains("\"on\""))
stateHasOn = true;
DeviceDescriptor device = repository.findOne(lightId); DeviceDescriptor device = repository.findOne(lightId);
if (device == null) { if (device == null) {
@@ -403,7 +424,6 @@ public class HueMulator implements HueErrorStringSet {
state.fillIn(); state.fillIn();
theHeaders = new Gson().fromJson(device.getHeaders(), NameValue[].class); theHeaders = new Gson().fromJson(device.getHeaders(), NameValue[].class);
responseString = this.formatSuccessHueResponse(state, request.body(), stateHasOn, lightId);
if((device.getMapType() != null && device.getMapType().equalsIgnoreCase("hueDevice"))) if((device.getMapType() != null && device.getMapType().equalsIgnoreCase("hueDevice")))
{ {
@@ -432,18 +452,21 @@ public class HueMulator implements HueErrorStringSet {
} }
myHueHome.setTheHUERegisteredUser(hueUser); myHueHome.setTheHUERegisteredUser(hueUser);
} }
else if(!responseString.contains("[{\"error\":"))
device.setDeviceState(state);
} }
else else
responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + "\",\"description\": \"No HUE configured\", \"parameter\": \"/lights/" + lightId + "state\"}}]"; responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + "\",\"description\": \"No HUE configured\", \"parameter\": \"/lights/" + lightId + "state\"}}]";
if(responseString == null || !responseString.contains("[{\"error\":")) {
state.setBri(calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc));
device.setDeviceState(state);
responseString = this.formatSuccessHueResponse(theStateChanges, request.body(), lightId);
}
return responseString; return responseString;
} }
if(stateHasBri) if(stateHasBri)
{ {
if(theStateChanges.getBri() > 0 && !theStateChanges.isOn()) if(theStateChanges.getBri() > 0 && !state.isOn())
state.setOn(true); state.setOn(true);
url = device.getDimUrl(); url = device.getDimUrl();
@@ -452,16 +475,26 @@ public class HueMulator implements HueErrorStringSet {
url = device.getOnUrl(); url = device.getOnUrl();
} }
else if(stateHasBriInc) { else if(stateHasBriInc) {
if((state.getBri() + theStateChanges.getBri_inc()) > 0 && !state.isOn())
state.setOn(true);
else if((state.getBri() + theStateChanges.getBri_inc()) <= 0 && state.isOn())
state.setOn(false);
url = device.getDimUrl();
if(url == null || url.length() == 0)
url = device.getOnUrl();
} }
else else
{ {
if (theStateChanges.isOn()) { if (theStateChanges.isOn()) {
url = device.getOnUrl(); url = device.getOnUrl();
state.setOn(true);
if(state.getBri() <= 0) if(state.getBri() <= 0)
state.setBri(255); state.setBri(255);
} else { } else {
url = device.getOffUrl(); url = device.getOffUrl();
state.setOn(false);
state.setBri(0); state.setBri(0);
} }
} }
@@ -549,9 +582,9 @@ public class HueMulator implements HueErrorStringSet {
if(thermoSetting.getControl().equalsIgnoreCase("temp")) { if(thermoSetting.getControl().equalsIgnoreCase("temp")) {
if(request.body().contains("bri")) { if(request.body().contains("bri")) {
if(bridgeSettings.isFarenheit()) if(bridgeSettings.isFarenheit())
thermoSetting.setTemp(String.valueOf((Double.parseDouble(replaceIntensityValue(thermoSetting.getTemp(), theStateChanges.getBri(), false)) - 32.0)/1.8)); thermoSetting.setTemp(String.valueOf((Double.parseDouble(replaceIntensityValue(thermoSetting.getTemp(), calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), false)) - 32.0)/1.8));
else else
thermoSetting.setTemp(String.valueOf(Double.parseDouble(replaceIntensityValue(thermoSetting.getTemp(), theStateChanges.getBri(), false)))); thermoSetting.setTemp(String.valueOf(Double.parseDouble(replaceIntensityValue(thermoSetting.getTemp(), calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), false))));
log.debug("Setting thermostat: " + thermoSetting.getName() + " to " + thermoSetting.getTemp() + "C"); log.debug("Setting thermostat: " + thermoSetting.getName() + " to " + thermoSetting.getTemp() + "C");
theNest.getThermostat(thermoSetting.getName()).setTargetTemperature(Float.parseFloat(thermoSetting.getTemp())); theNest.getThermostat(thermoSetting.getName()).setTargetTemperature(Float.parseFloat(thermoSetting.getTemp()));
} }
@@ -588,7 +621,7 @@ public class HueMulator implements HueErrorStringSet {
intermediate = callItems[i].getItem().substring(callItems[i].getItem().indexOf("://") + 3); intermediate = callItems[i].getItem().substring(callItems[i].getItem().indexOf("://") + 3);
else else
intermediate = callItems[i].getItem(); intermediate = callItems[i].getItem();
String anError = doExecRequest(intermediate, theStateChanges, lightId); String anError = doExecRequest(intermediate, calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), lightId);
if(anError != null) { if(anError != null) {
responseString = anError; responseString = anError;
i = callItems.length+1; i = callItems.length+1;
@@ -624,11 +657,11 @@ public class HueMulator implements HueErrorStringSet {
hostAddr = hostPortion; hostAddr = hostPortion;
InetAddress IPAddress = InetAddress.getByName(hostAddr);; InetAddress IPAddress = InetAddress.getByName(hostAddr);;
if(theUrlBody.startsWith("0x")) { if(theUrlBody.startsWith("0x")) {
theUrlBody = replaceIntensityValue(theUrlBody, theStateChanges.getBri(), true); theUrlBody = replaceIntensityValue(theUrlBody, calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), true);
sendData = DatatypeConverter.parseHexBinary(theUrlBody.substring(2)); sendData = DatatypeConverter.parseHexBinary(theUrlBody.substring(2));
} }
else { else {
theUrlBody = replaceIntensityValue(theUrlBody, theStateChanges.getBri(), false); theUrlBody = replaceIntensityValue(theUrlBody, calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), false);
sendData = theUrlBody.getBytes(); sendData = theUrlBody.getBytes();
} }
if(callItems[i].getItem().contains("udp://")) { if(callItems[i].getItem().contains("udp://")) {
@@ -650,7 +683,7 @@ public class HueMulator implements HueErrorStringSet {
} }
else if(callItems[i].getItem().contains("exec://")) { else if(callItems[i].getItem().contains("exec://")) {
String intermediate = callItems[i].getItem().substring(callItems[i].getItem().indexOf("://") + 3); String intermediate = callItems[i].getItem().substring(callItems[i].getItem().indexOf("://") + 3);
String anError = doExecRequest(intermediate, theStateChanges, lightId); String anError = doExecRequest(intermediate, calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), lightId);
if(anError != null) { if(anError != null) {
responseString = anError; responseString = anError;
i = callItems.length+1; i = callItems.length+1;
@@ -659,12 +692,12 @@ public class HueMulator implements HueErrorStringSet {
else { else {
log.debug("executing HUE api request to Http " + (device.getHttpVerb() == null?"GET":device.getHttpVerb()) + ": " + callItems[i].getItem()); log.debug("executing HUE api request to Http " + (device.getHttpVerb() == null?"GET":device.getHttpVerb()) + ": " + callItems[i].getItem());
String anUrl = replaceIntensityValue(callItems[i].getItem(), theStateChanges.getBri(), false); String anUrl = replaceIntensityValue(callItems[i].getItem(), calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), false);
String body; String body;
if (theStateChanges.isOn()) if (state.isOn())
body = replaceIntensityValue(device.getContentBody(), theStateChanges.getBri(), false); body = replaceIntensityValue(device.getContentBody(), calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), false);
else else
body = replaceIntensityValue(device.getContentBodyOff(), theStateChanges.getBri(), false); body = replaceIntensityValue(device.getContentBodyOff(), calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), false);
// make call // make call
if (doHttpRequest(anUrl, device.getHttpVerb(), device.getContentType(), body, theHeaders) == null) { if (doHttpRequest(anUrl, device.getHttpVerb(), device.getContentType(), body, theHeaders) == null) {
log.warn("Error on calling url to change device state: " + anUrl); log.warn("Error on calling url to change device state: " + anUrl);
@@ -680,14 +713,31 @@ public class HueMulator implements HueErrorStringSet {
} }
} }
if(!responseString.contains("[{\"error\":")) { if(responseString == null || !responseString.contains("[{\"error\":")) {
// FIXME update state with theStateChanges state.setBri(calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc));
device.setDeviceState(state); device.setDeviceState(state);
responseString = this.formatSuccessHueResponse(theStateChanges, request.body(), lightId);
} }
return responseString; return responseString;
}); });
} }
private int calculateIntensity(DeviceState state, StateChangeBody theChanges, boolean hasBri, boolean hasBriInc) {
int setIntensity = state.getBri();
if(hasBri) {
setIntensity = theChanges.getBri();
}
else if(hasBriInc) {
if((setIntensity + theChanges.getBri_inc()) < 0)
setIntensity = 0;
else if((setIntensity + theChanges.getBri_inc()) > 255)
setIntensity = 255;
else
setIntensity = setIntensity + theChanges.getBri_inc();
}
return setIntensity;
}
/* light weight templating here, was going to use free marker but it was a bit too /* light weight templating here, was going to use free marker but it was a bit too
* heavy for what we were trying to do. * heavy for what we were trying to do.
* *
@@ -805,11 +855,11 @@ public class HueMulator implements HueErrorStringSet {
return theContent; return theContent;
} }
private String doExecRequest(String anItem, StateChangeBody state, String lightId) { private String doExecRequest(String anItem, int intensity, String lightId) {
log.debug("Executing request: " + anItem); log.debug("Executing request: " + anItem);
String responseString = null; String responseString = null;
try { try {
Process p = Runtime.getRuntime().exec(replaceIntensityValue(anItem, state.getBri(), false)); Process p = Runtime.getRuntime().exec(replaceIntensityValue(anItem, intensity, false));
log.debug("Process running: " + p.isAlive()); log.debug("Process running: " + p.isAlive());
} catch (IOException e) { } catch (IOException e) {
log.warn("Could not execute request: " + anItem, e); log.warn("Could not execute request: " + anItem, e);
@@ -818,78 +868,123 @@ public class HueMulator implements HueErrorStringSet {
return responseString; return responseString;
} }
private String formatSuccessHueResponse(DeviceState state, String body, boolean stateHasOn, String lightId) { private String formatSuccessHueResponse(StateChangeBody state, String body, String lightId) {
String responseString = "["; String responseString = "[";
boolean justState = false; boolean notFirstChange = false;
if(stateHasOn) if(body.contains("\"on\""))
{ {
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/on\":"; responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/on\":";
if (state.isOn()) { if (state.isOn()) {
responseString = responseString + "true}}"; responseString = responseString + "true}}";
if(state.getBri() <= 0)
state.setBri(255);
} else { } else {
responseString = responseString + "false}}"; responseString = responseString + "false}}";
state.setBri(0);
} }
justState = true; notFirstChange = true;
} }
if(body.contains("bri")) if(body.contains("\"bri\""))
{ {
if(justState) if(notFirstChange)
responseString = responseString + ","; responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/bri\":" + state.getBri() + "}}"; responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/bri\":" + state.getBri() + "}}";
justState = true; notFirstChange = true;
} }
if(body.contains("bri_inc")) if(body.contains("\"bri_inc\""))
{ {
if(justState) if(notFirstChange)
responseString = responseString + ","; responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/bri_inc\":" + state.getBri() + "}}"; responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/bri_inc\":" + state.getBri_inc() + "}}";
justState = true; notFirstChange = true;
} }
if(body.contains("ct")) if(body.contains("\"ct\""))
{ {
if(justState) if(notFirstChange)
responseString = responseString + ","; responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/ct\":" + state.getCt() + "}}"; responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/ct\":" + state.getCt() + "}}";
justState = true; notFirstChange = true;
} }
if(body.contains("xy")) if(body.contains("\"xy\""))
{ {
if(justState) if(notFirstChange)
responseString = responseString + ","; responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/xy\":" + state.getXy() + "}}"; responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/xy\":" + state.getXy() + "}}";
justState = true; notFirstChange = true;
} }
if(body.contains("hue")) if(body.contains("\"hue\""))
{ {
if(justState) if(notFirstChange)
responseString = responseString + ","; responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/hue\":" + state.getHue() + "}}"; responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/hue\":" + state.getHue() + "}}";
justState = true; notFirstChange = true;
} }
if(body.contains("sat")) if(body.contains("\"sat\""))
{ {
if(justState) if(notFirstChange)
responseString = responseString + ","; responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/sat\":" + state.getSat() + "}}"; responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/sat\":" + state.getSat() + "}}";
justState = true; notFirstChange = true;
} }
if(body.contains("colormode")) if(body.contains("\"ct_inc\""))
{ {
if(justState) if(notFirstChange)
responseString = responseString + ","; responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/colormode\":" + state.getColormode() + "}}"; responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/ct_inc\":" + state.getCt_inc() + "}}";
justState = true; notFirstChange = true;
}
if(body.contains("\"xy_inc\""))
{
if(notFirstChange)
responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/xy_inc\":" + state.getXy_inc() + "}}";
notFirstChange = true;
}
if(body.contains("\"hue_inc\""))
{
if(notFirstChange)
responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/hue_inc\":" + state.getHue_inc() + "}}";
notFirstChange = true;
}
if(body.contains("\"sat_inc\""))
{
if(notFirstChange)
responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/sat_inc\":" + state.getSat_inc() + "}}";
notFirstChange = true;
}
if(body.contains("\"effect\""))
{
if(notFirstChange)
responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/effect\":" + state.getEffect() + "}}";
notFirstChange = true;
}
if(body.contains("\"transitiontime\""))
{
if(notFirstChange)
responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/transitiontime\":" + state.getTransitiontime() + "}}";
notFirstChange = true;
}
if(body.contains("\"alert\""))
{
if(notFirstChange)
responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/alert\":" + state.getAlert() + "}}";
notFirstChange = true;
} }
responseString = responseString + "]"; responseString = responseString + "]";

View File

@@ -23,7 +23,7 @@ public class UpnpSettingsResource {
+ "<manufacturer>Royal Philips Electronics</manufacturer>\n" + "<manufacturer>Royal Philips Electronics</manufacturer>\n"
+ "<manufacturerURL>http://www.bwssystems.com</manufacturerURL>\n" + "<manufacturerURL>http://www.bwssystems.com</manufacturerURL>\n"
+ "<modelDescription>Hue Emulator for HA bridge</modelDescription>\n" + "<modelDescription>Hue Emulator for HA bridge</modelDescription>\n"
+ "<modelName>Philips hue bridge 2012</modelName>\n" + "<modelNumber>929000226503</modelNumber>\n" + "<modelName>Philips hue bridge 2015</modelName>\n" + "<modelNumber>BSB002</modelNumber>\n"
+ "<modelURL>http://www.bwssystems.com/apps.html</modelURL>\n" + "<modelURL>http://www.bwssystems.com/apps.html</modelURL>\n"
+ "<serialNumber>0017880ae670</serialNumber>\n" + "<serialNumber>0017880ae670</serialNumber>\n"
+ "<UDN>uuid:88f6698f-2c83-4393-bd03-cd54a9f8595</UDN>\n" + "<serviceList>\n" + "<service>\n" + "<UDN>uuid:88f6698f-2c83-4393-bd03-cd54a9f8595</UDN>\n" + "<serviceList>\n" + "<service>\n"