建構裝置存取網頁應用程式

1. 簡介

開發人員可透過 Device Access 計畫提供的 Smart Device Management API (REST API),從應用程式控制 Google Nest 裝置。使用者必須同意第三方存取 Nest 裝置。

52f77aa38cda13a6.png

成功整合裝置存取權有三個重要步驟:

  1. 建立專案 - 在 Google Cloud Platform 中建立專案,並在 Device Access 控制台中註冊為開發人員。
  2. 帳戶連結 - 透過帳戶連結流程取得使用者,並擷取存取代碼。以授權碼換取存取權杖。
  3. 裝置控制:發出 Smart Device Management API 要求,透過存取權杖傳送指令來控制裝置。

在本程式碼研究室中,我們將深入瞭解 Device Access 的運作方式,方法是建構處理驗證的網路應用程式,並發出 Smart Device Management API 呼叫。我們也會探討如何使用 Node.js 和 Express 部署簡單的 Proxy 伺服器,以便轉送 Device Access 要求。

開始之前,建議先複習本程式碼研究室中會用到的常見網路技術,例如使用 OAuth 2.0 進行驗證使用 Node.js 建構網路應用程式,但這些並非必要條件。

事前準備

  • Node.js 8 以上版本
  • 已連結 Nest 溫度控制器的 Google 帳戶

課程內容

  • 設定 Firebase 專案,用於託管靜態網頁和雲端函式
  • 透過瀏覽器型網頁應用程式發出裝置存取要求
  • 使用 Node.js 和 Express 建構 Proxy 伺服器,以轉送要求

2. 建立專案

開發人員必須建立 Google Cloud Platform (GCP) 專案,才能設定 Device Access 整合。在 GCP 專案中產生的「用戶端 ID」和「用戶端密鑰」,將用於開發人員應用程式與 Google Cloud 之間的 OAuth 流程。開發人員也必須前往 Device Access 控制台建立專案,才能存取 Smart Device Management API。

Google Cloud Platform

前往 Google Cloud Platform。按一下「建立新專案」,然後提供專案名稱。系統也會顯示 Google Cloud 的專案 ID [GCP-Project-Id],請記下這個 ID,因為我們會在 Firebase 設定期間使用。(在本程式碼研究室中,我們會將這個 ID 稱為 [GCP-Project-Id])。

585e926b21994ac9.png

第一步是在專案中啟用必要的 API 程式庫。依序前往「APIs & Services」(API 和服務) >「Library」(程式庫),然後搜尋 Smart Device Management API。您必須啟用這項 API,才能授權專案對 Device Access API 呼叫提出要求。

14e7eabc422c7fda.png

在建立 OAuth 憑證之前,我們需要先設定專案的 OAuth 同意畫面。前往「APIs & Services」(API 和服務) >「OAuth consent screen」(OAuth 同意畫面)。在「使用者類型」部分,選擇「外部」。提供應用程式名稱和支援電子郵件,以及開發人員聯絡資訊,完成第一個畫面。系統要求提供「測試使用者」時,請務必在此步驟提供已連結裝置的電子郵件地址。

設定 OAuth 同意畫面後,請前往「APIs & Services」(API 和服務) >「Credentials」(憑證)。依序點按「+ 建立憑證」和「OAuth 用戶端 ID」。應用程式類型請選取「網頁應用程式」

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 時會用到。

3. Firebase 設定

開發人員可透過 Firebase 快速輕鬆地部署網頁應用程式。我們將使用 Firebase,為裝置存取整合開發用戶端網頁應用程式。

建立 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

初始化託管專案

登入後,下一步是為網頁應用程式初始化代管專案。在終端機中,前往要建立專案的資料夾,然後執行下列指令:

$ firebase init hosting

Firebase 會詢問一連串問題,協助您開始使用託管專案:

  1. 請選取選項:「使用現有專案」
  2. 為這個目錄選取預設 Firebase 專案 - 選擇***[GCP-Project-Id]***
  3. 您想使用哪個目錄做為公開目錄?— 公開
  4. 要設定為單一頁面應用程式嗎?— 是
  5. Set up automatic builds and deploys with GitHub? — 否

專案初始化完成後,即可使用下列指令將專案部署至 Firebase:

$ firebase deploy

Firebase 會掃描專案,並將必要檔案部署至雲端代管服務。

fe15cf75e985e9a1.png

在瀏覽器中開啟代管網址時,您應該會看到剛部署的網頁:

e40871238c22ebe2.png

您現在已瞭解如何使用 Firebase 部署網頁,接下來要部署程式碼研究室範例!

4. 程式碼研究室範例

您可以使用下列指令,複製 GitHub 上託管的 程式碼研究室存放區

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

這個存放區提供兩個獨立資料夾中的範例。codelab-start 資料夾包含必要檔案,可讓您從本程式碼研究室的目前進度開始。codelab-done 資料夾包含本程式碼研究室的完整版本,以及可正常運作的用戶端和 node.js 伺服器。

在本程式碼研究室中,我們將使用 codelab-start 資料夾中的檔案,但如果您在任何時候感到困惑,也可以參考 codelab-done 版本。

程式碼研究室範例檔案

codelab-start 資料夾的檔案結構如下:

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

Public 資料夾包含應用程式的靜態網頁。負責將網頁要求轉送至應用程式。在 codelab-done 版本中,您也會看到 functions 目錄,其中包含要在 Google Cloud 函式上部署的 Proxy 伺服器 (Express) 邏輯。firebase.json

部署 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 Client Ids」(OAuth 2.0 用戶端 ID) 清單下方,選取您在「專案建立」步驟中建立的用戶端 ID。將應用程式的重新導向 URI 新增至專案的「已授權的重新導向 URI」清單。

6d65b298e1f005e2.png

請嘗試登入!

前往您使用 Firebase 設定的託管網址,輸入合作夥伴憑證,然後按一下「登入」按鈕。用戶端 ID 和用戶端密鑰是您從 Google Cloud Platform 取得的憑證,專案 ID 則來自 Device Access Console。

78b48906a2dd7c05.png

使用者點選「登入」按鈕後,系統會引導他們完成企業的 OAuth 流程,首先會顯示 Google 帳戶的登入畫面。登入後,系統會要求使用者授予專案存取 Nest 裝置的權限。

e9b7887c4ca420.png

由於這是模擬應用程式,Google 會先發出警告,再進行重新導向!

b227d510cb1df073.png

按一下「進階」,然後選取「前往 web.app (不安全)」,即可完成應用程式的重新導向。

673a4fd217e24dad.png

這會提供 OAuth 程式碼,做為傳入 GET 要求的一部分,應用程式隨後會將該程式碼換成存取權杖和更新權杖。

6. 裝置控制

Device Access 範例應用程式會使用 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 後方)
  • 酬載 (*):API 呼叫所需的額外資料 (例如代表設定點溫度的數值)

我們會建立範例 UI 控制項 (列出裝置、設定模式、設定溫度),用來控制 Nest Thermostat:

86f8a193aa397421.png

這些 UI 控制項會從 scripts.js 呼叫對應的函式 (listDevices()postThermostatMode()postTemperatureSetpoint())。這些函式留空,供您實作!目標是選擇正確的方法/路徑,並將酬載傳遞至 deviceAccessRequest(...) 函式。

可列出裝置

最簡單的 Device Access 呼叫是 listDevices。這項要求會使用 GET,且不需要酬載。端點必須使用 projectId 結構化。完成 listDevices() 函式,如下所示:

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

儲存變更,然後使用下列指令再次部署 Firebase 專案:

$ firebase deploy

部署新版應用程式後,請嘗試重新載入頁面,然後按一下「列出裝置」。這時「裝置控制」下方應該會顯示清單,其中包含溫度控制器的 ID:

b64a198673ed289f.png

從清單中挑選裝置後,scripts.js 檔案中的 deviceId 欄位就會更新。接下來的兩個控制項,我們需要為要控制的特定裝置指定 deviceId

控制溫度控制器

在 Smart Device Management API 中,有兩種特徵可基本控制 Nest 溫度控制器。ThermostatModeTemperatureSetpoint。ThermostatMode 會將 Nest 溫度控制器設為四種模式之一:{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 伺服器 (選用)

恭喜!您已建構用戶端網頁應用程式,可從瀏覽器提出 Smart Device Management API 要求。如果您想在伺服器端建構,我們提供 Proxy 伺服器,可重新導向來自瀏覽器的要求,協助您快速入門。

這個 Proxy 伺服器會使用 Firebase Cloud Functions、Node.js 和 Express。

初始化 Cloud Functions

開啟新的終端機視窗,前往專案目錄並執行下列指令:

$ firebase init functions

Firebase 會詢問一連串問題,以便初始化 Cloud Functions:

  1. 您想使用哪種語言編寫 Cloud Functions?— JavaScript
  2. 要使用 ESLint 找出可能的錯誤並強制執行樣式嗎?—
  3. 要立即使用 npm 安裝依附元件嗎?— 是

這會在專案中初始化 functions 資料夾,並安裝必要的依附元件。您會發現專案資料夾包含函式目錄,其中有 index.js 檔案 (用於定義 Cloud Functions)、package.json 檔案 (用於定義設定),以及 node_modules 目錄 (用於存放依附元件)。

我們將使用兩個 npm 程式庫建構伺服器端功能:express 和 xmlhttprequest。您需要在 package.json 檔案的依附元件清單中新增下列項目:

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

然後從函式目錄執行 npm install,即可安裝專案的依附元件:

$ npm install

如果 npm 下載套件時發生問題,可以嘗試使用下列指令明確儲存 xmlhttprequest 和 express:

$ npm install express xmlhttprequest --save

升級至 Blaze 方案

使用 firebase deploy 指令時,您必須升級至 Blaze 方案,並在帳戶中新增付款方式。依序前往「Project Overview」 >「Usage and billing」,並確認已為專案選取 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

Proxy API 呼叫

現在伺服器已準備就緒,讓我們在 scripts.js 中定義 Proxy URI,供瀏覽器將要求傳送至這個位址:

const PROXY_URI = SERVER_URI + "/proxy";

然後新增 proxyRequest 函式,該函式與 deviceAccessRequest(...) 函式具有相同簽章,適用於間接裝置存取呼叫。scripts.js

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.js 內的 postThermostatMode()postTemperatureSetpoint() 函式中,將 deviceAccessRequest(...) 呼叫替換為 proxyRequest(...) 函式。

執行 firebase deploy 來更新應用程式。

$ firebase deploy

這樣一來,您現在就能在 Cloud Functions 上使用 Express 執行 Node.js Proxy 伺服器。

提供 Cloud Functions 權限

最後一個步驟是檢查雲端函式的存取權,確保用戶端應用程式可以呼叫這些函式。

在 Google Cloud Platform 中,前往選單的「Cloud Functions」分頁,然後選取您的 Cloud Function:

461e9bae74227fc1.png

依序點按「權限」和「新增成員」。將 allUsers 寫入新的成員欄位,然後選取「Cloud Functions」>「Cloud Functions Invoker」(Cloud Functions > Cloud Functions 叫用者) 做為角色。按一下「儲存」會顯示警告訊息:

3adb01644217578c.png

選取「允許公開存取」後,用戶端應用程式就能使用雲端函式。

恭喜,你已完成所有步驟。現在您可以前往網路應用程式,試試透過 Proxy 伺服器轉送的裝置控制項!

後續步驟

想擴展裝置存取權的專業知識嗎?如要進一步瞭解如何控制其他 Nest 裝置,請參閱特徵說明文件;如要瞭解如何推出產品,請參閱認證程序

透過裝置存取網頁應用程式範例應用程式,進一步提升技能。您將以程式碼研究室的經驗為基礎,部署可運作的網頁應用程式,控制 Nest 攝影機、門鈴和溫度控制器。