device IDを使ってAmazon Alexa端末を識別してみる

Alexa Skills Kit

陽春の候, 春爛漫の好季節を迎え、毎日お元気でご活躍のことと存じます、せーのです。今日はAlexaの複数の端末から同じスキルを使う時、それぞれの端末を識別してみよう、という実験です。

色んなIDがある

Alexa Skill Kitを組む際に難しいポイントとして「IDがやたらとある」という点が挙げられます。Application ID, Session ID, Request ID, User ID, device ID|、、、これらのIDをきちんと整理しておくと仕様的にも運用管理的にもとても楽になります。ちなみにこれらのIDはそれぞれこのような位置づけになります。

ID名 内容
Application ID Skill単位に振られるID
Session ID セッション単位(会話が始まってから終わるまで)で振られるID
Request ID リクエスト単位(1会話ごと)で振られるID
User ID ユーザアカウント単位(Skillを登録しているAmazon.comアカウント)で振られるID
Device ID 端末単位で振られるID

今回はこのうち「Device ID」を使って端末単位にAlexa製品を分けてみたいと思います。

deviceidの場所

deviceidはフルパスでは[event.context.System.device.deviceId]となります。Alexa Skillの管理コンソールでのtestではこの[event.context]から先がLambdaに送られないため、必ず"実機"での送信が必要となります。Amazon Echo等のAlexa製品じゃなくても、Raspberry Pi + AVSなどでも"端末"として認識し、event.contextが送られます。JSONで言うとevent.contextはこんな感じになります。

    "context": {
        "AudioPlayer": {
            "playerActivity": "IDLE"
        },
        "System": {
            "application": {
                "applicationId": "amzn1.ask.skill.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
            },
            "user": {
                "userId": "amzn1.ask.account.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
            },
            "device": {
                "deviceId": "amzn1.ask.device.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
                "supportedInterfaces": {
                    "AudioPlayer": {}
                }
            },
            "apiEndpoint": "https://api.amazonalexa.com"
        }
    },

device情報としてはdeviceIdの他に[supportedInterfaces]というのもあります。これはその名の通り「そのデバイスがサポートしているインターフェース」が入ります。例えば上に入っている「AudioPlayer」というのはレスポンスとしてオーディオファイル、つまり音声ファイル(mp3とか)が扱える製品ですよ、ということを示しています。これがデバイスがFire TV StickだとAudioPlayerインターフェースはないのでこの項目は出てきません。

ある程度IDについてわかった所で今回の主題である「deviceIdを使って端末を識別する」をやってみます。

やってみる

今回はテストということで予め調べておいた各端末のdeviceIdを変数として保管しておき、呼びかけられたら、その端末のdeviceIdによって違う答えを返す、というスキルを実装してみます。各端末のdeviceIdは、一つ簡単なスキルを書いて、そこで

console.log("event.context.System.device.deviceId: " + event.context.System.device.deviceId);

というような一文を入れておけばCloudWatch Logsに記録されます。実用的にはdeviceIdの登録Intent等を作っておいてDynamoDB辺りに保管しておけばスマートでしょう。

Lambdaの側はまず大元であるexports.handler内でまずdeviceIdを取得してグローバル変数に入れておきます。

var deviceid = "";
var supportedInterfaces = "";
exports.handler = (event, context, callback) => {
    try {
        deviceid = "";
        console.log(`event.session.application.applicationId=${event.session.application.applicationId}`);
        console.log("request: " + JSON.stringify(event));
        if (event.context) {
            
            deviceid = event.context.System.device.deviceId;
            supportedInterfaces = event.context.System.device.supportedInterfaces;
        } else {
            console.log("event.context is nothing");
        }
        
        /**
         * Uncomment this if statement and populate with your skill's application ID to
         * prevent someone else from configuring a skill that sends requests to this function.
         */
        /*
        if (event.session.application.applicationId !== 'amzn1.echo-sdk-ams.app.[unique-value-here]') {
             callback('Invalid Application ID');
        }
        */

        if (event.session.new) {
            onSessionStarted({ requestId: event.request.requestId }, event.session);
        }

        if (event.request.type === 'LaunchRequest') {
            onLaunch(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'IntentRequest') {
            onIntent(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'SessionEndedRequest') {
            onSessionEnded(event.request, event.session);
            callback();
        }
    } catch (err) {
        callback(err);
    }

次にonIntent内で用意しておいた変数と取得したdeviceIdを比べて、マッチしたものに合う回答をOutputSpeechに埋め込みます。

function onIntent(intentRequest, session, callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);
    console.log("deviceid=" + deviceid);
    console.log("supportedInterfaces" + JSON.stringify(supportedInterfaces));
    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;
    
    var devicedot = { deviceid: "amzn1.ask.device.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    devicename: "Amazon dot!"};
    var deviceecho = { deviceid: "amzn1.ask.device.YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY",
    devicename: "Amazon Echo!"};
    var devicetap = { deviceid: "amzn1.ask.device.ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ",
    devicename: "Amazon tap!"};
    var echosim = { deviceid: "amzn1.ask.device.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
    devicename: "echosim!"};
    var Lexi = { deviceid: "amzn1.ask.device.BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
    devicename: "Lexi iPhone App!"};
    var names=[devicedot, deviceecho, devicetap, echosim, Lexi];
    var devicename = "";
    for (var key in names){
        if (names[key].deviceid == deviceid){
            console.log("match!");
            devicename = names[key].devicename;
        }
    }
    
    
    let repromptText = 'Sorry, Please operate again';
    let sessionAttributes = {};
    const shouldEndSession = true;
    let speechOutput = "You are " + devicename;

    
    callback(sessionAttributes,
         buildSpeechletResponse(intent.name, speechOutput, repromptText, shouldEndSession));
}

ASK側の設定は非常に簡単で、ひとつの質問に一つのIntentをマッピングさせ、Slotはなし、とします。

Alexadeviceid2

これで準備は整いました。それぞれのAlexa端末から「Alexa, ask device confirmation about my device」と質問するとそれぞれの端末ごとに違う答えを返します。

alexa deviceid sample from Tsuyoshi Seino on Vimeo.

成功です。

まとめ

いかがでしたでしょうか。個人的にはこれでだいぶ使い勝手がよくなったな、と感じます。皆様も是非試してみて下さい。

参考リンク