Alexaからスマートホーム家電を時間差をつけて制御する (その2) 〜 Slack経由で家電のONタイマーを設定する.

# やりたいこと
自宅のスマートホーム化計画を進めていますが、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)

これの宅内サーバの部分を作ります. ([その１はこちら](https://qiita.com/kei3238/items/cc1c111922f090d366dd))

# HueライトのAPI初期設定
## 参考
* [Raspberry PiでHueを操作する](https://qiita.com/undo0530/items/0d9eee70da46f906062a)
* [Raspberry Pi + hubot + slackでPhilips Hueを制御する](http://kaiware007.hatenablog.jp/entry/2015/12/20/195522)

## LAN内のHue BridgeのIPアドレスを取得する.
ブラウザで https://discovery.meethue.com にアクセスすると、IPアドレス情報を含むJSONが得られる.

## APIをいじってみるためにCLIP API Debuggerにアクセスする.
ブラウザで ```https://[上で取得したIP]/debug/clip.html``` にアクセスする.

![スクリーンショット 2019-02-01 18.28.51.png](https://qiita-image-store.s3.amazonaws.com/0/324285/8e41c45e-a268-d79e-b72f-95a69f36eb71.png)

## ユーザを作成する.
* ``` URL: ```に``` /api ```、``` Message Body: ```に``` {"devicetype":"[your-device-name]"} ```、を入力してPOSTする. すると、``` "description": "link button not pressed" ```でエラーが返ってくる.
* 素直に、Hue Bridgeのボタンをポチッと押して、もう一度同じようにPUTしてみましょう.
* すると``` "username": "XXXXXXXXXXXXXXXXX" ```と、API利用時に必要なusernameが返ってきます.

## API commandの例

| URL: | Message Body: | Method | できること |
|:---:|:---:|:---:|:---:|
|/api/[your-username]/lights|なし|GET|Hue light一覧を取得できる|
|/api/[your-username]/groups|なし|GET|グループ一覧を取得できる|
|/api/[your-username]/lights/{light#}/state|{"on":true}|PUT|指定したライトをオンにする|
|/api/[your-username]/groups/{group#}/action|{"on":false}|PUT|指定したグループをオフにする|
|/api/[your-username]/groups/{group#}/action|{"on":true, "transitiontime": 100, "bri": 254}|PUT|指定したグループを1秒かけて明るさ254(最大)にする|

最後の``` /groups/{group#}/action ``` を使って、徐々にライトを明るくします. transitiontimeの単位は100 msecなので、15分かけて明るくしたい場合は9000と設定します.

## Node.jsでAPIを叩く方法.

```javascript
turnOnHueTargetGroup () {

  // Hue API.
  var hueApi = `http://${hueBridgeIp}/api/${username}/groups/${groupId}/action`

  axios
    .put(hueApi, {
      "on": true,
      "transitiontime":  Math.round(msecTransTime/100),
      "bri": this.brightness
    })
    .then(function (response) {
      console.info(
        `[INFO] Hue light API has been invoked at ${new Date()}.`
      )
    })
    .catch(e => {console.log(e)})
}
```

# Nature RemoのAPI初期設定
## 参考
* [Nature Remoの公式APIの使い方](https://qiita.com/sohsatoh/items/b710ab3fa05e77ab2b0a)
* [ture RemoのAPIでターミナルから家電を操作してみた](https://dev.classmethod.jp/etc/nature-remo-api/)

## アクセストークンの生成
``` https://home.nature.global/ ```からトークンを生成する.

## 登録された家電一覧を取得

```
curl -X GET "https://api.nature.global/1/appliances" -H "accept: application/json" -k --header "Authorization: Bearer ${your-token}" | jq .
```

以下のようなレスポンスが返ってくるはず.

```
[
  {
    "id": "xxxxxxxxxxxxxxx"
    "device": {
      "name": "リビングのRemo",
      "id": "xxxxxxxxxxxxxxx",
      "created_at": "2019-02-06T02:09:08Z",
      "updated_at": "2019-02-15T22:00:01Z",
      "mac_address": "xxxxxxxxxxxxxxx",
      "serial_number": "xxxxxxxxxxxxxxx",
      "firmware_version": "Remo-mini/1.0.87-g8b06f0e",
      "temperature_offset": 0,
      "humidity_offset": 0
    },
    "model": null,
    "type": "IR",
    "nickname": "エアコン停止用",
    "image": "ico_ac_1",
    "settings": null,
    "aircon": null,
    "signals": [
      {
        "id": "xxxxxxxxxxxxxxx",
        "name": "オフ",
        "image": "ico_off"
      }
    ]
  },
  { ... }
]
```

この中のsignals -> idがAPI経由で家電を操作するときに必要になります.

## API経由で家電を操作

### curl
```
curl -H 'Authorization: Bearer ${your-token}' -H "accept: application/json" -X POST "https://api.nature.global/1/signals/${your-signal-id}/send" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded"
```

### Node.js

```javascript

turnOnDevice () {
  var headers = {
    'Authorization': `Bearer ${remoApiToken}`,
    'accept': 'application/json',
    'Content-Type': 'application/x-www-form-urlencoded'
  };

  axios({
    method  : 'POST',
    url     : `https://api.nature.global/1/signals/${deviceOnSignalId}/send`,
    headers : headers,
  })
    .then(function (response) {
      console.info(
        `[INFO] Nature Remo API for the device has been invoked at ${new Date()}.`
      )
    })
    .catch(e => {console.log(e)})
}
```

# Slackから時刻設定情報を取得する

## 参考
* [APIを叩いてslackから発言を取ってくる](https://doruby.jp/users/baguette/entries/APIを叩いてslackから発言を取ってくる)
* [slack api channels.history](https://api.slack.com/methods/channels.history)

## Slack API tokenの取得
``` https://api.slack.com/apps ``` から作成する.

## 時刻設定情報が投稿されているChannelのIDを取得する.
``` https://api.slack.com/methods/channels.list/test ``` から取得する.

### Node.js

```javascript

getSlackPosts () {
  axios
    .get(`${this.slackApiUrl}?token=${slackApiToken}&channel=${slackChannelId}&count=${slackPostCount}`)
    .then(function (response) {
      var messages = response.data.messages;
      for (var message of messages) {
        // 取得したポストの中で、最新の有効な設定 (時刻情報 or OFF) を探す.
        if (checkSettingValue(message.text).valid) {
          var latestPost = message;
          break;
        }
        else {
          continue;
        }
      }
    })
}
```

# 宅内サーバで家電のONタイマーを設定する.

## コード概要 (Hueライトタイマー部分のみ抜粋)

大まかには、以下のような流れで処理が動きます.

1. Slackから時刻設定情報を取得 (例えば6:30、OFFなど).
2. transitiontime (設定時刻に対して何分前に家電をONにするか) を考慮して、HueのAPIを叩く時間を計算する. 例えばtransitiontimeが15分の場合、6:15が実際にAPIを叩く時間になる.
3. node-scheduleのタイマーの設定を6:15にして登録する.

```javascript

var axios = require('axios');
var schedule = require('node-schedule');
var ApiScheduler = require('./api-scheduler');
const setting = require('./setting.json');

class WakeUpRoutine extends ApiScheduler {
  constructor(setting) {
    // 各種設定の読み込み.
    ...
  }

  start () {
    // 各家電のタイマーを適当な設定で初期化.
    this.jobInvokeHue = 
      schedule.scheduleJob('0 0 0 1 1 0', function() {
      this.turnOnHueTargetGroup()
    }.bind(this));

    // 一旦タイマーをキャンセル.
    this.jobInvokeHue.cancel();

    // Slackの投稿をチェックするタイマーを設定. (下記　* * * * * は毎分確認)
    var jobCheckSlack = schedule.scheduleJob('* * * * *', function() {

      axios
        .get(`${this.slackApiUrl}?token=${this.slackApiToken}&channel=${this.slackChannelId}&count=${this.slackPostCount}`)
        .then(function (response) {
          var messages = response.data.messages;
          for (var message of messages) {
            if (this.checkSettingValue(message.text).valid) {
              var latestPost = message;
              break;
            }
            else {
              continue;
            }
          }

          ...
          // 有効な設定情報が得られた場合にタイマーを更新する.
          // HueライトのONタイマーを設定
          this.setInvokeTimer(
            'Hue',
            this.jobInvokeHue,
            targetTimeHue,
            this.msecTransTimeHue,
          );
        }.bind(this))
        .catch(e => {console.log(e)})
    }.bind(this))
  }
 
  turnOnHueTargetGroup () {
    // Hue API.
    var hueApi = `http://${this.hueBridgeIp}/api/${this.username}/groups/${this.groupId}/action`

    axios
      .put(hueApi, {
        "on": true,
        "transitiontime":  Math.round(this.msecTransTimeHue/100),
        "bri": this.brightness
      })
      .then(function (response) {
        console.info(
          `[INFO] Hue light API has been invoked at ${new Date()}.`
        )
      })
      .catch(e => {console.log(e)})
  }

  setInvokeTimer (name, timer, targetTime, transitionTime) {
    // 設定時間に対してtransitionTimeだけ早くタイマーを設定する.
    var [hour, minute] = this.calculateInvokeTime(targetTime, transitionTime)
    
    // タイマーを更新.
    timer.reschedule(`${minute} ${hour} * * *`)

    console.log(
      `[INFO] ${name} timer setting has been updated. New invoke time is ${hour}:${minute}.`
    )
  }
}

var scheduler = new WakeUpRoutine(setting)
scheduler.start()
```

## コード全体

[GitHub - wakeup-scheduler](https://github.com/kei3238/wakeup-scheduler)

## コンソール出力
Slackの時刻設定情報 (投稿) が更新されたときと、実際にタイマーで設定された時間になってAPIが叩かれたときに通知が出ます.

```
$ node server.js
[INFO] New target time has been set to 06:30
[INFO] Hue timer setting has been updated. New invoke time is 06:15.
[INFO] New target time has been set to 19:00
[INFO] Hue timer setting has been updated. New invoke time is 18:45.
[INFO] Hue light API has been invoked at Sat Feb 16 2019 18:45:00 GMT+0900 (JST).
```