Alexa スマートホームスキル イベントゲートウェイにChangeReportを送信する

# はじめに
Alexa センサー用のスマートホームスキルで`ChangeReport`をAlexaの`イベントゲートウェイ`に送信する際に、ユーザーをAlexaで認証する必要がある。その認証で必要なトークンの取得方法からイベントゲートウェイへの送信方法の覚書。

# 前提
- Alexaスキル　モデル：スマートホーム（センサー用）
- OAuthプロバイダー：LWA（Login with Amazon）

# 流れ
1. [LWAからトークンを取得](#lwaからトークンを取得)
2. [ChangeReportを作成](#changereportを作成)
3. [イベントゲートウェイに送信](#イベントゲートウェイに送信)

センサー用スマートホームスキルのエンドポイントのプロパティ値を変更した場合は必ずプロアクティブに`ChangeReport`イベントを送信する必要がある。

# LWAからトークンを取得
トークンの有効期限は取得後60分。60分を超え、無効になったトークンを使用してリクエストを送信すると、`401 Unauthorized`が返却され認証されない。そのため、有効期限が切れた場合は、前回取得したトークンに含まれる`refresh_token`を使用してトークンを再発行する必要がある。

### 新規リクエスト（初回）
一番最初にトークンを取得する場合のリクエスト。
<font color="OrangeRed"> `AcceptGrant`</font>ディレクティブの`code`フィールドの値を使用して、LWAにアクセストークンリクエストを送信し、アクセストークンを取得する。

```http:トークン新規発行リクエスト
POST /auth/o2/token HTTP/l.l
Host: api.amazon.com
Content-Type: application/x-www-form-urlencoded;Accept-Charset=UTF-8
 
grant_type=authorization_code&code=SplxlOBezQQYbYS6WxSbIA&client_id=【AlexaクライアントID:】&client_secret=【Alexaクライアントシークレット:】
```

### パラメータ

|No.|項目|値|備考|
|:-:|:--|:--|:--|
|1|grant_type|authorization_code|固定値|
|2|code|SRgccpEPdzDayxqHFsma|AcceptGrantから取得|
|3|client_id|【AlexaクライアントID:】|alexa developer console（※）から取得|
|4|client_secret|【Alexaクライアントシークレット:】|alexa developer console（※）から取得|

※alexa developer console -> 対象スキル -> ビルド -> アクセス権限 -> Alexaスキルメッセージング

### レスポンス
```json:200&nbsp;OK
{
  "access_token":"Atza_IwEBI...Fq2w8uYFfE",
  "refresh_token":"Atzr_IwEBI...f6ohOBg3ig",
  "token_type": "bearer",
  "expires_in": 3600
}
```


### ※code値の取得方法

`code値`は`AcceptGrant`レスポンスから取得する。
Alexaアプリを開き、対象のスマートホームスキルを有効にして、スキルとアカウントをリンクする。アカウントリンクが成功すると、`AcceptGrant`がスキルに送信されるので、CloudWatchログに`AcceptGrant`を出力させて、`directive.payload.grant.code`を取得する。Lambda関数を作成して`AcceptGrant`を [取得する方法はこちらに記載](https://qiita.com/takara1121/items/712d0db50be903c43c73)。

※もう一度`AcceptGrant`を送信したい場合は、Alexaアプリで対象スキルを`スキルを無効にする`にして、再有効化する。有効化時にアカウントリンクを設定すると`AcceptGrant`が送信される。

```json:AcceptGrant
{
  "directive": {
    "header": {
      "namespace": "Alexa.Authorization",
      "name": "AcceptGrant",
      "messageId": "873c203b-833c-46a5-8ed6-9c7d2444df35",
      "payloadVersion": "3"
    },
    "payload": {
      "grant": {
        "type": "OAuth2.AuthorizationCode",
        "code": "SRgccpEPdzDayxqHFsma"        ←  これ
      },
      "grantee": {
        "type": "BearerToken",
        "token": "Atza_IwEBI...z71DaTIJnI"
      }
    }
  }
}
```



### curlでaccess_tokenを取得してみる

Windowsのコマンドプロンプトで以下を実行する。

```:curl
c:\> curl -i -X POST -H "Content-Type: application/x-www-form-urlencoded; charset=utf-8" -d "grant_type=authorization_code&code=<directive.payload.grant.code>&client_id=<AlexaクライアントID>&client_secret=<Alexaクライアントシークレット>" https://api.amazon.com/auth/o2/token
```

こんな感じで結果が返ってくる。

```
HTTP/1.1 200 OK
Server: Server
Date: Wed, 12 Jan 2022 08:59:55 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 656
Connection: keep-alive
x-amz-rid: H3JRCKNG4SD4H96ZZ41V
x-amzn-RequestId: d22f0700-c8a4-43e1-adbe-664b59ec80ba
X-Amz-Date: Wed, 12 Jan 2022 08:59:55 GMT
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Vary: Content-Type,Accept-Encoding,X-Amzn-CDN-Cache,X-Amzn-AX-Treatment,User-Agent
Permissions-Policy: interest-cohort=()

{"access_token":"Atza|IwEBIDtWO-tfB5kMY5Evs-iDOIKVysDoxdqdak8F15dwAdZgNCtC-a8TXsr8Y3Pzjn1TT6riulDI8KseIPgqZoRe8AHkqGVLhO2tcj3_OL-RCV2ZEk9KkKukBo7YNmOHqZhbs1z-VBqNzBaeVlH8q2liVA8FYy8E8Jir6jmvVv3QX6g4NvOYxB9TYDeRlBEg_XvfECbn5cjbXEc7hhgdffDhJnLVLLfii_DnhVr7kKInuaXO_jEZ2ZIiVEHVqA5LwSPbZeXL8KbAnkP_Jf33Rd83K_eR","refresh_token":"Atzr|IwEBILi1ETOk-rWAqVRVI-ClT3Rf212dSv5QM3ohHQBqMz4X4D80Iojh8BaSc-gOsppXeDNAHvxuWTIP_l80JbaLZ0SwicDLYqe2av_NNO5pIpE_hzDccntZ_FUD-ZGi4Xz5TrZAnaVz4CIonATb8ei9fG01lqC5vh3UGkxrgGpkXLILZT_F6ZujMU7qoSPQGSCPwohrZAr9vo4J9xaXCnas_kOBvRmES4Ji59JaRcIKVvRLpU94h_3Thm7Jn5WURh_gFFFb6wxgJwPhnmnsxFKMYSmb","token_type":"bearer","expires_in":3600}
```


### 更新リクエスト（２回目以降）
２回目以降のトークン取得。URLは変わってないが、リクエストのパラメータが変わっているので注意。`code`ではなく` refresh_token `を使用する。

```http:トークン更新リクエスト
POST /auth/o2/token HTTP/l.l
Host: api.amazon.com
Content-Type: application/x-www-form-urlencoded;Accept-Charset=UTF-8
 
grant_type=refresh_token&refresh_token=Atzr_IwEBI...f6ohOBg3ig&client_id=【AlexaクライアントID:】&client_secret=【Alexaクライアントシークレット:】
```

### パラメータ

|No.|項目|値|備考|
|:-:|:--|:--|:--|
|1|grant_type|<font color="OrangeRed">refresh_token</font>|固定値|
|2|<font color="OrangeRed">refresh_token</font>|Atzr_IwEBI...f6ohOBg3ig||
|3|client_id|【AlexaクライアントID:】|alexa developer console（※）から取得|
|4|client_secret|【Alexaクライアントシークレット:】|alexa developer console（※）から取得|

※alexa developer console -> 対象スキル -> ビルド -> アクセス権限 -> Alexaスキルメッセージング

### レスポンス
```json:200&nbsp;OK
{
  "access_token":"Atza_IwEBI...MMbdgEWYWI",
  "refresh_token":"Atzr_IwEBI...f6ohOBg3ig",
  "token_type": "bearer",
  "expires_in": 3600
}
```

### curlでrefresh_tokenを取得してみる

Windowsのコマンドプロンプトで以下を実行する。

```:curl
c:\> curl -i -X POST -H "Content-Type: application/x-www-form-urlencoded; charset=utf-8" -d "grant_type=refresh_token&refresh_token=<１回目で取得したrefresh_token>&client_id=<AlexaクライアントID>&client_secret=<Alexaクライアントシークレット>" https://api.amazon.com/auth/o2/token
```

こんな感じで結果が返ってくる。

```
HTTP/1.1 200 OK
Server: Server
Date: Wed, 12 Jan 2022 08:42:45 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 678
Connection: keep-alive
x-amz-rid: 1ME24VEDQSZAW9ZF13D1
x-amzn-RequestId: fb439232-7e44-466c-886b-bd55ca9ddace
X-Amz-Date: Wed, 12 Jan 2022 08:42:45 GMT
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Vary: Content-Type,Accept-Encoding,X-Amzn-CDN-Cache,X-Amzn-AX-Treatment,User-Agent
Permissions-Policy: interest-cohort=()

{"access_token":"Atza|IwEBIF1cVmtHDvsBx8tnufdj97glJDvZyTBDM1ZMLXWQHSN1nlJSFJ-YTDomMV6vIGqHa7_lQ1nzHvKsg_PLFjVRq21_d_KFP_E8jb48kz_MctvPZNOsHcJCEAo7swbG0l5pyEUaCpKyDDU3Qlh8kxns_9PMj_ehTqQbhxLC0__RmJzRZuxkgt6w9z-qa7y77V5gdZxf1VIwkbwEqGY9wal4kpI5i4THtNawLQzmtlFhVNkP5RtOvSfNsspMHDG5huF7NvlBYc9XtRMEU8GgKwLWMfmILU18-tN6s50vC1yzbnOUQA","refresh_token":"Atzr|IwEBICo1PeLUZFMDzi_al8nCxH5hQYDky_6nI9Tmmvl-NJ4u7xO0-fkMr0rxSGJPOmVTm8-0AVMDmglzCE0ebnt8oUgo1BBXTY2gy5U1inxHx-o8-oO5UzWXIfHCbprXPncLQdF_ROeMHfOpeCNm31vooVjzpNFoNMCB3Ww64ROFSQlM1duUWqb2OLZRFgE2G_OuYNczaPoeDhHCoT9ee8UwvHfaWXn-Iccw2rfmhBPd9m_uVzu_R8dxJ-4rO8bzSvhOge6IyTGfMFRWwMMPm5bGBNVb","token_type":"bearer","expires_in":3600}
```


# ChangeReportを作成
`event.endpoint.scope.toke`に上記で取得した `access_token` を設定する。
`endpointId`などに、作成したスマートホームスキルの情報を設定する。

```json:ChangeReport
{
  "context": {},
  "event": {
    "header": {
      "messageId": "tkr-123-def-456",
      "namespace": "Alexa",
      "name": "ChangeReport",
      "payloadVersion": "3"
    },
    "endpoint": {
      "scope": {
        "type":"BearerToken",
        "token":"Atza_IwEBI...MMbdgEWYWI"  ←  access_token
       },
       "endpointId" :  "sensor-001" 
    },
    "payload": {
      "change": {
        "cause": {
          "type": "PHYSICAL_INTERACTION"
        },
        "properties": [
          {
            "namespace": "Alexa.MotionSensor",
            "name": "detectionState",
            "value": "DETECTED",
            "timeOfSample": "2020-03-22T16:20:50.52Z",
            "uncertaintyInMilliseconds": 0
          }
        ]
      }
    }
  }
}
```

# イベントゲートウェイに送信
スマートホームスキルの言語を<font color="OrangeRed">英語</font>で作成したとしても、<font color="OrangeRed">日本のAmazonアカウント</font>を使用している場合は、エンドポイントに<font color="OrangeRed">極東</font>を使用する。

### エンドポイント
- 北米：https://api.amazonalexa.com/v3/events
- ヨーロッパ：https://api.eu.amazonalexa.com/v3/events
- 極東：https://api.fe.amazonalexa.com/v3/events

### リクエスト
HTTPヘッダー`Authorization`の`Bearer`の後に半角スペースを空け、上記で取得した `access_token` を設定する。

```http:イベント送信リクエスト
POST /v3/events HTTP/1.1
Host: api.fe.amazonalexa.com
Authorization: Bearer Atza_IwEBI...MMbdgEWYWI
Content-Type: application/json

{
  "context": {},
  "event": {
    "header": {
      "messageId": "tkr-123-def-456",
      "namespace": "Alexa",
      "name": "ChangeReport",
      "payloadVersion": "3"
    },
    "endpoint": {
      "scope": {
        "type":"BearerToken",
        "token":"Atza_IwEBI...MMbdgEWYWI"
       },
       "endpointId" :  "sensor-001" 
    },
    "payload": {
      "change": {
        "cause": {
          "type": "PHYSICAL_INTERACTION"
        },
        "properties": [
          {
            "namespace": "Alexa.MotionSensor",
            "name": "detectionState",
            "value": "DETECTED",
            "timeOfSample": "2020-03-22T16:20:50.52Z",
            "uncertaintyInMilliseconds": 0
          }
        ]
      }
    }
  }
}
```

### レスポンス（成功）
`202 Accepted`が返却されれば成功。

```json:202&nbsp;Accepted

※何も返却されない。

```

### レスポンス（失敗）
トークンの有効期限が切れている場合は、`401 Unauthorized`が返却されるので、トークンを更新した後、再リクエストする。他のエラーの場合はマニュアルを参照し対応する。

```json:401&nbsp;Unauthorized
{
  "header": {
    "namespace": "System",
    "name": "Exception",
    "messageId": "fddd317c-2acf-4cba-a88e-fc0433385be8"
  },
  "payload": {
    "code": "INVALID_ACCESS_TOKEN_EXCEPTION",
    "description": "Access token is not valid."
  }
}
```

# おわりに

定型アクションで、スマートホーム（センサー）をトリガーにして、Alexaがアクションするところまで確認した。
今回は、センサーデバイスからではなくブラウザを使用して、ベントゲートウェイへ`ChengeReport`リクエストを送信した。

<img width="50%" alt="定型アクション" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426739/37e0ac2d-9c89-27ef-e6c0-4c6e08d21f1c.jpeg">

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

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


# 参考
- [スマートホームスキルの作成手順（※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へのユーザー認証を実現する（※Amazon公式サイト）](https://developer.amazon.com/ja-JP/docs/alexa/smarthome/authenticate-a-customer-permissions.html)

# 関連
- [Alexa センサー用のスマートホームスキルを作成し、定型アクションのトリガーとして利用する](https://qiita.com/takara1121/items/712d0db50be903c43c73)
- [M5Stick-VとM5Stick-Cを繋ぎ、Alexaスマートホームスキルとして使用できる顔認識デバイスを作ってみた](https://qiita.com/takara1121/items/8698847a073af3ea3c7c)