Switchbot 開閉センサーデータをラズパイで収集する

# どうでもいい前書き
初めて新生活始めた時にベランダの窓開けっ放しで出かけてしまったことがトラウマで今だに出かけるたびにドアの施錠が気になるんです。
~~まぁ、８階建の４階で防鳩ネットあったからそんな簡単に侵入できる環境ではないので空き巣の被害はなかったんですがね~~ 
そんな感じでleafee magをつけてたんですが、経年劣化（主に南側の窓に付けているので直射日光の影響をモロにうける）で買い替えを検討してたところ、[switchbotのドア開閉センサー](https://www.switchbot.jp/products/switchbot-contact-sensor)が仕様公開しているのでいろいろ出来そうだということでデータ収集をしてみました。

温湿度計のデータ収集は結構あるけど開閉センサーはないみたい・・・？

# Switchbotのデータ送信方法について
https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/blob/latest/devicetypes/contactsensor.md

ここの仕様によると２つのデータ送信方法があるようで
1. センサー（以下「測」）は常にブロードキャストメッセージを送っているので、SCAN_RSPをスキャン側（以下「探」）から送ると「測」は追加データ（RSP_DATA）を送る。データはその中に含まれる
1. 「探」は「測」に接続し特定のコマンドを送信する。すると「測」は「探」にデータを送る。データはその中に含まれる

一応1.が取得方法が簡単なので（アクティブスキャンかけて、22番データを拾ってくるだけ）実装してみましたが、10秒スキャンしても欠測が見られたりするなど不安定だったので2.の方法で実装します

## BLE commパケットの詳細
1. 「測」に接続し、withDelegateを作ります
　　（notifyを受信した時用のclass）
1. 「測」の受信用特性UUID(UUID:"cba20002-224d-11e6-9fb8-0002a5d5c51b"、GATT Handle:"0x000f")を有効にします
1. 「測」の送信用特性UUID(UUID:"cba20003-224d-11e6-9fb8-0002a5d5c51b"、GATT Handle:"0x000d")に「0x57 0x00 0x11」を送信する
1. 「測」からデータが渡され、1.のclass内で定義されているhandleNotification関数にdataとして渡され実行される

（表現的に合ってるかは知りませんがおおむねこんな感じです）

## 得られるデータについて

| 位置（バイト） | 内容 |
|:--:|:--|
| ０ | ステータスコード |
| １ | 0x01（固定値） |
| ２〜５ | 最後にドア開閉状態が変化してからの秒数　|
| ６ | 人感センサー<BR>(0x00：無人、0x01：有人) |
| ７ | 明るさセンサー <BR>(0x00：暗闇、0x01：明るい) |
| ８ | ドアセンサー<BR> (0x00：閉鎖、0x01：開放、0x02：開放(設定時間超過))|

# コード
## 全体図

``` python3:contact.py 
from time import time,sleep
import bluepy

#Bluepyのソケット作成
scanner = bluepy.btle.Scanner(0)

#BLEのHandle
SBOTCON_HANDLE_NOTIFY = 0x000f
SBOTCON_HANDLE_WRITE = 0x000d

#ドアの場所名・macアドレス（このmacアドレスはダミーのため、１６進数ではありません）
door_mac=[["部屋１","na:m1:k1:me:i1:k0"]]

#各ステータスの日本語訳
doormode=['施錠','解錠','解放']
door_RSP_en=['door_status','light_status','hal_time']

#ステータス一時保存用
peri_SwiBotCon    = []
status_store_sb   = []
SBotCo_Notify_Number=0

#Switch_Bot近接センサのデータ変換
def Switchbot_Contact_Conv(a):
    door_status    = int(a[8])
    light_status   = int(a[7])
    hal_time       = int(a[2]*256*256*256+a[3]*256*256+a[4]*256+a[5])
    return door_status,light_status,hal_time

#Switchbot用のnotify処理
class MyDelegate_SwitchBotContact(bluepy.btle.DefaultDelegate):
    def __init__(self, params):
        bluepy.btle.DefaultDelegate.__init__(self)

    def handleNotification(self, cHandle, data):
        status_store_sb[SBotCo_Notify_Number]=Switchbot_Contact_Conv(data)

#switchbotContactの接続処理用
def switchbot_conn(x):
    global peri_SwiBotCon
    connect=False
    peri_SwiBotCon[x] = bluepy.btle.Peripheral()
    print("センサー"+door_mac[x][0]+"に接続中。")

    while not connect:
        try:
            peri_SwiBotCon[x].connect(door_mac[x][1], bluepy.btle.ADDR_TYPE_RANDOM)
            connect=True
            peri_SwiBotCon[x].withDelegate(MyDelegate_SwitchBotContact(bluepy.btle.DefaultDelegate))
        except bluepy.btle.BTLEDisconnectError: #接続失敗時はやり直す
            connect=False
            continue

    
#ループ関数内のswitchbotの処理
def contact_loop_switchbot():
    global SBotCo_Notify_Number
    for i in range(len(door_mac)):
        try:
            SBotCo_Notify_Number=i
            peri_SwiBotCon[i].writeCharacteristic(SBOTCON_HANDLE_NOTIFY+1, b'\x01\x00' )
            peri_SwiBotCon[i].writeCharacteristic(SBOTCON_HANDLE_WRITE, b'\x57\x00\x11' )
            peri_SwiBotCon[i].waitForNotifications(1.0)
        except bluepy.btle.BTLEDisconnectError: #接続失敗した時は接続し直す
            switchbot_conn(i)

    
def main():
#Switchbotの接続処理
    global peri_SwiBotCon
    global status_store_sb  
    for x in range(len(door_mac)):
        peri_SwiBotCon.append(None)
        status_store_sb.append(None)
        switchbot_conn(x)

#終了時にdisconnectするよう処理
    try: 
        while True:
            contact_loop_switchbot()  #近接センサーのループ
            print(status_store_sb)
            sleep(60)                 #取得間隔を秒単位で指定。
    finally:
        for x in range(len(door_mac)):
            peri_SwiBotCon[x].disconnect()


if __name__ == "__main__":
    main() 



```

```pip```でbluepyをインストールして、該当macアドレスを変更した上でこれを実行すると、定期的に配列でデータが得られる。
macアドレスの確認方法は後述

## 主だったところの説明
### 端末のmacアドレスについて
一度switchbotの公式アプリに繋いで確認してもらった方が早いし確実だと思う。
特に開閉センサーはファームウェアのアップデートもあるので。（現在はVer1.1）
登録したら、該当するセンサーをタップ→設定→デバイス情報と押していくとBLE MACにMACアドレスが書いてあります。

### データ変換部について
`Switchbot_Contact_Conv`関数は、notifyで受けたデータ（バイト型）を処理して適切な値に読み替えてあげる関数。
python3はありがたいことに、バイト型の文字列を`x[0]`のように扱うとint型にしてくれるのである。
さらにSwitchbotのコミュニケーションパケットの書式が1バイト単位（HALtimeのみ３バイト使っている）で決められているのでビット処理がいらない（RSP_DATAはそうではなかった）
なのでそこまで難しいことになっていない。

### notify処理（handleNotification関数）
データを受け取るたびに処理するものをここに記述。
今回説明で複雑になるため割愛したが、得られたデータをRedisサーバに突っ込んで、Redisサーバ経由でM5Stackが表示するというやり方もあり寄りのアリだと思う。

### 接続処理（switchbot_conn関数）
配列番号を引数として渡すことで、ループ処理中の切断による再接続とも共通化させている。
接続失敗の場合だけは再度挑戦するような処理。
本来なら何回失敗したら飛ばすって設定入れるべきなんだろうけど、回数については要検討で。

### ループ処理（contact_loop_switchbot関数）
最初に記載した通り、通知設定を有効にし、情報取得用のコマンドを送り受信する。
途中で失敗したら取りやめて再接続（switchbot_conn関数へ）
door_macの長さを取ってループしているのは、将来的な拡張も備えてのため

### メイン関数
初期処理において、「測」に接続する前に保存用の配列拡張を実施。
ループ処理においては、contact_loop_switchbot関数を実行し、
実行結果を出力し、60秒間待機させる。
ctrl+Dで終了するときには、コネクションを解除する

### GATT Handleについて
[bluepyの公式ドキュメント](https://ianharvey.github.io/bluepy-doc/peripheral.html)によれば、```writeCharacteristic(handle, val, withResponse=False)```とあるので、渡すのはUUIDではなく、handleで渡さないといけない。
しかし、[switchbotの公式ドキュメント](https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/blob/latest/devicetypes/contactsensor.md#ble-communication-packet-basic-format)にはUUIDしか書いてないので調べる必要がある。

以下のプログラムで調べられる（繋がるまでひたすらループします）
``` python3: gethandle.py 

import bluepy

mac="C0:FF:EE:10:be:er" #このmacアドレスはダミーです。ご自身のcontact sensorのmacアドレスに書き換えます。

def main(x):
    while True:
        try:
            peri = bluepy.btle.Peripheral()
            peri.connect(x, bluepy.btle.ADDR_TYPE_RANDOM)  
            break
        except:
            print("device connect error")
            continue

    charas = peri.getCharacteristics()
    for chara in charas:
        print("======================================================")
        print(" UUID : %s" % chara.uuid )
        print(" Handle %04x: %s" % (chara.getHandle(), chara.propertiesToString()))
    peri.disconnect()
    return

main(mac)

```

すると、
```
 UUID : cba20002-224d-11e6-9fb8-0002a5d5c51b
 Handle 000d: WRITE NO RESPONSE WRITE 
======================================================
 UUID : cba20003-224d-11e6-9fb8-0002a5d5c51b
 Handle 000f: NOTIFY 
```
と得られるので、cba20002・・・(RX)のhandleは`0x000d`、cba20003・・・(TX)のhandleは`0x000f`であるとわかる。

# まとめ
Slackに定期的に施錠状況をPOSTさせたりできるようになったのでよかったです。
zabbixと連携させてログ収集させるのもありですね。
そのうちswitchbotネタは書きます。
接続周り、データ収集のたびに切断した方がいいのかとかBLEとつなぎっぱでバッテリー大丈夫なのかとかはおいおい調査していきたいと思います。ので、これが正解とは言えないと思います。

~~BLは詳しいのにBLE全くわからなくて本当に苦労しました
Eがつくだけでいーらい違い・・・~~

# 参考
[Bluetooth Low Energyのアドバータイズとスキャン - フィールドデザイン株式会社](https://fielddesign.jp/technology/ble/blespec_advertise/)

[bluepyで始めるBluetooth Low Energy(BLE)プログラミング - PRIDE](https://www.ipride.co.jp/blog/2510)