デバイス アクセス ウェブ アプリケーションを作成する

1. はじめに

デバイス アクセス プログラムが提供する Smart Device Management API は、デベロッパーのアプリケーションから Google Nest デバイスを制御できるようにするための REST API です。ユーザーは、サードパーティによる Google Nest デバイスへのアクセスについて同意する必要があります。

52f77aa38cda13a6.png

デバイス アクセスの統合を実装するための主なステップは次の 3 つです。

  1. プロジェクトの作成 - Google Cloud Platform でプロジェクトを作成し、デバイス アクセス コンソールでデベロッパーとして登録します。
  2. アカウント リンク - ユーザーがアカウント リンクのフローに沿って進むよう設定し、アクセスコードを取得して、コードをアクセス トークンと交換します。
  3. デバイスの制御 - アクセス トークンを含むコマンドを送信してデバイスを制御するための Smart Device Management API リクエストを作成します。

この Codelab では、認証を処理するウェブ アプリケーションを構築し、Smart Device Management API 呼び出しを行うことで、デバイス アクセスの仕組みを詳しく確認します。また、Node.js と Express を使用してデバイス アクセス リクエストをルーティングするシンプルなプロキシ サーバーのデプロイ方法についても確認します。

始める前に、OAuth 2.0 認証Node.js を使用したウェブアプリの構築など、この Codelab で使用する一般的なウェブ技術について復習しておくことをおすすめします(ただしこれらの知識は前提条件ではありません)。

必要なもの

  • Node.js 8 以上
  • Google Nest Thermostat にリンク済みの Google アカウント

学習内容

  • 静的ウェブページと Cloud Functions をホストする Firebase プロジェクトを設定する
  • ブラウザベースのウェブ アプリケーションを介してデバイス アクセス リクエストを発行する
  • Node.js と Express を使用してリクエストをルーティングするプロキシ サーバーを構築する

2. プロジェクトの作成

デベロッパーはデバイス アクセスの統合を設定するにあたり、Google Cloud Platform(GCP)プロジェクトを作成する必要があります。GCP プロジェクト内で生成されたクライアント IDクライアント シークレットが、デベロッパーのアプリケーションと Google Cloud 間の OAuth フローで使用されます。また、デベロッパーはデバイス アクセス コンソールに移動し、Smart Device Management API にアクセスするためのプロジェクトを作成する必要もあります。

Google Cloud Platform

Google Cloud Platform に移動し、[プロジェクトを作成] をクリックしてプロジェクト名を入力します。Google Cloud 用のプロジェクト ID [GCP-Project-Id] も表示されるので、記録しておいてください。これは Firebase の設定時に使用します(以降、この Codelab ではこの ID を [GCP-Project-Id] として表記します)。

585e926b21994ac9.png

まず、プロジェクトで必要になる API ライブラリを有効にします。[API とサービス] > [ライブラリ] に移動し、Smart Device Management API を検索してください。この API を有効にして、プロジェクトがデバイス アクセスの API 呼び出しにリクエストを送信することを承認する必要があります。

14e7eabc422c7fda.png

OAuth 認証情報の作成を開始する前に、プロジェクトの OAuth 同意画面を設定する必要があります。[API とサービス] > [OAuth 同意画面] に移動し、[ユーザーの種類] で [外部] を選択します。アプリの名前とサポート用メールアドレス、デベロッパーの連絡先情報を入力して、最初の画面を完了します。テストユーザーの入力を求められたときは、このステップで指定した(デバイスをリンク済みの)メールアドレスを入力します。

OAuth 同意画面の設定が終わったら、[API とサービス] > [認証情報] に移動します。[+ 認証情報を作成] をクリックし、[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

login コマンドを使用して、Google アカウントで Firebase CLI ツールにログインします。

$ firebase login

ホスト プロジェクトを初期化する

ログインできたら、次はウェブ アプリケーションのホスト プロジェクトを初期化します。ターミナルで、プロジェクトを作成するフォルダに移動して次のコマンドを実行します。

$ firebase init hosting

Firebase から、ホスト プロジェクトを開始するための一連の質問が返されます。

  1. Please select an option(オプションを選択してください)- 既存のプロジェクトを使用します
  2. Select a default Firebase project for this directory(このディレクトリのデフォルト Firebase プロジェクトを選択してください)- ***[GCP-Project-Id]*** を選択します
  3. What do you want to use as your public directory?(公開ディレクトリには何を使用しますか?)- Public
  4. Configure as a single page app?(シングルページ アプリとして設定しますか?)- Yes
  5. Set up automatic builds and deploys with GitHub?(GitHub で自動ビルドと自動デプロイを設定しますか?)- No

プロジェクトの初期化が完了したら、次のコマンドでプロジェクトを Firebase にデプロイします。

$ firebase deploy

Firebase によりプロジェクトがスキャンされ、必要なファイルがクラウド ホスティングにデプロイされます。

fe15cf75e985e9a1.png

ホスト URL をブラウザで開くと、デプロイしたページが表示されます。

e40871238c22ebe2.png

ここまで、Firebase でウェブページをデプロイする基本的な方法を説明しました。次は、Codelab サンプルのデプロイに進みます。

4. Codelab サンプル

GitHub でホストされている Codelab リポジトリのクローンを作成するには、次のコマンドを使用します。

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

このリポジトリ内に今回のサンプルが 2 つのフォルダで提供されています。codelab-start フォルダには、この Codelab のこの時点から開始するために必要なファイルが含まれています。codelab-done フォルダには、この Codelab の完全なバージョン(完全に機能するクライアントと Node.js サーバー)が含まれています。

この Codelab では全体を通じて codelab-start フォルダのファイルを使用しますが、行き詰まったときはいつでも codelab-done バージョンを参照してください。

Codelab サンプル ファイル

codelab-start フォルダのファイル構造は次のとおりです。

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

public フォルダには、アプリケーションの静的ページが含まれています。firebase.json により、ウェブ リクエストがアプリにルーティングされます。codelab-done バージョンでは、Google Cloud Functions にデプロイされるプロキシ サーバー(Express)のロジックを含む functions ディレクトリも表示されます。

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 行目)にご自身のホスト URL を指定してください。

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

この変更を行ってアプリを再デプロイすると、プロジェクトに使用されるリダイレクト URI が更新されます。

$ firebase deploy

リダイレクト URI を有効にする

scripts.js ファイルでリダイレクト URI を更新したら、その URI を、プロジェクト用に作成したクライアント ID で使用可能なリダイレクト URI のリストにも追加する必要があります。Google Cloud Platform の [認証情報] ページに移動します。このページには、プロジェクト用に作成したすべての認証情報が表示されます。

1a07b624b5e548da.png

[OAuth 2.0 クライアント ID] のリストで、「プロジェクトの作成」で作成したクライアント ID を選択します。アプリのリダイレクト URI をプロジェクトの [承認済みのリダイレクト URI] のリストに追加します。

6d65b298e1f005e2.png

ログインしてみましょう

Firebase で設定したホスト URL に移動し、パートナーの認証情報を入力して、[ログイン] をクリックします。クライアント ID とクライアント シークレットは、Google Cloud Platform から取得した認証情報です。プロジェクト ID はデバイス アクセス コンソールで確認できます。

78b48906a2dd7c05.png

[ログイン] をクリックすると、ユーザーはデベロッパーの OAuth フローに沿って、Google アカウントへのログイン画面から開始することになります。ログイン後、ユーザーはプロジェクトによる Google Nest デバイスへのアクセスを許可するよう求められます。

e9b7887c4ca420.png

これは模擬アプリなので、リダイレクトの発行前に Google による警告が表示されます。

b227d510cb1df073.png

[詳細設定] をクリックし、[web.app(安全ではないページ)に移動] を選択してアプリへのリダイレクトを完了します。

673a4fd217e24dad.png

これで、受信 GET リクエストの一部として OAuth コードが提供されます。アプリはこのコードをアクセス トークンや更新トークンと交換します。

6. デバイスの制御

デバイス アクセス サンプルアプリは、Smart Device Management REST API 呼び出しを使用して Google Nest デバイスを制御します。これらの呼び出しでは、GET または POST リクエストのヘッダーでアクセス トークンが渡されるとともに、特定のコマンドに必要なペイロードが渡されます。

ここではこれらの呼び出しを処理するための一般的なアクセス リクエスト関数を記述しています。使用する際は、この関数に正しいエンドポイントと、必要に応じてペイロード オブジェクトを指定してください。

function deviceAccessRequest(method, call, localpath, payload = null) {...}
  • method - HTTP リクエストのタイプ(GET または POST)
  • call - 今回の API 呼び出しを表す文字列。レスポンスのルーティングに使用されます(listDevicesthermostatModetemperatureSetpoint
  • localpath - リクエストの送信先エンドポイント。プロジェクト ID とデバイス ID を含みます(https://smartdevicemanagement.googleapis.com/v1 の末尾に追加)
  • payload (*) - API 呼び出しに必要な追加データ(例: 設定温度を表す数値)

それでは、Google Nest Thermostat を制御するサンプル UI コントロール([List Devices(デバイスリストを表示)]、[Set Mode(モードを設定)]、[Set Temp(温度を設定)])を作成しましょう。

86f8a193aa397421.png

これらの UI コントロールは、scripts.js から対応する関数(listDevices()postThermostatMode()postTemperatureSetpoint())を呼び出します。該当部分は空白のまま残してあるので、ご自身で実装してください。ここでの目的は、正しいメソッドまたはパスを選択し、ペイロードを deviceAccessRequest(...) 関数に渡すことです。

デバイスリストを表示する

最もシンプルなデバイス アクセス呼び出しは 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 フィールドが更新されます。あとの 2 つのコントロールについては、制御する特定のデバイスの deviceId を指定する必要があります。

サーモスタットの制御

Smart Device Management API には、Google Nest Thermostat の基本的な制御を行う 2 つのトレイトとして、ThermostatModeTemperatureSetpoint があります。ThermostatMode は、Google Nest Thermostat のモードを 4 種類(Off、Heat、Cool、HeatCool)のうちいずれかに設定します。ここでは、選択されたモードをペイロードの一部として指定する必要があります。

scripts.jspostThermostatMode() 関数の内容を次の形式に沿って置き換えてください。

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() は、Google Nest Thermostat の温度(摂氏)の設定を処理します。ペイロードに設定できるパラメータには、選択されたサーモスタット モードに応じて heatCelsiuscoolCelsius の 2 つがあります。

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 リクエストを行うクライアンサイドのウェブ アプリケーションを作成できました。ここからは、サーバーサイドでのビルドを希望する方のために、ブラウザからリクエストをリダイレクトできるプロキシ サーバーをご紹介します。

このプロキシ サーバーには、Firebase の Cloud Functions、Node.js、Express を使用します。

Cloud Functions を初期化する

新しいターミナル ウィンドウを開き、プロジェクト ディレクトリに移動して、次のコマンドを実行します。

$ firebase init functions

Firebase から、Cloud Functions を初期化するための一連の質問が返されます。

  1. What language would you like to use to write Cloud Functions?(Cloud Functions の記述言語を選択してください)- JavaScript
  2. Do you want to use ESLint to catch probable bugs and enforce style?(潜在的なバグの検出とスタイルの適用に ESLint を使用しますか?)- No
  3. Do you want to install dependencies with npm now?(npm で依存関係を今すぐインストールしますか?)- Yes

これで、プロジェクトの functions フォルダが初期化され、必要な依存関係がインストールされます。プロジェクト フォルダ内には functions ディレクトリがあり、index.js(使用する Cloud Functions を定義するファイル)、package.json(設定を定義するファイル)、依存関係が格納される node_modules ディレクトリで構成されていることがわかります。

ここでは、サーバーサイド機能を構築するために、express と xmlhttprequest という 2 つの npm ライブラリを使用します。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 の受信リクエストに応答するシンプルなフレームワークに従っています。提供しているサーブレットは、POST リクエストをリッスンし、ペイロードで指定された宛先 URL に送信して、転送の結果返されたレスポンスで応答します。

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 からの rewites 部分を調整する必要があります。

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

これで、/proxy で始まる URL が Express サーバーにルーティングされ、残りの URL は引き続き index.html に転送されるようになります。

プロキシ API 呼び出し

サーバーの準備が完了したので、ブラウザからこのアドレスにリクエストを送信できるように、scripts.js でプロキシ URI を定義しましょう。

const PROXY_URI = SERVER_URI + "/proxy";

次に、デバイス アクセスの間接的な呼び出しを行うために proxyRequest 関数を scripts.js に追加します。これには 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 起動元] を選択します。[保存] をクリックすると次の警告メッセージが表示されます。

3adb01644217578c.png

[一般公開アクセスを許可] を選択すると、クライアントサイド アプリケーションで Cloud Functions の関数を使用できるようになります。

これですべての手順が完了し、ウェブアプリから、プロキシ サーバー経由でルーティングされるデバイス制御用コントロールにアクセスできるようになりました。

次のステップ

デバイス アクセスに関する知識をさらに広げるには、トレイトのドキュメントで、その他の Google Nest デバイスの制御についての詳細をご確認ください。また、認定プロセスに関する記事で、プロダクトを世界各国でリリースする手順をご確認ください。

デバイス アクセス ウェブ アプリケーション サンプルアプリを使ってスキルを磨きましょう。この Codelab の経験を活かして、Google Nest のカメラ、ドアホン、サーモスタットを制御できるウェブ アプリケーションをデプロイすることができます。