WiFi制御スマートコンセントを換気扇に繋げて、省エネ快適生活を目指す


qiitaに記事を書くのは初めてになります。
一部お見苦しいかと思います...

@yumu19 さんに触発されておうちハックをはじめて約2年になります。
「電気代を節約しつつ快適な生活」を目指して、家では多数のデバイスが動いていますが、今日は換気扇自動制御をご紹介します。

* 在宅のときだけ換気扇は動く
* 外気のほうが涼しいのに冷房をしているときは、冷房をとめ、換気扇による外気導入をする
* 外気のほうが暖かいのに暖房をしているときは、暖房をとめ、換気扇による外気導入をする
* 料理を始めるときなどには貼り付けたAmazon Dash Buttonを触ると、30分間は換気扇が動く
* 二酸化炭素濃度（Netatmoで計測)が規制値より高くなったら、換気扇を動かす
    * 規制値は部屋の状態、他の機材の動作によって上下する
* 隣人がタバコをすっている時間帯は動作は控える（DashButtonによるリクエストは優先）

co2の動きはこんな感じになります。

![co2.png](https://qiita-image-store.s3.amazonaws.com/0/204320/f7cc9609-612a-902c-c5bc-52271633f9d2.png)

# WiFi制御コンセントの設置

## 換気扇のAC100V配線を確認

うちの台所の換気扇は埋め込まれていますが、レンジフードをあけると配線がみえました。その配線を追いかけると、100Vが普通のコンセントとは違うが2ピンのソケットに出ていました。

調べると、バイク、鉄道模型、タミヤの模型のバッテリーなどで使われている6.2mmピッチと判明、アマゾンでタミヤ用ケーブルを買ってきて、日本の家庭によくある100Vコネタクを付けました。

部品代はトータル500円くらいです。

## コンセントを買ってきてつなげる

Amazonで沢山うっている、WiFiで制御できるコンセントを調達します。「smart homeアプリで設定できます」という商品説明があるものを探します。「Merrosアプリで設定できます」という商品は別のAPIを使うものであり、こちらの現行バージョンのプロトコルはクラウド経由必須なのでおすすめできません。

https://www.amazon.co.jp/dp/B07M91WK6F
などですね。
だいたい1500円くらいです。

設置するとこんな感じです。
![IMG_5538.jpg](https://qiita-image-store.s3.amazonaws.com/0/204320/a2d4344b-a6d5-d950-83e2-07abaec634eb.jpeg)

## WiFiコンセントの設定を抜く

mitmproxy を使いながら、WiFiコンセントの設定をスマホの専用アプリで行います。
mitmproxyの使い方はこんな感じです。
https://qiita.com/wtotw/items/69290b178371c4d7cf76

WiFiコンセントのOEM供給元の企業に対して、かなり沢山の情報（位置情報なども）が流れているのが分かりますが、必要なのはuuid, localKeyという二つの値です。この二つを抜いたあと、DHCPサーバでMACアドレスから静的振り出しにして、ルータではそのIPからの外出を阻止します。

## pytuyaで制御

https://github.com/clach04/python-tuya
pytuyaをインストールし、こんな感じのコードを書きます。

```
#!/usr/bin/python

import re
import sys
import time
import pytuya

def main():
        args = sys.argv

        uuid=""
        host=""
        key=""
        
        if len(args)!=3:
                print "wificonsent.py switchname [on|off|state]"
                sys.exit(0)

        if args[1] == "fan":
                uuid="052000xxxxxxxxxxxxxx"
                host="192.168.x.xx"
                localKey="bbxxxxxxxxxxxx8"
        elif args[1] == "pot":
                uuid="022xxxxxxxxxxxx"
                localKey="50xxxxxxxxxxxxxx933d"
                host="192.168.x.xx”

        if not uuid:
                print "missing device"
                sys.exit(0)

        d = pytuya.OutletDevice(uuid, host, localKey)

        if re.match(r"(state)|(status)|(what)|(kio)|(cu)", args[2], re.IGNORECASE):
                if getstate(d):
                        print "on"
                        sys.exit(0)
                else:
                        print "off"
                        sys.exit(1)
        elif re.match(r"1|(on)|(start)|(up)|(enable)|(true)|(ok)|(go)||(yes)|(aga)|(valida)", args[2], re.IGNORECASE):
                if getstate(d):
                        sys.exit(0)
                else:
                        sendsignal(d, True)
                        sys.exit(0)
        elif re.match(r"0|(off)|(stop)|(down)|(disable)|(false)|(no)|(ne)|(sen)", args[2], re.IGNORECASE):                
                if not getstate(d):
                        sys.exit(0)
                else:
                        sendsignal(d, False)
                        sys.exit(0)
        else:
                print "kio?"
                sys.exit(2)

def getstate(d):
        count=0
        while True:
                if count > 2:
                        print "wificonsent: getstate: error"
                        sys.exit(1)
                try:
                        data = d.status()
                        if data['dps']['1']:
                                return True
                        else:
                                return False
                except Exception as e:
                        #print e
                        time.sleep(1)
                        count+=1
                        continue

        sys.exit(1)

def sendsignal(d, newmode):
        d.set_status(newmode)

if __name__ == "__main__":
        main()
```

on/offの判定だけで正規表現を使うのはもったいないかもしれませんが、こういうコマンドの使い方をいちいち人間が覚えておくほうがコストが高いので、そこはCPUに頑張ってもらいます。

# HomeKitから制御する

このままではユーザインタフェイスがひどいので、iOSとmojaveのHomeから制御できるようにします。

## homebridgeをいれる

https://qiita.com/tags/homebridge/items
など参照してください。

私の場合は、Raspberry Pi互換のarmマシンのDockerの中にいれています。

## homebridgeのcmdSwtich2に繋げる

config.jsonを書きます。

```
   {
            "platform": "cmdSwitch2",
            "name": "CMD Switch",
            "switches": [
                {
                    "name": “換気扇",
                    "on_cmd":  "/homebridge/wificonsent.py fan on",
                    "off_cmd": "/homebridge/wificonsent.py fan off",
                    "state_cmd" : "/homebridge/wificonsent.py fan state"
                },
                ....
   }
```

## iOSから動かす

これで
「Hey, Siri, 換気扇つけて」
ができます。

![IMG_2552.jpg](https://qiita-image-store.s3.amazonaws.com/0/204320/e2127bb7-fd3d-e933-6104-fa099fe7804d.jpeg)

# Amazon Dash Button

スマホだけというのもすこし不便なので、物理スイッチもつけます。
とはいえ、クックパッドをみながら「5分煮る」と書いてあればSiriに「5分後に教えて」というのですが...

## Amazon Dash Buttonを買い、両面テープで貼り付ける

500円です。
届く商品はなんでもいいです。
どうせ届きませんので...

![IMG_2555.jpg](https://qiita-image-store.s3.amazonaws.com/0/204320/2325f8b7-597f-e3cf-d92d-bd3ff0b6fc28.jpeg)

## 途中まで設定する

アマゾンアプリで設定をすすめますが、途中で放棄します。
押す度にAmazonから「商品を選んでください」のメールが来るところまでです。

## 通信を阻害する

電池でWiFiやってTLSしてくれるとは驚きですが、電池がなくなったりAmazonにも迷惑なので、MACアドレスを調べてルータで阻害します。
メールも来なくなります。

## Dash Buttonを押したら時刻を記録するようにする

http://docs.nekmo.org/amazon-dash/

から持ってきたソースを適当に書き換え、Dash Buttonが押されたら時刻をどこかに記録するようにします。
これもDockerの中にいれると管理が楽です。

この手で「Amazon Dash Buttonを押されるとhttpリクエストが飛ぶ → httpリクエストをpythonのbottleで受け取る → seleniumで通販サイトにリクエストを飛ばす」をすると、「Amazon Dash Buttonを楽天ボタンにする」なども可能です。
ただ、利用規約的な問題があるので、githubやqiitaに公開するのは難しいかもしれませんね。

# 自動制御プログラムを書く

はい、ここからが本番です。
が、すみません、擬似コードです。
たぶんココは正解があるものではなく、人によって生活スタイルも異なり、目標も違うものだと思いますので...

```

while(true){

    if(人感センサー() || ping("iphone") || ping("ipad")){
        at_home=true;
    }else{
　　　　　　　　　　　　　　　　at_home=false;
    }

    #実際は、以下の点に気をつけてエラー回避コードをいれる
    # 人感センサーの誤動作対策: 日の出〜日没後30分は無効
    # ping: スマホ類は省電力のためお休みすることが結構あるので、pingできないからといって居ないとは限らない。
    # スマホアプリ: codyl connectのWiFi検出、proximity eventsのGPSからトリガーが使えるが、移動中はトリガーの通信が確実に届かないことがあるので、これも検出漏れがある
    # HomeKit: AppleTVと組み合わせると、帰宅在宅をiOSから教えて貰うことができる。ただし、これも確実とは限らない。
    # これらを組み合わせて、「誤動作がなるべく少なく、誤動作しても痛くない形での在宅判断」をする必要があり、これだけでカナリ面倒なので本記事では省略

    if(at_home){
	#dash buttonが押されたら30分は継続して運転
	if(switch->最後にDashButtonからリクエストが来た時刻() < (現在時刻() - 30分)){
	    switch->set("換気扇", true);
	    goto end;
	    #  擬似コードなのでgoto使っているけど、実際は別ですよ？
	}

	#朝、ベランダでタバコを吸う人がいるので...
	# dashボタンは時間に関係なく動作
	if((現在時刻->hour()) == 7 || (現在時刻->hour() == 8)){
	    switch->set("換気扇", false);
	    goto end;
	}
	
	#エアコン(irkit制御)が無駄な動きをしているときは、換気扇で代用
	
	# このへんも本当は複雑で、エアコンの設定温度の他に
	# homebrige-thermostatにある
	#   targetHeatingCoolingState  （ユーザが指示している動作）と
	#   currentHeatingCoolingState （実際に今どうなっているかの状態）
	# の違いを扱う必要があります
	
	if((aircon->目標->動作() == "暖房") &&
	   (netatmo->外気温() > aircon->設定温度())){
	    #暖房をしているのに、実は外のほうが暖かい
	    aircon->現状->停止();
	    switch->set("換気扇", true);
	    goto end;
	}
	
	if((aircon->目標->動作() == "冷房") &&
	   (netatmo->外気温() < aircon->設定温度())){
	    #冷房をしているのに、実は外のほうが涼しい
	    aircon->現状->停止();
	    switch->set("換気扇", true);
	    goto end;
	}
	
	#二酸化炭素濃度が高ければ運転
	co2_limit=700;
	
	if((aircon->現状->動作() == "暖房") &&
	   (netatmo->外気温() < netatmo->室温())){
	    # 暖房しているが、外気温が室温より寒い
	    co2_limit=800;
	}
	if((aircon->現状->動作() == "冷房") &&
	   (netatmo->外気温() > aircon->設定温度())){
	    # 冷房しているが、外気温が室温より暑い
	    co2_limit=800;
	}
	if(switch->get("プロジェクター")){
	    # プロジェクターが動いているときはうるさくしないでほしい
	    co2_limit = co2_limit * 1.1;
	}

	#他いろいろ、条件を緩めたり厳しくしたり
    
	if(netatmo->二酸化炭素濃度() > co2_limit){
	    switch->set("換気扇", true);
	    hue->set("台所", "赤");
	    goto end;
	}
	
	#さらに、空気質センサーで埃や臭いがきついとかあればONにしますが、
	#今回は省略
	
	#goto endに落ちなくてここまできたら
	switch->set("換気扇", false);
	hue->set("台所", off);
    }else{
	#在宅ではないときは停止
	switch->set("換気扇", false);
    }
  end: 
    sleep(10秒);
}
```

HomeKit経由の人間の介入があっても、すぐに上書きされてしまうので、このへんは「どこまで人間の介入を尊重するか」という、人によって解の違う問題を解きながら考えましょう。
私は「人間の介入を検出したら、2時間は自動制御しない」という方法をとっています（この擬似コードには入っていません）。

この擬似コードはポーリングで書いていますが、温度などをMongoDBにいれてChange Streamでwatch()する等すると、イベントドリブンにすることができます。

# muninにグラフをだす

muninのpluginを書いておくと冒頭のグラフになります。

センサーの誤動作対策が大変ですが、楽しいですよ！