Alexa のカスタムスキルから Firebase のデータベースに書き込む

# はじめに

　本記事は、前回の[Amazon Echo(Alexa) + Raspberry Pi を使ってスマートホームシステムを作ろう](https://qiita.com/saeki4n/items/523fcab21a610837403b) の続きになります。

>　音声で部屋と台所の電気をON、OFFできるようになりましたが、出掛ける時などは「Alexa いってきます」といった一つの命令で複数のデバイスを操作できるようにしたいです。
　現状だと「アレクサ、部屋の電気を消して」→Alexa「はい、分かりました」（部屋の電気が消える）、「アレクサ、台所の電気を消して」といったように、順番に命令をしなければなりませんが、そんなの面倒です。手でやった方が早いです。
　そこでAlexaのカスタムスキルを構築して、一つの命令でまとめてデバイスを操作することを実現したいと思います。

　そんなわけで、Alexaカスタムスキルを利用してFirebaseのRealtime Databaseに書き込むことを実現したいと思います。そこで書き込んだ命令をMasterラズパイに解釈させることで部屋ラズパイと台所ラズパイをまとめて操作するできるようになるからです。

- Amazon Echo カスタムスキルで Firebase Realtime Databaseに命令を書き込み → MasterラズパイがFirebase Realtime Databaseを受諾 → 前回のように Firebase Realtime Databaseに命令を書き込み
- 部屋用と台所用ラズパイはFirebase Realtime Databaseに命令が書き込まれたら実行

# 前回から引き続き使用するもの
- Realtime Databaseに読み書きした Pythonコード
- firebase-adminsdk.json：firebaseの認証情報
- Realtime DatabaseのURL
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/8a7b49b3-fbfe-e55a-f863-e2bb82103b7f.png)


# Alexaカスタムスキルについて

　公式サイトのドキュメントはこちら。
https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/understanding-custom-skills.html
　クラウドサービスは進化が早いので、本記事公開半年以上経過していたら一度は見ておいた方がいいと思います。

　音声インターフェースをどうデザインするかといったドキュメントもあってとっても面白かったです。
https://developer.amazon.com/ja/designing-for-voice/

　とはいえ、今回は単純に命令するだけで、対話するレベルまでは作らないので参考程度です。

# 仕様について
　やりたいことはとりあえずこんなところでしょうか。
-　「アレクサ、おやすみなさい」で3分後に部屋と台所のライトをオフにする
-　「アレクサ、おはよう」で即時に部屋と台所のライトをオンにする

　カスタムスキルが行なうことはあくまでFirebaseのRealtime Databaseに書き込むだけです。あとの実際の命令実行はMasterラズパイが行なうようにします。

**2023年7月追記：**
仕様が変更されたようで、「おやすみなさい」や「おはよう」といった既存のAlexa機能が起動してしまう可能性のある呼び出し名では全く起動しなくなりました。
呼び出し名を例えば「おやすみなさいモード」として、実際に呼び出す際は「おやすみなさいモードをスタートして」というようにする必要があります。

[カスタムスキルの呼び出し名を決定する](https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/choose-the-invocation-name-for-a-custom-skill.html)

# カスタムスキルの構築手順

1. 開発コンソールにアクセスします。ログインはAlexaが登録されているAmazonアカウントで行ないます
https://developer.amazon.com/alexa/console/ask

2. ログインしたら「スキルの作成」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/3e42b709-d716-e939-204f-94e6882343cb.png)

3. スキルに名前を付けて、日本語を選択して、「次へ」ボタンをクリックします。
スキルの名前は「睡眠モード」にして、次に行きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/7e83f1c5-7f61-6e77-4272-0aef5536a525.png)

4. エクスペリエンスのタイプは「その他」でモデルは「カスタム」を選択。ホスティングサービスは今回はPythonでプログラムを作っていて、それを流用したいのでPythonを選択します。ホスト地域は近いところを選択しますが、どこも日本から遠いのでデフォルトの米国東部で良いでしょう。次に行きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/35be62fd-22a2-4ca8-6117-8192975fdb3f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/83174daa-0146-05c4-7817-d4fb75bbaa85.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/181aeb0b-a955-b47f-5d84-9b9afe146711.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/8eb473ba-895c-0366-e35d-bf113551b155.png)

5. テンプレートは「スクラッチ」を選択して、次に行きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/3417337d-9b74-7a48-1767-a12d33eda4f5.png)

6. 構成確認画面になるので、問題ないことを確認して「Create Skill」ボタンで作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/ea66d463-99ac-b717-f63c-2f372614528f.png)

7. 作成完了するまでしばらく待ちます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/c0ce0913-4f0c-747f-2135-ea4bf9b5018d.png)

8. 作成が完了するとこのような画面になるので、作ったスキル「睡眠モード」であることを確認して、「1．呼び出し名」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/ed5768bb-991c-f4f7-11cf-0601d1406833.png)

9. ~~「おやすみなさい」~~「おやすみなさいモード」を呼び出し名にして、モデルを保存します。**2023年7月変更**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/530d4a05-2e0d-2bae-55ca-d990f8efd476.png)
**2023年7月追記：**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/ef50bfc7-9d6a-d9f9-ef70-efaf959f0d49.png)


10. 正常に保存されたことを確認して「モデルをビルド」をクリックして、ビルドが終わるまで待機します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/e522b914-66b7-3535-f263-c938ae57dc8e.png)
ビルドが終わったらBuild Completedといったメッセージが出ます。ビルドが終わらないうちにテストをしても当然ながら音声を認識してくれないので重要です。（筆者はここで一日くらい躓きました……）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/04a45283-d2a3-3fac-70b3-217c1370dd20.png)

11. FirebaseのRealtime Databaseに書き込めるようにコードエディタで色々と書き換えていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/6b9ebb57-4a16-5b62-3194-80ab58aff39a.png)

　書き換えポイントは以下の3つです。
- requirements.txt に firebase_adminを追加
- firebase-adminsdk.json ファイルの追加
- lambda_function.py の変更

11.1. AlexaでもFirebaseが使えるように requirements.txt に firebase_admin を追加して保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/1e20e808-a5fd-a315-4bad-534ba5590bd6.png)
11.2. firebase-adminsdk.jsonのファイルの追加
　New Fileを選択して、firebase-adminsdk.jsonの作成し、認証情報ファイルの中身をコピーして保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/8c1b3fb8-8db0-eacc-05ed-4c698f55cf4f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/2c8c3223-7fa3-9363-9b3e-8b72237940be.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/53f8784d-92f7-c48d-7f66-854deefdc9e5.png)
11.3. lambda_function.py を変更します。ここは基本的に前回ラズパイで構築したコードを追加する形になります。
　Firebase Realtime Databaseの認証コードを追加します。今回は時間も扱うので `from datetime import datetime`も追加します。
```python:lambda_function.pyに追加するコード
from datetime import datetime
import firebase_admin
from firebase_admin import credentials, db

cred_filepath = "firebase-adminsdk.json"
cred = credentials.Certificate(cred_filepath)

database_url = "https://smarthomesystem-2c112-default-rtdb.firebaseio.com"
default_app = firebase_admin.initialize_app(cred, {"databaseURL": database_url})
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/c67fe01f-6c0f-9300-4057-bb6b0d6a95ca.png)

　`class LaunchRequestHandler(AbstractRequestHandler)` の `def handle(self, handler_input)`を下記のように変更します。Alexaからどんな命令(value)が、いつされたのか(time)、そして命令の処理状況(status)を保存したいので、このようになっています。またAlexaが命令を受け取ったことを表すために「おやみなさい。睡眠モードになります」と返事をしてくれるよう設定します。
```python:lambda_function.py
    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        orders_ref = db.reference("/orders")
        alexa_ref = orders_ref.child("Alexa")
        value_ref = alexa_ref.child("value")
        time_ref = alexa_ref.child("time")
        status_ref = alexa_ref.child("status")
        
        value_ref.set("sleep")
        time_ref.set(datetime.now().isoformat())
        status_ref.set("running")
        
        speak_output = "おやすみなさい。睡眠モードになります。"

        return (
            handler_input.response_builder
                .speak(speak_output)
                .response
        )
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/13d022bf-3983-d3bf-b2ad-0e18bf8a14a3.png)
12. 全ての書き換えが完了したらデプロイを行ないます。デプロイが終了したらテストに移ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/1e488136-1d85-54bd-6f82-c8bb24cc5a5a.png)

# カスタムスキルのテスト
1. ここを開発中に変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/07c20939-4616-3831-a075-fa411badd29a.png)

2. ~~「おやすみなさい」~~「おやすみなさいモードをスタートして」と入力してエンターを押したら、設定したように「おやすみなさい。睡眠モードになります。」と返事が返ってくれば想定通りです。**2023年7月変更**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/d8307877-dea2-911e-7010-bc246a318e58.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/337f9ef2-ed35-a875-3e84-9d13eef582af.png)
**2023年7月追記：**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/f22e3c6b-ea4f-c515-3c8d-c489c5584c1a.png)

3. Realtime Databaseの方を確認して、無事に書き込まれていたら成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/e8d21c5f-7731-62a2-fd46-13714129d20d.png)

# ラズパイ側のコードを追加する
　Realtime Databaseを常時監視し、変更があれば命令に合わせて実行するPythonコードを作成します。
　今回は「おやすみ」というAlexaからの命令があった3分後に部屋と台所のライトがOFFが実行されるようにします。また実行した後のStatusを finished にして再実行されないようにします。
　※ ここでは分かりやすく statusを finishedするようにしていますが、これだと命令データをループの度にダウンロードすることになってデータ量を無駄に使うので全消しした方がいいかもしれません。
```python:alexa2order.py
import logging
import os
import subprocess
from datetime import datetime, timedelta

import firebase_admin
from firebase_admin import credentials, db

main_folder = "/home/user/vscode"
log_Level = logging.INFO

current_path = os.path.dirname(os.path.abspath(__file__))

# ログの設定
sth = logging.StreamHandler()
flh = logging.FileHandler(f"{main_folder}/alexa2order.log")
logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    level=log_Level,
    handlers=[sth, flh],
)
logger = logging.getLogger(__name__)

cred_filepath = f"{current_path}/firebase-adminsdk.json"
cred = credentials.Certificate(cred_filepath)

database_url = "https://smarthomesystem-2c112-default-rtdb.firebaseio.com"
default_app = firebase_admin.initialize_app(cred, {"databaseURL": database_url})

orders_ref = db.reference("/orders")
alexa_ref = orders_ref.child("Alexa")
value_ref = alexa_ref.child("value")
time_ref = alexa_ref.child("time")
status_ref = alexa_ref.child("status")

pre_order_time = datetime.now().isoformat()
DIFF_JST_FROM_UTC = 9 # Alexaカスタムスキルでの時間はUTCで記録されるためコードでJSTに変換する用
try:
    while True:
        alex_order: str = value_ref.get()
        order_time = datetime.fromisoformat(time_ref.get()) + timedelta(
            hours=DIFF_JST_FROM_UTC
        )

        if alex_order == "sleep":
            if status_ref.get() == "running":
                if order_time + timedelta(minutes=3) < datetime.now(): # 3分以上経過したら命令実行
                    logger.info("お休みモードになりました。")
                    subprocess.Popen(
                        [f"{main_folder}/order.sh Room LED OFF"], shell=True
                    )
                    subprocess.Popen(
                        [f"{main_folder}/order.sh Kitchen LED OFF"], shell=True
                    )
                    status_ref.set("finished")
except KeyboardInterrupt:  # CTRL+C
    logger.debug("stop")
```
```sh:order.sh
#!/bin/sh
cd `dirname $0`
python3 set2firebase.py $1 $2 $3
```
前回の`set2firebase.py` はLED固定でしたが、拡張性を考慮して変数化してみました。
```python:set2firebase.py
import os
import sys
import firebase_admin
from firebase_admin import credentials, db

current_path = os.path.dirname(os.path.abspath(__file__))
place = str(sys.argv[1])
item = str(sys.argv[2])  # 修正ポイント
value = str(sys.argv[3]) # 修正ポイント

cred = credentials.Certificate(f"{current_path}/firebase-adminsdk-key.json")
default_app = firebase_admin.initialize_app(
    cred, {"databaseURL": "https://smarthomesystem-2c112-default-rtdb.firebaseio.com"}
)
orders_ref = db.reference("/orders")
item_ref = orders_ref.child(place).child(item) # 修正ポイント
item_ref.set(value) # 修正ポイント
```
　ここでは省略しますが、alexa2order.pyもラズパイを再起動しても自動起動するようにサービス化しておくのが良いでしょう。やり方は前回の「[Pythonコードの自動起動](https://qiita.com/saeki4n/items/523fcab21a610837403b#python%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E8%87%AA%E5%8B%95%E8%B5%B7%E5%8B%95)」を参考にしてください。

# おはようのAlexaカスタムスキルを作る

　前項のと同様にAlexaかステムスキルを作成し、呼び出し名を ~~「おはよう」~~「おはようモード」にします。**2023年7月変更**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/d0f8915d-289d-bd3b-f72f-902d5e3472fe.png)
**2023年7月追記：**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/d0a983e6-5d9b-f677-141b-e43e56804c77.png)

　起動は即時反映したいので、Masterラズパイを経由せず、部屋ラズパイと台所のラズパイに直接命令を送るようにFirebaseのRealtime Databaseの値を書き換えます。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/e9bf6b39-7208-b502-f7f1-4bce1a182099.png)

　テストで無事に認識して、データベースが書き換わったら成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/5214a742-61bd-11db-8670-85efffd1ed8a.png)
**2023年7月追記：**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/850bc243-1a5b-ed6c-8b5e-49f0ce4da4b1.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430322/f25056ed-30e3-f2e8-9fa5-976140dd6e9f.png)

# まとめ
　~~「おやすみなさい」や「おはよう」といった一言で~~  特定の呼び出し名で、FirebaseのRealtime Databaseに任意の値を書き込むことができる Alexaのカスタムスキルを構築することができました。**2023年7月変更**
　今回は単に命令するのみでしたが、対話的なAlexaのカスタムスキルが作れると面白いですね。

# やり方が分からないし、そもそも出来るかどうか分からないこと
　「おやすみなさい」や「おはよう」といった挨拶は、デフォルトでもAlexaは反応してくれます。
　今回それをカスタムスキルで上書きした形になっていますので、そこからデフォルト機能に繋げれば自然な挨拶をAlexaが返してくれるようになるといいなぁと思いましたが、ささっと検索したぐらいでは分かりませんでした。まだAlexaカスタムスキルのドキュメントをしっかり読めていないので、そこから仕様を見ていきたいですね。