【SwitchBot API+ GAS】温度と湿度から不快指数を快適に保つ！　エアコンの自動操作

<h1>きっかけ</h1>
最近、我が家では「SwitchBot」というスマート家電でエアコンを操作しています。アプリでは「室温が27度以上なら、冷房を25度に設定する」という設定をして、自動で26度近くを保っています。

しかし、湿度が低ければ26℃でも快適に感じますが、湿度が高いと26℃でも不快に感じることがあります。そのため**不快指数を計算した上で、エアコン設定をする**必要があると感じました。

<strong>不快指数とは？</strong>
不快指数は、「日中の蒸し暑さ」を表したものです。数字が大きいほど蒸し暑く不快で、65~70が快適とされています。そこから小さくなるほど、寒く不快となります。

気温と湿度から計算できます。
不快指数=0.81×気温+0.01×湿度×(0.99×気温-14.3)+46.3

| 不快指数 | 感じ方 |
|:-:|:-:|
| 〜55  | 寒い |
| 55〜60  |  肌寒い |
| 60〜65  |  何も感じない |
| 65〜70  |  快適⭐️←ここを常に保ちたい！ |
| 70〜75  |  人によっては暑い|
| 75〜80  |  半数以上が暑い|
| 80~  | 暑い |

↑[Wikipedia](https://ja.wikipedia.org/wiki/%E4%B8%8D%E5%BF%AB%E6%8C%87%E6%95%B0)より引用

この記事では
不快指数を使って快適な温度を自動で保つ仕組みを作っていこうと思います！

<h1>準備するもの</h1>
・SwitchBotハブ２（ハブと温湿度計がセットになったもの）


# 目次
- **[なぜAPIを使うのか](#なぜAPIを使うのか)**
- **[実行環境](#実行環境)**
- **[処理の流れ](#処理の流れ)**
- **SwitchBot APIの処理**
    - [トークンを取得する](#トークンを取得する)
    - [現在の温湿度を取得する](#現在の温湿度を取得する)
    - [エアコンを操作する](#エアコンを操作する)
- **[不快指数を計算する](#不快指数を計算する)**
- **GASでのデータの保存と取得**
    - [保存する](#保存する)
    - [保存したものを取得する](#保存したものを取得する)
- **[処理を組み合わせる](#処理を組み合わせる)**
- **[作った処理を5分ごとに呼び出す](#作った処理を5分ごとに呼び出す)**
   




<h1>なぜAPIを使うのか</h1>
「SwitchBotアプリだけで十分なんじゃないか」と思う方もいらっしゃるかもしれません。確かに、SwitchBotアプリは非常に使いやすく、処理に詳しくない方でも直感的に操作できます。しかし、そのカスタマイズ幅には限界があります。

例えば、SwitchBotアプリでは「不快指数」の計算など、特定の条件に基づく複雑な処理を行うのは難しいです。アプリの機能だけでは対応できないような細かいカスタマイズが必要な場合、APIを使って他のサーバーで処理することが有効です。

図で違いを表すとこうなります↓
![last 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/7050d0da-1cde-90ac-f151-ee0f5e0fc9a7.png)

![スクリーンショット 2024-08-15 14.30.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/ebf8361a-12de-2636-8613-67422daf2d2c.png)


「別のサーバー」というのは、処理を行う場所を用意すればなんでもいいです。おすすめなのが、環境構築が簡単なタイプです。Google Apps Script, AWS Lamda, Azure Functions などがあります。これらは、サーバーの設定や管理が不要で、すぐに作業を始められるので、特に便利です。この中でもGoogle Apps Scriptを選んだ理由については、次の実行環境にてお伝えします。


<h1>実行環境</h1>

このプロジェクトでは、無料で使えるGoogle Apps Script（GAS）を利用します。GASは、Googleが提供するオンラインのツールで、特別な知識がなくても簡単に使うことができます。
GASを使う理由は、2つです。


**１. 手軽さ**
GASは、Googleのサーバー上で直接動作するため、特別なサーバーの設定や管理が必要ない。つまり、前準備がとても楽！ 必要なのはGoogleアカウントのみ。

**2. 無料範囲内で処理が収まる**
無料アカウントの制限は1日に90分の処理時間。
例えば、5分ごとに処理を呼び出すと、1日に288回実行することになる。90分（5400秒）で割ると、各処理が18.75秒以内で終われば、無料枠で収まる計算。今回の場合、無料アカウント範囲内で十分処理が行える！



<h1>処理の流れ</h1>
以下の手順で、快適な温度を保つことができます。

![スクリーンショット 2024-08-15 14.54.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/59c8b7f6-d3f8-4083-3b10-38122453a331.png)


1. 現在の室温と湿度を取得する
2. 快適さを計算する
3. もし部屋が「快適」でない場合、エアコンの設定を変更する
    - 冷房が効きすぎている場合: 温度を1度上げる
    - 冷房の効きが弱い場合: 温度を1度下げる
    - 暖房が効きすぎている場合: 温度を1度下げる
    - 暖房の効きが弱い場合: 温度を1度上げる
    - 前回のエアコン操作から、そんなに経過していない場合（今回は15分間）は、操作をスキップ
4. このプロセスを5分ごとに繰り返す


ざっくりいうと、これで自然と「快適」の状態を保つように暖房や冷房の設定を自動的に行えます。


<h1>トークンを取得する</h1>

SwitchBot APIを操作するためには、SwitchAPIトークンを取得する必要があります。
「トークン」とは鍵のようなものです。鍵を持っていないと家に入れないように、このトークンを持っていないと、SwitchBotに繋がった家電を操作できません。鍵がなくても家に入れたら困りますよね。
同じように、自分のトークンを持っている人だけが、自分の家の家電を操作できるようになっています。

**トークンの取得の仕方**

![編集後トークン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/b157612c-a906-2217-18c6-db6b2939aaaa.png)


このトークンとクライアントシークレットは、SwitchBot APIを操作するたびに使用します。後で必要になります。


<h1>現在の温湿度を取得する</h1>

↓こちら記事を参考にさせていただいてます。

https://qiita.com/katta1024/items/bfd420329f48dce898d5




現在の温湿度を取得するためには、まず温湿度計のデバイスIDを取得する必要があります。


**① SwitchBotで管理しているデバイス一覧を取得して、温湿度センサのDeviceIDを見つける(初回のみ)**
```js:code.gs
const token = "トークン" // ⭐️ 先ほど取得したトークンを入力
const secret = "クライアントシークレット" // ⭐️先ほど取得したクライアントシークレットを入力


// 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();
}

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)
}

```
↓ これを実行します。
```
{
    "statusCode":100,
    "body":
    {"deviceList":[
        {"deviceId":"FA0010001000", ←⭐️温湿度を取得するのに必要。コピペ
        "deviceName":"ハブ2",
        "deviceType":"Hub 2",
        "enableCloudService":true,
        "hubDeviceId":"000000000000"}
    ],
    "infraredRemoteList":[
        {"deviceId":"02-123456789012-12345678",←⭐️エアコンを取得するのに必要。コピペ
        "deviceName":"エアコン",
        "remoteType":"Air Conditioner",
        "hubDeviceId":"FA2695A28085"}
    ]},
    "message":"success"
}
```

ハブ2のデバイスIDを取得できました。これを使って、デバイス状態を取得できます。
あとでエアコンを操作するのに必要なデバイスIDもどこかにコピペして、とっておきます。


**② デバイス状態（温度と湿度）を取得する**
先ほどと同様、[SwitchBot APIを用いて帰宅前にエアコンをつけるか尋ねるLINE Botを作成する](https://qiita.com/katta1024/items/bfd420329f48dce898d5)の記事を参考にさせていただいています。

```js:code.gs
function fetchCurrentTempAndHumid() {
  const deviceId = 'FA0010001000' // ←⭐️取得した温湿度計のデバイス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"]
  temp = jsonbody["temperature"]
  humi = jsonbody["humidity"]
  return {
    temp: temp,
    humi: humi
  };
}
```
これを実行すると、現在の温度と湿度が取得できました。
```
{
    "statusCode":100,
    "body":{
        "deviceId":"デバイスID",
        "deviceType":"Hub 2",
        "hubDeviceId":"ハブデバイスID",
        "humidity":72, // ⭐️湿度
        "temperature":25.8, // ⭐️温度
        "lightLevel":5,
        "version":"V1.0-1.1"
    },
    "message":"success"
}
```

<h1>エアコンを操作する</h1>

[SwitchBot APIのドキュメント](https://github.com/OpenWonderLabs/SwitchBotAPI/blob/main/README-v1.0.md#infrared-remote-device-example)曰く、以下のように設定するそうです

>~コマンドパラメータ~
{温度},{モード},{ファン速度},{電源状態}
例26,1,3,on

>~説明~
温度の単位は摂氏です。
モードには 1 (自動)、2 (冷房)、3 (乾燥)、4 (ファン)、5 (暖房) があります。
ファン速度には 1 (自動)、2 (低速)、3 (中速)、4 (高速) があります。
電源状態にはオンとオフがあります。

そのため、
温度設定: 「26」度
モード: 「2」(冷房モード)
風速: 「3」(中程度の風速)
状態: 「on」(エアコンを起動)
というような指示を出します。ここでは基本風力を３に設定しています。

```js:code.gs
// エアコン（冷房もしくは暖房）を指定した温度で稼働させる && 指示内容を保存
function operateAircon(airconType, temp){
  mode = 1 // モード：自動
  powerStatus = "off" // デフォルトは電源OFFとする
  windPower = 3 // 風力：中　に固定
  
  switch (airconType) {
    case "暖房":
      mode = 5;
      powerStatus = "on";
      break;
    case "冷房":
      mode = 2;
      powerStatus = "on";
      break;
  }

  const deviceId = '02-123456789012-12345678' // ⭐️取得したエアコンのデバイスIDを入力

  const path = '/v1.1/devices/' + deviceId + '/commands'
  const nonce = makeNonce()
  const timestamp = getUnixTimeString()
  const sign = getSignature(timestamp, nonce)
  const headers = {
    'Authorization': token,
    'sign': sign,
    't': timestamp,
    'nonce': nonce
  }
  const payload = JSON.stringify({
    'command': 'setAll',
    'parameter': `${temp},${mode},${windPower},${powerStatus}`,
    'commandType': 'command',
  });
  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).getContentText()
  Logger.log(res)
  // エアコンに指示を送った内容を保存
  saveAirConStatus(airconType, temp)
}
```


<h1>不快指数を計算する</h1>
不快指数=0.81×気温+0.01×湿度×(0.99×気温-14.3)+46.3
これをそのまま関数にします。

```js:code.gs
// 不快指数を計算して返す
function calcDiscomfortIndex(temp, humid) {
    const discomfort_index = 0.81 * temp + 0.01 * humi * (0.99 * temp - 14.3) + 46.3;
    Logger.log('不快指数: ' + discomfort_index);
    return discomfort_index
}
```
部屋の温度が25.8度、湿度が72%だと、不快指数は75.29..になります。
これは、不快指数：65~70が快適なので、「半数以上が暑い」と感じる不快レベルです。



<h1>保存する</h1>

２つの保存する処理を書いていきます。

**・前回エアコン操作した時刻と内容を取得する**
**・現在の気温を保存**

こちらは、SwitchBotAPIに備わっている部分ではないため、こちらで実装する必要があります。
GASのデータ保存には、いくつか方法がありますが、今回は「小さなデータを長期的に保存したい」ので、PropertiesServiceを使用します。

保存されたデータは、「設定」→「スクリプトプロパティ」から見ることができます　

![保存したものを見る.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/ff70789d-4891-af66-afb8-8f51c80188bb.png)



以下のように、「保存（更新含む）」が簡単にできます。
① 気温を保存する
```js:code.gs
function saveCurrentTemp(temp){
  PropertiesService.getScriptProperties().setProperty('CURRENT_TEMP', temp);
}
```

②　エアコン操作した時刻と操作した内容を保存する
```js:code.gs
function saveAirConStatus(airconType, temp) {
    var now = new Date();
    var timestamp = now.getTime(); // ミリ秒単位のタイムスタンプ

  // 辞書型のオブジェクトを作成
  var lastAirconStatus = {
    'timestamp': timestamp,
    'mode': airconType,
    'temp': temp
  };
  
  // オブジェクトをJSON形式の文字列に変換
  var jsonString = JSON.stringify(lastAirconStatus);

  // 保存
  PropertiesService.getScriptProperties().setProperty('LAST_AIRCON_STATUS', jsonString);
}
```



<h1>保存したものを取得する</h1>

「取得」も簡単にできます。

① 前回の実行時の気温を取得する
```js:code.gs
function getPreTemp(){
    preTemp = PropertiesService.getScriptProperties().getProperty('CURRENT_TEMP');
    return preTemp
}
```

② 前回エアコン操作した時刻と操作した内容を取得する処理
```js:code.gs
// airconStatus = {"timestamp":1723635029720,"mode":"冷房","temp":25}のようなデータを取得
function getAirconStatus() {
  // スクリプトプロパティからJSON文字列を取得
  var jsonString = PropertiesService.getScriptProperties().getProperty('LAST_AIRCON_STATUS');
  
  if (jsonString) {
    // JSON文字列をJavaScriptのオブジェクトに変換
    var dictionary = JSON.parse(jsonString);
    return dictionary
  } else {
  // データ取得できない場合はnullを返す
    return null
  }
}
```

<h1>処理を組み合わせる</h1>

```js:code.gs
const coolDownPeriod = 15 * 60 * 1000; // 連続操作禁止時間（今回は15分間）
const firstHeatingTemp = 25; // 初期設定温度（暖房）
const firstCoolingTemp = 28; // 初期設定温度（冷房）
const tempChangeThreshold = 1; // エアコン操作のしきい値（1度未満の変化の場合はエアコンが効いてないと判断し再度エアコンを操作）

// 5分ごとに呼び出す処理
function keepConfortably() {
  // 現在の温度と湿度を取得
  const { temp: currentTemp, humi: currentHumid }= fetchCurrentTempAndHumid()
  // 不快指数を求める
  var discomfortIndex = calcDiscomfortIndex(currentTemp, currentHumid);
  // 前回の実行時の温度を取得
  var preTemp = getPreTemp();
  // 現在の温度を保存
  saveCurrentTemp(currentTemp);
  if (preTemp == null) {
    Logger.log("前回の温度がないため、次の実行から調整します。")
    // 初回実行時は前回の温度がないため、何もしない
    return
  }
  if (discomfortIndex >= 65 && discomfortIndex <= 70) {
    Logger.log("快適です。")
    // 快適の場合は何もしない
    return;
  }

  // エアコンの状態を取得
  var airconStatus = getAirconStatus();
  if (airconStatus == null){
    // 初回は前回のエアコン状況のデータがないため、仮の値を設定する
    airconStatus = {mode: "停止", temp:25, timestamp:0}
  }
  var airconMode = airconStatus.mode; // 冷房もしくは暖房
  var lastOperatedTemp = airconStatus.temp // 最後にエアコン操作した時の指示温度
  var lastOperatedTimestamp = airconStatus.timestamp // 最後にエアコン操作した時刻

  if (airconMode == '停止') {
    if (discomfortIndex > 60 && discomfortIndex < 75) {
      Logger.log("快適ではありませんが、不快でもない範囲です。")
      // 不快ではない（快適に近い）ため、エアコン操作しない
      return
    }
    // エアコンが稼働していない場合はこの分岐に入る
    if (discomfortIndex >= 70) {
      // 不快指数が70以上（快適より暑い）かつエアコンが稼働していない場合、暖房をつける
      operateAircon('暖房', firstHeatingTemp);
      Logger.log("寒いです。暖房をつけました")
    } else {
      // 不快指数が70以上（快適より暑い）かつエアコンが稼働していない場合、冷房をつける
      operateAircon('冷房', firstCoolingTemp);
      Logger.log("暑いです。冷房をつけました")
    }
    return
  }

  // エアコンが稼働している場合、この分岐を通る
  // 前回のリモコン操作から連続操作禁止時間内であれば何もしない
  var now = new Date();
  var nowTimestamp = now.getTime(); // ミリ秒単位のタイムスタンプ
  if(nowTimestamp - lastOperatedTimestamp < coolDownPeriod) {
    // 前回の操作から15分間の間はエアコンの効きを待つため、何もしない
    Logger.log("前回の操作から15分経過していないため、操作しませんでした")
    return
  }

  if (discomfortIndex >= 70) {
    if (airconMode == '暖房') {
      // 暖房が効きすぎて暑い場合、暖房を１度下げる
      operateAircon('暖房', lastOperatedTemp - 1);
      Logger.log("暖房が効きすぎて暑いです。１度下げました")
    }else{
      // 冷房が稼働しているが暑い場合、この分岐を通る
      // 前回の実行時の温度から１度以上上がっている場合、エアコンが効き始めているためエアコン操作しない
      if (currentTemp - preTemp >= 1) {
          Logger.log("エアコンが効き始めています。待機します。")
          return
      }
      // 前回取得した温度から変化があまりない場合、冷房を１度下げる
      operateAircon('冷房', lastOperatedTemp - 1);
      Logger.log("冷房の効きが悪いため、１度下げました")
    }
  } else {
    if (airconMode == '冷房') {
      // 寒いかつ冷房が稼働している（効きすぎている）場合、冷房を１度上げる
      operateAircon('冷房', lastOperatedTemp + 1);
      Logger.log("冷房が効きすぎて寒いです。１度上げました")
    }else{  
      // 暖房が稼働しているが寒い場合、この分岐を通る
      // 前回の実行時の温度から１度以上下がっている場合、エアコンが効き始めているため、エアコン操作しない
      if (preTemp - currentTemp >= 1) {
          Logger.log("エアコンが効き始めています。待機します。")
          return
      }
      // 前回取得した温度から変化があまりない場合、暖房を１度上げる
      operateAircon('暖房', lastOperatedTemp + 1);
      Logger.log("暖房の効きが悪いため、１度上げました")
    }
  }    
}
```

<h1>作った処理を5分ごとに呼び出す</h1>　
ここまでで作った処理を五分ごとに呼び出す必要があります。
テストとして、ログ出力するだけの関数を作り、それを5分毎に呼び出します。

```js:code.gs
function createTimeDrivenTrigger() {
  // 既存のトリガーを削除（重複を防ぐため）
  const triggers = ScriptApp.getProjectTriggers();
  for (const trigger of triggers) {
    ScriptApp.deleteTrigger(trigger);
  }
  
  // 新しいトリガーを作成
  ScriptApp.newTrigger('keepConfortably') // ←関数名に合わせて変更⭐️
    .timeBased()
    .everyMinutes(5)
    .create();
}
```
この関数を実行することで、5分おきに処理が実行されます。
![全体実行度合い.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/bb0b3993-75e5-8fd8-f814-b1d0e32f2f86.png)




<h1>やってみた感想と結論</h1>

```
実行ログ

2024/08/14 21:56:50	情報	不快指数: 75.71119999999999 冷房の効きが悪いため、１度下げました
2024/08/14 22:01:50	情報	不快指数: 74.7668
2024/08/14 22:06:50	情報	不快指数: 74.05912
2024/08/14 22:16:51	情報	不快指数: 73.06272 冷房の効きが悪いため、１度下げました
⭐️「快適」範囲内なのに、寒い！
```

Wikipedia先生の言う「快適」な状態に部屋を保つことができましたが、私の座る場所はエアコンの風がダイレクトアタックする場所にあり、鳥肌が立つ寒さでした…。

さらに、温湿度センサーはエアコンの風が当たりにくい場所に設置してあったため、エアコンの効力を感知するのが一拍遅れてしまいました。その結果、私は「寒い寒い」と言いながら我慢し続け、センサーが「快適」な温度に達するのをひたすら待っていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747635/3a965f8f-22fa-4d11-584f-eb664f4bcc2d.png)


このことから、エアコンの風の巡回具合、服装の違い（セーター vs. タンクトップなど）などの**環境や個人差が影響し、不快指数の「快適」と個人の「快適」は必ずしも一致しない**ことがわかりました。完全なる自動調整は難しいですが、その難しさを理解できたのはよかったです。

今回は思うような結果が得られませんでしたが、「快適」を感じるためには多くの要素が関わることに気づきました。現在では、IoT技術を活用して「風力の調整」や「人の心拍数を測定する指輪」などが利用できるようになっています。また、「服装指数」などのデータも公開されており、これにより「不快指数」だけでなく、さまざまな尺度を基に本当の「快適」を実現する可能性が広がっています。

「不快指数」だけに限らず、ありとあらゆる尺度をもってすれば、いつか本当の「快適」を実現できるかもしれません。

いかがだったでしょうか？　皆さんもぜひ、自分にとっての「快適」を探るために、さまざまな技術やデータを試してみてください。一緒に「快適」を追求していきましょう！