├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build-aws.gradle ├── build-gcp.gradle ├── build.gradle ├── buildSrc └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── main ├── java │ └── com │ │ └── example │ │ ├── FakeAuthServlet.java │ │ ├── FakeTokenServlet.java │ │ ├── LoginServlet.java │ │ ├── MyDataStore.java │ │ ├── MyMqtt.java │ │ ├── MySmartHomeApp.java │ │ ├── ReportState.java │ │ ├── SmartHomeCreateServlet.java │ │ ├── SmartHomeDeleteServlet.java │ │ ├── SmartHomeServlet.java │ │ └── SmartHomeUpdateServlet.java ├── resources │ ├── log4j.properties │ └── mqtt.properties └── webapp │ └── WEB-INF │ └── appengine-web.xml └── test └── java └── com └── example └── SmartHomeEndToEndTest.java /.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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## NOTE: The instructions below were based on Google Sample Project smart-home-java 2 | 3 | # Java Smart Home Sample with MQTT interface to connect to IOT devices (ESP8266/Arduino/ESP32) 4 | 5 | This sample adaptation with MQTT is to help you get started quickly with the Java smart home library for Actions on Google. 6 | This example was used in the tutorial : How to connect ESP8266 and Arduino with Google Assistant and Google Home App without IFTTT 7 | https://youtu.be/0czm7VIgoZs - Part 2 8 | https://youtu.be/-tnYAI3mE24 - Part 1 9 | 10 | ## Setup Instructions 11 | 12 | ### Clone this project (my-smart-home-java-mqtt) 13 | 1. Clone this github project at your machine. git clone https://github.com/electrofun-smart/my-smart-home-java-mqtt.git 14 | 1. By using an IDEA (recommended Intellij) import and build the project. 15 | 1. Install Google Cloud SDK in your machine 16 | 17 | ### Create an Smart Home Action Project 18 | 19 | 1. Use the [Actions on Google Console](https://console.actions.google.com), click on a **New project** and add a project name and click **Create Project**. 20 | 1. Click **Smart Home** as a type of action and **Start building**. 21 | 1. Then add a name of your Smart Home action, in the **Display Name**, it will show latter in Android app Google Home as a brand service, then click **Save**. 22 | 1. Click on **Actions** at left Menu 23 | 1. Enter the URL for fulfillment, don forget to change the url with your project id, e.g. https://your_project_id.appspot.com/smarthome, click **Save**. 24 | 1. From the left menu click on **Account Linking**. 25 | 1. Under Client Information, add anything on the client ID and secret, it will not be used on this sample. 26 | 1. The Authorization URL is the hosted URL of your app with '/fakeauth' as the path, e.g. https://your_project_id.appspot.com/fakeauth 27 | 1. The Token URL is the hosted URL of your app with '/faketoken' as the path, e.g. https://your_project_id.appspot.com/faketoken 28 | 1. Then click **Save** 29 | 1. On the top navigation menu under **Test**, click on **Talk to 'Display name'**, to begin testing this app. Obs. An error will be returned but this is enough to have the service available o Google Home for test and personal use. 30 | 31 | ### Credentials and API enabling 32 | To fully utilize the features of this project, including the Report State API, you must setup account credentials. 33 | 1. Navigate to the [Google Cloud Console API & Services page](https://console.cloud.google.com/apis/credentials) 34 | 2. Be sure you are currently inside your project (view dropdown on the top of the page) 35 | 1. Select **Create Credentials** and create a **Service account key** 36 | 1. Create the account and download a JSON file. Save this file at you cloned project in the resource location at `src/main/resources/smart-home-key.json`. 37 | 1. Enable cloud billing in the project https://console.developers.google.com/apis/api/cloudbuild.googleapis.com/overview?project=your-project-id 38 | 2. Note that you need to create a billing account with an credit card, usually Google offers 300USD to be used in G Cloud applications. If your project is small and will not have too much requests on Google end-points the costs is very low per month. But be carefull, I recommend also that you set limits per month to avoid surprises. 39 | 1. Enable Google Home Graph API https://console.cloud.google.com/apis/api/homegraph.googleapis.com/overview?project=your-project-id 40 | 41 | ### Connect to Firebase 42 | 43 | 1. Open your project in the Firebase console (https://console.firebase.google.com/), click on the left menu in **Database** and **Create Database**. 44 | 1. Start in production Mode, choose the cloud firestore location that better fits your location, click **Done** 45 | 1. Click on **Rules** and edit as code below and click on **Publish** 46 | ``` 47 | rules_version = '2'; 48 | service cloud.firestore { 49 | match /databases/{database}/documents { 50 | match /{document=**} { 51 | allow read; 52 | allow write: if false; 53 | } 54 | } 55 | } 56 | 57 | ``` 58 | 1. Configure a `users` collection with a default user and a few default fields that match exactly: 59 | 2. Click **Start Collection** add users, click **Next**. Documment Id add 1234 and **Save**. Below 1234 click on **Add field** and add the 3 fields below where first 2 as String and homegraph as boolean. 60 | 61 | ``` 62 | users\ 63 | 1234 64 | fakeAccessToken: "123access" 65 | fakeRefreshToken: "123refresh" 66 | homegraph: false 67 | ``` 68 | 69 | ### Deploying the project 70 | 1. At Intellij with your project opened, open a terminal or Intellij terminal, type the command **gcloud init** (from Google SDK) and follow the options to select your user and project. At first time a url will be given to complete the user authentication. 71 | 2. Then type command **gcloud app create** and select the same location as you did on Firestore database. (it must be the same otherwise you will need to re-create the project). 72 | 1. At Intellij open the Gradle box (top right corner) under Tasks->app engine standard environment, double click on **appengineDeploy** 73 | 1. Your project will be deployed at Google Cloud and the url will be prompt at the end of the building. 74 | 1. Double check and update your project if needed at https://console.actions.google.com with url where app was deployed from step above. (Fulfillment URL and Authorization and Token Urls) 75 | 76 | #### Setup Account linking 77 | 78 | 1. On a device with the Google Assistant logged into the same account used 79 | to create the project in the Actions Console, enter your Assistant settings. 80 | 1. Click Home Control. 81 | 1. Click the '+' sign to add a device. 82 | 1. Find your project display name in the list of providers. 83 | 1. Log in to your service. 84 | 1. Nothing happens because there is no devices created yet. Next step I will show how to install frontend application to create devices. 85 | 86 | ### Setup front-end applicatin 87 | 1. You can also follow the setup for a local frontend to test adding and testing devices 88 | here: git clone https://github.com/actions-on-google/smart-home-frontend.git 89 | 90 | Assistant will only provide you control over items that are registered, so if you visit your front 91 | end and click the add icon to create a device your server will receive a 92 | new SYNC command. 93 | 94 | 95 | 96 | ### References & Issues 97 | 98 | #### Build for Google Cloud Platform 99 | 100 | 1. Instructions for [Google Cloud App Engine Standard Environment](https://cloud.google.com/appengine/docs/standard/java/) 101 | 1. Use gcloud CLI to set the project to the name of your Actions project. Use 'gcloud init' to initialize and set your Google cloud project to the name of the Actions project. 102 | 1. Deploy to [App Engine using Gradle](https://cloud.google.com/appengine/docs/flexible/java/using-gradle) by running the following command: `gradle appengineDeploy`. You can do this directly from 103 | IntelliJ by opening the Gradle tray and running the appEngineDeploy task. This will start the process to deploy the fulfillment code to Google Cloud App Engine. 104 | 105 | + Questions? Go to [StackOverflow](https://stackoverflow.com/questions/tagged/actions-on-google), [Assistant Developer Community on Reddit](https://www.reddit.com/r/GoogleAssistantDev/) or [Support](https://developers.google.com/assistant/support). 106 | + For bugs, please report an issue on Github. 107 | + Actions on Google [Documentation](https://developers.google.com/assistant) 108 | + Actions on Google [Codelabs](https://codelabs.developers.google.com/?cat=Assistant). 109 | 110 | -------------------------------------------------------------------------------- /build-aws.gradle: -------------------------------------------------------------------------------- 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 | task buildAWSZip(type: Zip) { 18 | from compileJava 19 | from processResources 20 | into('lib') { 21 | from configurations.runtime 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /build-gcp.gradle: -------------------------------------------------------------------------------- 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 | apply plugin: 'com.google.cloud.tools.appengine' 18 | 19 | appengine { 20 | deploy { 21 | projectId 'GCLOUD_CONFIG' 22 | version 'GCLOUD_CONFIG' 23 | } 24 | } 25 | 26 | if (project.hasProperty('appengineProjectId')) { 27 | appengine.deploy.projectId = project.appengineProjectId 28 | } 29 | 30 | if (project.hasProperty('appengineVersion')) { 31 | appengine.deploy.version = project.appengineVersion 32 | } 33 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 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 | plugins { 18 | id 'java' 19 | id 'war' 20 | id 'com.diffplug.gradle.spotless' version '3.27.1' 21 | id 'net.ltgt.errorprone' version '1.1.1' 22 | id 'org.gretty' version '3.0.1' 23 | } 24 | 25 | apply from: 'build-aws.gradle' 26 | apply from: 'build-gcp.gradle' 27 | 28 | java { 29 | sourceCompatibility = JavaVersion.VERSION_1_8 30 | targetCompatibility = JavaVersion.VERSION_1_8 31 | } 32 | 33 | repositories { 34 | mavenCentral() 35 | jcenter() 36 | } 37 | 38 | dependencies { 39 | providedCompile 'javax.servlet:javax.servlet-api:3.1.0' 40 | 41 | implementation 'org.slf4j:slf4j-log4j12:1.7.28' 42 | implementation 'org.jetbrains:annotations:13.0' 43 | 44 | implementation 'com.google.actions:actions-on-google:1.8.0' 45 | implementation 'com.google.code.gson:gson:2.8.6' 46 | implementation 'com.google.protobuf:protobuf-java:3.10.0' 47 | implementation 'com.google.protobuf:protobuf-java-util:3.10.0' 48 | 49 | implementation 'com.google.firebase:firebase-admin:6.12.0' 50 | implementation 'com.google.cloud:google-cloud-firestore:1.31.0' 51 | implementation 'com.google.auth:google-auth-library-oauth2-http:0.18.0' 52 | implementation 'com.google.api:api-common:1.8.1' 53 | 54 | // https://mvnrepository.com/artifact/org.eclipse.paho/org.eclipse.paho.client.mqttv3 55 | compile group: 'org.eclipse.paho', name: 'org.eclipse.paho.client.mqttv3', version: '1.2.4' 56 | // https://mvnrepository.com/artifact/org.processing/core 57 | compile group: 'org.processing', name: 'core', version: '3.3.6' 58 | // https://mvnrepository.com/artifact/com.jayway.jsonpath/json-path 59 | compile group: 'com.jayway.jsonpath', name: 'json-path', version: '2.4.0' 60 | 61 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' 62 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.0' 63 | testImplementation 'io.rest-assured:rest-assured:4.2.0' 64 | testImplementation 'org.hamcrest:hamcrest:2.2' 65 | 66 | errorprone 'com.google.errorprone:error_prone_core:2.3.4' 67 | errorproneJavac 'com.google.errorprone:javac:9+181-r4173-1' 68 | } 69 | 70 | test { 71 | if (project.hasProperty('restassuredBaseUri')) { 72 | systemProperty 'restassuredBaseUri', project.restassuredBaseUri 73 | } 74 | useJUnitPlatform() 75 | } 76 | 77 | gretty { 78 | integrationTestTask = 'test' 79 | contextPath = '/' 80 | } 81 | 82 | spotless { 83 | java { 84 | googleJavaFormat() 85 | removeUnusedImports() 86 | importOrder 'java', 'javax', 'org', 'com' 87 | } 88 | } 89 | 90 | tasks.withType(JavaCompile).configureEach { 91 | options.errorprone { 92 | // TODO(proppy): Fix requires JDK 9+. 93 | disable('DoubleBraceInitialization') 94 | error('CatchAndPrintStackTrace') 95 | error('SystemExitOutsideMain') 96 | error('ModifiedButNotUsed') 97 | error('UnusedMethod') 98 | error('UnusedVariable') 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /buildSrc/build.gradle: -------------------------------------------------------------------------------- 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 | plugins { 18 | id 'java' 19 | id 'java-gradle-plugin' 20 | } 21 | 22 | // TODO(proppy): workaround https://github.com/GoogleCloudPlatform/app-gradle-plugin/issues/371 23 | gradlePlugin { 24 | plugins { 25 | appenginePlugin { 26 | id = 'com.google.cloud.tools.appengine' 27 | implementationClass = 'com.google.cloud.tools.gradle.appengine.AppEnginePlugin' 28 | } 29 | } 30 | } 31 | 32 | repositories { 33 | mavenCentral() 34 | jcenter() 35 | } 36 | 37 | dependencies { 38 | compileOnly gradleApi() 39 | implementation 'com.google.cloud.tools:appengine-gradle-plugin:2.2.0' 40 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electrofun-smart/my-smart-home-java-mqtt/dfefae10066b09c3dcdb92c681c95063bdc9d318/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | + "
" 22 | + "" 25 | + "" 26 | + "
" 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/MyDataStore.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.util.ArrayList; 18 | import java.util.Date; 19 | import java.util.HashMap; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.concurrent.CompletableFuture; 23 | import java.util.concurrent.ExecutionException; 24 | 25 | import org.eclipse.paho.client.mqttv3.MqttException; 26 | import org.json.JSONObject; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import com.google.actions.api.smarthome.ExecuteRequest; 31 | import com.google.api.core.ApiFuture; 32 | import com.google.auth.oauth2.GoogleCredentials; 33 | import com.google.cloud.firestore.DocumentReference; 34 | import com.google.cloud.firestore.DocumentSnapshot; 35 | import com.google.cloud.firestore.FieldValue; 36 | import com.google.cloud.firestore.Firestore; 37 | import com.google.cloud.firestore.QueryDocumentSnapshot; 38 | import com.google.cloud.firestore.QuerySnapshot; 39 | import com.google.firebase.FirebaseApp; 40 | import com.google.firebase.FirebaseOptions; 41 | import com.google.firebase.cloud.FirestoreClient; 42 | 43 | public class MyDataStore { 44 | 45 | private static final Logger LOGGER = LoggerFactory.getLogger(MySmartHomeApp.class); 46 | private static MyDataStore ourInstance = new MyDataStore(); 47 | 48 | Firestore database; 49 | 50 | private static MyMqtt mqtt; 51 | 52 | static { 53 | try { 54 | mqtt = new MyMqtt(); 55 | } catch (MqttException | IOException e) { 56 | LOGGER.error("Error when creating sample mqtt " + e); 57 | } 58 | } 59 | 60 | public MyDataStore() { 61 | // Use a service account 62 | try { 63 | GoogleCredentials credentials = GoogleCredentials.getApplicationDefault(); 64 | String projectId = System.getenv("GOOGLE_CLOUD_PROJECT"); 65 | FirebaseOptions options = 66 | new FirebaseOptions.Builder().setCredentials(credentials).setProjectId(projectId).build(); 67 | FirebaseApp.initializeApp(options); 68 | database = FirestoreClient.getFirestore(); 69 | } catch (Exception e) { 70 | LOGGER.error("ERROR: invalid service account credentials. See README."); 71 | LOGGER.error(e.getMessage()); 72 | throw new RuntimeException(e); 73 | } 74 | } 75 | 76 | public static MyDataStore getInstance() { 77 | return ourInstance; 78 | } 79 | 80 | public List getDevices(String userId) 81 | throws ExecutionException, InterruptedException { 82 | ApiFuture deviceQuery = 83 | database.collection("users").document(userId).collection("devices").get(); 84 | return deviceQuery.get().getDocuments(); 85 | } 86 | 87 | public String getUserId(String token) throws ExecutionException, InterruptedException { 88 | if (token == null) { 89 | token = "Bearer 123access"; 90 | } 91 | ApiFuture userQuery = 92 | database.collection("users").whereEqualTo("fakeAccessToken", token.substring(7)).get(); 93 | QuerySnapshot usersSnapshot = userQuery.get(); 94 | List users = usersSnapshot.getDocuments(); 95 | 96 | DocumentSnapshot user; 97 | try { 98 | user = users.get(0); 99 | } catch (Exception e) { 100 | LOGGER.error("no user found!"); 101 | throw e; 102 | } 103 | 104 | return user.getId(); 105 | } 106 | 107 | public Boolean isHomegraphEnabled(String userId) throws ExecutionException, InterruptedException { 108 | DocumentSnapshot user = database.collection("users").document(userId).get().get(); 109 | return (Boolean) user.get("homegraph"); 110 | } 111 | 112 | public void setHomegraph(String userId, Boolean enable) { 113 | DocumentReference user = database.collection("users").document(userId); 114 | user.update("homegraph", enable); 115 | } 116 | 117 | public void updateDevice( 118 | String userId, String deviceId, Map states, Map params) 119 | throws ExecutionException, InterruptedException { 120 | DocumentReference device = 121 | database.collection("users").document(userId).collection("devices").document(deviceId); 122 | if (states != null) { 123 | device.update("states", states).get(); 124 | } 125 | if (params.containsKey("name")) { 126 | String name = params.get("name"); 127 | device.update("name", name != null ? name : FieldValue.delete()).get(); 128 | } 129 | if (params.containsKey("nickname")) { 130 | String nickname = params.get("nickname"); 131 | device.update("nickname", nickname != null ? nickname : FieldValue.delete()).get(); 132 | } 133 | if (params.containsKey("errorCode")) { 134 | String errorCode = params.get("errorCode"); 135 | device.update("errorCode", errorCode != null ? errorCode : FieldValue.delete()).get(); 136 | } 137 | if (params.containsKey("tfa")) { 138 | String tfa = params.get("tfa"); 139 | device.update("tfa", tfa != null ? tfa : FieldValue.delete()).get(); 140 | } 141 | if (params.containsKey("localDeviceId")) { 142 | String localDeviceId = params.get("localDeviceId"); 143 | if (localDeviceId != null) { 144 | Map otherDeviceId = new HashMap<>(); 145 | otherDeviceId.put("deviceId", localDeviceId); 146 | List otherDeviceIds = new ArrayList<>(); 147 | otherDeviceIds.add(otherDeviceId); 148 | device.update("otherDeviceIds", otherDeviceIds).get(); 149 | } else { 150 | device.update("otherDeviceIds", FieldValue.delete()).get(); 151 | } 152 | } 153 | } 154 | 155 | public void addDevice(String userId, Map data) 156 | throws ExecutionException, InterruptedException { 157 | String deviceId = (String) data.get("deviceId"); 158 | database 159 | .collection("users") 160 | .document(userId) 161 | .collection("devices") 162 | .document(deviceId) 163 | .set(data) 164 | .get(); 165 | } 166 | 167 | public void deleteDevice(String userId, String deviceId) 168 | throws ExecutionException, InterruptedException { 169 | database 170 | .collection("users") 171 | .document(userId) 172 | .collection("devices") 173 | .document(deviceId) 174 | .delete() 175 | .get(); 176 | } 177 | 178 | public Map getState(String userId, String deviceId) 179 | throws ExecutionException, InterruptedException { 180 | DocumentSnapshot device = 181 | database 182 | .collection("users") 183 | .document(userId) 184 | .collection("devices") 185 | .document(deviceId) 186 | .get() 187 | .get(); 188 | return (Map) device.get("states"); 189 | } 190 | 191 | public Map execute( 192 | String userId, String deviceId, ExecuteRequest.Inputs.Payload.Commands.Execution execution) 193 | throws Exception { 194 | 195 | DocumentSnapshot device = 196 | database 197 | .collection("users") 198 | .document(userId) 199 | .collection("devices") 200 | .document(deviceId) 201 | .get() 202 | .get(); 203 | Map deviceStates = (Map) device.getData().get("states"); 204 | Map states = new HashMap<>(); 205 | 206 | // if (device.contains("states")) { 207 | if (!deviceStates.isEmpty()) { 208 | states.putAll(deviceStates); 209 | } 210 | 211 | if (!(Boolean) states.get("online")) { 212 | throw new Exception("deviceOffline"); 213 | } 214 | 215 | if (device.contains("errorCode") && !device.getString("errorCode").isEmpty()) { 216 | throw new Exception(device.getString("errorCode")); 217 | } 218 | 219 | if (device.contains("tfa")) { 220 | if (device.getString("tfa").equals("ack") && execution.getChallenge() == null) { 221 | throw new Exception("ackNeeded"); 222 | } else if (!device.getString("tfa").isEmpty() && execution.getChallenge() == null) { 223 | throw new Exception("pinNeeded"); 224 | } else if (!device.getString("tfa").isEmpty() && execution.getChallenge() != null) { 225 | String pin = (String) execution.getChallenge().get("pin"); 226 | if (pin != null && !pin.equals(device.getString("tfa"))) { 227 | throw new Exception("challengeFailedPinNeeded"); 228 | } 229 | } 230 | } 231 | 232 | LOGGER.debug("switch execution command MyDataStore line 223"); 233 | 234 | switch (execution.command) { 235 | // action.devices.traits.AppSelector 236 | case "action.devices.commands.appSelect": { 237 | String newApplication = (String) execution.getParams().get("newApplication"); 238 | String newApplicationName = (String) execution.getParams().get("newApplicationName"); 239 | String currentApplication = newApplication != null ? newApplication : newApplicationName; 240 | database 241 | .collection("users") 242 | .document(userId) 243 | .collection("devices") 244 | .document(deviceId) 245 | .update("states.currentApplication", currentApplication); 246 | states.put("currentApplication", currentApplication); 247 | // ------ mqtt sending message to device ---------- 248 | publishMqtt(deviceId, "currentApplication", currentApplication); 249 | // ---------------------------------------------- 250 | break; 251 | } 252 | 253 | case "action.devices.commands.appInstall": { 254 | String newApplication = (String) execution.getParams().get("newApplication"); 255 | String newApplicationName = (String) execution.getParams().get("newApplicationName"); 256 | String currentApplication = newApplication != null ? newApplication : newApplicationName; 257 | LOGGER.info("Install app " + currentApplication); 258 | break; 259 | } 260 | 261 | case "action.devices.commands.appSearch": { 262 | String newApplication = (String) execution.getParams().get("newApplication"); 263 | String newApplicationName = (String) execution.getParams().get("newApplicationName"); 264 | String currentApplication = newApplication != null ? newApplication : newApplicationName; 265 | LOGGER.info("Search for app " + currentApplication); 266 | break; 267 | } 268 | 269 | // action.devices.traits.ArmDisarm 270 | case "action.devices.commands.ArmDisarm": 271 | if (execution.getParams().containsKey("arm")) { 272 | boolean isArmed = (boolean) execution.getParams().get("arm"); 273 | states.put("isArmed", isArmed); 274 | } else if (execution.getParams().containsKey("cancel")) { 275 | // Cancel value is in relation to the arm value 276 | boolean isArmed = (boolean) execution.getParams().get("arm"); 277 | states.put("isArmed", !isArmed); 278 | } 279 | if (execution.getParams().containsKey("armLevel")) { 280 | database 281 | .collection("users") 282 | .document(userId) 283 | .collection("devices") 284 | .document(deviceId) 285 | .update( 286 | "states.isArmed", 287 | states.get("isArmed"), 288 | "states.currentArmLevel", 289 | execution.getParams().get("armLevel")); 290 | states.put("currentArmLevel", execution.getParams().get("armLevel")); 291 | } else { 292 | database 293 | .collection("users") 294 | .document(userId) 295 | .collection("devices") 296 | .document(deviceId) 297 | .update("isArmed", states.get("isArmed")); 298 | } 299 | break; 300 | 301 | // action.devices.traits.Brightness 302 | case "action.devices.commands.BrightnessAbsolute": 303 | database 304 | .collection("users") 305 | .document(userId) 306 | .collection("devices") 307 | .document(deviceId) 308 | .update("states.brightness", execution.getParams().get("brightness")); 309 | states.put("brightness", execution.getParams().get("brightness")); 310 | // ------ mqtt sending message to device ---------- 311 | publishMqtt(deviceId, "brightness", execution.getParams().get("brightness")); 312 | // ---------------------------------------------- 313 | break; 314 | 315 | // action.devices.traits.CameraStream 316 | case "action.devices.commands.GetCameraStream": 317 | states.put("cameraStreamAccessUrl", "https://fluffysheep.com/baaaaa.mp4"); 318 | // ------ mqtt sending message to device ---------- 319 | publishMqtt(deviceId, "cameraStreamAccessUrl", "https://fluffysheep.com/baaaaa.mp4"); 320 | // ---------------------------------------------- 321 | break; 322 | 323 | // action.devices.traits.ColorSetting 324 | case "action.devices.commands.ColorAbsolute": 325 | String colorType; 326 | Object color; 327 | Map colorMap = (Map) execution.getParams().get("color"); 328 | 329 | if (colorMap.containsKey("spectrumRGB")) { 330 | database 331 | .collection("users") 332 | .document(userId) 333 | .collection("devices") 334 | .document(deviceId) 335 | .update("states.color.spectrumRgb", colorMap.get("spectrumRGB")); 336 | color = colorMap.get("spectrumRGB"); 337 | colorType = "spectrumRgb"; 338 | } else { 339 | if (colorMap.containsKey("spectrumHSV")) { 340 | database 341 | .collection("users") 342 | .document(userId) 343 | .collection("devices") 344 | .document(deviceId) 345 | .update("states.color.spectrumHsv", colorMap.get("spectrumHSV")); 346 | colorType = "spectrumHsv"; 347 | color = colorMap.get("spectrumHSV"); 348 | 349 | } else { 350 | if (colorMap.containsKey("temperature")) { 351 | database 352 | .collection("users") 353 | .document(userId) 354 | .collection("devices") 355 | .document(deviceId) 356 | .update("states.color.temperatureK", colorMap.get("temperature")); 357 | colorType = "temperatureK"; 358 | color = colorMap.get("temperature"); 359 | 360 | } else { 361 | throw new Exception("notSupported"); 362 | } 363 | } 364 | } 365 | //states.put(colorType, color); 366 | // ------ mqtt sending message to device ---------- 367 | publishMqtt(deviceId, colorType, color); 368 | // ---------------------------------------------- 369 | break; 370 | 371 | // action.devices.traits.Cook 372 | case "action.devices.commands.Cook": 373 | boolean startCooking = (boolean) execution.getParams().get("start"); 374 | if (startCooking) { 375 | // Start cooking 376 | Map dbStates = 377 | new HashMap() { 378 | { 379 | put("states.currentCookingMode", execution.getParams().get("cookingMode")); 380 | } 381 | }; 382 | if (execution.getParams().containsKey("foodPreset")) { 383 | dbStates.put("states.currentFoodPreset", execution.getParams().get("foodPreset")); 384 | } else { 385 | dbStates.put("states.currentFoodPreset", "NONE"); 386 | } 387 | if (execution.getParams().containsKey("quantity")) { 388 | dbStates.put("states.currentFoodQuantity", execution.getParams().get("quantity")); 389 | } else { 390 | dbStates.put("states.currentFoodQuantity", 0); 391 | } 392 | if (execution.getParams().containsKey("unit")) { 393 | dbStates.put("states.currentFoodUnit", execution.getParams().get("unit")); 394 | } else { 395 | dbStates.put("states.currentFoodUnit", "NONE"); 396 | } 397 | database 398 | .collection("users") 399 | .document(userId) 400 | .collection("devices") 401 | .document("deviceId") 402 | .update(dbStates); 403 | // Server getting response will handle any undefined values 404 | states.put("currentCookingMode", execution.getParams().get("cookingMode")); 405 | states.put("currentFoodPreset", execution.getParams().get("foodPreset")); 406 | states.put("currentFoodQuantity", execution.getParams().get("quantity")); 407 | states.put("currentFoodUnit", execution.getParams().get("unit")); 408 | } else { 409 | // Done cooking, reset 410 | database 411 | .collection("users") 412 | .document(userId) 413 | .collection("devices") 414 | .document("deviceId") 415 | .update( 416 | new HashMap() { 417 | { 418 | put("states.currentCookingMode", "NONE"); 419 | put("states.currentFoodPreset", "NONE"); 420 | put("states.currentFoodQuantity", 0); 421 | put("states.currentFoodUnit", "NONE"); 422 | } 423 | }); 424 | states.put("currentCookingMode", "NONE"); 425 | states.put("currentFoodPreset", "NONE"); 426 | } 427 | publishMqtt(deviceId, "start", execution.getParams().get("start")); 428 | break; 429 | 430 | case "action.devices.commands.selectChannel": 431 | // "params":{"channelCode":"cnn","channelName":"CNN","channelNumber":"200"} 432 | 433 | // ------ mqtt sending message to device ---------- 434 | publishMqtt(deviceId, "channelNumber", execution.getParams().get("channelNumber")); 435 | 436 | break; 437 | // action.devices.traits.Dispense 438 | case "action.devices.commands.Dispense": 439 | int amount = (int) execution.getParams().get("amount"); 440 | String unit = (String) execution.getParams().get("unit"); 441 | if (execution.getParams().containsKey("presetName") 442 | && execution.getParams().get("presetName").equals("cat food bowl")) { 443 | // Fill in params 444 | amount = 4; 445 | unit = "CUPS"; 446 | } 447 | Map amountLastDispensed = new HashMap(); 448 | amountLastDispensed.put("amount", amount); 449 | amountLastDispensed.put("unit", unit); 450 | Map dispenseUpdates = new HashMap<>(); 451 | dispenseUpdates.put( 452 | "states.dispenseItems", 453 | new HashMap[] { 454 | new HashMap() { 455 | { 456 | put("itemName", execution.getParams().get("item")); 457 | put("amountLastDispensed", amountLastDispensed); 458 | put("isCurrentlyDispensing", execution.getParams().containsKey("presetName")); 459 | } 460 | } 461 | }); 462 | database 463 | .collection("users") 464 | .document(userId) 465 | .collection("devices") 466 | .document(deviceId) 467 | .update(dispenseUpdates); 468 | states.put( 469 | "dispenseItems", 470 | new HashMap[] { 471 | new HashMap() { 472 | { 473 | put("itemName", execution.getParams().get("item")); 474 | put("amountLastDispensed", amountLastDispensed); 475 | put("isCurrentlyDispensing", execution.getParams().containsKey("presetName")); 476 | } 477 | } 478 | }); 479 | break; 480 | 481 | // action.devices.traits.Dock 482 | case "action.devices.commands.Dock": 483 | // This has no parameters 484 | database 485 | .collection("users") 486 | .document(userId) 487 | .collection("devices") 488 | .document(deviceId) 489 | .update("states.isDocked", true); 490 | states.put("isDocked", true); 491 | break; 492 | 493 | // action.devices.traits.EnergyStorage 494 | case "action.devices.commands.Charge": 495 | database 496 | .collection("users") 497 | .document(userId) 498 | .collection("devices") 499 | .document(deviceId) 500 | .update("states.isCharging", execution.getParams().get("charge")); 501 | states.put("isCharging", execution.getParams().get("charge")); 502 | break; 503 | 504 | // action.devices.traits.FanSpeed 505 | case "action.devices.commands.SetFanSpeed": 506 | database 507 | .collection("users") 508 | .document(userId) 509 | .collection("devices") 510 | .document(deviceId) 511 | .update("states.currentFanSpeedSetting", execution.getParams().get("fanSpeed")); 512 | states.put("currentFanSpeedSetting", execution.getParams().get("fanSpeed")); 513 | // ------ mqtt sending message to device ---------- 514 | publishMqtt(deviceId, "currentFanSpeedSetting", execution.getParams().get("fanSpeed")); 515 | // ---------------------------------------------- 516 | break; 517 | 518 | case "action.devices.commands.Reverse": 519 | database 520 | .collection("users") 521 | .document(userId) 522 | .collection("devices") 523 | .document(deviceId) 524 | .update("states.currentFanSpeedReverse", true); 525 | // ------ mqtt sending message to device ---------- 526 | publishMqtt(deviceId, "currentFanSpeedReverse", true); 527 | // ---------------------------------------------- 528 | break; 529 | 530 | // action.devices.traits.Fill 531 | case "action.devices.commands.Fill": 532 | Map updates = new HashMap<>(); 533 | String currentFillLevel = "none"; 534 | boolean fill = (boolean) execution.getParams().get("fill"); 535 | if (fill) { 536 | if (execution.getParams().containsKey("fillLevel")) { 537 | currentFillLevel = (String) execution.getParams().get("fillLevel"); 538 | } else { 539 | currentFillLevel = "half"; // Default fill level 540 | } 541 | } // Else the device is draining and the fill level is set to "none" by default 542 | updates.put("states.isFilled", fill); 543 | updates.put("states.currentFillLevel", currentFillLevel); 544 | database 545 | .collection("users") 546 | .document(userId) 547 | .collection("devices") 548 | .document(deviceId) 549 | .update(updates); 550 | states.put("isFilled", fill); 551 | states.put("currentFillLevel", currentFillLevel); 552 | break; 553 | 554 | // action.devices.traits.HumiditySetting 555 | case "action.devices.commands.SetHumidity": 556 | database 557 | .collection("users") 558 | .document(userId) 559 | .collection("devices") 560 | .document(deviceId) 561 | .update( 562 | "states.humiditySetpointPercent", 563 | execution.getParams().get("humiditySetpointPercent")); 564 | states.put("humiditySetpointPercent", execution.getParams().get("humiditySetpointPercent")); 565 | // ------ mqtt sending message to device ---------- 566 | publishMqtt(deviceId, "humiditySetPointPercent", execution.getParams().get("humiditySetpointPercent")); 567 | // ---------------------------------------------- 568 | break; 569 | 570 | // action.devices.traits.InputSelector 571 | case "action.devices.commands.SetInput": { 572 | String newInput = (String) execution.getParams().get("newInput"); 573 | database 574 | .collection("users") 575 | .document(userId) 576 | .collection("devices") 577 | .document(deviceId) 578 | .update("states.currentInput", newInput); 579 | states.put("currentInput", newInput); 580 | // ------ mqtt sending message to device ---------- 581 | publishMqtt(deviceId, "currentInput", newInput); 582 | // ---------------------------------------------- 583 | break; 584 | } 585 | 586 | case "action.devices.commands.PreviousInput": { 587 | Map attributes = (Map) device.getData().get("attributes"); 588 | String currentInput = (String) deviceStates.get("currentInput"); 589 | Map[] availableInputs = 590 | (Map[]) attributes.get("availableInputs"); 591 | int index = -1; 592 | for (int i = 0; i < availableInputs.length; i++) { 593 | String input = (String) availableInputs[i].get("key"); 594 | if (currentInput.equals(input)) { 595 | index = i; 596 | } 597 | } 598 | int previousInputIndex = Math.min(index - 1, 0); 599 | String newInput = (String) availableInputs[previousInputIndex].get("key"); 600 | 601 | database 602 | .collection("users") 603 | .document(userId) 604 | .collection("devices") 605 | .document(deviceId) 606 | .update("states.currentInput", newInput); 607 | states.put("currentInput", newInput); 608 | // ------ mqtt sending message to device ---------- 609 | publishMqtt(deviceId, "currentInput", newInput); 610 | // ---------------------------------------------- 611 | break; 612 | } 613 | 614 | case "action.devices.commands.NextInput": { 615 | Map attributes = (Map) device.getData().get("attributes"); 616 | String currentInput = (String) deviceStates.get("currentInput"); 617 | Map[] availableInputs = 618 | (Map[]) attributes.get("availableInputs"); 619 | int index = -1; 620 | for (int i = 0; i < availableInputs.length; i++) { 621 | String input = (String) availableInputs[i].get("key"); 622 | if (currentInput.equals(input)) { 623 | index = i; 624 | } 625 | } 626 | int nextInputIndex = Math.min(index + 1, availableInputs.length - 1); 627 | String newInput = (String) availableInputs[nextInputIndex].get("key"); 628 | 629 | database 630 | .collection("users") 631 | .document(userId) 632 | .collection("devices") 633 | .document(deviceId) 634 | .update("states.currentInput", newInput); 635 | states.put("currentInput", newInput); 636 | // ------ mqtt sending message to device ---------- 637 | publishMqtt(deviceId, "currentInput", newInput); 638 | // ---------------------------------------------- 639 | break; 640 | } 641 | 642 | // action.devices.traits.Locator 643 | case "action.devices.commands.Locate": 644 | database 645 | .collection("users") 646 | .document(userId) 647 | .collection("devices") 648 | .document(deviceId) 649 | .update( 650 | "states.silent", 651 | execution.getParams().get("silent"), 652 | "states.generatedAlert", 653 | true); 654 | states.put("generatedAlert", true); 655 | // ------ mqtt sending message to device ---------- 656 | publishMqtt(deviceId, "generatedAlert", true); 657 | // ---------------------------------------------- 658 | break; 659 | 660 | // action.devices.traits.LockUnlock 661 | case "action.devices.commands.LockUnlock": 662 | database 663 | .collection("users") 664 | .document(userId) 665 | .collection("devices") 666 | .document(deviceId) 667 | .update("states.isLocked", execution.getParams().get("lock")); 668 | states.put("isLocked", execution.getParams().get("lock")); 669 | // ------ mqtt sending message to device ---------- 670 | publishMqtt(deviceId, "isLocked", execution.getParams().get("lock")); 671 | // ---------------------------------------------- 672 | break; 673 | 674 | // action.devices.traits.NetworkControl 675 | case "action.devices.commands.EnableDisableGuestNetwork": { 676 | database 677 | .collection("users") 678 | .document(userId) 679 | .collection("devices") 680 | .document(deviceId) 681 | .update("states.guestNetworkEnabled", execution.getParams().get("enable")); 682 | states.put("guestNetworkEnabled", execution.getParams().get("enable")); 683 | break; 684 | } 685 | 686 | case "action.devices.commands.EnableDisableNetworkProfile": { 687 | List profiles = 688 | (List) ((Map) device.getData().get("attributes")).get("networkProfiles"); 689 | boolean profileExists = 690 | profiles.stream() 691 | .anyMatch( 692 | (String profile) -> profile.equals(execution.getParams().get("profile"))); 693 | if (!profileExists) { 694 | throw new RuntimeException("networkProfileNotRecognized"); 695 | } 696 | // No state change occurs 697 | break; 698 | } 699 | 700 | case "action.devices.commands.TestNetworkSpeed": { 701 | boolean testDownloadSpeed = (boolean) execution.getParams().get("testDownloadSpeed"); 702 | boolean testUploadSpeed = (boolean) execution.getParams().get("testUploadSpeed"); 703 | Map lastNetworkDownloadSpeedTest = 704 | (Map) ((Map) device.getData().get("states")) 705 | .get("lastNetworkDownloadSpeedTest"); 706 | Map lastNetworkUploadSpeedTest = 707 | (Map) ((Map) device.getData().get("states")) 708 | .get("lastNetworkUploadSpeedTest"); 709 | int unixTimestampSec = Math.toIntExact(new Date().getTime() / 1000); 710 | if (testDownloadSpeed) { 711 | lastNetworkDownloadSpeedTest.put("downloadSpeedMbps", (Math.random() * 100)); 712 | lastNetworkDownloadSpeedTest.put("unixTimestampSec", unixTimestampSec); 713 | } 714 | if (testUploadSpeed) { 715 | lastNetworkUploadSpeedTest.put("uploadSpeedMbps", (Math.random() * 100)); 716 | lastNetworkUploadSpeedTest.put("unixTimestampSec", unixTimestampSec); 717 | } 718 | 719 | database 720 | .collection("users") 721 | .document(userId) 722 | .collection("devices") 723 | .document(deviceId) 724 | .update( 725 | "states.lastNetworkDownloadSpeedTest", lastNetworkDownloadSpeedTest, 726 | "states.lastNetworkUploadSpeedTest", lastNetworkUploadSpeedTest); 727 | throw new RuntimeException("PENDING"); 728 | } 729 | 730 | case "action.devices.commands.GetGuestNetworkPassword": { 731 | states.put("guestNetworkPassword", "wifi-password-123"); 732 | } 733 | 734 | // action.devices.traits.OnOff 735 | case "action.devices.commands.OnOff": 736 | database 737 | .collection("users") 738 | .document(userId) 739 | .collection("devices") 740 | .document(deviceId) 741 | .update("states.on", execution.getParams().get("on")); 742 | states.put("on", execution.getParams().get("on")); 743 | // ------ mqtt sending message to device ---------- 744 | publishMqtt(deviceId, "on", execution.getParams().get("on")); 745 | // ------------------------------------------------ 746 | break; 747 | 748 | // action.devices.traits.OpenClose 749 | case "action.devices.commands.OpenClose": 750 | // Check if the device can open in multiple directions 751 | Map attributes = (Map) device.getData().get("attributes"); 752 | if (attributes != null && attributes.containsKey("openDirection")) { 753 | // The device can open in more than one direction 754 | String direction = (String) execution.getParams().get("openDirection"); 755 | List> openStates = 756 | (List>) states.get("openState"); 757 | openStates.forEach( 758 | state -> { 759 | if (state.get("openDirection").equals(direction)) { 760 | state.put("openPercent", execution.getParams().get("openPercent")); 761 | } 762 | }); 763 | states.put("openStates", openStates); 764 | database 765 | .collection("users") 766 | .document(userId) 767 | .collection("devices") 768 | .document(deviceId) 769 | .update("states.openState", openStates); 770 | // ------ mqtt sending message to device ---------- 771 | publishMqtt(deviceId, "openState", openStates); 772 | // ---------------------------------------------- 773 | 774 | } else { 775 | // The device can only open in one direction 776 | database 777 | .collection("users") 778 | .document(userId) 779 | .collection("devices") 780 | .document(deviceId) 781 | .update("states.openPercent", execution.getParams().get("openPercent")); 782 | states.put("openPercent", execution.getParams().get("openPercent")); 783 | // ------ mqtt sending message to device ---------- 784 | publishMqtt(deviceId, "openPercent", execution.getParams().get("openPercent")); 785 | // ---------------------------------------------- 786 | } 787 | break; 788 | 789 | // action.devices.traits.Reboot 790 | case "action.devices.commands.Reboot": 791 | database 792 | .collection("users") 793 | .document(userId) 794 | .collection("devices") 795 | .document(deviceId) 796 | .update("states.online", false); 797 | break; 798 | 799 | // action.devices.traits.Rotation 800 | case "action.devices.commands.RotateAbsolute": 801 | // Check if the device can open in multiple directions 802 | if (execution.getParams().containsKey("rotationPercent")) { 803 | database 804 | .collection("users") 805 | .document(userId) 806 | .collection("devices") 807 | .document(deviceId) 808 | .update("states.rotationPercent", execution.getParams().get("rotationPercent")); 809 | states.put("rotationPercent", execution.getParams().get("rotationPercent")); 810 | // ------ mqtt sending message to device ---------- 811 | publishMqtt(deviceId, "rotationPercent", execution.getParams().get("rotationPercent")); 812 | // ---------------------------------------------- 813 | } else if (execution.getParams().containsKey("rotationDegrees")) { 814 | database 815 | .collection("users") 816 | .document(userId) 817 | .collection("devices") 818 | .document(deviceId) 819 | .update("states.rotationDegrees", execution.getParams().get("rotationDegrees")); 820 | states.put("rotationDegrees", execution.getParams().get("rotationDegrees")); 821 | // ------ mqtt sending message to device ---------- 822 | publishMqtt(deviceId, "rotationDegrees", execution.getParams().get("rotationDegrees")); 823 | // ---------------------------------------------- 824 | } 825 | break; 826 | 827 | // action.devices.traits.RunCycle - No execution 828 | // action.devices.traits.Scene 829 | case "action.devices.commands.ActivateScene": 830 | database 831 | .collection("users") 832 | .document(userId) 833 | .collection("devices") 834 | .document(deviceId) 835 | .update("states.deactivate", execution.getParams().get("deactivate")); 836 | // Scenes are stateless 837 | break; 838 | 839 | // action.devices.traits.SoftwareUpdate 840 | case "action.devices.commands.SoftwareUpdate": 841 | database 842 | .collection("users") 843 | .document(userId) 844 | .collection("devices") 845 | .document(deviceId) 846 | .update( 847 | new HashMap() { 848 | { 849 | put("states.online", false); 850 | put("states.lastSoftwareUpdateUnixTimestampSec", new Date().getTime() / 1000); 851 | } 852 | }); 853 | break; 854 | 855 | // action.devices.traits.StartStop 856 | case "action.devices.commands.StartStop": 857 | database 858 | .collection("users") 859 | .document(userId) 860 | .collection("devices") 861 | .document(deviceId) 862 | .update("states.isRunning", execution.getParams().get("start")); 863 | states.put("isRunning", execution.getParams().get("start")); 864 | // ------ mqtt sending message to device ---------- 865 | publishMqtt(deviceId, "isRunning", execution.getParams().get("start")); 866 | // ---------------------------------------------- 867 | break; 868 | 869 | case "action.devices.commands.PauseUnpause": 870 | database 871 | .collection("users") 872 | .document(userId) 873 | .collection("devices") 874 | .document(deviceId) 875 | .update("states.isPaused", execution.getParams().get("pause")); 876 | states.put("isPaused", execution.getParams().get("pause")); 877 | // ------ mqtt sending message to device ---------- 878 | publishMqtt(deviceId, "isPaused", execution.getParams().get("pause")); 879 | // ---------------------------------------------- 880 | break; 881 | 882 | // action.devices.traits.Modes 883 | case "action.devices.commands.SetModes": 884 | Map currentModeSettings = 885 | (Map) states.getOrDefault("currentModeSettings", new HashMap()); 886 | currentModeSettings.putAll( 887 | (Map) execution 888 | .getParams() 889 | .getOrDefault("updateModeSettings", new HashMap())); 890 | database 891 | .collection("users") 892 | .document(userId) 893 | .collection("devices") 894 | .document(deviceId) 895 | .update("states.currentModeSettings", currentModeSettings); 896 | states.put("currentModeSettings", currentModeSettings); 897 | // ------ mqtt sending message to device ---------- 898 | publishMqtt(deviceId, "currentModeSettings", currentModeSettings); 899 | // ---------------------------------------------- 900 | break; 901 | 902 | // action.devices.traits.Timer 903 | case "action.devices.commands.TimerStart": 904 | database 905 | .collection("users") 906 | .document(userId) 907 | .collection("devices") 908 | .document(deviceId) 909 | .update("states.timerRemainingSec", execution.getParams().get("timerTimeSec")); 910 | states.put("timerRemainingSec", execution.getParams().get("timerTimeSec")); 911 | // ------ mqtt sending message to device ---------- 912 | publishMqtt(deviceId, "timerRemainingSec", execution.getParams().get("timerTimeSec")); 913 | // ---------------------------------------------- 914 | break; 915 | 916 | case "action.devices.commands.TimerAdjust": 917 | if ((int) states.get("timerRemainingSec") == -1) { 918 | // No timer exists 919 | throw new RuntimeException("noTimerExists"); 920 | } 921 | int newTimerRemainingSec = 922 | (int) states.get("timerRemainingSec") + (int) execution.getParams().get("timerTimeSec"); 923 | if (newTimerRemainingSec < 0) { 924 | throw new RuntimeException("valueOutOfRange"); 925 | } 926 | database 927 | .collection("users") 928 | .document(userId) 929 | .collection("devices") 930 | .document(deviceId) 931 | .update("states.timerRemainingSec", newTimerRemainingSec); 932 | states.put("timerRemainingSec", newTimerRemainingSec); 933 | // ------ mqtt sending message to device ---------- 934 | publishMqtt(deviceId, "timerRemainingSec", newTimerRemainingSec); 935 | // ---------------------------------------------- 936 | break; 937 | 938 | case "action.devices.commands.TimerPause": 939 | if ((int) states.get("timerRemainingSec") == -1) { 940 | // No timer exists 941 | throw new RuntimeException("noTimerExists"); 942 | } 943 | database 944 | .collection("users") 945 | .document(userId) 946 | .collection("devices") 947 | .document(deviceId) 948 | .update("states.timerPaused", true); 949 | states.put("timerPaused", true); 950 | // ------ mqtt sending message to device ---------- 951 | publishMqtt(deviceId, "timerPaused", true); 952 | // ---------------------------------------------- 953 | break; 954 | 955 | case "action.devices.commands.TimerResume": 956 | if ((int) states.get("timerRemainingSec") == -1) { 957 | // No timer exists 958 | throw new RuntimeException("noTimerExists"); 959 | } 960 | database 961 | .collection("users") 962 | .document(userId) 963 | .collection("devices") 964 | .document(deviceId) 965 | .update("states.timerPaused", false); 966 | states.put("timerPaused", false); 967 | // ------ mqtt sending message to device ---------- 968 | publishMqtt(deviceId, "timerPaused", false); 969 | // ---------------------------------------------- 970 | break; 971 | 972 | case "action.devices.commands.TimerCancel": 973 | if ((int) states.get("timerRemainingSec") == -1) { 974 | // No timer exists 975 | throw new RuntimeException("noTimerExists"); 976 | } 977 | database 978 | .collection("users") 979 | .document(userId) 980 | .collection("devices") 981 | .document(deviceId) 982 | .update("states.timerRemainingSec", -1); 983 | states.put("timerRemainingSec", 0); 984 | // ------ mqtt sending message to device ---------- 985 | publishMqtt(deviceId, "timerRemainingSec", 0); 986 | // ---------------------------------------------- 987 | break; 988 | 989 | // action.devices.traits.Toggles 990 | case "action.devices.commands.SetToggles": 991 | Map currentToggleSettings = 992 | (Map) states.getOrDefault("currentToggleSettings", new HashMap()); 993 | currentToggleSettings.putAll( 994 | (Map) execution 995 | .getParams() 996 | .getOrDefault("updateToggleSettings", new HashMap())); 997 | database 998 | .collection("users") 999 | .document(userId) 1000 | .collection("devices") 1001 | .document(deviceId) 1002 | .update("states.currentToggleSettings", currentToggleSettings); 1003 | states.put("currentToggleSettings", currentToggleSettings); 1004 | // ------ mqtt sending message to device ---------- 1005 | publishMqtt(deviceId, "currentToggleSettings", currentToggleSettings); 1006 | // ---------------------------------------------- 1007 | break; 1008 | 1009 | // action.devices.traits.TemperatureControl 1010 | case "action.devices.commands.SetTemperature": 1011 | database 1012 | .collection("users") 1013 | .document(userId) 1014 | .collection("devices") 1015 | .document(deviceId) 1016 | .update("states.temperatureSetpointCelsius", execution.getParams().get("temperature")); 1017 | states.put("temperatureSetpointCelsius", execution.getParams().get("temperature")); 1018 | states.put("temperatureAmbientCelsius", deviceStates.get("temperatureAmbientCelsius")); 1019 | // ------ mqtt sending message to device ---------- 1020 | publishMqtt(deviceId, "temperatureSetpointCelsius", execution.getParams().get("temperature")); 1021 | // ---------------------------------------------- 1022 | break; 1023 | 1024 | // action.devices.traits.TemperatureSetting 1025 | case "action.devices.commands.ThermostatTemperatureSetpoint": 1026 | database 1027 | .collection("users") 1028 | .document(userId) 1029 | .collection("devices") 1030 | .document(deviceId) 1031 | .update( 1032 | "states.thermostatTemperatureSetpoint", 1033 | execution.getParams().get("thermostatTemperatureSetpoint")); 1034 | states.put( 1035 | "thermostatTemperatureSetpoint", 1036 | execution.getParams().get("thermostatTemperatureSetpoint")); 1037 | states.put("thermostatMode", deviceStates.get("states.thermostatMode")); 1038 | states.put( 1039 | "thermostatTemperatureAmbient", deviceStates.get("thermostatTemperatureAmbient")); 1040 | states.put("thermostatHumidityAmbient", deviceStates.get("thermostatHumidityAmbient")); 1041 | if (states.containsKey("online")) { 1042 | states.remove("online"); 1043 | } 1044 | // ------ mqtt sending message to device ---------- 1045 | publishMqtt(deviceId, "thermostatTemperatureSetpoint", execution.getParams().get("thermostatTemperatureSetpoint")); 1046 | // ---------------------------------------------- 1047 | break; 1048 | 1049 | case "action.devices.commands.ThermostatTemperatureSetRange": 1050 | database 1051 | .collection("users") 1052 | .document(userId) 1053 | .collection("devices") 1054 | .document(deviceId) 1055 | .update( 1056 | "states.thermostatTemperatureSetpointLow", 1057 | execution.getParams().get("thermostatTemperatureSetpointLow"), 1058 | "states.thermostatTemperatureSetpointHigh", 1059 | execution.getParams().get("thermostatTemperatureSetpointHigh")); 1060 | states.put( 1061 | "thermostatTemperatureSetpoint", deviceStates.get("thermostatTemperatureSetpoint")); 1062 | states.put("thermostatMode", deviceStates.get("thermostatMode")); 1063 | states.put( 1064 | "thermostatTemperatureAmbient", deviceStates.get("thermostatTemperatureAmbient")); 1065 | states.put("thermostatHumidityAmbient", deviceStates.get("thermostatHumidityAmbient")); 1066 | // ------ mqtt sending message to device ---------- 1067 | publishMqtt(deviceId, "thermostatTemperatureSetpointLow", execution.getParams().get("thermostatTemperatureSetpointLow")); 1068 | // ---------------------------------------------- 1069 | // ------ mqtt sending message to device ---------- 1070 | publishMqtt(deviceId, "thermostatTemperatureSetpointHigh", execution.getParams().get("thermostatTemperatureSetpointHigh")); 1071 | // ---------------------------------------------- 1072 | break; 1073 | 1074 | case "action.devices.commands.ThermostatSetMode": 1075 | database 1076 | .collection("users") 1077 | .document(userId) 1078 | .collection("devices") 1079 | .document(deviceId) 1080 | .update("states.thermostatMode", execution.getParams().get("thermostatMode")); 1081 | states.put("thermostatMode", execution.getParams().get("thermostatMode")); 1082 | states.put( 1083 | "thermostatTemperatureSetpoint", deviceStates.get("thermostatTemperatureSetpoint")); 1084 | states.put( 1085 | "thermostatTemperatureAmbient", deviceStates.get("thermostatTemperatureAmbient")); 1086 | states.put("thermostatHumidityAmbient", deviceStates.get("thermostatHumidityAmbient")); 1087 | // ------ mqtt sending message to device ---------- 1088 | publishMqtt(deviceId, "thermostatMode", execution.getParams().get("thermostatMode")); 1089 | // ---------------------------------------------- 1090 | break; 1091 | 1092 | // action.devices.traits.TransportControl 1093 | // Traits are considered no-ops as they have no state 1094 | case "action.devices.commands.mediaPrevious": 1095 | LOGGER.info("Play the previous media"); 1096 | break; 1097 | 1098 | case "action.devices.commands.mediaNext": 1099 | LOGGER.info("Play the next media"); 1100 | break; 1101 | 1102 | case "action.devices.commands.mediaRepeatMode": 1103 | Boolean isOn = (Boolean) execution.getParams().get("isOn"); 1104 | Boolean isSingle = (Boolean) execution.getParams().get("isSingle"); 1105 | LOGGER.info("Repeat mode enabled: " + isOn + ". Single item enabled: " + isSingle); 1106 | break; 1107 | 1108 | case "action.devices.commands.mediaShuffle": 1109 | LOGGER.info("Shuffle the playlist of media"); 1110 | break; 1111 | 1112 | case "action.devices.commands.mediaClosedCaptioningOn": 1113 | String ccLanguage = (String) execution.getParams().get("closedCaptioningLanguage"); 1114 | String uqLanguage = (String) execution.getParams().get("userQueryLanguage"); 1115 | LOGGER.info("Closed captioning enabled for " + ccLanguage + " for user in " + uqLanguage); 1116 | break; 1117 | 1118 | case "action.devices.commands.mediaClosedCaptioningOff": 1119 | LOGGER.info("Closed captioning disabled"); 1120 | break; 1121 | 1122 | case "action.devices.commands.mediaPause": 1123 | database 1124 | .collection("users") 1125 | .document(userId) 1126 | .collection("devices") 1127 | .document(deviceId) 1128 | .update("states.playbackState", "PAUSED"); 1129 | states.put("playbackState", "PAUSED"); 1130 | // ------ mqtt sending message to device ---------- 1131 | publishMqtt(deviceId, "playbackState", "PAUSED"); 1132 | // ---------------------------------------------- 1133 | break; 1134 | 1135 | case "action.devices.commands.mediaResume": 1136 | database 1137 | .collection("users") 1138 | .document(userId) 1139 | .collection("devices") 1140 | .document(deviceId) 1141 | .update("states.playbackState", "PLAYING"); 1142 | states.put("playbackState", "PLAYING"); 1143 | // ------ mqtt sending message to device ---------- 1144 | publishMqtt(deviceId, "playbackState", "PLAYING"); 1145 | // ---------------------------------------------- 1146 | break; 1147 | 1148 | case "action.devices.commands.mediaStop": 1149 | database 1150 | .collection("users") 1151 | .document(userId) 1152 | .collection("devices") 1153 | .document(deviceId) 1154 | .update("states.playbackState", "STOPPED"); 1155 | states.put("playbackState", "STOPPED"); 1156 | // ------ mqtt sending message to device ---------- 1157 | publishMqtt(deviceId, "playbackState", "STOPPED"); 1158 | // ---------------------------------------------- 1159 | break; 1160 | 1161 | case "action.devices.commands.mediaSeekRelative": 1162 | int relativePositionMs = (int) execution.getParams().get("relativePositionMs"); 1163 | LOGGER.info("Seek to (now + " + relativePositionMs + ") ms"); 1164 | break; 1165 | 1166 | case "action.devices.commands.mediaSeekToPosition": 1167 | int absPositionMs = (int) execution.getParams().get("absPositionMs"); 1168 | LOGGER.info("Seek to " + absPositionMs + " ms"); 1169 | break; 1170 | 1171 | // action.devices.traits.Volume 1172 | case "action.devices.commands.setVolume": 1173 | int volumeLevel = (int) execution.getParams().get("volumeLevel"); 1174 | database 1175 | .collection("users") 1176 | .document(userId) 1177 | .collection("devices") 1178 | .document(deviceId) 1179 | .update("states.currentVolume", volumeLevel); 1180 | states.put("currentVolume", volumeLevel); 1181 | // ------ mqtt sending message to device ---------- 1182 | publishMqtt(deviceId, "currentVolume", volumeLevel); 1183 | // ---------------------------------------------- 1184 | break; 1185 | 1186 | case "action.devices.commands.volumeRelative": 1187 | int relativeSteps = Integer.valueOf(execution.getParams().get("relativeSteps").toString()); 1188 | int currentVolume = new Double(deviceStates.get("currentVolume").toString()).intValue(); 1189 | int newVolume = currentVolume + relativeSteps; 1190 | database 1191 | .collection("users") 1192 | .document(userId) 1193 | .collection("devices") 1194 | .document(deviceId) 1195 | .update("states.currentVolume", newVolume); 1196 | states.put("currentVolume", newVolume); 1197 | // ------ mqtt sending message to device ---------- 1198 | publishMqtt(deviceId, "currentVolume", newVolume); 1199 | // ----------------------------------------------- 1200 | break; 1201 | 1202 | case "action.devices.commands.mute": 1203 | boolean mute = (boolean) execution.getParams().get("mute"); 1204 | database 1205 | .collection("users") 1206 | .document(userId) 1207 | .collection("devices") 1208 | .document(deviceId) 1209 | .update("states.isMuted", mute); 1210 | states.put("isMuted", mute); 1211 | // ------ mqtt sending message to device ---------- 1212 | publishMqtt(deviceId, "isMuted", mute); 1213 | // ----------------------------------------------- 1214 | break; 1215 | } 1216 | 1217 | return states; 1218 | } 1219 | 1220 | private void publishMqtt(String topic, String key, Object value) { 1221 | CompletableFuture.runAsync(() -> { 1222 | try { 1223 | Map myState = new HashMap<>(); 1224 | myState.put(key, value); 1225 | JSONObject json = new JSONObject(myState); 1226 | String msg = json.toString(); 1227 | mqtt.publish(topic + "-client", 0, msg.getBytes()); 1228 | LOGGER.debug("Message = " + msg + " sent by MQTT from MyDataStore"); 1229 | } catch (Throwable throwable) { 1230 | LOGGER.error("failed to publish iot device: {" + topic + "}", throwable); 1231 | } 1232 | }); 1233 | } 1234 | } 1235 | -------------------------------------------------------------------------------- /src/main/java/com/example/MyMqtt.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2009, 2014 IBM Corp. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Eclipse Public License v2.0 5 | * and Eclipse Distribution License v1.0 which accompany this distribution. 6 | * The Eclipse Public License is available at 7 | * https://www.eclipse.org/legal/epl-2.0 8 | * and the Eclipse Distribution License is available at 9 | * https://www.eclipse.org/org/documents/edl-v10.php 10 | * Contributors: 11 | * Dave Locke - initial API and implementation and/or initial documentation 12 | */ 13 | 14 | package com.example; 15 | 16 | import java.io.BufferedReader; 17 | import java.io.FileNotFoundException; 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.io.InputStreamReader; 21 | import java.sql.Timestamp; 22 | import java.util.Date; 23 | import java.util.Properties; 24 | 25 | import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; 26 | import org.eclipse.paho.client.mqttv3.MqttCallback; 27 | import org.eclipse.paho.client.mqttv3.MqttClient; 28 | import org.eclipse.paho.client.mqttv3.MqttConnectOptions; 29 | import org.eclipse.paho.client.mqttv3.MqttException; 30 | import org.eclipse.paho.client.mqttv3.MqttMessage; 31 | import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence; 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 | * A sample application that demonstrates how to use the Paho MQTT v3.1 Client blocking API. 40 | * It can be run from the command line in one of two modes: 41 | * - as a publisher, sending a single message to a topic on the server 42 | * - as a subscriber, listening for messages from the server 43 | * There are three versions of the sample that implement the same features 44 | * but do so using using different programming styles: 45 | *
    46 | *
  1. Sample (this one) which uses the API which blocks until the operation completes
  2. 47 | *
  3. SampleAsyncWait shows how to use the asynchronous API with waiters that block until 48 | * an action completes
  4. 49 | *
  5. SampleAsyncCallBack shows how to use the asynchronous API where events are 50 | * used to notify the application when an action completes 51 | *
  6. 52 | *
53 | * If the application is run with the -h parameter then info is displayed that 54 | * describes all of the options / parameters. 55 | */ 56 | public class MyMqtt implements MqttCallback { 57 | private static final Logger LOGGER = LoggerFactory.getLogger(MySmartHomeApp.class); 58 | private final SmartHomeApp actionsApp = new MySmartHomeApp(); 59 | private String mqttuser, mqttpwd, mqttbroker, mqttclientid, mqttcleansession, mqttquietmode; 60 | InputStream inputStream; 61 | 62 | { 63 | try { 64 | GoogleCredentials credentials = 65 | GoogleCredentials.fromStream(getClass().getResourceAsStream("/smart-home-key.json")); 66 | actionsApp.setCredentials(credentials); 67 | } catch (Exception e) { 68 | LOGGER.error("couldn't load credentials"); 69 | } 70 | } 71 | /** 72 | * This is just for test purpose, just to verify connectivity 73 | * with your mqtt broker 74 | */ 75 | 76 | public static void main(String args[]) throws Exception { 77 | 78 | InputStreamReader r = new InputStreamReader(System.in); 79 | BufferedReader br = new BufferedReader(r); 80 | String name = ""; 81 | 82 | MyMqtt myMqttClient = new MyMqtt(); 83 | myMqttClient.subscribe("hello", 0); 84 | while (!name.equals("stop")) { 85 | System.out.println("Enter data: "); 86 | name = br.readLine(); 87 | System.out.println("data is: " + name); 88 | mqttPublish(myMqttClient, name); 89 | } 90 | 91 | br.close(); 92 | r.close(); 93 | } 94 | 95 | public static void mqttPublish(MyMqtt myMqttClient, String msg) { 96 | 97 | // Default settings: 98 | String action = "publish"; 99 | String topic = "hello"; 100 | int qos = 0; 101 | 102 | // With a valid set of arguments, the real work of 103 | // driving the client API can begin 104 | try { 105 | // Create an instance of this class 106 | 107 | // Perform the requested action 108 | if (action.equals("publish")) { 109 | myMqttClient.publish(topic, qos, msg.getBytes()); 110 | } else if (action.equals("subscribe")) { 111 | myMqttClient.subscribe(topic, qos); 112 | } 113 | } catch (MqttException me) { 114 | // Display full details of any exception that occurs 115 | System.out.println("reason " + me.getReasonCode()); 116 | System.out.println("msg " + me.getMessage()); 117 | System.out.println("loc " + me.getLocalizedMessage()); 118 | System.out.println("cause " + me.getCause()); 119 | System.out.println("excep " + me); 120 | me.printStackTrace(); 121 | } 122 | } // end publish 123 | 124 | // Private instance variables 125 | private MqttClient client; 126 | private String brokerUrl; 127 | private boolean quietMode; 128 | private MqttConnectOptions conOpt; 129 | private boolean clean; 130 | private String password; 131 | private String userName; 132 | 133 | /** 134 | * Constructs an instance of the sample client wrapper 135 | * 136 | */ 137 | public MyMqtt() 138 | throws MqttException, IOException { 139 | 140 | try { 141 | Properties prop = new Properties(); 142 | String propFileName = "mqtt.properties"; 143 | 144 | inputStream = getClass().getClassLoader().getResourceAsStream(propFileName); 145 | 146 | if (inputStream != null) { 147 | prop.load(inputStream); 148 | } else { 149 | throw new FileNotFoundException("property file '" + propFileName + "' not found in the classpath"); 150 | } 151 | 152 | // get the property value and print it out 153 | mqttbroker = prop.getProperty("broker"); 154 | mqttclientid = prop.getProperty("clientid"); 155 | mqttcleansession = prop.getProperty("cleansession"); 156 | mqttquietmode = prop.getProperty("quietmode"); 157 | mqttuser = prop.getProperty("user"); 158 | mqttpwd = prop.getProperty("pwd"); 159 | 160 | } catch (Exception e) { 161 | System.out.println("Exception: " + e); 162 | } finally { 163 | inputStream.close(); 164 | } 165 | 166 | this.brokerUrl = mqttbroker; 167 | this.quietMode = Boolean.valueOf(mqttquietmode); 168 | clean = Boolean.valueOf(mqttcleansession); 169 | this.password = mqttpwd; 170 | this.userName = mqttuser; 171 | // This sample stores in a temporary directory... where messages temporarily 172 | // stored until the message has been delivered to the server. 173 | // ..a real application ought to store them somewhere 174 | // where they are not likely to get deleted or tampered with 175 | String tmpDir = System.getProperty("java.io.tmpdir"); 176 | MqttDefaultFilePersistence dataStore = new MqttDefaultFilePersistence(tmpDir); 177 | 178 | try { 179 | // Construct the connection options object that contains connection parameters 180 | // such as cleanSession and LWT 181 | conOpt = new MqttConnectOptions(); 182 | conOpt.setCleanSession(clean); 183 | if (password != null) { 184 | conOpt.setPassword(this.password.toCharArray()); 185 | } 186 | if (userName != null) { 187 | conOpt.setUserName(this.userName); 188 | } 189 | 190 | // Construct an MQTT blocking mode client 191 | client = new MqttClient(this.brokerUrl, mqttclientid, dataStore); 192 | 193 | // Set this wrapper as the callback handler 194 | client.setCallback(this); 195 | 196 | } catch (MqttException e) { 197 | e.printStackTrace(); 198 | log("Unable to set up client: " + e.toString()); 199 | System.exit(1); 200 | } 201 | } 202 | 203 | /** 204 | * Publish / send a message to an MQTT server 205 | * 206 | * @param topicName the name of the topic to publish to 207 | * @param qos the quality of service to delivery the message at (0,1,2) 208 | * @param payload the set of bytes to send to the MQTT server 209 | * @throws MqttException 210 | */ 211 | public void publish(String topicName, int qos, byte[] payload) throws MqttException { 212 | 213 | // Connect to the MQTT server 214 | log("Connecting to " + brokerUrl + " with client ID " + client.getClientId()); 215 | if (!client.isConnected()) { 216 | client.connect(conOpt); 217 | log("Connected first time"); 218 | } else { 219 | log("Already Connected"); 220 | } 221 | 222 | String time = new Timestamp(System.currentTimeMillis()).toString(); 223 | log("Publishing at: " + time + " to topic \"" + topicName + "\" qos " + qos); 224 | 225 | // Create and configure a message 226 | MqttMessage message = new MqttMessage(payload); 227 | message.setQos(qos); 228 | 229 | // Send the message to the server, control is not returned until 230 | // it has been delivered to the server meeting the specified 231 | // quality of service. 232 | client.publish(topicName, message); 233 | 234 | // Disconnect the client 235 | // client.disconnect(); 236 | // log("Disconnected"); 237 | } 238 | 239 | /** 240 | * Subscribe to a topic on an MQTT server 241 | * Once subscribed this method waits for the messages to arrive from the server 242 | * that match the subscription. It continues listening for messages until the enter key is 243 | * pressed. 244 | * 245 | * @param topicName to subscribe to (can be wild carded) 246 | * @param qos the maximum quality of service to receive messages at for this subscription 247 | * @throws MqttException 248 | */ 249 | public void subscribe(String topicName, int qos) throws MqttException { 250 | 251 | // Connect to the MQTT server 252 | client.connect(conOpt); 253 | log("Connected to " + brokerUrl + " with client ID " + client.getClientId()); 254 | 255 | // Subscribe to the requested topic 256 | // The QoS specified is the maximum level that messages will be sent to the client at. 257 | // For instance if QoS 1 is specified, any messages originally published at QoS 2 will 258 | // be downgraded to 1 when delivering to the client but messages published at 1 and 0 259 | // will be received at the same level they were published at. 260 | log("Subscribing to topic \"" + topicName + "\" qos " + qos); 261 | client.subscribe(topicName, qos); 262 | 263 | // Disconnect the client from the server 264 | // client.disconnect(); 265 | // log("Disconnected"); 266 | } 267 | 268 | /** 269 | * Utility method to handle logging. If 'quietMode' is set, this method does nothing 270 | * 271 | * @param message the message to log 272 | */ 273 | private void log(String message) { 274 | if (!quietMode) { 275 | System.out.println(message); 276 | } 277 | } 278 | 279 | /****************************************************************/ 280 | /* Methods to implement the MqttCallback interface */ 281 | /****************************************************************/ 282 | 283 | /** 284 | * @see MqttCallback#connectionLost(Throwable) 285 | */ 286 | @Override 287 | public void connectionLost(Throwable cause) { 288 | // Called when the connection to the server has been lost. 289 | // An application may choose to implement reconnection 290 | // logic at this point. This sample simply exits. 291 | log("Connection to " + brokerUrl + " lost!" + cause); 292 | System.exit(1); 293 | } 294 | 295 | /** 296 | * @see MqttCallback#deliveryComplete(IMqttDeliveryToken) 297 | */ 298 | @Override 299 | public void deliveryComplete(IMqttDeliveryToken token) { 300 | // Called when a message has been delivered to the 301 | // server. The token passed in here is the same one 302 | // that was passed to or returned from the original call to publish. 303 | // This allows applications to perform asynchronous 304 | // delivery without blocking until delivery completes. 305 | // 306 | // This sample demonstrates asynchronous deliver and 307 | // uses the token.waitForCompletion() call in the main thread which 308 | // blocks until the delivery has completed. 309 | // Additionally the deliveryComplete method will be called if 310 | // the callback is set on the client 311 | // 312 | // If the connection to the server breaks before delivery has completed 313 | // delivery of a message will complete after the client has re-connected. 314 | // The getPendingTokens method will provide tokens for any messages 315 | // that are still to be delivered. 316 | } 317 | 318 | /** 319 | * @see MqttCallback#messageArrived(String, MqttMessage) 320 | */ 321 | @Override 322 | public void messageArrived(String topic, MqttMessage message) throws MqttException { 323 | // Called when a message arrives from the server that matches any 324 | // subscription made by the client 325 | String time = new Timestamp(System.currentTimeMillis()).toString(); 326 | System.out.println("Time:\t" + time + 327 | " Topic:\t" + topic + 328 | " Message:\t" + new String(message.getPayload()) + 329 | " QoS:\t" + message.getQos()); 330 | 331 | } 332 | 333 | /****************************************************************/ 334 | /* End of MqttCallback methods */ 335 | /****************************************************************/ 336 | 337 | } 338 | -------------------------------------------------------------------------------- /src/main/java/com/example/MySmartHomeApp.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.util.ArrayList; 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.concurrent.ExecutionException; 24 | 25 | import org.jetbrains.annotations.NotNull; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | import com.google.actions.api.smarthome.*; 30 | import com.google.cloud.firestore.QueryDocumentSnapshot; 31 | import com.google.gson.Gson; 32 | import com.google.home.graph.v1.DeviceProto; 33 | import com.google.protobuf.Struct; 34 | import com.google.protobuf.util.JsonFormat; 35 | 36 | public class MySmartHomeApp extends SmartHomeApp { 37 | 38 | private static final Logger LOGGER = LoggerFactory.getLogger(MySmartHomeApp.class); 39 | private static MyDataStore database = MyDataStore.getInstance(); 40 | 41 | @NotNull 42 | @Override 43 | public SyncResponse onSync(SyncRequest syncRequest, Map headers) { 44 | 45 | SyncResponse res = new SyncResponse(); 46 | res.setRequestId(syncRequest.requestId); 47 | res.setPayload(new SyncResponse.Payload()); 48 | 49 | String token = (String) headers.get("authorization"); 50 | String userId = ""; 51 | try { 52 | userId = database.getUserId(token); 53 | } catch (Exception e) { 54 | // TODO(proppy): add errorCode when 55 | // https://github.com/actions-on-google/actions-on-google-java/issues/44 is fixed. 56 | LOGGER.error("failed to get user id for token: %d", token); 57 | return res; 58 | } 59 | res.payload.agentUserId = userId; 60 | 61 | database.setHomegraph(userId, true); 62 | List devices = new ArrayList<>(); 63 | try { 64 | devices = database.getDevices(userId); 65 | } catch (ExecutionException | InterruptedException e) { 66 | LOGGER.error("failed to get devices", e); 67 | return res; 68 | } 69 | int numOfDevices = devices.size(); 70 | res.payload.devices = new SyncResponse.Payload.Device[numOfDevices]; 71 | for (int i = 0; i < numOfDevices; i++) { 72 | QueryDocumentSnapshot device = devices.get(i); 73 | SyncResponse.Payload.Device.Builder deviceBuilder = 74 | new SyncResponse.Payload.Device.Builder() 75 | .setId(device.getId()) 76 | .setType((String) device.get("type")) 77 | .setTraits((List) device.get("traits")) 78 | .setName( 79 | DeviceProto.DeviceNames.newBuilder() 80 | .addAllDefaultNames((List) device.get("defaultNames")) 81 | .setName((String) device.get("name")) 82 | .addAllNicknames((List) device.get("nicknames")) 83 | .build()) 84 | .setWillReportState((Boolean) device.get("willReportState")) 85 | .setRoomHint((String) device.get("roomHint")) 86 | .setDeviceInfo( 87 | DeviceProto.DeviceInfo.newBuilder() 88 | .setManufacturer((String) device.get("manufacturer")) 89 | .setModel((String) device.get("model")) 90 | .setHwVersion((String) device.get("hwVersion")) 91 | .setSwVersion((String) device.get("swVersion")) 92 | .build()); 93 | if (device.contains("attributes")) { 94 | Map attributes = new HashMap<>(); 95 | attributes.putAll((Map) device.get("attributes")); 96 | String attributesJson = new Gson().toJson(attributes); 97 | Struct.Builder attributeBuilder = Struct.newBuilder(); 98 | try { 99 | JsonFormat.parser().ignoringUnknownFields().merge(attributesJson, attributeBuilder); 100 | } catch (Exception e) { 101 | LOGGER.error("FAILED TO BUILD"); 102 | } 103 | deviceBuilder.setAttributes(attributeBuilder.build()); 104 | } 105 | if (device.contains("customData")) { 106 | Map customData = new HashMap<>(); 107 | customData.putAll((Map) device.get("customData")); 108 | // TODO(proppy): remove once 109 | // https://github.com/actions-on-google/actions-on-google-java/issues/43 is fixed. 110 | String customDataJson = new Gson().toJson(customData); 111 | deviceBuilder.setCustomData(customDataJson); 112 | } 113 | if (device.contains("otherDeviceIds")) { 114 | deviceBuilder.setOtherDeviceIds((List) device.get("otherDeviceIds")); 115 | } 116 | res.payload.devices[i] = deviceBuilder.build(); 117 | } 118 | 119 | return res; 120 | } 121 | 122 | @NotNull 123 | @Override 124 | public QueryResponse onQuery(QueryRequest queryRequest, Map headers) { 125 | QueryRequest.Inputs.Payload.Device[] devices = 126 | ((QueryRequest.Inputs) queryRequest.getInputs()[0]).payload.devices; 127 | QueryResponse res = new QueryResponse(); 128 | res.setRequestId(queryRequest.requestId); 129 | res.setPayload(new QueryResponse.Payload()); 130 | 131 | String token = (String) headers.get("authorization"); 132 | String userId = ""; 133 | try { 134 | userId = database.getUserId(token); 135 | } catch (Exception e) { 136 | LOGGER.error("failed to get user id for token: %d", headers.get("authorization")); 137 | res.payload.setErrorCode("authFailure"); 138 | return res; 139 | } 140 | 141 | Map> deviceStates = new HashMap<>(); 142 | for (QueryRequest.Inputs.Payload.Device device : devices) { 143 | try { 144 | Map deviceState = database.getState(userId, device.id); 145 | deviceState.put("status", "SUCCESS"); 146 | //deviceState.put("online", true); //TODO: Not sure about this line solution 147 | deviceStates.put(device.id, deviceState); 148 | //ReportState.makeRequest(this, userId, device.id, deviceState); 149 | } catch (Exception e) { 150 | LOGGER.error("QUERY FAILED: {}", e); 151 | Map failedDevice = new HashMap<>(); 152 | failedDevice.put("status", "ERROR"); 153 | failedDevice.put("errorCode", "deviceOffline"); 154 | deviceStates.put(device.id, failedDevice); 155 | } 156 | } 157 | res.payload.setDevices(deviceStates); 158 | return res; 159 | } 160 | 161 | @NotNull 162 | @Override 163 | public ExecuteResponse onExecute(ExecuteRequest executeRequest, Map headers) { 164 | ExecuteResponse res = new ExecuteResponse(); 165 | 166 | String token = (String) headers.get("authorization"); 167 | String userId = ""; 168 | try { 169 | userId = database.getUserId(token); 170 | } catch (Exception e) { 171 | LOGGER.error("failed to get user id for token: %d", headers.get("authorization")); 172 | res.setPayload(new ExecuteResponse.Payload()); 173 | res.payload.setErrorCode("authFailure"); 174 | return res; 175 | } 176 | 177 | List commandsResponse = new ArrayList<>(); 178 | List successfulDevices = new ArrayList<>(); 179 | Map states = new HashMap<>(); 180 | 181 | ExecuteRequest.Inputs.Payload.Commands[] commands = 182 | ((ExecuteRequest.Inputs) executeRequest.inputs[0]).payload.commands; 183 | for (ExecuteRequest.Inputs.Payload.Commands command : commands) { 184 | for (ExecuteRequest.Inputs.Payload.Commands.Devices device : command.devices) { 185 | try { 186 | states = database.execute(userId, device.id, command.execution[0]); 187 | successfulDevices.add(device.id); 188 | ReportState.makeRequest(this, userId, device.id, states); 189 | } catch (Exception e) { 190 | if (e.getMessage().equals("PENDING")) { 191 | ExecuteResponse.Payload.Commands pendingDevice = new ExecuteResponse.Payload.Commands(); 192 | pendingDevice.ids = new String[] {device.id}; 193 | pendingDevice.status = "PENDING"; 194 | commandsResponse.add(pendingDevice); 195 | continue; 196 | } 197 | if (e.getMessage().equals("pinNeeded")) { 198 | ExecuteResponse.Payload.Commands failedDevice = new ExecuteResponse.Payload.Commands(); 199 | failedDevice.ids = new String[] {device.id}; 200 | failedDevice.status = "ERROR"; 201 | failedDevice.setErrorCode("challengeNeeded"); 202 | failedDevice.setChallengeNeeded( 203 | new HashMap() { 204 | { 205 | put("type", "pinNeeded"); 206 | } 207 | }); 208 | failedDevice.setErrorCode(e.getMessage()); 209 | commandsResponse.add(failedDevice); 210 | continue; 211 | } 212 | if (e.getMessage().equals("challengeFailedPinNeeded")) { 213 | ExecuteResponse.Payload.Commands failedDevice = new ExecuteResponse.Payload.Commands(); 214 | failedDevice.ids = new String[] {device.id}; 215 | failedDevice.status = "ERROR"; 216 | failedDevice.setErrorCode("challengeNeeded"); 217 | failedDevice.setChallengeNeeded( 218 | new HashMap() { 219 | { 220 | put("type", "challengeFailedPinNeeded"); 221 | } 222 | }); 223 | failedDevice.setErrorCode(e.getMessage()); 224 | commandsResponse.add(failedDevice); 225 | continue; 226 | } 227 | if (e.getMessage().equals("ackNeeded")) { 228 | ExecuteResponse.Payload.Commands failedDevice = new ExecuteResponse.Payload.Commands(); 229 | failedDevice.ids = new String[] {device.id}; 230 | failedDevice.status = "ERROR"; 231 | failedDevice.setErrorCode("challengeNeeded"); 232 | failedDevice.setChallengeNeeded( 233 | new HashMap() { 234 | { 235 | put("type", "ackNeeded"); 236 | } 237 | }); 238 | failedDevice.setErrorCode(e.getMessage()); 239 | commandsResponse.add(failedDevice); 240 | continue; 241 | } 242 | 243 | ExecuteResponse.Payload.Commands failedDevice = new ExecuteResponse.Payload.Commands(); 244 | failedDevice.ids = new String[] {device.id}; 245 | failedDevice.status = "ERROR"; 246 | failedDevice.setErrorCode(e.getMessage()); 247 | commandsResponse.add(failedDevice); 248 | } 249 | } 250 | } 251 | 252 | ExecuteResponse.Payload.Commands successfulCommands = new ExecuteResponse.Payload.Commands(); 253 | successfulCommands.status = "SUCCESS"; 254 | successfulCommands.setStates(states); 255 | successfulCommands.ids = successfulDevices.toArray(new String[] {}); 256 | commandsResponse.add(successfulCommands); 257 | 258 | res.requestId = executeRequest.requestId; 259 | ExecuteResponse.Payload payload = 260 | new ExecuteResponse.Payload( 261 | commandsResponse.toArray(new ExecuteResponse.Payload.Commands[] {})); 262 | res.setPayload(payload); 263 | 264 | return res; 265 | } 266 | 267 | @NotNull 268 | @Override 269 | public void onDisconnect(DisconnectRequest disconnectRequest, Map headers) { 270 | String token = (String) headers.get("authorization"); 271 | try { 272 | String userId = database.getUserId(token); 273 | database.setHomegraph(userId, false); 274 | } catch (Exception e) { 275 | LOGGER.error("failed to get user id for token: %d", token); 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /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/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/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/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/SmartHomeUpdateServlet.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.Arrays; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Set; 25 | import java.util.stream.Collectors; 26 | 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 | import com.google.gson.Gson; 38 | import com.google.gson.JsonObject; 39 | import com.google.gson.JsonParser; 40 | 41 | /** 42 | * Handles request received via HTTP POST and delegates it to your Actions app. See: [Request 43 | * handling in Google App 44 | * Engine](https://cloud.google.com/appengine/docs/standard/java/how-requests-are-handled). 45 | */ 46 | @WebServlet(name = "smarthomeUpdate", urlPatterns = "/smarthome/update") 47 | public class SmartHomeUpdateServlet extends HttpServlet { 48 | private static final Logger LOGGER = LoggerFactory.getLogger(MySmartHomeApp.class); 49 | private static MyDataStore database = MyDataStore.getInstance(); 50 | private final SmartHomeApp actionsApp = new MySmartHomeApp(); 51 | //private String msg; 52 | private static final List UPDATE_DEVICE_PARAMS_KEYS = 53 | Arrays.asList(new String[] {"name", "nickname", "localDeviceId", "errorCode", "tfa"}); 54 | 55 | { 56 | try { 57 | GoogleCredentials credentials = 58 | GoogleCredentials.fromStream(getClass().getResourceAsStream("/smart-home-key.json")); 59 | actionsApp.setCredentials(credentials); 60 | } catch (Exception e) { 61 | LOGGER.error("couldn't load credentials"); 62 | } 63 | } 64 | 65 | @Override 66 | protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException { 67 | String body = req.getReader().lines().collect(Collectors.joining()); 68 | LOGGER.info("doPost, body = {}", body); 69 | JsonObject bodyJson = new JsonParser().parse(body).getAsJsonObject(); 70 | String userId = bodyJson.get("userId").getAsString(); 71 | String deviceId = bodyJson.get("deviceId").getAsString(); 72 | JsonObject states = bodyJson.getAsJsonObject("states"); 73 | Map deviceStates = 74 | states != null ? new Gson().fromJson(states, HashMap.class) : null; 75 | Map deviceParams = new HashMap<>(); 76 | Set deviceParamsKeys = bodyJson.keySet(); 77 | deviceParamsKeys.retainAll(UPDATE_DEVICE_PARAMS_KEYS); 78 | for (String k : deviceParamsKeys) { 79 | deviceParams.put(k, bodyJson.get(k).getAsString()); 80 | } 81 | try { 82 | database.updateDevice(userId, deviceId, deviceStates, deviceParams); 83 | if (deviceParams.containsKey("localDeviceId")) { 84 | actionsApp.requestSync(userId); 85 | } 86 | if (states != null) { 87 | ReportState.makeRequest(actionsApp, userId, deviceId, states); 88 | } 89 | } catch (Exception e) { 90 | LOGGER.error("failed to update device: {}", e); 91 | res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 92 | res.setHeader("Access-Control-Allow-Origin", "*"); 93 | res.setContentType("text/plain"); 94 | res.getWriter().println("ERROR"); 95 | return; 96 | } 97 | 98 | res.setStatus(HttpServletResponse.SC_OK); 99 | res.setHeader("Access-Control-Allow-Origin", "*"); 100 | res.setContentType("text/plain"); 101 | res.getWriter().println("OK"); 102 | // --------- Mqtt implementation -------------- 103 | //String msg = states.get("on").getAsString().equals("true") ? "on" : "off"; 104 | /*msg = states.toString(); 105 | if (states.has("thermostatTemperatureSetpoint")){ 106 | msg = "{'thermostatTemperatureSetpoint':"+states.get("thermostatTemperatureSetpoint")+"}"; 107 | } 108 | String topic = "hello"; 109 | CompletableFuture.runAsync(() -> { 110 | publishMqtt(topic, msg); // method call or code to be asynch. 111 | }); 112 | */ 113 | 114 | // --------------------------------------------- 115 | } 116 | 117 | @Override 118 | protected void doGet(HttpServletRequest request, HttpServletResponse response) 119 | throws IOException { 120 | response.setContentType("text/plain"); 121 | response.getWriter().println("/smarthome/update is a POST call"); 122 | } 123 | 124 | @Override 125 | protected void doOptions(HttpServletRequest req, HttpServletResponse res) { 126 | // pre-flight request processing 127 | res.setHeader("Access-Control-Allow-Origin", "*"); 128 | res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); 129 | res.setHeader("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Accept,Origin"); 130 | } 131 | /* 132 | public void publishMqtt(String topic, String msg){ 133 | try { 134 | //SampleAsyncCallBack mqtt = new SampleAsyncCallBack(); 135 | Sample mqtt = MySmartHomeApp.getInstanceMqtt(); 136 | mqtt.publish(topic, 0, msg.getBytes()); 137 | LOGGER.debug("Message = "+ msg +" sent by MQTT from UpdateServelet"); 138 | } catch (Throwable throwable) { 139 | LOGGER.error("failed to publish device: {}", throwable); 140 | } 141 | } */ 142 | } 143 | -------------------------------------------------------------------------------- /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/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 -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/appengine-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | java8 19 | true 20 | 21 | -------------------------------------------------------------------------------- /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> syncRequestInputs = new ArrayList<>(); 93 | syncRequest.put("inputs", syncRequestInputs); 94 | Map syncRequestIntent = new HashMap<>(); 95 | syncRequestIntent.put("intent", "action.devices.SYNC"); 96 | syncRequestInputs.add(syncRequestIntent); 97 | given() 98 | .contentType("application/json") 99 | .body(syncRequest) 100 | .when() 101 | .post("/smarthome") 102 | .then() 103 | .statusCode(200) 104 | .body( 105 | "requestId", equalTo(REQUEST_ID), 106 | "payload.devices[0].id", equalTo(DEVICE_ID), 107 | "payload.devices[0].name.name", equalTo(DEVICE_NAME), 108 | "payload.devices[0].type", equalTo(DEVICE_TYPE)); 109 | 110 | Map deviceUpdate = new HashMap<>(); 111 | deviceUpdate.put("userId", USER_ID); 112 | deviceUpdate.put("deviceId", DEVICE_ID); 113 | deviceUpdate.put("name", DEVICE_NAME_UPDATED); 114 | deviceUpdate.put("type", DEVICE_TYPE); 115 | given() 116 | .contentType("application/json") 117 | .body(deviceUpdate) 118 | .when() 119 | .post("/smarthome/update") 120 | .then() 121 | .statusCode(200); 122 | 123 | QueryDocumentSnapshot deviceUpdated = MyDataStore.getInstance().getDevices(USER_ID).get(0); 124 | assertEquals(DEVICE_NAME_UPDATED, deviceUpdated.get("name")); 125 | 126 | Map deviceDelete = new HashMap<>(); 127 | deviceDelete.put("userId", USER_ID); 128 | deviceDelete.put("deviceId", DEVICE_ID); 129 | given() 130 | .contentType("application/json") 131 | .body(deviceDelete) 132 | .when() 133 | .post("/smarthome/delete") 134 | .then() 135 | .statusCode(200); 136 | 137 | assertEquals(0, MyDataStore.getInstance().getDevices(USER_ID).size()); 138 | } 139 | } 140 | --------------------------------------------------------------------------------