Implemented support for rooms

I implemented full api support for rooms. That means:
- Create/Modify/Delete rooms/lightgroups
- Get information about group list / individual group
- Group actions: Change lighting for the whole group (except setting scenes, because scenes are not implemented in ha-bridge right now)
For now the rooms/groups can only be configured through the api and apps, it's not visible/changeable through the web gui.
This commit is contained in:
Florian Förderreuther
2017-07-27 23:20:02 +02:00
parent 3a5262ff33
commit 20ad6891e8
14 changed files with 879 additions and 69 deletions

2
.gitignore vendored
View File

@@ -12,3 +12,5 @@ data
/.settings/
/start.bat
/.classpath
sftp-config\.json

View File

@@ -90,6 +90,7 @@ public class BridgeSettings extends BackupHandler {
theBridgeSettings.setServerPort(System.getProperty("server.port", Configuration.DEFAULT_WEB_PORT));
theBridgeSettings.setUpnpConfigAddress(System.getProperty("upnp.config.address"));
theBridgeSettings.setUpnpDeviceDb(System.getProperty("upnp.device.db"));
theBridgeSettings.setUpnpGroupDb(System.getProperty("upnp.group.db"));
theBridgeSettings.setUpnpResponsePort(System.getProperty("upnp.response.port", Configuration.UPNP_RESPONSE_PORT));
theVeraAddress = System.getProperty("vera.address");

View File

@@ -24,6 +24,9 @@ public class BridgeSettingsDescriptor {
@SerializedName("upnpdevicedb")
@Expose
private String upnpdevicedb;
@SerializedName("upnpgroupdb")
@Expose
private String upnpgroupdb;
@SerializedName("veraaddress")
@Expose
private IpList veraaddress;
@@ -163,6 +166,12 @@ public class BridgeSettingsDescriptor {
public void setUpnpDeviceDb(String upnpDeviceDb) {
this.upnpdevicedb = upnpDeviceDb;
}
public String getUpnpGroupDb() {
return upnpgroupdb;
}
public void setUpnpGroupDb(String upnpGroupDb) {
this.upnpgroupdb = upnpGroupDb;
}
public IpList getVeraAddress() {
return veraaddress;
}

View File

@@ -76,7 +76,7 @@ public class HABridge {
theSettingResponder = new UpnpSettingsResource(bridgeSettings.getBridgeSettingsDescriptor());
theSettingResponder.setupServer();
// setup the class to handle the hue emulator rest api
theHueMulator = new HueMulator(bridgeSettings, theResources.getDeviceRepository(), homeManager);
theHueMulator = new HueMulator(bridgeSettings, theResources.getDeviceRepository(), theResources.getGroupRepository(), homeManager);
theHueMulator.setupServer();
// wait for the sparkjava initialization of the rest api classes to be complete
awaitInitialization();

View File

@@ -0,0 +1,61 @@
package com.bwssystems.HABridge.api.hue;
import java.util.ArrayList;
public class GroupClassTypes {
public final static String BATHROOM = "Bathroom";
public final static String BEDROOM = "Bedroom";
public final static String CARPORT = "Carport";
public final static String DINING = "Dining";
public final static String DRIVEWAY = "Driveway";
public final static String FRONT_DOOR = "Front door";
public final static String GARAGE = "Garage";
public final static String GARDEN = "Garden";
public final static String GYM = "Gym";
public final static String HALLWAY = "Hallway";
public final static String BEDROOM_KIDS = "Kids bedroom";
public final static String KITCHEN = "Kitchen";
public final static String LIVING_ROOM = "Living room";
public final static String NURSERY = "Nursery";
public final static String OFFICE = "Office";
public final static String OTHER = "Other";
public final static String RECREATION = "Recreation";
public final static String TERRACE = "Terrace";
public final static String TOILET = "Toilet";
ArrayList<String> groupClassTypes;
public GroupClassTypes() {
groupClassTypes = new ArrayList<String>();
groupClassTypes.add(BATHROOM);
groupClassTypes.add(BEDROOM);
groupClassTypes.add(CARPORT);
groupClassTypes.add(DINING);
groupClassTypes.add(DRIVEWAY);
groupClassTypes.add(FRONT_DOOR);
groupClassTypes.add(GARAGE);
groupClassTypes.add(GARDEN);
groupClassTypes.add(GYM);
groupClassTypes.add(HALLWAY);
groupClassTypes.add(BEDROOM_KIDS);
groupClassTypes.add(KITCHEN);
groupClassTypes.add(LIVING_ROOM);
groupClassTypes.add(NURSERY);
groupClassTypes.add(OFFICE);
groupClassTypes.add(OTHER);
groupClassTypes.add(RECREATION);
groupClassTypes.add(TERRACE);
groupClassTypes.add(TOILET);
}
public Boolean validateType(String type) {
if(type == null || type.trim().isEmpty())
return false;
for(String classType : groupClassTypes) {
if(type.trim().contentEquals(classType))
return true;
}
return false;
}
}

View File

@@ -1,8 +1,12 @@
package com.bwssystems.HABridge.api.hue;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.bwssystems.HABridge.dao.DeviceDescriptor;
import com.bwssystems.HABridge.dao.GroupDescriptor;
import com.google.gson.annotations.SerializedName;
public class GroupResponse {
@@ -16,6 +20,8 @@ public class GroupResponse {
private String type;
@SerializedName("class")
String class_name;
@SerializedName("state")
private GroupState state;
public DeviceState getAction() {
return action;
@@ -23,6 +29,14 @@ public class GroupResponse {
public void setAction(DeviceState action) {
this.action = action;
}
public GroupState getState() {
return state;
}
public void setState(GroupState state) {
this.state = state;
}
public String[] getLights() {
return lights;
}
@@ -48,34 +62,49 @@ public class GroupResponse {
public void setClass_name(String class_name) {
this.class_name = class_name;
}
public static GroupResponse createDefaultGroupResponse(List<DeviceDescriptor> deviceList) {
public static GroupResponse createDefaultGroupResponse(Map<String, DeviceResponse> deviceList) {
String[] theList = new String[deviceList.size()];
Boolean all_on = true;
Boolean any_on = false;
int i = 0;
for (DeviceDescriptor device : deviceList) {
theList[i] = device.getId();
for (Map.Entry<String, DeviceResponse> device : deviceList.entrySet()) {
theList[i] = device.getKey();
Boolean is_on = device.getValue().getState().isOn();
if (is_on)
any_on = true;
else
all_on = false;
i++;
}
GroupResponse theResponse = new GroupResponse();
theResponse.setAction(DeviceState.createDeviceState());
theResponse.setName("Lightset 0");
theResponse.setState(new GroupState(all_on, any_on));
theResponse.setName("Group 0");
theResponse.setLights(theList);
theResponse.setType("LightGroup");
return theResponse;
}
public static GroupResponse createOtherGroupResponse(List<DeviceDescriptor> deviceList) {
String[] theList = new String[deviceList.size()];
int i = 0;
for (DeviceDescriptor device : deviceList) {
theList[i] = device.getId();
i++;
}
GroupResponse theResponse = new GroupResponse();
theResponse.setAction(DeviceState.createDeviceState());
theResponse.setName("AGroup");
theResponse.setLights(theList);
theResponse.setType("Room");
theResponse.setClass_name("Other");
return theResponse;
public static GroupResponse createResponse(GroupDescriptor group, Map<String, DeviceResponse> lights){
GroupResponse response = new GroupResponse();
Boolean all_on = true;
Boolean any_on = false;
for (DeviceResponse light : lights.values()) {
Boolean is_on = light.getState().isOn();
if (is_on)
any_on = true;
else
all_on = false;
}
response.setState(new GroupState(all_on, any_on));
response.setAction(group.getAction());
response.setName(group.getName());
response.setType(group.getGroupType());
response.setLights(group.getLights());
response.setClass_name(group.getGroupClass());
return response;
}
}

View File

@@ -0,0 +1,38 @@
package com.bwssystems.HABridge.api.hue;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Florian Foerderreuther on 07/23/17
*/
public class GroupState {
private boolean all_on;
private boolean any_on;
public boolean isAllOn() {
return all_on;
}
public boolean isAnyOn() {
return any_on;
}
public void setState(boolean all_on, boolean any_on) {
this.all_on = all_on;
this.any_on = any_on;
}
public GroupState(boolean all_on, boolean any_on) {
this.all_on = all_on;
this.any_on = any_on;
}
@Override
public String toString() {
return "GroupState{" +
"all_on=" + all_on +
", any_on=" + any_on +
'}';
}
}

View File

@@ -18,13 +18,21 @@ import javax.xml.bind.DatatypeConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.bwssystems.HABridge.DeviceMapTypes;
import com.bwssystems.HABridge.api.CallItem;
import com.bwssystems.HABridge.api.hue.DeviceResponse;
import com.bwssystems.HABridge.dao.DeviceDescriptor;
import com.bwssystems.HABridge.plugins.hue.HueHome;
import com.bwssystems.HABridge.util.BackupHandler;
import com.bwssystems.HABridge.util.JsonTransformer;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import java.util.Collection;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
/*
* This is an in memory list to manage the configured devices and saves the list as a JSON string to a file for later
* loading.
@@ -86,6 +94,10 @@ public class DeviceRepository extends BackupHandler {
public List<DeviceDescriptor> findAllByRequester(String anAddress) {
List<DeviceDescriptor> list = new ArrayList<DeviceDescriptor>(devices.values());
return findAllByRequester(anAddress, list);
}
private List<DeviceDescriptor> findAllByRequester(String anAddress, Collection<DeviceDescriptor> list) {
List<DeviceDescriptor> theReturnList = new ArrayList<DeviceDescriptor>();
Iterator<DeviceDescriptor> anIterator = list.iterator();
DeviceDescriptor theDevice;
@@ -111,6 +123,41 @@ public class DeviceRepository extends BackupHandler {
return theReturnList;
}
public Map<String, DeviceResponse> findAllByGroupWithState(String[] lightsInGroup, String anAddress, HueHome myHueHome, Gson aGsonBuilder) {
Map<String, DeviceResponse> deviceResponseMap = new HashMap<String, DeviceResponse>();
Map<String, DeviceDescriptor> lights = new HashMap<String, DeviceDescriptor>(devices);
lights.keySet().retainAll(Arrays.asList(lightsInGroup));
for (DeviceDescriptor light : findAllByRequester(anAddress, lights.values())) {
DeviceResponse deviceResponse = null;
if(!light.isInactive()) {
if (light.containsType(DeviceMapTypes.HUE_DEVICE[DeviceMapTypes.typeIndex])) {
CallItem[] callItems = null;
try {
if(light.getOnUrl() != null)
callItems = aGsonBuilder.fromJson(light.getOnUrl(), CallItem[].class);
} catch(JsonSyntaxException e) {
log.warn("Could not decode Json for url items to get Hue state for device: " + light.getName());
callItems = null;
}
for (int i = 0; callItems != null && i < callItems.length; i++) {
if((callItems[i].getType() != null && callItems[i].getType().equals(DeviceMapTypes.HUE_DEVICE[DeviceMapTypes.typeIndex])) ||
(callItems[i].getItem() != null && callItems[i].getItem().getAsString() != null && callItems[i].getItem().getAsString().contains("hueName"))) {
deviceResponse = myHueHome.getHueDeviceInfo(callItems[i], light);
i = callItems.length;
}
}
}
if (deviceResponse == null) {
deviceResponse = DeviceResponse.createResponse(light);
}
deviceResponseMap.put(light.getId(), deviceResponse);
}
}
return (deviceResponseMap.size() == 0) ? null : deviceResponseMap;
}
public DeviceDescriptor findOne(String id) {
return devices.get(id);
}

View File

@@ -0,0 +1,139 @@
package com.bwssystems.HABridge.dao;
import com.bwssystems.HABridge.api.hue.DeviceState;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import com.bwssystems.HABridge.api.hue.GroupState;
import com.bwssystems.HABridge.dao.DeviceDescriptor;
/*
* Object to handle the device configuration
*/
public class GroupDescriptor{
@SerializedName("id")
@Expose
private String id;
@SerializedName("name")
@Expose
private String name;
@SerializedName("groupType")
@Expose
private String groupType;
@SerializedName("groupClass")
@Expose
private String groupClass;
@SerializedName("requesterAddress")
@Expose
private String requesterAddress;
@SerializedName("inactive")
@Expose
private boolean inactive;
@SerializedName("description")
@Expose
private String description;
@SerializedName("comments")
@Expose
private String comments;
@SerializedName("action")
@Expose
private DeviceState action;
@SerializedName("groupState")
@Expose
private GroupState groupState;
@SerializedName("lights")
@Expose
private String[] lights;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGroupType() {
return groupType;
}
public void setGroupType(String groupType) {
this.groupType = groupType;
}
public String getGroupClass() {
return groupClass;
}
public void setGroupClass(String groupClass) {
this.groupClass = groupClass;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public GroupState getGroupState() {
if(groupState == null)
groupState = new GroupState(false,false);
return groupState;
}
public void setGroupState(GroupState groupState) {
this.groupState = groupState;
}
public DeviceState getAction() {
if(action == null)
action = DeviceState.createDeviceState();
return action;
}
public void setAction(DeviceState action) {
this.action = action;
}
public boolean isInactive() {
return inactive;
}
public void setInactive(boolean inactive) {
this.inactive = inactive;
}
public String getRequesterAddress() {
return requesterAddress;
}
public void setRequesterAddress(String requesterAddress) {
this.requesterAddress = requesterAddress;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getComments() {
return comments;
}
public void setComments(String comments) {
this.comments = comments;
}
public String[] getLights() {
return lights;
}
public void setLights(String[] lights) {
this.lights = lights;
}
}

View File

@@ -0,0 +1,213 @@
package com.bwssystems.HABridge.dao;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.xml.bind.DatatypeConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.bwssystems.HABridge.dao.GroupDescriptor;
import com.bwssystems.HABridge.util.BackupHandler;
import com.bwssystems.HABridge.util.JsonTransformer;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.List;
/*
* This is an in memory list to manage the configured groups and saves the list as a JSON string to a file for later
* loading.
*/
public class GroupRepository extends BackupHandler {
private Map<String, GroupDescriptor> groups;
private Path repositoryPath;
private Gson gson;
private Integer nextId;
private Logger log = LoggerFactory.getLogger(GroupRepository.class);
public GroupRepository(String groupDb) {
super();
gson =
new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.create();
nextId = 0;
try {
repositoryPath = null;
log.info("loading group.db from " + groupDb);
repositoryPath = Paths.get(groupDb);
setupParams(repositoryPath, ".bk", "group.db-");
_loadRepository(repositoryPath);
} catch (Exception ex) {
groups = new HashMap<String, GroupDescriptor>();
}
}
public void loadRepository() {
if(repositoryPath != null)
_loadRepository(repositoryPath);
}
private void _loadRepository(Path aPath){
String jsonContent = repositoryReader(aPath);
groups = new HashMap<String, GroupDescriptor>();
if(jsonContent != null)
{
GroupDescriptor list[] = gson.fromJson(jsonContent, GroupDescriptor[].class);
for(int i = 0; i < list.length; i++) {
list[i].setGroupState(null);
put(list[i].getId(), list[i]);
if(Integer.decode(list[i].getId()) > nextId) {
nextId = Integer.decode(list[i].getId());
}
}
}
}
public List<GroupDescriptor> findAll() {
List<GroupDescriptor> list = new ArrayList<GroupDescriptor>(groups.values());
return list;
}
public List<GroupDescriptor> findActive() {
List<GroupDescriptor> list = new ArrayList<GroupDescriptor>();
for(GroupDescriptor aGroup : new ArrayList<GroupDescriptor>(groups.values())) {
if(!aGroup.isInactive())
list.add(aGroup);
}
return list;
}
public List<GroupDescriptor> findAllByRequester(String anAddress) {
List<GroupDescriptor> list = new ArrayList<GroupDescriptor>(groups.values());
List<GroupDescriptor> theReturnList = new ArrayList<GroupDescriptor>();
Iterator<GroupDescriptor> anIterator = list.iterator();
GroupDescriptor theGroup;
String theRequesterAddress;
HashMap<String,String > addressMap;
while (anIterator.hasNext()) {
theGroup = anIterator.next();
theRequesterAddress = theGroup.getRequesterAddress();
addressMap = new HashMap<String, String>();
if(theRequesterAddress != null) {
if (theRequesterAddress.contains(",")) {
String[] theArray = theRequesterAddress.split(",");
for (String v : theArray) {
addressMap.put(v.trim(), v.trim());
}
} else
addressMap.put(theRequesterAddress, theRequesterAddress);
}
if (theRequesterAddress == null || theRequesterAddress.length() == 0 || addressMap.containsKey(anAddress))
theReturnList.add(theGroup);
}
return theReturnList;
}
public GroupDescriptor findOne(String id) {
return groups.get(id);
}
private void put(String id, GroupDescriptor aDescriptor) {
groups.put(id, aDescriptor);
}
public void save() {
save(groups.values().toArray(new GroupDescriptor[0]));
}
public void save(GroupDescriptor[] descriptors) {
String theNames = "";
for(int i = 0; i < descriptors.length; i++) {
if(descriptors[i].getId() != null && descriptors[i].getId().length() > 0)
groups.remove(descriptors[i].getId());
else {
nextId++;
descriptors[i].setId(String.valueOf(nextId));
}
put(descriptors[i].getId(), descriptors[i]);
theNames = theNames + " " + descriptors[i].getName() + ", ";
}
String jsonValue = gson.toJson(findAll());
repositoryWriter(jsonValue, repositoryPath);
log.debug("Save group(s): " + theNames);
}
public Integer getNewId() {
return nextId + 1;
}
public String delete(GroupDescriptor aDescriptor) {
if (aDescriptor != null) {
groups.remove(aDescriptor.getId());
JsonTransformer aRenderer = new JsonTransformer();
String jsonValue = aRenderer.render(findAll());
repositoryWriter(jsonValue, repositoryPath);
return "Group with id '" + aDescriptor.getId() + "' deleted";
} else {
return "Group not found";
}
}
private void repositoryWriter(String content, Path filePath) {
if(Files.exists(filePath) && !Files.isWritable(filePath)){
log.error("Error file is not writable: " + filePath);
return;
}
if(Files.notExists(filePath.getParent())) {
try {
Files.createDirectories(filePath.getParent());
} catch (IOException e) {
log.error("Error creating the directory: " + filePath + " message: " + e.getMessage(), e);
}
}
try {
Path target = null;
if(Files.exists(filePath)) {
target = FileSystems.getDefault().getPath(filePath.getParent().toString(), "group.db.old");
Files.move(filePath, target);
}
Files.write(filePath, content.getBytes(), StandardOpenOption.CREATE);
if(target != null)
Files.delete(target);
} catch (IOException e) {
log.error("Error writing the file: " + filePath + " message: " + e.getMessage(), e);
}
}
private String repositoryReader(Path filePath) {
String content = null;
if(Files.notExists(filePath) || !Files.isReadable(filePath)){
log.warn("Error reading the file: " + filePath + " - Does not exist or is not readable. continuing...");
return null;
}
try {
content = new String(Files.readAllBytes(filePath));
} catch (IOException e) {
log.error("Error reading the file: " + filePath + " message: " + e.getMessage(), e);
}
return content;
}
}

View File

@@ -25,6 +25,8 @@ import com.bwssystems.HABridge.api.CallItem;
import com.bwssystems.HABridge.dao.BackupFilename;
import com.bwssystems.HABridge.dao.DeviceDescriptor;
import com.bwssystems.HABridge.dao.DeviceRepository;
import com.bwssystems.HABridge.dao.GroupDescriptor;
import com.bwssystems.HABridge.dao.GroupRepository;
import com.bwssystems.HABridge.dao.ErrorMessage;
import com.bwssystems.HABridge.util.JsonTransformer;
import com.google.gson.Gson;
@@ -38,6 +40,7 @@ public class DeviceResource {
private static final String API_CONTEXT = "/api/devices";
private static final Logger log = LoggerFactory.getLogger(DeviceResource.class);
private DeviceRepository deviceRepository;
private GroupRepository groupRepository;
private HomeManager homeManager;
private BridgeSettings bridgeSettings;
private Gson aGsonHandler;
@@ -46,6 +49,7 @@ public class DeviceResource {
public DeviceResource(BridgeSettings theSettings, HomeManager aHomeManager) {
bridgeSettings = theSettings;
this.deviceRepository = new DeviceRepository(bridgeSettings.getBridgeSettingsDescriptor().getUpnpDeviceDb());
this.groupRepository = new GroupRepository(bridgeSettings.getBridgeSettingsDescriptor().getUpnpGroupDb());
homeManager = aHomeManager;
aGsonHandler = new GsonBuilder().create();
setupEndpoints();
@@ -55,6 +59,10 @@ public class DeviceResource {
return deviceRepository;
}
public GroupRepository getGroupRepository() {
return groupRepository;
}
private void setupEndpoints() {
log.info("HABridge device management service started.... ");
before(API_CONTEXT + "/*", (request, response) -> {
@@ -122,6 +130,15 @@ public class DeviceResource {
log.debug(errorMessage);
return new ErrorMessage(errorMessage);
}
try {
if(devices[i].getColorUrl() != null && !devices[i].getColorUrl().isEmpty())
callItems = aGsonHandler.fromJson(devices[i].getColorUrl(), CallItem[].class);
} catch(JsonSyntaxException e) {
response.status(HttpStatus.SC_BAD_REQUEST);
errorMessage = "Bad color URL JSON in create device(s) for name: " + devices[i].getName() + " with color URL: " + devices[i].getColorUrl();
log.debug(errorMessage);
return new ErrorMessage(errorMessage);
}
}
deviceRepository.save(devices);

View File

@@ -10,6 +10,7 @@ import com.bwssystems.HABridge.api.UserCreateRequest;
import com.bwssystems.HABridge.api.hue.DeviceResponse;
import com.bwssystems.HABridge.api.hue.DeviceState;
import com.bwssystems.HABridge.api.hue.GroupResponse;
import com.bwssystems.HABridge.api.hue.GroupClassTypes;
import com.bwssystems.HABridge.api.hue.HueApiResponse;
import com.bwssystems.HABridge.api.hue.HueConfig;
import com.bwssystems.HABridge.api.hue.HueError;
@@ -29,6 +30,7 @@ import static spark.Spark.halt;
import static spark.Spark.options;
import static spark.Spark.post;
import static spark.Spark.put;
import static spark.Spark.delete;
import org.apache.http.HttpStatus;
@@ -38,6 +40,7 @@ import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Arrays;
/**
* Based on Armzilla's HueMulator - a Philips Hue emulator using sparkjava rest server
@@ -48,6 +51,7 @@ public class HueMulator {
private static final String HUE_CONTEXT = "/api";
private DeviceRepository repository;
private GroupRepository groupRepository;
private HomeManager homeManager;
private HueHome myHueHome;
private BridgeSettingsDescriptor bridgeSettings;
@@ -55,8 +59,9 @@ public class HueMulator {
private Gson aGsonHandler;
private DeviceMapTypes validMapTypes;
public HueMulator(BridgeSettings bridgeMaster, DeviceRepository aDeviceRepository, HomeManager aHomeManager) {
public HueMulator(BridgeSettings bridgeMaster, DeviceRepository aDeviceRepository, GroupRepository aGroupRepository, HomeManager aHomeManager) {
repository = aDeviceRepository;
groupRepository = aGroupRepository;
validMapTypes = new DeviceMapTypes();
bridgeSettingMaster = bridgeMaster;
bridgeSettings = bridgeSettingMaster.getBridgeSettingsDescriptor();
@@ -117,8 +122,19 @@ public class HueMulator {
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.type("application/json");
response.status(HttpStatus.SC_OK);
log.debug("group add requested from " + request.ip() + " user " + request.params(":userid") + " with body " + request.body());
return "[{\"success\":{\"id\":\"1\"}}]";
return addGroup(request.params(":userid"), request.ip(), request.body());
});
delete(HUE_CONTEXT + "/:userid/groups/:groupid", "application/json", (request, response) -> {
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.type("application/json");
response.status(HttpStatus.SC_OK);
return deleteGroup(request.params(":userid"), request.params(":groupid"), request.ip());
});
put(HUE_CONTEXT + "/:userid/groups/:groupid", "application/json", (request, response) -> {
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.type("application/json");
response.status(HttpStatus.SC_OK);
return modifyGroup(request.params(":userid"), request.params(":groupid"), request.ip(), request.body());
});
// http://ip_address:port/api/:userid/groups/<groupid>/action
// Dummy handler
@@ -128,8 +144,7 @@ public class HueMulator {
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.type("application/json");
response.status(HttpStatus.SC_OK);
log.debug("put action to groups API from " + request.ip() + " user " + request.params(":userid") + " with body " + request.body());
return "[{\"error\":{\"address\": \"/groups/0/action/scene\", \"type\":7, \"description\": \"invalid value, dummy for parameter, scene\"}}]";
return changeGroupState(request.params(":userid"), request.params(":groupid"), request.body(), request.ip());
});
// http://ip_address:port/api/{userId}/scenes returns json objects of
// all scenes configured
@@ -614,23 +629,191 @@ public class HueMulator {
return "{}";
}
private Object groupsListHandler(String userId, String requestIp) {
log.debug("hue group list requested: " + userId + " from " + requestIp);
private Object addGroup(String userId, String ip, String body) {
HueError[] theErrors = null;
Map<String, GroupResponse> groupResponseMap = null;
log.debug("group add requested from " + ip + " user " + userId + " with body " + body);
theErrors = bridgeSettingMaster.getBridgeSecurity().validateWhitelistUser(userId, null, bridgeSettingMaster.getBridgeSecurity().isUseLinkButton());
if (theErrors == null) {
if(bridgeSettingMaster.getBridgeSecurity().isSettingsChanged())
bridgeSettingMaster.updateConfigFile();
groupResponseMap = new HashMap<String, GroupResponse>();
groupResponseMap.put("1", (GroupResponse) this.groupsIdHandler("1", userId, requestIp));
return groupResponseMap;
GroupResponse theGroup = null;
try {
theGroup = aGsonHandler.fromJson(body, GroupResponse.class);
} catch (Exception e) {
theGroup = null;
}
if (theGroup == null) {
log.warn("Could not parse add group body. No group created.");
return aGsonHandler.toJson(HueErrorResponse.createResponse("5", "/groups/lights",
"invalid/missing parameters in body", null, null, null).getTheErrors(), HueError[].class);
}
List<GroupDescriptor> groups = groupRepository.findAll();
GroupDescriptor newGroup = new GroupDescriptor();
String type = theGroup.getType();
String groupClass = theGroup.getClass_name();
// check type
if (type == null || type.trim().equals("")) {
type = (groupClass == null || groupClass.trim().equals("")) ? "LightGroup" : "Room";
} else if (!type.equals("LightGroup") && !type.equals("Room")) {
type = "LightGroup";
}
// Everything else than a room must contain lights
if (!type.equals("Room")) {
if (theGroup.getLights() == null || theGroup.getLights().length == 0) {
return aGsonHandler.toJson(HueErrorResponse.createResponse("5", "/groups/lights",
"invalid/missing parameters in body", null, null, null).getTheErrors(), HueError[].class);
}
} else { // check room class if it's a room
if (groupClass == null || groupClass.trim().equals("")) {
groupClass = GroupClassTypes.OTHER;
} else if (!new GroupClassTypes().validateType(groupClass)) {
return aGsonHandler.toJson(HueErrorResponse.createResponse("7", "/groups/class",
"invalid value, " + groupClass + ", for parameter, class", null, null, null).getTheErrors(), HueError[].class);
}
}
String name = theGroup.getName();
Integer newId = groupRepository.getNewId();
if (name == null || name.trim().equals("")) {
name = type + " " + newId;
}
newGroup.setGroupType(type);
newGroup.setGroupClass(groupClass);
newGroup.setName(name);
newGroup.setLights(theGroup.getLights());
groups.add(newGroup);
groupRepository.save(groups.toArray(new GroupDescriptor[0]));
return "[{\"success\":{\"id\":\"" + newId + "\"}}]";
}
return theErrors;
}
private Object deleteGroup(String userId, String groupId, String ip) {
HueError[] theErrors = null;
log.debug("group delete requested from " + ip + " user " + userId);
theErrors = bridgeSettingMaster.getBridgeSecurity().validateWhitelistUser(userId, null, bridgeSettingMaster.getBridgeSecurity().isUseLinkButton());
if (theErrors == null) {
if(bridgeSettingMaster.getBridgeSecurity().isSettingsChanged())
bridgeSettingMaster.updateConfigFile();
GroupDescriptor 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 {
groupRepository.delete(group);
return "[{\"success\":\"/groups/" + groupId + " deleted\"}}]";
}
}
return theErrors;
}
private Object modifyGroup(String userId, String groupId, String ip, String body) {
HueError[] theErrors = null;
log.debug("group modify requested from " + ip + " user " + userId + " with body " + body);
theErrors = bridgeSettingMaster.getBridgeSecurity().validateWhitelistUser(userId, null, bridgeSettingMaster.getBridgeSecurity().isUseLinkButton());
if (theErrors == null) {
if(bridgeSettingMaster.getBridgeSecurity().isSettingsChanged())
bridgeSettingMaster.updateConfigFile();
GroupDescriptor 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 {
String successString = "[";
GroupResponse theGroup = null;
try {
theGroup = aGsonHandler.fromJson(body, GroupResponse.class);
} catch (Exception e) {
theGroup = null;
}
if (theGroup == null) {
log.warn("Could not parse modify group body. Group unchanged.");
return aGsonHandler.toJson(HueErrorResponse.createResponse("5", "/groups/lights",
"invalid/missing parameters in body", null, null, null).getTheErrors(), HueError[].class);
}
String type = theGroup.getType();
String groupClass = theGroup.getClass_name();
String name = theGroup.getName();
if (!(name == null || name.trim().equals(""))) {
group.setName(name);
successString += "{\"success\":{\"/groups/" + groupId + "/name\":\"" + name + "\"}},";
}
if (!group.getGroupType().equals("Room")) {
if (!(groupClass == null || groupClass.trim().equals(""))) {
return aGsonHandler.toJson(HueErrorResponse.createResponse("6", "/groups/" + groupId + "/class",
"parameter, /groups/" + groupId + "/class, not available", null, null, null).getTheErrors(), HueError[].class);
}
if (theGroup.getLights() != null) {
if (theGroup.getLights().length == 0) {
return aGsonHandler.toJson(HueErrorResponse.createResponse("7", "/groups/" + groupId + "/lights",
"invalid value, " + Arrays.toString(theGroup.getLights()) + ", for parameter, /groups" + groupId + "/lights", null, null, null).getTheErrors(), HueError[].class);
} else {
group.setLights(theGroup.getLights());
successString += "{\"success\":{\"/groups/" + groupId + "/lights\":\"" + Arrays.toString(theGroup.getLights()) + "\"}},";
}
}
} else { // check room class if it's a room
if (!(groupClass == null || groupClass.trim().equals(""))) {
if (!new GroupClassTypes().validateType(groupClass)) {
return aGsonHandler.toJson(HueErrorResponse.createResponse("7", "/groups/class",
"invalid value, " + groupClass + ", for parameter, class", null, null, null).getTheErrors(), HueError[].class);
} else {
group.setGroupClass(groupClass);
successString += "{\"success\":{\"/groups/" + groupId + "/class\":\"" + groupClass + "\"}},";
}
}
if (theGroup.getLights() != null) {
group.setLights(theGroup.getLights());
successString += "{\"success\":{\"/groups/" + groupId + "/lights\":\"" + Arrays.toString(theGroup.getLights()) + "\"}},";
}
}
groupRepository.save();
return (successString.length() == 1) ? "[]" : successString.substring(0, successString.length()-1) + "]";
}
}
return theErrors;
}
private Object groupsListHandler(String userId, String requestIp) {
HueError[] theErrors = null;
Map<String, GroupResponse> groupResponseMap = null;
if (bridgeSettings.isTraceupnp())
log.info("Traceupnp: hue group list requested: " + userId + " from " + requestIp);
log.debug("hue group list requested: " + userId + " from " + requestIp);
theErrors = bridgeSettingMaster.getBridgeSecurity().validateWhitelistUser(userId, null, bridgeSettingMaster.getBridgeSecurity().isUseLinkButton());
if (theErrors == null) {
if(bridgeSettingMaster.getBridgeSecurity().isSettingsChanged())
bridgeSettingMaster.updateConfigFile();
List<GroupDescriptor> groupList = groupRepository.findAllByRequester(requestIp);
groupResponseMap = new HashMap<String, GroupResponse>();
for (GroupDescriptor group : groupList) {
GroupResponse groupResponse = null;
if(!group.isInactive()) {
Map<String, DeviceResponse> lights = repository.findAllByGroupWithState(group.getLights(), requestIp, myHueHome, aGsonHandler);
groupResponse = GroupResponse.createResponse(group, lights);
groupResponseMap.put(group.getId(), groupResponse);
}
}
}
if (theErrors != null)
return theErrors;
return groupResponseMap;
}
private Object groupsIdHandler(String groupId, String userId, String requestIp) {
log.debug("hue group id: <" + groupId + "> requested: " + userId + " from " + requestIp);
@@ -641,14 +824,20 @@ public class HueMulator {
bridgeSettingMaster.updateConfigFile();
if (groupId.equalsIgnoreCase("0")) {
GroupResponse theResponse = GroupResponse.createDefaultGroupResponse(repository.findActive());
GroupResponse theResponse = GroupResponse.createDefaultGroupResponse((Map<String, DeviceResponse>)lightsListHandler(userId, requestIp));
return theResponse;
} else {
GroupDescriptor 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 {
Map<String, DeviceResponse> lights = repository.findAllByGroupWithState(group.getLights(), requestIp, myHueHome, aGsonHandler);
GroupResponse theResponse = GroupResponse.createResponse(group, lights);
return theResponse;
}
if (!groupId.equalsIgnoreCase("0")) {
GroupResponse theResponse = GroupResponse.createOtherGroupResponse(repository.findActive());
return theResponse;
}
theErrors = HueErrorResponse.createResponse("3", userId + "/groups/" + groupId, "Object not found", null, null, null).getTheErrors();
}
return theErrors;
@@ -666,7 +855,6 @@ public class HueMulator {
bridgeSettingMaster.updateConfigFile();
List<DeviceDescriptor> deviceList = repository.findAllByRequester(requestIp);
// List<DeviceDescriptor> deviceList = repository.findActive();
deviceResponseMap = new HashMap<String, DeviceResponse>();
for (DeviceDescriptor device : deviceList) {
DeviceResponse deviceResponse = null;
@@ -1023,4 +1211,63 @@ public class HueMulator {
return responseString;
}
private Object changeGroupState(String userId, String groupId, String body, String ipAddress) {
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());
if (theErrors == null) {
if(bridgeSettingMaster.getBridgeSecurity().isSettingsChanged())
bridgeSettingMaster.updateConfigFile();
Map<String, DeviceResponse> lights = null;
if (groupId.equalsIgnoreCase("0")) {
lights = (Map<String, DeviceResponse>)lightsListHandler(userId, ipAddress);
} else {
GroupDescriptor 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 (lights != null) {
StateChangeBody theStateChanges = null;
try {
theStateChanges = aGsonHandler.fromJson(body, StateChangeBody.class);
} catch (Exception e) {
theStateChanges = null;
}
if (theStateChanges == null) {
log.warn("Could not parse state change body. Light state not changed.");
return aGsonHandler.toJson(HueErrorResponse.createResponse("2", "/groups/" + groupId + "/action",
"Could not parse state change body.", null, null, null).getTheErrors(), HueError[].class);
}
boolean turnOn = false;
boolean turnOff = false;
if (!(body.contains("\"bri_inc\"") || body.contains("\"bri\""))) {
if (!(body.contains("\"xy\"") || body.contains("\"ct\"") || body.contains("\"hue\""))) {
if (theStateChanges.isOn()) {
turnOn = true;
} else if (!theStateChanges.isOn()) {
turnOff = true;
}
}
}
for (Map.Entry<String, DeviceResponse> light : lights.entrySet()) {
if (turnOff && !light.getValue().getState().isOn())
continue;
if (turnOn && light.getValue().getState().isOn())
continue;
changeState(userId, light.getKey(), body, ipAddress);
}
return "[]";
}
}
return theErrors;
}
}

View File

@@ -167,7 +167,7 @@
<th>Target Item</th>
<th>Delay</th>
<th>Count</th>
<!--<th>Filter IPs</th>-->
<th>Filter IPs</th>
<th>Http Verb</th>
<th>Http Body</th>
<th>Http Headers</th>
@@ -187,9 +187,9 @@
id="item-delay" ng-model="onItem.delay" placeholder="millis"></textarea></td>
<td><textarea rows="1" class="form-control" style="max-width: 80px; min-width: 80px"
id="item-count" ng-model="onItem.count" placeholder="number"></textarea></td>
<!--<td><textarea rows="1" class="form-control"
<td><textarea rows="1" class="form-control"
id="item-filterIPs" ng-model="onItem.filterIPs"
placeholder="restrict IPs"></textarea></td>-->
placeholder="restrict IPs"></textarea></td>
<td><select name="item-http-verb" id="item-http-verb" style="max-width: 80px"
ng-model="onItem.httpVerb">
<option value="">---</option>
@@ -240,9 +240,9 @@
<td><textarea rows="1" cols="2" class="form-control" style="max-width: 80px; min-width: 80px"
id="item-count" ng-model="newOnItem.count"
placeholder="number"></textarea></td>
<!--<td><textarea rows="1" cols="16" class="form-control"
<td><textarea rows="1" cols="16" class="form-control"
id="item-filterIPs" ng-model="newOnItem.filterIPs"
placeholder="restrict IPs"></textarea></td>-->
placeholder="restrict IPs"></textarea></td>
<td><select name="item-http-verb" id="item-http-verb" style="max-width: 80px"
ng-model="newOnItem.httpVerb">
<option value="">---</option>
@@ -301,7 +301,7 @@
<th>Target Item</th>
<th>Delay</th>
<th>Count</th>
<!--<th>Filter IPs</th>-->
<th>Filter IPs</th>
<th>Http Verb</th>
<th>Http Body</th>
<th>Http Headers</th>
@@ -322,9 +322,9 @@
id="item-delay" ng-model="dimItem.delay" placeholder="millis"></textarea></td>
<td><textarea rows="1" cols="2" class="form-control" style="max-width: 80px; min-width: 80px"
id="item-count" ng-model="dimItem.count" placeholder="number"></textarea></td>
<!--<td><textarea rows="1" cols="16" class="form-control"
<td><textarea rows="1" cols="16" class="form-control"
id="item-filterIPs" ng-model="dimItem.filterIPs"
placeholder="restrict IPs"></textarea></td>-->
placeholder="restrict IPs"></textarea></td>
<td><select name="item-http-verb" id="item-http-verb" style="max-width: 80px"
ng-model="dimItem.httpVerb">
<option value="">---</option>
@@ -375,9 +375,9 @@
<td><textarea rows="1" cols="2" class="form-control" style="max-width: 80px; min-width: 80px"
id="item-count" ng-model="newDimItem.count"
placeholder="number"></textarea></td>
<!--<td><textarea rows="1" cols="16" class="form-control"
<td><textarea rows="1" cols="16" class="form-control"
id="item-filterIPs" ng-model="newDimItem.filterIPs"
placeholder="restrict IPs"></textarea></td> -->
placeholder="restrict IPs"></textarea></td>
<td><select name="item-http-verb" id="item-http-verb" style="max-width: 80px"
ng-model="newDimItem.httpVerb">
<option value="">---</option>
@@ -436,7 +436,7 @@
<th>Target Item</th>
<th>Delay</th>
<th>Count</th>
<!--<th>Filter IPs</th>-->
<th>Filter IPs</th>
<th>Http Verb</th>
<th>Http Body</th>
<th>Http Headers</th>
@@ -457,9 +457,9 @@
id="item-delay" ng-model="offItem.delay" placeholder="millis"></textarea></td>
<td><textarea rows="1" cols="2" class="form-control" style="max-width: 80px; min-width: 80px"
id="item-count" ng-model="offItem.count" placeholder="number"></textarea></td>
<!--<td><textarea rows="1" cols="16" class="form-control"
<td><textarea rows="1" cols="16" class="form-control"
id="item-filterIPs" ng-model="offItem.filterIPs"
placeholder="restrict IPs"></textarea></td>-->
placeholder="restrict IPs"></textarea></td>
<td><select name="item-http-verb" id="item-http-verb" style="max-width: 80px"
ng-model="offItem.httpVerb">
<option value="">---</option>
@@ -510,9 +510,9 @@
<td><textarea rows="1" cols="2" class="form-control" style="max-width: 80px; min-width: 80px"
id="item-count" ng-model="newOffItem.count"
placeholder="number"></textarea></td>
<!--<td><textarea rows="1" cols="16" class="form-control"
<td><textarea rows="1" cols="16" class="form-control"
id="item-filterIPs" ng-model="newOffItem.filterIPs"
placeholder="restrict IPs"></textarea></td>-->
placeholder="restrict IPs"></textarea></td>
<td><select name="item-http-verb" id="item-http-verb" style="max-width: 80px"
ng-model="newOffItem.httpVerb">
<option value="">---</option>
@@ -570,7 +570,7 @@
<th>Target Item</th>
<th>Delay</th>
<th>Count</th>
<!--<th>Filter IPs</th>-->
<th>Filter IPs</th>
<th>Http Verb</th>
<th>Http Body</th>
<th>Http Headers</th>
@@ -591,9 +591,9 @@
id="item-delay" ng-model="colorItem.delay" placeholder="millis"></textarea></td>
<td><textarea rows="1" cols="2" class="form-control" style="max-width: 80px; min-width: 80px"
id="item-count" ng-model="colorItem.count" placeholder="number"></textarea></td>
<!--<td><textarea rows="1" cols="16" class="form-control"
<td><textarea rows="1" cols="16" class="form-control"
id="item-filterIPs" ng-model="colorItem.filterIPs"
placeholder="restrict IPs"></textarea></td>-->
placeholder="restrict IPs"></textarea></td>
<td><select name="item-http-verb" id="item-http-verb" style="max-width: 80px"
ng-model="colorItem.httpVerb">
<option value="">---</option>
@@ -644,9 +644,9 @@
<td><textarea rows="1" cols="2" class="form-control" style="max-width: 80px; min-width: 80px"
id="item-count" ng-model="newColorItem.count"
placeholder="number"></textarea></td>
<!--<td><textarea rows="1" cols="16" class="form-control"
<td><textarea rows="1" cols="16" class="form-control"
id="item-filterIPs" ng-model="newColorItem.filterIPs"
placeholder="restrict IPs"></textarea></td>-->
placeholder="restrict IPs"></textarea></td>
<td><select name="item-http-verb" id="item-http-verb" style="max-width: 80px"
ng-model="newColorItem.httpVerb">
<option value="">---</option>

View File

@@ -75,6 +75,13 @@
ng-model="bridge.settings.upnpdevicedb"
placeholder="data/device.db"></td>
</tr>
<tr>
<td>Groups DB Path and File</td>
<td><input id="bridge-settings-upnpgroupdb"
class="form-control" type="text"
ng-model="bridge.settings.upnpgroupdb"
placeholder="data/group.db"></td>
</tr>
<tr>
<td>UPNP IP Address</td>
<td><input id="bridge-settings-upnpconfigaddress"