赤ちゃんのミルク等の時間を Google Spreadseet に記録する Alexa カスタムスキルを作る
概要
タイトルの通り、赤ちゃんのうんち、おしっこ、ミルクの時間を Google Spreadseet に記録する Alexa カスタムスキルを作ったという話です。
ほぼほぼこちらをマネさせていただきました。mm
こちらも参考にさせていただきました。
作ったものはこちらになります。
背景
先日、息子が生まれました。
で、先天性の病気が見つかり、手術し、今入院中で、僕が日勤、妻が夜勤と交替しながら、一日中、病院で付き添い育児をしております。(育休を取ってる)
病院では、ミルク、うんち、おしっこの時間や量を測定していて、
時間は、泣き出した理由の見当に役立つので、家で育児するようになってからも記録したいな、と思いました。
そして、育児では両手がふさがることが多いので、家にある Amazon Echo Dot で音声で記録できればな、と思いました。
手順
1. 記録用の Spreadseet を用意
2. Spreadseet へ書き込む Apps Script を作成
ツール > スクリプト エディタ から作成できる。
records.gs。onFormSubmit はグーグルフォームからの記録用、registerXXX はアレクサからの記録用。
function onFormSubmit(e) { var startTime = Date.now(); var keys = Object.keys(e.namedValues); var dateKey; var eventKey; var memoKey; keys.forEach(function(key) { if (key.indexOf('日時') > -1) { dateKey = key; } else if (key.indexOf('イベント') > -1) { eventKey = key; } else if (key.indexOf('メモ') > -1) { memoKey = key; } }); var date = e.namedValues[dateKey][0] ? new Date(e.namedValues[dateKey][0]) : new Date(); var events = e.namedValues[eventKey][0].split(/,\s*/); var memo = e.namedValues[memoKey][0]; if (events.indexOf(TYPE_NAME.poo) > -1) { records.appendJournalRecord(date, TYPE.POO, memo); } if (events.indexOf(TYPE_NAME.pee) > -1) { records.appendJournalRecord(date, TYPE.PEE, memo); } if (events.indexOf(TYPE_NAME.milk) > -1) { records.appendJournalRecord(date, TYPE.MILK, memo); } if (events.indexOf(TYPE_NAME.health) > -1) { records.appendJournalRecord(date, TYPE.HEALTH, memo); } var executionTime = Date.now() - startTime; Logger.log('onFormSubmit took ' + executionTime + ' ms'); } function registerPoo() { var startTime = Date.now(); records.appendJournalRecord(new Date(), TYPE.POO); var executionTime = Date.now() - startTime; Logger.log('registerPoo took ' + executionTime + ' ms'); } function registerPee() { var startTime = Date.now(); records.appendJournalRecord(new Date(), TYPE.PEE); var executionTime = Date.now() - startTime; Logger.log('registerPee took ' + executionTime + ' ms'); } function registerPooAndPee() { var startTime = Date.now(); records.appendJournalRecord(new Date(), TYPE.POO); records.appendJournalRecord(new Date(), TYPE.PEE); var executionTime = Date.now() - startTime; Logger.log('registerPooAndPee took ' + executionTime + ' ms'); } function registerMilk() { var startTime = Date.now(); records.appendJournalRecord(new Date(), TYPE.MILK); var executionTime = Date.now() - startTime; Logger.log('registerMilk took ' + executionTime + ' ms'); } var records = {}; records.getSheet = function () { if (!records.sheet) { records.sheet = SpreadsheetApp.getActive().getSheetByName('records'); } return records.sheet; } records.appendJournalRecord = function (date, type, memo) { var startTime = Date.now(); Logger.log('appendJournalRecord started'); records.getSheet().appendRow(records.createJournalRecordRowContent(date, type, memo)); var executionTime = Date.now() - startTime; Logger.log('appendJournalRecord took ' + executionTime + ' ms'); }; records.createJournalRecordRowContent = function (date, type, memo) { var row = []; row.push("'" + date.toLocaleDateString()); row.push("'" + date.toLocaleTimeString().replace(/[^:0-9]/g, '')); row.push(TYPE_NAME[type]); if (memo) { row.push(memo); } return row; }
const.gs
var COLUMN = { DATE: 'date', TIME: 'time', EVENT: 'event', MEMO: 'memo' } var TYPE = { POO: 'poo', PEE: 'pee', MILK: 'milk', HEALTH: 'health' }; var TYPE_NAME = { 'poo': 'うんち', 'pee': 'おしっこ', 'milk': 'ミルク', 'health': '体調' }; var TYPE_BY_NAME = {}; Object.keys(TYPE_NAME).forEach(function(key) { TYPE_BY_NAME[TYPE_NAME[key]] = key; });
3. Apps Script をAPIとして公開
ここが一番てこづった。
Google Cloud Platform プロジェクトを作成し、Apps Script を関連づける。
そして、「GCP プロジェクトの Apps Script API を有効化」「OAuth 2.0 クライアントIDとシークレットの取得」「OAuth 2.0 Playground でアクセストークンとリフレッシュトークンの取得」
参考はこちら。
4. Alexaスキルを開発
Alexaスキルの開発は初めてだったが、公式が動画教材を作っていて、とてもわかりやすかった。
Alexa-hostedスキルで開発すれば alexa developer console 上で開発が完結する。すごい。
対話モデルの設定
alexa developer console メニュー上のビルド > JSONエディター から json をアップロード。
そして、モデルを保存し、モデルをビルド。
{ "interactionModel": { "languageModel": { "invocationName": "リクログ", "intents": [ { "name": "AMAZON.CancelIntent", "samples": [] }, { "name": "AMAZON.HelpIntent", "samples": [ "使い方", "ヘルプ" ] }, { "name": "AMAZON.StopIntent", "samples": [ "ありがとう", "終了", "終わり", "ストップ" ] }, { "name": "RegisterPooIntent", "slots": [], "samples": [ "うんち" ] }, { "name": "RegisterPeeIntent", "slots": [], "samples": [ "おしっこ" ] }, { "name": "RegisterPooAndPeeIntent", "slots": [], "samples": [ "うんちとおしっこ", "おしっことうんち" ] }, { "name": "RegisterMilkIntent", "slots": [], "samples": [ "ミルク" ] }, { "name": "AMAZON.NavigateHomeIntent", "samples": [] } ], "types": [] } } }
Lambda 関数の作成
alexa developer console メニュー上のコードエディタ。
package.json に googleapis を追加。
"dependencies": { "ask-sdk-core": "^2.6.0", "ask-sdk-model": "^1.18.0", "aws-sdk": "^2.326.0", "googleapis": "^25.0.0" }
gas-accessor.js を作成。
const google = require('googleapis'); const OAuth2 = google.auth.OAuth2; const CLIENT_ID = process.env['CLIENT_ID']; const CLIENT_SECRET = process.env['CLIENT_SECRET']; const ACCESS_TOKEN = process.env['ACCESS_TOKEN']; const REFRESH_TOKEN = process.env['REFRESH_TOKEN']; const SCRIPT_ID = process.env['SCRIPT_ID']; const DEV_MODE = process.env['DEV_MODE'] ? /^true$/i.test(process.env['DEV_MODE']) : false; const gasAccessor = {}; gasAccessor.executeFunction = function (functionName, callback, opt_parameter) { var startTime = Date.now(); console.log('executeFunction started [functionName=' + functionName + ', parameter=' + opt_parameter); const auth = new OAuth2(CLIENT_ID, CLIENT_SECRET); auth.setCredentials({ access_token: ACCESS_TOKEN, refresh_token: REFRESH_TOKEN }); const script = google.script('v1'); script.scripts.run({ auth: auth, scriptId: SCRIPT_ID, resource: { function: functionName, parameters: [opt_parameter], devMode: DEV_MODE } }, (err, result) => { var turnAroundTime = Date.now() - startTime; console.log(functionName + ' API execution took ' + turnAroundTime + ' ms'); if (err || result.data.error) { console.error(JSON.stringify(err)); console.error(JSON.stringify(result.data.error)); throw 'API Execution Failure'; } else { console.log(JSON.stringify(result.data.response)); callback(result.data.response.result); var callbackExecutionTime = Date.now() - startTime - turnAroundTime; console.log('callback execution took ' + callbackExecutionTime + ' ms'); } }); }; module.exports = gasAccessor;
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 gasAccessor = require('./gas-accessor'); const LaunchRequestHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'; }, handle(handlerInput) { const speakOutput = 'こんにちは。リクログでは、うんち、おしっこ、ミルクが記録できます。何をしますか?'; return handlerInput.responseBuilder .speak(speakOutput) .reprompt(speakOutput) .getResponse(); } }; const RegisterPooIntentHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' && Alexa.getIntentName(handlerInput.requestEnvelope) === 'RegisterPooIntent'; }, handle(handlerInput) { gasAccessor.executeFunction('registerPoo', function (result) { console.log(`IntentHandler executed gasAccessor.executeFunction registerPoo.`); }.bind(this)); const speakOutput = 'うんちを記録しました。'; return handlerInput.responseBuilder .speak(speakOutput) .getResponse(); } }; const RegisterPeeIntentHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' && Alexa.getIntentName(handlerInput.requestEnvelope) === 'RegisterPeeIntent'; }, handle(handlerInput) { gasAccessor.executeFunction('registerPee', function (result) { console.log(`IntentHandler executed gasAccessor.executeFunction registerPee.`); }.bind(this)); const speakOutput = 'おしっこを記録しました。'; return handlerInput.responseBuilder .speak(speakOutput) .getResponse(); } }; const RegisterPooAndPeeIntentHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' && Alexa.getIntentName(handlerInput.requestEnvelope) === 'RegisterPooAndPeeIntent'; }, handle(handlerInput) { gasAccessor.executeFunction('registerPooAndPee', function (result) { console.log(`IntentHandler executed gasAccessor.executeFunction registerPooAndPee.`); }.bind(this)); const speakOutput = 'うんちとおしっこを記録しました。'; return handlerInput.responseBuilder .speak(speakOutput) .getResponse(); } }; const RegisterMilkIntentHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' && Alexa.getIntentName(handlerInput.requestEnvelope) === 'RegisterMilkIntent'; }, handle(handlerInput) { gasAccessor.executeFunction('registerMilk', function (result) { console.log(`IntentHandler executed gasAccessor.executeFunction registerMilk.`); }.bind(this)); const speakOutput = 'ミルクを記録しました。'; return handlerInput.responseBuilder .speak(speakOutput) .getResponse(); } }; const HelpIntentHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent'; }, handle(handlerInput) { const speakOutput = 'うんち、おしっこ、ミルクが記録できます。何をしますか?'; return handlerInput.responseBuilder .speak(speakOutput) .reprompt(speakOutput) .getResponse(); } }; const CancelAndStopIntentHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent' || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent'); }, handle(handlerInput) { const speakOutput = '終了します。バイバイ、またね。'; return handlerInput.responseBuilder .speak(speakOutput) .getResponse(); } }; const SessionEndedRequestHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest'; }, handle(handlerInput) { // Any cleanup logic goes here. return handlerInput.responseBuilder.getResponse(); } }; // The intent reflector is used for interaction model testing and debugging. // It will simply repeat the intent the user said. You can create custom handlers // for your intents by defining them above, then also adding them to the request // handler chain below. const IntentReflectorHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'; }, handle(handlerInput) { const intentName = Alexa.getIntentName(handlerInput.requestEnvelope); const speakOutput = `You just triggered ${intentName}`; return handlerInput.responseBuilder .speak(speakOutput) //.reprompt('add a reprompt if you want to keep the session open for the user to respond') .getResponse(); } }; // Generic error handling to capture any syntax or routing errors. If you receive an error // stating the request handler chain is not found, you have not implemented a handler for // the intent being invoked or included it in the skill builder below. const ErrorHandler = { canHandle() { return true; }, handle(handlerInput, error) { console.log(`~~~~ Error handled: ${error.stack}`); const speakOutput = `エラーが発生しました。もう一度、お願いします。`; return handlerInput.responseBuilder .speak(speakOutput) .reprompt(speakOutput) .getResponse(); } }; // 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, RegisterPooIntentHandler, RegisterPeeIntentHandler, RegisterPooAndPeeIntentHandler, RegisterMilkIntentHandler, HelpIntentHandler, CancelAndStopIntentHandler, SessionEndedRequestHandler, IntentReflectorHandler, // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers ) .addErrorHandlers( ErrorHandler, ) .lambda();
5. 開発したAlexaスキルをテスト
alexa developer console メニュー上のテストより試す。
「リクログでうんち」
そして、amazon echo dot 実機で試す。
「アレクサ、リクログでうんち」
以上。
実際に使ってみて変更を加えるかも。