2019年10月24日木曜日

実験5日目

問題とその解決策


間違った応答による誤操作


「就寝前の薬を飲んでください」に対して間違って「夜のお薬飲んだよ」と答えてしまい、夜の予定が「(済)」に変わってしまった。これは実験2日目に起きたのと同じインシデント。

対応策


WebAPI (A2)のNotifyNotTaking()関数でカレンダーのタイトルと色を変更するが、ここで現在時刻が開始時刻(start_time)~終了時刻(end_time)の間にあるかどうか調べて、もしなければ時間外である旨応答する。

ここで、GASからどうやって Google Home に喋らせるかが問題になる。
Google Homeにプッシュ通知する製品にGHKitというものがあるらしい。「外部イベントに応じたプッシュ通知で、好みのメッセージをタイミングよくGoogle Homeで音声再生します!」という触れ込みである。
また、GAS、firebase、google-home-notifierを連携させて指定した時刻になるとGoogle Spreadsheetに入力したテキストをGoogle Homeに喋らせるアプリの開発例がある。

我々の開発しているシステムは、firebaseは使っていないが、どちらかというと後者に近い。ベアボーン上で稼働している発話PUSHサーバ(example.js)をngrokでインターネットから利用可能なWebAPIにして、GASから発話依頼するのが最も簡単な方法だろう。
手順はこうである。
  1. WebAPI (A2) はIFTTTからのカレンダー更新要求(Webhooksを使ってWebAPI を叩く)を待ち受ける。
  2. IFTTTからカレンダー更新要求が届いたら、現在時刻と開始時刻(start_time)~終了時刻(end_time)を比較し、時間外であればベアボーン上で稼働している発話PUSHサーバ(example.js)へ発話依頼する。

問題点


ここで問題が発生した。example.js起動時に出力されたngrokが払い出す公開URLの期限が8時間に制限されていることが分かった
これを回避するには、ngrokのサイトからアクセストークンを取得すればよい。そして発話PUSHサーバ(~/pill-reminder/example.js)のHTTPリクエストをListenするプログラムで下記のようにアクセストークンを指定してngrok.connect()関数を実行する。
/**
 * serverPortをlisten
 *
 */
app.listen(serverPort, function () {
  // トークンを設定
  ngrok.authtoken(token);
  // 現状のセッションを一旦終了
  ngrok.kill();
 
  ngrok.connect({authtoken: token, addr: serverPort, region: 'jp'}, function (err, url) { //修正
    console.log('Endpoints:');
    console.log('    http://' + ip + ':' + serverPort + '/google-home-notifier');
    console.log('    ' + url + '/google-home-notifier');
    console.log('GET example:');
    console.log('curl -X GET ' + url + '/google-home-notifier?text=Hello+Google+Home');
    console.log('POST example:');
    console.log('curl -X POST -d "text=Hello Google Home" ' + url + '/google-home-notifier');
  });
})
リスト1.HTTPリクエストのlisten部分


ここで、tokenはngrokのサイトから取得したAuthtokenである。

※リスト1はngrokのconnectメソッドを利用して公開URLを払い出している。これはv2.3.0の使用方法である。
一方,ngrokを非同期で起動して公開URLを払い出すサンプルがネット上にいくつかある。これはv3.2.5の書き方である。それに従ってリスト1を書き換えたものを以下に示す。

app.listen(serverPort, function () {
 putLog('Endpoints:');
 putLog('    http://' + ip + ':' + serverPort + '/google-home-notifier');
   
 connectNgrok(serverPort).then(url => {
  putLog('    ' + url + '/google-home-notifier');
  putLog('GET example:');
  putLog('curl -X GET ' + url + '/google-home-notifier?text=Hello+Google+Home');
  putLog('POST example:');
  putLog('curl -X POST -d "text=Hello Google Home" ' + url + '/google-home-notifier');
 });
})

/** 
 * ngrokを非同期で起動
 */
async function connectNgrok(port) {
 try {
  let url = await ngrok.connect({
   addr: port,
   region: 'jp', 
   configPath: '~/.ngrok2/ngrok.yml',
   authtoken: token
  });
  return url;
 } catch (err) {
  putLog(err.name + ': ' + err.message);
 }
}
リスト1b.

最初、ngrokのurl払い出しがなかなかうまくいかなかった。試行錯誤しているうち、ngrok.connectメソッドのパラメタに
region: 'jp',
configPath: '~/.ngrok2/ngrok.yml',
を追加すると払い出しができるようになった。regionに'jp'(日本)を設定したのが功を奏したのではないだろうか。近くのサーバを使用することでレスポンスが改善され、ngrok.connectが正しく動作したものと思われる。

ここで教訓になったことは、ngrok v2.3.0とv3.5.0では公開URLの払い出し方法が異なるということだ。

次にWebAPI (A2) を以下のように修正する。
/**
 * 「[朝|昼|夜|就寝前]のお薬飲んだよ」に対して服用済みに変えるファンクション
 *
 * @param  {String}   timing     [朝|昼|夜|就寝前]
 *
 */
function NotifyNotTaking(timing) {
  //var timing = e.parameter.timing;
  //「の薬」とつくイベントの取得  
  //var timing =e.parameter.timing;
  debug('NotifyNotTaking: timing=' + timing);
  
  // 当日の予定の中から timing (朝|昼|夜|就寝前)を含む予定を検索
  var events = CalendarApp.getDefaultCalendar().getEventsForDay(
    new Date(),
    {search: timing}
    // {search:'夜の食後の薬'}
  );  

  // 取得した予定からターゲットの予定を探してタイトルと色を変更
  events.forEach(function(event,i,array){ 
    var title = event.getTitle();
    debug('NotifyNotTaking: title=' + title); 
    if(title.match(/^.の食.の薬\(.+\)$/) || title.match(/就寝前の薬\(.+\)$/)) {
      // 現在時刻と開始時刻(start_time)~終了時刻(end_time)を比較し、時間内であればタイトルを(済)にし、色を変え、LINEへ通知する
      if (withinTime(event.getStartTime(), event.getEndTime())) {
        // タイトルの変更
        // ”(”の文字の位置を取得する(http://cya.sakura.ne.jp/js/string.htm#indexOf)
        result = event.getTitle().indexOf("(");
        debug('NotifyNotTaking: result=' + result);
        // "("以降の文字を切り取る。
        results = event.getTitle().substr(0,result);
        debug('NotifyNotTaking: results=' + results);
        // タイトルを○○の薬(済)に変更する。  
        event.setTitle(results+'(済)');
        // 色を青に変更する。
        event.setColor(CalendarApp.EventColor.BLUE);
        //LINEに送るメッセージ
        LINE_notify(timing+'のお薬を飲みました。');
      } else {
        // 現在時刻と開始時刻(start_time)~終了時刻(end_time)を比較し、時間外であればベアボーン上で稼働している発話PUSHサーバ(example.js)へ発話依頼する。
        var message = '時間外に「' + timing + 'のお薬をのみました」とのメッセージを受け取りました。この要求を無視します。';
        debug('NotifyNotTaking: ' + message);
        LINE_notify(message);
      }
    }
  });
};

/**
 * 現在時刻が開始時刻(start_time)~終了時刻(end_time)の間にあるかどうかをチェックする
 * @param  {Date}   start_time     日付
 * @param  {Date}   end_time       日付
 * @return {Boolean}               true:現在時刻が開始時刻(start_time)~終了時刻(end_time)の間にある
 *                                 false: 〃 間にない
 */
function withinTime(start_time, end_time) {
  var current_time = new Date();
  var result = (start_time <= current_time && current_time <= end_time) ? true : false;
  return result;
}

// google-home-notifyにメッセージを送るファンクション
function GOOGLE_HOME_notify(message){
  var options =
  {
    "method"  : "post",
    "payload" : "text=" + message
  };

  // ngrokで払い出された公開用URLxxxxxxxxを書き直す
  UrlFetchApp.fetch("https://xxxxxxxx.ap.ngrok.io/google-home-notifier",options);
}
リスト2.WebAPI (A2) の修正

関数NotifyNotTaking()では、現在時刻が服用開始時刻~終了時刻の間かどうかwithinTime()関数を用いてチェックして、もし外れていたらGOOGLE_HOME_Notify()関数を使って発話PUSHサーバへ発話要求を行う。

 GOOGLE_HOME_Notify()のUrlFetchApp.fetchメソッドのurlはベアボーン上で稼働する発話PUSHサーバのurlである。これはngrokによって払い出されたものを使う。したがって,発話PUSHサーバが異なれば,当然このurlも書き換えなければならない。

0 件のコメント:

コメントを投稿