构建具备设备权限的 Web 应用

1. 简介

设备权限计划提供了 Smart Device Management API,这是一个可供开发者通过自己的应用控制 Google Nest 设备的 REST API。用户需要同意第三方访问其 Nest 设备。

52f77aa38cda13a6.png

若要成功集成设备权限,需要完成三个关键步骤:

  1. 创建项目 - 在 Google Cloud Platform 中创建一个项目,并在设备访问权限控制台中注册成为开发者。
  2. 关联账号 - 使用户完成账号关联流程,并检索访问代码。使用该代码兑换访问令牌。
  3. 控制设备 - 通过发送包含访问令牌的命令发出 Smart Device Management API 请求,以控制设备。

在此 Codelab 中,我们将构建一个用于处理身份验证和调用 Smart Device Management API 的 Web 应用,借此深入了解设备权限的运作方式。此外,我们还将探索如何使用 Node.js 和 Express 部署简单的代理服务器以发送设备权限请求。

开始学习之前,你最好先回顾一下我们将在此 Codelab 中使用的常用 Web 技术,例如使用 OAuth 2.0 进行身份验证使用 Node.js 构建 Web 应用,但并非必须回顾。

所需条件

  • Node.js 8 或更高版本
  • 已关联 Nest Thermostat 的 Google 账号

学习内容

  • 设置托管静态网页和 Cloud Functions 的 Firebase 项目
  • 通过基于浏览器的 Web 应用发出设备权限请求
  • 使用 Node.js 和 Express 构建代理服务器以路由你的请求

2. 创建项目

开发者需要创建一个 Google Cloud Platform (GCP) 项目,以便设置设备权限集成。在开发者的应用和 Google Cloud 之间的 OAuth 流程中,将用到该 GCP 项目中生成的客户端 ID客户端密钥。此外,开发者还需要访问设备访问权限控制台,以便创建一个用于访问 Smart Device Management API 的项目。

Google Cloud Platform

转到 Google Cloud Platform。点击创建新项目并提供项目名称。此时,系统还会显示 Google Cloud 的项目 ID [GCP-Project-Id],请记下该 ID,因为我们在设置 Firebase 期间会用到该 ID。(在此 Codelab 中,我们会将该 ID 称为 [GCP-Project-Id]。)

585e926b21994ac9.png

第一步是对我们的项目启用必要的 API 库。依次转到 API 和服务 > 库,然后搜索 Smart Device Management API。你需要启用该 API 才能授权你的项目向 Device Access API 调用发出请求。

14e7eabc422c7fda.png

在继续创建 OAuth 凭据之前,我们需要为项目配置 OAuth 权限请求页面。依次转到 API 和服务 > OAuth 同意屏幕。对于用户类型,选择外部。为你的应用提供名称和支持服务电子邮件地址以及开发者联系信息,完成第一个屏幕所需的配置。当系统要求提供测试用户时,请务必在这一步提供具有关联设备的电子邮件地址。

配置 OAuth 权限请求页面后,请依次转到 API 和服务 > 凭据。点击 +创建凭据并选择 OAuth 客户端 ID。对于应用类型,选择 Web 应用

5de534212d44fce7.png

为你的客户端提供一个名称,然后点击创建。稍后,我们将添加授权 JavaScript 来源和授权重定向 URI。完成该过程后,系统会显示与相应 OAuth 2.0 客户端相关联的 [Client-Id][Client-Secret]

e6a670da18952f08.png

设备访问权限控制台

转到设备访问权限控制台。如果你之前没有使用过设备访问权限控制台,你首先会看到服务条款协议,并须支付 5 美元的注册费。

创建一个新项目,然后为其命名。在下一个窗口中,提供你在上一步中从 GCP 收到的 [Client-Id]

f8a3f27354bc2625.png

启用事件并完成项目创建步骤后,你会转到该项目的首页。[Project-Id] 将列在你为该项目指定的名称下方。

db7ba33d8b707148.png

请记下你的 [Project-Id],因为我们会在向 Smart Device Management API 发送请求时使用该 ID。

3. 设置 Firebase

Firebase 为开发者提供了一种快速简便的 Web 应用部署方式。我们将使用 Firebase 开发一个客户端 Web 应用,以便实现设备权限集成。

创建 Firebase 项目

转到 Firebase 控制台。点击添加项目,然后选择你在“创建项目”这一步创建的项目。这样就会创建一个 Firebase 项目,且该项目会关联到你的 GCP 项目 [GCP-Project-Id]

成功创建 Firebase 项目后,你应该会看到以下画面:

dbb02bbacac093f5.png

安装 Firebase 工具

Firebase 提供了一套用于构建和部署应用的 CLI 工具。如需安装这些工具,请打开新的终端窗口并运行以下命令。此操作将在全局范围内安装 Firebase 工具。

$ npm i -g firebase-tools

如需验证 Firebase 工具是否已正确安装,请检查版本信息。

$ firebase --version

你可以输入登录命令,使用你的 Google 账号登录 Firebase CLI 工具。

$ firebase login

初始化托管项目

能够登录后,下一步就是为你的 Web 应用初始化一个托管项目。从终端转到你要在其中创建项目的文件夹,然后运行以下命令:

$ firebase init hosting

Firebase 会要求你回答一组问题,以便开始初始化托管项目:

  1. 请选择一个选项 - 使用现有项目
  2. 为该目录选择一个默认的 Firebase 项目 - 选择 ***[GCP-Project-Id]***
  3. 你想使用什么作为公共目录?- 公共文件夹
  4. 是否要配置为单页应用?- 是
  5. 是否设置使用 GitHub 自动构建和部署?- 否

在该项目初始化后,你可使用以下命令将其部署到 Firebase:

$ firebase deploy

Firebase 将扫描你的项目,并将必要的文件部署到云端托管。

fe15cf75e985e9a1.png

在浏览器中打开托管网址时,你应该会看到刚才部署的网页:

e40871238c22ebe2.png

现在,你已基本了解如何使用 Firebase 部署网页,接下来,我们开始部署此 Codelab 示例!

4. Codelab 示例

你可使用以下命令克隆在 GitHub 上托管的 Codelab 代码库

$ git clone https://github.com/google/device-access-codelab-web-app.git

在该代码库中,我们以两个单独的文件夹提供示例。codelab-start 文件夹包含一些必要的文件,可供你以此 Codelab 当前的进度为起始点着手进行部署。codelab-done 文件夹包含此 Codelab 的完整版本,其中包含功能齐全的客户端和 Node.js 服务器。

在学习此 Codelab 的过程中,我们将使用 codelab-start 文件夹中的文件;但如果你遇到问题,也可随时参考完成后的 Codelab 版本。

Codelab 示例文件

codelab-start 文件夹的文件结构如下所示:

public
├───index.html
├───scripts.js
├───style.css
firebase.json

公共文件夹包含该应用的静态页面。firebase.json 负责将 Web 请求路由到我们的应用。在 codelab-done 版本中,你还会看到一个 functions 目录,其中包含将要在 Google Cloud Functions 中部署的代理服务器 (Express) 的逻辑。

部署 Codelab 示例

codelab-start 中的文件复制到你项目的目录中。

$ firebase deploy

Firebase 完成部署后,你应该能够看到相应 Codelab 应用:

e84c1049eb4cca92.png

启动身份验证流程需要使用合作伙伴凭据,我们会在下一部分介绍有关详情。

5. 处理 OAuth

OAuth 是有关授予访问权限的网络标准,通常供用户在不共享密码的情况下向第三方应用授予访问其账号信息的权限。我们使用 OAuth 2.0 让开发者能够通过设备权限访问用户设备。

7ee31f5d9c37f699.png

指定重定向 URI

OAuth 流程的第一步包括将一组参数传递到 Google OAuth 2.0 端点。在征得用户同意后,Google OAuth 服务器会向你的重定向 URI 发出包含授权代码的请求。

scripts.js 中,使用你自己的托管网址更新 SERVER_URI 常量(第 19 行):

const SERVER_URI = "https://[GCP-Project-Id].web.app";

使用以上更改重新部署该应用后,系统会更新你的项目所使用的重定向 URI。

$ firebase deploy

启用重定向 URI

在脚本文件中更新重定向 URI 后,你还必须将其添加到你已为项目创建的客户端 ID 所允许的重定向 URI 列表中。转到 Google Cloud Platform 中的“凭据”页面,其中列出了已为你的项目创建的所有凭据:

1a07b624b5e548da.png

OAuth 2.0 客户端 ID 列表下,选择你在“创建项目”这一步创建的客户端 ID。将应用的重定向 URI 添加到项目的授权重定向 URI 列表中。

6d65b298e1f005e2.png

尝试登录!

转到你使用 Firebase 设置的托管网址,输入你的合作伙伴凭据,然后点击 SIGN IN 按钮。客户端 ID 和客户端密钥是你从 Google Cloud Platform 获得的凭据,项目 ID 来自设备访问权限控制台。

78b48906a2dd7c05.png

点击 SIGN IN 按钮后,系统会引导用户完成贵企业的 OAuth 流程(从登录其 Google 账号的屏幕开始)。登录后,系统会要求用户为你的项目提供访问其 Nest 设备的权限。

e9b7887c4ca420.png

由于这是模拟应用,因此 Google 会在发出重定向前发出警告!

b227d510cb1df073.png

点击“Advanced”,然后选择“Go to web.app (unsafe)”以完成到你的应用的重定向。

673a4fd217e24dad.png

这样便会在传入 GET 请求中提供一个 OAuth 代码,然后应用会使用此代码交换访问令牌和刷新令牌。

6. 控制设备

设备权限示例应用使用 Smart Device Management REST API 调用来控制 Google Nest 设备。这些调用需要传递 GET 或 POST 请求标头中的访问令牌,以及特定命令所需的载荷。

我们编写了一个用于处理此类调用的通用权限请求函数。不过,你需要为该函数提供正确的端点并根据需要提供载荷对象!

function deviceAccessRequest(method, call, localpath, payload = null) {...}
  • method - HTTP 请求的类型(GETPOST)
  • call - 代表我们的 API 调用的字符串,用于路由响应(listDevicesthermostatModetemperatureSetpoint
  • localpath - 请求的目标端点,其中包含项目 ID 和设备 ID(附加在 https://smartdevicemanagement.googleapis.com/v1 后面)
  • payload (*) - API 调用所需的其他数据(例如,表示设定温度的数值)

我们将构建示例界面控件(List Devices、Set Mode、Set Temp)来控制 Nest Thermostat:

86f8a193aa397421.png

这些界面控件会从 scripts.js 调用相应的函数(listDevices()postThermostatMode()postTemperatureSetpoint())。我们已将这些函数留空供你实现!目标是选择正确的方法/路径,并将载荷传递到 deviceAccessRequest(...) 函数。

List Devices

最简单的设备权限调用是 listDevices。它使用 GET 请求,且无需载荷。端点需要使用 projectId 进行构建。请完成 listDevices() 函数,具体代码如下所示:

function listDevices() {
  var endpoint = "/enterprises/" + projectId + "/devices";
  deviceAccessRequest('GET', 'listDevices', endpoint);
}

保存你的更改,然后使用以下命令重新部署你的 Firebase 项目:

$ firebase deploy

部署应用的新版本后,请尝试重新加载该页面,然后点击 LIST DEVICES。该操作应该会填充“Device Control”下方的列表,届时应该会显示你的温控器的 ID:

b64a198673ed289f.png

从列表中选择设备后,scripts.js 文件中的 deviceId 字段便会更新。对于后两个控件,我们需要为要控制的特定设备指定 deviceId

温控器控制

在 Smart Device Management API 中,有两个特征用于对 Nest Thermostat 进行基本控制:ThermostatModeTemperatureSetpoint。ThermostatMode 可将 Nest Thermostat 的模式设为四种可能的模式之一:{Off、Heat、Cool、HeatCool}。然后,我们需要在载荷中提供所选模式。

scripts.js 中的 postThermostatMode() 函数替换为以下代码:

function postThermostatMode() {
  var endpoint = "/enterprises/" + projectId + "/devices/" + deviceId + ":executeCommand";
  var tempMode = id("tempMode").value;
  var payload = {
    "command": "sdm.devices.commands.ThermostatMode.SetMode",
    "params": {
      "mode": tempMode
    }
  };
  deviceAccessRequest('POST', 'thermostatMode', endpoint, payload);
}

下一个函数 postTemperatureSetpoint() 可设置 Nest Thermostat 的温度(以摄氏度为单位)。根据所选温控器模式,可在载荷中设置两个参数,即 heatCelsiuscoolCelsius

function postTemperatureSetpoint() {
  var endpoint = "/enterprises/" + projectId + "/devices/" + deviceId + ":executeCommand";
  var heatCelsius = parseFloat(id("heatCelsius").value);
  var coolCelsius = parseFloat(id("coolCelsius").value);

  var payload = {
    "command": "",
    "params": {}
  };
  
  if ("HEAT" === id("tempMode").value) {
    payload.command = "sdm.devices.commands.ThermostatTemperatureSetpoint.SetHeat";
    payload.params["heatCelsius"] = heatCelsius;
  }
  else if ("COOL" === id("tempMode").value) {
    payload.command = "sdm.devices.commands.ThermostatTemperatureSetpoint.SetCool";
    payload.params["coolCelsius"] = coolCelsius;
  }
  else if ("HEATCOOL" === id("tempMode").value) {
    payload.command = "sdm.devices.commands.ThermostatTemperatureSetpoint.SetRange";
    payload.params["heatCelsius"] = heatCelsius;
    payload.params["coolCelsius"] = coolCelsius;
  } else {
    console.log("Off and Eco mode don't allow this function");
    return;
  }
  deviceAccessRequest('POST', 'temperatureSetpoint', endpoint, payload);
}

7. Node.js 服务器(可选)

恭喜!你已构建了一个客户端 Web 应用,该应用可通过浏览器发出 Smart Device Management API 请求。如果你想在服务器端进行构建,我们提供了一个可对来自浏览器的请求进行重定向的代理服务器,助你快速上手。

对于该代理服务器,我们将使用 Firebase Cloud Functions、Node.js 和 Express。

初始化 Cloud Functions

打开一个新的终端窗口,转到你的项目目录,然后运行以下命令:

$ firebase init functions

Firebase 会要求你回答一组问题,以便初始化 Cloud Functions:

  1. 你想使用什么语言编写 Cloud Functions 函数?- JavaScriptJavaScript
  2. 是否要使用 ESLint 捕获可能出现的 bug 并强制执行样式?- 否
  3. 是否要立即使用 npm 安装依赖项?- 是

这会在你的项目中初始化一个 functions 文件夹,并安装必要的依赖项。你会看到你的项目文件夹中包含一个 functions 目录,其中的 index.js 文件用于定义 Cloud Functions 函数,package.json 用于定义设置,node_modules 目录则包含依赖项。index.jsindex.jsindex.js

我们将使用两个 npm 库来构建服务器端功能:express 和 xmlhttprequest。你需要将以下条目添加到 package.json 文件中的依赖项列表中:

"xmlhttprequest": "^1.8.0",
"express": "^4.17.0"

然后,从 functions 目录运行 npm install,这应当会为你的项目安装依赖项:

$ npm install

如果 npm 在下载软件包时遇到问题,可以尝试使用以下命令明确保存 xmlhttprequest 和 express:

$ npm install express xmlhttprequest --save

升级到 Blaze 方案

使用 firebase deploy 命令要求你升级到 Blaze 方案,这需要你为账号添加一种付款方式。依次转到项目概览 > 使用量和结算,确保为你的项目选择 Blaze 方案。

c6a5e5a21397bef6.png

构建 Express 服务器

Express 服务器采用简单的框架来响应传入 GETPOST 请求。我们构建了一个 servlet,用于监听 POST 请求、将请求传送到载荷中指定的目标网址,以及使用通过传输收到的回复进行响应。

将 functions 目录中的 index.js 文件修改为如下所示的代码:

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
const functions = require('firebase-functions');
const express = require('express');
const http = require('http');

const app = express();
app.use(express.json());


//***** Device Access - Proxy Server *****//

// Serving Get Requests (Not used) 
app.get('*', (request, response) => {
  response.status(200).send("Hello World!");
});
// Serving Post Requests
app.post('*', (request, response) => {
  
  setTimeout(() => {
    // Read the destination address from payload:
    var destination = request.body.address;
    
    // Create a new proxy post request:
    var xhr = new XMLHttpRequest();
    xhr.open('POST', destination);
    
    // Add original headers to proxy request:
    for (var key in request.headers) {
            var value = request.headers[key];
      xhr.setRequestHeader(key, value);
    }
    
    // Add command/parameters to proxy request:
    var newBody = {};
    newBody.command = request.body.command;
    newBody.params = request.body.params;
    
    // Respond to original request with the response coming
    // back from proxy request (to Device Access Endpoint)
    xhr.onload = function () {
      response.status(200).send(xhr.responseText);
    };
    
    // Send the proxy request!
    xhr.send(JSON.stringify(newBody));
  }, 1000);
});

// Export our app to firebase functions:
exports.app = functions.https.onRequest(app);

为了将请求路由到我们的服务器,我们需要将来自 firebase.json 的重写调整为如下所示的代码:

{
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [{
        "source": "/proxy**",
        "function": "app"
      },{
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

这会将以 /proxy 开头的网址路由到我们的 Express 服务器,并让其余网址继续路由到 index.html

代理 API 调用

现在,我们的服务器已准备就绪,接下来,我们要在 scripts.js 中定义一个代理 URI,以便让浏览器向该地址发送请求:

const PROXY_URI = SERVER_URI + "/proxy";

然后,向 scripts.js 添加一个 proxyRequest 函数(其签名与 deviceAccessRequest(...) 函数相同),以便间接调用设备权限。

function proxyRequest(method, call, localpath, payload = null) {
    var xhr = new XMLHttpRequest();
    
    // We are doing our post request to our proxy server:
    xhr.open(method, PROXY_URI);
    xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
    xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
    xhr.onload = function () {
      // Response is passed to deviceAccessResponse function:
      deviceAccessResponse(call, xhr.response);
    };
    
    // We are passing the device access endpoint in address field of the payload:
    payload.address = "https://smartdevicemanagement.googleapis.com/v1" + localpath;
    if ('POST' === method && payload)
        xhr.send(JSON.stringify(payload));
    else
        xhr.send();
}

最后一步是在 scripts.jspostThermostatMode()postTemperatureSetpoint() 函数中,将 deviceAccessRequest(...) 调用替换为 proxyRequest(...) 函数。

运行 firebase deploy 以更新应用。

$ firebase deploy

这样一来,你就拥有了一个可在 Cloud Functions 中使用 Express 的有效 Node.js 代理服务器。

提供 Cloud Functions 函数权限

最后一步是检查 Cloud Functions 函数的访问权限,并确保你的客户端应用能够调用这些函数。

在 Google Cloud Platform 中,从菜单转到 Cloud Functions 标签页,然后选择你的 Cloud Functions 函数:

461e9bae74227fc1.png

点击权限,然后点击添加成员将 allUsers 写入新成员字段,然后依次选择“Cloud Functions”>“Cloud Functions Invoker”作为角色。点击“保存”将显示一条警告消息:

3adb01644217578c.png

选择“允许公开访问”,你的客户端应用即可使用你的 Cloud Functions 函数。

恭喜 – 你已完成所有步骤。现在,你可以转到自己的 Web 应用,试一试通过代理服务器路由的设备控件!

后续步骤

想要扩展你在设备权限方面的专业知识?你可查看特征文档,详细了解如何控制其他 Nest 设备;还可查看认证流程,了解向全球发布产品的步骤!

你还可以通过设备权限 Web 应用示例应用进一步提升你的技能。在此示例应用中,你将以学过的 Codelab 为基础,构建并部署一款能够正常运行的 Web 应用来控制 Nest 摄像头、门铃和温控器。