From 95c342b5484e71c9430973d9d2267f4e00eb0d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=B6rderreuther?= Date: Sun, 30 Jul 2017 16:28:01 +0200 Subject: [PATCH] Amazon Echo support for groups So the groups are now somewhat usable with an Amazon Echo. This is a bit of a workaround: The group gets presented to the Echo as another fake light. For that to work you must manually enable this feature for every room by adding "exposeAsLight":"192.168.0.30" to the room in group.db (restart ha-bridge afterwards). Use the ip-address of your echo. No need to do that for other devices, because these can handle rooms directly. The fake light for the group will only be shown/usable to the specified ip-addresses. --- .../HABridge/api/hue/DeviceResponse.java | 21 +++++ .../HABridge/api/hue/GroupResponse.java | 3 + .../HABridge/dao/DeviceRepository.java | 6 +- .../HABridge/dao/GroupDescriptor.java | 17 +++- .../HABridge/dao/GroupRepository.java | 11 +++ .../bwssystems/HABridge/hue/HueMulator.java | 82 ++++++++++++++++--- 6 files changed, 123 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/bwssystems/HABridge/api/hue/DeviceResponse.java b/src/main/java/com/bwssystems/HABridge/api/hue/DeviceResponse.java index c1d59f1..99c61bd 100644 --- a/src/main/java/com/bwssystems/HABridge/api/hue/DeviceResponse.java +++ b/src/main/java/com/bwssystems/HABridge/api/hue/DeviceResponse.java @@ -1,6 +1,7 @@ package com.bwssystems.HABridge.api.hue; import com.bwssystems.HABridge.dao.DeviceDescriptor; +import com.bwssystems.HABridge.dao.GroupDescriptor; /** * Created by arm on 4/14/15. @@ -129,4 +130,24 @@ public class DeviceResponse { return response; } + + public static DeviceResponse createResponseForVirtualLight(GroupDescriptor group){ + DeviceResponse response = new DeviceResponse(); + response.setState(group.getAction()); + + response.setName(group.getName()); + response.setUniqueid("00:17:88:5E:D3:FF-" + String.format("%02X", Integer.parseInt(group.getId()))); + response.setManufacturername("Philips"); + response.setType("Extended color light"); + response.setModelid("LCT010"); + response.setSwversion("1.15.2_r19181"); + response.setSwconfigid("F921C859"); + response.setProductid("Philips-LCT010-1-A19ECLv4"); + + response.setLuminaireuniqueid(null); + + return response; + } + + } diff --git a/src/main/java/com/bwssystems/HABridge/api/hue/GroupResponse.java b/src/main/java/com/bwssystems/HABridge/api/hue/GroupResponse.java index 46092c9..573f517 100644 --- a/src/main/java/com/bwssystems/HABridge/api/hue/GroupResponse.java +++ b/src/main/java/com/bwssystems/HABridge/api/hue/GroupResponse.java @@ -70,6 +70,9 @@ public class GroupResponse { Boolean any_on = false; int i = 0; for (Map.Entry device : deviceList.entrySet()) { + if (Integer.parseInt(device.getKey()) >= 10000) { // don't show fake lights for other groups + continue; + } theList[i] = device.getKey(); Boolean is_on = device.getValue().getState().isOn(); if (is_on) diff --git a/src/main/java/com/bwssystems/HABridge/dao/DeviceRepository.java b/src/main/java/com/bwssystems/HABridge/dao/DeviceRepository.java index f048300..6fcc599 100644 --- a/src/main/java/com/bwssystems/HABridge/dao/DeviceRepository.java +++ b/src/main/java/com/bwssystems/HABridge/dao/DeviceRepository.java @@ -124,10 +124,14 @@ public class DeviceRepository extends BackupHandler { } public Map findAllByGroupWithState(String[] lightsInGroup, String anAddress, HueHome myHueHome, Gson aGsonBuilder) { + return findAllByGroupWithState(lightsInGroup, anAddress, myHueHome, aGsonBuilder, false); + } + + public Map findAllByGroupWithState(String[] lightsInGroup, String anAddress, HueHome myHueHome, Gson aGsonBuilder, boolean ignoreAddress) { Map deviceResponseMap = new HashMap(); Map lights = new HashMap(devices); lights.keySet().retainAll(Arrays.asList(lightsInGroup)); - for (DeviceDescriptor light : findAllByRequester(anAddress, lights.values())) { + for (DeviceDescriptor light : (ignoreAddress ? lights.values() : findAllByRequester(anAddress, lights.values()))) { DeviceResponse deviceResponse = null; if(!light.isInactive()) { if (light.containsType(DeviceMapTypes.HUE_DEVICE[DeviceMapTypes.typeIndex])) { diff --git a/src/main/java/com/bwssystems/HABridge/dao/GroupDescriptor.java b/src/main/java/com/bwssystems/HABridge/dao/GroupDescriptor.java index 7977a10..2c134e5 100644 --- a/src/main/java/com/bwssystems/HABridge/dao/GroupDescriptor.java +++ b/src/main/java/com/bwssystems/HABridge/dao/GroupDescriptor.java @@ -34,15 +34,16 @@ public class GroupDescriptor{ @SerializedName("comments") @Expose private String comments; - @SerializedName("action") - @Expose + private DeviceState action; - @SerializedName("groupState") - @Expose private GroupState groupState; + @SerializedName("lights") @Expose private String[] lights; + @SerializedName("exposeAsLight") + @Expose + private String exposeAsLight; public String getName() { @@ -136,4 +137,12 @@ public class GroupDescriptor{ public void setLights(String[] lights) { this.lights = lights; } + + public void setExposeAsLight(String exposeFor) { + this.exposeAsLight = exposeFor; + } + + public String getExposeAsLight() { + return exposeAsLight; + } } \ No newline at end of file diff --git a/src/main/java/com/bwssystems/HABridge/dao/GroupRepository.java b/src/main/java/com/bwssystems/HABridge/dao/GroupRepository.java index b9be496..1bba331 100644 --- a/src/main/java/com/bwssystems/HABridge/dao/GroupRepository.java +++ b/src/main/java/com/bwssystems/HABridge/dao/GroupRepository.java @@ -115,6 +115,17 @@ public class GroupRepository extends BackupHandler { return theReturnList; } + public List findVirtualLights(String anAddress) { + List list = new ArrayList(); + for (GroupDescriptor group : groups.values()) { + String expose = group.getExposeAsLight(); + if (expose != null && expose.contains(anAddress)) { + list.add(group); + } + } + return list; + } + public GroupDescriptor findOne(String id) { return groups.get(id); } diff --git a/src/main/java/com/bwssystems/HABridge/hue/HueMulator.java b/src/main/java/com/bwssystems/HABridge/hue/HueMulator.java index c7486cb..1633fc0 100644 --- a/src/main/java/com/bwssystems/HABridge/hue/HueMulator.java +++ b/src/main/java/com/bwssystems/HABridge/hue/HueMulator.java @@ -151,7 +151,7 @@ public class HueMulator { response.header("Access-Control-Allow-Origin", request.headers("Origin")); response.type("application/json"); response.status(HttpStatus.SC_OK); - return changeGroupState(request.params(":userid"), request.params(":groupid"), request.body(), request.ip()); + return changeGroupState(request.params(":userid"), request.params(":groupid"), request.body(), request.ip(), false); }); // http://ip_address:port/api/{userId}/scenes returns json objects of // all scenes configured @@ -440,7 +440,7 @@ public class HueMulator { response.header("Access-Control-Allow-Origin", request.headers("Origin")); response.type("application/json"); response.status(HttpStatus.SC_OK); - return changeState(request.params(":userid"), request.params(":id"), request.body(), request.ip()); + return changeState(request.params(":userid"), request.params(":id"), request.body(), request.ip(), false); }); } @@ -890,6 +890,13 @@ public class HueMulator { deviceResponseMap.put(device.getId(), deviceResponse); } } + + // handle groups which shall be exposed as fake lights to selected devices like amazon echos + List groups = groupRepository.findVirtualLights(requestIp); + for (GroupDescriptor group : groups) { + deviceResponseMap.put(String.valueOf(Integer.parseInt(group.getId()) + 10000), + DeviceResponse.createResponseForVirtualLight(group)); + } } if (theErrors != null) @@ -991,6 +998,11 @@ public class HueMulator { if (theErrors != null) return theErrors; + if (Integer.parseInt(lightId) >= 10000) { + GroupDescriptor group = groupRepository.findOne(String.valueOf(Integer.parseInt(lightId) - 10000)); + return DeviceResponse.createResponseForVirtualLight(group); + } + DeviceDescriptor device = repository.findOne(lightId); if (device == null) { // response.status(HttpStatus.SC_NOT_FOUND); @@ -1069,7 +1081,10 @@ public class HueMulator { return responseString; } - private String changeState(String userId, String lightId, String body, String ipAddress) { + private String changeState(String userId, String lightId, String body, String ipAddress, boolean ignoreRequester) { + if (Integer.parseInt(lightId) >= 10000) { + return changeGroupState(userId, String.valueOf(Integer.parseInt(lightId) - 10000), body, ipAddress, true); + } String responseString = null; String url = null; StateChangeBody theStateChanges = null; @@ -1161,10 +1176,13 @@ public class HueMulator { } for (int i = 0; callItems != null && i < callItems.length; i++) { - if(!filterByRequester(device.getRequesterAddress(), ipAddress) || !filterByRequester(callItems[i].getFilterIPs(), ipAddress)) { - log.warn("filter for requester address not present in: (device)" + device.getRequesterAddress() + " OR then (item)" + callItems[i].getFilterIPs() + " with request ip of: " + ipAddress); - continue; + if (!ignoreRequester) { + if(!filterByRequester(device.getRequesterAddress(), ipAddress) || !filterByRequester(callItems[i].getFilterIPs(), ipAddress)) { + log.warn("filter for requester address not present in: (device)" + device.getRequesterAddress() + " OR then (item)" + callItems[i].getFilterIPs() + " with request ip of: " + ipAddress); + continue; + } } + if (callItems[i].getCount() != null && callItems[i].getCount() > 0) aMultiUtil.setSetCount(callItems[i].getCount()); else @@ -1239,7 +1257,7 @@ public class HueMulator { } - private Object changeGroupState(String userId, String groupId, String body, String ipAddress) { + private String changeGroupState(String userId, String groupId, String body, String ipAddress, boolean fakeLightResponse) { log.debug("PUT action to group " + groupId + " from " + ipAddress + " user " + userId + " with body " + body); HueError[] theErrors = null; theErrors = bridgeSettingMaster.getBridgeSecurity().validateWhitelistUser(userId, null, bridgeSettingMaster.getBridgeSecurity().isUseLinkButton()); @@ -1247,16 +1265,24 @@ public class HueMulator { if(bridgeSettingMaster.getBridgeSecurity().isSettingsChanged()) bridgeSettingMaster.updateConfigFile(); + GroupDescriptor group = null; + Integer targetBriInc = null; + Integer targetBri = null; + DeviceState state = null; Map lights = null; if (groupId.equalsIgnoreCase("0")) { lights = (Map)lightsListHandler(userId, ipAddress); } else { - GroupDescriptor group = groupRepository.findOne(groupId); + group = groupRepository.findOne(groupId); if (group == null || group.isInactive()) { return aGsonHandler.toJson(HueErrorResponse.createResponse("3", "/groups/" + groupId, "resource, /groups/" + groupId + ", not available", null, null, null).getTheErrors(), HueError[].class); } else { - lights = repository.findAllByGroupWithState(group.getLights(), ipAddress, myHueHome, aGsonHandler); + if (fakeLightResponse) { + lights = repository.findAllByGroupWithState(group.getLights(), ipAddress, myHueHome, aGsonHandler, true); + } else { + lights = repository.findAllByGroupWithState(group.getLights(), ipAddress, myHueHome, aGsonHandler); + } } } @@ -1272,6 +1298,23 @@ public class HueMulator { return aGsonHandler.toJson(HueErrorResponse.createResponse("2", "/groups/" + groupId + "/action", "Could not parse state change body.", null, null, null).getTheErrors(), HueError[].class); } + + if (group != null) { + if (body.contains("\"bri_inc\"")) { + targetBriInc = new Integer(theStateChanges.getBri_inc()); + } + else if (body.contains("\"bri\"")) { + targetBri = new Integer(theStateChanges.getBri()); + } + + state = group.getAction(); + if (state == null) { + state = DeviceState.createDeviceState(); + group.setAction(state); + } + } + + boolean turnOn = false; boolean turnOff = false; if (!(body.contains("\"bri_inc\"") || body.contains("\"bri\""))) { @@ -1284,23 +1327,38 @@ public class HueMulator { } } for (Map.Entry light : lights.entrySet()) { + log.debug("Processing light" + light.getKey() + ": " + turnOn + " " + turnOff + " " + light.getValue().getState().isOn()); // ignore on/off for devices that are already on/off if (turnOff && !light.getValue().getState().isOn()) continue; if (turnOn && light.getValue().getState().isOn()) continue; - changeState(userId, light.getKey(), body, ipAddress); + changeState(userId, light.getKey(), body, ipAddress, fakeLightResponse); } // construct success response: one success message per changed property, but not per light + if (group != null) { // if not group 0 + String response = formatSuccessHueResponse(theStateChanges, body, String.valueOf(Integer.parseInt(groupId) + 10000), + state, targetBri, targetBriInc, true); + group.setAction(state); + if (fakeLightResponse) { + return response; + } + } + String successString = "["; for (String pairStr : body.replaceAll("[{|}]", "").split(",\\s*\"")) { String[] pair = pairStr.split(":"); - successString += "{\"success\":{ \"address\": \"/groups/" + groupId + "/action/" + pair[0].replaceAll("\"", "").trim() + "\", \"value\": " + pair[1].trim() + "}},"; + if (fakeLightResponse) { + successString += "{\"success\":{ \"/lights/" + String.valueOf(Integer.parseInt(groupId) + 10000) + "/state/" + pair[0].replaceAll("\"", "").trim() + "\": " + pair[1].trim() + "}},"; + } else { + successString += "{\"success\":{ \"address\": \"/groups/" + groupId + "/action/" + pair[0].replaceAll("\"", "").trim() + "\", \"value\": " + pair[1].trim() + "}},"; + } + } return (successString.length() == 1) ? "[]" : successString.substring(0, successString.length()-1) + "]"; } } - return theErrors; + return aGsonHandler.toJson(theErrors); } }