"○○のアプリにつないで"不要の Google Home 対応スマートホームアプリの実装

## はじめに
みなさん、Google Home を使っていますか。
私は先日のセールに釣られて2台購入し、天気を聞いたりラジオを流したり、タイマーセットをしたりテレビ消したりしてます。[^1]

さて、Google Home で家電を動かすという記事はいくつかあるようですが、外部サービスの IFTTT を使った構成例が多いですね。
 
IFTTT を使った連携の場合、トリガとなるフレーズを登録するだけなので、小数のアクションを用意するのは非常に簡単です。ただし、決められたフレーズを検知する機能しかないので、様々な場面に対応して柔軟に制御するには多数のフレーズを別々のアクションに設定する必要があります。[^2]

一方で Actions on Google を使った会話アプリを作る場合、最初に"ねぇ Google、〇〇のテストアプリにつないで"など、明示的にアプリに繋ぎに行く必要があり面倒です。[^3]


実は、Google Home には家電を操作する機能があらかじめ備わっています。試しに「エアコンつけて」と話しかけてみましょう。

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">ねぇGoogle、エアコンつけて <a href="https://t.co/ahrd9QVkOu">pic.twitter.com/ahrd9QVkOu</a></p>&mdash; td2sk (@td2sk) <a href="https://twitter.com/td2sk/status/942326148351934470?ref_src=twsrc%5Etfw">December 17, 2017</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


「サーモスタットがまだ設定されていません。Google Homeアプリを開いて、スマートホームからサーモスタットを追加してください」と返事が来ました。
実際に該当の画面を開いてみると、以下のようになっています。

![Screenshot_20171216-123219.png](https://qiita-image-store.s3.amazonaws.com/0/16112/51c5282d-ee5c-8943-b831-f0a20ed61b71.png)

おうち自動化でお馴染みの Philips Hue や Nest もありますね。
私は残念ながら該当サービスの製品を一つも持っていないため、試すことはできません。
しかし、なんとかしてこの一覧に自作のサービスを登録できれば、自作のデバイスからおうちの家電を制御できるわけです。
しかも、家電の制御はGoogle Homeの標準機能ですから、言葉の揺れ対策が組み込まれており、比較的柔軟に操作することができます。
たとえば、以下のような機能が標準で備わっています。[^4]

* 「電気つけて」「電球つけて」「ライトつけて」「明かりつけて」「照明つけて」 などの言葉の揺れ対策
* 「電気ついてる?」 などの状態問い合わせ
* 「寝室の電気けして」「リビングの電気つけて」 など、複数ある場合の呼び分け
* 「電気を全部けして」などの一括操作 [^5]

さっそく作ってみましょう。


## やること
* スマートホームアプリの概要
* スマートホームアプリの構成
  * スマートホーム Web サービス
    * OAuth2.0 認可サーバの実装(別記事)
    * 家電制御 API の実装
* Google Assistant の設定
  * Actions on Google プロジェクトの作成
  * スマートホームアプリとの接続
* 制御デバイス
  * ESP32 による赤外線学習リモコン製作(別記事)


## スマートホームアプリ
Google Homeアプリのスマートホームに対応したアプリは、どうしたら作れるのでしょうか。
Googleの開発者向けサイトにある、Actions on Google のSmartHomeの項目にシステム構成や実装の手引があります。[^6]
https://developers.google.com/actions/smarthome/

電球を操作する場合のシステム構成・連携図は次の通りです。

![home.png](https://qiita-image-store.s3.amazonaws.com/0/16112/5a99382b-8b79-49c2-1a3b-ce7e39d770b0.png)

上図に「電気をつけて」と指示を出した場合のフローを加えてみます。

![home2.png](https://qiita-image-store.s3.amazonaws.com/0/16112/2773d919-a242-4a2f-7912-d9964362b585.png)

Google Assistant 側には、おうち家電のデータベースがあるため、音声コマンドを解釈して、スマートホームアプリ経由で家電を操作することができます。

今回は、スマートホームアプリとそれに対応した家電を実装するわけですが、現実問題として家電を製作するのはハードルが高すぎますね。そこで、赤外線学習リモコンを使って以下のような構成にします。

![home5.png](https://qiita-image-store.s3.amazonaws.com/0/16112/e0388395-e419-95c1-1a6f-1bb53587f8dd.png)

## スマートホームアプリ
スマートホームアプリと聞いただけでは、人によって思い浮かべる内容が異なると思います(スマホアプリなのか Web アプリなのか、等々)。

本記事において、(Google Assitant 対応の)スマートホームアプリとは「Google とは独立に各家電メーカーなどが実装している Web サービスのうち、Google Assistant 向けの API を持つもの」と定義します。
一般に、Google Home連携のスマートホームアプリには以下の機能が必要です。

* ユーザ管理
  * 登録
  * ログイン
* 家電管理
  * 登録
  * 制御
* システム間連携
  * OAuth2.0 に基づく認可 API
  * Google Assistant 仕様に基づく家電操作 API

今回の記事では、ユーザは自分ひとりですから、ユーザ管理、家電管理は省略して、ソースコードにベタ書きすることとします。
したがって、主に実装する必要があるのは Google Assistant との連携部分になります。

### OAuth2.0 について
Google Assistant 連携 OAuth2.0 サーバについては、記事を分割しました。[^7]
OAuth2.0 認可サーバを自前で用意するのが難しい場合は、以下記事で、解説とすぐ使える実装を提供予定です。

* Google Assitatnt と連携するための必要最小限の OAuth2.0 認可サーバ実装(今週中に記事公開予定)

以降、本記事では、上記 OAuth2.0 認可サーバの記事に合わせて、以下を前提とします。

* OAuth2.0 の Authorization Code Grant が、以下 API で提供されていること
    * GET /authorize
    * POST /token
* 家電操作用の以下 API に、OAuth2.0 のアクセストークンによるアクセス制限が実装されていること
    * POST /google_assistant/endpoint
* 両 API とも、正規の証明書をもって、https サーバとしてインターネット上に公開されていること
    * 上記記事では ngrok を使います

さて、OAuth2.0 認可サーバができたとして、次に進みましょう。

## Actions on Google の設定
Actions on Google は、Google Assistant 対応の音声コマンド(アクション)を開発するためのプラットフォームです。Google Assistant 連携アプリを作るためには、Actions on Google の設定が必要になります。

スマートホームアプリとの接続に必要な設定は以下の通りです。

* Actions on Google のテストプロジェクト作成
* HomeGraph API の有効化と API キーの発行
* スマートホームアプリの URL を登録
* Google Home 連携アプリとしての設定

### Actions on Google のテストプロジェクト作成
まずは、Actions on Google のテストプロジェクトを作成しましょう。
Actions on Google(https://console.actions.google.com/) にアクセスして、新規プロジェクトを作成します。
Add/import project を選択し、Project name を適当に設定して CREATE PROJECT を選択すると、プロジェクトポータルに移動します。

![aog.png](https://qiita-image-store.s3.amazonaws.com/0/16112/a49a25d4-2b4e-5ecd-ec77-793611bb92e5.png)

Actions SDK の BUILD を選択するとダイアログが表示されるので、"2 Update app" の項目のコマンドをコピーしておきましょう。
なお、このコマンドの --project の引数が今回製作する Actions on Google のプロジェクト ID になります。(後ほど使います)

![aog2.png](https://qiita-image-store.s3.amazonaws.com/0/16112/2b80e0c8-ec90-7672-669d-793e495413e0.png)

次に、デプロイツイールである gactions を使って、アプリの持つ機能を設定します。

まず以下のページから、環境に合わせた gactions バイナリを取得してください。
https://developers.google.com/actions/tools/gactions-cli

次に、action.json を以下の内容で作成してください。これはスマートホーム用の設定になります。
url のところには、あなたのスマートホームアプリの家電連携 API の URL を設定してください。
(たとえば ngrok を使っている場合は https://(ngrok 指定のサブドメイン).ngrok.com/google_assistant/endpoint などになります)

```json:action.json
{
  "actions": [{
    "name": "actions.devices",
    "deviceControl": {
    },
    "fulfillment": {
      "conversationName": "automation"
    }
  }],
  "conversations": {
    "automation": {
      "name": "automation",
      "url": "https://<あなたのドメイン>/google_assistant/endpoint"
    }
  }
}
```

先ほどコピーしたコマンドの PROJECT_NAME のところを action.json に変更して実行すると、このファイルが Actions on Google に取り込まれます。

```sh
$ gactions update --action_package action.json --project (あなたのプロジェクト ID)
```

実行が完了すると Actions on Google のプロジェクトポータルが更新されます。

![aog3.png](https://qiita-image-store.s3.amazonaws.com/0/16112/151b285e-0f39-044c-7965-7ec9e9dbd914.png)


プロジェクトポータルの指示に従ってプロジェクト設定を進めます。主な設定項目は以下の通りです。

* 2 App information の内容を入力。アプリ名や説明、アイコンなどを設定します。設定が足りないとエラーになります。
* 6 Account linking (optional) を設定します
  * 1. Grant type: Authorization code にして NEXT
  * 2. Client information
      * Client ID: 認可サーバが Google Assistant を区別するのに使う ID を自由に設定。今回の例では'google_assistant'とします
      * Client secret: Google Assistant が使うパスワード。OAuth2.0 認可サーバで POST /token 時にクライアント検証で利用します
      * Authorization URL: GET /authorize の URL
      * Token URL: POST /token の URL
  * 3 Configure your client (optional)
      * scopes に OAuth2.0 のスコープ文字列を設定。開発するスマートホームアプリのために好きな値を設定できます。今回の例では'smarthome_operation'としましょう
  * 4 Testing instructions
      * Google の中の人がアプリ審査時に使うアカウント情報を記載する自由記入欄です。今回は審査に出さないので適当に書いておきましょう

すべて入力し、画面下の "TEST DRAFT" をクリックするとアプリが有効化されます。

### HomeGraph API の有効化
Google Assistant は、スマートホームアプリから通知された家電一覧のデータベースを持っています。

![hg.png](https://qiita-image-store.s3.amazonaws.com/0/16112/e567f7a9-ebdf-ca41-ea98-bd0119707656.png)

この家電の情報を管理する API が HomeGraph API です。Google Assistant から家電の制御をするため、この API の有効化が必要です。

まず、HomeGraph API の管理画面(https://console.cloud.google.com/launcher/details/google/homegraph.googleapis.com) にアクセスして、HomeGraph API を有効化してください。

次に、HomeGraph API にアクセスするための API キーを発行します。
同 URL から、管理 > 認証情報 > 認証情報を作成 > API キー と進んで、キーを発行します。
発行された API キーは後ほど使います。

### 動作確認
ここまでたどり着けば、ついにスマートホームアプリとして Google Home との連携が可能になっているはずです。
さっそくお手元のスマートフォンで Google Home アプリを起動して、 スマートホーム > アカウント管理 を見てみましょう。

![Screenshot_20171218-215724.png](https://qiita-image-store.s3.amazonaws.com/0/16112/808aafb1-5f85-bb8b-312c-20653fb71c2c.png)

自作した Google Home アプリが表示されていますね！
早速タップしてみると、途中でエラーになります。
そういえばまだ POST /google_assistant/endpoint を実装していませんでした。

次はいよいよ最後のステップ、POST /google_assistant/endpoint の実装です。

### POST /google_assistant/endpoint 実装
前提として、前述の OAuth2.0 実装に従い、この API はアクセストークンによる制限がかかっているものとします。

Google Assistant が家電を制御するために渡してくる JSON フォーマットは、以下ページに記載されています。
https://developers.google.com/actions/smarthome/create-app#build_fulfillment

実際に送られてくるのは以下3種類のコマンドです。

* action.devices.SYNC
    * 家電一覧を問い合わせる際に使われます。ここで返した家電リストが HomeGraph に登録されます
* action.devices.EXECUTE
    * 家電の操作(例: 電気つけて)の際に使われます。
* action.devices.QUERY
    * 家電の状態(例: 電気がついてる?)の問い合わせに使われます

一つずつ見ていきましょう。

### action.devices.SYNC
Google Assistant が、スマートホームアプリに対して家電一覧を問い合わせる際に呼ばれます。
具体的な呼び出しタイミングは以下の二通りです。

* スマートフォンの Google Home アプリで、アカウント連携の初回実行時
* HomeGraph API 経由で家電更新を通知したとき

家電一覧に変更(追加や削除、リネーム)があった際は、アカウント連携を一度解除して再接続するか、スマートホームアプリから HomeGraph API で家電更新の通知を飛ばす必要があります。

具体的な呼び出しフォーマットは以下のようになります。

```json
{
  "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
  "inputs": [{
    "intent": "action.devices.SYNC"
  }]
}
```

inputs.intent に操作が指定されています(後述の EXECUTE, SYNC も同様)。
action.devices.SYNC に対しては、以下のような JSON を返します。

```json
{
  "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
  "payload": {
    "agentUserId": "1836.15267389",
    "devices": [{
      "id": "123",
      "type": "action.devices.types.LIGHT",
        "traits": [
          "action.devices.traits.OnOff"
        ],
        "name": {
          "defaultNames": ["LED Lamp"],
          "name": "lamp1",
          "nicknames": ["LED Lamp"]
        },
        "willReportState": false
      }]
  }
}
```

requestId にはリクエストで受け取ったものと同じ値をセットします。
ここで使われている payload 以下のパラメータは以下の通りです

* agentUserId: スマートホームアプリで管理しているユーザ ID を**String型**で渡します。メールアドレスなど変更されうる値は避けましょう
* devices: 家電一覧を配列で渡します
    * id: 家電 ID を**String型**で渡します
    * type: 家電の種類を指定します
        * 家電の種類は https://developers.google.com/actions/smarthome/guides/ で規定されています
    * traits: 家電の持つ操作を配列で指定します
        * 今回の例では "action.devices.traits.OnOff" (オン/オフ機能を持つ) のみ設定しています
        * traits の取りうる値は https://developers.google.com/actions/smarthome/traits/ で規定されています
    * name: 家電の名前を指定します
        * defaultNames: 家電の製品名を指定します(メーカー名、型番など)
        * name: 家電の名前を指定します。ユーザが呼んだり、Google Home からの返答で読み上げられます
        * nicknames: その他、ユーザが自由に設定できる別名を指定します
    * willReportState: 状態変化を通知できる家電なら true, ポーリング等で都度確認する場合は false

その他、どのようなパラメータが定義されているかは公式ドキュメントで確認してください。
https://developers.google.com/actions/smarthome/create-app#actiondevicessync

ソースコードを修正して、 POST /google_assistant/endpoint が正しいレスポンスを返すようにすれば、Google Home アプリでのアカウント連携が動くようになります。再度 Google Home アプリに戻り、アカウント連携を実行してみましょう。

![Screenshot_20171213-230011.png](https://qiita-image-store.s3.amazonaws.com/0/16112/4e218cd3-5512-1335-ab39-3cbd92c08ae1.png)

家電が登録されました！

#### 家電一覧の更新
家電一覧に変更がある場合や異なる種類の家電を登録する場合、action.devices.SYNC の戻り値を変更するだけでなく、HomeGraph に対して更新要求を行う必要があります。HomeGraph API のところで発行した API キーを使って、以下のコマンドを実行しましょう。

```sh
# agentUserId, API_KEY は、環境に合わせて設定してください
curl -i -s -k -X POST -H "Content-Type: application/json" -d "{agent_user_id: \"agentUserId\"}" \
  "https://homegraph.googleapis.com/v1/devices:requestSync?key=API_KEY"
```

スマートホームアプリに対して、再度 action.devices.SYNC のリクエストが飛んできて、家電一覧が更新されます。
きちんとしたスマートホームアプリを実装した際は、ユーザが家電を追加・削除する毎に、上記 API を呼ぶことになるでしょう。

### action.devices.EXECUTE
action.devices.EXECUTE は、Google Assistant が家電を制御するときに呼ばれます。
以下のようなリクエストが飛んできます。

```json
{
  "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
  "inputs": [{
    "intent": "action.devices.EXECUTE",
    "payload": {
      "commands": [{
        "devices": [{
          "id": "123"
        },{
          "id": "456"
        }],
        "execution": [{
          "command": "action.devices.commands.OnOff",
          "params": {
            "on": true
          }
        }]
      }]
    }
  }]
}
```

細かい説明は省略しますが、"デバイス123とデバイス456の電源をオンにする"という形式になっていることはすぐ分かるでしょう。

レスポンスは以下のように返します。

```json
{
  "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
  "payload": {
    "commands": [{
      "ids": ["123"],
      "status": "SUCCESS"
    },{
      "ids": ["456"],
      "status": "ERROR",
      "errorCode": "deviceTurnedOff"
    }]
  }
}
```

成功・失敗のステータス毎に、id をまとめて返却するだけですね。

このリクエストが届いた際に、該当の家電を制御するよう実装すれば、ついに Google Home から家電を制御できます！

その他パラメータは以下を参照してください。
https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute

### actions.devices.QUERY
最後は状態問い合わせの API です。
リクエストは以下の形式です。

```json
{
  "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
  "inputs": [{
    "intent": "action.devices.QUERY",
    "payload": {
      "devices": [{
        "id": "123"
      },{
        "id": "456"
      }]
    }
  }]
}
```

action.devices.EXECUTE の場合と同様ですね。
レスポンスは以下の形式です。

```json
{
  "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
  "payload": {
    "devices": {
      "123": {
        "on": true,
        "online": true
      },
      "456": {
        "on": true,
        "online": true,
        "brightness": 80,
        "color": {
          "name": "cerulean",
          "spectrumRGB": 31655
        }
      }
    }
  }
}
```

action.devices.QUERY の仕様は以下ページに記載されています。
https://developers.google.com/actions/smarthome/create-app#actiondevicesquery

"on"や"online"、"brightness"など、レスポンス使える属性は、家電登録時に指定した traits で決まります。
どのようは値が使えるかは、以下で確認できます。
https://developers.google.com/actions/smarthome/traits/

以上、3つの処理で Google Assistant との連携は完了です！
繰り返しになりますが、以下のような音声コマンドによって、対応したリクエストを受け取れるはずです。

* 「電気つけて」「電球つけて」「ライトつけて」「明かりつけて」「照明つけて」 などの言葉の揺れ対策
* 「電気ついてる?」 などの状態問い合わせ
* 「寝室の電気けして」「リビングの電気つけて」 など、複数ある場合の呼び分け
* 「電気を全部けして」などの一括操作 [^5]

最後に、Ruby 製の軽量 Web フレームワークである sinatra による家電操作の実装例を掲載します。

```ruby
post '/google_assistant/endpoint' do
begin
  # ここでアクセストークンのチェックを行います
rescue
  unauthorized
end

begin
  body = JSON.parse(request.body.read)
  request_id = body['requestId']
  bad_request unless request_id

  intent = body['inputs'][0]['intent']
  case intent
  when 'action.devices.SYNC'
    # この例では家電(ライト)を1つで決め打ち。実際はデータベース等から動的にレスポンスを生成します
    body = {
      requestId: request_id,
      payload: {
        agentUserId: "#{user_id}",
        devices: [
          id: '123',
          type: 'action.devices.types.LIGHT',
          traits: [
            'action.devices.traits.OnOff'
          ],
          name: {
            defaultNames: ['ESP32 LED'],
            name: 'LED 1',
            nicknames: ['LED Lamp']
          },
          willReportState: false
        ]
      }
    }
  when 'action.devices.EXECUTE'
    payload = body['inputs'][0]['payload']
    # 例ではコマンドが1つのみ届く想定の実装になっています
    # 実際は配列で届くので、ループで回して処理してください
    command = payload['commands'][0]
    bad_request unless command['devices'][0]['id'] == '123'
    bad_request unless command['execution'][0]['command'] == 'action.devices.commands.OnOff'
    # おうち LAN 内で動いている赤外線学習リモコンに設定した URL を呼びます
    # ここでは点灯を /on、消灯を /off で実装したものとします
    if command['execution'][0]['params']['on']
      Net::HTTP.post_form(URI.parse('http://(赤外線学習リモコンのIPアドレス)/on'), {})
    else
      Net::HTTP.post_form(URI.parse('http://(赤外線学習リモコンのIPアドレス)/off'), {})
    end

    body = {
      requestId: request_id,
      payload: {
        commands: [{
          ids: ['123'],
          status: 'SUCCESS',
          states: {
            on: command['execution'][0]['params']['on'],
            online: true
          }
        }]
      }
    }
  when 'action.devices.QUERY'
    # 繰り返しになるため省略します
  else
    bad_request
  end

  body.to_json

rescue
  bad_request
end
end
```

## 赤外線による家電制御
今回製作したスマートホームアプリから任意の家電を制御する最後のツールとして、赤外線学習リモコンが必要になります。
とはいえ、大半の実装は前節で終わっています。後は action.devices.EXECUTE の呼び出しを解釈して、適宜赤外線リモコンの API を呼ぶだけです。

自由に操作できる API さえあればどんな製品を用いても結構ですが、今回は赤外線学習リモコンも自作してみました。製作についてはこちらをどうぞ。 https://qiita.com/td2sk/items/4c0ef83bcc7e74e5e8d5

## デモ
ライトの登録とスイッチオンを試してみました。わかりやすさのために、赤外線リモコンを省き、直接 LED の点灯を行っています。

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">LEDをGoogle Homeに登録して制御できた <a href="https://t.co/1nNOhOAcMb">pic.twitter.com/1nNOhOAcMb</a></p>&mdash; td2sk (@td2sk) <a href="https://twitter.com/td2sk/status/940976693593833472?ref_src=twsrc%5Etfw">December 13, 2017</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

「○○のアプリにつないで」等のフレーズなしに、簡単に操作できていますね。

## まとめ
* Google Assistant 対応のスマートホームアプリを実装しました
    * 「○○のテストアプリにつないで」などのフレーズなしに家電が操作できました
* 赤外線学習リモコンを実装しました
* OAuth2.0 認可サーバを実装しました (後日記事公開予定)

3分割の大変長い記事でしたが、最後まで読んでいただきありがとうございました。
来年も良いおうちハックを！


[^1]: テレビを付けるときは PS4 か Nintendo Switch かで入力切り替えがいるのでリモコン頼りです。不便 
[^2]: 言葉の揺れ対策として、1つのアクションに3つの文章を登録できます
[^3]: 一応暗黙の呼び出しもできるようですが
[^4]: 自分の思い付きで呼びかけて反応したもの一覧です。他にもあれば教えてください！
[^5]: 外出時に便利。「いってきます」に連動させるなど
[^6]: ここをみて、「あー分かったわー、後は実装するだけだわー」となった人は、この記事を読まなくても大丈夫です
[^7]: メインであるこの記事より長いです。詳しく読まずにソースコードだけ拾って使っていただいてかまいません