Bluetooth ワットチェッカー RS-BTWATTCH2 のデータを取得する (Windows, Python, Bleak)

# 1: はじめに

Bluetooth (BLE) ドングルを接続した Windows 11 で、RS-BTWATTCH2 が送信するアドバイタズデータを受信し、各種情報（リレー ON/OFF 状態, 電圧, 電流, 消費電力）を取得する Python プログラムを紹介しています。

* できること
    * 各種情報を **30秒に1回程度** の頻度で取得する
* できないこと
    * リレーを遠隔操作する
    * 各種情報を1秒ごとに取得する

Windows で動作を確認していますが、Bluetooth ペアリングせずにデータを取得しているため、Linux などでも同じように動くと思われます。

## 1-1: Bluetooth ワットチェッカー RS-BTWATTCH2

Bluetooth ワットチェッカー RS-BTWATTCH2 (ラトックシステム) は、スマホ専用アプリを用いると Bluetooth 接続でリアルタイムの消費電力を測定したり、リレーの ON/OFF を操作したりできます。

残念ながら、Windows にはアプリが用意されておらず、リアルタイム測定やリレー ON/OFF に関する詳細な仕様は公開されていません。  
ただし、Bluetooth (BLE) で30秒に1回送信しているアドバイタズデータには各種情報が含まれており、仕様も公開されています。

https://www.ratocsystems.com/products/sensor/watt/rsbtwattch2/

## 1-2: Wi-Fi ワットチェッカー RS-WFWATTCH1
今回は Bluetooth 接続のワットチェッカーを使用していますが、同じラトックシステムが販売している Wi-Fi 接続のワットチェッカーの方が頻繁にデータを取得できて良さそうです。

https://qiita.com/yamaokunousausa/items/2faedd6481093e73e2ca

## 1-3: 参考情報

今回はアドバイタデータから情報を取得するプログラムですが、GATT接続？で専用アプリと同等に通信するプログラムは他にあります。  
ただ、私の環境では動きませんでした。

* GitHub - [vpcf/py_btwattch2](https://github.com/vpcf/py_btwattch2)
* GitHub Gist - [vpcf/RS-BTWATTCH2用 測定クライアント試作](https://gist.github.com/vpcf/e46606f8cd07ae7054d6c4525ab7d90d)
* Gomasy's blog - [Bluetooth ワットチェッカー（RS-BTWATTCH2）で遊ぶ with Raspberry Pi 4](https://blog.gomasy.jp/17.html)

ワットチェッカー販売元のラトックシステムが、アドバイタデータのフォーマットを公開しています。
公開されたフォーマットを参考に、プログラムを作成しています。（資料請求すると PDF データでもらえる）

https://sol.ratocsystems.com/solution/btwattch2fmt/

# 2: 前準備
GATT 接続とは異なり、Bluetooth ペアリングの必要はありません。  
ただ、使っているワットチェッカーの MAC アドレスを調べるためにペアリングしたほうが良いかもしれません。

```bash
# Bleak のインストールは必要
pip install bleak
```

# 3: Python プログラム

```python:btwattch2_advertise.py
import asyncio
from bleak import BleakScanner
from datetime import datetime
import struct

"""
必要ライブラリのインストール
pip install bleak
"""

# BTWATTCH2 の Manufacturer ID
MANUFACTURER_ID = 0x0B60

# BTWATTCH2 MACアドレス
BTWATTCH2_ADDRESS = None  # 不明な場合
BTWATTCH2_ADDRESS = "E3:35:D6:6B:21:24"  # 調べた場合

# 受信時刻を記録（デバイスごとに保存）
last_received_time = {}

def parse_values(bytes):
    """
    データを解析し、電力値(W)を取得
    bytes: 01f4030000000000 (例)
           ^ ^   ^   ^
           1 2   3   4
    1: リレーの状態、符号なし1バイト 0: オフ, 1: オン
    2: 電圧の10倍値、符号なし2バイト 単位 [V]
    3: 電流値、符号なし2バイト 単位 [mA]
    4: 電力の1000倍値、符号なし3バイト 単位 [W]
    バイトオーダーはリトルエンディアン

    詳細は https://sol.ratocsystems.com/solution/btwattch2fmt/
    """
    if len(bytes) == 8:
        # 先頭のみ解釈
        relay, voltage, current = struct.unpack("<BHH", bytes[:5])
        # 電力値を解釈
        power = int.from_bytes(bytes[5:8], byteorder="little", signed=False)
        return {
            "relay": relay == 1,
            "voltage": voltage / 10,
            "current": current,
            "power": power / 1000
        }
    return None

def detection_callback(device, advertisement_data):
    """アドバタイズデータを取得するコールバック"""

    # BTWATTCH2 の MAC アドレスが指定されているとき
    if BTWATTCH2_ADDRESS and device.address.upper() != BTWATTCH2_ADDRESS:
        return  # 指定アドレス以外は無視

    manufacturer_data = advertisement_data.manufacturer_data
    if not manufacturer_data:
        return  # manufacturer_data が存在しない場合は無視
    
    current_time = datetime.now()
    last_time = last_received_time.get(device.address, None)

    for manufacturer_id, bytes in manufacturer_data.items():
        # 時刻記録
        timestamp = current_time.strftime("%Y-%m-%d %H:%M:%S")

        # デバッグ
        # print(f"[{timestamp}] {device.name}({device.address}), ManufacturerID: {hex(manufacturer_id)}")

        # BTWATTCH2 以外は無視
        if manufacturer_id != MANUFACTURER_ID:
            return
        
        # 30秒以内に受信した場合は無視
        if last_time and (current_time - last_time).total_seconds() < 30:
            return
        
        last_received_time[device.address] = current_time
        
        values = parse_values(bytes)
        print(f"[{timestamp}] Values: {values})")

async def scan_for_manufacturer_data():
    scanner = BleakScanner(detection_callback=detection_callback)

    print("Scanning for RS-BTWATTCH2...")
    while True:
        try:
            await scanner.start()
            await asyncio.sleep(10)
            await scanner.stop()
        except Exception as e:
            print(f"Error: {e}")
            await asyncio.sleep(30)  # 再試行

asyncio.run(scan_for_manufacturer_data())

```

# 4: 実行結果
```bash
python btwattch2_advertise.py
```

```:実行結果
Scanning for RS-BTWATTCH2...
[2025-02-24 16:57:51] Values: {'relay': True, 'voltage': 100.3, 'current': 0, 'power': 0.0})
[2025-02-24 16:58:21] Values: {'relay': True, 'voltage': 100.0, 'current': 0, 'power': 0.0})
```