Alexaスキルでリマインダーを使用する

Alexa-Skill

コンテンツ

はじめに

Alexaスキルでのリマインダーの使い方について紹介します。

リマインダーの使い方は、すでに数多くの紹介記事があります。ここでは、少し変わった使い方として、必要に応じてリマインダーをスキップする方法を合わせて紹介します。またリマインダーの使用で試行錯誤した結果についても記します。

やりたいこと

毎日決まった時間に、あるタスクを実行するようにユーザーにリマインドします。ただし、その時間より前に、ユーザーがそのタスクを実行した場合は、リマインドしません(スキップします)。タスクの実行は、ユーザーによって報告されるものとします。

課題

リマインダー自体はAlexaでサポートされています。毎日特定の時間にリマインドすることもできます。しかし、スキルからリマインダーを設定すると、その後のリマインダーの実行はAlexa側で管理されているため、タスクの完了とは関係なく、常にリマインダーが通知されてしまいます。

解決策

リマインダーを毎日繰り返すように設定する場合、リマインドする時間とは別に、リマインドをいつから有効にするかを設定できます。例えば、明日からとか一週間後からといった設定が可能です。

この機能を利用して、タスクの実行がスキルに報告されたタイミンクで、リマインダーの開始日を明日に変更することで、当日のリマインドをスキップします。

タスクの実行が無い場合は、当初の設定どおりに当日のリマインダーが通知されます。

ユースケース

下記の3つのユースケースを実装します。ここでは散歩に行くようにリマインドすることにし、スキル名を「散歩コーチ」としています。

UC1: アレクサ、散歩コーチでリマインドして

  • 毎日繰り返すリマインダーを設定します。
  • テストを容易にするために、リマインドする時間は現在時刻から2分後の時刻とします。2分以内にタスクの実行を報告することで、リマインダーのスキップを確認できます。
  • リマインダーへのアクセス権がユーザーから付与されていない場合は、ユーザーにアクセス権の付与を求めます。
  • すでにリマインダーが設定されている場合は、新しい時刻でリマインドします。

UC2: アレクサ、散歩コーチでリマインダーを解除して

  • リマインダーを解除します。
  • リマインダーが設定されていない場合は、その旨を伝えます。
  • リマインダーへのアクセス権がユーザーから付与されていない場合は、ユーザーにアクセス権の付与を求めます。

UC3: アレクサ、散歩コーチで散歩を記録して

  • 当日のリマインドがまだの場合は、リマインダーをスキップします。
  • リマインダーへのアクセス権がユーザーから付与されていない場合は、ユーザーにアクセス権の付与を求めます。

リマインダーについての考慮点

ここではリマインダーを使用する上で、知っておくと良いと思われることを、まとめました。

スキルとリマインダーのスコープ

操作対象のリマインダー

スキルからは、自分が作成したリマインダーのみ、取得、変更、削除ができます。

例えば本スキルでは、リマインダーを設定する際に、すでに設定されているリマインダーがあれば、全て削除しています(*)。リマインダーを解除する場合も同様です。この削除対象となるリマインダーは(APIで取得できるリマインダーは)、本スキルが作成したリマインダーのみになります。

*仕様上はたかだか1つですが、エラー処理を簡略化するためにループ内で全削除しています。

リマインダーとデバイスの関係

複数のデバイスがある時は、リマインダーの通知先は、Alexaアプリの設定によります。全てのデバイスで通知するオンにすると、リマインダーは全てのデバイスで通知されます。オフにすると、リマインダーを設定したデバイスのみで通知されます。

Alexaアプリからリマインダーを作成する場合は、通知先のデバイスを選択できます。スキルからも同じことができても良さそうですが、APIドキュメントを読む限りではそのような機能は無さそうです。

もしリマインダーの通知先が指定できれば、特定の部屋でのみリマインダーを通知するとか、色々と面白いユースケースが考えられそうですが …

リマインダーとパーソンの関係

Alexaでは音声プロファイルを登録して、家族をそれぞれパーソンとして識別できます。同じスキルの中から、各パーソンごとにリマインダーを設定、解除したい場合もあると思います。しかしAlexaにはリマインダーとパーソンを紐付ける機能はありません。もしリマインダーとパーソンを紐付ける場合は、そのような紐付けをスキル側で行う必要があります。

アクセス権の取得方法

スキルからリマインダーにアクセスするには、ユーザーにリマインダーへのアクセス権を付与してもらう必要があります。これには次のステップが必要です。

  • 開発者コンソールで、リマインダーへのアクセス権をリクエストする
  • Alexaアプリで、リマインダーへのアクセスをユーザーが付与する、または
  • スキル内でユーザーに対して、リマインダーへのアクセス権をリクエストする

以下、それぞれのステップを見てみます。

開発者コンソールでアクセス権限をリクエスト

開発者コンソールで、対象のスキルのビルドタブを選び、左側のメニューからモデルをクリックします。

メニューが切り替わるので、更にアクセス権限をクリックします。アクセス権をユーザーにリクエストする項目がリストされていますから、その中からリマインダーを選んでオンにします。

ここでの設定は、あくまでスキルがリマインダーへのアクセス権を必要としていることを、宣言するだけです。実際にリマインダーにアクセスするには、この後に述べるようにユーザーにアクセス権を付与してもらう必要があります。

Alexaアプリでアクセス権を付与

Alexaアプリからスキルを有効にすると、アクセス権の付与画面が表示され、リマインダーへのアクセス権を付与できます。Alexaアプリからのアクセス権の付与は何時でもできます。

スキルの中からアクセス権をリクエスト

ユーザーがAlexaアプリからアクセス権を付与しなかった場合、スキルの実行時にユーザーからのアクセス権の付与を得る必要があります。これには下記の2つの方法があります。

  • Alexaアプリにアクセス権をリクエストするカードを表示
    ユーザーがカード上でアクセス権を付与できます。スキル側の処理は単純ですが、Alexaアプリを操作するために、スマフォが手元に必要です。また対話も一旦終了するので、アクセス権を付与した後に、元の発話(例: リマインドして)を繰り返す必要があります。
  • 音声対話の中でアクセス権をリクエスト
    ユーザーが音声で承諾することで、アクセス権を付与できます。スキル側の処理は少し複雑になりますが、スマフォが手元に無くても付与でき、また対話も継続するので、ユーザビリティは良くなります。

本スキルでは後者の方法を取ります。

アクセス権の有無の確認方法

アクセス権があるかどうかは、リマインダーAPIにアクセスしてみないと判りません。具体的には、APIを呼び出したあとに戻される、ステータスコードで判定します。アクセス権の有無について考慮する必要があるのは、下記の3つのステータスコードです。

  • 200 – リマインダーへのアクセス権あり
  • 401 – リマインダーへのアクセス権なし (認証に相当)
  • 403 – リマインダーへのアクセス権はあるが、リマインダーにアクセスできない (認可に相当)

このうち403は、テストした範囲では、開発者コンソールのAlexaシミュレータから「リマインドして」とリクエストした時に発生しました。リマインダー作成のAPIで、403のコードと一緒に”DEVICE_NOT_SUPPORTED“というエラーメッセージが返ってきます。どうやらAlexaシミュレータからは、リマインダーを作成できないようです。これはAlexaシミュレータに、リマインダーをユーザーに通知する機能が無いからと推測しています。(しかしスキルで設定したリマインダーは、Alexaアプリでの設定によっては、全てのデバイスに通知することもできますから、敢えて拒否する必要も無いのではと思いますが …)

認証と認可の違い

一般的に、認証はあるサービスに対するアクセスができるがどうかを示します。認可は認証を得た上で、そのサービスのあるリソースに対してある操作ができるかどうかを示します。例えば社員データベースにログインし(認証)、自分のデータは見られるが他人のデータは見られない(認可)、といった具合です。(大雑把な説明ですが …)

上記の403は、リマインダーにはアクセスできる(認証OK)が、Alexaシミュレータ上でリマインダーを作成する権限はない(認可NG)、と解釈すれば良さそうです。

リマインダーAPIの新しい要求形式

リマインダーAPIを通して、リマインダーの作成や更新を行う際に指定するバラメータに、新しい形式がリリースされています。ただし、本稿の執筆時点(2021年3月)では日本語版のAPIドキュメント(こちら)には記述されておらず、英語版のAPIドキュメント(こちら)にのみ記述されています。英語版のドキュメントによれば、日本語版のドキュメントの要求形式はDeprecatedとされており、何れサポートされなくなる可能性もありそうです。本稿では、この新しい要求形式を採用しています。

リマインダーのガイドライン

リマインダー使用のガイドライン(こちら)を守る必要があります。ガイドラインでは、リマインダーの内容をユーザーに確認するように求められています。本スキルでは、この確認を省略しており、この点でガイドラインからは外れています。

対話モデルの定義

対話モデルでは、ユースケースに対応した下記の3つのインテントを定義します。

  • UC1: EnableReminderIntent
  • UC2: DisableReminderIntent
  • UC3: RecordTaskIntent

UC1: EnableReminderIntentの定義

UC2: DisableReminderIntentの定義

UC3: RecordTaskIntentの定義

スキルの実装

ユースケースのインテントごとに処理を実装しています。詳しくはソースコードを参照してください。「付録. ソースコード」にgithubへのリンクを載せています。

ここでは「UC1: 散歩コーチでリマインドして」を取り上げて、主な処理について説明することにします。UC1のフローを下図に示します。UC2, UC3でも同様のフローがあります。

リマインダーサービスへのアクセス

Alexa SDKには、リマインダーにアクセスするために、リマインダーサービスクライアントが提供されています。これを下記のgetReminderClient()関数で取得しています。

function getReminderClient(handlerInput) {
    return handlerInput.serviceClientFactory.getReminderManagementServiceClient();
}

リマインダーの取得

本スキルによって設定されたリマインダーを、下記のgetReminders()関数で取得します。

リマインダーサービスクライアント経由で行いますが、下記の2つの例外を処理する必要があります。

  • 401 – アクセス権が無い(nullを返す)
  • 404 – 設定されているリマインダーが無い(空の配列を返す)
async function getReminders(reminderClient) {
    console.log('getReminders');

    try {
        return await reminderClient.getReminders();
    }
    catch (error) {
        if (error.name === 'ServiceError') {
            console.log(`Service Error - Code: ${error.statusCode}, Message: ${error.message}`);

            if (error.statusCode === 401) {
                // アクセス権が無い
                return null;
            }
            else if (error.statusCode === 404) {
                // 設定されているリマインダーが無い
                return { alerts: [] };
            }
        }
        else {
            console.log(`None Service Error: ${error.stack}`);
        }

        throw error;
    }
}

リマインダーの設定

下記のenableReminder()関数で、リマインダーを設定しています。

リマインダーサービスクライアント経由で行いますが、下記の例外を処理する必要があります。

  • 403 – リマインダーが設定できない
async function enableReminder(reminderClient, responseBuilder) {
    console.log('enableReminder');

    const reminders = await getReminders(reminderClient, TokenToEnableReminder);

    if (reminders) {
        // アクセス権が付与されている
        for (const alert of reminders.alerts) {
            // 既存のリマインダーを削除
            await reminderClient.deleteReminder(alert.alertToken);
        }

        // リマインダーを作成
        const startDateTimeString = getStartDateTimeStringToday();

        const now = new Date();
        const recurrenceTime = {};
        recurrenceTime.byHour = LocalDate.getHours(now);
        recurrenceTime.byMinute = LocalDate.getMinutes(now) + ReminderDelayInMinutes;
        recurrenceTime.bySecond = LocalDate.getSeconds(now);

        const reminderRequest = buildReminderRequest(startDateTimeString, recurrenceTime);

        try {
            await reminderClient.createReminder(reminderRequest);

            return responseBuilder
                .speak(`リマインダーを${recurrenceTime.byHour}時${recurrenceTime.byMinute}分${recurrenceTime.bySecond}秒に設定しました。`)
                .withShouldEndSession(true)
                .getResponse();
        }
        catch (error) {
            if (error.name === 'ServiceError') {
                console.log(`Service Error - Code: ${error.statusCode}, Message: ${error.message}`);

                if (error.statusCode === 403) {
                    // リマインダーが設定できない (Alexaシミュレータなど)
                    return responseBuilder
                        .speak('このデバイスでは、リマインダーを設定できません。')
                        .withShouldEndSession(true)
                        .getResponse();
                }
            }
            else {
                console.log(`None Service Error: ${error.stack}`);
            }

            throw error;
        }
    }
    else {
        // アクセス権が付与されていない
        // Alexaにユーザーからアクセス権を取得するように依頼する
        return buildGrantRemindersAccessResponse(
            responseBuilder,
            TokenToEnableReminder);
    }
}

リマインダーは、テスト用途として、現在時刻から2分後の時刻を、絶対時刻(SCHEDULED_ABSOLUTE)で指定しています。2分以内に「散歩」が報告されれば、当日のリマインダーをスキップします。

下記のbuildReminderRequest()関数でリクエストを作成しています。

function buildReminderRequest(startDateTimeString, recurrenceTime) {
    const reminderText = '散歩の時間';
    const now = new Date();
    const requestTime = LocalDate.toISOStringNZ(now);

    const ssmlText = `<speak>散歩をしてください。</speak>`;
    const reminderRequest = {
        requestTime: requestTime,
        trigger: {
            type: "SCHEDULED_ABSOLUTE",
            timeZoneId: 'Asia/Tokyo', // Default is the device's timezone
            recurrence: {
                startDateTime: startDateTimeString,
                recurrenceRules: [
                    `FREQ=DAILY;BYHOUR=${recurrenceTime.byHour};BYMINUTE=${recurrenceTime.byMinute};BYSECOND=${recurrenceTime.bySecond};INTERVAL=1;`
                ]
            }
        },
        alertInfo: {
            spokenInfo: {
                content: [{
                    locale: 'ja-JP',
                    text: reminderText,
                    ssml: ssmlText
                }]
            }
        },
        pushNotification: {
            status: 'ENABLED'
        }
    }

    return reminderRequest;
}

主な設定項目は、下記の通りです。

  • trigger.type: 'SCHEDULED_ABSOLUTE
    絶対時間でリマインダーの時間を指定します。他にはSCHEDULED_RELATIVEが指定可能です。
  • trigger.timeZoneId: 'Asia/Tokyo'
    リマインダー指定時間のタイムソーンを指定します。タイムゾーンを指定しない場合は、リマインダーの設定を要求したデバイスのタイムゾーンとなります。
  • trigger.recurrence.startDateTime
    リマインダーが有効になる日時を指定します。実際にリマインダーが通知される日時ではありません。この日時以降に、次のrecurrenceRulesで指定されたリマインダーが、有効になります。例えば、翌日以降に有効となる、といった指定も可能です。getStartDateTimeStringToday()関数では、当日の午前0時を指定しています。
  • trigger.recurrence.recurrenceRules
    リマインダーの繰り返しルールを指定します。こちらが実際にリマインダーが通知される時間になります。複数のリマインダーを設定することもできます。
    • FREQ=DAILY
      毎日繰り返します。他に、WEEKLY, MONTHLY, YEARLYが指定可能です。
    • BYHOUR, BYMINUTE, BYSECOND
      リマインダーが通知される時間です。

LocalDateについて

コードの中で使っているLocalDateは、筆者の作成したユーティリティです。指定したタイムゾーンの日時への変換を行います。

アクセス権の取得

アクセス権の取得依頼

getReminders()関数の中で401例外が発生すると、それをトリガーにして、アクセス権の付与をユーザーに依頼するよう、Alexaにレスポンスを返します。

下記のbuildGrantRemindersAccessResponse()関数で、Alexaに返すレスポンスを作成しています。

function buildGrantRemindersAccessResponse(responseBuilder, token) {
    console.log('buildGrantRemindersAccessResponse');

    return response = responseBuilder
        .addDirective({
            type: 'Connections.SendRequest',
            name: 'AskFor',
            payload: {
                '@type': 'AskForPermissionsConsentRequest',
                '@version': '1',
                'permissionScope': 'alexa::alerts:reminders:skill:readwrite'
            },
            token: token
        })
        .getResponse();
}

AskForレスポンスの処理

スキルからアクセス権の取得をAlexaに依頼すると、Alexaがユーザーと対話して、アクセス権の付与をリクエストします。その後Alexaサーバーからスキルが呼び出され、ユーザーのレスポンス(付与の有無)が通知されます。レスポンスには以下の3つの何れかが返されます。

  • ACCEPTED(付与された)
  • DENIED(拒否された)
  • NOT_ANSWERED(応答が無かった、またはAlexaが理解できなかった)

下記にAskForに対するハンドラを示します。

const ReminderPermissionResponseHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'Connections.Response'
            && handlerInput.requestEnvelope.request.name === 'AskFor'
    },
    async handle(handlerInput) {
        console.log('PermissionResponseHandler');

        const { status, token, payload } = handlerInput.requestEnvelope.request;
        let response;

        if (status.code === '200') {
            switch (payload.status) {
                case 'ACCEPTED':
                    const reminderClient = getReminderClient(handlerInput);
                    const responseBuilder = handlerInput.responseBuilder;

                    if (token === TokenToEnableReminder) {
                        response = await enableReminder(reminderClient, responseBuilder);
                    }
                    else if (token === TokenToDisableReminder) {
                        response = await disableReminder(reminderClient, responseBuilder);
                    }
                    else {
                        response = await skipReminder(reminderClient, responseBuilder);
                    }
                    break;
                case 'DENIED':
                case 'NOT_ANSWERED':
                    let operation;

                    if (token === TokenToEnableReminder) {
                        operation = '設定';
                    }
                    else if (token === TokenToDisableReminder) {
                        operation = '解除';
                    }
                    else {
                        operation = 'スキップ';
                    }

                    const msg =
                        `リマインダーにアクセスする許可を頂けなかったので、リマインダーの${operation}はしていません。` +
                        'アクセスを許可する場合は、再度リクエストを繰り返すか、Alexaアプリのホーム画面で、リマインダーへのアクセスを許可してください。';

                    if (!payload.isCardThrown) {
                        response = handlerInput.responseBuilder
                            .speak(msg)
                            .withAskForPermissionsConsentCard(
                                ['alexa::alerts:reminders:skill:readwrite'])
                            .withShouldEndSession(true)
                            .getResponse();
                    }
                    else {
                        response = handlerInput.responseBuilder
                            .speak(msg)
                            .withShouldEndSession(true)
                            .getResponse();
                    }
                    break;
                default:
                    // 発生しないはず
                    console.log(`AskForReminder failed: ${status.code}, ${status.message}`);
                    response = handlerInput.responseBuilder
                        .speak('内部エラーが発生しました。')
                        .withShouldEndSession(true)
                        .getResponse();
            }
        }
        else if (status.code === '204') {
            console.log(`AskForReminder failed: ${status.code}, ${status.message}`);
            response = handlerInput.responseBuilder
                .speak('リクエストの処理を打ち切ります。')
                .withShouldEndSession(true)
                .getResponse();
        }
        else {
            console.log(`AskForReminder failed: ${status.code}, ${status.message}`);
            response = handlerInput.responseBuilder
                .speak('内部エラーが発生しました。')
                .withShouldEndSession(true)
                .getResponse();
        }

        return response;
    }
};

ステータスコード200の時に、payload.statusの値により、ユーザーからのレスポンスが判ります。

ACCEPTEDの場合、アクセス権が付与されています。この場合は、リマインダーを設定します。

DENIEDの場合、ユーザーによりアクセス権の付与が拒否されています。リマインダーは設定しなかった旨を返します。

NOT_ANSWEREDの場合、ユーザーから応答が無かったか、Alexaが理解できなかった可能性があり、アクセス権は付与されていません。リマインダーは設定しなかった旨を返します。

DENIEDNOT_ANSWEREDの時、payload.isCardThrownを確認しています。ここがfalseの場合は、Alexaアプリにカードを表示して、リマインダーへのアクセス権を求めるように、Alexaにレスポンスします。この部分は、こちらのサンプルコードを参考にしました。Banana Stand Reminders API Demo – Alexa Cookbook

ただし、テストした限りでは、常にfalseで渡ってきます。アクセス権の付与をユーザーにリクエストする際に、Alexaアプリにカードを表示する方法と、音声での付与を求める方法があることを、先に述べました。ここでは音声で付与を求める方法を使っていますから、カードの表示はしていません。なので常にfalseになっているのではないかと推測しています。(では逆にどういう場合にtrueとなるのか … 未調査です)

ACCEPTED(承諾)後の処理の継続

本スキルでは、アクセス権の取得はUC1, UC2, UC3の何れでも発生し得ます。Alexaとのやり取りは同じですが、ACCEPTされた後の処理は、UC1, UC2, UC3でそれぞれ異なります。

どの状況(UC)でアクセス権が要求されたのかを知るために、tokenを使います。リクエスト時に指定したtokenが、ACCEPT時にも伝達されてくるので、tokenの値によって後続の処理を決めています。

if (token === TokenToEnableReminder) {
    response = await enableReminder(reminderClient, responseBuilder);
}
else if (token === TokenToDisableReminder) {
    response = await disableReminder(reminderClient, responseBuilder);
}
else {
    response = await skipReminder(reminderClient, responseBuilder);
}

リマインダーの解除要求(UC2)やタスクの実行報告時(UC3)に、リマインダーへのアクセス権が無い場合は、アクセス権を求めるのではなく、単純に「リマンイダーは設定されていません。」と応答することでも良いと思います。通常はアクセス権が無ければ、リマインダーは設定されていないからです。ただし特殊なケースとして、リマインダーを設定した後に、Alexaアプリでアクセス権を取り消している場合があります。

スキルによっては、複数のアクセス権を必要とする場合もあります。例えばリマインダーとタイマーへのアクセスです。このような場合も、tokenに何に対する(リマインダー又はタイマー)アクセス権なのかを指定することで、後続の適切な処理を選択できます。

リマインダーのスキップ

方法自体は単純です。現在のリマインダーの設定を読み出し、リマインダーが有効になる日付を翌日に変更します。時間は同じです。これでまだ当日のリマインダーが出されていなければ、スキップとなります。

ただし下記のコードでは、リマインダーの更新ではなく、リマインダーの削除と作成を実行しています。これについては、この後のセクションで理由を説明しています。

async function skipReminder(reminderClient, responseBuilder) {
    console.log('skipReminder');

    const reminders = await getReminders(reminderClient);
    let response;

    if (reminders) {
        // アクセス権が付与されている
        if (reminders.alerts.length > 0) {
            // 既存のリマインダーがある
            for (const reminder of reminders.alerts) {
                // 既存のリマインダーを更新
                const now = new Date();
                const recurrenceTime = {};
                recurrenceTime.byHour = LocalDate.getHours(now);
                recurrenceTime.byMinute = LocalDate.getMinutes(now) + ReminderDelayInMinutes;
                recurrenceTime.bySecond = LocalDate.getSeconds(now);
                const newReminder = buildReminderRequest(
                    getStartDateTimeStringTomorrow(),
                    recurrenceTime);
                await reminderClient.deleteReminder(reminder.alertToken);
                await reminderClient.createReminder(newReminder);
                // 本来下記のように開始日のみ書き換えて更新すれば良い。
                // 複数デバイスがあるときに、リマインダを作成したデバイス以外に更新が伝わらない
                // という現象があり、削除と作成を組み合わせている。
                // reminder.trigger.recurrence.startDateTime = getStartDateTimeStringTomorrow();
                // await reminderClient.updateReminder(reminder.alertToken, reminder);
            }

            response = responseBuilder
                .speak('お疲れさまです。明日またリマインドしますね。')
                .withShouldEndSession(true)
                .getResponse();
        }
        else {
            // 既存のリマインダーは無い
            response = responseBuilder
                .speak('お疲れさまです。リマインダーは設定されていません。')
                .withShouldEndSession(true)
                .getResponse();
        }
    }
    else {
        // アクセス権が付与されていない
        // Alexaにユーザーからアクセス権を取得するように依頼する
        response = buildGrantRemindersAccessResponse(
            responseBuilder,
            TokenToSkipReminder);
    }

    return response;
}

リマインダーAPIの謎の現象

リマインダーAPIをスキルから使用する中で、幾つかの問題に直面しました。筆者のAPIに対する理解不足や、使用方法の誤りが原因かも知れませんが、原因を解明することができませんでした。そこで取り敢えず記録に残すことにしました。

なおこれらの現象は本稿執筆時点(2021年3月)でのものです。

リマインダーの設定に関する謎

以下は本スキルで使用している日次繰り返し(DAILY)のリマインダーが対象になります。

旧APIで開始日が翌日のリマインダーを作成できない

リマインダーの作成時に、日付を翌日に指定しても、実際には当日に設定されている現象がありました。そこで設定時刻と設定日の組み合わせで結果がどのようになるのかを調査しました。結果は下表の通りです。このうち、#1は仕様として理解できなくもありません。しかし#4は翌日開始のリマインダーを設定できないことになります。

ケース設定時刻設定日結果
#1現在時刻より-1分当日翌日
#2現在時刻より +1分 当日当日
#3現在時刻より -1分 翌日翌日
#4現在時刻より +1分 翌日当日

リマインダーの更新時にも同様の問題が発生しました。すなわちまだ通知されていない(設定時間になっていない)リマインダーの開始日を翌日に設定しようとしても、当日のままでした。これではリマインダーのスキップが出来ないことになり、本スキルにとっては重大な問題です。

ケース設定時刻設定日更新内容結果
#1現在時刻 +1分 当日通知前に翌日に当日
#2現在時刻 +1分 当日通知後に翌日に翌日

同様のことを新APIでも調査した結果を下表に示します。こちらは想定通りの動きをしています。

設定時刻設定日結果
現在時刻より -1分 当日当日
現在時刻より +1分 当日当日
現在時刻より -1分 翌日翌日
現在時刻より +1分 翌日翌日

リマインダーの更新時にも問題ありません。

ケース設定時刻設定日更新内容結果
#1現在時刻 +1分 当日通知前に翌日に翌日
#2現在時刻 +1分 当日通知後に翌日翌日

この結果から、本スキルでは新APIを採用しています。

新APIでリマインダーの更新が他のデバイスに反映されない

3台のデバイスを登録してテスト中に、リマインダーの更新が、リマインダーを作成したデバイスにしか反映されない現象がありました。

ケース設定時刻設定日更新内容結果
#1現在時刻 +1分 当日+1分3台とも2分後に通知
#2現在時刻 +1分 当日翌日設定デバイスは翌日に通知
他の2台は1分後に通知
& 翌日にも通知

リマインダーを一旦削除して、翌日の開始日で新しいリマインダーを作成したところ、この問題は発生しなくなりました。本スキルでは、この方法で問題を回避しています。

アクセス権の付与に関する謎

NOT_ANSWEREDが発生しない

アクセス権の付与をAlexa経由で求めたあと、Connections.Response (AskFor)を受け取り、ユーザーのレスポンスをpayload.statusで確認しています。この時NOT_ANSWEREDというステータスが返る場合があることになっています。しかしテストした限りでは、NOT_ANSWEREDが返ることはありませんでした。

何も発話せずに放置すると「・・・許可しますか?」というメッセージを3回繰り返した後、「ブッ」というエラー音を発してタイムアウトになりました。この時スキルには、何も呼び出しはありませんでした。

NOT_ANSWEREDの定義は、下記のようになっています。

NOT_ANSWERED – ユーザーが権限付与のリクエストに応答しなかったか、応答が理解されませんでした。この場合、Alexaはユーザーに再プロンプトを出します。

英語版ドキュメントの同じ箇所の記述です。

NOT_ANSWERED – the user did not answer the request for permissions, or the response was not understood. In this case, Alexa will re-prompt the user.

良くわからないのは、「Alexaはユーザーに再プロンプトを出します。」となっている部分です。英語版でも”Alexa will …”となっていますが、これだとAlexa側での対処を説明しているように読めます。”Skill should …”ならばスキルに対する要件と読めますが …

ステータス 204

上記のNOT_ANSWEREDのテスト中に、「ハロー」とか関係のない発話をしてみました。このときステータスに204 (Session cancelled by user)が返ってくることがありました。

AskForPermissionsConsentCardがAlexaアプリのホームに表示されない

DENIEDNOT_ANSWEREDのときに、AlexaアプリにAskForPermissionsConsentCardを表示するように、レスポンスしてみました。しかしAlexaアプリのホームには、このカードが表示されませんでした。

調べてみると、Alexaアプリの「アクティビティ」画面には、このカードが表示されていました。これに関して、英語のAlexa Skill Kit (ASK)フォーラムに、こちらのようなやり取りがありました。

回答部分を下記に示します。これによれば、ホームではなくアクティビティに表示されることはあり得るようです。

KirkC@Amazon answered · Oct 22 at 7:58 AM

Hi. Not all cards show up in the "Home" tab of the iOS / Android Alexa app.

In fact, using my own test skill I was able to reproduce the behavior described where my skill's responses included the following card object:


"card": {
   "type": "AskForPermissionsConsent",
   "permissions": [
      "alexa::alerts:reminders:skill:readwrite"
   ]
}


And no mention of the reminders permission showed up on the "Home" section. Instead, this card, and indeed most categories of card responses, instead show up under "More > Activity".

以前あるスキルで認定を取った時には、認定の要件で「ホーム画面にAskForPermissionsConsentCardを表示すること」とあり、そのように対応したことがあります。当時はAlexaアプリのホームにカードが表示されましたが …

その他の謎

Alexaアプリのリマインダー一覧

Alexaアプリを使うと、そのアカウントで作成したリマインダーの一覧が見られる、と思っていました。しかし本スキルから作成したリマインダーは表示されません。それどころか、Alexaデバイスから設定した標準のリマインダーも表示されません。唯一表示されるのは、Alexaアプリのメニューから手動で作成したリマインダーだけです。以前テストした時には、スキルからのリマインダーも一覧に表示されたと記憶していますが …

おわりに

リマインダーの使い方について紹介しました。

以前自作のスキルにリマインダーを組み込んだ時に試行錯誤をしたので、その結果をまとめたいと思い本稿を書き始めました。今回手順を見直すことで、試行錯誤の途中で経験した「謎の現象」も、解明できることを期待していました。しかし相変わらず「謎の現象」に悩むことになり、大いにモヤモヤ感が残っています。筆者の使い方に問題があるのか … 今後も新たな情報があれば更新したいと思います。

参考情報

付録. ソースコード

GitHubのレポジトリ

更新記録

日付内容
2021/03/28初版公開                    

コメント

タイトルとURLをコピーしました