普通の加湿器をラズパイでスマートにしてHomeKitにつなげる

# はじめに

このご時世、加湿器は大切です。
このご時世でなくても、東京などの雪の降らない地方の冬は乾燥しやすいために加湿器は必須といえます。

今回は以下のものを組み合わせ、加湿器をスマートにしてSiriから制御することができるようにします。

* 普通の加湿器
* WiFiで制御できるコンセント
* Raspberry Pi
* 温度湿度センサー
* CdS
* AD変換IC

加湿器がスマートになることによって以下のことができるようになります。

* SiriからのON/OFF、目標湿度の設定
* タイマーや他のセンサーなどと組み合わせたON/OFF
* 目標湿度に従った制御
* タンクの水がなくなったのを検出してお知らせ

![IMG_9935.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204320/25042c28-702e-fba2-eed2-ac7746c73ac5.png)


# 用意するもの

加湿器とラズパイ以外にかかるお金は合計しても二千円程度です。

## Raspberry Pi

ご家庭にある普通のラズパイでGPIOがいくつか余っていればそのまま相乗りできます。
我が家では熱帯魚水槽や各種センサーが繋がっている3bに加湿器も繋げています。

## 加湿器

加湿器は余計な機能のないものを用意します。
スタート・ストップボタンとかタイマーとか中途半端に賢い機能は不要です。
「コンセントを挿せば動く、抜けば止まる」という単純さが必要です。
無駄なことはしなくていいので必要なことだけを忠実にしてくれるヤツをラズパイで手なづけます。
「中途半端はかえって面倒」とはまさに世の真理!

たとえば以下のような製品。

[Amazon | アイリスオーヤマ 加湿器 加熱式加湿器 グリーン SHM-260R1-G | アイリスオーヤマ(IRIS OHYAMA) | 加湿器](https://www.amazon.co.jp/dp/B07WP9DHGR)

我が家にあるのはもっと古い以下の製品ですが、この手の製品なんて毎年変わるものでもありません...

[Amazon \| VICS\(ヴィックス\) スチーム式加湿器 V750 \| VICKS \(ヴイックス\) \| ホーム＆キッチン 通販](https://www.amazon.co.jp/gp/product/B00NN2BMEA)

## 湿度センサー

ラズパイにつなげて湿度（と気温）がとれるセンサーです。
AM2322やDHT11などです。

[温湿度センサ　モジュール　ＡＭ２３２２: センサ一般 秋月電子通商-電子部品・ネット通販](https://akizukidenshi.com/catalog/g/gM-10880/)
[温湿度センサ　モジュール　ＤＨＴ１１: センサ一般 秋月電子通商-電子部品・ネット通販](https://akizukidenshi.com/catalog/g/gM-07003/)

## CdSとA/D変換

今回ターゲットとする加湿器には水がなくなると赤く光るLEDがあるので、それを検出するのに使います。
適当にプルアップまたはプルダウンして測ります。

Raspberry Piではアナログ数値はそのままでは読めませんので、A/D変換をかけて読みます。

[１０ｂｉｔ　２ｃｈ　ＡＤコンバータ　ＭＣＰ３００２－Ｉ／Ｐ: 半導体 秋月電子通商-電子部品・ネット通販](https://akizukidenshi.com/catalog/g/gI-02584/)

# WiFi制御コンセント

以下の製品の場合はDHCPで固定IPv4を振り出すと、そのままそのIPアドレスを使って制御できます。
つまり、共有鍵のようなものは不要です。

[Amazon.co.jp： TP-Link WiFiスマートプラグ 2個セット 遠隔操作 Echo シリーズ / Googleホーム / LINE Clova 対応 音声コントロール ハブ不要 3年保証 HS105P2: ホーム＆キッチン](https://www.amazon.co.jp/
/dp/B07FY4GDDQ)

# 工作

CdSを水切れ表示LEDの前につけ、遮光のため黒いシールで覆い、プルアップしてAD変換し、温度センサーもつなげます。

何度か試して、閾値を探します。

![IMG_9936.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204320/220fc126-cd10-cc86-ad47-2ef46df07ac4.jpeg)

# ソフトウェア

## 湿度を読む

別に目新しい話でもないので、以下を参照。

[Raspberry PiとDHT11で温度・湿度を測る - Qiita](https://qiita.com/mininobu/items/1ba0223af84be153b850)

## CdSを読む

別に目新しい話でもないので、以下を参照。

[Raspberry Pi 3でpythonを使いA/Dコンバーターでアナログ信号を検出する！ - Qiita](https://qiita.com/RyosukeKamei/items/387e4a1fafb1d27a220f)

## コンセント制御

別に目新しい話でも(ry

[GitHub - softScheck/tplink-smartplug: TP-Link WiFi SmartPlug Client and Wireshark Dissector](https://github.com/softScheck/tplink-smartplug)

## 制御プログラム本体

以下の処理をするループをまわします。

``` sh
if 湿度の制御が必要 ; then
  湿度を読み取る
  if 現在の目標湿度より読んだ湿度が高い ; then
    加湿器をオフ
  else
    加湿器をオン

  if 加湿器稼働中 ; then
    cdsの出力値を読み取る
    if LED点灯 ;then
      水なしフラグをたてる
    else
      水なしフラグをおろす
else
  加湿器をオフ

```

## homebridge

さて、ここまで駆け足でしたがここからは少し細かく...

homebridgeですが加湿器は`HumidifierDehumidifier`となります。
今回は何でも制御できる便利なHttp Advanced AccessoryからCGI経由で繋げます。

[GitHub - staromeste/homebridge-http-advanced-accessory: Supports all devices on HomeBridge Platform / Bridges devices to http](https://github.com/staromeste/homebridge-http-advanced-accessory)
[homebridge-http-advanced-accessory - npm](https://www.npmjs.com/package/homebridge-http-advanced-accessory)

``` config.json
 {
            "accessory" : "HttpAdvancedAccessory",
            "service"   : "HumidifierDehumidifier",
            "name"      : "Humidifier",
            "forceRefreshDelay": 300,
            "debug" : false,
            "optionCharacteristic" : [
                "RelativeHumidityHumidifierThreshold",
                "WaterLevel" ],
            "props" : {
                "CurrentHumidifierDehumidifierState": {
                    "validValues": [
                        0,
                        2 ]
                },
                "TargetHumidifierDehumidifierState": {
                    "validValues": [
                        1
                    ]
                },
                "RelativeHumidityHumidifierThreshold": {
                    "Format": "float",
                    "Permissions": [
                        "pr",
                        "pw",
                        "ev"
                    ],
                    "maxValue": 100,
                    "minStep": 1,
                    "minValue": 0,
                    "unit": "percentage"
                },
                "WaterLevel": {
                    "Format": "float",
                    "Permissions": [
                        "pr",
                        "ev"
                    ],
                    "maxValue": 100,
                    "minValue": 0,
                    "unit": "percentage"
                }
            },
            "urls" : {
                "getCurrentRelativeHumidity": {
                    "url" : "http://localhost:801/httpadv/humi/state",
                    "mappers" : [
                        {
                            "type": "jpath",
                            "parameters": {
                                "jpath": "$.CurrentRelativeHumidity",
                                "index": 0
                            }
                        }
                    ]
                },
                "getCurrentHumidifierDehumidifierState": {
                    "url" : "http://localhost:801/httpadv/humi/state",
                    "mappers" : [
                        {
                            "type": "jpath",
                            "parameters": {
                                "jpath": "$.CurrentHumidifierDehumidifierState",
                                "index": 0
                            }
                        }
                    ]
                },
                "getTargetHumidifierDehumidifierState": {
                    "url" : "http://localhost:801/httpadv/humi/state",
                    "mappers" : [
                        {
                            "type": "jpath",
                            "parameters": {
                                "jpath": "$.TargetHumidifierDehumidifierState",
                                "index": 0
                            }
                        }
                    ]
                },
                "getRelativeHumidityHumidifierThreshold": {
                    "url" : "http://helen.maidlab.jp:801/httpadv/humi/state",
                    "mappers" : [
                        {
                            "type": "jpath",
                            "parameters": {
                                "jpath": "$.RelativeHumidityHumidifierThreshold",
                                "index": 0
                            }
                        }
                    ]
                },
                "getWaterLevel": {
                    "url" : "http://localhost:801/httpadv/humi/state",
                    "mappers" : [
                        {
                            "type": "jpath",
                            "parameters": {
                                "jpath": "$.WaterLevel",
                                "index": 0
                            }
                        }
                    ]
                },
                "getActive": {
                    "url" : "http://localhost:801/httpadv/humi/state",
                    "mappers" : [
                        {
                            "type": "jpath",
                            "parameters": {
                                "jpath": "$.Active",
                                "index": 0
                            }
                        }
                    ]
                },

                "setActive": {
                    "url" : "http://localhost:801/httpadv/humi/set?active=${value}",
                    "httpMethod":"GET",
                    "body" : "{value}",
                    "mappers" : []
                },
                "setRelativeHumidityHumidifierThreshold": {
                    "url" : "http://localhost:801/httpadv/humi/set?target=${value}",
                    "httpMethod":"GET",
                    "body" : "{value}",
                    "mappers" : []
                }
            }
        }
```

stateのcgiは呼ばれたら以下のようなjsonを返すようにします。

``` json
{
    "Active": 1,
    "WaterLevel": 100,
    "CurrentRelativeHumidity": 56,
    "CurrentHumidifierDehumidifierState": 2,
    "TargetHumidifierDehumidifierState": 1,
    "RelativeHumidityHumidifierThreshold": 60
}
```

それぞれの項目の細かい意味は
[HAP-NodeJS/HomeKit.ts at master · homebridge/HAP-NodeJS · GitHub](https://github.com/homebridge/HAP-NodeJS/blob/master/src/lib/gen/HomeKit.ts)
にあります。

* Active

自動制御中なら1、停止中なら0です。
実際に加湿しているかどうかではなく、加湿器の制御が有効かどうかということです。

* WaterLevel

水切れランプが点灯していれば0，そうでなければ100のbinaryな検出になります。
超音波の水面センサーや重量センサーを作ればもっと細かくとれるでしょう。

* CurrentRelativeHumidity

センサーでとってきた現在の部屋の相対湿度です。

* CurrentHumidifierDehumidifierState

加湿中は2、湿度が目標より高いなどで待機中は1、待機すらしてない（activeが0）のときは0です。

* TargetHumidifierDehumidifierState

加湿器の場合、これは1に固定です。
除湿機能もある場合は別の値にもなります。

* RelativeHumidityHumidifierThreshold

目標とする相対湿度の値です。

あとはsetする側のcgiも適当に書き、全部まとめてコンテナの中に封入しておきましょう。

# 完成!

## iOS Home.app

iOSではこんな感じの画面です

![IMG_9933.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204320/743b7338-9b5a-36da-2c44-00973d5625b0.png)
![IMG_9934.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204320/eae09680-f4ad-4975-40c3-20d195de32ca.png)

# 可視化

![home_humi-pinpoint=1606876576-1606947181.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204320/0f7cb39b-b3b6-4016-efb0-5b8db8eed4f6.png)

緑色になっている部分が、加湿器が動作中（目標湿度より低い実測湿度）です。
このグラフには室外モニタ用のAM2320も載っていますが、この子は雨が降るとすぐに天井に張り付いてしまう子なのです...

冬の夜の湿度を高めに誘導しているのですが、それが本当に好ましいことなのかどうかは知りません...

[湿度 適正 冬 - Google 検索](https://www.google.com/search?q=%E6%B9%BF%E5%BA%A6%20%E9%81%A9%E6%AD%A3%20%E5%86%AC)

# おわりに

2020年、思わぬ要因による「おうち時間」の長時間化により、「ラズパイスマートホーム」「おうちハック」を楽しまれた方も多いのではないでしょうか。
この調子でおうちを快適にし、おうち時間を楽しみ、「おうちから出てもいいですよ」の時代までおうちで元気に過ごしましょう！

# その後 （2022年秋追記）

* RaspberryPiではなく、ESP32にしました
* この加湿器は水漏れして壊れてしまったので、米国でみかける塩を加えて電気分解するタイプに変更しました
* 正常動作しているかどうかは、噴出口の温度センサーで測ります
* 重さセンサーを設置し、水の量を加湿器全体の重さで測るようにしました
* 観測値はESP32からinfluxdbに直接書き込み、homebridgeの方は`|> last()`の値を読むようにしました
* `WebServer` もあげ、prometheusのメトリクスもだすようにしました（観測値ではなく正常稼働しているかどうかのモニタ）
* おかげさまで感染することもなく過ごしております（笑）