はじめに
うちには幼稚園に通う子供がいます。「バス来ちゃうよ、ご飯急いで!」「遅れちゃうよ、早くトイレに行ってきて!」そんなやり取りが繰り返される毎日。少しだけ険悪なムードになることもあって、はぁ、なんだか朝から嫌だなぁ...。
と、Google Homeで任意の言葉を読み上げることができると知った妻より「これ、使えないの?」との発言。人に指摘されるのを嫌うのは、幼稚園児だって同じはず。ナイスアイデア!
やりたいこと
妻から指定された要件は以下でした。
- 指定時刻に、指定した文章を読み上げて。
- 各曜日ごとに読み上げる文章は変更したいな。
- 読み上げ以外にも、指定時刻に特定の処理を実行できるようにしておいて。
お、おっけー...。
どうやったか
処理フロー
処理フローは、Googleスプレッドシート→Firebase→ラズパイ→Google Homeとしました。
それぞれの連携では(1)Google Apps Script、(2)firebase(Realtime Databaseの更新イベントをフック)、(3)google-home-notifierを利用しました。
Googleスプレッドシート
読み上げる文章が妻でも簡単にメンテナンスでき、また、毎分のトリガーを容易に設定できることから、入力はGoogleスプレッドシートを利用することにしました。曜日ごとにシートを分け、以下のフォーマットで読み上げる文章などを指定しました。(buscatchの指定はとりあえずここでは無視してください)
続いてスクリプトです。Google Apps ScriptでFirebaseへ通知するものを作ります。定期実行される処理として、対象曜日のシートから現在時刻のデータ(Command、Option)を探し、その内容をFirebaseへ通知します。
function checkCommand() {
var now = new Date();
now.setSeconds(0, 0);
var name = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][now.getDay()];
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getSheetByName(name);
if (sheet) {
var data = sheet.getSheetValues(2, 1, sheet.getLastRow(), sheet.getLastColumn());
data.forEach(function(value, index) {
var time = new Date(value[0]);
time.setFullYear(now.getFullYear(), now.getMonth(), now.getDate());
time.setSeconds(0, 0);
var diff = (now.getTime() - time.getTime()) / (1000 * 60);
if (Math.round(diff) == 0) {
var command = value[1];
var option = value[2];
notifyFirebase(command, option);
}
});
}
}
通知先のFirebaseでは、Databaseを以下のように作成しています。通知内容をwordの値として書き込み、その更新イベントをラズパイでフックします(フック処理は次項で触れます)。
プロジェクト名-xxxxx
└ googlehome
└ word: ""
そのため、通知処理としては、以下のように、CommandとOptionの内容をFirebaseへPutします。
function notifyFirebase(command, option) {
var url = 'https://xxxxx-xxxxx.firebaseio.com/googlehome.json';
var value = option.length > 0 ? command + ' ' + option : command;
var options = {
'method' : 'put',
'contentType': 'application/json',
'payload' : JSON.stringify({'word': value})
};
UrlFetchApp.fetch(url, options);
}
最後に、上記のcheckCommand()を毎分のトリガーで指定すれば終了です。
Raspberry Pi
firebase
今度は、Firebaseへの書き込みを、Realtime Databaseの更新イベントをフックしているラズパイで受け取る部分です。前回の記事でも参照した実装(拡張性があって助かります)へ分岐処理を追加して対応しました。受け取った情報は、Google Homeで読み上げを実施する別プロジェクトへ流します。
var firebase = require("firebase");
var config = {
//省略
};
firebase.initializeApp(config);
const path = "/googlehome";
const key = "word";
const db = firebase.database();
db.ref(path).on("value", function(changedSnapshot) {
const value = changedSnapshot.child(key).val();
if (value) {
const words = value.split(" ");
const command = getJsonData(words[0], {
...
//読み上げ
"announce": () => {
const command = "sh /home/pi/announce/index.sh ";
const content = words.slice(1, words.length).join(" ");
return command + content.replace(/'/g,"\\'");
},
...
})();
if (command) {
const exec = require('child_process').exec;
exec(command);
db.ref(path).set({[key]: ""});
}
}
});
読み上げる文章を半角スペースで再結合したり、アポストロフィーをエスケープしているのは、英語の文章も扱えるように配慮したためです。
google-home-notifier
Google Homeでの読み上げは、以下のようにしました。
const googlehome = require('google-home-notifier')
var args = process.argv.slice(2, process.argv.length);
var msg = args.length > 0 ? args.join(' ') : '通知内容が指定されませんでした';
var isJapanese = false;
for (var i = 0; i < msg.length; ++i) {
if (msg.charCodeAt(i) >= 256) {
isJapanese = true;
break;
}
}
const language = isJapanese ? 'ja' : 'en';
googlehome.device('Google-Home', language);
googlehome.notify(msg, function(res) {
console.log(res);
});
引数で渡された文章をgoogle-home-notifierへ渡します。日本語以外は英語として扱うことにしました。スペースが含まれる英語の文章を「"」などで囲まずに引数指定されても動作できるよう、引数群を半角スペースで結合しています。
この実装を、以下のシェルスクリプト経由で実行しています。
#!/bin/sh
cd `dirname $0`
node index.js $*
どうなったか
(例1)
Google Home:「トイレに行きましたか」
子供:「行きました」
お、返事したぞ。
(例2)
子供:「朝ごはんは美味しいですか、って言われる前に食べ始めなきゃね」
よしよし。
うまくいっているかな、という感じです。
おわりに
実際には、Google Apps Scriptへ休日・祝日判定も組み込み、平日のみ動作するようにしています。また、Googleスプレッドシートで指定しているbuscatchコマンドでは、幼稚園バスの運行状況をスクレイピングして読み上げさせています(「あとxx分で到着予定」)。以前と比べ、だいぶ快適な朝になってきたと思っています。
まだ、英語での読み上げは利用していませんが、これは子供に興味が出てきたタイミングに合わせて、徐々に導入していこうと思っています。発音、折り紙付きだと思いますし。子供からどういう反応が返ってくるのか、楽しみにしています。