RaspberryPiでスマートホーム 〜Alexaのスマートホームスキルを開発〜
前回Alexaのカスタムスキルを作成して、音声で照明のオンオフができるようになりました。
ただ、カスタムスキルだと必ず呼び出し名を言わないといけません。”ラズパイ”と、たった4語ですが、面倒です。
Alexaのスマートホームスキルなら普通に”アレクサ、電気つけて”で電気をつけられます。そっちの方が便利なのでスマートホームスキルを作ることにしました。今回もAWS IoTを使ってMQTTでラズパイにメッセージを送ります。
スマートホームスキルの処理の流れですが、以下のような感じです。
(参考:スマートホームスキルAPIについて)
AlexaからLambdaが呼び出され、さらにそこから”Smart Device Manufacture Cloud(スマートホームスキル対応機器を作ったメーカーのサービス)”が呼び出されて、機器が操作されます。返事は非同期で”Smart Device Manufacture Cloud”からAlexaに返すこともできるようです。
今回の場合、スマートホーム対応機器はラズパイということになります。”Smart Device Manufacture Cloud”はなし、というかAWS IoTがそれになるのかな?
Echo Dot → Alexa → Lambda → AWS IoT → ラズパイ → 照明と流れていきます。
前提条件
スマートホームスキルを作るには、いくつか前提条件があります。
Lambdaが必須
スマートホームスキルはLambdaを使うことが前提です。一旦Lambda関数で受け取った後は、別のサイトで処理することも可能です。
Lambdaのリージョンはオレゴン
日本語の場合、Lambdaのリージョンはオレゴンでないといけません。東京だと駄目。いずれは東京でもよくなるとは思いますが、現状はオレゴン。
で、残念なことにオレゴンのLambdaから東京のAWS IoTには通信できないようです。となると、以前ラズパイを東京で登録したので、再度オレゴンで登録しなおす必要があります。
ラズパイをオレゴンで登録しなおしたら、以前作ったJavascriptもエンドポイントを変える必要があります。
前回のAlexaカスタムスキル用のLambdaもオレゴンに移さないと動かないのですが…
こちらは今回スマートホームスキルが完成したら、不要になるのでほっときます。
OAuth2プロバイダが必要
上記の説明図の通り、スマートホームスキルではスキルを開発する人(会社)が提供するサービスと接続されることを前提に考えられています。
そのためスキルのアカウントとそのサービス上でのアカウントを安全につなぐためにAccount Linkingという仕組みを使います。Account Linkingではこちら側サービスのOAuth2プロバイダが必須になるんですが…当然そんなもの持ってません。
これを個人が自分で用意するのは大変なので、今回はAmazonをOAuth2プロバイダとして利用します。
Amazon Developer Console
まずはAmazon Developer Consoleで”スマートホーム”を選択して、作成していきます。
名前は”AlexaSmartHome”にしました。言語を”日本語”にしておきます。
スマートホームの画面が表示されます。LambdaのARNがないと先に進めないので、こちらは一旦ここで離れます。離れる前に画面中央にあるスキルIDをコピーしておきます。Lambda側で必要になります。
AWS Lambda
Lambda関数を作成します。設計図から”home”で検索し、でてきた”alexa-smart-home-skill-adapter”を選択して、”設定”を押します。
先ほど書いた通り、忘れずにリージョンを”オレゴン”にしておきます。私はこれではまりました…
基本情報を入力します。関数名は”alexaSmartHome”、ロールは前回作った”roleAlexaHome”を選択、”Alexa Smart Home トリガー”の”アプリケーションID”に先ほどコピーしたスキルIDを貼り付け、”トリガーの有効化”にチェックを入れます。
入力したら、画面下の”関数の作成”ボタンを押します。
関数が作成されます。”Alexa Smart Home”と”AWS IoT”がセットされていることを確認します。
そして画面下部のコードの部分に以下のソースを上書きします。ソースは以下のサイトを参考にしました。
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 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
const AWS = require('aws-sdk'); const ENDPOINT = "(エンドポイント)"; exports.handler = function (request, context) { switch (request.directive.header.namespace) { case "Alexa.Discovery": if (request.directive.header.name === 'Discover') { handleDiscovery(request, context, ""); } break; case "Alexa.PowerController": handlePowerControl(request, context); break; } function handleDiscovery(request, context) { var payload = { "endpoints": [ { "endpointId": "LIGHT-001", "manufacturerName": "my-scribble.net", "friendlyName": "照明", "description": "部屋の照明", "displayCategories": ["LIGHT"], "cookie": {}, "capabilities": [ { "type": "AlexaInterface", "interface": "Alexa.PowerController", "version": "3", "properties": { "supported": [{ "name": "powerState" }], "retrievable": true } } ] }, { "endpointId": "LIGHT-001", "manufacturerName": "my-scribble.net", "friendlyName": "電気", "description": "部屋の照明", "displayCategories": ["LIGHT"], "cookie": {}, "capabilities": [ { "type": "AlexaInterface", "interface": "Alexa.PowerController", "version": "3", "properties": { "supported": [{ "name": "powerState" }], "retrievable": true } } ] }, ] }; var header = request.directive.header; header.name = "Discover.Response"; context.succeed({ event: { header: header, payload: payload } }); } function handlePowerControl(request, context) { var requestMethod = request.directive.header.name; var endPoint = request.directive.endpoint.endpointId; var resource = ""; var command = ""; var option = ""; var responseNamespace = ""; var responseName = ""; var responseResult = ""; switch (endPoint) { case "LIGHT-001": resource = "light"; break; } switch (requestMethod) { case "TurnOn": command = "power"; option = "on"; responseNamespace = "Alexa.PowerController"; responseName = "powerState"; responseResult = "ON"; break; case "TurnOff": command = "power"; option = "off"; responseNamespace = "Alexa.PowerController"; responseName = "powerState"; responseResult = "OFF"; break; } if (resource && command && option) { submit(resource, command, option, function(){ sendResponse(responseNamespace, responseName, responseResult); }); } } function sendResponse(namespace, name, value, payload) { var contextResult = { "properties": [{ "namespace": namespace, "name": name, "value": value, "timeOfSample": "2017-09-03T16:20:50.52Z", //retrieve from result. "uncertaintyInMilliseconds": 50 }] }; var responseHeader = request.directive.header; responseHeader.namespace = "Alexa"; responseHeader.name = "Response"; responseHeader.messageId = responseHeader.messageId + "-R"; var response = { context: contextResult, event: { header: responseHeader }, payload: payload }; context.succeed(response); } function submit(resource, command, params, callback) { var data = { topic: 'homeapp/to', payload: JSON.stringify({ "resource": resource, "command": command, "parameters": params, }), qos: 0 }; var iotdata = new AWS.IotData({endpoint: ENDPOINT}); iotdata.publish(data, function(err, data){ if(err){ console.log("submit(): failure - " + data); } else{ console.log("submit(): success"); } if (callback) { callback(); } }); } }; |
(エンドポイント)はラズパイのエンドポイントを指定します。
まずはhandler()関数が呼ばれます。
ディレクティブから操作の種類(request.directive.header.namespace)に応じて、呼び出す関数を振り分けます。現状はhandleDiscovery()とhandlerPowerControl()のみです。
”デバイスを探して”と呼び出されると、handlerDiscovery()が呼び出され、機器の一覧を返します。”endpointId”は機器のIDで、”friendlyName”がユーザーが呼ぶ名称です。今回”電気”、”照明”で同じ機器を制御したかったので、2つに同じIDを振ってますが、このやり方が正しいのかは、分かりません。
以下が参考になります。
”電気をつけて”と呼び出されると、handlerPowerControl()が呼び出されます。request.directive.endpoint.endpointIdからendpointIdを取り出して対象の機器を取得、また操作内容request.directive.header.nameに応じて、オンかオフかを判断します。機器と操作に応じた引数でsubmit()関数を呼び出し、AWS IoTにpublish()します。
コードを貼り付けたら画面右上の”保存”を押し、ARNをコピーしてAmazon Developer Consoleに戻ります。
そして、”デフォルトのエンドポイント”にコピーしたARNを貼り付け、”保存”ボタンを押します。
さらに”アカウントリンクを設定”ボタンを押して、次に進みます。
ここでまた一旦こちらを離れます。今度はOAuth2関係の設定をAmazon Developer Consoleの別の画面で行います。
Login With Amazon
Amazon Developer Consoleのトップに行きます。”アプリ&サービス”をクリックします。
”Amazonでログイン”をクリック。
”Create a New Security Profile”ボタンを押します。
出てきた画面で必要な情報を入力します。
”Security Profile Name”には”AlexaSmartHome”、”Security Profile Description”も同じく、”Consent Privacy Notice URL”には適当なアドレス、このブログ配下の存在しないアドレス”https://my-scribble.net/privacy.html”を入れてます。
入力したら”Save”ボタンを押します。
”Show Client ID and Client Secret”をクリック。
表示された”Client ID”と”Client Secret”を…
アレクサスキルの画面に戻って、”クライアントID”と”クライアントシークレット”に貼り付けます。
さらに
- 認証画面のURI…”https://www.amazon.com/ap/oa?redirect_url=”に続けて、画面下の”リダイレクト先のURL”の一番上のアドレスを入力
- アクセストークンのURI…”https//api.amazon.com/auth/o2/token”
- クライアントの認可方法…”HTTP Basic認証(推奨)”
- スコープ…”profile”
と入力します。
先ほどの画面に戻り、右の設定ボタンを押し、”Web Settings”を選びます。
右下の”Edit”ボタンを押し、”Allowed Return URLs”に先ほどのアレクサスキル画面の”リダイレクト先のURL”の一番上のアドレスをコピーして、貼り付けます。
これで準備完了です。スマホでスマートホームスキルを有効にします。
アレクサアプリ
スマホでアレクサアプリを開き、左上のメニューボタンを押して”スマートホーム”を選択。
”有効なスマートホームスキル”を選択。
すると自作のスマートホームスキルが表示されるはずですので、それを選択。
”有効にする”を押します。
アカウントのリンクが始まります。今回はAmazonでログインを使ったのでアマゾンのログイン画面が出てきます。IDとパスワードを入力してログインします。
”許可”します。
リンクが完了です。左上の×を押して、閉じます。
”端末の検出”ボタンを押して、検出を開始します。
しばらく待つと、端末が認識されるはずです。今回の場合、端末はソースで指定した”照明”と”電気”です。
ソースで設定した内容が、こういう風に認識されています。
テスト
テストはカスタムスキル同様、Amazon Developer Console、Lambda、実機で確認します。以下を参考にしてみてください。
ただ、Amazon Developer Consoleのテストではカスタムスキルと違って、JSONを吐き出してくれないので、そこはちょっと不便でした。あとスマホでスキル有効にしてログインしてないと、テスト機能が使えません。
引っかかった点
自分が引っかかったのは…
Lambdaのリージョンを東京にしていた
これが、一番はまった…
オレゴンのLambdaから東京のAWS IoTに通信ができない
結局、ラズパイをオレゴンに登録しなおし、そちらのエンドポイントを使うようにしました。
非同期処理
IotData.publish()関数は非同期なんですが、コールバックの外でアレクサに返事返してました。
これ前回のカスタムスキルの時もおんなじことやってた…前回とは別のサンプルソース使って、おんなじミスしてました。
サンプルソースが間違ってる?
Alexaへのレスポンスの”event.header.name”と”event.header.namespace”がサンプルソースのままでは、うまくいきませんでした。
アカウントリンクの認証画面URLが間違っていた
スマホでスキルを有効にした後に、ログイン画面が表示されませんでした。
参考情報
以下のサイトが役に立ちました。
- スマートホームスキルAPIのメッセージリファレンス
- スマートホームスキルの作成手順
- Alexa.Discoveryインターフェース
- Alexa Account Linking: 5 Steps to Seamlessly Link Your Alexa Skill with Login with Amazon
- HOWTO: Add OAUTH to your Alexa Smart Home skill in 10 minutes
感想
今回は自分専用だったということもありますが、思ったよりかは楽に作れました。特にAlexa、Lambdaの接続辺りは、Amazonが色々とやってくれているので便利でした。
処理の部分はもちろんやることによって大変さが変わってくると思います。不特定多数に公開するのであれば、自前でOAuth2用意したり、ユーザーの管理だったりも必要でしょうし、エラー処理もしっかりしないといけないですし。
”アレクサ、電気つけて”だけで電気つくのはやっぱり便利です。”ラズパイで”って言わなくていいだけでも、全然違いますね。作ってよかったです。
しかし、これで前回せっかく作ったカスタムスキルは要らなくなってしまった…
追記
PCの電源のオン・オフができるようになりました。
- アイリスオーヤマ(IRIS OHYAMA)