SwitchBot 温湿度計の測定値を BLE Advertisement パケットから直接読み取る

#はじめに
先日、Amazon でセールになっていたので [SwitchBot 温湿度計（SwitchBot Meter）](https://www.switchbot.jp/meter) を購入しました。この機器は Bluetooth を内蔵しており、温度・湿度を本体でデジタル表示するとともに、スマホアプリでグラフ描画することができます。同時販売されている Hub シリーズを組み合わせることにより、IFTTT のトリガーとすることも可能なようです。

同様の機器は各社からリリースされており、特に Xiaomi の製品は海外で広く利用されているようで、BLE Advertisement パケットの解析もされて、Xiaomi 製 Hub やスマホアプリを使用せずとも機器から測定値を直接取得することも容易です。

どうせ SwitchBot 温度計も同じようなものだろうとたかをくくって、ちゃんと調べないままポチってしまったのですが、ググってみても情報は見当たらず途方に暮れかけていたところ、Android アプリが難読化処理されていないことがわかったため、リバースエンジニアリング（というほど大したものではない）して、温湿度計が発信する BLE Advertisement パケットから直接測定値を取得することができるようになりました。

(2019.12.29 追記)
SwitchBot の開発元である WanderLabs から、温湿度計の BLE 仕様が Meter BLE open API として公開されました。
https://github.com/OpenWonderLabs/python-host/wiki/Meter-BLE-open-API

Android アプリのソース上では定義されていたビットの一部が Reserved とのみ記載されていますが、温度・湿度に関する部分は、ここで紹介したとおりの仕様となっていました。

# BLE Advertisement パケットフォーマット
![switchbot meter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224648/58138330-52ab-46c4-fbcc-4de30787f9bb.png)
SwitchBot 温湿度計の情報は、Advertisement パケットのうち、UUID: 0x0d00 (00000d00-0000-1000-8000-00805f9b34fb) の 6オクテットの Service Data として およそ 2 秒間隔で送信されています。

(2019.11.19 追記)
Service Data の取得にはアクティブスキャン（BLE Peripheral からの Advertisement を受信後、BLE Central から SCAN_REQ を投げて追加情報の送信を要求する）の実行が必要です。パッシブスキャンでは Manufacturer Data しか送信されてきません。

ｔ1 + t2/10 で取得できる値は温度の絶対値なので、isTemperatureAboveFreezing を見て、これが false の場合はマイナス値として扱うようにします。

スマホアプリで温度・湿度に対する上限・下限アラートを設定し、実際の測定値がその閾値を超えると b4 ～ b7 が true になります。本体での温度表示を華氏に設定すると isTemperatureUnitF が true になりますが、送信されてくる値自体は摂氏のままで変わりません。

ちなみに SwitchBot の方のフォーマットはこんな感じ。こちらは 3オクテットになります。
![switchbot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224648/be2fe7bb-6527-f94f-8279-426aa29b24f5.png)
isDualStateMode は、スマホアプリで壁スイッチモードを有効にすると true になります。isStatusOFF や group ID がどのように使用されているのかはわかりませんでした。



#サンプルコード
SwitchBot 温湿度計からの BLE Advertisement を受信するたびにその内容を表示するコードです。
Python3.8.0 + bluepy 1.3.0 の組み合わせでの動作を確認しています。

(2019.11.18 追記)
このコードでは、断続的にアクティブスキャンを実行しており、SwitchBot の電池の消耗を早める可能性があります。実運用時には、スキャンは間欠的に実行するべきでしょう。

(2020.07.14 追記)
断続的なアクティブスキャンを続けて半年以上経ちましたが、未だに電池残量は 100% を示し続けています。アクティブスキャンによる電池消耗は、実用上気にする必要はなさそうです。

```python:switchbot-meter.py
import binascii
from bluepy.btle import Scanner, DefaultDelegate

macaddr = 'xx:xx:xx:xx:xx:xx'

class ScanDelegate( DefaultDelegate ):
  def __init__( self ):
    DefaultDelegate.__init__( self )

  def handleDiscovery( self, dev, isNewDev, isNewData ):
    if dev.addr == macaddr:
      for ( adtype, desc, value ) in dev.getScanData():
        if ( adtype == 22 ):
          servicedata = binascii.unhexlify( value[4:] )
          
          battery = servicedata[2] & 0b01111111
          isTemperatureAboveFreezing = servicedata[4] & 0b10000000
          temperature = ( servicedata[3] & 0b00001111 ) / 10 + ( servicedata[4] & 0b01111111 )
          if not isTemperatureAboveFreezing:
            temperature = -temperature
          humidity = servicedata[5] & 0b01111111
          
          isEncrypted            = ( servicedata[0] & 0b10000000 ) >> 7
          isDualStateMode        = ( servicedata[1] & 0b10000000 ) >> 7
          isStatusOff            = ( servicedata[1] & 0b01000000 ) >> 6
          isTemperatureHighAlert = ( servicedata[3] & 0b10000000 ) >> 7
          isTemperatureLowAlert  = ( servicedata[3] & 0b01000000 ) >> 6
          isHumidityHighAlert    = ( servicedata[3] & 0b00100000 ) >> 5
          isHumidityLowAlert     = ( servicedata[3] & 0b00010000 ) >> 4
          isTemperatureUnitF     = ( servicedata[5] & 0b10000000 ) >> 7
          
          print( '----' )
          print( 'battery: '     + str( battery ) )
          print( 'temperature: ' + str( temperature ) )
          print( 'humidity: '    + str( humidity ) )
          print( '' )
          print( 'isEncrypted: '            + str( bool( isEncrypted ) ) )
          print( 'isDualStateMode: '        + str( bool( isDualStateMode ) ) )
          print( 'isStatusOff: '            + str( bool( isStatusOff ) ) )
          print( 'isTemperatureHighAlert: ' + str( bool( isTemperatureHighAlert ) ) )
          print( 'isTemperatureLowAlert: '  + str( bool( isTemperatureLowAlert ) ) )
          print( 'isHumidityHighAlert: '    + str( bool( isHumidityHighAlert ) ) )
          print( 'isHumidityLowAlert: '     + str( bool( isHumidityLowAlert ) ) )
          print( 'isTemperatureUnitF: '     + str( bool( isTemperatureUnitF ) ) )
          print( '----' )

scanner = Scanner().withDelegate( ScanDelegate() )
scanner.scan( 0 )
```

#おわりに
というわけで我が家では、SwitchBot 温湿度計をケースに入れてベランダに設置し、外気の温度・湿度を Python → MQTT → Home Assistant という経路でモニタリングしはじめました。以前は ESP32 に BME280 を接続し、5分に 1回 deepsleep から目覚めて測定値を MQTT で publish するデバイスを作っていたのですが、こちらの方が無造作に転がしておけるし、測定間隔も頻繁になる上に電池の持ちも良いのではと期待しています（ESP32 の時は単 3 電池 4 本で 3ヶ月ぐらいの電池の持ちだった）。

Home Assistant か ESPHome の component を書いて、MQTT を経由せず直接測定値を扱えるようにしたいところですが、はてさて。
![graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224648/5026dbe8-82f0-9881-2d85-28030331bcc4.png)
ユーザプログラムからも扱いやすく技適の問題のない Bluetooth 温湿度計が、比較的安価に Amazon で購入できるというのはうれしいですね。次のセールがあったら、自宅内のまだ測定していないところ用に買い増ししたいところです。