Google Apps Scriptと連携してAmazon Alexaに次のバスの時間を教えてもらう

公開日:2019/09/28 更新日:2019/09/28
Google Apps Scriptと連携してAmazon Alexaに次のバスの時間を教えてもらうのサムネイル

はじめに

最近Amazon Echo dotを購入して使っていますが、思っていたよりも便利な上、Amazon Alexa(以降、Alexa)のスキル開発が楽しいです。この記事では、ユーザーの呼びかけに応答してAlexaからGoogle Apps Scriptで公開しているスクリプトにPOSTリクエストを送信し、その応答をもとにユーザーに返答させる手順をまとめます。実際の例として、Alexaに直近のバス時間を教えてもらうシステムを構築しました。

構築するシステムのイメージ

以下のようなイメージになります。実際には何でも良いですが、ここでは例としてユーザーが「アレクサ、マイバスで東京のバスを教えて」とAlexaに喋りかけると、現在時刻に基づいて直近2本分のバスの時間をAlexaが回答してくれるシステムを構築します。なお、「マイバス」というのは、バス時間を返してくれる機能の独自に設定した呼び名です。Alexaでは独自機能を使用する際には、その機能に名前を付与してそれを呼びかけて使用します。

alexa-system-image-cut.png

上記は、ユーザからの問いかけに対して、指定したAPIやサービスに対してAlexaからPOSTリクエストを送信し、その回答結果に基づいてAlexaがユーザに回答するシステムになります。これは色々なAPIと組み合わせればかなり多くのことに応用できると思います。

前提と環境

Alexaでは、任意の呼びかけに対して任意の応答をさせるユーザ独自の機能を「Alexaスキル」と読んでいます。 開発するAlexaスキルの実体となるコードはAlexaホスト(AWS Lamda上)か、各自任意のホスト場所を指定できます。この記事ではAlexaホストを使用する前提とします。Alexaホストを使用することで、すべてWebブラウザ上で開発できます。具体的には、alexa developer conosleのコードエディタ上でコードも記述できます。

この記事では、Alexaスキルの最低限の設定は完了しているとし、AlexaとGoogle Apps Scriptそれぞれのコードのみを記載します。Alexaスキルの開発に必要な手順を順番にまとめようと考えていましたが、以下の公式チュートリアルがとても充実していて遥かにわかりやすいためそちらをご参照ください。特に、最初はインテント、スロットという用語の意味が分からずに全体的な理解が進みませんでしたが、これらについても例を用いて説明されておりわかりやすかったです。

developer.amazon.com

ブログで基礎から学んでみよう

Alexaスキルの作成と設定は上記に記載した公式チュートリアルを通して済んでいる前提とし、以降では、ユーザの呼びかけに対してAlexaからGoogle Apps ScriptへPOSTリクエストを送信して結果を受信し、受信した結果をユーザに回答するためのコード(Alexa側)と、AlexaからのPOSTリクエストを受信して結果を返すコード(Google Apps Script側)についてのみ説明します。

Alexaスキルの設定内容について

以降で説明するコードでは、Alexaスキルの設定として以下を前提としています。

項目 内容
呼び出し名 マイバス
インテント callBusTime
スロット Destination
スロットタイプ スロットタイプ名をLIST_OF_DEST、リストを東京、大阪、名古屋の3つ
上記のインテント名、スロット名が以降のAlexaのコードの中で使用するので対応を見てみてください。

Google Apps ScriptにPOSTリクエストを送信するコードをAlexaに実装する

axiosをAlexaで使えるようにする

事前準備として、AlexaからPOSTリクエストを実行するために必要なaxiosを使えるようにしておきます。alexa developer consoleのコードエディタ上でライブラリを追加したい場合は、package.jsonにライブラリ名とバージョンをdependenciesに追記します。具体的には、axiosの場合は以下のようにします。"axios": "^0.19.0"を追記しています。

package.json
{
  "name": "hello-world",
  "version": "1.1.0",
  "description": "alexa utility for quickly building skills",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Amazon Alexa",
  "license": "ISC",
  "dependencies": {
    "ask-sdk-core": "^2.6.0",
    "ask-sdk-model": "^1.18.0",
    "aws-sdk": "^2.326.0",
    "axios": "^0.19.0"
  }
}

Google Apps ScriptにPOSTリクエストを送信するコード

ユーザの呼びかけに含まれる変数(Alexaスキルでは、スロットと呼びます。ここの例では、バス時間を知りたい行き先名)を取り出して、何かしらのAPIサービス(ここではGoogle Apps Script)にPOSTリクエストを送信してその応答結果に基づいてAlexaに喋らせるコードを以下に載せます。以下は、「マイバスで東京のバスを教えて」というユーザの呼びかけから「東京」という情報を取り出して、Google Apps Scriptに対して「東京」という情報を含めてPOSTリクエストを送信します。Google Apps Scriptでは、「東京」に基づいてバス時間結果を返します。ユーザの呼びかけにどのようなスロットを含めるかはユーザ側で自由に設定できます。

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 Axios = require('axios'); // axios使用のために追記

// Goolge Apps Scriptで公開しているスクリプト。POSTリクエストの送信先URL
const API_URL = 'https://script.google.com/macros/s/slslpiLko84kAkelKolAuAMioiwjeptijaw/exec';

// (...途中省略...)

// バス時間をAPI(Google Apps Script)に問い合わせて結果をユーザに回答するためのハンドラ
const MyBusIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'callBusTime'; // 作成したインテント名
    },

    async handle(handlerInput) {
        let speechText;
        // 以下のようにhandlerInput.requestEnvelope.request.intent.slots.スロット名 でスロット値を取得できる
        const destination = handlerInput.requestEnvelope.request.intent.slots.Destination; // 作成したスロット名

        if (destination) {
            try {
                await Axios.post(API_URL,{
                    dest: destination // Google Apps Scriptに送信するデータ
                }).then(function(res) {
                if(!res.data.error) {
                  const nextTime = res.data.nextbus;
                  const nextnextTime = res.data.afternextbus;
                  speechText = '次のバスの時間は' + nextTime + 'です。その次のバス時間は' + nextnextTime + 'です。'
                } else {
                  speechText = 'エラーが発生しました。' + res.data.error.message
                }
                
                }).catch(function(error) {
                   speechText ='不明なエラーです。';
                });
                return handlerInput.responseBuilder
                    .speak(speechText)
                    .getResponse();
            } catch (error) {
                return handlerInput.responseBuilder
                    .speak('APIのリクエストに失敗しました。')
                    .getResponse();
            }

        } else {
            throw new Error('不明なスロット値です。.');
        }
    }
};
// (...以降省略...)

// The SkillBuilder 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(
        LaunchRequestHandler,
        MyBusIntentHandler, // バス時間をAPI(Google Apps Script)に問い合わせて結果をユーザに回答するためのハンドラ
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler, 
    )
    .addErrorHandlers(
        ErrorHandler,
    )
    .lambda();

上記のコードは、Alexaデフォルトのコードから変更部分だけを載せています。

AlexaからのPOSTリクエストに応答するGoogle Apps Script

Google Apps Scriptで、スプレッドシートに記載した時刻表と現在時刻を元に直近2つのバス到着予定時刻を返すAPIを作成します。具体的には、以下のような時刻表を持つスプレッドシートを「東京」、「大阪」、「名古屋」という名前で用意します。以下のバス時刻表は、平日、土曜日、日曜日で列が別れています。

bustime-table.png

ここでは、目的地に応じていくつかシートを用意しており、それぞれにバス時刻表を持たせています。理由は、例えば「アレクサ、マイバスを開いて東京へのバスを教えて」と呼びかけた時は、「東京」というシートを参照するようにし、「アレクサ、マイバスを開いて大阪へのバスを教えて」と呼びかけた時は、「大阪」というシートを参照するようにすることで、目的地に応じた時刻表の時間を教えてもらえるようにするためです。

上記を踏まえ、Google Apps Scriptのコードは以下のようにしました。とりあえず動けばという程度なので色々と脆いままですがご了承ください。

function doPost(e) {
  var destList = ["東京","大阪", "名古屋"]; // Alexa側で登録するスロットリストと合わせる
  var params = JSON.parse(e.postData.getDataAsString()); // POSTされたデータを取得
  var destination = params.dest.value;  // Alexaで指定したPOSTデータを取得
  var result = {};
     
  var output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);  
  
  // Alexaから送信された目的地名を持つシートの存在確認
  if (destList.indexOf(destination) >= 0){
   
    try{
      // 現在時刻から直近の2本のバス時刻を取得する適当な関数
      returnNextBusDate(destination, result);    
    } catch(e) {
      addLog("error :" + e);
    }
  } else {
    result = {
   "error": {
       "code": "invalid_destination",
       "message": "目的地が見つかりません。"
     }
   };
  }
  
  output.setContent(JSON.stringify(result));
  // リクエスト元(Alexa)に返す
  return output;
}
  
function returnNextBusDate(sheetname, busresult) {
// Alexaから受信した目的地(上記、destinationに格納)と一致するシート名を持つシートを取得
  var spreadsheetId = "3i9of2oi42o8il2j92odjlkmlijeroilwHY";
  var spreadsheet = SpreadsheetApp.openById(spreadsheetId);
      var sheet = spreadsheet.getSheetByName(sheetname);

       // 現在時刻
       var nd = new Date();
       var hourNow = nd.getHours(); // 時
       var minutesNow = nd.getMinutes(); // 分
       var dayNow = nd.getDay(); // 曜日

       // 列インデックス
       var colIndex = 1;

       var tempDate = new Date();
       var lastRow = sheet.getLastRow();

       var nextBusDate = new Date();      // 次バス時刻
       var afternextBusDate = new Date(); // 次々バス時刻

       // 日曜日
       if(dayNow === 0) {
         colIndex = 5 // E列

       // 土曜日  
       } else if ( dayNow === 6) { 
         colIndex = 3 // C列

       // 平日  
       } else if ( dayNow > 0 && dayNow < 6) { // dailyday
         colIndex = 1 // A列

       } else { // error 
       }

      for(var i = 2; i <= lastRow; i++) {
        // バス時刻表の上から順番に時間を取得
        tempDate.setHours(sheet.getRange(i, colIndex).getValue());
        tempDate.setMinutes(sheet.getRange(i, colIndex + 1).getValue());
          
        // valueOfで時間を数値に換算して比較
        if( nd.valueOf() < tempDate.valueOf() ) {
            nextBusDate = tempDate;
            afternextBusDate.setHours(sheet.getRange(i+1,colIndex).getValue());
            afternextBusDate.setMinutes(sheet.getRange(i+1,colIndex + 1).getValue());
            break;
         }
       }
       
       busresult["nextbus"] = Utilities.formatDate( nextBusDate, 'Asia/Tokyo', 'hh:mm');
       busresult["afternextbus"] = Utilities.formatDate( afternextBusDate, 'Asia/Tokyo', 'hh:mm');
       
    return busresult;
  }
  
  function addLog(text) {
  var spreadsheetId = "3i9of2oi42o8il2j92odjlkmlijeroilwHY";
  var sheetName = "log";
  var spreadsheet = SpreadsheetApp.openById(spreadsheetId);
  var sheet = spreadsheet.getSheetByName(sheetName);
  sheet.appendRow([new Date(),text]);
}

上記のGoogle Apps Scriptは、匿名ユーザでもアクセスできるよう公開しておく必要があります。リスクも考慮の上、重要な情報を返さないようご注意ください。Google Apps Scriptの公開方法については以下にまとめています。

www.virment.com

Google Apps Scriptで作成したコードをウェブアプリケーションとして公開する手順をメモします。 

動作確認方法

AlexaホストでAlexaスキルを開発すると、動作確認もブラウザでalexa developer consoleにアクセスして簡単に実行できます。これらの一連の手順についても冒頭に載せた公式チュートリアルに記載されています。

まとめ

最初はAlexaスキル独自の用語がいくつかあり戸惑うかもしれませんが、公式のチュートリアルが充実していてわかりやすいので、まずはこれをゆっくり読んでみるのがおすすめです。AlexaからAPIへのPOSTリクエスト送信ができることで色々な応用に使えると思います。

開発アプリ

nanolog.app

毎日の小さな出来事をなんでも記録して、ログとして残すためのライフログアプリです。