ベアボーンサーバ

ベアボーンサーバの機能


図1に示すように、ベアボーンはWebAPI A3を使って当日の処方情報(朝~就寝)を取得する。その後、ベアボーンは5分後に確認通知が届くようにタイマー設定を行う。確認通知が届いたら、WebAPI A3を用いて該当する薬が服用済みかどうか確認する。このとき催促カウンタをカウントアップしてWebAPI A3の引数にセットする。まだ服用していなければ発話サーバに催促メッセージの要求を行う。服用済みであれば何もしない。催促メッセージの要求を受けた発話サーバは、Google Homeに催促メッセージを送る(WebAPI A5)。この催促を5回繰り返しても服用済みにならなければGoogleカレンダの該当する服薬予定のタイトルを「応答なし」、色を黄色に変更する。

図1.シーケンス図

インターフェース


WebAPI A3 のインターフェースはGETメソッドを使用する。

当日の服薬予定を取得する場合

リクエスト:パラメタなし
レスポンス:
{
 "drug_notifies": [
  {
   "title": "朝の食後の薬(未)",
   "description": "クラビット細粒10% 100mg(レボフロキサシンとして) 3g",
   "id": "4unup4q0nvecekung915hhaq68@google.com",
   "location": "0",
   "startTime": "2019-10-11T08:36:00.000Z",
   "endTime": "2019-10-11T08:39:00.000Z",
   "color": "11"
  },
  {
   "title": "夜の食後の薬(未)",
   "description": "クラビット細粒10% 100mg(レボフロキサシンとして) 3g",
   "id": "0s826bi4tecvamgadmklj9faod@google.com",
   "location": "0",
   "startTime": "2019-10-11T10:00:00.000Z",
   "endTime": "2019-10-11T10:03:00.000Z",
   "color": "11"
  },
  {
   "title": "昼の食後の薬(未)",
   "description": "クラビット細粒10% 100mg(レボフロキサシンとして) 3g",
   "id": "2tjms6utgpmqi07nma49v4tt0e@google.com",
   "location": "0",
   "startTime": "2019-10-11T11:00:00.000Z",
   "endTime": "2019-10-11T11:03:00.000Z",
   "color": "11"
  }
 ],
 "num_reminder": "0",
 "taiking_status": "0",
 "status": "0"
}
リスト1.レスポンスJSONデータ(全件要求の場合)


指定した服薬予定を取得する場合

リクエスト:?id=[イベントid]&num_reminder=[催促回数]
レスポンス:
{
 "drug_notifies": [
  {
   "title": "夜の食後の薬(応答無)",
   "description": "クラビット細粒10% 100mg(レボフロキサシンとして) 3g",
   "id": "5ihpjn5979l6pt0u8v6molgfvc@google.com",
   "location": "0",
   "startTime": "2019-10-12T22:15:00.000Z",
   "endTime": "2019-10-12T22:20:00.000Z",
   "color": "5"
  }
 ],
 "num_reminder": "11",
 "taiking_status": "0",
 "status": "0"
}
リスト2.レスポンスJSONデータ(指定した服薬予定を要求する場合)

プログラム


プログラム名

~/pill-reminder/pill-reminder.js

メインプログラム

var os = require('os');

// 発話PUSHサーバのIPアドレス取得(発話PUSHサーバは本プログラムと同じサーバ上で稼働していると仮定している)
var localAddress = getLocalAddress();
var ip = localAddress.ipv4[0].address;

const C_INTERVAL = 5 * 60 * 1000;

var api4 = 'http://' + ip + ':8091/google-home-notifier';
var api3 = 'https://script.google.com/macros/s/AKfycb...0nHMRJ/exec'; // WebAPI(A3) カレンダーの服薬予定取得(pill.kumw)

var webclient = require("request");

putLog("pill-reminder start");

/**
*
* WebAPI(A3)を叩いて本日のスケジュールを取得
*
*/
webclient.get(
 {
  url: api3,
 }, 
 function (error, response, body) {
  if(!error) {
   var json = JSON.parse(body);
   putLog('本日の服薬スケジュール:\n' + JSON.stringify(json, null, " "));
   for ( var i= 0; i < json.drug_notifies.length; i++ ){
    putLog('startTime=' + json.drug_notifies[i].startTime + ', title=' + json.drug_notifies[i].title + ', id=' + json.drug_notifies[i].id);
    var startTime = new Date(json.drug_notifies[i].startTime);
    var now = new Date();
    var df = startTime - now;
    if (df > 0) {
     putLog('この予定は' + parseInt(df / 60000) + '分後に発話されます。');
     var speech = getSpeech(json.drug_notifies[i].title);
     setTimeout(dosing_remind, df, speech, 1, json.drug_notifies[i].id );
    } else {
     putLog('この予定はすでに終了しているので発話されません。');
    }
   }
  } else {
   putLog('WebAPI A3 呼び出しでエラーが発生しました(当日分):' + error);
  }
 }
);
リスト3.メインプログラム (pill-reminder.js)

起動するとすぐにWebAPI A3を使って当日の予定全てを取得する。取得した予定の開始時間になったら関数dosing_remindを起動するようにsetTimeout関数で設定する。このとき発話メッセージ(speech)、服薬予定を識別するid(json.drug_notifies[i].id)を引数に設定する。

なお、このプログラムは最後の予定の催促が終了すると停止するようになっている。そこで、crontabを使って毎日深夜にプログラムを起動して、その日の処理を行うようにした。

予定の時刻になったときに呼び出される関数(dosing_remind)

/**
*
* 時間がきたときの処理
*
*/
function dosing_remind(speech, counter, id) {
 putLog('"' + speech + '"の発話時刻になりました(' + counter + '回目), id=' + id);
 if (counter > 10) {
  putLog('催促回数が上限を超えました');
  return;
 }

 // WebAPI A3 をid指定で叩いて服用済みかどうかを確認する
 webclient.get(
  {
   url: api3 + "?id=" + id + "&num_reminder=" + counter
  }, 
  function (error, response, body) {
   if(!error) {
    var json = JSON.parse(body);
    putLog('当該服薬スケジュール(id=' + id + '):\n' + JSON.stringify(json, null, " "));
    if(json.taiking_status == "0" && !isOver(json)){
     message = speech + 'を飲んでください';
     putLog(message + '(' + counter + '回目)');
     // まだ開始時刻(json.drug_notifies[0].startTime)になっていなければ、開始時刻にタイマーを設定する
     var startTime = new Date(json.drug_notifies[0].startTime);
     var now = new Date();
     var df = startTime - now;
     if (df > 0) {
      putLog('予定が変更されています。この予定は' + parseInt(df / 60000) + '分後に発話されます。');
      setTimeout(dosing_remind, df, speech, counter, id);//繰り返し
     } else {
      // Google homeへ発話依頼(開始時刻を過ぎている場合)
      counter++;
      setTimeout(dosing_remind, C_INTERVAL, speech, counter, id);//繰り返し
      webclient.get({
       url: api4,
       qs: {
        text: message,
       }
      },
      function (error, response, body) {
       if(!error) {
        putLog('発話依頼成功:' + body);
       } else {
        putLog('発話依頼失敗:' + error);
       }
      });
     }
    } else {
     putLog('済になったか催促上限オーバーになったので催促を終了します。');
    }

   } else {
    putLog('WebAPI A3 呼び出しに失敗しました:' + error + '(id=' + id + ')');
   }
  }
 );
}
リスト4.予定の開始時間になったら呼び出される関数dosing_remind 

予定の時刻になったらこの関数が呼び出される。引数には催促メッセージ speech と催促回数 counter 、そして服薬予定idが設定されてくる。まず、催促回数が10回を超えていたら何もしないで関数を抜ける。そうでなければ、WebAPI A3 (api3) を使って当該idの服薬予定(リスト2)を読み込む。まだ薬を飲んでいない(json.taiking_status == "0")かまたは終了時刻に達していない(!isOver(json))場合、まだ服薬開始時刻になっていなかったらその時刻に自分自身(dosing_remind)が呼び出されるようにタイマーをセットし、開始時刻を経過していたらWebAPI A4 (api4) を用いて発話サーバに発話をリクエストする。その際、C_INTERVALに設定された時間が経過した後に、催促回数をカウントアップして自分自身(dosing_remind)が呼び出されるようにタイマーをセットする。こうして薬を飲んだことをGoogle homeに伝えてGoogleカレンダーが服用済みに変わるまで催促を続ける。

その他の部品関数

/**
 * 発話内容を編集
 * title=昼の食後の薬(未)
 * (未)を除去して返す関数
 */
function getSpeech(title) {
 var match = title.match(/^(.+)\((.+)\)$/);
 return match[1];
}

/**
 * 終了時刻が過ぎているかを確認
 * @params {object} json
 *
 {
 "drug_notifies": [
  {
   "title": "昼の食後の薬(応答無)",
   "description": "クラビット細粒10% 100mg(レボフロキサシンとして) 3g",
   "id": "4unup4q0nvecekung915hhaq68@google.com",
   "location": "0",
   "startTime": "2019-10-10T23:37:00.000Z",
   "endTime": "2019-10-11T00:37:00.000Z",
   "color": "5"
  }
 ],
 "num_reminder": "10",
 "taiking_status": "0",
 "status": "0"
 }
 */
function isOver(json) {
 if (json.drug_notifies.length > 0) {
  var endTime = new Date(json.drug_notifies[0].endTime);
  var now = new Date();
  var df = endTime - now;
  if (df < 0) {
   putLog('isOver: 終了時刻を過ぎています。');
   return true;
  }
 } else {
  putLog('isOver: json.drug_notifiesが空です。');
  return false;
 }
 return false;
}

/**
 * ログの出力
 *
 */
function putLog(msg) {
 console.log(formatDate(new Date()) + ": " + msg);
}

/**
 * 日付をフォーマットする
 * @param  {Date}   date     日付
 * @param  {String} [format] フォーマット
 * @return {String}          フォーマット済み日付
 */
function formatDate(date, format) {
  if (!format) format = 'YYYY-MM-DD hh:mm:ss.SSS';
  format = format.replace(/YYYY/g, date.getFullYear());
  format = format.replace(/MM/g, ('0' + (date.getMonth() + 1)).slice(-2));
  format = format.replace(/DD/g, ('0' + date.getDate()).slice(-2));
  format = format.replace(/hh/g, ('0' + date.getHours()).slice(-2));
  format = format.replace(/mm/g, ('0' + date.getMinutes()).slice(-2));
  format = format.replace(/ss/g, ('0' + date.getSeconds()).slice(-2));
  if (format.match(/S/g)) {
    var milliSeconds = ('00' + date.getMilliseconds()).slice(-3);
    var length = format.match(/S/g).length;
    for (var i = 0; i < length; i++) format = format.replace(/S/, milliSeconds.substring(i, i + 1));
  }
  return format;
};

/**
 * ローカルIPアドレスの取得
 *
 */
function getLocalAddress() {
 var ifacesObj = {}
 ifacesObj.ipv4 = [];
 ifacesObj.ipv6 = [];
 var interfaces = os.networkInterfaces();

 for (var dev in interfaces) {
  interfaces[dev].forEach(function(details){
   if (!details.internal){
    switch(details.family){
     case "IPv4":
      ifacesObj.ipv4.push({name:dev, address:details.address});
      break;
     case "IPv6":
      ifacesObj.ipv6.push({name:dev, address:details.address})
      break;
    }
   }
  });
 }
 return ifacesObj;
};
リスト5.その他の部品関数 

関数 getSpeech() はタイトルから発話メッセージを作成して返す関数である。正規表現を使ってタイトルから「(未)」を除去している。

関数isOver()は終了時間を超えていないかをチェックし、超えていればtrueを、いなければfalseを返す関数である。

関数putLog()はログを出力する関数である。ログに出力するメッセージを受け取り、その前に日時を付けてコンソールへ表示する。

関数formatDate()は引数に与えられた日時を編集して返す関数である。デフォルトでは”YYYY-MM-DD hh:mm:ss.SSS"の形式に編集された日時が返却される。

最後に関数getLocalAddress()はこのプログラムが稼働中のサーバのIPアドレスを返す関数である。これは、WebAPI A4を構成する発話PUSHサーバのURL(api4)を作成するために利用する(リスト3)。

ポータビリティ

このプログラム(pill-reminder)を移植する際に考慮すべき点は、WebAPI A3のURLであるapi3である。

0 件のコメント:

コメントを投稿