SwitchBot APIを用いて帰宅前にエアコンをつけるか尋ねるLINE Botを作成する

# 今日も寒いですね⛄

昨日今日で今季最強寒波が来てるというニュースを見ました。

こんなに寒いと家に出るのも億劫ですが、家に帰った時に部屋が暖かいとちょっと嬉しいですよね。

帰宅を迎えてくれる家族がいればいいですが、あいにくうちは単身世帯。

SwitchBot を導入したので遠隔からエアコンのオンオフが出来るんですが、つけるのを忘れて帰宅して「また忘れた～！」となることを何回も経験しています。

# 「忘れてしまうのなら自動化してしまおう！」

ありがたいことにSwitchBot公式からSwitchBot  APIというものが出ています。

https://github.com/OpenWonderLabs/SwitchBotAPI

これを使えば、エアコンのオンオフが自動化できます。

帰宅前にトリガー実行してやれば毎回暖かい家に帰れるのです、、、！

# 用意するもの
今回はGoogle Apps Script(GAS)でAPIの呼び出しをトリガー実行するようにしたいと思います。
SwitchBot製品やエアコンはあらかじめSwitchbotアプリに登録しておいてください。

・Googleアカウント
・SwitchBot スマートリモコン ハブミニ
・SwitchBot 温湿度計
・赤外線リモコンで動くエアコン

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451285/9cee6ff4-b181-220f-d537-9186b43abb4a.jpeg" width="500">

[画像引用元：Amazon.co.jp（SwitchBot スマートリモコン ハブミニ）](https://www.amazon.co.jp/dp/B07TTH5TMW)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451285/473f2385-99ac-c4d0-e8f7-ac8fc6922f21.jpeg" width="500">


[画像引用元：Amazon.co.jp（SwitchBot 温湿度計）](https://www.amazon.co.jp/dp/B07L4QNZVF)


# 手順
## SwitchbotのAPIトークンとシークレットの取得
SwitchbotのAPIを呼び出すにあたり、APIトークンとシークレットというものが必要です。

これは、SwitchBot アプリの「設定」→「アプリバージョン」を連打→「開発者向けオプション」から入手することができます。APIの呼び出しの度に必要なのでメモっておきましょう。

**注意：APIトークンとシークレットが他人に知れると勝手に家電のオンオフをされてしまいます**

参考：https://qiita.com/youtoy/items/b18ffba932937f20fc7c

## 署名を作成する関数の作成
SwitchBotAPIには、Ver1.0とVer1.1があって、現状ではどちらでも使えるようです。Ver1.0とVer1.1の違いは[こちら](https://blog.switchbot.jp/announcement/api-v1-1/)を確認してください。簡単にまとめるとよりセキュアになった感じです。Switchbotロックが販売されたことによりセキュリティ面を強化したかったのだと思います。（なので認証が少しややこしくなっています、、）今回はせっかくなのでVer.1.1を使ってみます。Ver.1.1では、APIの呼び出しに署名を追加する必要があります。

署名の作成方法は以下です。

1. 「トークン + UNIXタイムスタンプ + ナンス（任意の文字列）」の文字列を作成する。
1. 1.の文字列をシークレットを使ってHMACSHA256変換する。
1. 2.を大文字に変換する

GAS上では以下のコードの```getUnixTimeString()```でUNIXタイムスタンプの取得、```makeNonce()```でナンスの生成、```getSignature()```で署名の生成ができます。```token```と```sectret```はひとつ前の手順でアプリ上から取得したものを使います。

```javascript:getSignature.gs
const token = 'アプリ上で取得したトークン'
const sectret = 'アプリ上で取得したシークレット'

// SwitchBotAPIの署名を発行する関数
function getSignature(timestamp, nonce) {
  const signature = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_256, token + timestamp + nonce, secret)
  return Utilities.base64Encode(signature).toUpperCase()
}

// 現在のUNIXTIMEを文字列で返す関数
function getUnixTimeString() {
  const date = new Date();
  return date.getTime().toString()
}

// ナンスとしてUUIDを生成する関数
function makeNonce() {
  return Utilities.getUuid();
}

```

生成した署名はAPIを呼び出すときのヘッダーに指定してあげる必要があります。

## デバイス情報の取得
ここから、APIを呼び出していきます。まずは操作するデバイスの情報を取得していきます。APIを呼び出してデバイス情報一覧を取得します。GASで以下のスクリプトを実行します。

```javascript:getDeviceInfo.gs
function getDeviceInfo() {
  const nonce = makeNonce()
  const timestamp = getUnixTimeString()
  const path = '/v1.1/devices'
  const sign = getSignature(timestamp, nonce)
  const headers = {
    'Authorization': token,
    'sign': sign,
    't': timestamp,
    'nonce': nonce
  }
  const options = {
    'method': 'GET',
    'headers': headers,
  };
  const res = UrlFetchApp.fetch('https://api.switch-bot.com' + path, options).getContentText()
  Logger.log(res)
}

```

レスポンスで以下のようなjsonが返ってきます。操作したいのは、温湿度計の"Meter 92"とエアコンなので、この二つの```deviceId```をメモしておきます。

```
{
	"statusCode": 100,
	"body": {
		"deviceList": [
			{
				"deviceId": "xxxxxxxxxxxx",
				"deviceName": "Meter 92",
				"deviceType": "Meter",
				"enableCloudService": true,
				"hubDeviceId": "000000000000"
			},
			{
				"deviceId": "xxxxxxxxxxxx",
				"deviceName": "Hub Mini A4",
				"deviceType": "Hub Mini",
				"enableCloudService": false,
				"hubDeviceId": "000000000000"
			}
		],
		"infraredRemoteList": [
			{
				"deviceId": "01-000000000000-00000000",
				"deviceName": "エアコン",
				"remoteType": "Air Conditioner",
				"hubDeviceId": "xxxxxxxxxxxx"
			}
		]
	},
	"message": "success"
}
```

## 温湿度情報を取得する関数の作成
今度は、APIを呼び出して温湿度計の情報を取得し、戻り値として返す関数を作成します。

GASで以下のスクリプトを実行します。```deviceId```には先ほど取得した温湿度計のものを使用します。

```javascript:getTempandHumi.gs
function getTempandHumi() {
  const deviceId = 'xxxxxxxxxxxx' //温湿度計のデバイスID

  const path = '/v1.1/devices/' + deviceId + '/status'
  const nonce = makeNonce()
  const timestamp = getUnixTimeString()
  const sign = getSignature(timestamp, nonce)
  const headers = {
    'Authorization': token,
    'sign': sign,
    't': timestamp,
    'nonce': nonce
  }
  const options = {
    'method': 'GET',
    'headers': headers,
  };
  const res = UrlFetchApp.fetch('https://api.switch-bot.com' + path, options).getContentText()
  Logger.log(res)
  jsonbody = JSON.parse(res)["body"]
  now = getCurrentTimeString()
  temp = jsonbody["temperature"]
  humi = jsonbody["humidity"]
  return { now, temp, humi }
}
```

これで、温度、湿度を取得することができました。

最後のreturnの行を ```SpreadsheetApp.getActive().getSheetByName("シート1").appendRow([now, temp, humi])```などにして、GASでトリガーを設定すればスプレッドシートに温湿度を記録し続けることもできます。私の部屋は毎分温湿度を取得するようにしてます。（SwitchBotのAPI上限が10000/日なのでそこだけ注意です。）

## エアコンを操作する関数の作成
いよいよエアコンの操作するAPIの呼び出しに移りたいと思います。GASでの関数は以下のようになります。```deviceId```には先ほど取得したエアコンのものを使用します。

```javascript:controlAircon.gs
function controlAircon(temperature, mode, fanspeed, powerstate) {
  // input parameter
  // temperature：the unit of temperature is in celsius. e.g.) "26"
  // mode："1" (auto), "2" (cool), "3" (dry), "4" (fan), "5" (heat)
  // fanspeed："1" (auto), "2" (low), "3" (medium), "4" (high)
  // power state："on",  "off"

  const deviceId = '01-000000000000-00000000' //エアコンのデバイスID

  const path = '/v1.1/devices/' + deviceId + '/commands'
  const nonce = makeNonce()
  const timestamp = getUnixTimeString()
  const sign = getSignature(timestamp, nonce)

  const request_body = {
    "command": "setAll",
    "parameter": temperature + "," + mode + "," + fanspeed + "," + powerstate,
    "commandType": "command"
  }

  payload = JSON.stringify(request_body)

  const headers = {
    'Authorization': token,
    'sign': sign,
    't': timestamp,
    'nonce': nonce,
  }

  const options = {
    'method': 'POST',
    'headers': headers,
    'payload': payload,
    'contentType': 'application/json; charset=utf-8',
  };

  const res = UrlFetchApp.fetch('https://api.switch-bot.com' + path, options)
  Logger.log(res)
}

```

注意点として、```request_body```の```parameter```ですが、設定温度、モード（"1" が自動, "2" が冷房, "3" が除湿, "4" が送風, "5"が暖房）、ファンスピード（"1" が自動, "2" が低, "3"が中, "4"が高）、パワーステート（"on"または"off"）を文字列としてくっつけて与えてやる必要があります。詳しくは[SwitchBotAPIのGitHub](https://github.com/OpenWonderLabs/SwitchBotAPI#send-device-control-commands:~:text=with%20commandType%3Dcustomize-,Air%20Conditioner,-command)を解読してください。

## トリガー実行する関数の作成
温湿度を取得する準備とエアコンを操作する準備が整ったのでトリガー実行する関数を作っていきます。
以下のように、室温が20℃以下だったら、エアコンを26℃の暖房で設定するようにしてみました。

```javascript:AirconOn.gs
function AirconOn() {
  const { now, temp, humi } = getTempandHumi()
  if (Number(temp) < 20.0) {
    controlAircon("26", "5", "1", "on")
  }
}
```
これを帰宅前の時間に合わせてトリガー実行してやれば、帰宅前に自動でエアコンをつけておくことが可能になりました！


# これで解決と思いきや、、、
ここまで読んで途中でお気付きの方も多いかもしれませんが、このままだと室温が20℃以下の時は毎日起動してしまいます。ちょっと出張や旅行に行って帰ってきたらエアコンがつけっぱなしだったなんてことが起きてしまったら精神的ダメージは計り知れません。

ここはLINEを用いて少し改良してみたいと思います

# 帰宅前にエアコンをつけるか尋ねるLINE Botを作成する
LINE公式アカウントを用いて帰宅前にエアコンをつけるか尋ねるLINE Botを作成していきます。結局操作が必要になってしまい本末転倒感は否めませんが、LINEで通知があれば見る機会がぐっと上がりますし、一応理にかなっていると思っています。

今回は、エアコンをつけるかどうかの問いをLINE botから送信し、クイックリプライで返答できるように実装してみました。参考：https://developers.line.biz/ja/docs/messaging-api/using-quick-reply/

```CHANNEL_ACCESS_TOKEN```と```uid```には、[LINE Developers](https://developers.line.biz/ja/)上で、公式アカウントを作成して管理画面から得られるチャンネルアクセストークンと「あなたのユーザーID」として表示されるIDを取得し入力します。

GAS上でここまで出てきた関数群をWebアプリとしてデプロイしてあげて、LINE DevelopersでWebhookに設定してあげて、GAS上で帰宅時間前に、```askAirconOn()```をトリガー実行してあげれば完成です！


```javascript:linebot.gs
// LINEチャンネルアクセストークン
const CHANNEL_ACCESS_TOKEN = 'XXXXXXXXXXXXXXX' // LINE公式アカウントのチャンネルアクセストークン
const uid = 'XXXXXXXXXXXXXXX'// LINE developerで「あなたのユーザーID」として表示されるID

function askAirconOn() {
  const { now, temp, humi } = getTempandHumi()
  if (Number(temp) < 20.0) {
    const text = "お部屋が寒くなっているみたいですが、暖房をつけますか？"
    const quickreply = {
      "items": [
        {
          "type": "action",
          "action": {
            "type": "postback",
            "label": "Yes",
            "data": "AirconOn",
            "displayText": "Yes",
          }
        },
        {
          "type": "action",
          "action": {
            "type": "postback",
            "label": "No",
            "data": "doNothing",
            "displayText": "No",
          }
        }
      ]
    }
    sendQuickreplyTextToUser(uid, text, quickreply)
  }
}

function doPost(request) {
  // POSTリクエストをJSONデータにパース
  const receiveJSON = JSON.parse(request.postData.contents);
  for (i = 0; i < receiveJSON.events.length; i++) {
    const event = receiveJSON.events[i];
    if (event.type == "postback") { // postbackイベントなら
      replyToken = event.replyToken
      if (event.postback.data == "AirconOn") {
        controlAircon("26", "5", "1", "on")
        replyToUser(replyToken, "エアコンをオンにしました。")
      } else if (event.postback.data == "doNothing") {
        replyToUser(replyToken, "承知しました。")
      }
    }
  }
}

//LINE公式アカウントから任意のアカウントに対してクイックリプライ付きメッセージを送る関数
function sendQuickreplyTextToUser(uid, text, quickreply) {
  const message = {
    "to": uid,
    "messages": [{
      "type": "text",
      "text": text,
      "quickReply": quickreply
    }]
  }
  const options = {
    "method": "post",
    "headers":
    {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
    },
    "payload": JSON.stringify(message)
  };
  // LINE該当ユーザーに送信
  UrlFetchApp.fetch("https://api.line.me/v2/bot/message/push", options)
}

//LINE公式アカウントからリプライを送る関数
function replyToUser(replyToken, text) {
  const replyText = {
    "replyToken": replyToken,
    "messages": [{
      "type": "text",
      "text": text,
    }]
  }
  const options = {
    "method": "post",
    "headers":
    {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
    },
    "payload": JSON.stringify(replyText)
  };
  // LINE該当ユーザーに送信
  UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
}

```

# さいごに
長くなってしまいましたが、完成イメージとしてはこんな感じです。
「Yes」をタップするとエアコンがつきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451285/de377a47-4dba-ae24-4396-9a75e6d8aceb.jpeg" width="500">

他にもエアコンの温度の細やかな設定や、[リッチメニュー](https://developers.line.biz/ja/docs/messaging-api/using-rich-menus/)から操作できるようにしたり、[トリガー実行を決まった時刻に実行](https://qiita.com/shobota/items/93f293f1cc2797d5a151)したりあるいはスヌーズ機能をつけたりと出来ることがいろいろありそうです。

GASを使うと無料でできてしまうのがいいですね。

冬場のエアコン以外にも色々なものや状況に使えると思うので是非お試し下さい！


(2021/12/20 修正) SwitchBotの綴りを間違えてたのを修正しました…