Finished implementation of generic UDP handling. This will support the

LimitlessLED Bridge. Updated Readme for UDP and more on configuration
setup. Fixed issue with HueMulator handliong due to devices without
mapTypes.

Fixes #6.
This commit is contained in:
Admin
2015-12-07 16:34:37 -06:00
parent cf772334c4
commit 3c54ccd56d
3 changed files with 72 additions and 30 deletions

View File

@@ -1,5 +1,5 @@
# ha-bridge # ha-bridge
Emulates Philips Hue api to other home automation gateways such as an Amazon Echo. The Bridge has helpers to build devices for the gateway for the Logitech Harmony Hub, Vera, Vera Lite or Vera Edge. Alternatively the Bridge supports custom calls as well. The Bridge handles basic commands such as "On", "Off" and "brightness" commands of the hue protocol. You can provide multiple harmony hubs for control. Emulates Philips Hue api to other home automation gateways such as an Amazon Echo. The Bridge handles basic commands such as "On", "Off" and "brightness" commands of the hue protocol. This bridge can control most devices that have a distinct API. In the cases of systems that require authorization and/or have API's that cannot be handled in the current method, a module may need to be built. The Harmony Hub is such a module. The Bridge has helpers to build devices for the gateway for the Logitech Harmony Hub, Vera, Vera Lite or Vera Edge. Alternatively the Bridge supports custom calls as well.
## Build ## Build
To customize and build it yourself, build a new jar with maven: To customize and build it yourself, build a new jar with maven:
``` ```
@@ -32,8 +32,16 @@ The password for the user name of the MyHarmony.com account for the Harmony Hub.
Upnp has been very closed on this platform to try and respond as a hue and there is now a setting to control if it is more open or strict, Add -Dupnp.strict=`<true|false>` to your command line to have the emulator respond to what it thinks is an echo to a hue or any other device. The default is upnp.strict=true. Upnp has been very closed on this platform to try and respond as a hue and there is now a setting to control if it is more open or strict, Add -Dupnp.strict=`<true|false>` to your command line to have the emulator respond to what it thinks is an echo to a hue or any other device. The default is upnp.strict=true.
### -Dtrace.upnp=`<true|false>` ### -Dtrace.upnp=`<true|false>`
Turn on tracing for upnp discovery messages. The default is false. Turn on tracing for upnp discovery messages. The default is false.
## Web Config ## HA Bridge Device Configuration
Configure by going to the url for the host you are running on or localhost with port you have assigned: and use the helpers for the Vera or Harmony Hub to create devices that the Echo will find. You must configure devices before you will have any thing for the Echo to receive. The easy way to get devices configures is with the web interface by going to the url for the host you are running on or localhost with port you have assigned: and use the helpers for the Vera or Harmony Hub to create devices that the Echo will find.
Another way to add a device is through the Manual Add Tab. This allows you to manually enter the name, the on and off URLs and select if there are custom handling with the type of call that can be made. This allows for control of anything that has a distinct request that can be executed so you are not limited to the Vera or the Harmony.
The format of these can be the default HTTP request which excecutes the URLs formtted as http://<your stuff here> as a GET. Other options to this are to select the HTTP Verb and add the data type and add a body that is passed with the request. Another option that is detected by the bridge is to use the udp://<ip_address>:<port>/<your stuff here to send a UDP request. If your data for the UDP request is formatted as "0x00F009B9" lexical hex format, the bridge will convert the UDP data into a binary stream to send. Sed the examples inthe Configuration REST API Usage Section to get an idea.
Also, you may want to use the REST API's listed below to configure your devices.
Web Configuration Access
``` ```
http://<ip address>:<port> http://<ip address>:<port>
``` ```
@@ -50,7 +58,7 @@ Turn on / off your connected home device | "Turn on/off [connected home device n
Set the brightness of compatible lights | "Set brightness to [##]%." OR "Dim the lights to [##]%." Set the brightness of compatible lights | "Set brightness to [##]%." OR "Dim the lights to [##]%."
To view or remove devices that Alexa knows about, you can use the mobile app `Menu / Settings / Connected Home`. To view or remove devices that Alexa knows about, you can use the mobile app `Menu / Settings / Connected Home`.
## Configuration REST API usage ## Configuration REST API Usage
This section will describe the REST api available for configuration. The REST body examples are all formatted for easy reading, the actual body usage should be like this: This section will describe the REST api available for configuration. The REST body examples are all formatted for easy reading, the actual body usage should be like this:
``` ```
{"var1":"value1","var2":"value2","var3:"value3"} {"var1":"value1","var2":"value2","var3:"value3"}
@@ -62,7 +70,7 @@ Add a new device to the HA Bridge configuration. There is a basic examples and t
``` ```
POST http://host:8080/api/devices POST http://host:8080/api/devices
``` ```
#### Body arguments #### Body Arguments
Name | Type | Description | Required Name | Type | Description | Required
-----|-------|--------------|------------ -----|-------|--------------|------------
name | string | A name for the device. This is also the utterance value that the Echo will use. | Required name | string | A name for the device. This is also the utterance value that the Echo will use. | Required
@@ -73,7 +81,7 @@ httpVerb | string | This is used for "custom" calls that the user would like to
contentType | string | This is an http type string such as "application/text" or "application/xml" or "application/json". | Optional contentType | string | This is an http type string such as "application/text" or "application/xml" or "application/json". | Optional
contentBody | string | This is the content body that you would like to send when executing an "on" request. | Optional contentBody | string | This is the content body that you would like to send when executing an "on" request. | Optional
contentBodyOff | string | This is the content body that you would like to send when executing an "off" request. | Optional contentBodyOff | string | This is the content body that you would like to send when executing an "off" request. | Optional
#### Basic example #### Basic Example
``` ```
{ {
"name" : "bedroom light", "name" : "bedroom light",
@@ -82,8 +90,8 @@ contentBodyOff | string | This is the content body that you would like to send w
"offUrl" : "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=41" "offUrl" : "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=41"
} }
``` ```
#### Dimming and value passing control example #### Dimming Control Example
Dimming is also supported by using the expressions ${intensity.percent} for 0-100 or ${intensity.byte} for 0-255 or custom values using ${intensity.math(<your expression using "X" as the value to operate on>)} i.e. "${intensity.math(X/4)}". Dimming is also supported by using the expressions ${intensity.percent} for 0-100 or ${intensity.byte} for 0-255 for straight pass trhough of the value.
e.g. e.g.
``` ```
{ {
@@ -95,13 +103,26 @@ e.g.
``` ```
See the echo's documentation for the dimming phrase. See the echo's documentation for the dimming phrase.
#### POST/PUT support example #### Value Passing Control Example
You can control items that require special calculated values using ${intensity.math(<your expression using "X" as the value to operate on>)} i.e. "${intensity.math(X/4)}".
e.g.
```
{
"name": "Thermostat,
"deviceType": "custom",
"offUrl": "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=10",
"onUrl": "http://192.168.1.201:3480/data_request?id=action&output_format=json&DeviceNum=10&serviceId=urn:upnp-org:serviceId:Dimming1&action=SetLoadLevelTarget&newLoadlevelTarget=${intensity.math(X/4)}"
}
```
See the echo's documentation for the dimming phrase.
#### POST/PUT Support Example
``` ```
This will allow control of any other application that may need more then GET. You can also use the dimming and value control commands within the URLs as well. This will allow control of any other application that may need more then GET. You can also use the dimming and value control commands within the URLs as well.
e.g: e.g:
{ {
"name": "test device", "name": "test device",
"deviceType": "switch", "deviceType": "custom",
"offUrl": "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=31", "offUrl": "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=31",
"onUrl": "http://192.168.1.201:3480/data_request?id=action&output_format=json&DeviceNum=31&serviceId=urn:upnp-org:serviceId:Dimming1&action=SetLoadLevelTarget&newLoadlevelTarget=${intensity.percent}", "onUrl": "http://192.168.1.201:3480/data_request?id=action&output_format=json&DeviceNum=31&serviceId=urn:upnp-org:serviceId:Dimming1&action=SetLoadLevelTarget&newLoadlevelTarget=${intensity.percent}",
"httpVerb":"POST", "httpVerb":"POST",
@@ -115,12 +136,20 @@ Anything that takes an action as a result of an HTTP request will probably work
``` ```
{ {
"name": "night mode", "name": "night mode",
"deviceType": "switch", "deviceType": ""custom",
"offUrl": "http://192.168.1.201:3480/data_request?id=lu_action&serviceId=urn:micasaverde-com:serviceId:HomeAutomationGateway1&action=SetHouseMode&Mode=1", "offUrl": "http://192.168.1.201:3480/data_request?id=lu_action&serviceId=urn:micasaverde-com:serviceId:HomeAutomationGateway1&action=SetHouseMode&Mode=1",
"onUrl": "http://192.168.1.201:3480/data_request?id=lu_action&serviceId=urn:micasaverde-com:serviceId:HomeAutomationGateway1&action=SetHouseMode&Mode=3" "onUrl": "http://192.168.1.201:3480/data_request?id=lu_action&serviceId=urn:micasaverde-com:serviceId:HomeAutomationGateway1&action=SetHouseMode&Mode=3"
} }
``` ```
#### Response Here is a UDP example that can send binary data.
```
{
"name": "UDPPacket",
"deviceType": "custom",
"offUrl": "udp://192.168.1.1:8899/0x460055",
"onUrl": "udp://192.168.1.1:8899/0x450055"
}
```#### Response
Name | Type | Description Name | Type | Description
-----|-------|------------- -----|-------|-------------
id | number | This is the ID assigned to the device and used for lookup. id | number | This is the ID assigned to the device and used for lookup.
@@ -141,12 +170,12 @@ contentBodyOff | string | This is the content body that you would like to send w
"offUrl" : "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=41" "offUrl" : "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=41"
} }
``` ```
### Update a device ### Update a Device
Update an existing device using it's ID that was given when the device was created and the update could contain any of the fields that are used and shown in the previous examples when adding a device. Update an existing device using it's ID that was given when the device was created and the update could contain any of the fields that are used and shown in the previous examples when adding a device.
``` ```
POST http://host:8080/api/devices/<id> POST http://host:8080/api/devices/<id>
``` ```
#### Body arguments #### Body Arguments
Name | Type | Description | Required Name | Type | Description | Required
-----|-------|--------------|------------ -----|-------|--------------|------------
id | number | This is the ID assigned to the device and used for lookup. id | number | This is the ID assigned to the device and used for lookup.
@@ -178,7 +207,7 @@ contentBodyOff | string | This is the content body that you would like to send w
"offUrl" : "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=41" "offUrl" : "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=41"
} }
``` ```
### Get all devices ### Get All Devices
Get all devices saved in the HA bridge configuration. Get all devices saved in the HA bridge configuration.
``` ```
GET http://host:8080/api/devices GET http://host:8080/api/devices
@@ -201,7 +230,7 @@ Individual entries are the same as a single device but in json list format.
"offUrl" : "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=41" "offUrl" : "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=41"
}] }]
``` ```
### Get a specific device ### Get a Specific Device
Get a device by ID assigned from creation and saved in the HA bridge configuration. Get a device by ID assigned from creation and saved in the HA bridge configuration.
``` ```
GET http://host:8080/api/devices/<id> GET http://host:8080/api/devices/<id>
@@ -217,7 +246,7 @@ The response is the same layout as defined in the add device response.
"offUrl" : "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=41" "offUrl" : "http://192.168.1.201:3480/data_request?id=action&output_format=json&serviceId=urn:upnp-org:serviceId:SwitchPower1&action=SetTarget&newTargetValue=0&DeviceNum=41"
} }
``` ```
### Delete a specific device ### Delete a Specific Device
Delete a device by ID assigned from creation and saved in the HA bridge configuration. Delete a device by ID assigned from creation and saved in the HA bridge configuration.
``` ```
DELETE http://host:8080/api/devices/<id> DELETE http://host:8080/api/devices/<id>

View File

@@ -5,7 +5,7 @@
<groupId>com.bwssystems.HABridge</groupId> <groupId>com.bwssystems.HABridge</groupId>
<artifactId>ha-bridge</artifactId> <artifactId>ha-bridge</artifactId>
<version>1.2.1a</version> <version>1.2.2</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>HA Bridge</name> <name>HA Bridge</name>

View File

@@ -39,11 +39,15 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.net.DatagramPacket;
import java.net.DatagramSocket; import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.xml.bind.DatatypeConverter;
/** /**
* Based on Armzilla's HueMulator - a Philips Hue emulator using sparkjava rest server * Based on Armzilla's HueMulator - a Philips Hue emulator using sparkjava rest server
*/ */
@@ -62,6 +66,7 @@ public class HueMulator {
private HttpClient httpClient; private HttpClient httpClient;
private ObjectMapper mapper; private ObjectMapper mapper;
private BridgeSettings bridgeSettings; private BridgeSettings bridgeSettings;
private byte[] sendData;
public HueMulator(BridgeSettings theBridgeSettings, DeviceRepository aDeviceRepository, HarmonyHome theHarmonyHome){ public HueMulator(BridgeSettings theBridgeSettings, DeviceRepository aDeviceRepository, HarmonyHome theHarmonyHome){
@@ -266,14 +271,14 @@ public class HueMulator {
try { try {
state = mapper.readValue(request.body(), DeviceState.class); state = mapper.readValue(request.body(), DeviceState.class);
} catch (IOException e) { } catch (IOException e) {
log.error("Object mapper barfed on input of body.", e); log.warn("Object mapper barfed on input of body.", e);
responseString = "[{\"error\":{\"type\": 2, \"address\": \"/lights/" + lightId + ",\"description\": \"Object mapper barfed on input of body.\"}}]"; responseString = "[{\"error\":{\"type\": 2, \"address\": \"/lights/" + lightId + ",\"description\": \"Object mapper barfed on input of body.\"}}]";
return responseString; return responseString;
} }
DeviceDescriptor device = repository.findOne(lightId); DeviceDescriptor device = repository.findOne(lightId);
if (device == null) { if (device == null) {
log.error("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;
} }
@@ -301,27 +306,27 @@ public class HueMulator {
else else
responseString = responseString + "]"; responseString = responseString + "]";
if(device.getDeviceType().toLowerCase().contains("activity") || device.getMapType().equalsIgnoreCase("harmonyActivity")) if(device.getDeviceType().toLowerCase().contains("activity") || (device.getMapType() != null && device.getMapType().equalsIgnoreCase("harmonyActivity")))
{ {
log.debug("executing activity to Harmony: " + url); log.debug("executing activity to Harmony: " + url);
RunActivity anActivity = new Gson().fromJson(url, RunActivity.class); RunActivity anActivity = new Gson().fromJson(url, RunActivity.class);
HarmonyHandler myHarmony = myHarmonyHome.getHarmonyHandler(device.getTargetDevice()); HarmonyHandler myHarmony = myHarmonyHome.getHarmonyHandler(device.getTargetDevice());
if(myHarmony == null) if(myHarmony == null)
{ {
log.error("Should not get here, no harmony hub available"); log.warn("Should not get here, no harmony hub available");
responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + ",\"description\": \"Should not get here, no harmony hub available\", \"parameter\": \"/lights/" + lightId + "state\"}}]"; responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + ",\"description\": \"Should not get here, no harmony hub available\", \"parameter\": \"/lights/" + lightId + "state\"}}]";
} }
else else
myHarmony.startActivity(anActivity); myHarmony.startActivity(anActivity);
} }
else if(device.getDeviceType().toLowerCase().contains("button") || device.getMapType().equalsIgnoreCase("harmonyButton")) else if(device.getDeviceType().toLowerCase().contains("button") || (device.getMapType() != null && device.getMapType().equalsIgnoreCase("harmonyButton")))
{ {
log.debug("executing button press to Harmony: " + url); log.debug("executing button press to Harmony: " + url);
ButtonPress aDeviceButton = new Gson().fromJson(url, ButtonPress.class); ButtonPress aDeviceButton = new Gson().fromJson(url, ButtonPress.class);
HarmonyHandler myHarmony = myHarmonyHome.getHarmonyHandler(device.getTargetDevice()); HarmonyHandler myHarmony = myHarmonyHome.getHarmonyHandler(device.getTargetDevice());
if(myHarmony == null) if(myHarmony == null)
{ {
log.error("Should not get here, no harmony hub available"); log.warn("Should not get here, no harmony hub available");
responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + ",\"description\": \"Should not get here, no harmony hub available\", \"parameter\": \"/lights/" + lightId + "state\"}}]"; responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + ",\"description\": \"Should not get here, no harmony hub available\", \"parameter\": \"/lights/" + lightId + "state\"}}]";
} }
else else
@@ -330,14 +335,22 @@ public class HueMulator {
else if(url.startsWith("udp://")) else if(url.startsWith("udp://"))
{ {
try { try {
DatagramSocket responseSocket = new DatagramSocket(10000);
String intermediate = url.substring(6); String intermediate = url.substring(6);
String ipAddr = intermediate.substring(0, intermediate.indexOf(':')); String ipAddr = intermediate.substring(0, intermediate.indexOf(':'));
String port = intermediate.substring(intermediate.indexOf(':'), intermediate.indexOf('/')); String port = intermediate.substring(intermediate.indexOf(':') + 1, intermediate.indexOf('/'));
String theBody = intermediate.substring(intermediate.indexOf('/')+1); String theBody = intermediate.substring(intermediate.indexOf('/')+1);
DatagramSocket responseSocket = new DatagramSocket(Integer.parseInt(port));
if(theBody.startsWith("0x")) {
sendData = DatatypeConverter.parseHexBinary(theBody.substring(2));
}
else
sendData = theBody.getBytes();
InetAddress IPAddress = InetAddress.getByName(ipAddr);
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, Integer.parseInt(port));
responseSocket.send(sendPacket);
responseSocket.close(); responseSocket.close();
} catch (IOException e) { } catch (IOException e) {
log.error("Could not send UDP Datagram packet for request.", e); log.warn("Could not send UDP Datagram packet for request.", e);
responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + ",\"description\": \"Error on calling out to device\", \"parameter\": \"/lights/" + lightId + "state\"}}]"; responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + ",\"description\": \"Error on calling out to device\", \"parameter\": \"/lights/" + lightId + "state\"}}]";
} }
} }
@@ -353,7 +366,7 @@ public class HueMulator {
body = replaceIntensityValue(device.getContentBodyOff(), state.getBri()); body = replaceIntensityValue(device.getContentBodyOff(), state.getBri());
// make call // make call
if (!doHttpRequest(url, device.getHttpVerb(), device.getContentType(), body)) { if (!doHttpRequest(url, device.getHttpVerb(), device.getContentType(), body)) {
log.error("Error on calling url to change device state: " + url); log.warn("Error on calling url to change device state: " + url);
responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + ",\"description\": \"Error on calling url to change device state\", \"parameter\": \"/lights/" + lightId + "state\"}}]"; responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + ",\"description\": \"Error on calling url to change device state\", \"parameter\": \"/lights/" + lightId + "state\"}}]";
} }
} }
@@ -393,7 +406,7 @@ public class HueMulator {
Integer endResult = Math.round(result.floatValue()); Integer endResult = Math.round(result.floatValue());
request = request.replace(INTENSITY_MATH + mathDescriptor + INTENSITY_MATH_CLOSE, endResult.toString()); request = request.replace(INTENSITY_MATH + mathDescriptor + INTENSITY_MATH_CLOSE, endResult.toString());
} catch (Exception e) { } catch (Exception e) {
log.error("Could not execute Math: " + mathDescriptor, e); log.warn("Could not execute Math: " + mathDescriptor, e);
} } } }
return request; return request;
} }
@@ -426,7 +439,7 @@ public class HueMulator {
return true; return true;
} }
} catch (IOException e) { } catch (IOException e) {
log.error("Error calling out to HA gateway", e); log.warn("Error calling out to HA gateway", e);
} }
return false; return false;
} }