Alexa に次のバスが何分後に来るかしゃべらせるスキルを作った

いつもナビタイムの「次のバス」スキルにお世話になっていました。

これは「最寄りのバス停を登録しておくと "次のバスのスキルを開いて" というだけで次のバスが何分後に来るか、さらにその次のバスは何時何分か」を教えてくれる便利なスキルです。

ところが、このスキルが今月末にサービス終了するようです。

これは非常に困るので自分で Alexa スキルを作ります。

Alexa を開発できるようにするための事前準備は前回の記事をご参照ください。

talkeyboid.com

コード書くぞ!

まずは結論から。以下のコードで実現できました。※時刻表の値はダミーです。

# -*- coding: utf-8 -*-
import logging
import ask_sdk_core.utils as ask_utils

from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_core.dispatch_components import AbstractExceptionHandler
from ask_sdk_core.handler_input import HandlerInput

from ask_sdk_model import Response

# 日付時刻系インポート
from datetime import date, datetime, timezone, timedelta
import jpholiday
import math

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 時刻表マスタ ※祝日はsunday扱い
def get_today_table(weekday_en, year, month, day):
    master_timetable = {
        "weekday": [
            datetime(year, month, day, 6, 5, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 23, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 48, 00, tzinfo=timezone(timedelta(hours=9))),
            ... # 以下略
            ],
        "saturday": [
            datetime(year, month, day, 6, 5, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 45, 00, tzinfo=timezone(timedelta(hours=9))),
            ... # 以下略
            ],
        "sunday": [
            datetime(year, month, day, 6, 5, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 45, 00, tzinfo=timezone(timedelta(hours=9))),
            ... # 以下略
            ]
    }
    return master_timetable[weekday_en];

class LaunchRequestHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return ask_utils.is_request_type("LaunchRequest")(handler_input)

    def handle(self, handler_input):
        # 日時取得
        timezone_tokyo = timezone(timedelta(hours=9))
        datetime_now_jst = datetime.now(timezone_tokyo)
        
        year = datetime_now_jst.year
        month = datetime_now_jst.month
        day = datetime_now_jst.day
        
        # 曜日判定:月曜0-日曜6
        weekday = date(year, month, day).weekday()
        
        weekday_en = ""
        if weekday == 5:
            weekday_en = "saturday"
        elif weekday == 6:
            weekday_en = "sunday"
        else:
            weekday_en = "weekday"
        
        # 祝日判定
        if jpholiday.is_holiday(date(year, month, day)):
            weekday_en = "sunday"
        
        # 時刻表取得
        timetable = get_today_table(weekday_en, year, month, day)
        
        # 次とその次のバスを取得
        next_bus = []
        for item in timetable:
            if datetime_now_jst < item:
                next_bus.append(item)
        
        next_bus = next_bus[:2]
        
        # リストのサイズで終バス判断し返却値を出し分ける
        if len(next_bus) == 0:
            speak_output = "もう今日のバスはありません。"
        elif len(next_bus) == 1:
            diff = next_bus[0] - datetime_now_jst
            minute_after = math.floor(diff.total_seconds()/60)
            next_bus_hour = next_bus[0].hour
            next_bus_minute = next_bus[0].minute
            speak_output = f"次のバスは{minute_after}分後の{next_bus_hour}時{next_bus_minute}分です。このバスが最終です。"
        else:
            diff_1 = next_bus[0] - datetime_now_jst
            minute_after_1 = math.floor(diff_1.total_seconds()/60)
            diff_2 = next_bus[1] - datetime_now_jst
            minute_after_2 = math.floor(diff_2.total_seconds()/60)
            next_bus_hour = next_bus[1].hour
            next_bus_minute = next_bus[1].minute
            speak_output = f"次のバスは{minute_after_1}分後です。その次は{minute_after_2}分後の{next_bus_hour}時{next_bus_minute}分です。"
        
        return (
            handler_input.response_builder
                .speak(speak_output)
                # .ask(speak_output) # これがあるとセッションが維持される
                .set_should_end_session(True) # 1回しゃべったらセッション切る
                .response
        )


sb = SkillBuilder()
sb.add_request_handler(LaunchRequestHandler())

lambda_handler = sb.lambda_handler()

めちゃシンプルなので特に解説することもないかと思いますが、どうせ忘れるので自分自身のメモとして残しておきます。

import

今回は祝日判定のために jpholiday を利用するので requirements.txt に追記します。

boto3==1.9.216
ask-sdk-core==1.11.0
jpholiday # 追記

github.com

時刻計算のために datetimetimedelta を分に換算するために math をインポートします。

from datetime import date, datetime, timezone, timedelta
import jpholiday
import math

時刻表マスタ

時刻表はハードコーディングします。自分だけで使うしバス停も1つなのでこっちの方が圧倒的保守性に優れてるためです。

DynamoDB 使うならパーティションキーに バス会社名-路線名-バス停名 、ソートキーに 時刻 がよいでしょうか。

それなら範囲検索できますしおすし(知らんけど)。

def get_today_table(weekday_en, year, month, day):
    master_timetable = {
        "weekday": [
            datetime(year, month, day, 6, 5, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 23, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 48, 00, tzinfo=timezone(timedelta(hours=9))),
            ... # 以下略
            ],
        "saturday": [
            datetime(year, month, day, 6, 5, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 45, 00, tzinfo=timezone(timedelta(hours=9))),
            ... # 以下略
            ],
        "sunday": [
            datetime(year, month, day, 6, 5, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 45, 00, tzinfo=timezone(timedelta(hours=9))),
            ... # 以下略
            ]
    }
    return master_timetable[weekday_en];

「あと何分後」を計算するために時刻差を取得する必要がありますが、バス時刻表って日付ごとではなく曜日ごとです。

そのため year, month, day を指定することで "その日" の時刻表を取得できるようにしています。

weekday_en によって曜日ごとの時刻表を出し分けています。

処理部分

LaunchRequestHandler の中身は普通にごり押しで実装してるだけです。

前回、エラーをキャッチしてしまう問題は .ask(speak_output) していたためでした。これをするとセッションが維持されるっぽいので外しています。

また、Alexa を放っておいてもスキルが勝手に終了せず2回しゃべってしまう問題はAlexa が一回しゃべったらすぐにセッションを切る設定(.set_should_end_session(True))をすることで解決しました。

あと、時刻表リストを全部ぶん回して最後に silce しててあんまりかっこよくはないんですが、時刻表なんか高々数十くらいのオーダーなんでこれでいいです。

動かすぞ!

動きました。※バス時刻はダミーです。

www.youtube.com

とりあえず動いてよかったです。

ベータ版テストでしか動かせてないので、次はちゃんとプライベートで公開できるようにしたいですね(Alexa for Business めんどくさそうなのでベータテストを3カ月に1回更新するでもいいかも???)。