Amazon Echo dotで在室状況を把握する。

# 前書き
我が家には4台のAmazon Echo dotがあり、各部屋＋リビングに１台ずつ置いています。
少し前に、Amazon Echo dotが超音波で入室・退室時にアクションを起こせるようになったとニュースで見て、仕事中でもどの部屋に誰がいるのか確認出来たらと思い、実装しました。

ご指摘・ご質問あれば、コメントいただければと思います。

# 構成＆動作
以下に構成図と簡単な動作を示します。

![enterleave.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704447/12672768-2e56-9d34-14f3-7a0eb47d3808.png)

Amazon Echo dotに人の入室・退室を検知すると、どこどこの部屋に入室 or 退室しました。と発話されたとするアクションを取る。
クラウド上にあるAlexaサービスで音声処理されて、登録しているエンドポイント（今回はheroku)に通知が行く。
通知された内容に従って、Firestore Database上のステータス（入室/退室）を更新し、Firestore Database上にログとしても保存する。
LINEを使って、各部屋の在場状況および入室・退室の履歴を確認できる。

# 構築
環境の構築については、
記事[LINE BOT経由でRaspberry Piに接続したUSBカメラの画像を取得してみる](https://qiita.com/TVCAdev/items/fc6a00d24ffa08f53c41)を参照ください。

また、前回の記事[LINE BOT経由でパパの居場所（androidスマホ）を地図で教える（成功編）](https://qiita.com/TVCAdev/items/ec08b008ddc3d62e629b)でFirebaseのCloud Firestore（Cloud Messagingに使用するトークンの保存に使用）を使用しましたので、今回も在場状況のステータスおよび入退室のログ保存に使用したいと思います。

## Alexaの設定
まず、Alexaの設定を行います。

:::note warn
Amazon開発者アカウントが無い方は、
[Alexaスキル開発トレーニングシリーズ第1回: 初めてのスキル開発](https://developer.amazon.com/ja/blogs/alexa/post/31c9fd71-f34f-49fc-901f-d74f4f20e28d/alexatraining-firstskill)を参照して、Amazon開発者アカウントを作成ください。
:::

Alexa開発者コンソールから、スキルを作成します。
ポイントは、スキルのバックエンドリソースを「ユーザー定義のプロビジョニング」にすることです。（いつものherokuサーバーに飛ばします。)
![skill_create.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704447/37e0f386-66d0-4181-12e0-2fa1b0f9919f.png)


スキルが作成出来たら、エンドポイントの設定で自分のURL(heroku上)をセットします。
![endpoint.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704447/3fb13574-a490-0287-160e-ad455c1b9d9d.png)

:::note warn
以下で出てくるスロットやインテント等の概念については、
[Alexaスキル開発トレーニングシリーズ第2回: カスタムスキル](https://developer.amazon.com/ja/blogs/alexa/post/93bad9f6-377f-4ada-887c-279e7346f729/alexatraining-customskill)および[Alexaスキル開発トレーニングシリーズ第3回: スロットの利用](https://developer.amazon.com/ja/blogs/alexa/post/36665011-8980-471b-bcb0-d51e1d19f06a/alexatraining-customskill-slots)に詳しく記載されているので、必要に応じて参照ください。
:::

今回は、"どこどこ"で"入室" or "退室"と発話するので、"どこどこ"の部分と"入室" or "退室"の部分をスロットとして、ROOMとACTIONとして定義します。
![slot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704447/1b9a913e-cb0e-d247-6912-426cab629563.png)

ROOMはこんな感じで対象の部屋を設定します。
![slot_room.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704447/b81a62fd-209b-86c9-f7f4-82e6018761a8.png)

ACTIONはこんな感じで対象の部屋を設定します。
![slot_action.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704447/6691c93d-6bd8-9f0b-46f1-ae1e61e730e8.png)

最後にインテントを設定します。
設定したインテントAlexaEnterLeaveIntentだと、"リビングで入室"などの発話に対応可能となります。
![インテント.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704447/3f0eb114-57c2-9d24-3619-0ba08d848f29.png)

## エンドポイントの設定

エンドポイントに来た読み込みに対応するheroku側の設定になります。

```package.json
{
  "name": "tvca-line-bot",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@line/bot-sdk": "^7.4.0",
    "ask-sdk": "^2.12.1",
    "ask-sdk-express-adapter": "^2.12.1",
    "express": "^4.17.1",
    "firebase-admin": "^10.1.0",
    "socket.io": "^4.4.0"
  }
}
```
ASK(Alexa Skills Kit)のパッケージask-sdkおよびask-sdk-express-adapterが必要になります。
SDKの詳細は[ASK SDK for Node.js](http://ask-sdk-node-typedoc.s3-website-us-east-1.amazonaws.com/index.html)、[Alexa Skills Kit SDK](https://developer.amazon.com/ja-JP/docs/alexa/sdk/alexa-skills-kit-sdks.html)辺りを参照してください。

以下から、エンドポイント側のコードになります。

```heroku_app.js(1)
const Alexa = require('ask-sdk');
const { ExpressAdapter } = require('ask-sdk-express-adapter');
```
ASKの初期設定関連です。

```heroku_app.js(2)
     77 /*
     78  * Function for Alexa IntentRequest
     79  */
     80 const AlexaEnterLeaveIntentHandler = {
     81     canHandle(handlerInput) {
     82         return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
     83             && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AlexaEnterLeaveIntent';
     84     },
     85     handle(handlerInput) {
     86         console.log("AlexaEnterLeaveIntentHandler was called...");
     87
     88         const roomName = Alexa.getSlotValue(handlerInput.requestEnvelope, 'room');
     89         const actionName = Alexa.getSlotValue(handlerInput.requestEnvelope, 'action');
     90         console.log("room: " + roomName + " action: " + actionName);
     91
     92         // update room status.
     93         const inRoomRef = db.collection('state').doc('inroom');
     94
     95         let set_obj = {};
     96         if (actionName == '入室') {
     97             set_obj[roomName] = true;
     98         } else {
     99             set_obj[roomName] = false;
    100         }
    101         inRoomRef.update(set_obj)
    102             .then(doc => {
    103                 console.log('updating ' + roomName + ' to ' + actionName + ' was succeed.');
    104             })
    105             .catch((error) => {
    106                 console.log('updating ' + roomName + ' to ' + actionName + ' was failed.: %s', error);
    107             });
    108
    109         // save logs
    110         const LogsRef = db.collection('log').doc('Inroom').collection('Logs');
    111
    112         LogsRef.add({
    113             name: roomName,
    114             action: actionName,
    115             date: firebaseadmin.firestore.FieldValue.serverTimestamp()
    116         })
    117             .catch((error) => {
    118                 console.log('adding log was error.:', error);
    119             });
    120
    121         return handlerInput.responseBuilder
    122             .getResponse();
    123     }
    124 };
```
メインのハンドラー関数AlexaEnterLeaveIntentHandlerを定義しています。
81～84行目でIntentRequestで、かつインテント名がAlexaEnterLeaveIntentに対してのみ処理を行うことを宣言しています。
88行目で入力されたインテントからroomスロット部分("リビング"や"ママの部屋"の部分)、
89行目でactionスロット部分("入室"か"退室"の部分)を抜き出してます。

95行目から107行目で、Firebase Databaseのステータスをアップデートしています。
具体的には、stateコレクションのinroomドキュメントがあり、そこにフィールド名が部屋名("リビング"や"ママの部屋"等)でbooleanタイプの値を持っているので、対象の部屋名の値をアップデートしています。

109行目から119行目でログファイルを保存しています。
logコレクションのInroomドキュメント以下にLogsコレクションがあるので、入室か退室を検知するたびにログのドキュメントファイルを追加していきます。

後は特にユーザとのやり取りが不要なので、121,122行目で何もしないResponseを返します。

```heroku_app.js(3)
    126 // set alexa handlers
    127 const skillBuilder = Alexa.SkillBuilders // get SkillBuilder
    128     .custom() // get CustomSkillBuilder
    129     .withSkillId(process.env.ALEXA_ENTERLEAVE_SKILL_ID) // whether my skill or not
    130     .addRequestHandlers(
    131         AlexaEnterLeaveIntentHandler
    132     );
    133 const skill = skillBuilder.create();
    134 const express_adapter = new ExpressAdapter(skill, true, true);
```
AlexaEnterLeaveIntentHandlerをセットする処理です。
ほぼ定型ですが、129行目で念のためのSkillIdのチェックを入れています。
スキル一覧画面の「スキルIDをコピー」をクリックすることでコピーできます。
![SkillID.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704447/b7d3b018-1a17-e6a7-bc70-f7d8b802f64a.png)

```heroku_app.js(3)
    435 /*
    436  * function is called when alexa skill(checking presence in room) was called.
    437  */
    438 app.post("/" + process.env.ALEXA_INROOM_URL, express_adapter.getRequestHandlers(), (req, res) => {
    439     console.log("ALEXA_INROOM_URL called...");
    440
    441     console.log(req);
    442
    443 });
```
環境変数ALEXA_INROOM_URLは、エンドポイントに設定したURLになります。
express_adapter.getRequestHandlers()を呼び出すことにより、130～132行目で登録したAlexaEnterLeaveIntentHandlerが呼び出され、trueを返した場合のみ本関数が呼び出されます。

他の改修部分は、Firebaseへの保存・読み込み部分およびLINEとのインタフェース部分ですので、割愛します。
前回の記事[Raspberry piとTVをHDMIで繋げてTVを制御する。](https://qiita.com/TVCAdev/items/f3fcddc78289b31bae03)を参照ください。

[ソースコード](https://github.com/TVCAdev/tvca-line-bot/releases/tag/AlexaEnterLeaveAdded)を参照ください。

## Alexa側の設定
最後にAmazon Echo dotに設定を行います。
Amazon Alexaアプリを立ち上げて、「その他」－「定型アクション」を開きます。
右上の＋マークを押し、「実行条件を設定」－「スマートホーム」に進み、デバイス一覧より対象のAmazon Echo Dotを選択します。
実行条件から、「人が検知された時」を選択し、「アクションを追加」－「カスタム」で
「在室確認でリビングで入室」を入力します。
「在室確認」はスキル名称、「リビング」はスロットROOMで指定した部屋名称の中からデバイス一覧で選択したAmazon Echo dotが置かれている部屋を入力してください。実行条件が「人が検知された時」なので「入室」としてください。実行条件が「人がいないと判定された時」の場合は、「退室」としてください。
上記設定後、Amazon Echo dotが「人が検知された時」および「人がいないと判定された時」にAlexaに対してフレーズを発行してくれて、該当するスキルが呼ばれ、firebaseの在室のステータスが更新されます。

:::note warn
もし、あなたが私と同様にAmazon Echo dotを使用しているのであれば、過渡な期待は禁物です。
部屋から出ても、すぐには「人がいないと判定された時」にはなりません。
ここら辺は条件の設定が一切できないので、あくまで参考ぐらいで使用してください。
:::

# 総括
jetson nanoが定価で欲しいので、在庫ありになったときに通知をくれる機能を開発したいです。
Raspberry pi 3Bで頑張っていますが、重たくなってきました。（Visual Studio codeでRaspberry Pi 3Bに接続して開発しているのですが、nodeが大量に立ち上がってそれが重たい。)

メモリはもちろん1GBで足りないので、仮想メモリを大量に使ってます。SSDでなければ、実用に耐えれないでしょう・・・

この記事が少しでも、他の方の役に立てば幸いです。