IoT(モノのインターネット)デベロッパーは、ユーザーが Google Home アプリのタップ コントロールやアシスタントへの音声コマンドでデバイスを操作できるスマートホーム アクションを構築できます。

スマートホーム アクションではホームグラフが使用されます。家やスマートホーム デバイスに関するコンテキスト データを提供するホームグラフは、家の論理的な地図のようなものです。このコンテキストにより、アシスタントはユーザーのリクエストを屋内の位置と関連付けて、より自然な形で理解できるようになります。たとえば、ホームグラフには、「さまざまなメーカーの複数のデバイス(エアコン、照明、扇風機、掃除機など)が置かれたリビングルーム」というコンセプトを保存できます。

前提条件
- スマートホーム アクションの作成に関するデベロッパー ガイド
 
目標
この Codelab では、仮想スマート洗濯機を管理するクラウド サービスを公開し、スマートホーム アクションを構築してアシスタントにリンクします。
演習内容
- スマートホーム クラウド サービスをデプロイする方法
 - サービスをアシスタントにリンクする方法
 - デバイスの状態変化を Google に公開する方法
 
必要なもの
- ウェブブラウザ(Google Chrome など)
 - Google Home アプリがインストールされている iOS または Android デバイス
 - Node.js バージョン 10.16 以降
 - Google Cloud 請求先アカウント
 
アクティビティ管理を有効にする
アシスタントで使用する Google アカウントで、次のアクティビティ管理を有効にします。
- ウェブとアプリのアクティビティ
 - デバイス情報
 - 音声アクティビティ
 
Actions プロジェクトを作成する
- Actions on Google Developer Console に移動します。
 - [New Project](新しいプロジェクト)をクリックし、プロジェクトの名前を入力して [Create Project](プロジェクトを作成)をクリックします。
 
スマートホーム アプリを選択する
Actions Console の概要画面で [Smart Home](スマートホーム)を選択します。
[Smart home](スマートホーム)エクスペリエンス カードを選択すると、プロジェクト コンソールが表示されます。
Firebase CLI をインストールする
Firebase コマンドライン インターフェース(CLI)を使用すると、ウェブアプリをローカルで提供し Firebase Hosting にデプロイできます。
CLI をインストールするには、ターミナルから次の npm コマンドを実行します。
npm install -g firebase-tools
CLI が正しくインストールされたことを確認するには、次のコマンドを実行します。
firebase --version
Google アカウントで Firebase CLI を承認するには、次のコマンドを実行します。
firebase login
開発環境の設定が完了したので、スターター プロジェクトをデプロイし、すべてが正しく設定されていることを確認しましょう。
ソースコードを取得する
下のリンクをクリックして、この Codelab のサンプルを開発マシンにダウンロードします。
または、コマンドラインから GitHub リポジトリのクローンを作成することもできます。
git clone https://github.com/googlecodelabs/smarthome-washer.git
プロジェクトについて
スターター プロジェクトには、以下のサブディレクトリが含まれています。
public:スマート洗濯機の状態を簡単に制御、モニタリングするためのフロントエンド UI。functions:Cloud Functions for Firebase と Firebase Realtime Database を使用してスマート洗濯機を管理する、実装が完了したクラウド サービス。
Firebase に接続する
washer-start ディレクトリに移動し、Actions プロジェクトに Firebase CLI を設定します。
cd washer-start firebase use <project-id>
Firebase にデプロイする
functions フォルダに移動し、npm. を使用して必要な依存関係をすべてインストールします。
cd functions npm install
これで依存関係のインストールとプロジェクトの設定が完了し、アプリを実行する準備が整いました。
firebase deploy
コンソールに次のような出力が表示されます。
... ✔ Deploy complete! Project Console: https://console.firebase.google.com/project/<project-id>/overview Hosting URL: https://<project-id>.firebaseapp.com
このコマンドによって、いくつかの Cloud Functions for Firebase とともにウェブアプリがデプロイされます。
ブラウザで Hosting URL(https://<project-id>.firebaseapp.com)を開き、ウェブアプリを表示します。次のようなインターフェースが表示されます。
このウェブ UI は、デバイスの状態を表示したり変更したりするためのサードパーティ プラットフォームを表したものです。データベースへのデバイス情報の入力を開始するには、[UPDATE](更新)をクリックします。ページの表示は変化しませんが、洗濯機の現在の状態がデータベースに保存されます。
次は、Actions Console を使用して、デプロイしたクラウド サービスを Google アシスタントにリンクします。
Actions Console プロジェクトを設定する
[Overview](概要)> [Build your Action](アクションの構築) で、[Add Action(s)](アクションを追加)を選択します。スマートホーム インテントのフルフィルメントを提供する Cloud Functions の URL を入力し、[Save](保存)をクリックします。
https://us-central1-<project-id>.cloudfunctions.net/smarthome
[Develop] > [Invocation](呼び出し)タブで、アクションの [Display Name](表示名)を追加して [Save] をクリックします。この名前は Google Home アプリに表示されます。
アカウントのリンクを有効にするには、左側のナビゲーションで [Develop] > [Account linking](アカウントのリンク)を選択します。以下の値でアカウントのリンクを設定します。
クライアント ID  | 
  | 
クライアント シークレット  | 
  | 
認証 URL  | 
  | 
トークンの URL  | 
  | 
[Save] をクリックしてアカウントのリンク設定を保存し、[Test](テスト)をクリックしてプロジェクトでのテストを有効にします。
[Simulator](シミュレータ)にリダイレクトされます。[Testing on Device](デバイスでのテスト)アイコン()にカーソルを移動し、プロジェクトでテストが有効になっていることを確認します。
これで、デバイスの状態とアシスタントをリンクするために必要な Webhook の実装を開始できるようになりました。
アクションの設定が完了したら、次はデバイスを追加してデータを送信します。クラウド サービスでは、以下のインテントを処理する必要があります。
SYNCインテント。ユーザーがリンク済みのデバイスについてアシスタントが問い合わせたときに発生します。これは、ユーザーがアカウントをリンクしたときにサービスに送信されます。ユーザーのすべてのデバイスとその機能を格納した JSON ペイロードで応答する必要があります。QUERYインテント。デバイスの現在の状態をアシスタントが問い合わせたときに発生します。リクエストされた各デバイスの状態を格納した JSON ペイロードで応答する必要があります。EXECUTEインテント。アシスタントがユーザーに代わってデバイスを制御するときに発生します。リクエストされた各デバイスの実行状態を格納した JSON ペイロードで応答する必要があります。DISCONNECTインテント。ユーザーがアシスタントとアカウントのリンクを解除したときに発生します。このユーザーのデバイスのイベントをアシスタントに送信するのを停止する必要があります。
以降のセクションでは、すでにデプロイした関数を、これらのインテントを処理できるように更新します。
SYNC レスポンスを更新する
functions/index.js を開きます。このファイルには、アシスタントからのリクエストに応答するコードが含まれています。
SYNC インテントを処理するには、デバイスのメタデータと機能を返す必要があります。onSync 配列の JSON に、デバイス情報と、洗濯機の推奨トレイトを追加します。
index.js
app.onSync((body) => {
  return {
    requestId: body.requestId,
    payload: {
      agentUserId: USER_ID,
      devices: [{
        id: 'washer',
        type: 'action.devices.types.WASHER',
        traits: [
          'action.devices.traits.OnOff',
          'action.devices.traits.StartStop',
          'action.devices.traits.RunCycle',
        ],
        name: {
          defaultNames: ['My Washer'],
          name: 'Washer',
          nicknames: ['Washer'],
        },
        deviceInfo: {
          manufacturer: 'Acme Co',
          model: 'acme-washer',
          hwVersion: '1.0',
          swVersion: '1.0.1',
        },
        willReportState: true,
        attributes: {
          pausable: true,
        },
      }],
    },
  };
});
Firebase にデプロイする
Firebase CLI を使用して、更新したクラウド フルフィルメントをデプロイします。
firebase deploy --only functions
Google アシスタントにリンクする
スマートホーム アクションをテストするには、プロジェクトを Google アカウントにリンクする必要があります。そうすると、同じアカウントにログインしている Google アシスタント画面と Google Home アプリでテストできるようになります。
- スマートフォンで Google アシスタントの設定を開きます。なお、コンソールと同じアカウントでログインする必要があります。
 - [Google アシスタント] > [設定] > [スマートホーム]([アシスタント] の下)に移動します。
 - 右下にあるプラス(+)アイコンを選択します。
 - テストアプリが [test] 接頭辞と設定した表示名とともに表示されます。
 - そのアイテムを選択します。Google アシスタントがサービスで認証を行い、
SYNCリクエストを送信してデバイスのリストをユーザーに提供するようサービスに依頼します。 
Google Home アプリを開いて、洗濯機デバイスが表示されることを確認します。
クラウド サービスが洗濯機デバイスを Google に正しく報告できたので、次は、デバイスの状態をリクエストしてコマンドを送信する機能を追加します。
QUERY インテントを処理する
QUERY インテントには一連のデバイスが含まれています。デバイスごとに現在の状態を返す必要があります。
functions/index.js で、インテント リクエストに含まれる対象デバイスのリストを処理するよう QUERY ハンドラを編集します。
index.js
app.onQuery(async (body) => {
  const {requestId} = body;
  const payload = {
    devices: {},
  };
  const queryPromises = [];
  const intent = body.inputs[0];
  for (const device of intent.payload.devices) {
    const deviceId = device.id;
    queryPromises.push(queryDevice(deviceId)
        .then((data) => {
        // Add response to device payload
          payload.devices[deviceId] = data;
        }
        ));
  }
  // Wait for all promises to resolve
  await Promise.all(queryPromises);
  return {
    requestId: requestId,
    payload: payload,
  };
});
リクエストに含まれるデバイスごとに、Realtime Database に保存されている現在の状態を返します。洗濯機の状態データを返すように queryFirebase 関数と queryDevice 関数を更新します。
index.js
const queryFirebase = async (deviceId) => {
  const snapshot = await firebaseRef.child(deviceId).once('value');
  const snapshotVal = snapshot.val();
  return {
    on: snapshotVal.OnOff.on,
    isPaused: snapshotVal.StartStop.isPaused,
    isRunning: snapshotVal.StartStop.isRunning,
  };
};
const queryDevice = async (deviceId) => {
  const data = await queryFirebase(deviceId);
  return {
    on: data.on,
    isPaused: data.isPaused,
    isRunning: data.isRunning,
    currentRunCycle: [{
      currentCycle: 'rinse',
      nextCycle: 'spin',
      lang: 'en',
    }],
    currentTotalRemainingTime: 1212,
    currentCycleRemainingTime: 301,
  };
};
EXECUTE インテントを処理する
EXECUTE インテントは、デバイスの状態を更新するコマンドを処理します。レスポンスが返すのは、各コマンドのステータス(SUCCESS、ERROR、PENDING など)と更新後のデバイスの状態です。
functions/index.js で、更新が必要なトレイトのリストと各コマンドの対象デバイスのセットを処理するよう EXECUTE ハンドラを編集します。
index.js
app.onExecute(async (body) => {
  const {requestId} = body;
  // Execution results are grouped by status
  const result = {
    ids: [],
    status: 'SUCCESS',
    states: {
      online: true,
    },
  };
  const executePromises = [];
  const intent = body.inputs[0];
  for (const command of intent.payload.commands) {
    for (const device of command.devices) {
      for (const execution of command.execution) {
        executePromises.push(
            updateDevice(execution, device.id)
                .then((data) => {
                  result.ids.push(device.id);
                  Object.assign(result.states, data);
                })
                .catch(() => functions.logger.error('EXECUTE', device.id)));
      }
    }
  }
  await Promise.all(executePromises);
  return {
    requestId: requestId,
    payload: {
      commands: [result],
    },
  };
});
コマンドと対象デバイスごとに、リクエストされたトレイトに対応する Realtime Database の値を更新します。該当する Firebase 参照を更新して更新後のデバイスの状態を返すように updateDevice 関数を変更します。
index.js
const updateDevice = async (execution, deviceId) => {
  const {params, command} = execution;
  let state; let ref;
  switch (command) {
    case 'action.devices.commands.OnOff':
      state = {on: params.on};
      ref = firebaseRef.child(deviceId).child('OnOff');
      break;
    case 'action.devices.commands.StartStop':
      state = {isRunning: params.start};
      ref = firebaseRef.child(deviceId).child('StartStop');
      break;
    case 'action.devices.commands.PauseUnpause':
      state = {isPaused: params.pause};
      ref = firebaseRef.child(deviceId).child('StartStop');
      break;
  }
  return ref.update(state)
      .then(() => state);
};
3 つのインテントをすべて実装したら、アクションで洗濯機を制御できるかどうかをテストします。
Firebase にデプロイする
Firebase CLI を使用して、更新したクラウド フルフィルメントをデプロイします。
firebase deploy --only functions
洗濯機をテストする
スマートフォンで以下の音声コマンドを試して、値が変化することを確認します。
「OK Google, 洗濯機をオンにして。」
「OK Google, 洗濯機を一時停止して。」
「OK Google, 洗濯機を止めて。」
また、洗濯機の現在の状態を確認することもできます。
「OK Google, 洗濯機はオンになってる?」
「OK Google, 洗濯機は動いてる?」
「OK Google, 洗濯機の今のステップを教えて」
これらのクエリとコマンドは、ナビゲーション メニューの [Develop] > [Functions](関数)> [Logs](ログ)をクリックして確認することもできます。

以上でクラウド サービスとスマートホーム インテントの統合がすべて完了し、洗濯機デバイスの現在の状態の制御とクエリの実行ができるようになりました。ただし、サービスがアシスタントにイベント情報(デバイスのプレゼンスや状態の変化など)を自動的に送信する手段がまだ実装されていません。
Request Sync を使用すると、ユーザーがデバイスを追加または削除したときやデバイスの機能が変更されたときに、新しい同期リクエストをトリガーできます。Report State を使用すると、ユーザーがデバイスの状態を物理的に変更したとき(たとえば照明のスイッチをオンにしたとき)や、別のサービスを使用してその状態を変更したときに、デバイスの状態をクラウド サービスから自動的にホームグラフに送信できるようになります。
このセクションでは、フロントエンドのウェブアプリからこれらのメソッドを呼び出すためのコードを追加します。
HomeGraph API を有効にする
HomeGraph API を使用すると、ユーザーのホームグラフ内のデバイスとその状態を保存して照会できます。この API を使用するには、まず Google Cloud Console を開いて HomeGraph API を有効にする必要があります。
Google Cloud Console でアクションの <project-id>. に一致するプロジェクトを選択し、HomeGraph API の [API ライブラリ] 画面で [有効にする]をクリックします。
Report State を有効にする
Realtime Database への書き込みによって、スターター プロジェクトの reportstate 関数がトリガーされます。データベースに書き込まれたデータをキャプチャして Report State 経由でホームグラフに送信するよう、functions/index.js の reportstate 関数を更新します。
index.js
exports.reportstate = functions.database.ref('{deviceId}').onWrite(
    async (change, context) => {
      functions.logger.info('Firebase write event triggered Report State');
      const snapshot = change.after.val();
      const requestBody = {
        requestId: 'ff36a3cc', /* Any unique ID */
        agentUserId: USER_ID,
        payload: {
          devices: {
            states: {
              /* Report the current state of our washer */
              [context.params.deviceId]: {
                on: snapshot.OnOff.on,
                isPaused: snapshot.StartStop.isPaused,
                isRunning: snapshot.StartStop.isRunning,
              },
            },
          },
        },
      };
      const res = await homegraph.devices.reportStateAndNotification({
        requestBody,
      });
      functions.logger.info('Report state response:', res.status, res.data);
    });
Request Sync を有効にする
フロントエンド ウェブ UI のアイコンを更新すると、スターター プロジェクトの requestsync 関数がトリガーされます。functions/index.js の requestsync 関数に、HomeGraph API の呼び出しを実装します。
index.js
exports.requestsync = functions.https.onRequest(async (request, response) => {
  response.set('Access-Control-Allow-Origin', '*');
  functions.logger.info(`Request SYNC for user ${USER_ID}`);
  try {
    const res = await homegraph.devices.requestSync({
      requestBody: {
        agentUserId: USER_ID,
      },
    });
    functions.logger.info('Request sync response:', res.status, res.data);
    response.json(res.data);
  } catch (err) {
    functions.logger.error(err);
    response.status(500).send(`Error requesting sync: ${err}`);
  }
});
Firebase にデプロイする
Firebase CLI を使用して、更新したコードをデプロイします。
firebase deploy --only functions
実装をテストする
ウェブ UI で再読み込みボタン 
 をクリックし、Firebase コンソールのログに同期リクエストが表示されていることを確認します。

次に、フロントエンド ウェブ UI で洗濯機の属性を調整し、[Update] をクリックします。状態の変化が Google に報告されていることを Firebase コンソールのログで確認します。

以上で、スマートホーム アクションを使用するデバイス クラウド サービスとアシスタントの統合が完了しました。
さらに詳しく
さらに詳しく学びたい方は、以下のことをお試しください。
- デバイスに Modes や Toggles を追加する。
 - サポートされているトレイトをデバイスに追加する。
 - スマートホームのローカル実行を検討する。
 - GitHub のサンプルで詳しいコードを確認する。
 
アクションの審査(アクションをユーザーに公開するための認定プロセスを含む)を受ける前に行うテストと送信についての詳細もご確認ください。