はじめに
AlexaのSmartHomeSkillは、洗濯機や扇風機などの家電を、Alexaを使って操作するための機能です。PowerOnOffやModeControllerなどのあらかじめ決められたインターフェイスがあり、それらを利用して機能を組み立てていきます。
SmartHomeSkillでは、音声認識から意図解釈まではAmazon側でビルドされたモデルによって実行するため、開発者が意図解釈のために対話エージェントを訓練する必要はありません。
やりたいこと
Alexaから、自分たちで作成したSmartHomeSkillを呼び出します。
バックエンドのサーバで,Alexaからの通信を受けて,ユーザを認証し,デバイス制御クラウドと連携し,機器の制御を実行します.
以下のチュートリアルのコードでは,Discovery(機器探索)とPowerController(複数あるControlDirectiveのひとつ)までできるようになっています.
https://developer.amazon.com/ja-JP/docs/alexa/smarthome/steps-to-build-a-smart-home-skill.html
なんだか尻切れトンボで終わっていて,これだけでは満足にSmartHomeSkillを動かすことができませんので,以下の要素を追加してそれなりに動くSmartHomeSkillにすることを目指します.
- AcceptGrantリクエストをハンドリングする
- ReportStateリクエストをハンドリングする
- ChangeReportなどのプロアクティブな通知系を実装する
- EndpointHealthインターフェイスを実装する(推奨)
おまけで,ModeControllerを使えるようにします。
AcceptGrantリクエストをハンドリングする(アカウントリンクの実装)
やること
- デバイス制御クラウドで利用できるIDプロバイダの情報をAlexa開発者コンソールのアカウントリンク設定で設定します.Token URLや,Authorization URL,プロバイダのクライアントID,クライアントシークレットなど,必要な情報を設定し,AlexaからIDプロバイダのログイン画面を呼び出したり,トークンを受け取ったりできるようにします.使われるIDプロバイダによって設定すべき情報は異なります.このとき,IDプロバイダ側では,クライアントID等の発行をしたり,AlexaのリダイレクトURIをホワイトリストに登録したりする必要があります.ホワイトリストに登録すべきURIは,Alexa開発者コンソールのアカウントリンクのページに表示されています.
- バックエンドLambdaで,AcceptGrantリクエストを処理するプログラムを実装をします.AcceptGrantで送られてくるユーザのAmazonアカウントの認可コードを使い,LWA(Login With Amazon)のTokenエンドポイントを実行し,Amazonのアクセストークンとリフレッシュトークンを取得し,DBに保存します.このアクセストークンは,サーバからプロアクティブにAlexaに通知を送るとき(ChangeReportなど)に必要となります.LWAを呼び出すときに必要となるクライアントIDとクライアントシークレットは,Alexa開発者コンソールのアカウントリンクのページに表示されているものを使います.
AcceptGrantハンドリングのコードサンプルは以下です.
AlexaのAcceptGrantリクエストに付帯するユーザのAmazon認証情報でLWAにトークンを要求し,ユーザのAmazonのアクセストークンとリフレッシュトークンを取得するための実装です.
得られたトークンはDBに保存します.
下記のコードのOAUTH_CLIENT_ID
に作成したAlexaスキルのクライアントID、OAUTH_CLIENT_SECRET
に、同じくAlexaスキルのクライアントシークレットを設定します。
下記の関数は、以下の記事からコピペしました(https://qiita.com/poruruba/items/5e31b82bfbdeef20519d)。
(Requestモジュールじゃなくて,Axiosで書き直した方が良いですね,RequestはDeprecatedのようでした)
function handleAuthorization(request, context) {
console.log("handleAuthorization function was called.");
var code = request.directive.payload.grant.code;
var options = {
url: 'https://api.amazon.com/auth/o2/token',
method: 'POST',
headers: { 'Content-Type':'application/json' },
json: {
grant_type: 'authorization_code',
code: code,
client_id: OAUTH_CLIENT_ID,
client_secret: OAUTH_CLIENT_SECRET
}
};
return new Promise((resolve, reject) => {
Request(options, function(error, response, body) {
if(error){
console.log('error: ', error);
return reject(error);
}
console.log(body);
accessToken = body.accessToken;
// accessTokenをDBに保存
var header = JSON.parse(JSON.stringify(request.directive.header));
header.name = 'AcceptGrant.Response';
context.succeed({event: {header: header, payload:{}}});
return resolve();
});
});
}
Lambda関数の冒頭の、リクエストの種類によって処理を振り分けている箇所にて、AcceptGrantリクエストを処理するために、上記の関数を呼び出す処理を追加します。
else if (request.directive.header.namespace == 'Alexa.Authorization') {
log("DEBUG:", "Authorization request", JSON.stringify(request));
handleAuthorization(request, context);
}
ここまでできた時点で、スマホのAlexaアプリから、スマートホームスキルのアカウントリンクができるようになっており,さらに,ユーザのAmazon資格情報を取得できるようになりました.
ReportStateリクエストをハンドリングする
AlexaAppなどでスマートホームスキルで検出済みの機器の状態を確認したり,機器を画面上から操作したりすることができます.アプリの画面上で機器の状態を確認するとき,Alexaから機器の状態がどうなっているのかサーバに問い合わせがきます.それがReportStateリクエストです.
これに対する対応は簡単で,ユーザに紐づく機器の状態を応答するだけです.
例えば,機器の状態がDBに保存されているのであればそれを参照して応答することになりますし,そうでなければ,機器に直接問い合わせて,その結果を応答することも考えられます.
Discoveryで指定したインターフェイスのうち,retrievableをTrueとしたもの全てについて応答する必要があります.
ChangeReportなどのプロアクティブな通知系を実装する
機器は,ユーザによって手動で状態を変えられたりする可能性があります.たとえば,エアコンであれば,Alexaからの操作ではなく,リモコンによる操作で設定温度が変えられるといったことが考えられます,
そうしたとき,Alexaに対して,最新の機器の状態を教えてあげる必要があります.それがChangeReportです.
また,機器が追加されたり,機器が削除されたときには,それぞれAddOrUpdateReport,DeleteReportを送信することになっています.
これらのプロアクティブな通知系は,AcceptGrantのときに保存したAmazonのアクセストークンを使ってAlexaに通知します.
なお,Discoveryで応答したインターフェイスのうち,proactivelyReportedをTrueにしたもの全てについて応答する必要があります.
EndpointHealthインターフェイスを実装する(推奨)
EndpointHealthは,機器に到達可能かどうかを応答するインターフェイスです.Alexaに,OKまたはUnreachableを応答します.
サーバから機器に接続できる状態ならば,OK,そうでないときはUnreachableを返すイメージです.
作ることが必須ではありませんが,推奨されているようです.
おまけ ModeControllerの追加
ModeControllerインターフェイス
https://developer.amazon.com/ja-JP/docs/alexa/device-apis/alexa-modecontroller.html
handleDiscovery関数内にて、デバイスのCapabilityを定義している箇所があります。ここに、ModeControllerのインターフェイスを追加します。
{
"type": "AlexaInterface",
"interface": "Alexa.ModeController",
"instance": "Washer.Mode",
"version": "3",
"properties": {
"supported": [
{
"name": "mode"
}
],
"retrievable": true,
"proactivelyReported": true,
"nonControllable": false
},
"capabilityResources": {
"friendlyNames": [
{
"@type": "text",
"value": {
"text": "モード",
"locale": "ja-JP"
}
}
]
},
"configuration": {
"ordered": false,
"supportedModes": [
{
"value": "Mode.Delicate",
"modeResources": {
"friendlyNames": [
{
"@type": "text",
"value": {
"text": "デリケート",
"locale": "ja-JP"
}
}
]
}
}
]
}
} // end of modecontroller interface
これで、アカウントリンク済みの状態で、「デリケートモードにして」と発話すると、Alexa側としては、デリケートモードがデバイスに存在していることは認識してくれます。
ただ、デリケートモードにしてくれと言われた時の実際の動作が規定されていないので、そこを決めてやります。
以下のデモコードを追加します。
function handleModeControl(request, context) {
log("DEBUG handleModeControl called.");
// 検出中に渡されたデバイスIDを取得します
var requestMethod = request.directive.header.name;
var instance = request.directive.header.instance;
var responseHeader = request.directive.header;
responseHeader.namespace = "Alexa";
responseHeader.name = "Response";
responseHeader.messageId = responseHeader.messageId + "-R";
// リクエスト中のユーザートークンパスを取得します
var requestToken = request.directive.endpoint.scope.token;
var modeResult = request.directive.payload.mode;
// 本来は、ここで洗濯機の制御APIを呼び出します
var contextResult = {
"properties": [{
"namespace": "Alexa.ModeController",
"instance": instance,
"name": "mode",
"value": modeResult,
"timeOfSample": "2017-09-03T16:20:50.52Z", //結果から取得します。
"uncertaintyInMilliseconds": 50
}]
};
var response = {
context: contextResult,
event: {
header: responseHeader,
endpoint: {
scope: {
type: "BearerToken",
token: requestToken
},
endpointId: "demo_id"
},
payload: {}
}
};
log("DEBUG Alexa.PowerController ", JSON.stringify(response));
context.succeed(response);
}
さらに、Lambdaの冒頭の処理分岐部分に以下を追記します。
else if (request.directive.header.namespace == 'Alexa.ModeController'){
log("DEBUG:", JSON.stringify(request));
handleModeControl(request, context);
}
これで、スマホアプリからデリケートモードにして、と発話すると、handleModeControl()が呼ばれるようになります。
ほぼ同様にして、他のインターフェイスも実装することができます。
おわりに
AlexaのSmartHomeSkillの公式チュートリアルのその後...的な内容です.
あれだけでは足りないので,あと,最低これくらいはいるでしょうという内容になります.
ご参考になれば幸いです.