やりたいこと
自宅のスマートホーム化計画を進めていますが、Alexaからスマート家電を操作する際の自由度が思いのほか低いなと感じました. 例えば、スマートライトであるHueの単純なON/OFFはアレクサから操作できるが、起床ルーチン (6:00にセットすると5:45からライトがだんだん明るくなる、など) はAlexaから設定できない、など. 私が具体的にやりたかったのは、例えば、
- Amazon Echoに「アレクサ、明日の朝7:00に起こして」とお願いする.
- Philips Hueを6:45から7:00にかけて徐々に明るくする.
- Nature Remo経由で、冷房/暖房を6:30にONにする.
- Amazon Echoのアラームを7:00に設定する.
というもの. 起床時間に対して前もって各家電をONにする、というのが主な目的です. このうち、4. 以外は実現の目処が立ったので作業ログを残します (Alexaカスタムスキル経由でのアラーム設定は現状許可されていないようです). ちなみに、Alexaの定型ルーチンでも似たようなことができるはずですが、以下のような制約が出ると考え、採用しませんでした
- 声 (アレクサ、〜時に起こして) で時刻を設定することができない (Alexa App経由で時刻を変更する必要がある).
- Hueの起床ルーチンを設定できない (指定の時刻になったらいきなり明るくなる).
- 起床時間のXX分前から冷房/暖房をつける、という設定ができない. Alexa Appで起床時刻のXX分前から定型アクションを開始し、冷房/暖房をつけた後、定型アクションをXX分待機させ、その後Hueライトをつける、とすれば似たような挙動にはなるが、起床時刻のXX分前の時刻を計算して設定する必要があり、面倒.
作るものの全体像
これのAlexa Custom SkillとLambdaの部分を作ります. (その2はこちら) 宅内サーバを立てているのは、HueのAPIがLocalのものしかないからです.
Alexa Custom Skill
発話の中から時刻情報を抽出するためのスキル「朝のルーティーン」を実装します.
参考情報
Alexa Custom Skillの作成・実装方法
Amazon Echo (Alexa) のSkillの開発に必要な基本概念を押さえる
Alexaスキル 開発チュートリアル
Alexa Custom Skillのテスト・実機へのインストール方法
Alexaスキルのテスト方法
カスタムインテントの設定
AWS Lambda関数の実装
全体
事前準備
- Slack Incoming WebhookのURLを取得しておく. (SlackのWebhook URL取得手順)
- Lambdaの環境変数として
APP_ID
(スキルID) とSLACK_WEBHOOK_PATH
(上で取得したURLのhttps://hooks.slack.com
より後) を設定しておく.
関数コード
Alexaで応答する部分をSlackのwebhookを実行する関数のコールバックとして実行するのがポイントでしょうか. (そうしないとwebhookが完了する前にlambdaが終了してしまった)
'use strict';
const Alexa = require('alexa-sdk');
const https = require('https');
// Get ENV variables.
const APP_ID = process.env.ALEXA_APP_ID;
const slackWebhookPath = process.env.SLACK_WEBHOOK_PATH;
const languageStrings = {
'ja': {
translation: {
TALKSCRIPTS: [
'朝のルーティーンの設定を行います。例えば、アレクサ、朝のルーティーンで六時二十分に起こして、などです。',
],
SKILL_NAME: '朝のルーティーン',
HELP_MESSAGE: '起床予定時間を伝えると、朝のルーティーンが自動で設定されます',
HELP_REPROMPT: '朝のルーティーンを設定しますか',
STOP_MESSAGE: 'わかりました。',
},
},
};
const handlers = {
'LaunchRequest': function () {
this.emitWithState('ExplanationIntent');
},
'ExplanationIntent': function () {
const talkscripts = this.t('TALKSCRIPTS');
// Create speech output
const speechOutput = talkscripts.join('');
this.emit(':tellWithCard', speechOutput, this.t('SKILL_NAME'), speechOutput);
},
'AMAZON.HelpIntent': function () {
const speechOutput = this.t('HELP_MESSAGE');
const reprompt = this.t('HELP_MESSAGE');
this.emit(':ask', speechOutput, reprompt);
},
'AMAZON.CancelIntent': function () {
this.emit(':tell', this.t('STOP_MESSAGE'));
},
'AMAZON.StopIntent': function () {
this.emit(':tell', this.t('STOP_MESSAGE'));
},
'SetTimeIntent': function () {
// Get intent object.
let intent = this.event.request.intent;
// Get slot value.
var time = intent.slots.Time.value;
// Triger Slack post webhook.
trigerSlackWebhook(time, () => {
// Generate speech text.
const speechOutput = time + 'に朝のルーティーンを設定しました.';
this.response.cardRenderer(this.t('SKILL_NAME'), speechOutput);
// Make speech.
this.response.speak(speechOutput);
this.emit(':responseReady');
});
},
'SetOffIntent': function () {
// Triger Slack post webhook.
trigerSlackWebhook('OFF', () => {
// Generate speech text.
const speechOutput = '朝のルーティーンをオフにしました.';
this.response.cardRenderer(this.t('SKILL_NAME'), speechOutput);
// Make speech.
this.response.speak(speechOutput);
this.emit(':responseReady');
});
},
};
function trigerSlackWebhook (designatedTime, callback) {
let postData = {
"text": designatedTime,
};
let postDataStr = JSON.stringify(postData);
const options = {
hostname: 'hooks.slack.com',
port: 443,
path: slackWebhookPath,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': postDataStr.length
},
};
const slackWebhook = https.request(options, (res) => {
var responseString = "";
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
res.on('data', (d) => {
responseString = d;
});
res.on('end', () => {
console.log(responseString);
callback();
});
});
slackWebhook.on('error', (e) => {
console.error(e);
});
slackWebhook.write(postDataStr);
slackWebhook.end();
}
exports.handler = function (event, context) {
const alexa = Alexa.handler(event, context);
alexa.appId = APP_ID;
// To enable string internationalization (i18n) features, set a resources object.
alexa.resources = languageStrings;
alexa.registerHandlers(handlers);
alexa.execute();
};
テスト
以下のようにSlackに投稿されることを確認し、β版として実機にインストールすれば完了です.