Alexa センサー用のスマートホームスキルを作成し、定型アクションのトリガーとして利用する


# はじめに

Alexaスキルを音声以外の方法で起動させたかったのが発端。

2019年10月末、ようやく定型アクションでスキルが起動できるようになった。さらに[Echo Flex（※今は取扱い停止。）](https://amzn.to/3vZ6oOE) も発売され、モーションセンサー（別売）を使うことで定型アクションのトリガーとして使えるようになった。であればスマートホームスキルのセンサー用のスキルを作れば定型アクションのトリガーとして使えるのではないか？と考えセンサー用のスマートホームスキルを作ろうと思ったのがキッカケ。

※[第四世代のEcho Dot](https://amzn.to/3t189ZQ) も人感センサーに似た、[在室感知](https://amzn.to/3t51nlP)という機能が追加されていて、こちらも定型アクションのトリガーとして使用可能となっている。反応はセンサーの方が良いように思う。

![全体図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/75176961-b0c3-5977-b6f2-c268d67ea716.png)


# 前提
- カスタムスキルを作ったことがある
- Alexaスキルモデル：スマートホーム（センサー用）
- OAuthプロバイダ：Login with Amazon（LWA）

# スキル作成の流れ

1. [スマートホームスキルを作成](#スマートホームスキルを作成)
2. [LWAでセキュリティプロファイルを作成](#LWAでセキュリティプロファイルを作成)
3. [Lambda関数を作成](#Lambda関数を作成)
4. [Lambda関数のテスト](#Lambda関数のテスト)
5. [スマートホームスキルの詳細設定](#スマートホームスキルの詳細設定)
6. [スマートホームスキルのテスト](#スマートホームスキルのテスト)

# スマートホームスキルを作成

Alexa Developer Consoleで、センサー用のスマートホームスキルを新規作成する。

#### 1. [Alexa Developer Console](https://developer.amazon.com/alexa/console/ask)にログインする

#### 2. スキルを作成する

`スキルの作成`ボタンをクリックし、必要な情報を入力後`スキル作成`ボタンをクリックする。
<font color="OrangeRed">センサー用</font>スマートホームスキルは、デフォルトの言語は<font color="OrangeRed">英語（米国）</font>を選択する。

|No.|項目|値|
|:-:|:--|:--|
|1|スキル名|任意（制限事項はマニュアル参照）|
|2|デフォルトの言語|<font color="OrangeRed">英語（米国）</font>|
|3|スキルに追加するモデルを選択|スマートホーム|

![新しいスキルを作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/5ef452b4-9278-5097-4d6a-2c19966f6095.png)

※種類が豊富！

#### 3. スキルの情報を入力する

`ペイロードのバージョン`で`v3（推奨）`を選択したら、いったん`保存`ボタンをクリックしてスキルを保存しておく。
LWAのセキュリティプロファイルやLambda関数を作成した後に、スマートホームスキルのエンドポイントとアカウントリンクを設定する。

Lambda関数作成時にスキルIDが必要になるので、`スキルID`をメモ帳などに保管しておく。
※Alexa Developer Console開けば確認できますが・・・

|No.|項目|値|
|:-:|:--|:--|
|1|ペイロードのバージョン|v3（推奨）|

![ビルド](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/fa2bbbb7-76e2-83ac-c664-63ef4f6bf54b.png)

# LWAでセキュリティプロファイルを作成

アカウントリンクに必要なクライアントIDとクライアントシークレットを作成する。

#### 1. [Amazon開発者ポータル](https://developer.amazon.com/dashboard)にログインする

画面のデザインが変わってる。2020年4月時点のAmazon開発者ポータル画面。

![Amazon開発者ポータル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/dc12171f-09da-9071-f403-617366d71804.png)


#### 2. セキュリティプロファイルを作成する

上部のメニューから`Login with Amazon`をクリックし`セキュリティプロファイルを新規作成`ボタンをクリックする。
必要な情報を入力後、`保存`ボタンをクリックする。

|No.|項目|値|
|:-:|:--|:--|
|1|セキュリティプロファイル名|任意|
|2|セキュリティプロファイルの説明|任意|
|3|プライバシー規約同意書URL|任意　※空欄可   |
|4|同意のロゴ画像|任意　※選択しなくても良い|

![セキュリティプロファイル管理](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/70220a9d-9db5-2a09-f977-e8e33f14cda0.png)

#### 3. クライアントIDとクライアントシークレットの確認

対象セキュリティプロファイルの`クライアントIDおよびクライアントシークレットを表示`リンクをクリックすると、クライアントIDおよびクライアントシークレットが表示されるので、これをメモ帳などに保管しておく。
※後で確認することでもできる。
※IDが表示されているが、このセキュリティプロファイルは削除済み。

![クライアントIDおよびクライアントシークレットを表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/291d04af-54cf-facd-3871-f15d67b5ed29.png)



# Lambda関数を作成

Lambda関数を新規作成する。

#### 1. [AWSコンソール](https://signin.aws.amazon.com/signin?redirect_uri=https%3A%2F%2Fconsole.aws.amazon.com%2Fconsole%2Fhome%3Fstate%3DhashArgs%2523%26isauthcode%3Dtrue&client_id=arn%3Aaws%3Aiam%3A%3A015428540659%3Auser%2Fhomepage&forceMobileApp=0)にログインする

#### 2. Lambdaを選択する

スマートホームスキルはLambdaのリージョンが指定されているので注意。
センサー用のスマートホームスキルは"スキルの言語に<font color="OrangeRed">英語（米国）</font>を設定する"と記載されているが、Lambdaのリージョンは<font color="OrangeRed">米国西部（オレゴン）</font>を選択しないと動作しない。最初、スキルの言語が英語（米国）なので、Lambdaは米国東部（バージニア北部）で作成したけど動かなかった。

|No.|スキルの言語| Lambdaリージョン |
|:-:|:--|:--|
|1|<font color="OrangeRed">英語（米国）</font>または英語（カナダ）|米国東部（バージニア北部）|
|2|英語（英国）、英語（インド）、ドイツ語、フランス語（フランス）|EU（アイルランド）|
|3|日本語および英語（オーストラリア）|<font color="OrangeRed">米国西部（オレゴン）</font>|

#### 3. Lambda関数を作成する

`関数の作成`ボタンをクリックし必要な情報を選択および入力した後、画面右下の`関数の作成`ボタンをクリックする。
ロールに関しては、マニュアルにはいろいろ書いてあるけど、今回はサンプルなので"基本的なLambda アクセス権限で新しいロールを作成"で作成することにした。詳細はマニュアル参照。

|No.|項目|値|
|:-:|:--|:--|
|1|オプション|一から作成|
|2|名前|Lambda関数の名前を入力します|
|3|ランタイム|Node.js 18.x|
|4|ロール|基本的な Lambda アクセス権限で新しいロールを作成|

![関数の作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/0215a0cc-b108-04a4-fe61-b11cf4405b81.png)


#### 4. トリガーを追加する

設定タブの`＋トリガーを追加`ボタンをクリックし、トリガーの設定の一覧の中から`Alexa Smart Home`を選択する。

![トリガーを追加](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/c364e61c-178e-8abc-72eb-65094c0ae4f7.png)


#### 5. トリガーを設定する

`トリガーの設定`セクションで、`アプリケーションID`欄に、先ほど保管した`スキルID`を追加する。 
`トリガーの有効化`はオンのままにしておく。
`追加`ボタンをクリックした後、`保存`ボタンをクリックする。

![トリガーの設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/92a4ea0c-27f5-7d0c-8f20-5cc1bd47c056.png)

#### 6. コードを作成する

```JavaScript:マニュアル記載のサンプルをセンサー用に修正したもの
exports.handler = function (request, context) {
    if (request.directive.header.namespace === 'Alexa.Discovery' && request.directive.header.name === 'Discover') {
        log("DEBUG:", "Alexa.Discovery:",  JSON.stringify(request));
        handleDiscovery(request, context, "");
    }
    else if (request.directive.header.namespace === 'Alexa') {
        log("DEBUG:", "Alexa:", JSON.stringify(request));
        if (request.directive.header.name === 'ReportState' ) {
            log("DEBUG:", "Alexa:", JSON.stringify(request));
            handleMotionSensor(request, context);
        }
    }
    else if (request.directive.header.namespace === 'Alexa.MotionSensor') {
        log("DEBUG:", "Alexa.MotionSensor:", JSON.stringify(request));
        if (request.directive.header.name === 'detectionState' ) {
            log("DEBUG:", "Alexa.MotionSensor:", JSON.stringify(request));
            handleMotionSensor(request, context);
        }
    }
    else if (request.directive.header.namespace === 'Alexa.EndpointHealth') {
        log("DEBUG:", "Alexa.EndpointHealth:", JSON.stringify(request));
        handleMotionSensor(request, context);
    } 
    else if (request.directive.header.name === 'AcceptGrant') {
        log("DEBUG:", "AcceptGrant:", JSON.stringify(request));    // AcceptGrantの内容をlogに出力
        var response = {
            "event": {
              "header": {
                "namespace": "Alexa.Authorization",
                "name": "AcceptGrant.Response",
                "messageId": request.directive.header.messageId,
                "payloadVersion": "3"
              },
              "payload": {}
           }
        };
        context.succeed(response);
    } 
    else {
      log("DEBUG:", "Other request:", JSON.stringify(request));
      log("DEBUG:", "Other context:", JSON.stringify(context));
    }
    
    function handleDiscovery(request, context) {
        var payload = {
            "endpoints":
            [
              {
                "endpointId": "sensor-001",
                "manufacturerName": "TAKARA MOTION SENSOR",
                "friendlyName": "TAKARAのセンサー",
                "description": "TAKARAのセンサー（スマートホームスキル）",
                "displayCategories": ["MOTION_SENSOR"],
                "cookie": {
                  "key1": "このエンドポイントのキーと値のペアです",
                  "key2": "複数のエントリーがある場合があります",
                  "key3": "参照目的で使用します",
                  "key4": "現在のエンドポイントの状態を維持するためには使わないでください"
                },
                "capabilities": [
                  {
                    "type": "AlexaInterface",
                    "interface": "Alexa",
                    "version": "3"
                  },
                  {
                    "type": "AlexaInterface",
                    "interface": "Alexa.MotionSensor",
                    "version": "3",
                    "properties": {
                      "supported": [
                        {
                          "name": "detectionState"
                        }
                      ],
                      "proactivelyReported": true,
                      "retrievable": true
                    }
                  },
                  {
                    "type": "AlexaInterface",
                    "interface": "Alexa.EndpointHealth",
                    "version": "3",
                    "properties": {
                      "supported": [
                        {
                          "name": "connectivity"
                        }
                      ],
                      "proactivelyReported": true,
                      "retrievable": true
                    }
                  }
                ]
              }
            ]
        };
        var header = request.directive.header;
        header.name = "Discover.Response";
        log("DEBUG", "Discovery Response:", JSON.stringify({ header: header, payload: payload }));
        context.succeed({ event: { header: header, payload: payload } });
    }

    function log(message, message1, message2) {
        console.log(message + message1 + message2);
    }

    function handleMotionSensor(request, context) {
        // 検出中に渡されたデバイスIDを取得します
        log("DEBUG", "handleMotionSensor: ", JSON.stringify({ request }));
        var requestMethod = request.directive.header.name;
        var responseHeader = request.directive.header;
        responseHeader.namespace = "Alexa";
        responseHeader.name = "Response";
        responseHeader.messageId = responseHeader.messageId + "-R";
        // リクエスト中のユーザートークンパスを取得します
        var requestToken = request.directive.endpoint.scope.token;
        var SHResult;

        // デバイス制御クラウドを呼び出して制御します 
        SHResult = "DETECTED";
        var contextResult = {
            "properties": [
                            {
                              "namespace": "Alexa.MotionSensor",
                              "name": "detectionState",
                              "value": SHResult,
                              "timeOfSample": "2017-02-03T16:20:50.52Z",
                              "uncertaintyInMilliseconds": 0
                            }
                          ]
        };
        var response = {
            context: contextResult,
            event: {
                header: responseHeader,
                endpoint: {
                    scope: {
                        type: "BearerToken",
                        token: requestToken
                    },
                    endpointId: "sensor-001"
                },
                payload: {}
            }
        };
        log("DEBUG", "Alexa.MotionSensor ", JSON.stringify(response));
        context.succeed(response);
    }
};
```

#### 7. LambdaのARNをコピーする

作成したLambda関数のARNをメモ帳などに保管しておく。

# Lambda関数のテスト

作成したLambda関数をテストする。

#### 1. テストを作成する

画面右上の方にある`テスト`ボタンをクリックする。

![Lambdaのテスト](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/85526662-4a5e-7680-5223-0b490b5868d9.png)

<font color="OrangeRed">※現在は`テスト`の位置が変わっていて、テストタブになっています。</font>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/f099483a-94da-5830-1b01-8b053e937ab3.png)


#### 2. テストイベントを作成する

イベントテンプレートは、デフォルトのHello Worldのままにしておく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/046d04bb-324b-75df-6c06-c404b6d48de0.png)


```json:Discoveryディレクティブ
{
    "directive": {
        "header": {
            "namespace": "Alexa.Discovery",
            "name": "Discover",
            "payloadVersion": "3",
            "messageId": "1bd5d003-31b9-476f-ad03-71d471922820"
        },
        "payload": {
            "scope": {
                "type": "BearerToken",
                "token": "access-token-from-skill"
            }
        }
    }
}
```

イベント名に`Discovery`と入力し、エディターのコンテンツをすべて以下のコードで置き換え`変更を保存`ボタンをクリックする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/346d5182-272b-0941-5323-265d159a84e2.png)


#### 3. テストを実行する

`テスト`ボタンをクリックする。成功すると以下のような結果が表示される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/770db793-45de-a2c9-b135-192d543d92fe.png)


詳細をクリックし、以下のような実行結果が表示されれば成功。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/845185c9-ae35-af19-2ad2-dc027c1f3138.png)


```json:レスポンス例
Response:
{
  "event": {
    "header": {
      "namespace": "Alexa.Discovery",
      "name": "Discover.Response",
      "payloadVersion": "3",
      "messageId": "1bd5d003-31b9-476f-ad03-71d471922820"
    },
    "payload": {
      "endpoints": [
        {
          "endpointId": "sensor-001",
          "manufacturerName": "TAKARA MOTION SENSOR",
          "friendlyName": "TAKARAのセンサー",
          "description": "TAKARAのセンサー（スマートホームスキル）",
          "displayCategories": [
            "MOTION_SENSOR"
          ],
          "cookie": {
            "key1": "このエンドポイントのキーと値のペアです",
            "key2": "複数のエントリーがある場合があります",
            "key3": "参照目的で使用します",
            "key4": "現在のエンドポイントの状態を維持するためには使わないでください"
          },
          "capabilities": [
            {
              "type": "AlexaInterface",
              "interface": "Alexa",
              "version": "3"
            },
            {
              "type": "AlexaInterface",
              "interface": "Alexa.MotionSensor",
              "version": "3",
              "properties": {
                "supported": [
                  {
                    "name": "detectionState"
                  }
                ],
                "proactivelyReported": true,
                "retrievable": true
              }
            },
            {
              "type": "AlexaInterface",
              "interface": "Alexa.EndpointHealth",
              "version": "3",
              "properties": {
                "supported": [
                  {
                    "name": "connectivity"
                  }
                ],
                "proactivelyReported": true,
                "retrievable": true
              }
            }
          ]
        }
      ]
    }
  }
}
```


# スマートホームスキルの詳細設定

スマートホームスキルにエンドポイントと、アカウントリンクを設定する。

#### 1. [Alexa Developer Console](https://developer.amazon.com/alexa/console/ask)に戻る

#### 2. エンドポイントを設定する

スマートホームサービスのエンドポイントのデフォルトのエンドポイントボックスに、作成したLambda関数のARNを入力する
`保存`ボタンをクリックする。

![エンドポイント](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/741e5af9-16a0-8308-46ff-48be114b93ff.png)


#### 3. アカウントリンクを設定する

左ペインから`アカウントリンク`をクリックし、必要事項を入力した後`保存`ボタンをクリックする。

`認証画面のURI`は、[認可リクエスト](https://developer.amazon.com/ja/docs/login-with-amazon/authorization-code-grant.html#authorization-request)を参照。
`アクセストークンのURI`は、[アクセストークンリクエスト](https://developer.amazon.com/ja/docs/login-with-amazon/authorization-code-grant.html#access-token-request)を参照。

|No.|項目|値|
|:-:|:--|:--|
|1|認証画面のURI|https://www.amazon.com/ap/oa|
|2|アクセストークンのURI|https://api.amazon.com/auth/o2/token|
|3|ユーザーのクライアントID|※セキュリティプロファイルの`クライアントID`|
|4|ユーザーのシークレット|※セキュリティプロファイルの`クライアントシークレット`|
|5|ユーザーの認可スキーム|HTTP Basic認証 （推奨）|
|6|スコープ|profile|
|7|ドメインリスト||
|8|デフォルトのアクセストークンの有効期限||
|9|Alexaのリダイレクト先のURL|https://pitangui.amazon.com/api/skill/link/MA67IWRRJKXIU<br>https://layla.amazon.com/api/skill/link/MA67IWRRJKXIU<br>https://alexa.amazon.co.jp/api/skill/link/MA67IWRRJKXIU|

![アカウントリンク](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/fb702a33-5f54-6f31-84fb-0746d6e87e06.png)


# スマートホームスキルのテスト

スキルの詳細設定が完了し、Lambda関数のテストが完了したら、スマートホームスキルをテストする。

#### 1. [Alexaアプリ](https://alexa.amazon.co.jp/spa/index.html)にログインする

#### 2. 対象のスマートホームスキルを有効にする

`有効にする`ボタンをクリックしてスキルを有効にすると、スキルをデバイス制御クラウドにアカウントリンクするページにリダイレクトされる。

<img width="30%" alt="スキル有効化" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/703c95cc-3018-1410-090d-306f6f7ed69e.png">

#### 3. アカウントリンク

`Eメールアドレス`と`Amazonのパスワード`を入力し、`ログイン`ボタンをクリックする。
"スマートホームが正常にリンクされました。"と表示されれば成功。

<img width="30%" alt="アカウントリンク" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/9b924698-4994-78e0-0cb8-4109b94245a7.png">　<img width="30%" alt="アカウントリンク成功" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/302147d5-7ea4-556a-119d-6c381750af4f.png">


#### 4. スマートホームスキルを検出する

Alexaアプリで`端末を検出`し、作成したスマートホームスキルがデバイスの一覧に表示されれば成功。

<img width="30%" alt="すべてのデバイス" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/70ff6802-685a-90d0-dbc9-03b642749c86.png">

#### 5. 動作テスト

Alexaアプリで`定型アクション`を作成する。
実行条件の設定で`スマートホーム`を選択し、デバイス一覧で作成したスマートホームスキルを選択し、実行条件を`検出`に設定する。
※センサー用のスマートホームスキルにしないとデバイス一覧には表示されない。


<img width="30%" alt="定型アクションの実行条件を設定" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/fbd03f71-73c6-dea2-593e-ccd3385c57b1.png">　<img width="30%" alt="定型アクションのデバイスを選択" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/6a46d2f3-0113-f33f-e240-856210e86050.png">　<img width="30%" alt="実行条件は検出にする" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/d49abaaa-7091-0f7b-5a5c-2dc33af28c69.png">

外部からイベントゲートウェイに[ChangeReportを送信](https://qiita.com/takara1121/items/e2bc22f0f51068c40d25)して、定型アクションが動作すれば成功。
今回はブラウザから`ChangeReport`を送信した。

[![デモ](http://img.youtube.com/vi/KBA4IWf-Q2Y/0.jpg)](http://www.youtube.com/watch?v=KBA4IWf-Q2Y)

記載している内容は、2020年4月5日時点での情報。変更されている可能性があるので注意。


# おわりに

実は、センサーデバイスがなくても`ChangeReport`ディレクティブを送信してあげればスマートホームスキルは動いてくれる。つまり、イベントゲートウェイに`ChangeReport`ディレクティブを送信する仕組みは、デバイスでもアプリでもプログラムでもブラウザでも何でも良い。顔認証、距離、温度、湿度や天気など、いろいろな変化をトリガーにしてAlexaを起動することができるようになる。


# 参考
- [スマートホームスキルの作成手順（※Amazon公式サイト）](https://developer.amazon.com/ja-JP/docs/alexa/smarthome/steps-to-build-a-smart-home-skill.html)
- [センサー用のスマートホームスキルを作成する（※Amazon公式サイト）](https://developer.amazon.com/ja-JP/docs/alexa/smarthome/build-smart-home-skills-for-sensors.html)

# 関連
- [Alexa スマートホームスキル イベントゲートウェイにChangeReportを送信する](https://qiita.com/takara1121/items/e2bc22f0f51068c40d25)
- [M5Stick-VとM5Stick-Cを繋ぎ、Alexaスマートホームスキルとして使用できる顔認識デバイスを作ってみた](https://qiita.com/takara1121/items/8698847a073af3ea3c7c)