Compare commits

...

23 Commits

Author SHA1 Message Date
Admin
84fb79f9d9 Fixed issue with "Test Dim" button not presenting the value selector
when there was a body with a dim command. Added ability to specifically
set the web server address port.

Fixes #225
Fixes #217
2016-11-08 11:27:43 -06:00
BWS Systems
3702de8efd Merge pull request #222 from TheOriginalAndrobot/master
Update documentation with Google Home info
2016-11-05 11:33:44 -05:00
Androbot
f0ab9afd66 Update documentation with Google Home info 2016-11-05 08:48:35 -07:00
Admin
e5c4e09543 Update readme for version. 2016-11-02 16:33:57 -05:00
Admin
425ac9fb91 Updated upnp response handling to warn on failure and not stop. Added
configuration of echo url for those overseas. Fixed overwriting device
issue when adding a manual device add or after a restart. Added default
IpV4 handling with override for ha-bridge. Fixed Dim URL http body
handling on PUT and POST. Added documentation for Kodi Volume handling.

Fixes #207
Fixes #202
Fixes #201
Fixes #198
Fixes #194
Fixes #193
2016-11-02 16:32:57 -05:00
BWS Systems
32203b65be Merge pull request #189 from bwssytems/postv3.1fixes
Fixes #129 
Fixes #161 
Fixes #166 
Fixes #168 
Fixes #172 
Fixes #173 
Fixes #174 
Fixes #181
2016-10-18 08:42:01 -05:00
Admin
7e9600d2a7 Update release version number. 2016-10-18 08:35:27 -05:00
BWS Systems
dcc470486f Merge pull request #185 from martinbutt/master
Docs fix
2016-10-14 08:17:26 -05:00
Martin Butt
330adbdd4c Docs fix 2016-10-13 15:21:27 -07:00
Admin
23d43b0136 I got some more info from another source about the discovery responses
of a real hue. It sends three responses not just one. Also I have
updated the S/N UUID and the Hue Bridge ID creation as they are
different.
2016-10-10 13:23:29 -05:00
Admin
004276d3ea Changed handling of upnp rsponses from the upnp listener and the
huemulator udp calls to use the same response port.
2016-10-07 16:29:55 -05:00
Admin
e90e0f69ef Updated headers content type to be just "application/json" instead of
"application/json; charset utf-8".
2016-10-04 13:34:47 -05:00
Admin
7163a12384 Updating upnp responses to be more inline with what the hue hub does. To
do so, added the mac address as the id in the bridge id an, serial
number and uuid
2016-10-03 15:07:09 -05:00
Admin
bb65650e53 Added ip check if upnp config address is not available on the current
host. Added checks for content type and body for put and post requests.
2016-09-29 16:21:15 -05:00
Admin
7f7e96465b Switched default web port to be 80. This should resolve some issues with
first time configs and external apps. Changed upnp listener response to
previous style. Set default for link button presses status to true.
2016-09-29 09:25:47 -05:00
Admin
8ff7bc0120 Updated upnp response for M-SEARCH again. Updated devices to have
numbering more in line with how the hue bridge is done. Added special
generation of hue device unique id as the philips api spec shows.  Added
a renumber function to convert id's.
2016-09-28 16:18:53 -05:00
Admin
4da5f65d89 Updatting upnp responses 2016-09-26 17:03:30 -05:00
Admin
faa67827c6 Updated configuration items for hue responses. ADded call thru for hue
devices configured for state info. added dim content body.
2016-09-23 15:46:39 -05:00
Admin
bbce1f4235 Merge remote-tracking branch 'origin/master' into postv3.1fixes 2016-09-22 09:27:02 -05:00
BWS Systems
8575deadf1 Update readne.md for a missed version change
Changed line for download of ha-bridge jar
2016-09-22 09:02:28 -05:00
Admin
ad4015927c Add check for host is down exception on upnp send in listener and not
shutdown only a warning.
2016-09-21 16:33:57 -05:00
BWS Systems
f7df6951b0 Update README.md
Updated version to be the latest in the command examples
2016-09-20 08:29:01 -05:00
Admin
c20d046b30 Updated upnp response to be closer to the hue bridge v2 spec. Updated
handling for is on when bri is 0. Updated exec to check for empty call.
2016-09-15 15:41:59 -05:00
25 changed files with 832 additions and 240 deletions

120
README.md
View File

@@ -1,5 +1,5 @@
# ha-bridge
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.
Emulates Philips Hue api to other home automation gateways such as an Amazon Echo or Google Home. 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 and so is the Nest module. The Bridge has helpers to build devices for the gateway for the Logitech Harmony Hub, Vera, Vera Lite or Vera Edge, Nest and the ability to proxy all of your real Hue bridges behind this bridge.
@@ -21,7 +21,7 @@ Then locate the jar and start the server with:
ATTENTION: This requires JDK 1.8 to run
```
java -jar ha-bridge-2.5.0.jar
java -jar ha-bridge-3.2.2.jar
```
### Automation on Linux systems
To have this configured and running automatically there are a few resources to use. One is using Docker and a docker container has been built for this and can be gotten here: https://github.com/aptalca/docker-ha-bridge
@@ -38,7 +38,7 @@ After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/java -jar -Dconfig.file=/home/pi/amazon-echo/data/habridge.config /home/pi/amazon-echo/ha-bridge-2.5.0.jar
ExecStart=/usr/bin/java -jar -Dconfig.file=/home/pi/amazon-echo/data/habridge.config /home/pi/amazon-echo/ha-bridge-3.2.2.jar
[Install]
WantedBy=multi-user.target
@@ -46,11 +46,11 @@ WantedBy=multi-user.target
Basic script setup to run the bridge on a pi.
Create the directory and make sure that ha-bridge-2.5.0.jar is in your /home/pi/habridge directory.
Create the directory and make sure that ha-bridge-3.2.2.jar is in your /home/pi/habridge directory.
```
pi@raspberrypi:~ $ mkdir habridge
pi@raspberrypi:~ $ cd habridge
pi@raspberrypi:~/habridge $ wget https://github.com/bwssytems/ha-bridge/releases/download/v2.0.6/ha-bridge-2.5.0.jar
pi@raspberrypi:~/habridge $ wget https://github.com/bwssytems/ha-bridge/releases/download/v3.2.2/ha-bridge-3.2.2.jar
```
Edit the shell script for starting:
```
@@ -60,7 +60,7 @@ Then cut and past this, modify any locations that are not correct
```
cd /home/pi/habridge
rm /home/pi/habridge/habridge-log.txt
nohup java -jar /home/pi/habridge/ha-bridge-2.5.0.jar > /home/pi/habridge/habridge-log.txt 2>&1 &
nohup java -jar /home/pi/habridge/ha-bridge-3.2.2.jar > /home/pi/habridge/habridge-log.txt 2>&1 &
chmod 777 /home/pi/habridge/habridge-log.txt
```
Exit and save the file with ctrl-X and follow the prompts and then execute on the command line:
@@ -83,14 +83,15 @@ The default location for the configuration file to contain the settings for the
java -jar -Dconfig.file=/home/me/data/myhabridge.config ha-bridge-W.X.Y.jar
```
### -Dserver.port=`<port number>`
The default port number for the bridge is 8080. To override what the default or what is in the configuration file for this parameter, specify -Dserver.port=`<port number>` explicitly. This is especially helpful if you are running the ha-bridge for the first time and have another application on port 8080. The command line example:
The default port number for the bridge is 80. To override what the default or what is in the configuration file for this parameter, specify -Dserver.port=`<port number>` explicitly. This is especially helpful if you are running the ha-bridge for the first time and have another application on port 80. The command line example:
```
java -jar -Dserver.port=80 ha-bridge-W.X.Y.jar
```
Note: if using with a Google Home device, port 80 *must* be used.
## HA Bridge Usage and Configuration
This section will cover the basics of configuration and where this configuration can be done. This requires that you have started your bridge process and then have pointed your
favorite web interface by going to the http://<my ip address>:<port> or http://localhost:<port> with port you have assigned. The default quick link is http://localhost:8080 for yoru reference.
favorite web interface by going to the http://<my ip address>:<port> or http://localhost:<port> with port you have assigned. The default quick link is http://localhost for yoru reference.
### The Bridge Devices Tab
This screen allows you to see your devices you have configured for the ha-bridge to present to a controller, such as an Amazon Echo/Dot. It gives you a count of devices as there have been reports that the Echo only supports a limited number, but has been growing as of late, YMMV. You can test each device from this page as this calls the ha-bridge just as a controller would, i.e. the Echo. This is useful to make sure your configuration for each device is correct and for trouble shooting. You can also manages your devices as well by editing and making a new device copy as well as deleting it.
@@ -107,8 +108,10 @@ The default location for the configuration file to contain the settings for the
The default location for the db to contain the devices as they are added is "data/devices.db". If you would like a different filename or directory, specify `<directory>/<filename> explicitly.
#### UPNP IP Address
The server defaults to the first available address on the host if this is not given. This default may NOT be the correct IP that is your public IP for your host on the network. It is best to set this parameter to not have discovery issues. Replace this value with the server ipv4 address you would like to use as the address that any upnp device will call after discovery.
#### Web Server IP Address
The server defaults to all interfaces on the machine (0.0.0.0). Replace this value with the server ipv4 address you would like to use as the address that will bind to a specific ip address on an interface if you would like. This is only necessary if you want to isolate how access is handled to the web UI.
#### Web Server Port
The server defaults to running on port 8080. To override what the default is, specify a different number. ATTENTION: If you want to use any of the apps made for the Hue to control this bridge, you should set this port to 80.
The server defaults to running on port 80. To override what the default is, specify a different number. ATTENTION: If you want to use any of the apps made for the Hue to control this bridge, you should keep this port set to 80.
#### UPNP Response Port
The upnp response port that will be used. The default is 50000.
#### Vera Names and IP Addresses
@@ -176,7 +179,7 @@ http://192.168.1.1:8180/set/this/value/${intensity.percent}
PUT
http://192.168.1.1:8280/set/this
ContentBody: {"someValue":"${intensity..byte}"}
ContentBody: {"someValue":"${intensity.byte}"}
udp://192.168.1.1:5000/0x45${intensity.percent}55
@@ -272,6 +275,39 @@ DIM Commands |
To see what Alexa thinks you said, you can check in the home page for your Alexa.
To view or remove devices that Alexa knows about, you can use the mobile app `Menu / Settings / Connected Home` or go to http://echo.amazon.com/#cards.
## Google Assistant
Google Home is supported as of v3.2.0 and forward, but only if the bridge is running on port 80.
Use the Google Home app on a phone to add new "home control" devices by going into `Settings / Home Control / +`
as described [here](https://support.google.com/googlehome/answer/7124115?hl=en&ref_topic=7125624#homecontrol).
Click on `Philips Hue` under the `Add new` section. If ha-bridge is on the same network as the
phone as well as the Home device, then the app should quickly pass through the pairing step and
populate with all of the devices. If instead it takes you to a Philips Hue login page, this means
that the bridge was not properly discovered.
Then you can say "OK Google, Turn on the office light" or whatever name you have given your configured devices.
The Google Assistant can also group lights into rooms as described in the main [help article](https://support.google.com/googlehome/answer/7072090?hl=en&ref_topic=7029100).
Here is the table of items to use to tell Google what you want to do. Note that either "OK Google"
or "Hey Google" can be used as a trigger.
To do this: | Say "Hey Google", then...
------------|--------------------------
To turn on/off a light | "Turn on <light name>"
Dim a light | "Dim the <light name>"
Brighten a light | "Brighten the <light name>"
Set a light brightness to a certain percentage | "Set <light name> to 50%"
Dim/Brighten lights by a certain percentage | "Dim/Brighten <light name> by 50%"
Turn on/off all lights in room | “Turn on/off lights in <room name>"
Turn on/off all lights | “Turn on/off all of the lightsâ€<C3A2>
To see what Home thinks you said, you can ask "Hey Google, What did I say?" or check the history in the app.
New or removed devices are picked up automatically as soon as they are added/removed from ha-bridge.
No re-discovery step is necessary.
## 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:
```
@@ -282,7 +318,7 @@ These calls can be accomplished with a REST tool using the following URLs and HT
### Add a device
Add a new device to the HA Bridge configuration. There is a basic examples and then three alternate examples for the add. Please note that dimming is supported as well as custom value based on the dimming number given from the echo. This is under the Dimming and Value example.
```
POST http://host:8080/api/devices
POST http://host/api/devices
```
#### Body Arguments
Name | Type | Description | Required
@@ -932,22 +968,50 @@ ST: upnp:rootdevice\r\n
ST: ssdp:all\r\n
```
If this criteria is met, the following response is provided to the calling application:
If this criteria is met, the following three responses are provided to the calling application:
```
HTTP/1.1 200 OK\r\n
CACHE-CONTROL: max-age=86400\r\n
EXT:\r\n
LOCATION: http://192.168.1.1:8080/description.xml\r\n
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1\r\n
ST: urn:schemas-upnp-org:device:basic:1\r\n
"USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1\r\n\r\n
HTTP/1.1 200 OK
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age=100
EXT:
LOCATION: http://192.168.1.1:80/description.xml
SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0
hue-bridgeid: 001E06FFFE123456
ST: upnp:rootdevice
USN: uuid:2f402f80-da50-11e1-9b23-001e06123456::upnp:rootdevice
```
```
HTTP/1.1 200 OK
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age=100
EXT:
LOCATION: http://192.168.1.1:80/description.xml
SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0
hue-bridgeid: 001E06FFFE123456
ST: uuid:2f402f80-da50-11e1-9b23-001e06123456
USN: uuid:2f402f80-da50-11e1-9b23-001e06123456
```
```
HTTP/1.1 200 OK
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age=100
EXT:
LOCATION: http://192.168.1.1:80/description.xml
SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0
hue-bridgeid: 001E06FFFE123456
ST: urn:schemas-upnp-org:device:basic:1
USN: uuid:2f402f80-da50-11e1-9b23-001e06123456
```
Note that `192.168.1.1` and `12345` are replaced with the actual IP address and last 6 digits of the MAC address, respectively.
### UPNP description service
The bridge provides the description service which is used by the calling app to interogate access details after it has decided the upnp multicast response is the correct device.
#### Get Description
```
GET http://host:8080/description.xml
GET http://host:80/description.xml
```
#### Response
```
@@ -957,18 +1021,18 @@ GET http://host:8080/description.xml
<major>1</major>\n
<minor>0</minor>\n
</specVersion>\n
<URLBase>http://192.168.1.1:8080/</URLBase>\n
<URLBase>http://192.168.1.1:80/</URLBase>\n
<device>\n
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>\n
<friendlyName>HA-Bridge (192.168.1.1)</friendlyName>\n
<friendlyName>Philips hue (192.168.1.1)</friendlyName>\n
<manufacturer>Royal Philips Electronics</manufacturer>\n
<manufacturerURL>http://www.bwssystems.com</manufacturerURL>\n
<modelDescription>Hue Emulator for HA bridge</modelDescription>\n
<modelName>Philips hue bridge 2012</modelName>\n
<modelNumber>929000226503</modelNumber>\n
<modelURL>http://www.bwssystems.com/apps.html</modelURL>\n
<manufacturerURL>http://www.philips.com</manufacturerURL>\n
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>\n"
<modelName>Philips hue bridge 2015</modelName>\n
<modelNumber>BSB002</modelNumber>\n
<modelURL>http://www.meethue.com</modelURL>\n
<serialNumber>0017880ae670</serialNumber>\n
<UDN>uuid:88f6698f-2c83-4393-bd03-cd54a9f8595</UDN>\n
<UDN>uuid:2f402f80-da50-11e1-9b23-001788102201</UDN>\n
<serviceList>\n
<service>\n
<serviceType>(null)</serviceType>\n

160
kodivolume.md Normal file
View File

@@ -0,0 +1,160 @@
# Kodi volume control using dim commands & JSON-RPC
You can use HA Bridge to adjust the Kodi software volume output. This allows you to use dim commands and set the volume level with percentage values.
### What is JSON-RPC?
The short answer is JSON-RPC an interface to communicate with Kodi. [See the official Kodi Wiki for more info](http://kodi.wiki/view/JSON-RPC_API)
### Setup Kodi to allow control through JSON-RPC
In Kodi navigate to Settings/Services/Control ([screenshot](http://kodi.wiki/view/Settings/Services/Control))
Turn **ON** the following:
- Allow control of Kodi via HTTP
- Allow remote control from applications on this system
- Allow remote control from applications on other systems
Change the **username** to something unique and set a strong **password**.
Make a note of the **PORT**
### Adding the device to HA Bridge
Access the HA Bridge Configuration in your browser and open the **Manual Add** tab.
#### Name
Give the device a unique name that doesn<73>t include **<EFBFBD>volume<EFBFBD>** as it will cause conflicts with the Echo<68>s built in volume controls. A device name of **<EFBFBD>cody sound<6E>** works well.
#### Device type
Select **TCP** in the dropdown
### URLs
This section might seem a little long winded and if you know what you are doing then feel free to jump ahead.
#### Building the URL
We need to log into the Kodi web server without having to fill in the popup each time. You can do this by putting the username and password in the URL. It is not a good idea to do this on other websites as it does put your password in clear view, but for your local network it is fine.
Use the example below replacing the relevant sections with the details that you defined in the Kodi settings screen in the first step. Replacing the IP with the address of the machine that Kodi is running on.
```
http://KODI_USERNAME:KODI_PASSWORD@192.168.1.123:8080/jsonrpc
```
##### Testing the URL in a browser
Before you continue, open your custom URL in a browser (making sure Kodi is running). If all is working as it should you will see a big page of JSON that starts with:
``` json
{
"description": "JSON-RPC API of XBMC",
"id": "http://xbmc.org/jsonrpc/ServiceDescription.json",
```
If you don<6F>t see something that looks like the code above, then go back and double check your settings.
#### The JSON request
The URL is what connects you to Kodi, JSON is what is used to communicate with it. The JSON that is used to set the volume level is:
``` json
{"jsonrpc":"2.0","method":"Application.SetVolume","params":{"volume":100},"id":1}
```
##### Joining the URL and JSON
Join the two together by adding `?request=` to the end of the URL and then add the JSON to the end of the request. You will end up with something like:
```
http://KODI_USERNAME:KODI_PASSWORD@192.168.1.123:8080/jsonrpc?request={"jsonrpc":"2.0","method":"Application.SetVolume","params":{"volume":100},"id":1}
```
##### Testing the request in a browser
Go ahead and test the combined URL/JSON in a browser changing **100** to whatever level you want to set. Kodi should adjust the volume accordingly, try a few different levels to be sure it is working correctly.
The browser will reformat the URL each time you press return so don<6F>t build the URL in the browser bar without making a copy first.
Ideally build the URL in a text document which you can easily edit and then copy/paste each time.
#### Prepare the three URLs
You want to end up with three full URLs in your text file, one for each of the commands.
**ON**
```
http://KODI_USERNAME:KODI_PASSWORD@192.168.1.123:8080/jsonrpc?request={"jsonrpc":"2.0","method":"Application.SetVolume","params":{"volume":100},"id":1}
```
**DIM**
```
http://KODI_USERNAME:KODI_PASSWORD@192.168.1.123:8080/jsonrpc?request={"jsonrpc":"2.0","method":"Application.SetVolume","params":{"volume":45},"id":1}
```
**OFF**
```
http://KODI_USERNAME:KODI_PASSWORD@192.168.1.123:8080/jsonrpc?request={"jsonrpc":"2.0","method":"Application.SetVolume","params":{"volume":0},"id":1}
```
#### Encoding the JSON part
You should now be able to control the volume of Kodi using the structured URL you built above in a browser. If you can<61>t get it to work in a browser then you won<6F>t be able to get it to work in HA Bridge.
In order for HA Bridge to send the JSON request you need to encode the URL. This means we take this:
```
http://KODI_USERNAME:KODI_PASSWORD@192.168.1.123:8080/jsonrpc?request={"jsonrpc":"2.0","method":"Application.SetVolume","params":{"volume":100},"id":1}
```
And turn it into this:
```
http://KODI_USERNAME:KODI_PASSWORD@192.168.1.123:8080/jsonrpc?request=%7B%22jsonrpc%22%3A%222.0%22%2C%22method%22%3A%22Application.SetVolume%22%2C%22params%22%3A%7B%22volume%22%3A100%7D%2C%22id%22%3A1%7D
```
Note that we are only encoding the JSON, the URL part we leave as is.
Using the online tool [www.url-encode-decode.com](http://www.url-encode-decode.com/), copy the JSON part (shown in bold below) and paste it into the left hand field.
http://KODI_USERNAME:KODI_PASSWORD@192.168.1.123:8080/jsonrpc?request=**{"jsonrpc":"2.0","method":"Application.SetVolume","params":{"volume":100},"id":1}**
Click the **Encode url** button.
Copy the output from the right hand box and paste it on a new line in your text file.
```
%7B%22jsonrpc%22%3A%222.0%22%2C%22method%22%3A%22Application.SetVolume%22%2C%22params%22%3A%7B%22volume%22%3A100%7D%2C%22id%22%3A1%7D
```
#### Join the URL and JSON
Finally you want to insert the URL part before the request (shown in bold below)
**http://KODI_USERNAME:KODI_PASSWORD@192.168.1.123:8080/jsonrpc?request=**%7B%22jsonrpc%22%3A%222.0%22%2C%22method%22%3A%22Application.SetVolume%22%2C%22params%22%3A%7B%22volume%22%3A100%7D%2C%22id%22%3A1%7D
### HA Bridge
Paste the final URL in **On URL** field. Just do the one for now, add the device and in the Bridge Control tab click Save.
Test the button in the Bridge Devices tab and hopefully it should turn the volume up in Kodi.
Go ahead and repeat the steps for the **Off URL**. All the same steps but change 100% to 0%. If you want to use the previous URL you can, just find the 100% in the encoded URL.
`<EFBFBD>%22%3A%7B%22volume%22%3A`**100**`%7D%2C%22id%22%3A1%7D`
and change it to 0
`<EFBFBD>%22%3A%7B%22volume%22%3A`**0**`%7D%2C%22id%22%3A1%7D`
#### Dim JSON
The Dim URL uses the `${intensity.percent}` to take the given number from your voice command and pass it to Kodi.
Here is the JSON to <20>Dim<69> the volume
``` json
{"jsonrpc":"2.0","method":"Application.SetVolume","params":{"volume":"${intensity.percent}""},"id":1}
```
You don<6F>t need to encode the `${intensity.percent}` part. This means you can simply replace the number value (100/0) with `${intensity.percent}` as shown below.
`<EFBFBD>%22%3A%7B%22volume%22%3A`**${intensity.percent}**`%7D%2C%22id%22%3A1%7D`
### Controlling the Device
You can use the commands as listed in the [README](https://github.com/bwssytems/ha-bridge#ask-alexa)
<EFBFBD>Set Cody Sound to 50 percent<6E>
<EFBFBD>Cody Sound to 70 percent<6E>
Remembering that <20>Turn on Cody Sound<6E> will set the volume to 100%, and <20>Turn off Cody Sound<6E> will mute.

View File

@@ -5,7 +5,7 @@
<groupId>com.bwssystems.HABridge</groupId>
<artifactId>ha-bridge</artifactId>
<version>3.1.0</version>
<version>3.2.2</version>
<packaging>jar</packaging>
<name>HA Bridge</name>
@@ -43,7 +43,7 @@
<dependency>
<groupId>com.github.bwssytems</groupId>
<artifactId>nest-controller</artifactId>
<version>1.0.8</version>
<version>1.0.9</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>

View File

@@ -31,6 +31,11 @@ public class BridgeSettings extends BackupHandler {
super();
bridgeControl = new BridgeControlDescriptor();
theBridgeSettings = new BridgeSettingsDescriptor();
String ipV6Stack = System.getProperty("ipV6Stack");
if(ipV6Stack == null || !ipV6Stack.equalsIgnoreCase("true")) {
System.setProperty("java.net.preferIPv4Stack" , "true");
}
}
public BridgeControlDescriptor getBridgeControl() {
return bridgeControl;
@@ -39,11 +44,9 @@ public class BridgeSettings extends BackupHandler {
return theBridgeSettings;
}
public void buildSettings() {
InetAddress address = null;
String addressString = null;
String theVeraAddress = null;
String theHarmonyAddress = null;
String configFileProperty = System.getProperty("config.file");
if(configFileProperty == null) {
Path filePath = Paths.get(Configuration.CONFIG_FILE);
@@ -109,34 +112,18 @@ public class BridgeSettings extends BackupHandler {
}
if(theBridgeSettings.getUpnpConfigAddress() == null || theBridgeSettings.getUpnpConfigAddress().equals("")) {
try {
log.info("Getting an IP address for this host....");
Enumeration<NetworkInterface> ifs = NetworkInterface.getNetworkInterfaces();
while (ifs.hasMoreElements() && addressString == null) {
NetworkInterface xface = ifs.nextElement();
Enumeration<InetAddress> addrs = xface.getInetAddresses();
String name = xface.getName();
int IPsPerNic = 0;
while (addrs.hasMoreElements() && IPsPerNic == 0) {
address = addrs.nextElement();
if (InetAddressUtils.isIPv4Address(address.getHostAddress())) {
log.debug(name + " ... has IPV4 addr " + address);
if(!name.equalsIgnoreCase(Configuration.LOOP_BACK_INTERFACE)|| !address.getHostAddress().equalsIgnoreCase(Configuration.LOOP_BACK_ADDRESS)) {
IPsPerNic++;
addressString = address.getHostAddress();
log.info("Adding " + addressString + " from interface " + name + " as our default upnp config address.");
}
}
}
}
} catch (SocketException e) {
log.error("Cannot get ip address of this host, Exiting with message: " + e.getMessage(), e);
return;
}
theBridgeSettings.setUpnpConfigAddress(addressString);
addressString = checkIpAddress(null, true);
if(addressString != null) {
theBridgeSettings.setUpnpConfigAddress(addressString);
log.info("Adding " + addressString + " as our default upnp config address.");
}
else
log.error("Cannot get ip address of this host.");
}
else {
addressString = checkIpAddress(theBridgeSettings.getUpnpConfigAddress(), false);
if(addressString == null)
log.warn("The upnp config address, " + theBridgeSettings.getUpnpConfigAddress() + ", does not match any known IP's on this host.");
}
if(theBridgeSettings.getUpnpResponsePort() == null)
@@ -258,4 +245,39 @@ public class BridgeSettings extends BackupHandler {
return content;
}
private String checkIpAddress(String ipAddress, boolean checkForLocalhost) {
Enumeration<NetworkInterface> ifs = null;
try {
ifs = NetworkInterface.getNetworkInterfaces();
} catch(SocketException e) {
log.error("checkIpAddress cannot get ip address of this host, Exiting with message: " + e.getMessage(), e);
return null;
}
String addressString = null;
InetAddress address = null;
while (ifs.hasMoreElements() && addressString == null) {
NetworkInterface xface = ifs.nextElement();
Enumeration<InetAddress> addrs = xface.getInetAddresses();
String name = xface.getName();
int IPsPerNic = 0;
while (addrs.hasMoreElements() && IPsPerNic == 0) {
address = addrs.nextElement();
if (InetAddressUtils.isIPv4Address(address.getHostAddress())) {
log.debug(name + " ... has IPV4 addr " + address);
if(checkForLocalhost && (!name.equalsIgnoreCase(Configuration.LOOP_BACK_INTERFACE) || !address.getHostAddress().equalsIgnoreCase(Configuration.LOOP_BACK_ADDRESS))) {
IPsPerNic++;
addressString = address.getHostAddress();
log.debug("checkIpAddress found " + addressString + " from interface " + name);
}
else if(ipAddress != null && ipAddress.equalsIgnoreCase(address.getHostAddress())){
addressString = ipAddress;
IPsPerNic++;
}
}
}
}
return addressString;
}
}

View File

@@ -32,6 +32,8 @@ public class BridgeSettingsDescriptor {
private boolean halconfigured;
private Map<String, WhitelistEntry> whitelist;
private boolean settingsChanged;
private String myechourl;
private String webaddress;
public BridgeSettingsDescriptor() {
super();
@@ -45,6 +47,8 @@ public class BridgeSettingsDescriptor {
this.farenheit = true;
this.whitelist = null;
this.settingsChanged = false;
this.myechourl = "echo.amazon.com/#cards";
this.webaddress = "0.0.0.0";
}
public String getUpnpConfigAddress() {
return upnpconfigaddress;
@@ -208,6 +212,18 @@ public class BridgeSettingsDescriptor {
public void setSettingsChanged(boolean settingsChanged) {
this.settingsChanged = settingsChanged;
}
public String getMyechourl() {
return myechourl;
}
public void setMyechourl(String myechourl) {
this.myechourl = myechourl;
}
public String getWebaddress() {
return webaddress;
}
public void setWebaddress(String webaddress) {
this.webaddress = webaddress;
}
public Boolean isValidVera() {
if(this.getVeraAddress() == null || this.getVeraAddress().getDevices().size() <= 0)
return false;

View File

@@ -6,7 +6,7 @@ public class Configuration {
public final static String DEFAULT_ADDRESS = "1.1.1.1";
public final static String LOOP_BACK_ADDRESS = "127.0.0.1";
public final static String LOOP_BACK_INTERFACE = "lo";
public final static String DEFAULT_WEB_PORT = "8080";
public final static String DEFAULT_WEB_PORT = "80";
public final static String DEFAULT_BUTTON_SLEEP = "100";
public static final int UPNP_DISCOVERY_PORT = 1900;
public static final String UPNP_MULTICAST_ADDRESS = "239.255.255.250";

View File

@@ -13,6 +13,7 @@ import com.bwssystems.NestBridge.NestHome;
import com.bwssystems.hal.HalHome;
import com.bwssystems.harmony.HarmonyHome;
import com.bwssystems.hue.HueHome;
import com.bwssystems.util.UDPDatagramSender;
public class HABridge {
@@ -39,6 +40,7 @@ public class HABridge {
HueHome hueHome;
HalHome halHome;
HueMulator theHueMulator;
UDPDatagramSender udpSender;
UpnpSettingsResource theSettingResponder;
UpnpListener theUpnpListener;
SystemControl theSystem;
@@ -52,9 +54,9 @@ public class HABridge {
bridgeSettings = new BridgeSettings();
while(!bridgeSettings.getBridgeControl().isStop()) {
bridgeSettings.buildSettings();
log.info("HA Bridge (v" + theVersion.getVersion() + ") initializing....");
log.info("HA Bridge initializing....");
// sparkjava config directive to set ip address for the web server to listen on
// ipAddress("0.0.0.0"); // not used
ipAddress(bridgeSettings.getBridgeSettingsDescriptor().getWebaddress());
// sparkjava config directive to set port for the web server to listen on
port(bridgeSettings.getBridgeSettingsDescriptor().getServerPort());
// sparkjava config directive to set html static file location for Jetty
@@ -72,29 +74,37 @@ public class HABridge {
halHome = new HalHome(bridgeSettings.getBridgeSettingsDescriptor());
// setup the class to handle the resource setup rest api
theResources = new DeviceResource(bridgeSettings.getBridgeSettingsDescriptor(), harmonyHome, nestHome, hueHome, halHome);
// setup the class to handle the hue emulator rest api
theHueMulator = new HueMulator(bridgeSettings.getBridgeSettingsDescriptor(), theResources.getDeviceRepository(), harmonyHome, nestHome, hueHome);
theHueMulator.setupServer();
// setup the class to handle the upnp response rest api
theSettingResponder = new UpnpSettingsResource(bridgeSettings.getBridgeSettingsDescriptor());
theSettingResponder.setupServer();
// wait for the sparkjava initialization of the rest api classes to be complete
awaitInitialization();
// start the upnp ssdp discovery listener
theUpnpListener = new UpnpListener(bridgeSettings.getBridgeSettingsDescriptor(), bridgeSettings.getBridgeControl());
if(theUpnpListener.startListening())
log.info("HA Bridge (v" + theVersion.getVersion() + ") reinitialization requessted....");
else
bridgeSettings.getBridgeControl().setStop(true);
if(bridgeSettings.getBridgeSettingsDescriptor().isSettingsChanged())
bridgeSettings.save(bridgeSettings.getBridgeSettingsDescriptor());
// setup the UDP Datagram socket to be used by the HueMulator and the upnpListener
udpSender = UDPDatagramSender.createUDPDatagramSender(bridgeSettings.getBridgeSettingsDescriptor().getUpnpResponsePort());
if(udpSender == null) {
bridgeSettings.getBridgeControl().setStop(true);
}
else {
// setup the class to handle the hue emulator rest api
theHueMulator = new HueMulator(bridgeSettings.getBridgeSettingsDescriptor(), theResources.getDeviceRepository(), harmonyHome, nestHome, hueHome, udpSender);
theHueMulator.setupServer();
// wait for the sparkjava initialization of the rest api classes to be complete
awaitInitialization();
// start the upnp ssdp discovery listener
theUpnpListener = new UpnpListener(bridgeSettings.getBridgeSettingsDescriptor(), bridgeSettings.getBridgeControl(), udpSender);
if(theUpnpListener.startListening())
log.info("HA Bridge (v" + theVersion.getVersion() + ") reinitialization requessted....");
else
bridgeSettings.getBridgeControl().setStop(true);
if(bridgeSettings.getBridgeSettingsDescriptor().isSettingsChanged())
bridgeSettings.save(bridgeSettings.getBridgeSettingsDescriptor());
}
bridgeSettings.getBridgeControl().setReinit(false);
stop();
nestHome.closeTheNest();
nestHome = null;
harmonyHome.shutdownHarmonyHubs();
harmonyHome = null;
udpSender.closeResponseSocket();
}
log.info("HA Bridge (v" + theVersion.getVersion() + ") exiting....");
System.exit(0);

View File

@@ -84,7 +84,7 @@ public class DeviceResponse {
response.setState(device.getDeviceState());
response.setName(device.getName());
response.setUniqueid(device.getId());
response.setUniqueid(device.getUniqueid());
response.setManufacturername("Philips");
response.setType("Dimmable light");
response.setModelid("LWB004");

View File

@@ -17,7 +17,7 @@ public class DeviceState {
private String colormode;
private boolean reachable;
private List<Double> xy;
private int transitiontime;
// private int transitiontime;
public boolean isOn() {
return on;
@@ -98,13 +98,13 @@ public class DeviceState {
public void setXy(List<Double> xy) {
this.xy = xy;
}
public int getTransitiontime() {
return transitiontime;
}
// public int getTransitiontime() {
// return transitiontime;
// }
public void setTransitiontime(int transitiontime) {
this.transitiontime = transitiontime;
}
// public void setTransitiontime(int transitiontime) {
// this.transitiontime = transitiontime;
// }
public static DeviceState createDeviceState() {
DeviceState newDeviceState = new DeviceState();

View File

@@ -1,5 +1,10 @@
package com.bwssystems.HABridge.api.hue;
import java.util.List;
import com.bwssystems.HABridge.dao.DeviceDescriptor;
import com.bwssystems.HABridge.dao.DeviceRepository;
public class GroupResponse {
private DeviceState action;
private String[] lights;
@@ -23,11 +28,17 @@ public class GroupResponse {
this.name = name;
}
public static GroupResponse createGroupResponse(String[] theLights) {
public static GroupResponse createGroupResponse(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("Lightset 0");
theResponse.setLights(theLights);
theResponse.setLights(theList);
return theResponse;
}
}

View File

@@ -40,11 +40,11 @@ public class HueConfig
SimpleDateFormat dateFormatGmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
dateFormatGmt.setTimeZone(TimeZone.getTimeZone("UTC"));
aConfig.setMac(HueConfig.getMacAddress(ipaddress));
aConfig.setApiversion("1.10.0");
aConfig.setApiversion("1.15.0");
aConfig.setPortalservices(false);
aConfig.setGateway(ipaddress);
aConfig.setSwversion("01028090");
aConfig.setLinkbutton(false);
aConfig.setSwversion("01035934");
aConfig.setLinkbutton(true);
aConfig.setIpaddress(ipaddress);
aConfig.setProxyport(0);
aConfig.setSwupdate(Swupdate.createSwupdate());
@@ -56,7 +56,7 @@ public class HueConfig
aConfig.setLocaltime(dateFormat.format(new Date()));
aConfig.setTimezone(TimeZone.getDefault().getID());
aConfig.setZigbeechannel("6");
aConfig.setBridgeid(HuePublicConfig.getBridgeIdFromMac(aConfig.getMac(), ipaddress));
aConfig.setBridgeid(HuePublicConfig.createConfig(name, ipaddress).getHueBridgeIdFromMac());
aConfig.setModelid("BSB002");
aConfig.setFactorynew(false);
aConfig.setReplacesbridgeid(null);

View File

@@ -1,13 +1,11 @@
package com.bwssystems.HABridge.api.hue;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.StringTokenizer;
import javax.xml.bind.DatatypeConverter;
public class HuePublicConfig
{
@@ -23,10 +21,10 @@ public class HuePublicConfig
public static HuePublicConfig createConfig(String name, String ipaddress) {
HuePublicConfig aConfig = new HuePublicConfig();
aConfig.setMac(HuePublicConfig.getMacAddress(ipaddress));
aConfig.setApiversion("1.10.0");
aConfig.setSwversion("01028090");
aConfig.setApiversion("1.15.0");
aConfig.setSwversion("01035934");
aConfig.setName(name);
aConfig.setBridgeid(HuePublicConfig.getBridgeIdFromMac(aConfig.getMac(), ipaddress));
aConfig.setBridgeid(aConfig.getHueBridgeIdFromMac());
aConfig.setModelid("BSB002");
aConfig.setFactorynew(false);
aConfig.setReplacesbridgeid(null);
@@ -67,23 +65,22 @@ public class HuePublicConfig
return sb.toString();
}
protected static String getBridgeIdFromMac(String macAddr, String ipAddr)
public String getSNUUIDFromMac()
{
StringTokenizer st = new StringTokenizer(macAddr, ":");
String bridgeId = "";
String port = null;
StringTokenizer st = new StringTokenizer(this.getMac(), ":");
String bridgeUUID = "";
while(st.hasMoreTokens()) {
bridgeId = bridgeId + st.nextToken();
bridgeUUID = bridgeUUID + st.nextToken();
}
if(ipAddr.contains(":")) {
port = ipAddr.substring(ipAddr.indexOf(":"));
BigInteger bigInt = BigInteger.valueOf(Integer.getInteger(port).intValue());
byte[] theBytes = bigInt.toByteArray();
bridgeId = bridgeId + DatatypeConverter.printHexBinary(theBytes);
}
else
bridgeId = bridgeId + "0800";
return bridgeId;
bridgeUUID = bridgeUUID.toLowerCase();
return bridgeUUID.toLowerCase();
}
protected String getHueBridgeIdFromMac()
{
String cleanMac = this.getSNUUIDFromMac();
String bridgeId = cleanMac.substring(0, 6) + "FFFE" + cleanMac.substring(6);
return bridgeId.toUpperCase();
}
public String getMac() {

View File

@@ -11,6 +11,9 @@ public class DeviceDescriptor{
@SerializedName("id")
@Expose
private String id;
@SerializedName("uniqueid")
@Expose
private String uniqueid;
@SerializedName("name")
@Expose
private String name;
@@ -50,6 +53,9 @@ public class DeviceDescriptor{
@SerializedName("contentBodyOff")
@Expose
private String contentBodyOff;
@SerializedName("contentBodyDim")
@Expose
private String contentBodyDim;
private DeviceState deviceState;
@@ -125,6 +131,14 @@ public class DeviceDescriptor{
this.id = id;
}
public String getUniqueid() {
return uniqueid;
}
public void setUniqueid(String uniqueid) {
this.uniqueid = uniqueid;
}
public String getHeaders() {
return headers;
}
@@ -165,6 +179,14 @@ public class DeviceDescriptor{
this.contentBodyOff = contentBodyOff;
}
public String getContentBodyDim() {
return contentBodyDim;
}
public void setContentBodyDim(String contentBodyDim) {
this.contentBodyDim = contentBodyDim;
}
public DeviceState getDeviceState() {
if(deviceState == null)
deviceState = DeviceState.createDeviceState();

View File

@@ -2,6 +2,7 @@ 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;
@@ -9,8 +10,11 @@ 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 java.util.Random;
import javax.xml.bind.DatatypeConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -29,7 +33,7 @@ public class DeviceRepository extends BackupHandler {
private Map<String, DeviceDescriptor> devices;
private Path repositoryPath;
private Gson gson;
final private Random random = new Random();
private Integer nextId;
private Logger log = LoggerFactory.getLogger(DeviceRepository.class);
public DeviceRepository(String deviceDb) {
@@ -41,6 +45,7 @@ public class DeviceRepository extends BackupHandler {
repositoryPath = null;
repositoryPath = Paths.get(deviceDb);
setupParams(repositoryPath, ".bk", "device.db-");
nextId = 0;
_loadRepository(repositoryPath);
}
@@ -57,6 +62,9 @@ public class DeviceRepository extends BackupHandler {
DeviceDescriptor list[] = gson.fromJson(jsonContent, DeviceDescriptor[].class);
for(int i = 0; i < list.length; i++) {
put(list[i].getId(), list[i]);
if(Integer.decode(list[i].getId()) > nextId) {
nextId = Integer.decode(list[i].getId());
}
}
}
}
@@ -84,8 +92,17 @@ public class DeviceRepository extends BackupHandler {
for(int i = 0; i < descriptors.length; i++) {
if(descriptors[i].getId() != null && descriptors[i].getId().length() > 0)
devices.remove(descriptors[i].getId());
else
descriptors[i].setId(String.valueOf(random.nextInt(Integer.MAX_VALUE)));
else {
nextId++;
descriptors[i].setId(String.valueOf(nextId));
}
if(descriptors[i].getUniqueid() == null || descriptors[i].getUniqueid().length() == 0) {
BigInteger bigInt = BigInteger.valueOf(Integer.decode(descriptors[i].getId()));
byte[] theBytes = bigInt.toByteArray();
String hexValue = DatatypeConverter.printHexBinary(theBytes);
descriptors[i].setUniqueid("00:17:88:5E:D3:" + hexValue + "-" + hexValue);
}
put(descriptors[i].getId(), descriptors[i]);
theNames = theNames + " " + descriptors[i].getName() + ", ";
}
@@ -94,6 +111,28 @@ public class DeviceRepository extends BackupHandler {
log.debug("Save device(s): " + theNames);
}
public void renumber() {
List<DeviceDescriptor> list = new ArrayList<DeviceDescriptor>(devices.values());
Iterator<DeviceDescriptor> deviceIterator = list.iterator();
Map<String, DeviceDescriptor> newdevices = new HashMap<String, DeviceDescriptor>();;
nextId = 0;
log.debug("Renumber devices.");
while(deviceIterator.hasNext()) {
nextId++;
DeviceDescriptor theDevice = deviceIterator.next();
theDevice.setId(String.valueOf(nextId));
BigInteger bigInt = BigInteger.valueOf(nextId);
byte[] theBytes = bigInt.toByteArray();
String hexValue = DatatypeConverter.printHexBinary(theBytes);
theDevice.setUniqueid("00:17:88:5E:D3:" + hexValue + "-" + hexValue);
newdevices.put(theDevice.getId(), theDevice);
}
devices = newdevices;
String jsonValue = gson.toJson(findAll());
repositoryWriter(jsonValue, repositoryPath);
}
public String delete(DeviceDescriptor aDescriptor) {
if (aDescriptor != null) {
devices.remove(aDescriptor.getId());

View File

@@ -8,6 +8,7 @@ import static spark.Spark.delete;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
@@ -280,6 +281,21 @@ public class DeviceResource {
return halHome.getDevices();
}, new JsonTransformer());
// http://ip_address:port/api/devices/exec/renumber CORS request
options(API_CONTEXT + "/exec/renumber", "application/json", (request, response) -> {
response.status(HttpStatus.SC_OK);
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.header("Access-Control-Allow-Methods", "POST");
response.header("Access-Control-Allow-Headers", request.headers("Access-Control-Request-Headers"));
response.header("Content-Type", "text/html; charset=utf-8");
return "";
});
post (API_CONTEXT + "/exec/renumber", "application/json", (request, response) -> {
log.debug("Renumber devices.");
deviceRepository.renumber();
return null;
}, new JsonTransformer());
get (API_CONTEXT + "/backup/available", "application/json", (request, response) -> {
log.debug("Get backup filenames");
response.status(HttpStatus.SC_OK);

View File

@@ -27,6 +27,7 @@ import com.bwssystems.hue.HueHome;
import com.bwssystems.hue.HueUtil;
import com.bwssystems.nest.controller.Nest;
import com.bwssystems.util.JsonTransformer;
import com.bwssystems.util.UDPDatagramSender;
import com.google.gson.Gson;
import net.java.dev.eval.Expression;
@@ -60,8 +61,6 @@ import java.io.DataOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.Charset;
@@ -99,12 +98,13 @@ public class HueMulator implements HueErrorStringSet {
private SSLConnectionSocketFactory sslsf;
private RequestConfig globalConfig;
private BridgeSettingsDescriptor bridgeSettings;
private UDPDatagramSender theUDPDatagramSender;
private byte[] sendData;
private String hueUser;
private String errorString;
public HueMulator(BridgeSettingsDescriptor theBridgeSettings, DeviceRepository aDeviceRepository, HarmonyHome theHarmonyHome, NestHome aNestHome, HueHome aHueHome){
public HueMulator(BridgeSettingsDescriptor theBridgeSettings, DeviceRepository aDeviceRepository, HarmonyHome theHarmonyHome, NestHome aNestHome, HueHome aHueHome, UDPDatagramSender aUdpDatagramSender) {
httpClient = HttpClients.createDefault();
// Trust own CA and all self-signed certs
sslcontext = SSLContexts.createDefault();
@@ -136,6 +136,7 @@ public class HueMulator implements HueErrorStringSet {
else
this.myHueHome = null;
bridgeSettings = theBridgeSettings;
theUDPDatagramSender = aUdpDatagramSender;
hueUser = null;
errorString = null;
}
@@ -173,14 +174,7 @@ public class HueMulator implements HueErrorStringSet {
}
if(groupId.equalsIgnoreCase("0")) {
List<DeviceDescriptor> deviceList = repository.findAll();
String[] theList = new String[deviceList.size()];
int i = 0;
for (DeviceDescriptor device : deviceList) {
theList[i] = device.getId();
i++;
}
GroupResponse theResponse = GroupResponse.createGroupResponse(theList);
GroupResponse theResponse = GroupResponse.createGroupResponse(repository.findAll());
return new Gson().toJson(theResponse, GroupResponse.class);
}
@@ -267,7 +261,7 @@ public class HueMulator implements HueErrorStringSet {
if(bridgeSettings.isTraceupnp())
log.info("Traceupnp: hue lights list requested: " + userId + " from " + request.ip());
log.debug("hue lights list requested: " + userId + " from " + request.ip());
response.type("application/json; charset=utf-8");
response.type("application/json");
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.status(HttpStatus.SC_OK);
if(validateWhitelistUser(userId, false) == null) {
@@ -280,7 +274,41 @@ public class HueMulator implements HueErrorStringSet {
List<DeviceDescriptor> deviceList = repository.findAll();
Map<String, DeviceResponse> deviceResponseMap = new HashMap<>();
for (DeviceDescriptor device : deviceList) {
DeviceResponse deviceResponse = DeviceResponse.createResponse(device);
DeviceResponse deviceResponse = null;
String responseString;
if((device.getMapType() != null && device.getMapType().equalsIgnoreCase("hueDevice"))) {
HueDeviceIdentifier deviceId = new Gson().fromJson(device.getOnUrl(), HueDeviceIdentifier.class);
if(myHueHome.getTheHUERegisteredUser() == null) {
hueUser = HueUtil.registerWithHue(httpClient, deviceId.getIpAddress(), device.getName(), myHueHome.getTheHUERegisteredUser(), this);
if(hueUser == null) {
return errorString;
}
myHueHome.setTheHUERegisteredUser(hueUser);
}
// make call
responseString = doHttpRequest("http://"+deviceId.getIpAddress()+"/api/"+myHueHome.getTheHUERegisteredUser()+"/lights/"+deviceId.getDeviceId(), HttpGet.METHOD_NAME, device.getContentType(), null, null);
if (responseString == null) {
log.warn("Error on calling hue device to get state: " + device.getName());
deviceResponse = DeviceResponse.createResponse(device);
}
else if(responseString.contains("[{\"error\":") && responseString.contains("unauthorized user")) {
myHueHome.setTheHUERegisteredUser(null);
hueUser = HueUtil.registerWithHue(httpClient, deviceId.getIpAddress(), device.getName(), myHueHome.getTheHUERegisteredUser(), this);
if(hueUser == null) {
return errorString;
}
myHueHome.setTheHUERegisteredUser(hueUser);
deviceResponse = DeviceResponse.createResponse(device);
}
else {
deviceResponse = new Gson().fromJson(responseString, DeviceResponse.class);
if(deviceResponse == null)
deviceResponse = DeviceResponse.createResponse(device);
}
}
else
deviceResponse = DeviceResponse.createResponse(device);
deviceResponseMap.put(device.getId(), deviceResponse);
}
return deviceResponseMap;
@@ -292,7 +320,7 @@ public class HueMulator implements HueErrorStringSet {
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.header("Access-Control-Allow-Methods", "GET, POST, PUT");
response.header("Access-Control-Allow-Headers", request.headers("Access-Control-Request-Headers"));
response.header("Content-Type", "text/html; charset=utf-8");
response.header("Content-Type", "text/html");
return "";
});
// http://ip_address:port/api with body of user request returns json object for a success of user add
@@ -321,7 +349,7 @@ public class HueMulator implements HueErrorStringSet {
log.debug("hue api user create requested for device type: " + aDeviceType + " and username: " + newUser);
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.type("application/json; charset=utf-8");
response.type("application/json");
response.status(HttpStatus.SC_OK);
return "[{\"success\":{\"username\":\"" + newUser + "\"}}]";
} );
@@ -332,7 +360,7 @@ public class HueMulator implements HueErrorStringSet {
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.header("Access-Control-Allow-Methods", "GET, POST, PUT");
response.header("Access-Control-Allow-Headers", request.headers("Access-Control-Request-Headers"));
response.header("Content-Type", "text/html; charset=utf-8");
response.header("Content-Type", "text/html");
return "";
});
// http://ip_address:port/api/* with body of user request returns json object for a success of user add - This method is for Harmony Hub
@@ -356,7 +384,7 @@ public class HueMulator implements HueErrorStringSet {
aDeviceType = "<not given>";
log.debug("HH trace: hue api user create requested for device type: " + aDeviceType + " and username: " + newUser);
response.type("application/json; charset=utf-8");
response.type("application/json");
response.status(HttpStatus.SC_OK);
return "[{\"success\":{\"username\":\"" + newUser + "\"}}]";
} );
@@ -368,7 +396,7 @@ public class HueMulator implements HueErrorStringSet {
log.debug("hue api public config requested, from " + request.ip());
HuePublicConfig apiResponse = HuePublicConfig.createConfig("Philips hue", bridgeSettings.getUpnpConfigAddress());
response.type("application/json; charset=utf-8");
response.type("application/json");
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.status(HttpStatus.SC_OK);
return apiResponse;
@@ -377,7 +405,7 @@ public class HueMulator implements HueErrorStringSet {
// http://ip_address:port/api/{userId}/config returns json objects for the config
get(HUE_CONTEXT + "/:userid/config", "application/json", (request, response) -> {
String userId = request.params(":userid");
response.type("application/json; charset=utf-8");
response.type("application/json");
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.status(HttpStatus.SC_OK);
if(bridgeSettings.isTraceupnp())
@@ -399,7 +427,7 @@ public class HueMulator implements HueErrorStringSet {
get(HUE_CONTEXT + "/:userid", "application/json", (request, response) -> {
String userId = request.params(":userid");
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.type("application/json; charset=utf-8");
response.type("application/json");
response.status(HttpStatus.SC_OK);
log.debug("hue api full state requested: " + userId + " from " + request.ip());
if(validateWhitelistUser(userId, false) == null) {
@@ -409,9 +437,9 @@ public class HueMulator implements HueErrorStringSet {
return theErrorResp.getTheErrors();
}
List<DeviceDescriptor> descriptorList = repository.findAll();
HueApiResponse apiResponse = new HueApiResponse("Philips hue", bridgeSettings.getUpnpConfigAddress(), bridgeSettings.getWhitelist());
Map<String, DeviceResponse> deviceList = new HashMap<>();
List<DeviceDescriptor> descriptorList = repository.findAll();
if (descriptorList != null) {
descriptorList.forEach(descriptor -> {
DeviceResponse deviceResponse = DeviceResponse.createResponse(descriptor);
@@ -429,7 +457,7 @@ public class HueMulator implements HueErrorStringSet {
String userId = request.params(":userid");
String lightId = request.params(":id");
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.type("application/json; charset=utf-8");
response.type("application/json");
response.status(HttpStatus.SC_OK);
log.debug("hue light requested: " + lightId + " for user: " + userId + " from " + request.ip());
if(validateWhitelistUser(userId, false) == null) {
@@ -448,7 +476,41 @@ public class HueMulator implements HueErrorStringSet {
} else {
log.debug("found device named: " + device.getName());
}
DeviceResponse lightResponse = DeviceResponse.createResponse(device);
DeviceResponse lightResponse = null;
String responseString;
if((device.getMapType() != null && device.getMapType().equalsIgnoreCase("hueDevice"))) {
HueDeviceIdentifier deviceId = new Gson().fromJson(device.getOnUrl(), HueDeviceIdentifier.class);
if(myHueHome.getTheHUERegisteredUser() == null) {
hueUser = HueUtil.registerWithHue(httpClient, deviceId.getIpAddress(), device.getName(), myHueHome.getTheHUERegisteredUser(), this);
if(hueUser == null) {
return errorString;
}
myHueHome.setTheHUERegisteredUser(hueUser);
}
// make call
responseString = doHttpRequest("http://"+deviceId.getIpAddress()+"/api/"+myHueHome.getTheHUERegisteredUser()+"/lights/"+deviceId.getDeviceId(), HttpGet.METHOD_NAME, device.getContentType(), null, null);
if (responseString == null) {
log.warn("Error on calling hue device to get state: " + device.getName());
lightResponse = DeviceResponse.createResponse(device);
}
else if(responseString.contains("[{\"error\":") && responseString.contains("unauthorized user")) {
myHueHome.setTheHUERegisteredUser(null);
hueUser = HueUtil.registerWithHue(httpClient, deviceId.getIpAddress(), device.getName(), myHueHome.getTheHUERegisteredUser(), this);
if(hueUser == null) {
return errorString;
}
myHueHome.setTheHUERegisteredUser(hueUser);
lightResponse = DeviceResponse.createResponse(device);
}
else {
lightResponse = new Gson().fromJson(responseString, DeviceResponse.class);
if(lightResponse == null)
lightResponse = DeviceResponse.createResponse(device);
}
}
else
lightResponse = DeviceResponse.createResponse(device);
return lightResponse;
}, new JsonTransformer());
@@ -459,7 +521,7 @@ public class HueMulator implements HueErrorStringSet {
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.header("Access-Control-Allow-Methods", "GET, POST, PUT");
response.header("Access-Control-Allow-Headers", request.headers("Access-Control-Request-Headers"));
response.header("Content-Type", "text/html; charset=utf-8");
response.header("Content-Type", "text/html");
return "";
});
// http://ip_address:port/api/{userId}/lights/{lightId}/bridgeupdatestate uses json object to update the internal bridge lights state.
@@ -474,7 +536,7 @@ public class HueMulator implements HueErrorStringSet {
boolean stateHasBriInc = false;
log.debug("Update state requested: " + userId + " from " + request.ip() + " body: " + request.body());
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.type("application/json; charset=utf-8");
response.type("application/json");
response.status(HttpStatus.SC_OK);
if(validateWhitelistUser(userId, false) == null) {
log.debug("Valudate user, No User supplied");
@@ -490,8 +552,12 @@ public class HueMulator implements HueErrorStringSet {
return responseString;
}
if (request.body().contains("\"bri\""))
stateHasBri = true;
if (request.body().contains("\"bri\"")) {
if(theStateChanges.isOn() && theStateChanges.getBri() == 0)
stateHasBri = false;
else
stateHasBri = true;
}
if (request.body().contains("\"bri_inc\""))
stateHasBriInc = true;
@@ -539,7 +605,7 @@ public class HueMulator implements HueErrorStringSet {
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.header("Access-Control-Allow-Methods", "GET, POST, PUT");
response.header("Access-Control-Allow-Headers", request.headers("Access-Control-Request-Headers"));
response.header("Content-Type", "text/html; charset=utf-8");
response.header("Content-Type", "text/html");
return "";
});
// http://ip_address:port/api/{userId}/lights/{lightId}/state uses json object to set the lights state
@@ -559,7 +625,7 @@ public class HueMulator implements HueErrorStringSet {
boolean stateHasBriInc = false;
log.debug("hue state change requested: " + userId + " from " + request.ip() + " body: " + request.body());
response.header("Access-Control-Allow-Origin", request.headers("Origin"));
response.type("application/json; charset=utf-8");
response.type("application/json");
response.status(HttpStatus.SC_OK);
if(validateWhitelistUser(userId, false) == null) {
log.debug("Valudate user, No User supplied");
@@ -576,8 +642,12 @@ public class HueMulator implements HueErrorStringSet {
return responseString;
}
if (request.body().contains("\"bri\""))
stateHasBri = true;
if (request.body().contains("\"bri\"")) {
if(theStateChanges.isOn() && theStateChanges.getBri() == 0)
stateHasBri = false;
else
stateHasBri = true;
}
if (request.body().contains("\"bri_inc\""))
stateHasBriInc = true;
@@ -836,10 +906,7 @@ public class HueMulator implements HueErrorStringSet {
}
if(callItems[i].getItem().contains("udp://")) {
log.debug("executing HUE api request to UDP: " + callItems[i].getItem());
DatagramSocket responseSocket = new DatagramSocket(Integer.parseInt(port));
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, Integer.parseInt(port));
responseSocket.send(sendPacket);
responseSocket.close();
theUDPDatagramSender.sendUDPResponse(new String(sendData), IPAddress, Integer.parseInt(port));
}
else if(callItems[i].getItem().contains("tcp://"))
{
@@ -864,7 +931,9 @@ public class HueMulator implements HueErrorStringSet {
String anUrl = replaceIntensityValue(callItems[i].getItem(), calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), false);
String body;
if (state.isOn())
if(stateHasBri || stateHasBriInc)
body = replaceIntensityValue(device.getContentBodyDim(), calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), false);
else if (state.isOn())
body = replaceIntensityValue(device.getContentBody(), calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), false);
else
body = replaceIntensityValue(device.getContentBodyOff(), calculateIntensity(state, theStateChanges, stateHasBri, stateHasBriInc), false);
@@ -974,33 +1043,39 @@ public class HueMulator implements HueErrorStringSet {
protected String doHttpRequest(String url, String httpVerb, String contentType, String body, NameValue[] headers) {
HttpUriRequest request = null;
String theContent = null;
ContentType parsedContentType = null;
StringEntity requestBody = null;
if(contentType != null && contentType.length() > 0) {
parsedContentType = ContentType.parse(contentType);
if(body != null && body.length() > 0)
requestBody = new StringEntity(body, parsedContentType);
}
try {
if(HttpGet.METHOD_NAME.equalsIgnoreCase(httpVerb) || httpVerb == null) {
request = new HttpGet(url);
}else if(HttpPost.METHOD_NAME.equalsIgnoreCase(httpVerb)){
HttpPost postRequest = new HttpPost(url);
ContentType parsedContentType = ContentType.parse(contentType);
StringEntity requestBody = new StringEntity(body, parsedContentType);
postRequest.setEntity(requestBody);
if(requestBody != null)
postRequest.setEntity(requestBody);
request = postRequest;
}else if(HttpPut.METHOD_NAME.equalsIgnoreCase(httpVerb)){
HttpPut putRequest = new HttpPut(url);
ContentType parsedContentType = ContentType.parse(contentType);
StringEntity requestBody = new StringEntity(body, parsedContentType);
putRequest.setEntity(requestBody);
if(requestBody != null)
putRequest.setEntity(requestBody);
request = putRequest;
}
} catch(IllegalArgumentException e) {
log.warn("Error calling out to HA gateway: IllegalArgumentException in log", e);
log.warn("Error creating outbound http request: IllegalArgumentException in log", e);
return null;
}
log.debug("Making outbound call in doHttpRequest: " + request);
if(headers != null && headers.length > 0) {
for(int i = 0; i < headers.length; i++) {
request.setHeader(headers[i].getName(), headers[i].getValue());
}
}
try {
if(headers != null && headers.length > 0) {
for(int i = 0; i < headers.length; i++) {
request.setHeader(headers[i].getName(), headers[i].getValue());
}
}
HttpResponse response;
if(url.startsWith("https"))
response = httpclientSSL.execute(request);
@@ -1025,19 +1100,30 @@ public class HueMulator implements HueErrorStringSet {
return theContent;
}
private String doExecRequest(String anItem, int intensity, String lightId) {
private String doExecRequest(String anItem, int intensity, String lightId) {
log.debug("Executing request: " + anItem);
String responseString = null;
try {
Process p = Runtime.getRuntime().exec(replaceIntensityValue(anItem, intensity, false));
log.debug("Process running: " + p.isAlive());
} catch (IOException e) {
log.warn("Could not execute request: " + anItem, e);
responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId + "\",\"description\": \"Error on calling out to device\", \"parameter\": \"/lights/" + lightId + "state\"}}]";
}
return responseString;
}
String responseString = null;
if(anItem != null && !anItem.equalsIgnoreCase("")) {
try {
Process p = Runtime.getRuntime().exec(replaceIntensityValue(anItem, intensity, false));
log.debug("Process running: " + p.isAlive());
} catch (IOException e) {
log.warn("Could not execute request: " + anItem, e);
responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId
+ "\",\"description\": \"Error on calling out to device\", \"parameter\": \"/lights/" + lightId
+ "state\"}}]";
}
}
else {
log.warn("Could not execute request. Request is empty.");
responseString = "[{\"error\":{\"type\": 6, \"address\": \"/lights/" + lightId
+ "\",\"description\": \"Error on calling out to device\", \"parameter\": \"/lights/" + lightId
+ "state\"}}]";
}
return responseString;
}
private String formatSuccessHueResponse(StateChangeBody state, String body, String lightId, DeviceState deviceState) {
String responseString = "[";
@@ -1169,8 +1255,8 @@ public class HueMulator implements HueErrorStringSet {
if(notFirstChange)
responseString = responseString + ",";
responseString = responseString + "{\"success\":{\"/lights/" + lightId + "/state/transitiontime\":" + state.getTransitiontime() + "}}";
if(deviceState != null)
deviceState.setTransitiontime(state.getTransitiontime());
// if(deviceState != null)
// deviceState.setTransitiontime(state.getTransitiontime());
notFirstChange = true;
}
@@ -1202,7 +1288,7 @@ public class HueMulator implements HueErrorStringSet {
private String validateWhitelistUser(String aUser, boolean strict) {
if(aUser == null ||aUser.equalsIgnoreCase("undefined") || aUser.equalsIgnoreCase("null") || aUser.equalsIgnoreCase(""))
return null;
String validUser = null;
boolean found = false;
if(bridgeSettings.getWhitelist() != null) {

View File

@@ -6,6 +6,8 @@ import org.slf4j.LoggerFactory;
import com.bwssystems.HABridge.BridgeControlDescriptor;
import com.bwssystems.HABridge.BridgeSettingsDescriptor;
import com.bwssystems.HABridge.Configuration;
import com.bwssystems.HABridge.api.hue.HuePublicConfig;
import com.bwssystems.util.UDPDatagramSender;
import java.io.IOException;
import java.net.*;
@@ -16,69 +18,56 @@ import org.apache.http.conn.util.*;
public class UpnpListener {
private Logger log = LoggerFactory.getLogger(UpnpListener.class);
private int upnpResponsePort;
private UDPDatagramSender theUDPDatagramSender;
private int httpServerPort;
private String responseAddress;
private boolean strict;
private boolean traceupnp;
private BridgeControlDescriptor bridgeControl;
private boolean discoveryTemplateLatest;
private String discoveryTemplate = "HTTP/1.1 200 OK\r\n" +
"CACHE-CONTROL: max-age=86400\r\n" +
private String responseTemplate1 = "HTTP/1.1 200 OK\r\n" +
"HOST: %s:%s\r\n" +
"CACHE-CONTROL: max-age=100\r\n" +
"EXT:\r\n" +
"LOCATION: http://%s:%s/description.xml\r\n" +
"SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.10.0\r\n" +
"ST: urn:schemas-upnp-org:device:basic:1\r\n" +
"USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1\r\n\r\n";
private String discoveryTemplateOld = "HTTP/1.1 200 OK\r\n" +
"CACHE-CONTROL: max-age=86400\r\n" +
"SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0\r\n" +
"hue-bridgeid: %s\r\n" +
"ST: upnp:rootdevice\r\n" +
"USN: uuid:2f402f80-da50-11e1-9b23-%s::upnp:rootdevice\r\n\r\n";
private String responseTemplate2 = "HTTP/1.1 200 OK\r\n" +
"HOST: %s:%s\r\n" +
"CACHE-CONTROL: max-age=100\r\n" +
"EXT:\r\n" +
"LOCATION: http://%s:%s/description.xml\r\n" +
"SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1\r\n" +
"SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0\r\n" +
"hue-bridgeid: %s\r\n" +
"ST: uuid:2f402f80-da50-11e1-9b23-%s\r\n" +
"USN: uuid:2f402f80-da50-11e1-9b23-%s\r\n\r\n";
private String responseTemplate3 = "HTTP/1.1 200 OK\r\n" +
"HOST: %s:%s\r\n" +
"CACHE-CONTROL: max-age=100\r\n" +
"EXT:\r\n" +
"LOCATION: http://%s:%s/description.xml\r\n" +
"SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.15.0\r\n" +
"hue-bridgeid: %s\r\n" +
"ST: urn:schemas-upnp-org:device:basic:1\r\n" +
"USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1\r\n\r\n";
public UpnpListener(BridgeSettingsDescriptor theSettings, BridgeControlDescriptor theControl) {
"USN: uuid:2f402f80-da50-11e1-9b23-%s\r\n\r\n";
public UpnpListener(BridgeSettingsDescriptor theSettings, BridgeControlDescriptor theControl, UDPDatagramSender aUdpDatagramSender) {
super();
upnpResponsePort = theSettings.getUpnpResponsePort();
theUDPDatagramSender = aUdpDatagramSender;
httpServerPort = Integer.valueOf(theSettings.getServerPort());
responseAddress = theSettings.getUpnpConfigAddress();
strict = theSettings.isUpnpStrict();
traceupnp = theSettings.isTraceupnp();
bridgeControl = theControl;
discoveryTemplateLatest = true;
}
@SuppressWarnings("resource")
public boolean startListening(){
log.info("UPNP Discovery Listener starting....");
DatagramSocket responseSocket = null;
MulticastSocket upnpMulticastSocket = null;
Enumeration<NetworkInterface> ifs = null;
boolean portLoopControl = true;
int retryCount = 0;
while(portLoopControl) {
try {
responseSocket = new DatagramSocket(upnpResponsePort);
if(retryCount > 0)
log.info("Upnp Response Port issue, found open port: " + upnpResponsePort);
portLoopControl = false;
} catch(SocketException e) {
if(retryCount == 0)
log.warn("Upnp Response Port is in use, starting loop to find open port for 20 tries - configured port is: " + upnpResponsePort);
if(retryCount >= 20) {
portLoopControl = false;
log.error("Upnp Response Port issue, could not find open port - last port tried: " + upnpResponsePort + " with message: " + e.getMessage());
return false;
}
}
if(portLoopControl) {
retryCount++;
upnpResponsePort++;
}
}
try {
upnpMulticastSocket = new MulticastSocket(Configuration.UPNP_DISCOVERY_PORT);
} catch(IOException e){
@@ -135,10 +124,10 @@ public class UpnpListener {
upnpMulticastSocket.receive(packet);
if (isSSDPDiscovery(packet)) {
try {
sendUpnpResponse(responseSocket, packet.getAddress(), packet.getPort());
sendUpnpResponse(packet.getAddress(), packet.getPort());
} catch (IOException e) {
log.error("UpnpListener encountered an error sending upnp response packet. Shutting down", e);
error = true;
log.warn("UpnpListener encountered an error sending upnp response packet. IP: " + packet.getAddress().getHostAddress() + " with message: " + e.getMessage());
log.debug("UpnpListener send upnp exception: ", e);
}
}
} catch (IOException e) {
@@ -155,7 +144,6 @@ public class UpnpListener {
}
}
upnpMulticastSocket.close();
responseSocket.close();
if (bridgeControl.isReinit())
log.info("UPNP Discovery Listener - ended, restart found");
if (bridgeControl.isStop())
@@ -184,7 +172,8 @@ public class UpnpListener {
log.info("Traceupnp: isSSDPDiscovery found message to be valid under strict rules - strict: " + strict);
log.info("Traceupnp: SSDP packet from " + packet.getAddress().getHostAddress() + ":" + packet.getPort() + ", body: " + packetString);
}
log.debug("isSSDPDiscovery found message to be valid under strict rules - strict: " + strict);
else
log.debug("isSSDPDiscovery found message to be valid under strict rules - strict: " + strict);
return true;
}
else if (!strict)
@@ -194,7 +183,8 @@ public class UpnpListener {
log.info("Traceupnp: isSSDPDiscovery found message to be valid under loose rules - strict: " + strict);
log.info("Traceupnp: SSDP packet from " + packet.getAddress().getHostAddress() + ":" + packet.getPort() + ", body: " + packetString);
}
log.debug("isSSDPDiscovery found message to be valid under loose rules - strict: " + strict);
else
log.debug("isSSDPDiscovery found message to be valid under loose rules - strict: " + strict);
return true;
}
}
@@ -205,17 +195,35 @@ public class UpnpListener {
return false;
}
protected void sendUpnpResponse(DatagramSocket socket, InetAddress requester, int sourcePort) throws IOException {
protected void sendUpnpResponse(InetAddress requester, int sourcePort) throws IOException {
String discoveryResponse = null;
if(discoveryTemplateLatest)
discoveryResponse = String.format(discoveryTemplate, responseAddress, httpServerPort);
String bridgeId = null;
String bridgeSNUUID = null;
HuePublicConfig aHueConfig = HuePublicConfig.createConfig("temp", responseAddress);
bridgeId = aHueConfig.getBridgeid();
bridgeSNUUID = aHueConfig.getSNUUIDFromMac();
discoveryResponse = String.format(responseTemplate1, Configuration.UPNP_MULTICAST_ADDRESS, Configuration.UPNP_DISCOVERY_PORT, responseAddress, httpServerPort, bridgeId, bridgeSNUUID);
if(traceupnp) {
log.info("Traceupnp: sendUpnpResponse discovery responseTemplate1 is <<<" + discoveryResponse + ">>>");
}
else
discoveryResponse = String.format(discoveryTemplateOld, Configuration.UPNP_MULTICAST_ADDRESS, Configuration.UPNP_DISCOVERY_PORT, responseAddress, httpServerPort);
if(traceupnp)
log.info("Traceupnp: sendUpnpResponse discovery template with address: " + responseAddress + " and port: " + httpServerPort);
log.debug("sendUpnpResponse discovery responseTemplate1 is <<<" + discoveryResponse + ">>>");
theUDPDatagramSender.sendUDPResponse(discoveryResponse, requester, sourcePort);
discoveryResponse = String.format(responseTemplate2, Configuration.UPNP_MULTICAST_ADDRESS, Configuration.UPNP_DISCOVERY_PORT, responseAddress, httpServerPort, bridgeId, bridgeSNUUID, bridgeSNUUID);
if(traceupnp) {
log.info("Traceupnp: sendUpnpResponse discovery responseTemplate2 is <<<" + discoveryResponse + ">>>");
}
else
log.debug("sendUpnpResponse discovery template with address: " + responseAddress + " and port: " + httpServerPort);
DatagramPacket response = new DatagramPacket(discoveryResponse.getBytes(), discoveryResponse.length(), requester, sourcePort);
socket.send(response);
log.debug("sendUpnpResponse discovery responseTemplate2 is <<<" + discoveryResponse + ">>>");
theUDPDatagramSender.sendUDPResponse(discoveryResponse, requester, sourcePort);
discoveryResponse = String.format(responseTemplate3, Configuration.UPNP_MULTICAST_ADDRESS, Configuration.UPNP_DISCOVERY_PORT, responseAddress, httpServerPort, bridgeId, bridgeSNUUID);
if(traceupnp) {
log.info("Traceupnp: sendUpnpResponse discovery responseTemplate3 is <<<" + discoveryResponse + ">>>");
}
else
log.debug("sendUpnpResponse discovery responseTemplate3 is <<<" + discoveryResponse + ">>>");
theUDPDatagramSender.sendUDPResponse(discoveryResponse, requester, sourcePort);
}
}

View File

@@ -4,6 +4,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.bwssystems.HABridge.BridgeSettingsDescriptor;
import com.bwssystems.HABridge.api.hue.HuePublicConfig;
import static spark.Spark.get;
@@ -31,8 +32,8 @@ public class UpnpSettingsResource {
+ "<modelName>Philips hue bridge 2015</modelName>\n"
+ "<modelNumber>BSB002</modelNumber>\n"
+ "<modelURL>http://www.meethue.com</modelURL>\n"
+ "<serialNumber>0017880ae670</serialNumber>\n"
+ "<UDN>uuid:2f402f80-da50-11e1-9b23-001788102201</UDN>\n"
+ "<serialNumber>%s</serialNumber>\n"
+ "<UDN>uuid:2f402f80-da50-11e1-9b23-%s</UDN>\n"
+ "<serviceList>\n"
+ "<service>\n"
+ "<serviceType>(null)</serviceType>\n"
@@ -77,7 +78,8 @@ public class UpnpSettingsResource {
log.debug("upnp device settings requested: " + " from " + request.ip() + ":" + request.port());
String portNumber = Integer.toString(request.port());
String filledTemplate = null;
filledTemplate = String.format(hueTemplate, theSettings.getUpnpConfigAddress(), portNumber, theSettings.getUpnpConfigAddress());
String bridgeIdMac = HuePublicConfig.createConfig("temp", theSettings.getUpnpConfigAddress()).getSNUUIDFromMac();
filledTemplate = String.format(hueTemplate, theSettings.getUpnpConfigAddress(), portNumber, theSettings.getUpnpConfigAddress(), bridgeIdMac, bridgeIdMac);
if(theSettings.isTraceupnp())
log.info("Traceupnp: upnp device settings template filled with address: " + theSettings.getUpnpConfigAddress() + " and port: " + portNumber);
else

View File

@@ -0,0 +1,72 @@
package com.bwssystems.util;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UDPDatagramSender {
private Logger log = LoggerFactory.getLogger(UDPDatagramSender.class);
private DatagramSocket responseSocket = null;
private int udpResponsePort;
public UDPDatagramSender() {
super();
udpResponsePort = 0;
}
public static UDPDatagramSender createUDPDatagramSender(int udpResponsePort) {
UDPDatagramSender aDatagramSender = new UDPDatagramSender();
if(aDatagramSender.initializeSocket(udpResponsePort))
return aDatagramSender;
else
return null;
}
private boolean initializeSocket(int port) {
log.info("Initializing UDP response Seocket...");
udpResponsePort = port;
boolean portLoopControl = true;
int retryCount = 0;
while(portLoopControl) {
try {
responseSocket = new DatagramSocket(udpResponsePort);
portLoopControl = false;
} catch(SocketException e) {
if(retryCount == 0)
log.warn("UDP Response Port is in use, starting loop to find open port for 20 tries - configured port is: " + udpResponsePort);
if(retryCount >= 20) {
portLoopControl = false;
log.error("UDP Response Port issue, could not find open port - last port tried: " + udpResponsePort + " with message: " + e.getMessage());
return false;
}
}
if(portLoopControl) {
retryCount++;
udpResponsePort++;
}
}
log.info("UDP response Seocket initialized to: " + udpResponsePort);
return true;
}
public int getUdpResponsePort() {
return udpResponsePort;
}
public void closeResponseSocket() {
responseSocket.close();
}
public void sendUDPResponse(String udpResponse, InetAddress requester, int sourcePort) throws IOException {
log.debug("Sending response string: <<<" + udpResponse + ">>>");
if(responseSocket == null)
throw new IOException("Socket not initialized");
DatagramPacket response = new DatagramPacket(udpResponse.getBytes(), udpResponse.length(), requester, sourcePort);
responseSocket.send(response);
}
}

View File

@@ -36,7 +36,7 @@
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="http://echo.amazon.com/#cards" target="_blank">My Echo</a></li>
<li><a href="http://{{bridge.settings.myechourl}}" target="_blank">My Echo</a></li>
<li><a href="https://github.com/bwssytems/ha-bridge/blob/master/README.md" target="_blank">Help</a></li>
<li class="dropdown">
<a id="dLabel" href="" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">About <span class="caret"></span></a>

View File

@@ -115,12 +115,24 @@ app.service('bridgeService', function ($http, $window, ngToast) {
);
};
this.renumberDevices = function () {
return $http.post(this.state.base + "/exec/renumber").then(
function (response) {
self.viewDevices();
},
function (error) {
self.displayError("Cannot renumber devices from habridge: ", error);
}
);
};
this.clearDevice = function () {
if(self.state.device == null)
self.state.device = [];
self.state.device.id = "";
self.state.device.mapType = null;
self.state.device.mapId = null;
self.state.device.uniqueid = null;
self.state.device.name = "";
self.state.device.onUrl = "";
self.state.device.dimUrl = "";
@@ -131,6 +143,7 @@ app.service('bridgeService', function ($http, $window, ngToast) {
self.state.device.httpVerb = null;
self.state.device.contentType = null;
self.state.device.contentBody = null;
self.state.device.contentBodyDim = null;
self.state.device.contentBodyOff = null;
self.state.olddevicename = "";
};
@@ -756,11 +769,7 @@ app.controller('ViewingController', function ($scope, $location, $http, $window,
(type == "off" && (bridgeService.aContainsB(device.offUrl, "${intensity.byte}") ||
bridgeService.aContainsB(device.offUrl, "${intensity.percent}") ||
bridgeService.aContainsB(device.offUrl, "${intensity.math("))) ||
(type == "dim" && (bridgeService.aContainsB(device.dimUrl, "${intensity.byte}") ||
bridgeService.aContainsB(device.dimUrl, "${intensity.percent}") ||
bridgeService.aContainsB(device.dimUrl, "${intensity.math(") ||
bridgeService.aContainsB(device.deviceType, "passthru") ||
bridgeService.aContainsB(device.mapType, "hueDevice"))))) {
(type == "dim"))) {
$scope.bridge.device = device;
$scope.bridge.type = type;
ngDialog.open({
@@ -784,6 +793,9 @@ app.controller('ViewingController', function ($scope, $location, $http, $window,
bridgeService.editDevice(device);
$location.path('/editdevice');
};
$scope.renumberDevices = function() {
bridgeService.renumberDevices();
};
$scope.backupDeviceDb = function (optionalbackupname) {
bridgeService.backupDeviceDb(optionalbackupname);
};
@@ -976,6 +988,7 @@ app.controller('VeraController', function ($scope, $location, $http, bridgeServi
httpVerb: $scope.device.httpVerb,
contentType: $scope.device.contentType,
contentBody: $scope.device.contentBody,
contentBodyDim: $scope.device.contentBodyDim,
contentBodyOff: $scope.device.contentBodyOff
};
}
@@ -1309,6 +1322,7 @@ app.controller('HueController', function ($scope, $location, $http, bridgeServic
httpVerb: $scope.device.httpVerb,
contentType: $scope.device.contentType,
contentBody: $scope.device.contentBody,
contentBodyDim: $scope.device.contentBodyDim,
contentBodyOff: $scope.device.contentBodyOff
};
}
@@ -1627,6 +1641,7 @@ app.controller('HalController', function ($scope, $location, $http, bridgeServic
httpVerb: $scope.device.httpVerb,
contentType: $scope.device.contentType,
contentBody: $scope.device.contentBody,
contentBodyDim: $scope.device.contentBodyDim,
contentBodyOff: $scope.device.contentBodyOff
};
}

View File

@@ -25,6 +25,11 @@
<h2 class="panel-title">Current devices
({{bridge.devices.length}})</h2>
</div>
<form name="form">
<p>
<button class="btn btn-primary" type="submit" ng-click="renumberDevices()">Renumber Devices</button>
</p>
</form>
<scrollable-table watch="bridge.devices">
<table class="table table-bordered table-striped table-hover">
<thead>

View File

@@ -107,6 +107,14 @@
Clear Device</button>
</div>
</div>
<div class="form-group">
<label class="col-xs-12 col-sm-2 control-label" for="device-unique-id">Unique Id (used for Hue responses) </label>
<div class="col-xs-8 col-sm-7">
<input type="text" class="form-control" id="device-unique-id"
ng-model="device.uniqueid" placeholder="AA:BB:CC:DD:EE:FF-XX" readonly>
</div>
</div>
<div ng-if="device.mapType" class="form-group">
<label class="col-xs-12 col-sm-2 control-label" for="device-map-id">Map
ID </label>
@@ -218,6 +226,19 @@
<div class="clearfix visible-xs"></div>
</div>
</div>
<div ng-if="device.httpVerb" class="form-group">
<div class="row">
<label class="col-xs-12 col-sm-2 control-label"
for="device-content-body-dim">Content Body Dim</label>
<div class="col-xs-8 col-sm-7">
<textarea rows="3" class="form-control" id="device-content-body-dim"
ng-model="device.contentBodyDim"
placeholder="Content Body Dim for specific GET/PUT/POST type"></textarea>
</div>
<div class="clearfix visible-xs"></div>
</div>
</div>
<div ng-if="device.httpVerb" class="form-group">
<div class="row">
<label class="col-xs-12 col-sm-2 control-label"

View File

@@ -233,6 +233,19 @@
<div class="clearfix visible-xs"></div>
</div>
</div>
<div ng-if="device.httpVerb" class="form-group">
<div class="row">
<label class="col-xs-12 col-sm-2 control-label"
for="device-content-body-dim">Content Body Dim</label>
<div class="col-xs-8 col-sm-7">
<textarea rows="3" class="form-control" id="device-content-body-dim"
ng-model="device.contentBodyDim"
placeholder="Content Body Dim for specific GET/PUT/POST type"></textarea>
</div>
<div class="clearfix visible-xs"></div>
</div>
</div>
<div ng-if="device.httpVerb" class="form-group">
<div class="row">
<label class="col-xs-12 col-sm-2 control-label"

View File

@@ -82,6 +82,13 @@
ng-model="bridge.settings.upnpconfigaddress"
placeholder="192.168.1.1"></td>
</tr>
<tr>
<td>Web Server IP Address</td>
<td><input id="bridge-settings-webaddress"
class="form-control" type="text"
ng-model="bridge.settings.webaddress"
placeholder="0.0.0.0"></td>
</tr>
<tr>
<td>Web Server Port</td>
<td><input id="bridge-settings-serverport"
@@ -272,6 +279,12 @@
ng-model="bridge.settings.traceupnp" ng-true-value=true
ng-false-value=false> {{bridge.settings.traceupnp}}</td>
</tr>
<tr>
<td>My Echo URL</td>
<td><input id="bridge-settings-myechourl" class="form-control"
type="text" ng-model="bridge.settings.myechourl"
placeholder="echo.amazon.com/#cards"></td>
</tr>
</table>
</form>
</div>