🍣

alexaで自分の最寄駅の直近の時刻を知らせるスキルを開発してみた

に公開

Alexaで自分の最寄駅の直近の時刻を知らせるスキルを開発してみた

こんなスキルないかなと...

  • 地方住まいなので電車は一つの路線のみ
  • よく乗る電車は行き先が大体同じ方面
  • 駅近に住んでいるので直近の発車時刻に家を出ても間に合う
  • 最寄駅からの直近の発車時刻を知らせる機能ないかな?

こんな要件にしよう

「次の電車は?」とAlexaに尋ねると、直近の時刻を知らせてくれる

開発開始

Alexa Developerにログインする

こちらからAlexa Developerにログイン。

ログイン画面

注意点

Amazonは日本版と米国版でアカウントが異なります。同じID/PWでも共有されないため、Alexa Developerは日本版のアカウントでログインしてください。間違えて米国版で開発すると、自分のAlexa端末でテストできなくなります。


スキル作成

  1. ログイン後、以下を選択。

  2. スキル作成ボタンを押下。

  3. スキル名とプライマリーロケールを設定(日本語)。

  4. エクスペリエンス等の設定。

    • エクスペリエンスタイプ:その他
    • モデル:カスタム
    • ホスティング:Alexa-hosted (Node.js)
    • ホスト地域:米国西部(オレゴン)
  5. テンプレートはデフォルトでOK。

  6. スキル作成を押下。


ちょっと触ってみる

呼び出し名の設定

ビルドタブで InvocationsSkill Invocation Name を "テスト" に編集。

※ 編集後は必ず "保存 → ビルド" すること。

テスト

  • テストタブでステータスを「開発中」に変更
  • Alexaシミュレータに「テスト」と入力して実行

応答:Welcome, you can say Hello or Help. Which would you like to try?

どこから呼ばれてる?

エディタタブで index.js を確認。

LaunchRequestHandler が呼ばれてるようです。これは以下で定義。

exports.handler = Alexa.SkillBuilders.custom()

実際に開発してみる

ビルドタブの編集

呼び出し名を「次の電車」に変更

Intentの編集

Interaction ModelIntents

HelloWorldIntent を編集(※新しく作っても反応しなかったため)。

GetTrainTimeIntent に名称を変更し、以下のようにサンプル発話を設定:

次の博多行き
博多行きの電車は
何時の電車がある
電車の時刻教えて
次の電車は

保存してビルド!


コードエディタの編集

  • LaunchRequestHandler にロジック追加
  • HelloWorldIntentHandler を削除
`index.js` のコード(クリックで開く)
// index.js
/* *
 * This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).
 * Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
 * session persistence, api calls, and more.
 * */
const Alexa = require('ask-sdk-core');
const trainTimes = require('./trainTimes');

// JST対応
function getNowJST() {
    const utc = new Date();
    const jst = new Date(utc.getTime() + (9 * 60 * 60 * 1000));
    return jst;
}

// 運行ダイヤの種類を取得
function getScheduleType(date) {
    const day = date.getDay(); // 0:日曜, 6:土曜
    if (day === 0) {
        return 'holiday'; // 日曜は祝日扱い
    } else if (day === 6) {
        return 'saturday';
    } else {
        return 'weekday';
    }
}

// 次の電車時刻を取得
function getNextTrainTime(date) {
    const nowMinutes = date.getHours() * 60 + date.getMinutes();
    const scheduleType = getScheduleType(date);
    const schedule = trainTimes[scheduleType];

    for (const timeStr of schedule) {
        const [hour, minute] = timeStr.split(':').map(Number);
        const trainMinutes = hour * 60 + minute;
        if (trainMinutes > nowMinutes) {
            return timeStr;
        }
    }

    return null;
}

const GetTrainTimeIntent = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
        handle(handlerInput) {
        const now = getNowJST();
        const scheduleType = getScheduleType(now);
        const nextTime = getNextTrainTime(now);

        let typeStr = {
            weekday: '平日',
            saturday: '土曜日',
            holiday: '日曜・祝日'
        }[scheduleType];

        let speakOutput = '';
        if (nextTime) {
            speakOutput = 今日は${typeStr}ダイヤです。次のホゲホゲ駅行きは、${nextTime} 発です。;
        } else {
            speakOutput = 今日は${typeStr}ダイヤですが、もう運行が終了しています。;
        }

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }

};


const HelpIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'You can say hello to me! How can I help?';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

const CancelAndStopIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
                || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
    },
    handle(handlerInput) {
        const speakOutput = 'Goodbye!';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};
/* *
 * FallbackIntent triggers when a customer says something that doesn’t map to any intents in your skill
 * It must also be defined in the language model (if the locale supports it)
 * This handler can be safely added but will be ingnored in locales that do not support it yet 
 * */
const FallbackIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.FallbackIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'Sorry, I don\'t know about that. Please try again.';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
/* *
 * SessionEndedRequest notifies that a session was ended. This handler will be triggered when a currently open 
 * session is closed for one of the following reasons: 1) The user says "exit" or "quit". 2) The user does not 
 * respond or says something that does not match an intent defined in your voice model. 3) An error occurs 
 * */
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
    },
    handle(handlerInput) {
        console.log(~~~~ Session ended: ${JSON.stringify(handlerInput.requestEnvelope)});
        // Any cleanup logic goes here.
        return handlerInput.responseBuilder.getResponse(); // notice we send an empty response
    }
};
/* *
 * The intent reflector is used for interaction model testing and debugging.
 * It will simply repeat the intent the user said. You can create custom handlers for your intents 
 * by defining them above, then also adding them to the request handler chain below 
 * */
const IntentReflectorHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest';
    },
    handle(handlerInput) {
        const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
        const speakOutput = You just triggered ${intentName};

        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};
/**
 * Generic error handling to capture any syntax or routing errors. If you receive an error
 * stating the request handler chain is not found, you have not implemented a handler for
 * the intent being invoked or included it in the skill builder below 
 * */
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        const speakOutput = 'Sorry, I had trouble doing what you asked. Please try again.';
        console.log(~~~~ Error handled: ${JSON.stringify(error)});

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

/**
 * This handler acts as the entry point for your skill, routing all request and response
 * payloads to the handlers above. Make sure any new handlers or interceptors you've
 * defined are included below. The order matters - they're processed top to bottom 
 * */
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        GetTrainTimeIntent,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        FallbackIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler)
    .addErrorHandlers(
        ErrorHandler)
    .withCustomUserAgent('sample/hello-world/v1.2')
    .lambda();



`trainTimes.js` を作成(クリックで開く)
// trainTimes.js
module.exports = {
    weekday: [
    "05:12", "05:32",
    "06:04", "06:21", "06:30", "06:41", "06:49",
    "07:00", "07:09", "07:15", "07:24", "07:29", "07:35", "07:40", "07:46", 
    "08:01", "08:06", "08:15", "08:25", "08:38", "08:52",
    "09:03", "09:17", "09:33", "09:42", "09:58",
    "10:13", "10:35", "10:50",
    "11:04", "11:18", "11:36", "11:49",
    "12:03", "12:18", "12:35", "12:49",
    "13:03", "13:18", "13:34", "13:49",
    "14:03", "14:18", "14:35", "14:49",
    "15:03", "15:18", "15:35", "15:49",
    "16:06", "16:20", "16:37", "16:55",
    "17:08", "17:19", "17:42", "17:59",
    "18:16", "18:28", "18:44",
    "19:03", "19:19", "19:39", "19:54",
    "20:02", "20:15", "20:30", "20:48",
    "21:09", "21:29", "21:48",
    "22:04", "22:23", "22:46",
    "23:15"
  ],
    saturday: [
        "05:12", "05:32", "05:58",
        "06:25", "06:46", "06:59",
        "07:15", "07:30", "07:51",
        "08:01", "08:13", "08:26", "08:44",
        "09:01", "09:13", "09:25", "09:46", "09:58",
        "10:13", "10:32", "10:50",
        "11:06", "11:20", "11:36", "11:49",
        "12:06", "12:20", "12:36", "12:51",
        "13:07", "13:20", "13:38", "13:54",
        "14:07", "14:20", "14:35", "14:51",
        "15:08", "15:25", "15:38", "15:46", "15:57",
        "16:19", "16:29", "16:45",
        "17:04", "17:19", "17:34", "17:51",
        "18:06", "18:18", "18:34", "18:48",
        "19:08", "19:18", "19:36", "19:51",
        "20:03", "20:17", "20:34", "20:47", "20:58",
        "21:18", "21:37", "21:57",
        "22:20", "22:49",
        "23:15"
    ],
    holiday: [
    "05:12", "05:42", "05:58",
    "06:25", "06:46", "06:59",
    "07:15", "07:40", "07:51",
    "08:01", "08:13", "08:26", "08:44",
    "09:01", "09:13", "09:25", "09:46", "09:58",
    "10:15", "10:42", "10:50",
    "11:06", "11:20", "11:36", "11:49",
    "12:01", "12:20", "12:36", "12:51",
    "13:07", "13:21", "13:38", "13:54",
    "14:07", "14:21", "14:35", "14:51",
    "15:08", "15:21", "15:38", "15:46", "15:57",
    "16:19", "16:21", "16:45",
    "17:04", "17:19", "17:34", "17:51",
    "18:06", "18:18", "18:34", "18:48",
    "19:08", "19:11", "19:36", "19:51",
    "20:03", "20:17", "20:34", "20:47", "20:58",
    "21:18", "21:37", "21:57",
    "22:22", "22:49",
    "23:15"
  ]
};


アプリ側で確認する

スマホのalesaアプリで以下のように進んで開発したスキルが確認ができればデバイスで使えるようになってると思います!


まとめ

今回は「次の電車の時間をAlexaが教えてくれるスキル」を作ってみました。

  • 地方在住だと電車の本数が少なくて、次が何分後かを一瞬で知りたい
  • Alexaに聞くだけでわかるのは地味に便利
  • Alexaスキル開発は初めてでもとっつきやすい!

この記事が、地方民やAlexaスキル開発を始めたい方の参考になれば嬉しいです。

Discussion