Alexaからスマートホーム家電を時間差をつけて制御する (その１) 〜 Alexaで時刻情報を取得してSlackにポストする

# やりたいこと
自宅のスマートホーム化計画を進めていますが、Alexaからスマート家電を操作する際の自由度が思いのほか低いなと感じました. 例えば、スマートライトであるHueの単純なON/OFFはアレクサから操作できるが、起床ルーチン (6:00にセットすると5:45からライトがだんだん明るくなる、など) はAlexaから設定できない、など. 私が具体的にやりたかったのは、例えば、

1. Amazon Echoに「アレクサ、明日の朝7:00に起こして」とお願いする.
2. Philips Hueを6:45から7:00にかけて徐々に明るくする.
3. Nature Remo経由で、冷房/暖房を6:30にONにする.
4. Amazon Echoのアラームを7:00に設定する.

というもの. 起床時間に対して前もって各家電をONにする、というのが主な目的です. このうち、4. 以外は実現の目処が立ったので作業ログを残します (Alexaカスタムスキル経由でのアラーム設定は現状許可されていないようです). ちなみに、Alexaの定型ルーチンでも似たようなことができるはずですが、以下のような制約が出ると考え、採用しませんでした

* 声 (アレクサ、〜時に起こして) で時刻を設定することができない (Alexa App経由で時刻を変更する必要がある).
* Hueの起床ルーチンを設定できない (指定の時刻になったらいきなり明るくなる).
* 起床時間のXX分前から冷房/暖房をつける、という設定ができない. Alexa Appで起床時刻のXX分前から定型アクションを開始し、冷房/暖房をつけた後、定型アクションをXX分待機させ、その後Hueライトをつける、とすれば似たような挙動にはなるが、起床時刻のXX分前の時刻を計算して設定する必要があり、面倒.

# 作るものの全体像
![20190201_SmartHome_WakeUpRoutine-4.png](https://qiita-image-store.s3.amazonaws.com/0/324285/be584115-cdb1-8526-005d-670fe6d39987.png)

これのAlexa Custom SkillとLambdaの部分を作ります. ([その２はこちら](https://qiita.com/kei3238/items/c274de7f23adc47ae9ff)) 宅内サーバを立てているのは、HueのAPIがLocalのものしかないからです.


# Alexa Custom Skill
発話の中から時刻情報を抽出するためのスキル「朝のルーティーン」を実装します. 

## 参考情報
Alexa Custom Skillの作成・実装方法
[Amazon Echo (Alexa) のSkillの開発に必要な基本概念を押さえる](https://qiita.com/chroju/items/3f30fe424df36e4b4d80)
[Alexaスキル 開発チュートリアル](https://qiita.com/zono_0/items/57247bed70dbe2b3ce28)

Alexa Custom Skillのテスト・実機へのインストール方法
[Alexaスキルのテスト方法](https://qiita.com/second113/items/8feda078bd555dd5d6a1)

## カスタムインテントの設定

1. 時刻を取得するためのカスタムスロットの設定
![timeslot.png](https://qiita-image-store.s3.amazonaws.com/0/324285/e79750a1-a18a-d219-f902-32083b43765d.png)


2. 時刻を取得するためのカスタムインテントの設定
![settimeintent.png](https://qiita-image-store.s3.amazonaws.com/0/324285/26b67a8a-5900-7877-bb98-d562495b450a.png)

3. 時刻設定"オフ"を取得するためのカスタムスロットの設定
![setoffintent.png](https://qiita-image-store.s3.amazonaws.com/0/324285/033c3f17-06c9-c639-3ec1-19d14f909015.png)

## AWS Lambda関数の実装

### 全体
![lambda.png](https://qiita-image-store.s3.amazonaws.com/0/324285/891d24de-7252-a8d2-15e0-bb39b1649ba7.png)

事前準備
* Slack Incoming WebhookのURLを取得しておく. ([SlackのWebhook URL取得手順](https://qiita.com/vmmhypervisor/items/18c99624a84df8b31008))
* Lambdaの環境変数として``` APP_ID ``` (スキルID) と``` SLACK_WEBHOOK_PATH ``` (上で取得したURLの``` https://hooks.slack.com ```より後) を設定しておく.

### 関数コード
Alexaで応答する部分をSlackのwebhookを実行する関数のコールバックとして実行するのがポイントでしょうか. (そうしないとwebhookが完了する前にlambdaが終了してしまった)

```javascript
'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();
};
```

# テスト
![alexa_skill_test.png](https://qiita-image-store.s3.amazonaws.com/0/324285/aafb23b9-0d74-1aed-2758-f01bdea3b6d0.png)
以下のようにSlackに投稿されることを確認し、[β版として実機にインストール](https://qiita.com/second113/items/8feda078bd555dd5d6a1)すれば完了です.

![slack.png](https://qiita-image-store.s3.amazonaws.com/0/324285/3957157a-77e3-4847-d9c6-73b73bee29b7.png)