diff --git a/Dockerfile b/Dockerfile index 5801fb6..4ffeab6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,4 +8,4 @@ RUN mvn install EXPOSE 8080 -CMD ["bash", "-c", "java -jar target/amazon-echo-bridge-*.jar --upnp.config.address=$(ip route get 8.8.8.8 | egrep -o '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\s*$')"] +CMD ["bash", "-c", "java -Xmx256M -jar target/amazon-echo-bridge-*.jar --upnp.config.address=$(ip route get 8.8.8.8 | egrep -o '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\s*$')"] diff --git a/README.md b/README.md old mode 100755 new mode 100644 index cbd2e10..884f24c --- a/README.md +++ b/README.md @@ -4,6 +4,17 @@ Amazon Echo Bridge allows you to quickly emulate a Phillips Hue bridge, bringing Also, with an easy to use POST/PUT REST API, it's never been easier before to get your devices up and running with the Amazon Echo with your own embedded applications! +## Release notes: +v0.4.0 +change log: + +* require --upnp.config.address= to be specified during startup +* support more than 25 emulated devices currently set to 75, can be increased at 25 device increments by specifying --emulator.portcount= default is 3 thus 3*25 = 75 total devices. Works by taking emulator.baseport and opening n number of ports sequentially from baseport to baseport+portcount +* relaxed http response codes to anything in the 200 to less than 300 http response codes to support misbehaving resources + +other notes: +Ive seen some folks able to run this but not able to discover devices. I would recommend checking for devices with duplicate names as i have seen this to cause the echo to reject all devices. The lazy way would be to delete the /data directory and start over. + ## Quick Start There are currently three different ways to run the pre-built jar file: @@ -24,8 +35,13 @@ After the application is started and running, you can access the configurator by Input your devices using the form at the bottom of the page, add command URLs to parse (useful if you use a system like OpenHAB), and save. -Instruct your Amazon Echo to take control of your devices by saying "Alexa, discover my devices" and your all set! +Instruct your Amazon Echo to learn about your devices by saying "Alexa, discover my devices" and your all set! + +## Using + +You can now control devices with your Amazon Echo by saying "Alexa, Turn on the office light" or other names you have given your configured devices. +To view or remove devices that Alexa knows about, you can use the mobile app Menu / Settings / Connected Home, This is needed if you remove a device from the Amazon Echo Bridge. ## Build @@ -49,7 +65,7 @@ To build the jar file yourself, make your changes and simply run Maven like this mvn install ``` -Then locate the jar and start the server using the instructions above. +Then locate the jar and start the server using the instructions above. By default maven will put the jar file in the target directory. ```java -jar target/amazon-echo-bridge-*.jar``` ## POST/PUT REST API diff --git a/pom.xml b/pom.xml old mode 100644 new mode 100755 index 574a7c8..6950b74 --- a/pom.xml +++ b/pom.xml @@ -1,67 +1,70 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - com.armzilla.ha - amazon-echo-bridge - 0.2.1 - jar + com.armzilla.ha + amazon-echo-bridge + 0.4.0 + jar - Amazon Echo Bridge - Emulates a Philips Hue bridge to allow the Amazon Echo to integrate seamlessly into various home automation systems. + Amazon Echo Bridge + Emulates a Philips Hue bridge to allow the Amazon Echo to integrate seamlessly into various home + automation systems. + - - org.springframework.boot - spring-boot-starter-parent - 1.2.3.RELEASE - - + + org.springframework.boot + spring-boot-starter-parent + 1.2.6.RELEASE + + + - - UTF-8 - com.armzilla.ha.SpringbootEntry - 1.8 - + + UTF-8 + com.armzilla.ha.SpringbootEntry + 1.8 + 4.5.1 + - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.data - spring-data-elasticsearch - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.apache.httpcomponents - httpclient - 4.3.6 - - - - - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-test + test + + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + com.h2database + h2 + runtime + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/src/main/java/com/armzilla/ha/ConfigChecker.java b/src/main/java/com/armzilla/ha/ConfigChecker.java new file mode 100644 index 0000000..4fb2a5f --- /dev/null +++ b/src/main/java/com/armzilla/ha/ConfigChecker.java @@ -0,0 +1,22 @@ +package com.armzilla.ha; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * Created by arm on 9/16/16. + */ +@Component +public class ConfigChecker implements InitializingBean { + + @Value("${upnp.config.address}") + private String responseAddress; + + @Override + public void afterPropertiesSet() throws Exception { + if(responseAddress == null || responseAddress.isEmpty() ){ + throw new IllegalArgumentException("please provide the IP(v4) address of the interface you want the bridge to listen on using --upnp.config.address="); + } + } +} diff --git a/src/main/java/com/armzilla/ha/SpringbootEntry.java b/src/main/java/com/armzilla/ha/SpringbootEntry.java index 304201e..c965ca5 100644 --- a/src/main/java/com/armzilla/ha/SpringbootEntry.java +++ b/src/main/java/com/armzilla/ha/SpringbootEntry.java @@ -2,15 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling -@ComponentScan public class SpringbootEntry { public static void main(String[] args) { SpringApplication.run(SpringbootEntry.class, args); } + } diff --git a/src/main/java/com/armzilla/ha/TomcatConnectorBean.java b/src/main/java/com/armzilla/ha/TomcatConnectorBean.java new file mode 100644 index 0000000..2fc132f --- /dev/null +++ b/src/main/java/com/armzilla/ha/TomcatConnectorBean.java @@ -0,0 +1,41 @@ +package com.armzilla.ha; + +import org.apache.catalina.connector.Connector; +import org.apache.coyote.http11.Http11NioProtocol; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; +import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * Created by arm on 9/12/15. + */ +@Component + +public class TomcatConnectorBean { + @Value("${emulator.portbase}") + private int portBase; + @Value("${emulator.portcount}") + private int portCount; + @Bean + public EmbeddedServletContainerFactory servletContainer() { + TomcatEmbeddedServletContainerFactory tomcat = null; + for(int i = 0; i < portCount; i ++) { + if(tomcat == null){ + tomcat = new TomcatEmbeddedServletContainerFactory(portBase + i); + }else{ + tomcat.addAdditionalTomcatConnectors(createConnector(portBase + i)); + } + } + return tomcat; + } + + private Connector createConnector(int portNumber) { + Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); + Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler(); + connector.setScheme("http"); + connector.setPort(portNumber); + return connector; + } +} diff --git a/src/main/java/com/armzilla/ha/api/hue/HueApiResponse.java b/src/main/java/com/armzilla/ha/api/hue/HueApiResponse.java index 67e360e..d7d996b 100644 --- a/src/main/java/com/armzilla/ha/api/hue/HueApiResponse.java +++ b/src/main/java/com/armzilla/ha/api/hue/HueApiResponse.java @@ -1,7 +1,5 @@ package com.armzilla.ha.api.hue; -import com.armzilla.ha.api.hue.DeviceResponse; - import java.util.Map; /** diff --git a/src/main/java/com/armzilla/ha/dao/DeviceDescriptor.java b/src/main/java/com/armzilla/ha/dao/DeviceDescriptor.java index 30e0fb5..d92e31f 100644 --- a/src/main/java/com/armzilla/ha/dao/DeviceDescriptor.java +++ b/src/main/java/com/armzilla/ha/dao/DeviceDescriptor.java @@ -1,12 +1,15 @@ package com.armzilla.ha.dao; -import org.springframework.data.annotation.Id; -import org.springframework.data.elasticsearch.annotations.Document; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; /** * Created by arm on 4/13/15. */ -@Document(indexName = "device", type = "devicedescriptor", shards = 1, replicas = 0, refreshInterval = "-1") +@Entity +@Table(name = "devices") + public class DeviceDescriptor{ @Id private String id; diff --git a/src/main/java/com/armzilla/ha/dao/DeviceRepository.java b/src/main/java/com/armzilla/ha/dao/DeviceRepository.java index 3b0a80d..83d1f48 100644 --- a/src/main/java/com/armzilla/ha/dao/DeviceRepository.java +++ b/src/main/java/com/armzilla/ha/dao/DeviceRepository.java @@ -1,16 +1,15 @@ package com.armzilla.ha.dao; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.data.repository.CrudRepository; import java.util.List; /** * Created by arm on 4/13/15. */ -public interface DeviceRepository extends ElasticsearchRepository { +public interface DeviceRepository extends CrudRepository { Page findByDeviceType(String type, Pageable request); List findAll(); DeviceDescriptor findOne(String id); diff --git a/src/main/java/com/armzilla/ha/hue/HueMulator.java b/src/main/java/com/armzilla/ha/hue/HueMulator.java index 35af355..5934c4c 100644 --- a/src/main/java/com/armzilla/ha/hue/HueMulator.java +++ b/src/main/java/com/armzilla/ha/hue/HueMulator.java @@ -19,6 +19,7 @@ import org.apache.http.util.EntityUtils; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.http.HttpHeaders; @@ -50,6 +51,10 @@ public class HueMulator { private HttpClient httpClient; private ObjectMapper mapper; + @Value("${emulator.portbase}") + private int portBase; + @Value("${emulator.portcount}") + private int portCount; public HueMulator(){ httpClient = HttpClients.createDefault(); //patched for now, moving away from HueMulator doing work @@ -60,8 +65,10 @@ public HueMulator(){ @RequestMapping(value = "/{userId}/lights", method = RequestMethod.GET, produces = "application/json") public ResponseEntity> getUpnpConfiguration(@PathVariable(value = "userId") String userId, HttpServletRequest request) { - log.info("hue lights list requested: " + userId + " from " + request.getRemoteAddr()); - Page deviceList = repository.findByDeviceType("switch", new PageRequest(0,100)); + log.info("hue lights list requested: " + userId + " from " + request.getRemoteAddr() + request.getLocalPort()); + + int pageNumber = request.getLocalPort()-portBase; + Page deviceList = repository.findByDeviceType("switch", new PageRequest(pageNumber, 25)); Map deviceResponseMap = new HashMap<>(); for (DeviceDescriptor device : deviceList) { deviceResponseMap.put(device.getId(), device.getName()); @@ -71,13 +78,15 @@ public ResponseEntity> getUpnpConfiguration(@PathVariable(va @RequestMapping(value = "/*", method = RequestMethod.POST, produces = "application/json") public ResponseEntity postAPI(HttpServletRequest request) { + log.info("registered device: " + request.toString()); return new ResponseEntity("[{\"success\":{\"username\":\"lights\"}}]", HttpStatus.OK); } @RequestMapping(value = "/{userId}", method = RequestMethod.GET, produces = "application/json") public ResponseEntity getApi(@PathVariable(value = "userId") String userId, HttpServletRequest request) { log.info("hue api root requested: " + userId + " from " + request.getRemoteAddr()); - Page descriptorList = repository.findByDeviceType("switch", new PageRequest(0, 100)); + int pageNumber = request.getLocalPort()-portBase; + Page descriptorList = repository.findByDeviceType("switch", new PageRequest(pageNumber, 25)); if (descriptorList == null) { return new ResponseEntity<>(null, null, HttpStatus.NOT_FOUND); } @@ -201,8 +210,9 @@ protected boolean doHttpRequest(String url, String httpVerb, String contentType, try { HttpResponse response = httpClient.execute(request); EntityUtils.consume(response.getEntity()); //close out inputstream ignore content - log.info("GET on URL responded: " + response.getStatusLine().getStatusCode()); - if(response.getStatusLine().getStatusCode() == 200){ + int httpResponseCode = response.getStatusLine().getStatusCode(); + log.info("GET on URL responded: " + httpResponseCode); + if(httpResponseCode >= 200 && httpResponseCode < 300){ //had complaints that some apps do not respond back with pure 200 return true; } } catch (IOException e) { diff --git a/src/main/java/com/armzilla/ha/upnp/UpnpListener.java b/src/main/java/com/armzilla/ha/upnp/UpnpListener.java index 00e59ec..802ff4b 100644 --- a/src/main/java/com/armzilla/ha/upnp/UpnpListener.java +++ b/src/main/java/com/armzilla/ha/upnp/UpnpListener.java @@ -26,17 +26,27 @@ public class UpnpListener { @Value("${upnp.response.port}") private int upnpResponsePort; - @Value("${server.port}") - private int httpServerPort; - @Value("${upnp.config.address}") private String responseAddress; + @Value("${emulator.portbase}") + private int portBase; + @Value("${emulator.portcount}") + private int portCount; + + @Value("${upnp.disable}") + private boolean disable; + @Autowired private ApplicationContext applicationContext; @Scheduled(fixedDelay = Integer.MAX_VALUE) public void startListening(){ + + if (disable) { + return; + } + log.info("Starting UPNP Discovery Listener"); try (DatagramSocket responseSocket = new DatagramSocket(upnpResponsePort); @@ -71,7 +81,9 @@ public void startListening(){ String packetString = new String(packet.getData()); if(isSSDPDiscovery(packetString)){ log.debug("Got SSDP Discovery packet from " + packet.getAddress().getHostAddress() + ":" + packet.getPort()); - sendUpnpResponse(responseSocket, packet.getAddress(), packet.getPort()); + for(int i = 0; i < portCount; i ++) { + sendUpnpResponse(responseSocket, packet.getAddress(), packet.getPort(), portBase+i, i); + } } } @@ -100,13 +112,13 @@ protected boolean isSSDPDiscovery(String body){ String discoveryTemplate = "HTTP/1.1 200 OK\r\n" + "CACHE-CONTROL: max-age=86400\r\n" + "EXT:\r\n" + - "LOCATION: http://%s:%s/upnp/amazon-ha-bridge/setup.xml\r\n" + + "LOCATION: http://%s:%s/upnp/%s/setup.xml\r\n" + "OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n" + "01-NLS: %s\r\n" + "ST: urn:schemas-upnp-org:device:basic:1\r\n" + "USN: uuid:Socket-1_0-221438K0100073::urn:Belkin:device:**\r\n\r\n"; - protected void sendUpnpResponse(DatagramSocket socket, InetAddress requester, int sourcePort) throws IOException { - String discoveryResponse = String.format(discoveryTemplate, responseAddress, httpServerPort, getRandomUUIDString()); + protected void sendUpnpResponse(DatagramSocket socket, InetAddress requester, int sourcePort, int gatewayPort, int emulatorId) throws IOException { + String discoveryResponse = String.format(discoveryTemplate, responseAddress, gatewayPort, "amazon-ha-bridge" + emulatorId, "D1710C33-328D-4152-A5FA-5382541A92FF"); DatagramPacket response = new DatagramPacket(discoveryResponse.getBytes(), discoveryResponse.length(), requester, sourcePort); socket.send(response); } diff --git a/src/main/java/com/armzilla/ha/upnp/UpnpSettingsResource.java b/src/main/java/com/armzilla/ha/upnp/UpnpSettingsResource.java index b8fbcc6..dd90b01 100644 --- a/src/main/java/com/armzilla/ha/upnp/UpnpSettingsResource.java +++ b/src/main/java/com/armzilla/ha/upnp/UpnpSettingsResource.java @@ -1,6 +1,7 @@ package com.armzilla.ha.upnp; import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -16,8 +17,9 @@ @Controller @RequestMapping("/upnp") public class UpnpSettingsResource { - private Logger log = Logger.getLogger(UpnpSettingsResource.class); + @Value("${emulator.portcount}") + private int portCount; private String hueTemplate = "\n" + "\n" + @@ -35,8 +37,8 @@ public class UpnpSettingsResource { "Philips hue bridge 2012\n" + "929000226503\n" + "http://www.armzilla.com/amazon-echo-ha-bridge\n" + - "01189998819991197253\n" + - "uuid:88f6698f-2c83-4393-bd03-cd54a9f8595\n" + + "%s\n" + + "uuid:%s\n" + "\n" + "\n" + "(null)\n" + @@ -72,7 +74,7 @@ public ResponseEntity getUpnpConfiguration(@PathVariable(value="deviceId log.info("upnp device settings requested: " + deviceId + " from " + request.getRemoteAddr()); String hostName = request.getLocalAddr(); String portNumber = Integer.toString(request.getLocalPort()); - String filledTemplate = String.format(hueTemplate, hostName, portNumber, hostName); + String filledTemplate = String.format(hueTemplate, hostName, request.getLocalPort(), hostName, deviceId, deviceId); return new ResponseEntity<>(filledTemplate, null, HttpStatus.OK); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ed8db7f..2fe75d1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,13 @@ upnp.response.port=50000 -server.port=8080 -upnp.config.address=192.168.1.1 +upnp.config.address= +emulator.portbase=8080 +emulator.portcount=3 +upnp.disable=false + + +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=echo +spring.datasource.password=echo +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.datasource.url=jdbc:h2:file:./store +spring.jpa.hibernate.ddl-auto=update diff --git a/src/test/java/demo/DemoApplicationTests.java b/src/test/java/demo/DemoApplicationTests.java index 8ca1356..2f51058 100644 --- a/src/test/java/demo/DemoApplicationTests.java +++ b/src/test/java/demo/DemoApplicationTests.java @@ -3,12 +3,14 @@ import com.armzilla.ha.SpringbootEntry; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SpringbootEntry.class) +@TestPropertySource(locations="classpath:test.properties") @WebAppConfiguration public class DemoApplicationTests { diff --git a/src/test/resources/test.properties b/src/test/resources/test.properties new file mode 100644 index 0000000..841379d --- /dev/null +++ b/src/test/resources/test.properties @@ -0,0 +1,5 @@ +upnp.response.port=50000 +upnp.config.address=192.168.1.1 +emulator.portbase=8080 +emulator.portcount=3 +upnp.disable=false