bluetooth
BLE
IoT
おうちハック
SwitchBot
64
どのような問題がありますか?

この記事は最終更新日から1年以上が経過しています。

投稿日

更新日

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

はじめに

先日、Amazon でセールになっていたので SwitchBot 温湿度計(SwitchBot 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
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 しか送信されてきません。

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

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

ちなみに SwitchBot の方のフォーマットはこんな感じ。こちらは 3オクテットになります。
switchbot.png
isDualStateMode は、スマホアプリで壁スイッチモードを有効にすると true になります。isStatusOFF や group ID がどのように使用されているのかはわかりませんでした。

サンプルコード

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

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

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

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
ユーザプログラムからも扱いやすく技適の問題のない Bluetooth 温湿度計が、比較的安価に Amazon で購入できるというのはうれしいですね。次のセールがあったら、自宅内のまだ測定していないところ用に買い増ししたいところです。

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
warpzone

コメント

リンクをコピー
このコメントを報告

Thank you. I am eager to use your code, but my Pi cannot see the BLE advertisements for my meters. When I run your code, it waits indefinitely with no output. I also cannot see the BLE advertisements with hcitool. I think that this may be related to meter firmware. Mine are 2.5. Which firmware are you using, please?

329/5000
ありがとうございました。 私はあなたのコードを使用したいと思っていますが、私の「Raspberry Pi」は私のメーターのBLE広告を見ることができません。 コードを実行すると、出力なしで無期限に待機します。 また、hcitoolを使用してBLE広告を表示できません。 これはメーターのファームウェアに関連していると思われます。 私は2.5です。 どのファームウェアを使用していますか?

0
(編集済み)
リンクをコピー
このコメントを報告

Hi,

My SwitchBot Meter's firmware version is 2.4, so it's older than yours.
(and it seems to be unable to update on my Android app.)

Acording to official API document (https://github.com/OpenWonderLabs/python-host/wiki/Meter-BLE-open-API#BLE_Communication_Data_Message_Basic_Format), clients can read values proactively.

I don't know, but official apps possively read values from newer meters with proactive method.

1
リンクをコピー
このコメントを報告

Thank you for the reply. I will keep trying to get an answer from Switchbot themselves.

0
リンクをコピー
このコメントを報告

If you get any response from SwitchBot, I would be happy if you could share it here :-)

0
リンクをコピー
このコメントを報告

I will let you know.

0
リンクをコピー
このコメントを報告

Hello - I finally got a response from Switchbot when they released an updated version of python-host a few days ago. They never addressed my questions about firmware.

Both that updated version and your script now work with my Meters. The only possible reason I can think of is that something in my environment was different before, because I was also trying to get the older version of python-host working at the same time.

I've now put together a similar project: https://github.com/ronschaeffer/sbm2mqtt

Your project was a very big help in understanding the structure of the Meter advertisements. Thank you very much.

2
どのような問題がありますか?
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
2022年に流行る技術予想
~
64
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー