├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── src
├── main
│ ├── resources
│ │ ├── mqtt.properties
│ │ └── log4j.properties
│ ├── webapp
│ │ └── WEB-INF
│ │ │ └── appengine-web.xml
│ └── java
│ │ └── com
│ │ └── example
│ │ ├── LoginServlet.java
│ │ ├── FakeAuthServlet.java
│ │ ├── FakeTokenServlet.java
│ │ ├── SmartHomeServlet.java
│ │ ├── SmartHomeDeleteServlet.java
│ │ ├── SmartHomeCreateServlet.java
│ │ ├── ReportState.java
│ │ ├── SmartHomeUpdateServlet.java
│ │ ├── MySmartHomeApp.java
│ │ ├── MyMqtt.java
│ │ └── MyDataStore.java
└── test
│ └── java
│ └── com
│ └── example
│ └── SmartHomeEndToEndTest.java
├── .gitignore
├── CONTRIBUTING.md
├── gradlew.bat
├── gradlew
├── README.md
└── LICENSE
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/electrofun-smart/my-smart-home-java-mqtt/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/src/main/resources/mqtt.properties:
--------------------------------------------------------------------------------
1 | # Example of mqtt credentials - update accordingly
2 | broker=wss://broker.shiftr.io
3 | clientid=test1
4 | user=youruser
5 | pwd=yourpwd
6 | cleansession=false
7 | quietmode=false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | ._*
3 | .Spotlight-V100
4 | .Trashes
5 | ehthumbs.db
6 | Thumbs.db
7 | smart-home-key.json
8 | mqtt.properties
9 | # Intellij
10 | *.iml
11 | *.iws
12 | .idea/
13 | build/
14 | .gradle/
15 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/src/main/resources/log4j.properties:
--------------------------------------------------------------------------------
1 | # Root logger
2 | log4j.rootLogger=DEBUG, stdout
3 |
4 | # log to console
5 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender
6 | log4j.appender.stdout.Target=System.out
7 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
8 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p:: %m%n
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/appengine-web.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 | java8
19 | true
20 |
21 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to become a contributor and submit your own code
2 |
3 | ## Contributor License Agreements
4 |
5 | We'd love to accept your sample apps and patches! Before we can take them, we
6 | have to jump a couple of legal hurdles.
7 |
8 | Please fill out either the individual or corporate Contributor License Agreement
9 | (CLA).
10 |
11 | * If you are an individual writing original source code and you're sure you
12 | own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual).
13 | * If you work for a company that wants to allow you to contribute your work,
14 | then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate).
15 |
16 | Follow either of the two links above to access the appropriate CLA and
17 | instructions for how to sign and return it. Once we receive it, we'll be able to
18 | accept your pull requests.
19 |
20 | ## Contributing A Patch
21 |
22 | 1. Submit an issue describing your proposed change to the repo in question.
23 | 1. The repo owner will respond to your issue promptly.
24 | 1. If your proposed change is accepted, and you haven't already done so, sign a
25 | Contributor License Agreement (see details above).
26 | 1. Fork the desired repo, develop and test your code changes.
27 | 1. Ensure that your code adheres to the existing style in the sample to which
28 | you are contributing. Refer to the
29 | [Google Cloud Platform Samples Style Guide](https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the
30 | recommended coding standards for this organization.
31 | 1. Ensure that your code has an appropriate set of unit tests which all pass.
32 | 1. Submit a pull request.
--------------------------------------------------------------------------------
/src/main/java/com/example/LoginServlet.java:
--------------------------------------------------------------------------------
1 | package com.example;
2 |
3 | import java.io.IOException;
4 | import java.net.URLDecoder;
5 |
6 | import javax.servlet.annotation.WebServlet;
7 | import javax.servlet.http.HttpServlet;
8 | import javax.servlet.http.HttpServletRequest;
9 | import javax.servlet.http.HttpServletResponse;
10 |
11 | @WebServlet(name = "login", description = "Trivial login page", urlPatterns = "/login")
12 | public class LoginServlet extends HttpServlet {
13 | @Override
14 | protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
15 | String redirectURL = req.getParameter("responseurl") + "&code=xxxxxx";;
16 | res.setStatus(HttpServletResponse.SC_OK);
17 | res.setContentType("text/html");
18 | String formData =
19 | ""
20 | + "
"
21 | + ""
27 | + ""
28 | + "";
29 | res.getWriter().print(formData);
30 | res.getWriter().flush();
31 | }
32 |
33 | @Override
34 | protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
35 | // Here, you should validate the user account.
36 | // In this sample, we do not do that.
37 | String redirectURL = URLDecoder.decode(req.getParameter("responseurl"), "UTF-8");
38 | res.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
39 | res.setHeader("Location", redirectURL);
40 | res.getWriter().flush();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/com/example/FakeAuthServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Google LLC
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * https://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 |
14 | package com.example;
15 |
16 | import java.io.IOException;
17 | import java.net.URLDecoder;
18 |
19 | import javax.servlet.annotation.WebServlet;
20 | import javax.servlet.http.HttpServlet;
21 | import javax.servlet.http.HttpServletRequest;
22 | import javax.servlet.http.HttpServletResponse;
23 |
24 | // With @WebServlet annotation the webapp/WEB-INF/web.xml is no longer required.
25 | @WebServlet (name = "auth", description = "Requests: Trivial request", urlPatterns = "/fakeauth")
26 | public class FakeAuthServlet extends HttpServlet {
27 |
28 | @Override
29 | protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
30 | String redirectURL =
31 | String.format(
32 | "%s?state=%s&code=%s",
33 | URLDecoder.decode(req.getParameter("redirect_uri"), "UTF8"),
34 | req.getParameter("state"),
35 | "xxxxxx");
36 | String loginUrl = res.encodeRedirectURL("/login?responseurl=" + redirectURL);
37 | res.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
38 | res.setHeader("Location", loginUrl);
39 | res.getWriter().flush();
40 | }
41 |
42 | @Override
43 | protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
44 | res.setContentType("text/plain");
45 | res.getWriter().println("/fakeauth should be a GET");
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/com/example/FakeTokenServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example;
18 |
19 | import java.io.IOException;
20 |
21 | import javax.servlet.annotation.WebServlet;
22 | import javax.servlet.http.HttpServlet;
23 | import javax.servlet.http.HttpServletRequest;
24 | import javax.servlet.http.HttpServletResponse;
25 |
26 | import com.google.gson.JsonObject;
27 |
28 | // With @WebServlet annotation the webapp/WEB-INF/web.xml is no longer required.
29 | @WebServlet(name = "tokem", description = "Requests: Trivial request", urlPatterns = "/faketoken")
30 | public class FakeTokenServlet extends HttpServlet {
31 |
32 | private static int secondsInDay = 86400;
33 |
34 | @Override
35 | protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
36 | res.setContentType("text/plain");
37 | res.getWriter().println("/faketoken should be a POST");
38 | }
39 |
40 | @Override
41 | protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
42 | String grantType = req.getParameter("grant_type");
43 |
44 | JsonObject jsonRes = new JsonObject();
45 | jsonRes.addProperty("token_type", "bearer");
46 | jsonRes.addProperty("access_token", "123access");
47 | jsonRes.addProperty("expires_in", secondsInDay);
48 | if (grantType.equals("authorization_code")) {
49 | jsonRes.addProperty("refresh_token", "123refresh");
50 | }
51 | res.setStatus(HttpServletResponse.SC_OK);
52 | res.setContentType("application/json");
53 | System.out.println("response = " + jsonRes.toString());
54 | res.getWriter().write(jsonRes.toString());
55 | res.getWriter().flush();
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
34 |
35 | @rem Find java.exe
36 | if defined JAVA_HOME goto findJavaFromJavaHome
37 |
38 | set JAVA_EXE=java.exe
39 | %JAVA_EXE% -version >NUL 2>&1
40 | if "%ERRORLEVEL%" == "0" goto init
41 |
42 | echo.
43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
44 | echo.
45 | echo Please set the JAVA_HOME variable in your environment to match the
46 | echo location of your Java installation.
47 |
48 | goto fail
49 |
50 | :findJavaFromJavaHome
51 | set JAVA_HOME=%JAVA_HOME:"=%
52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
53 |
54 | if exist "%JAVA_EXE%" goto init
55 |
56 | echo.
57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
58 | echo.
59 | echo Please set the JAVA_HOME variable in your environment to match the
60 | echo location of your Java installation.
61 |
62 | goto fail
63 |
64 | :init
65 | @rem Get command-line arguments, handling Windows variants
66 |
67 | if not "%OS%" == "Windows_NT" goto win9xME_args
68 |
69 | :win9xME_args
70 | @rem Slurp the command line arguments.
71 | set CMD_LINE_ARGS=
72 | set _SKIP=2
73 |
74 | :win9xME_args_slurp
75 | if "x%~1" == "x" goto execute
76 |
77 | set CMD_LINE_ARGS=%*
78 |
79 | :execute
80 | @rem Setup the command line
81 |
82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
83 |
84 | @rem Execute Gradle
85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
86 |
87 | :end
88 | @rem End local scope for the variables with windows NT shell
89 | if "%ERRORLEVEL%"=="0" goto mainEnd
90 |
91 | :fail
92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
93 | rem the _cmd.exe /c_ return code!
94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
95 | exit /b 1
96 |
97 | :mainEnd
98 | if "%OS%"=="Windows_NT" endlocal
99 |
100 | :omega
101 |
--------------------------------------------------------------------------------
/src/main/java/com/example/SmartHomeServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example;
18 |
19 | import java.io.IOException;
20 | import java.util.Enumeration;
21 | import java.util.HashMap;
22 | import java.util.Map;
23 | import java.util.concurrent.ExecutionException;
24 | import java.util.stream.Collectors;
25 |
26 | import javax.servlet.ServletException;
27 | import javax.servlet.annotation.WebServlet;
28 | import javax.servlet.http.HttpServlet;
29 | import javax.servlet.http.HttpServletRequest;
30 | import javax.servlet.http.HttpServletResponse;
31 |
32 | import org.slf4j.Logger;
33 | import org.slf4j.LoggerFactory;
34 |
35 | import com.google.actions.api.smarthome.SmartHomeApp;
36 | import com.google.auth.oauth2.GoogleCredentials;
37 |
38 | /**
39 | * Handles request received via HTTP POST and delegates it to your Actions app. See: [Request
40 | * handling in Google App
41 | * Engine](https://cloud.google.com/appengine/docs/standard/java/how-requests-are-handled).
42 | */
43 | @WebServlet(name = "smarthome", urlPatterns = "/smarthome")
44 | public class SmartHomeServlet extends HttpServlet {
45 | private static final Logger LOG = LoggerFactory.getLogger(MySmartHomeApp.class);
46 | private final SmartHomeApp actionsApp = new MySmartHomeApp();
47 |
48 | {
49 | try {
50 | GoogleCredentials credentials =
51 | GoogleCredentials.fromStream(getClass().getResourceAsStream("/smart-home-key.json"));
52 | actionsApp.setCredentials(credentials);
53 | } catch (Exception e) {
54 | LOG.error("couldn't load credentials");
55 | }
56 | }
57 |
58 | @Override
59 | protected void doPost(HttpServletRequest req, HttpServletResponse res)
60 | throws IOException, ServletException {
61 | String body = req.getReader().lines().collect(Collectors.joining());
62 | LOG.info("doPost, body = {}", body);
63 | Map headerMap = getHeaderMap(req);
64 | try {
65 | String response = actionsApp.handleRequest(body, headerMap).get();
66 | res.setStatus(HttpServletResponse.SC_OK);
67 | res.setHeader("Access-Control-Allow-Origin", "*");
68 | res.setContentType("application/json");
69 | writeResponse(res, response);
70 | } catch (ExecutionException | InterruptedException e) {
71 | LOG.error("failed to handle fulfillment request", e);
72 | throw new ServletException(e);
73 | }
74 | }
75 |
76 | @Override
77 | protected void doGet(HttpServletRequest request, HttpServletResponse response)
78 | throws IOException {
79 | response.setContentType("text/plain");
80 | response
81 | .getWriter()
82 | .println(
83 | "ActionsServlet is listening but requires valid POST "
84 | + "request to respond with Action response.");
85 | }
86 |
87 | private void writeResponse(HttpServletResponse res, String asJson) throws IOException {
88 | System.out.println("response = " + asJson);
89 | res.getWriter().write(asJson);
90 | res.getWriter().flush();
91 | }
92 |
93 | private Map getHeaderMap(HttpServletRequest req) {
94 | Map headerMap = new HashMap<>();
95 | Enumeration headerNames = req.getHeaderNames();
96 | while (headerNames.hasMoreElements()) {
97 | String name = (String) headerNames.nextElement();
98 | String val = req.getHeader(name);
99 | headerMap.put(name, val);
100 | }
101 | return headerMap;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/main/java/com/example/SmartHomeDeleteServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example;
18 |
19 | import java.io.IOException;
20 | import java.util.stream.Collectors;
21 |
22 | import javax.servlet.annotation.WebServlet;
23 | import javax.servlet.http.HttpServlet;
24 | import javax.servlet.http.HttpServletRequest;
25 | import javax.servlet.http.HttpServletResponse;
26 |
27 | import org.slf4j.Logger;
28 | import org.slf4j.LoggerFactory;
29 |
30 | import com.google.actions.api.smarthome.SmartHomeApp;
31 | import com.google.auth.oauth2.GoogleCredentials;
32 | import com.google.gson.JsonObject;
33 | import com.google.gson.JsonParser;
34 |
35 | /**
36 | * Handles request received via HTTP POST and delegates it to your Actions app. See: [Request
37 | * handling in Google App
38 | * Engine](https://cloud.google.com/appengine/docs/standard/java/how-requests-are-handled).
39 | */
40 | @WebServlet(name = "smarthomeDelete", urlPatterns = "/smarthome/delete")
41 | public class SmartHomeDeleteServlet extends HttpServlet {
42 | private static final Logger LOGGER = LoggerFactory.getLogger(MySmartHomeApp.class);
43 | private static MyDataStore database = MyDataStore.getInstance();
44 |
45 | private final SmartHomeApp actionsApp = new MySmartHomeApp();
46 |
47 | {
48 | try {
49 | GoogleCredentials credentials =
50 | GoogleCredentials.fromStream(getClass().getResourceAsStream("/smart-home-key.json"));
51 | actionsApp.setCredentials(credentials);
52 | } catch (Exception e) {
53 | LOGGER.error("couldn't load credentials");
54 | }
55 | }
56 |
57 | @Override
58 | protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
59 | String body = req.getReader().lines().collect(Collectors.joining());
60 | LOGGER.info("doPost, body = {}", body);
61 | JsonObject bodyJson = new JsonParser().parse(body).getAsJsonObject();
62 | String userId = bodyJson.get("userId").getAsString();
63 | String deviceId = bodyJson.get("deviceId").getAsString();
64 | try {
65 | database.deleteDevice(userId, deviceId);
66 | } catch (Exception e) {
67 | LOGGER.error("adding device failed: {}", e);
68 | res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
69 | res.setHeader("Access-Control-Allow-Origin", "*");
70 | res.setContentType("text/plain");
71 | res.getWriter().println("ERROR");
72 | return;
73 | }
74 |
75 | try {
76 | actionsApp.requestSync(userId);
77 | } catch (Exception e) {
78 | LOGGER.error("request sync failed: {}", e);
79 | }
80 |
81 | res.setStatus(HttpServletResponse.SC_OK);
82 | res.setHeader("Access-Control-Allow-Origin", "*");
83 | res.setContentType("text/plain");
84 | res.getWriter().println("OK");
85 | }
86 |
87 | @Override
88 | protected void doGet(HttpServletRequest request, HttpServletResponse response)
89 | throws IOException {
90 | response.setContentType("text/plain");
91 | response.getWriter().println("/smarthome/delete is a POST call");
92 | }
93 |
94 | @Override
95 | protected void doOptions(HttpServletRequest req, HttpServletResponse res) {
96 | // pre-flight request processing
97 | res.setHeader("Access-Control-Allow-Origin", "*");
98 | res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
99 | res.setHeader("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Accept,Origin");
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/main/java/com/example/SmartHomeCreateServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example;
18 |
19 | import java.io.IOException;
20 | import java.util.HashMap;
21 | import java.util.Map;
22 | import java.util.stream.Collectors;
23 |
24 | import javax.servlet.annotation.WebServlet;
25 | import javax.servlet.http.HttpServlet;
26 | import javax.servlet.http.HttpServletRequest;
27 | import javax.servlet.http.HttpServletResponse;
28 |
29 | import org.slf4j.Logger;
30 | import org.slf4j.LoggerFactory;
31 |
32 | import com.google.actions.api.smarthome.SmartHomeApp;
33 | import com.google.auth.oauth2.GoogleCredentials;
34 | import com.google.gson.Gson;
35 |
36 | /**
37 | * Handles request received via HTTP POST and delegates it to your Actions app. See: [Request
38 | * handling in Google App
39 | * Engine](https://cloud.google.com/appengine/docs/standard/java/how-requests-are-handled).
40 | */
41 | @WebServlet(name = "smarthomeCreate", urlPatterns = "/smarthome/create")
42 | public class SmartHomeCreateServlet extends HttpServlet {
43 | private static final Logger LOGGER = LoggerFactory.getLogger(MySmartHomeApp.class);
44 | private static MyDataStore database = MyDataStore.getInstance();
45 |
46 | // Setup creds for requestSync
47 | private final SmartHomeApp actionsApp = new MySmartHomeApp();
48 |
49 | {
50 | try {
51 | GoogleCredentials credentials =
52 | GoogleCredentials.fromStream(getClass().getResourceAsStream("/smart-home-key.json"));
53 | actionsApp.setCredentials(credentials);
54 | } catch (Exception e) {
55 | LOGGER.error("couldn't load credentials");
56 | }
57 | }
58 |
59 | @Override
60 | protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
61 | String body = req.getReader().lines().collect(Collectors.joining());
62 | LOGGER.info("doPost, body = {}", body);
63 | Map device = new Gson().fromJson(body, HashMap.class);
64 |
65 | String userId = (String) device.get("userId");
66 | Map deviceData = (Map) device.get("data");
67 |
68 | try {
69 | database.addDevice(userId, deviceData);
70 | } catch (Exception e) {
71 | LOGGER.error("adding device failed: {}", e);
72 | res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
73 | res.setHeader("Access-Control-Allow-Origin", "*");
74 | res.setContentType("text/plain");
75 | res.getWriter().println("ERROR");
76 | return;
77 | }
78 |
79 | try {
80 | actionsApp.requestSync(userId);
81 | } catch (Exception e) {
82 | LOGGER.error("request sync failed: {}", e);
83 | }
84 |
85 | res.setStatus(HttpServletResponse.SC_OK);
86 | res.setHeader("Access-Control-Allow-Origin", "*");
87 | res.setContentType("text/plain");
88 | res.getWriter().println("OK");
89 | }
90 |
91 | @Override
92 | protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
93 | res.setContentType("text/plain");
94 | res.getWriter().println("/smarthome/create is a POST call");
95 | }
96 |
97 | @Override
98 | protected void doOptions(HttpServletRequest req, HttpServletResponse res) {
99 | // pre-flight request processing
100 | res.setHeader("Access-Control-Allow-Origin", "*");
101 | res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
102 | res.setHeader("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Accept,Origin");
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/main/java/com/example/ReportState.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example;
18 |
19 | import java.util.Map;
20 |
21 | import org.eclipse.paho.client.mqttv3.MqttException;
22 | import org.slf4j.Logger;
23 | import org.slf4j.LoggerFactory;
24 |
25 | import com.google.actions.api.smarthome.SmartHomeApp;
26 | import com.google.gson.Gson;
27 | import com.google.gson.JsonObject;
28 | import com.google.gson.JsonParser;
29 | import com.google.home.graph.v1.HomeGraphApiServiceProto;
30 | import com.google.protobuf.Struct;
31 | import com.google.protobuf.Value;
32 | import com.google.protobuf.util.JsonFormat;
33 |
34 | /**
35 | * A singleton class to encapsulate state reporting behavior with changing ColorSetting state
36 | * values.
37 | */
38 | final class ReportState {
39 | private static final Logger LOGGER = LoggerFactory.getLogger(MySmartHomeApp.class);
40 |
41 | private ReportState() {
42 | }
43 |
44 | /**
45 | * Creates and completes a ReportStateAndNotification request
46 | *
47 | * @param actionsApp The SmartHomeApp instance to use to make the gRPC request
48 | * @param userId The agent user ID
49 | * @param deviceId The device ID
50 | * @param states A Map of state keys and their values for the provided device ID
51 | */
52 | public static void makeRequest(
53 | SmartHomeApp actionsApp, String userId, String deviceId, Map states) {
54 | // Convert a Map of states to a JsonObject
55 | JsonObject jsonStates = (JsonObject) JsonParser.parseString(new Gson().toJson(states));
56 | ReportState.makeRequest(actionsApp, userId, deviceId, jsonStates);
57 | }
58 |
59 | /**
60 | * Creates and completes a ReportStateAndNotification request
61 | *
62 | * @param actionsApp The SmartHomeApp instance to use to make the gRPC request
63 | * @param userId The agent user ID
64 | * @param deviceId The device ID
65 | * @param states A JSON object of state keys and their values for the provided device ID
66 | */
67 | public static void makeRequest(
68 | SmartHomeApp actionsApp, String userId, String deviceId, JsonObject states) {
69 | // Do state name replacement for ColorSetting trait
70 | // See https://developers.google.com/assistant/smarthome/traits/colorsetting#device-states
71 | JsonObject colorJson = states.getAsJsonObject("color");
72 | if (colorJson != null && colorJson.has("spectrumRgb")) {
73 | colorJson.add("spectrumRGB", colorJson.get("spectrumRgb"));
74 | colorJson.remove("spectrumRgb");
75 | }
76 | Struct.Builder statesStruct = Struct.newBuilder();
77 | try {
78 | JsonFormat.parser().ignoringUnknownFields().merge(new Gson().toJson(states), statesStruct);
79 | } catch (Exception e) {
80 | LOGGER.error("FAILED TO BUILD");
81 | }
82 |
83 | HomeGraphApiServiceProto.ReportStateAndNotificationDevice.Builder deviceBuilder =
84 | HomeGraphApiServiceProto.ReportStateAndNotificationDevice.newBuilder()
85 | .setStates(
86 | Struct.newBuilder()
87 | .putFields(deviceId, Value.newBuilder().setStructValue(statesStruct).build()));
88 |
89 | HomeGraphApiServiceProto.ReportStateAndNotificationRequest request =
90 | HomeGraphApiServiceProto.ReportStateAndNotificationRequest.newBuilder()
91 | .setRequestId(String.valueOf(Math.random()))
92 | .setAgentUserId(userId) // our single user's id
93 | .setPayload(
94 | HomeGraphApiServiceProto.StateAndNotificationPayload.newBuilder()
95 | .setDevices(deviceBuilder))
96 | .build();
97 |
98 | actionsApp.reportState(request);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/test/java/com/example/SmartHomeEndToEndTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example;
18 |
19 | import static io.restassured.RestAssured.*;
20 | import static io.restassured.matcher.RestAssuredMatchers.*;
21 | import static org.hamcrest.Matchers.*;
22 | import static org.junit.jupiter.api.Assertions.*;
23 |
24 | import java.util.ArrayList;
25 | import java.util.HashMap;
26 | import java.util.List;
27 | import java.util.Map;
28 | import java.util.concurrent.ExecutionException;
29 |
30 | import org.junit.jupiter.api.AfterAll;
31 | import org.junit.jupiter.api.BeforeAll;
32 | import org.junit.jupiter.api.Test;
33 |
34 | import com.google.cloud.firestore.QueryDocumentSnapshot;
35 |
36 | class SmarHomeEndToEndTest {
37 | private static final String USER_ID = "test-user-id";
38 | private static final String DEVICE_ID = "test-device-id";
39 | private static final String DEVICE_NAME = "test-device-name";
40 | private static final String DEVICE_NAME_UPDATED = "test-device-name-updated";
41 | private static final String DEVICE_TYPE = "action.devices.types.LIGHT";
42 | private static final String DEVICE_PLACEHOLDER = "test-device-placeholder";
43 | private static final String REQUEST_ID = "request-id";
44 |
45 | @BeforeAll()
46 | static void initAll() throws ExecutionException, InterruptedException {
47 | Map testUser = new HashMap<>();
48 | if (System.getProperty("restassuredBaseUri") != null) {
49 | io.restassured.RestAssured.baseURI = System.getProperty("restassuredBaseUri");
50 | }
51 | testUser.put("fakeAccessToken", "123access");
52 | testUser.put("fakeRefreshToken", "123refresh");
53 | MyDataStore.getInstance().database.collection("users").document(USER_ID).set(testUser).get();
54 | }
55 |
56 | @AfterAll()
57 | static void tearDownAll() throws ExecutionException, InterruptedException {
58 | MyDataStore.getInstance().database.collection("users").document(USER_ID).delete().get();
59 | }
60 |
61 | @Test
62 | void testCreateSyncUpdateDelete() throws ExecutionException, InterruptedException {
63 | Map deviceCreate = new HashMap<>();
64 | deviceCreate.put("userId", USER_ID);
65 | Map deviceData = new HashMap<>();
66 | deviceData.put("deviceId", DEVICE_ID);
67 | deviceData.put("name", DEVICE_NAME);
68 | deviceData.put("type", DEVICE_TYPE);
69 | deviceData.put("traits", new ArrayList());
70 | deviceData.put("defaultNames", new ArrayList());
71 | deviceData.put("nicknames", new ArrayList());
72 | deviceData.put("willReportState", false);
73 | deviceData.put("roomHint", DEVICE_PLACEHOLDER);
74 | deviceData.put("manufacturer", DEVICE_PLACEHOLDER);
75 | deviceData.put("model", DEVICE_PLACEHOLDER);
76 | deviceData.put("hwVersion", DEVICE_PLACEHOLDER);
77 | deviceData.put("swVersion", DEVICE_PLACEHOLDER);
78 | deviceCreate.put("data", deviceData);
79 | given()
80 | .contentType("application/json")
81 | .body(deviceCreate)
82 | .when()
83 | .post("/smarthome/create")
84 | .then()
85 | .statusCode(200);
86 |
87 | QueryDocumentSnapshot deviceCreated = MyDataStore.getInstance().getDevices(USER_ID).get(0);
88 | assertEquals(DEVICE_ID, deviceCreated.get("deviceId"));
89 |
90 | Map syncRequest = new HashMap<>();
91 | syncRequest.put("requestId", REQUEST_ID);
92 | List