RaspberryPiでスマートホーム 〜AlexaでMQTT〜
2019/09/20 |
|
2018/05/26 |
|
MQTT(AWS IoT)使ってラズパイでJSONデータを受信、USB赤外線リモートアドバンスで照明の操作ができるようになりました。
Javascriptを使ってブラウザからMQTTで送信して、ラズパイに指令することもできるようになりました。
今度はAlexaを使って指令してみます。私はAmazon Echo Dotを使います。
AlexaのスキルをAmazon Developer Consoleで、実際の処理(MQTTで送信)の部分をAWS Lambdaで作成していきます。
Alexaスキルの作成
Amazon Developer Consoleでのスキルの作成方法は、以下の記事を参考にしてください。
ここではポイントのみ記載していきます。
モデルを決める
スキルを作るにあたって、呼び出し名、発話サンプルを決めます。
操作したいのは照明です。オン・オフとリラックスモード、常夜灯を実装したいと思います。
- アレクサ、ラズパイで照明をつけて
- アレクサ、ラズパイで照明を消して
- アレクサ、ラズパイで照明をリラックスモードにして
- アレクサ、ラズパイで照明を常夜灯にして
で、これを実装しようとすると、まず考えるのはスロットを対象機器(”照明”)とコマンド(”つけて”、”消して”)にすることです。つまり発話サンプルはこんな感じです。
”{TARGET} を {COMMAND}”
TARGET部分が”照明”、COMMAND部分が”つけて”か”消して”に差し込まれるわけです。
問題発生
が、しかし…
結論から先に言うと、これだと問題がありました。単語がスマートホームスキルとかぶるようで、自分のスキルではなくスマートホームが呼ばれてうまくいきませんでした。
あれやこれや苦闘してようやく見つけた解決法はスロットを使わないことでした。
つまり”しょうめいをつけて”、”しょうめいをけして”、とベタに記述してやるとうまくいきました。どうもスロット使うと駄目みたいです。
ちょっとださくなりますが、しょうがないのでベタに行きます。
スキルの作成
Amazon Developer Consoleでモデルを定義します。
スキル名は”AlexaHome”にしました。
呼び出し名は”ラズパイ”。
次にインテントを作成していきます。作ったインテントは4つ。
照明をつけるインテントの発話サンプル。
照明を消す。
リラックスモード。
常夜灯。
それぞれ似たような文章を登録しておくと、それらの文章でも照明が操作できるようになります。
インテントを作成したら保存して、ビルドします。
AWS Lambda関数の作成
AWSでの処理の作成方法は以下の記事を参考にしてください。Lambda関数を作成します。実際のコードは後述します。
ロールの作成
上記の記事では、Lambda関数作成時に自動的に必要な権限が割り当てられたロールが作成されました。今回はAWS IoTを使うのでその権限が必要なのと、自動的に作られた奴は名前が長ったらしいので、手動で作成しました。
IAMの画面でロールを作成します。
”AWS サービス”、”Labmda”を選択して、”次のステップ:アクセス権限”を押します。
検索窓に”iot”と入力して検索、一覧の中から”AWSIoTDataAccess”にチェックを入れます。
次にログを保存できる権限をつけます。検索窓に”logs”と入力して検索、一覧の中から”AmazonAPIGatewayPushToCrowdLogs”にチェックを入れて、”次のステップ:タグ”ボタンを押します。
タグは不要なので”次のステップ:確認”で次へ進みます。
ロール名を入力します。”alexaHome”にしました。”ロールの作成”ボタンを押して、完成です。
作成したLambda関数に戻り、画面中ほどにあるロールを今作成したものに変えます。そして保存すると…
IoTが追加されたはずです。
ソース
以下のソースを、AWSに貼り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
/* eslint-disable func-names */ /* eslint-disable no-console */ const Alexa = require('ask-sdk'); const Aws = require('aws-sdk'); function submit(item) { var speechOutput = ""; var senddata = { topic: 'homeapp/to', payload: JSON.stringify({ "resource": item.resource, "command": item.command, "parameters": item.params, }), qos: 1 }; var iotdata = new Aws.IotData({endpoint: ENDPOINT}); return new Promise((resolve, reject) => { iotdata.publish(senddata, function(err, data) { if(err) { console.log("Failure: ",err); speechOutput = FAILURE_MESSAGE; reject(speechOutput); } else { console.log("Success"); speechOutput = item.speech; resolve(speechOutput); } }); }); } const IntentHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; var ret = false; if (request.type === 'IntentRequest'){ if (request.intent.name in data) { ret = true; } } return ret; }, async handle(handlerInput) { var item = data[handlerInput.requestEnvelope.request.intent.name]; var speechOutput = await submit(item); return handlerInput.responseBuilder .speak(speechOutput) .withSimpleCard(SKILL_NAME, speechOutput) .getResponse(); }, }; const HelpHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return request.type === 'LaunchRequest' || (request.type === 'IntentRequest' && request.intent.name === 'AMAZON.HelpIntent'); }, handle(handlerInput) { return handlerInput.responseBuilder .speak(HELP_MESSAGE) .reprompt(HELP_REPROMPT) .getResponse(); }, }; const ExitHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return request.type === 'IntentRequest' && (request.intent.name === 'AMAZON.CancelIntent' || request.intent.name === 'AMAZON.StopIntent'); }, handle(handlerInput) { return handlerInput.responseBuilder .speak(STOP_MESSAGE) .getResponse(); }, }; const SessionEndedRequestHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; return request.type === 'SessionEndedRequest'; }, handle(handlerInput) { console.log(`Session ended with reason: ${handlerInput.requestEnvelope.request.reason}`); return handlerInput.responseBuilder.getResponse(); }, }; const ErrorHandler = { canHandle() { return true; }, handle(handlerInput, error) { console.log(`Error handled: ${error.message}`); return handlerInput.responseBuilder .speak('エラーが発生しました。') .getResponse(); }, }; const SKILL_NAME = 'AlexaHome'; const GET_FACT_MESSAGE = 'Here\'s your fact: '; const HELP_MESSAGE = '家電を操作します。対象の機器とコマンドを言ってください。'; const HELP_REPROMPT = '対象の機器とコマンドを言ってください。'; const STOP_MESSAGE = 'さようなら'; const FAILURE_MESSAGE = '失敗しました!'; const ENDPOINT = "(エンドポイント)"; const data = { 'LIGHT_ON': { 'resource': 'light', 'command': 'power', 'params': 'on', 'speech': '照明をつけました。' }, 'LIGHT_OFF': { 'resource': 'light', 'command': 'power', 'params': 'off', 'speech': '照明を消しました。' }, 'LIGHT_RELAX': { 'resource': 'light', 'command': 'mode', 'params': 'relax', 'speech': '照明をリラックスモードにしました。' }, 'LIGHT_SLEEP': { 'resource': 'light', 'command': 'mode', 'params': 'sleep', 'speech': '照明を常夜灯にしました。' }, }; const skillBuilder = Alexa.SkillBuilders.standard(); exports.handler = skillBuilder .addRequestHandlers( IntentHandler, HelpHandler, ExitHandler, SessionEndedRequestHandler ) .addErrorHandlers(ErrorHandler) .lambda(); |
ポイントはMQTTでデータを送信するsubmit()関数。これは非同期です。ask-sdkのバージョン1ではAlexaへの返事はemit()関数を呼び出すだけでよかったので、submit()関数にコールバックを登録して、そのコールバックからemit()関数を呼んでました。
今回使用したバージョン2では、ハンドラーからreturnしてやらないといけなくなりました。えっ、ということはハンドラーの中では非同期処理ができない?もちろんそんなことはありませんでした。ハンドラーはPromiseを返してもOKだそうです。なのでasync、awaitで解決。ちょっとはまりました…
今回はインテントが複数あるので、インテントに応じたコマンド等をあらかじめ用意しておいたdata配列から取得します。
あとは、ほぼサンプルプログラムを流用しています。
ラズパイのエンドポイントを確認
”(エンドポイント)”を実際のエンドポイントに置き換えておきます。エンドポイントはラズパイをAWSに登録したときのものです。
IoT Coreのモノのページで、登録したラズパイを選択。
”操作”をクリック、表示されたHTTPSがエンドポイントになります。
関数を作成したら、Amazon Developer Consoleのエンドポイントにこの関数のARNを貼り付けましょう。
テスト
Amazon Developper Consoleでテスト
ではやってみます。まずはAmazon Developer Consoleのテスト用ツールを使います。
テキストを貼りつけて、エンターキーで送信です。画面はhandler関数で、うまく戻り値を返してないときの結果です。非同期は面倒くさいよ…
実機でテスト
続いて実機です。
うまくいかないときは…
テストは以下の記事を参考にしてみてください。
チェックポイントとしては…
スキルのモデル定義(Amazon Developer Console)
- ビルドは成功したか
- インテント名は間違ってないか
- 発話サンプルは間違ってないか
- エンドポイントのARNは間違ってないか
発話サンプルは入力した後に、右側の”+”を押し忘れてたことがよくありました。
Alexaシミュレータ(Amazon Developer Console)
- 結果が正常に返ってくるか
ここで動けばまあ大丈夫でしょうが、そこへたどり着くまでが大変…ここで出力されるJSONはLambda関数のテストで重宝します。
Lambda関数(AWS)
- ”一から作成”ではなく”Serverless Application Repositoryの参照”から作成したか→今回のやり方の場合は、こうしないと必要なライブラリがデプロイされない
- 文法エラーはでてないか
- エンドポイントは間違ってないか
- スキル作成時に設定したインテント名とあっているか
- AlexaシミュレーターのJSONデータを使ってテストする
- CloudWatchのログを見る
Alexaシミュレーターで吐き出されるJSONをLambda関数のテストデータとして使えるのが便利です。何度も使いました。
あとは、ソース中でConsole.log()すると、CloudWatchで見れるのでそれも役に立ちます。
AWS IoT
- データがAWS IoTまで来ているか
- 正しいJSONフォーマットで来ているか
AWS IoTの画面でトピックをサブスクライブして、監視します。
ラズパイ
- データがラズパイまで来ているか
- 正しいJSONフォーマットで来ているか
- 照明を操作するコマンドは、単体でちゃんと動作しているか
テスト中は受信プログラムをバックグラウンドではなく、フォアグラウンドで動かしておけばprint()文の内容を画面で確認できます。
python homeapp.py
Alexaアプリ(スマホ)
- 自分の言葉が正しく認識されているか
Alexaaアプリの履歴で話した言葉がどう認識されたか見れるので、確認しましょう。
感想
MQTTは便利
MQTTは便利です。同じ仕組みで他の機器からも簡単に操作できます。AWS IoTボタンもいけそうですね。でもボタンを押すくらいなら、そもそもリモコン使えって話だけど…
追記
同じことをスマートホームスキルでやってみました。こちらは”ラズパイで”とつけなくていいので便利です。
- アイリスオーヤマ(IRIS OHYAMA)