40
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

株式会社ACCESSAdvent Calendar 2017

Day 9

育児日記をDashボタンとAlexaでつける

Last updated at Posted at 2017-12-09

先月、長男が生まれました。ライフイベントの時には、何か作らなければなりません。(ちなみに、結婚式では余興用のゲームを作りました)

とりあえず、育児日記をIoT化してみました。
育児日記とは、乳幼児の授乳・おしっこ・うんこ等を記録するものです。

NAVERまとめ:何を記録すればいいの? 育児日記の種類・書き方まとめ
https://matome.naver.jp/odai/2140775827668974801

NAVERまとめにはちゃんと書いていませんが、、育児日記は、主に、赤ちゃんの次の授乳のタイミング・うんちのタイミングを知るために使うものだと理解しています。特に新生児は概ね2時間ごとに授乳ですので。
あまりに頻繁なので、紙に記録はどう考えても面倒です。頻繁なので、スマホアプリさえ面倒です。

※ ACCESS advent calendarですが、例によって、ACCESSとは何の関係もありません。趣味で作ったものです。
※ Amazon Dash buttonのこういった使い方を推奨するものではありません。
※ 以下、画像はいらすとやにとてもお世話になっています。

何を作ったか

というわけで、以下のようなものを作りました。

育児日記をDashボタンとAlexaでつける (2).png

  • Amazon Dash Button を押すと、授乳・おしっこ・うんち等を記録できる
  • 記録結果は、Google Spreadsheetに保存される
  • 記録されたことをLINEに通知する
  • Echo dotsに「アレクサ、育児日記で授乳を記録」と話しかけることでもボタンを押したのと同じ効果
  • LINEに返信すると、任意のコメントを保存できる(日々に体重とか)

実際に運用しているLINEはこんな感じです。

スクリーンショット 2017-12-09 0.27.31.png

こんな感じに記録されます。

スクリーンショット 2017-12-09 0.00.04.png

ボタンはこんな感じで、ベビーベッドのそばに置いています。

buttons.png

何が便利かというと、

  • おむつ替えも授乳もかなり頻繁で、ノートや、スマホアプリに記録するのは面倒になってくる
    • => ボタンやアレクサなら簡単なので、面倒になりにくい
  • 次いつ授乳だっけ?をすぐに確認したい
    • => いつも使って居るLINEで確認できる

といった感じです。

作り方

基本的に、なるべく楽をしてプロトタイプを作るを目標にします。製品を作るわけでは無いですし、そもそもどういう仕様にするか決まっていないので、できるだけ手を抜いて使いやすいプロトタイプを作るにはどうするか、を目指しました。

  • 最初は、勉強のためGolangを使ってサーバー書こうと思いましたが、UI作ったりするのが面倒なので、データ確認・加工が容易なGoogle Spreadsheetを使うことにしました
  • プロトタイプなので、認証は考えないことにします

用意するもの

  • Raspberry Pi Zero W 1台
  • Amazon Dash Button 必要な数 (中古でも大丈夫です)
  • Amazon Echo dots (Alexa) 1台
    * Google のアカウント
  • AWSのアカウント
  • Amazon.co.jpのアカウント(Alexa設定済み)
  • Amazon.co.jpと同じID/パスワードでログインしたdeveloper.amazon.com のアカウント

Dashボタンの設定、Raspberry Piの設定

まずは、Dashボタンを設定します。基本的に以下の流れですが、IFTTTは使いません。

参考:Raspberry Pi Zero W で Amazon Dash Button をただのIoTボタンとして使う流れ
https://qiita.com/ikeyasu/items/68725287eb3cae4770bf

上記を参考に、以下のように設定します。

  1. 使用するAmazon Dash buttonをセットアップ
  2. SSH が繋げるようになるまでRaspberry Pi Zero Wを設定 (ここまで)
  3. dasher を cloneして、npm install
    • $ git clone https://github.com/maddox/dasher.git && cd dasher && npm install
  4. 使用するAmazon Dash buttonのMacアドレスを取得
  5. 下記のconfig/config.json を設定
    • addressは、4. で取得したMacアドレスです。
    • urlはあとで変更します
  6. $ sudo npm start で Dasher を起動。こちらを参考に自動起動するようにする
config/config.json
{"buttons":[
  {
    "name" : "授乳左",
    "address": "XX:XX:XX:XX:XX:XX",
    "url": "https://script.google.com/macros/s/XXXX/exec",
    "method": "POST",
    "json": true,
    "protocol": "udp",
    "body": {"type":"button", "value1":"授乳左"}
  }
  ,{
    "name" : "授乳右",
    "address": "XX:XX:XX:XX:XX:XX",
    "url": "https://script.google.com/macros/s/XXXX/exec",
    "method": "POST",
    "json": true,
    "protocol": "udp",
    "body": {"type":"button", "value1":"授乳右"}
  }
  ,{
    "name" : "おしっこ ",
    "address": "XX:XX:XX:XX:XX:XX",
    "url": "https://script.google.com/macros/s/XXXX/exec",
    "method": "POST",
    "json": true,
    "protocol": "udp",
    "body": {"type":"button", "value1":"おしっこ"}
  }
  ,{
    "name" : "うんち ",
    "address": "XX:XX:XX:XX:XX:XX",
    "url": "https://script.google.com/macros/s/XXXX/exec",
    "method": "POST",
    "json": true,
    "protocol": "udp",
    "body": {"type":"button", "value1":"うんち"}
  }
]}

Google Apps Script の記述

次に、Google Spreadsheetを用意します。以下のような感じです。(これは、公開用に少し修正しています)

※ このままだと設定されたスクリプトは見れませんが、自分のドライブにコピーすると見れます。

要点は

  • 1枚目のシートは"diary"で、ここに育児日記を記録
  • 2枚目のシートは"log1"でデバッグ用のログ
  • 3枚目のシートは"config"で設定を保存

次に、Google Apps Scriptの設定をします。「ツール」メニューから「スクリプトエディタ」を開いてください。

スクリーンショット 2017-12-09 22.22.10.png

スクリプトエディタ:

スクリーンショット 2017-12-09 22.22.46.png

スクリプトエディタに以下のコードを貼り付けます。定数のところは適宜修正の必要が有ります。SPREADSHEET_ID は、以下の部分です。

スクリーンショット 2017-12-09 11.00.14.png

Google Apps Script に設定するコード:

code.js
var ACCESS_TOKEN = 'XXX'; // LINEのACCESS Token(後述)
var SPREADSHEET_ID = 'XXX'; // Google SpreadsheetのID (上記の通り)

function push(to, text) {
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + ACCESS_TOKEN,
  };

  var postData = {
    "to" : to,
    "messages" : [
      {
        'type':'text',
        'text':text,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  var res = UrlFetchApp.fetch(url, options);  
  logging("push(" + to + "," + text + "): " + res.getResponseCode().toString() + ": " + res.getContentText());
  return res;
}

function pushToGroup(text) {
  push(getConfig("lineGroupId"), text);
}

function convertDate(rawDateString) {
  return Utilities.formatDate(parseDate(rawDateString), "JST", "yyyy/MM/dd HH:mm");
}

function parseDate(rawDateString) {
  // September 23, 2017 at 11:34PM
  matched = rawDateString.match(/(\w+) (\d+), (\d+) at (\d+:\d+)(.*$)/);
  // format for Aug 09 1995 00:00:00 PM GMT+0900
  return new Date(matched[1] + " " + matched[2] + " " + matched[3] + " " + matched[4] + " " + matched[5] + " GMT+0900");  
}

function isLineMessage(json) {
  if (!json) return false;
  if ("events" in json && "type" in json.events[0]) return json.events[0].type === "message";
  return false;
}

function isLineJoin(json) {
  if (!json) return false;
  if ("events" in json && "type" in json.events[0]) return json.events[0].type === "join";
  return false;
}

function isButton(json) {
  if (!json) return false;
  if ("type" in json) return json.type === "button";
  return false;
}

function logging(str) {
  var logSheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('log1');
  logSheet.getRange(logSheet.getLastRow() + 1, 1).setValue(str);
}

function now() {
  return Utilities.formatDate(new Date(), "JST", "yyyy/MM/dd HH:mm");
}

function getConfigRange(key) {
  var sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('config');
  var data = sheet.getRange("A1:B10").getValues();
  for(var i = 0; i<data.length;i++) {
    if(data[i][0] === key){
      return sheet.getRange(i + 1, 1, 1, 2);
    }
  }
  var lastraw = sheet.getLastRow();
  return sheet.getRange(lastraw + 1, 1, 1, 2);
}

function setConfig(key, value) {
  var range = getConfigRange(key);
  range.setValues([[key, value]]);
}

function getConfig(key) {
  var range = getConfigRange(key);
  return range.getValues()[0][1];
}

function getBabyDiary(num) {
  num = (num === undefined || num < 1) ? 20 : num;
  var sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('diary');
  var lastraw = sheet.getLastRow();
  var startraw = lastraw - num + 1;
  startPos = Math.max(1, startraw);
  num = lastraw - startraw + 1;
  return sheet.getRange(startraw, 1, num, 2).getValues();
}

function addRow(value) {
  var logSheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('diary');
  var lastraw = logSheet.getLastRow();
  logSheet.getRange(lastraw + 1, 1).setValue(now());
  logSheet.getRange(lastraw + 1, 2).setValue(value);
  logging("addRow():" + value);
}

function isDuplicated(value) {
  var logSheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('diary');
  var lastraw = logSheet.getLastRow();
  var lastDate = new Date(logSheet.getRange(lastraw, 1).getValue());
  var nowDate = new Date(now());
  return (nowDate - lastDate <= (60 * 1000)) &&
    (logSheet.getRange(lastraw, 2).getValue() == value);
}

function execLineMessage(json) {
  addRow(json.events[0].message.text);
  pushToGroup(now() + " 記録しました。");
}

function execLineJoin(json) {
  setConfig("lineGroupId", json.events[0].source.groupId)
}

function execButton(json) {
  logging("execButton():" + json.value1);
  var value = json.value1;
  if (isDuplicated(value)) return;
  addRow(value);
  pushToGroup(now() + " " + json.value1);
}

function doPost(e) {
  logging(e.postData.contents);
  var json = JSON.parse(e.postData.contents);
  if (isLineMessage(json)) execLineMessage(json);
  if (isLineJoin(json)) execLineJoin(json);
  if (isButton(json)) execButton(json);
}

function onChange(event) {
  var sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('diary');
  var date = sheet.getRange(sheet.getLastRow(), 1).getValue();
  var action = sheet.getRange(sheet.getLastRow(), 2).getValue();
  var msg = convertDate(date) + " " + action;
  pushToGroup(msg);
}

function test() {
  // test date
  var date = parseDate('September 23, 2017 at 11:34PM');
  Logger.log(date.getFullYear() === 2017 && date.getMonth() === 8 && date.getDate() === 23);
  Logger.log(convertDate('September 23, 2017 at 11:34PM') === '2017/09/23 23:34');

  // isLineMessage
  var input = '{"events":[{"type":"message","replyToken":"AA","source":{"userId":"BB","type":"user"},"timestamp":1506215175088,"message":{"type":"text","id":"123","text":"てて"}}]}';
  Logger.log(isLineMessage(JSON.parse(input)));

  // isLineJoin
  var join = '{"events":[{"type":"join","replyToken":"AA","source":{"groupId":"BB","type":"group"},"timestamp":1512199397996}]}';
  Logger.log(isLineJoin(JSON.parse(join)));

  // config
  var r = "rand-" + Math.random().toString(36).slice(-8); // random string
  Logger.log(getConfig("test") !== r);
  Logger.log(setConfig("test", r) === undefined);
  Logger.log(getConfig("test") === r);

  Logger.log(getBabyDiary(1).length === 1);
  Logger.log(getBabyDiary(20).length === 20);
}

※ プログラムの解説は時間のあるときに。。

次に、上記スクリプトを公開します。「公開」メニューから、「Webアプリケーションとして公開」を選択してください。

スクリーンショット 2017-12-09 22.23.29.png

以下、プロダクトバージョンを「新規作成」にして、コメントを適等に書いてください。
アプリケーションにアクセスできるユーザーを「全員(匿名ユーザーを含む)」にしてください。(セキュリティは無くなりますが、プロトタイプなので良しとします)
そして、「導入」をクリックします。

スクリーンショット 2017-12-09 22.24.18.png

次に、初回のみ「承認が必要です」というダイアログが出ます。これは、「許可を確認」をクリックします。
アカウントを選択してください。
「このアプリは確認されていません」が出る場合は、「詳細」をクリックし、「XXX(安全ではないページ)に移動」をクリックします。

完了すると、以下のような画面が出ます。このURLをコピーしてください。

スクリーンショット 2017-12-09 22.24.50.png

そして、Raspberry Pi Zero W の設定に戻り、urlの部分をここで取得したURLに書き換えます。

これで、Amazon Dash buttonを押すと、Google Spreadsheetの項目が増えるはずです。

LINE の設定

ここまででも、「ボタンを押すとその時間と項目がGoogle Spreadsheetに記録される」が実現されているので、それなりに使えるのですが、毎回、Google Spreadsheetに確認に行くというのは、なんとも面倒です。
そこで、普段から使っているLINEで確認できるようにしてみました。

こちらを参考にしました

LINEのMessaging APIを使って投稿(push)と返信(reply)を使ってみる。
https://ameblo.jp/ponkotsuameba/entry-12212318642.html

Line Channelを作成・Messeage APIの設定

LINE公式ドキュメント Messaging APIを利用するには:
https://developers.line.me/ja/docs/messaging-api/getting-started/

  1. LINE developers 画面にログイン https://developers.line.me/ja/

  2. 「Messaging API(ボット)をはじめる」をクリック

  3. https://developers.line.me/ja/docs/messaging-api/getting-started/ に沿って、Channelを作成

    • アプリ名:「育児日記」
    • アプリ説明:適等に
    • プラン:Developer trial
    • 種別:適等に
    • メールアドレス
    • Kobito.tCKqZG.png
  4. 確認を押し、利用規約に同意

  5. Consoleに戻ると、今追加した「育児日記」が追加されているので、クリック

    • スクリーンショット 2017-12-09 22.25.04.png
  6. メッセージ送受信設定の設定を行う

    • アクセストークンの「再発行」をクリックして、アクセストークンを取得
    • Webhook送信を「使用する」に変更
    • Webhook URL に先ほどのGoogle Apps ScriptのURL(https://script.google.com/macros/ で始まるURL)を設定
    • Bot のグループトークへの参加は、好みで「利用する」に。利用した方が、家族内で、育児日記を共有できて良いです。
    • Kobito.2glIYM.png

Alexa の設定

さて、ここまでのところで、Dashボタンを押すと、LINEに投稿する流れはできています。が、授乳しているときは、往々にして手を離せないもの。という事で、手に入れたばかりのEcho dots(Alexa)で投稿できるようにしてみました。
基本的に、 https://dev.classmethod.jp/etc/first-step-of-making-alexa-custom-skills/ の流れです。本来であれば、AWS Lambdaを使わずに、Google Apps Scriptだけで、Alexa Skillを書けるはずですが、SDK無しで書く方法のドキュメントが見当たらなかったので、Lambdaを使っています。
以下、 https://dev.classmethod.jp/etc/first-step-of-making-alexa-custom-skills/ の流れを一読した上で、読んでください。

  1. https://developer.amazon.com/edw/home.html#/skills でスキル作成。 一番Basicな、カスタム対話モデルを選択。言語は日本語。
  2. 対話モデルはBuilderを使う。Intentを追加。授乳・うんち・うんこなど必要な分だけ、Intentを追加する。Utteranceは思いつく限りのパターンを記載。
  3. 次にLambda関数を設定
    • package.json を変更しているので、classmethodの記事と違って、ローカルで変更してアップロードの必要が有ります。関数のエクスポートから、デプロイパッケージをダウンロード・解凍してください
      • スクリーンショット 2017-12-09 22.26.14.png
    • 以下の通り、ファイルを変更してください。index.js の中で、Google Apps ScriptのURLを書く部分があるので、そこは置き換えてください。その後、npm installし、zip -r lambda.zip package.json index.js node_modulesしてlambda.zipを作成し、lambdaにアップロードしてください
      • スクリーンショット 2017-12-09 22.40.49.png
    • ARNを取得してください
      • Lambda_Management_Console.png
  4. あとは、classmethodの記事の通りです。
package.json
{
  "name": "alexa-skill-kit-sdk-factskill",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "alexa-sdk": "^1.0.10",
    "bluebird": "^3.5.1",
    "superagent": "^3.8.1",
    "superagent-bluebird-promise": "^4.2.0"
  }
}
index.js
'use strict';
 
const Alexa = require('alexa-sdk');
const request = require('superagent-bluebird-promise');
const Promise = require('bluebird');

const APP_ID = '';  // TODO replace with your app ID (OPTIONAL).

function post(obj) {
    console.log('post');
    return request.post('https://script.google.com/macros/s/XXXXX') // <==Google Apps ScriptのWebアプリケーションのURLに置き換えてください
      .send(obj)
      .set('Content-Type', 'application/json');
}
 
exports.handler = function (event, context) {
    const alexa = Alexa.handler(event, context);
    alexa.APP_ID = APP_ID;
    // To enable string internationalization (i18n) features, set a resources object.
    alexa.registerHandlers({
        'LaunchRequest': function () {
            this.emit(':ask', '記録したい内容を教えてください。');
        },
        'breastfeedingLeft': function () {
            var self = this;
            post({"type":"button", "value1":"授乳左"}).then(function() {
                self.emit(':tell', '授乳、左で記録しました。');
            });
        },
        'breastfeedingRight': function () {
            var self = this;
            post({"type":"button", "value1":"授乳右"}).then(function() {
                self.emit(':tell', '授乳、右で記録しました。');
            });
        },
        'pee': function () {
            var self = this;
            post({"type":"button", "value1":"うんち"}).then(function() {
                self.emit(':tell', 'うんちで記録しました。');
            });
        },
        'poo': function () {
            var self = this;
            post({"type":"button", "value1":"おしっこ"}).then(function() {
                self.emit(':tell', 'おしっこで記録しました。');
            });
        },
        'AMAZON.HelpIntent': function () {
            this.emit(':ask', '授乳左、授乳右、うんこ、オシッコ、などと言ってください。それが記録されます。');
        },
        'AMAZON.CancelIntent': function () {
            this.emit(':tell', '授乳日記を終わります');
        },
        'AMAZON.StopIntent': function () {
            this.emit(':tell', '授乳日記を終わります');
        }
    });
    alexa.execute();
};

これで、Alexaから、記録できるようになりました。

まとめ

作り方をざっと書いただけですが、今回のポイントは前に書いたとおり、いかに手を抜いて、使えるプロトタイプを作るかでした。概ね、その目的は達成できたと思います。
一番効果的だったのは、Google Apps Scriptでした。サーバーの管理が不要ですし、SpreadsheetのUIが使えるので、UIを作らなくても、一応動く状態に持っていけます。
また、LINEのボットを使う事で、普段使いで辛くないプロトタイプにできたように思います

今後の展望としては、もう少しまともなWeb画面を作りたいです。実際の利用では、LINEで確認するのが多いのですが、過去のデータを俯瞰的に見るような画面は必要そうです。
また、Alexaで直近の授乳から何時間経ったか、簡単に聞けると良いかなとも思っています。

最後に、絶賛育児中で育児日記をつけていて、こういうのを使いたいけど、ここに書かれている情報だけでは構築が難しい、、という方は、遠慮無く、Twitterやコメントでご質問ください。

明日は、@katoken-0215 さんのAWSの話しです!

40
29
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?