├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── firebase.json
└── public
├── api.js
├── app.js
├── auth.js
├── events.js
├── images
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── camera-webrtc.png
├── camera.png
├── doorbell-webrtc.png
├── doorbell.png
├── empty.png
├── failure.png
├── help.png
├── success.png
└── thermostat.png
├── index.html
├── logger.js
├── style.css
├── third_party
└── kjur.min.js
├── ui.js
└── webrtc.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement (CLA). You (or your employer) retain the copyright to your
10 | contribution; this simply gives us permission to use and redistribute your
11 | contributions as part of the project. Head over to
12 | to see your current agreements on file or
13 | to sign a new one.
14 |
15 | You generally only need to submit a CLA once, so if you've already submitted one
16 | (even if it was for a different project), you probably don't need to do it
17 | again.
18 |
19 | ## Code reviews
20 |
21 | All submissions, including submissions by project members, require review. We
22 | use GitHub pull requests for this purpose. Consult
23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
24 | information on using pull requests.
25 |
26 | ## Community Guidelines
27 |
28 | This project follows
29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/).
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Device Access Web Application Sample
2 |
3 | 
4 |
5 | Device Access enables access, control, and management of Nest devices within partner apps, solutions, and smart home ecosystems, using the Smart Device Management (SDM) API.
6 |
7 | Developers can use the code in this repository with the directions provided below to deploy a functioning web application to control Nest cameras, doorbells, and thermostats.
8 |
9 | If you are new to Device Access, we recommend you to start with the [Building a Device Access Web Application Codelab](https://developers.google.com/nest/device-access/codelabs/web-app).
10 |
11 |
12 | ## Deploying the Sample App
13 |
14 | After creating a project on [Firebase](https://firebase.google.com/), use the steps below to deploy the sample app.
15 |
16 | Clone the sample app:
17 |
18 | `git clone https://github.com/google/device-access-sample-web-app.git`
19 |
20 | Navigate into project directory:
21 |
22 | `cd device-access-sample-web-app`
23 |
24 | Link the app with your Firebase project:
25 |
26 | `firebase use --add [PROJECT-ID]`
27 |
28 | Deploy the app to your Firebase project:
29 |
30 | `firebase deploy`
31 |
32 | You can then access the app at your Hosting URL ([https://[PROJECT-ID].web.app](#)).
33 |
34 | ## Using the Sample App
35 |
36 | Enter the OAuth credentials you created on [Google Cloud Platform](https://console.cloud.google.com/), as well as the Project Id from [Device Access Console](https://console.nest.google.com/device-access/) to complete the account linking flow for a Google account with a linked Nest device.
37 |
38 | Make sure to add the redirect url `[PROJECT-ID].web.app/auth` in list of authorized URLs for your Client Id on Google Cloud Platform to prevent `Error 400: redirect_url_mismatch`. More instructions on this can be found at [Handling OAuth](https://developers.google.com/nest/device-access/codelabs/web-app#4) section from the [Building a Device Access Web Application Codelab](https://developers.google.com/nest/device-access/codelabs/web-app).
39 |
40 | You can try out a live demo at [device-access-sample.web.app](https://device-access-sample.web.app/).
41 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "public",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "rewrites": [
10 | {
11 | "source": "**",
12 | "destination": "/index.html"
13 | }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/public/api.js:
--------------------------------------------------------------------------------
1 |
2 | /* Copyright 2022 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 |
18 | // Device Access Variables:
19 | let streamExtensionToken = "";
20 |
21 |
22 | /** deviceAccessRequest - Issues requests to Device Access Rest API */
23 | function deviceAccessRequest(method, call, localpath, payload = "") {
24 | let xhr = new XMLHttpRequest();
25 | xhr.open(method, selectedAPI + localpath);
26 | xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
27 | xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
28 |
29 | xhr.onload = function () {
30 | if(xhr.status === 200) {
31 | let responsePayload = "* Payload: \n" + xhr.response;
32 | pushLog(LogType.HTTP, method + " Response", responsePayload);
33 | deviceAccessResponse(method, call, xhr.response);
34 | } else {
35 | pushError(LogType.HTTP, method + " Response", xhr.responseText);
36 | }
37 | };
38 |
39 | let requestEndpoint = "* Endpoint: \n" + selectedAPI + localpath;
40 | let requestAuthorization = "* Authorization: \n" + 'Bearer ' + accessToken;
41 | let requestPayload = "* Payload: \n" + JSON.stringify(payload, null, 4);
42 | pushLog(LogType.HTTP, method + " Request",
43 | requestEndpoint + "\n\n" + requestAuthorization + "\n\n" + requestPayload);
44 |
45 | if (method === 'POST' && payload && payload !== "") {
46 | xhr.send(JSON.stringify(payload));
47 | } else {
48 | xhr.send();
49 | }
50 | }
51 |
52 | /** deviceAccessResponse - Parses responses from Device Access API calls */
53 | function deviceAccessResponse(method, call, response) {
54 | pushLog(LogType.HTTP, method + " Response", response);
55 | let data = JSON.parse(response);
56 | // Check if response data is empty:
57 | if(!data) {
58 | pushError(LogType.ACTION, "Empty Response!", "Device Access response contains empty response!");
59 | return;
60 | }
61 | // Based on the original request call, interpret the response:
62 | switch(call) {
63 | case 'listDevices':
64 | clearDevices(); // Clear the previously detected devices.
65 |
66 | // Check for detected devices:
67 | if(!data.devices) {
68 | pushError(LogType.ACTION, "No Devices!", "List Devices response contains no devices!");
69 | return;
70 | }
71 |
72 | // Iterate over detected devices:
73 | for (let i = 0; i < data.devices.length; i++) {
74 | // Parse Device Id:
75 | let scannedId = data.devices[i].name;
76 | let startIndexId = scannedId.lastIndexOf('/');
77 | let deviceId = scannedId.substring(startIndexId + 1);
78 | // Parse Device Type:
79 | let scannedType = data.devices[i].type;
80 | let startIndexType = scannedType.lastIndexOf('.');
81 | let deviceType = scannedType.substring(startIndexType + 1);
82 | // Parse Device Structure:
83 | let scannedAssignee = data.devices[i].assignee;
84 | let startIndexStructure = scannedAssignee.lastIndexOf('/structures/');
85 | let endIndexStructure = scannedAssignee.lastIndexOf('/rooms/');
86 | let deviceStructure = scannedAssignee.substring(startIndexStructure + 12, endIndexStructure);
87 |
88 | // Handle special case for Displays (Skip, no support!)
89 | if(deviceType === "DISPLAY")
90 | continue;
91 |
92 | // Handle special case for Thermostats (Read Temperature Unit)
93 | if(deviceType === "THERMOSTAT") {
94 | let tempScale = data.devices[i].traits["sdm.devices.traits.Settings"].temperatureScale;
95 | if(tempScale === "FAHRENHEIT") {
96 | document.getElementById("heatUnit").innerText = "°F";
97 | document.getElementById("coolUnit").innerText = "°F";
98 | } else {
99 | document.getElementById("heatUnit").innerText = "°C";
100 | document.getElementById("coolUnit").innerText = "°C";
101 | }
102 | }
103 |
104 | // Parse Device Room:
105 | let scannedName = data.devices[i].traits["sdm.devices.traits.Info"].customName;
106 | let scannedRelations = data.devices[i].parentRelations;
107 | let scannedRoom = scannedRelations[0]["displayName"];
108 | // Parse Device Name:
109 | let deviceName = scannedName !== "" ? scannedName : scannedRoom + " " + stringFormat(deviceType);
110 | // Parse Device Traits:
111 | let deviceTraits = Object.keys(data.devices[i].traits);
112 |
113 | // WebRTC check:
114 | let traitCameraLiveStream = data.devices[i].traits["sdm.devices.traits.CameraLiveStream"];
115 |
116 | if(traitCameraLiveStream) {
117 | let supportedProtocols = traitCameraLiveStream.supportedProtocols;
118 | if (supportedProtocols && supportedProtocols.includes("WEB_RTC")) {
119 | deviceType += "-webrtc";
120 | initializeWebRTC();
121 | }
122 | }
123 |
124 | addDevice(new Device(deviceId, deviceType, deviceName, deviceStructure, deviceTraits));
125 | }
126 | break;
127 | case 'listStructures':
128 | console.log("List Structures!");
129 | break;
130 | case 'generateStream':
131 | timestampGenerateStreamResponse = new Date();
132 | updateAnalytics();
133 | console.log(`Generate Stream response - `, timestampGenerateStreamResponse);
134 | if(data["results"] && (data["results"].hasOwnProperty("streamExtensionToken") || data["results"].hasOwnProperty("mediaSessionId")))
135 | updateStreamExtensionToken(data["results"].streamExtensionToken || data["results"].mediaSessionId);
136 | if(data["results"] && data["results"].hasOwnProperty("answerSdp")) {
137 | updateWebRTC(data["results"].answerSdp);
138 | pushLog(LogType.ACTION, "[Video Stream]", "");
139 | }
140 | break;
141 | case 'refreshStream':
142 | timestampExtendStreamResponse = new Date();
143 | updateAnalytics();
144 | console.log(`Refresh Stream response - `, timestampExtendStreamResponse);
145 | if(data["results"] && (data["results"].hasOwnProperty("streamExtensionToken") || data["results"].hasOwnProperty("mediaSessionId")))
146 | updateStreamExtensionToken(data["results"].streamExtensionToken || data["results"].mediaSessionId);
147 | break;
148 | case 'stopStream':
149 | timestampStopStreamResponse = new Date();
150 | updateAnalytics();
151 | console.log(`Stop Stream response - `, timestampStopStreamResponse);
152 | initializeWebRTC();
153 | break;
154 | case 'fanMode':
155 | if(document.getElementById("btnFanMode").textContent === "Activate Fan")
156 | document.getElementById("btnFanMode").textContent = "Deactivate Fan";
157 | else
158 | document.getElementById("btnFanMode").textContent = "Activate Fan";
159 | break;
160 | case 'thermostatMode':
161 | console.log("Thermostat Mode!");
162 | break;
163 | case 'temperatureSetpoint':
164 | console.log("Temperature Setpoint!");
165 | break;
166 | default:
167 | pushError(LogType.ACTION, "Error", "Unrecognized Request Call!");
168 | }
169 | }
170 |
171 | /** openResourcePicker - Opens Resource Picker on a new browser tab */
172 | function openResourcePicker() {
173 | window.open(selectedResourcePicker);
174 | }
175 |
176 |
177 | /// Device Access API ///
178 |
179 | /** onListDevices - Issues a ListDevices request */
180 | function onListDevices() {
181 | let endpoint = "/enterprises/" + projectId + "/devices";
182 | deviceAccessRequest('GET', 'listDevices', endpoint);
183 | }
184 |
185 | /** onListStructures - Issues a ListStructures request */
186 | function onListStructures() {
187 | let endpoint = "/enterprises/" + projectId + "/structures";
188 | deviceAccessRequest('GET', 'listStructures', endpoint);
189 | }
190 |
191 | /** onFan - Issues a FanMode change request */
192 | function onFan() {
193 | let endpoint = "/enterprises/" + projectId + "/devices/" + selectedDevice.id + ":executeCommand";
194 | // Construct the payload:
195 | let payload = {
196 | "command": "sdm.devices.commands.Fan.SetTimer",
197 | "params": {}
198 | };
199 | // Set correct FanMode based on the current selection:
200 | switch (document.getElementById("btnFanMode").textContent) {
201 | case "Activate Fan":
202 | payload.params["timerMode"] = "ON";
203 | payload.params["duration"] = "3600s";
204 | break;
205 | case "Deactivate Fan":
206 | payload.params["timerMode"] = "OFF";
207 | break;
208 | default:
209 | pushError(LogType.ACTION, "Error", "Button Mode not recognized!");
210 | return;
211 | }
212 | deviceAccessRequest('POST', 'fanMode', endpoint, payload);
213 | }
214 |
215 | /** onThermostatMode - Issues a ThermostatMode request */
216 | function onThermostatMode() {
217 | let endpoint = "/enterprises/" + projectId + "/devices/" + selectedDevice.id + ":executeCommand";
218 | let tempMode = document.getElementById("sctThermostatMode").value;
219 | let payload = {
220 | "command": "sdm.devices.commands.ThermostatMode.SetMode",
221 | "params": {
222 | "mode": tempMode
223 | }
224 | };
225 | deviceAccessRequest('POST', 'thermostatMode', endpoint, payload);
226 | }
227 |
228 | /** onTemperatureSetpoint - Issues a TemperatureSetpoint request */
229 | function onTemperatureSetpoint() {
230 | let endpoint = "/enterprises/" + projectId + "/devices/" + selectedDevice.id + ":executeCommand";
231 | let heatCelsius = parseFloat(document.getElementById("txtHeatTemperature").value);
232 | let coolCelsius = parseFloat(document.getElementById("txtCoolTemperature").value);
233 | // Convert temperature values based on temperature unit:
234 | if(document.getElementById("heatUnit").innerText === "°F") {
235 | heatCelsius = (heatCelsius - 32) * 5 / 9;
236 | }
237 | if(document.getElementById("coolUnit").innerText === "°F") {
238 | coolCelsius = (coolCelsius - 32) * 5 / 9;
239 | }
240 | // Construct the payload:
241 | let payload = {
242 | "command": "",
243 | "params": {}
244 | };
245 | // Set correct temperature fields based on the selected ThermostatMode:
246 | switch (document.getElementById("sctThermostatMode").value) {
247 | case "HEAT":
248 | payload.command = "sdm.devices.commands.ThermostatTemperatureSetpoint.SetHeat";
249 | payload.params["heatCelsius"] = heatCelsius;
250 | break;
251 | case "COOL":
252 | payload.command = "sdm.devices.commands.ThermostatTemperatureSetpoint.SetCool";
253 | payload.params["coolCelsius"] = coolCelsius;
254 | break;
255 | case "HEATCOOL":
256 | payload.command = "sdm.devices.commands.ThermostatTemperatureSetpoint.SetRange";
257 | payload.params["heatCelsius"] = heatCelsius;
258 | payload.params["coolCelsius"] = coolCelsius;
259 | break;
260 | default:
261 | pushError(LogType.ACTION, "Invalid Mode!", "Off and Eco modes don't allow this function!"
262 | + "\n (Try changing the Thermostat Mode to some other value)");
263 | return;
264 | }
265 | deviceAccessRequest('POST', 'temperatureSetpoint', endpoint, payload);
266 | }
267 |
268 | /** onGenerateStream - Issues a GenerateRtspStream request */
269 | function onGenerateStream() {
270 | clearAnalytics(true);
271 | timestampGenerateStreamRequest = new Date();
272 | updateAnalytics();
273 | console.log(`onGenerateStream() - `, timestampGenerateStreamRequest);
274 |
275 | let endpoint = "/enterprises/" + projectId + "/devices/" + selectedDevice.id + ":executeCommand";
276 | let payload = {
277 | "command": "sdm.devices.commands.CameraLiveStream.GenerateRtspStream"
278 | };
279 | deviceAccessRequest('POST', 'generateStream', endpoint, payload);
280 | }
281 |
282 | /** onExtendStream - Issues a ExtendRtspStream request */
283 | function onExtendStream() {
284 | timestampExtendStreamRequest = new Date();
285 | updateAnalytics();
286 | console.log(`onExtendStream() - `, timestampExtendStreamRequest);
287 |
288 | let endpoint = "/enterprises/" + projectId + "/devices/" + selectedDevice.id + ":executeCommand";
289 | let payload = {
290 | "command": "sdm.devices.commands.CameraLiveStream.ExtendRtspStream",
291 | "params": {
292 | "streamExtensionToken" : streamExtensionToken
293 | }
294 | };
295 | deviceAccessRequest('POST', 'refreshStream', endpoint, payload);
296 | }
297 |
298 | /** onStopStream - Issues a StopRtspStream request */
299 | function onStopStream() {
300 | timestampStopStreamRequest = new Date();
301 | updateAnalytics();
302 | console.log(`onStopStream() - `, timestampStopStreamRequest);
303 |
304 | let endpoint = "/enterprises/" + projectId + "/devices/" + selectedDevice.id + ":executeCommand";
305 | let payload = {
306 | "command": "sdm.devices.commands.CameraLiveStream.StopRtspStream",
307 | "params": {
308 | "streamExtensionToken" : streamExtensionToken
309 | }
310 | };
311 | deviceAccessRequest('POST', 'stopStream', endpoint, payload);
312 | }
313 |
314 | /** onGenerateStream_WebRTC - Issues a GenerateWebRtcStream request */
315 | function onGenerateStream_WebRTC() {
316 | clearAnalytics(true);
317 | timestampGenerateWebRtcStreamRequest = new Date();
318 | updateAnalytics();
319 | console.log(`onGenerateStream_WebRTC() - `, timestampGenerateWebRtcStreamRequest);
320 |
321 | let endpoint = "/enterprises/" + projectId + "/devices/" + selectedDevice.id + ":executeCommand";
322 | let payload = {
323 | "command": "sdm.devices.commands.CameraLiveStream.GenerateWebRtcStream",
324 | "params": {
325 | "offerSdp": offerSDP
326 | }
327 | };
328 |
329 | deviceAccessRequest('POST', 'generateStream', endpoint, payload);
330 | }
331 |
332 | /** onExtendStream_WebRTC - Issues a ExtendWebRtcStream request */
333 | function onExtendStream_WebRTC() {
334 | timestampExtendWebRtcStreamRequest = new Date();
335 | updateAnalytics();
336 | console.log(`onExtendStream_WebRTC() - `, timestampExtendWebRtcStreamRequest);
337 |
338 | let endpoint = "/enterprises/" + projectId + "/devices/" + selectedDevice.id + ":executeCommand";
339 | let payload = {
340 | "command": "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream",
341 | "params": {
342 | "mediaSessionId" : streamExtensionToken
343 | }
344 | };
345 | deviceAccessRequest('POST', 'refreshStream', endpoint, payload);
346 | }
347 |
348 | /** onStopStream_WebRTC - Issues a StopWebRtcStream request */
349 | function onStopStream_WebRTC() {
350 | timestampStopWebRtcStreamRequest = new Date();
351 | updateAnalytics();
352 | console.log(`onStopStream_WebRTC() - `, timestampStopWebRtcStreamRequest);
353 |
354 | let endpoint = "/enterprises/" + projectId + "/devices/" + selectedDevice.id + ":executeCommand";
355 | let payload = {
356 | "command": "sdm.devices.commands.CameraLiveStream.StopWebRtcStream",
357 | "params": {
358 | "mediaSessionId" : streamExtensionToken
359 | }
360 | };
361 | deviceAccessRequest('POST', 'stopStream', endpoint, payload);
362 | }
363 |
--------------------------------------------------------------------------------
/public/app.js:
--------------------------------------------------------------------------------
1 |
2 | /* Copyright 2022 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 |
18 | // State Variables:
19 | let isSignedIn = false;
20 | let isSubscribed = false;
21 | let selectedDevice;
22 |
23 |
24 | /// Primary Functions ///
25 |
26 | /** init - Initializes the loaded javascript */
27 | async function init() {
28 | readStorage(); // Reads data from browser's local storage if available
29 | await handleAuth(); // Checks incoming authorization code from /auth path
30 | await exchangeCode(); // Exchanges authorization code to an access token
31 | await refreshAccess(); // Retrieves a new access token using refresh token
32 | initializeDevices(); // Issues a list devices call if logged-in
33 | }
34 |
35 | /** readStorage - Reads data from browser's local storage if available */
36 | function readStorage() {
37 |
38 | if (localStorage["logs"]) {
39 | // Parse local storage for logs:
40 | const parsedStorage = JSON.parse(localStorage["logs"]);
41 | // Read the parsed storage:
42 | if (Array.isArray(parsedStorage))
43 | for (let i = 0; i < parsedStorage.length; i++)
44 | logs.push(parsedStorage[i]);
45 | // Display ingested logs:
46 | addLogEntries(logs);
47 | }
48 |
49 | if (localStorage["clientId"]) {
50 | updateClientId(localStorage["clientId"]);
51 | }
52 | if (localStorage["clientSecret"]) {
53 | updateClientSecret(localStorage["clientSecret"]);
54 | }
55 | if (localStorage["projectId"]) {
56 | updateProjectId(localStorage["projectId"]);
57 | }
58 |
59 | if (localStorage["oauthCode"]) {
60 | updateOAuthCode(localStorage["oauthCode"]);
61 | }
62 | if (localStorage["accessToken"]) {
63 | updateAccessToken(localStorage["accessToken"]);
64 | }
65 | if (localStorage["refreshToken"]) {
66 | updateRefreshToken(localStorage["refreshToken"]);
67 | }
68 |
69 | if (localStorage["isSignedIn"] === true || localStorage["isSignedIn"] === "true") {
70 | updateSignedIn(localStorage["isSignedIn"]);
71 | }
72 | // Update the App Controls based on isSignedIn:
73 | updateAppControls();
74 |
75 | if (localStorage["subscriptionId"]) {
76 | updateSubscriptionId(localStorage["subscriptionId"]);
77 | }
78 |
79 | if (localStorage["serviceAccountKey"]) {
80 | updateServiceAccountKey(localStorage["serviceAccountKey"]);
81 | }
82 |
83 | if (localStorage["logFilter"]) {
84 | logFilter = localStorage["logFilter"].split(",");
85 | }
86 | // Update the Log Filters based on logFilter:
87 | updateLogFilter(logFilter);
88 | }
89 |
90 | /** initializeDevices - Issues a list devices call if logged-in */
91 | function initializeDevices() {
92 | if(isSignedIn) {
93 | clickListDevices();
94 | }
95 | }
96 |
97 |
98 | /// Helper Functions ///
99 |
100 | /** Device Object Model */
101 | class Device {
102 | constructor(id, type, name, structure, traits) {
103 | this.id = id;
104 | this.type = type;
105 | this.name = name;
106 | this.structure = structure;
107 | this.traits = traits;
108 | }
109 | }
110 |
111 | /** addDevice - Add device to Device Control list */
112 | function addDevice(device) {
113 | // Create an Option object
114 | let opt = document.createElement("option");
115 |
116 | // Assign text and value to Option object
117 | opt.text = device.name;
118 | opt.value = JSON.stringify(device);
119 |
120 | // Add an Option object to Drop Down List Box
121 | document.getElementById("sctDeviceList").options.add(opt);
122 |
123 | // If this is the first device added, choose it
124 | if(document.getElementById("sctDeviceList").options.length === 1) {
125 | selectedDevice = device;
126 | showDeviceControls();
127 | }
128 | }
129 |
130 | /** clearDevices - Clear Device Control list */
131 | function clearDevices() {
132 | let deviceListLength = document.getElementById("sctDeviceList").options.length;
133 | for (let i = deviceListLength - 1; i >= 0; i--) {
134 | document.getElementById("sctDeviceList").options[i] = null;
135 | }
136 | hideDeviceControls();
137 | }
138 |
139 | /** stringFormat - Formats input string to Upper Camel Case */
140 | function stringFormat(str) {
141 | return str.replace(/(\w)(\w*)/g,
142 | function(g0,g1,g2){return g1.toUpperCase() + g2.toLowerCase();});
143 | }
144 |
--------------------------------------------------------------------------------
/public/auth.js:
--------------------------------------------------------------------------------
1 |
2 | /* Copyright 2022 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 |
18 | // Server Credentials:
19 | const TOKEN_ENDPOINT = "https://www.googleapis.com/oauth2/v4/token";
20 | const OAUTH_SCOPE = "https://www.googleapis.com/auth/sdm.service";
21 |
22 | // Configuration Variables:
23 | let selectedAPI = "https://smartdevicemanagement.googleapis.com/v1";
24 | let selectedEndpoint = "https://nestservices.google.com/partnerconnections/";
25 | let selectedResourcePicker = "https://nestservices.google.com/partnerconnections";
26 |
27 | // Partner Credentials:
28 | let clientId = "";
29 | let clientSecret = "";
30 | let projectId = "";
31 |
32 | // Authentication Variables:
33 | let oauthCode = "";
34 | let accessToken = "";
35 | let refreshToken = "";
36 |
37 |
38 | /** signIn - Initiates the OAuth flow for Account Linking */
39 | function signIn() {
40 | // Calculating the redirect URI for current window
41 | let redirectURI = window.location.origin + '/auth';
42 |
43 | // Google's OAuth 2.0 endpoint for requesting an access token
44 | let oauthEndpoint = selectedEndpoint + projectId + "/auth";
45 |
46 | // Create