Hobby Science&Experiment

愛と工作の日々

趣味でやっている工作や勉強したことのメモ書きです。

Switchbot温度計とラズパイで快適IoTライフ

Switchbot温度計をフル活用するためラズパイで測定値を取得することを試みます。意外と情報が少なく苦戦したため記録しておきます。

Switchbot温湿度計

ボタンプッシュロボットでおなじみのWonderlabs IncによるIoT温湿度計です。電源も電池駆動なので使い勝手が良く、スマートハウス利用には無限の可能性を感じます。

同社が発売中のHubを利用すればAlexaやその他の家電と連携させることも可能です。私はHubが必要と知らずに1年前に買ってショックを受けました…。単体でもBluetoothスマホアプリと連携可能ですが、アプリの出来があんまり良くないです。
私はケチな上ラズパイを中心にスマートホームを組みたいと考えているため、Hubを使わない方法を模索します。

うまくいかなかったコード

検索すると一番にヒットするこちらの記事の方法は、私の環境では上手く行きませんでした・・・。少しコードをいじったりしてみたのですが、小手先の修正では動かず・・・。原因が分かったら報告します。
qiita.com

実行結果

$ sudo python3  switchbot_meter.py
^CTraceback (most recent call last):
  File "switchbot_meter.py", line 48, in <module>
    scanner.scan( 0 )
  File "/usr/local/lib/python3.5/dist-packages/bluepy/btle.py", line 853, in scan
    self.process(timeout)
  File "/usr/local/lib/python3.5/dist-packages/bluepy/btle.py", line 821, in process
    resp = self._waitResp(['scan', 'stat'], remain)
  File "/usr/local/lib/python3.5/dist-packages/bluepy/btle.py", line 347, in _waitResp
    rv = self._helper.stdout.readline()
KeyboardInterrupt

実行すると固まってしまいますので、キーボードインタラプトしてます。スキャンのタイムアウトが無効になっているため、いつまでもスキャンを続けてるみたいです。デバイスはラズパイからは見えているのに、なんでスキャンが終わらないんだろう。
結局ギブアップして、別の方法を探しました。

とりあえず動いたコード

諦め掛けていたのですが、スイッチボット開発元のWonderLabsが公開しているpython-hostというライブラリの中にそれっぽいのを見つけました。
GitHub - OpenWonderLabs/python-host: The python code running on Raspberry Pi or other Linux based boards to control SwitchBot.
f:id:tara-chang:20200615215728p:plain
インストール方法はリンク先のgithubに詳しく書いてあります。このライブラリは、ボタンタイプのスイッチボットでも使えるやつなので、私はすでにインストールしてました。
インストールが済んだら早速ディレクトリに移動し、 switchbot_meter.pyを実行してみます。MACアドレスを入れる所がないのですが、とりあえず実行。

~$ cd python-host
~/python-host $ sudo python switchbot_meter.py

結果

Usage: "sudo python switchbot.py [mac_addr  cmd]" or "sudo python switchbot.py"
Start scanning...
22 16b Service Data 000d5410d6089a40 16
scan timeout
(0, [u'XX:XX:XX:XX:XX:XX', "Humiture:26.8'C 64%"])
Input the device number to control:

おお、温度と湿度が取得できました。私のスイッチボットの表示温度、MACアドレスの値が表示されてます!ちなみにpython3のコード(switchbot_meter_py3.py)を実行する際には、pip3でpexpectのインストールが必要でした(No module named 'pexpect’ というエラーが出る)。
「Input the device number to control:」の表示でデバイスナンバーの入力を求められましたので、0を入れると、処理が先に進みました。

Input the device number to control:0
Preparing to connect.
Connection successful.
Traceback (most recent call last):
  File "switchbot_meter.py", line 232, in <module>
    main()
  File "switchbot_meter.py", line 222, in main
    trigger_device(bluetooth_adr)
  File "switchbot_meter.py", line 167, in trigger_device
    tempFra = int(data[3], 16) / 10.0
IndexError: string index out of range

コネクション成功、その後よくわからないエラーで止まってしまいました。デバイスナンバーに関してはおそらく複数台のスイッチボットを一括で管理することを想定しているのではないでしょうか。今回は1台ですので、0以上の数字はレンジアウトとなります。
とりあえず温度と湿度をラズパイで取得する、という目的はこれで達せられそうです。返り値がシンプルになるようにコードを書き換えます。

とりあえず動いたコードを修正

やっつけではありますがシンプルに温度と湿度とアドレスだけ返してくれるように修正しました。ライブラリが揃っていればpython2でも3でも動くと思います。

#!/usr/bin/env python3
import pexpect
import sys
from bluepy.btle import Scanner, DefaultDelegate
import binascii

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

class DevScanner(DefaultDelegate):
    def __init__( self ):
        DefaultDelegate.__init__(self)
        #print("Scanner inited")

    def dongle_start(self):
        self.con = pexpect.spawn('hciconfig hci0 up')
        time.sleep(1)

    def dongle_restart(self):
        print("restart bluetooth dongle")
        self.con = pexpect.spawn('hciconfig hci0 down')
        time.sleep(3)
        self.con = pexpect.spawn('hciconfig hci0 up')
        time.sleep(3)

    def scan_loop(self):
        # service_uuid = '1bc5d5a50200b89fe6114d22000da2cb'
        service_uuid = 'cba20d00-224d-11e6-9fb8-0002a5d5c51b'
        menufacturer_id = '5900f46d2c8a5f31'
        dev_list =[]
        bot_list =[]
        enc_list =[]
        link_list =[]
        meter_list=[]
        self.con = pexpect.spawn('hciconfig')
        pnum = self.con.expect(["hci0",pexpect.EOF,pexpect.TIMEOUT])
        if pnum==0:
            self.con = pexpect.spawn('hcitool lescan')
            #self.con.expect('LE Scan ...', timeout=5)
            scanner = Scanner().withDelegate(DevScanner())
            devices = scanner.scan(10.0)
            print("Start scanning...")
        else:
            raise Error("no bluetooth error")

        for dev in devices:
            mac = 0
            for (adtype, desc, value) in dev.getScanData():
                # print(adtype,desc,value)
                if desc == '16b Service Data':
                    model = binascii.a2b_hex(value[4:6])
                    mode  = binascii.a2b_hex(value[6:8])
                    if len(value) == 16:
                        # print(adtype,desc,value,len(value))
                        # celsius
                        tempFra = int(value[11:12].encode('utf-8'), 16) / 10.0
                        tempInt = int(value[12:14].encode('utf-8'), 16)
                        if tempInt < 128:
                            tempInt *= -1
                            tempFra *= -1
                        else:
                            tempInt -= 128
                        meterTemp = tempInt + tempFra
                        meterHumi = int(value[14:16].encode('utf-8'), 16) % 128
                        # print("meter:", meterTemp, meterHumi)
                    else:
                        meterTemp = 0
                        meterHumi = 0
                elif desc == 'Local name':
                    if value == "WoHand":
                        mac   = dev.addr
                        model = 'H'
                        mode  = 0
                    elif value == "WoMeter":
                        mac   = dev.addr
                        model = 'T'
                        mode = 0
                elif desc == 'Complete 128b Services' and value == service_uuid :
                    mac = dev.addr

            if mac != 0 :
                #print(binascii.b2a_hex(model),binascii.b2a_hex(mode))
                dev_list.append([mac, model.decode('utf-8'), mode, meterTemp, meterHumi])

        # print(dev_list)
        for (mac, dev_type,mode,meterTemp, meterHumi) in dev_list:
            if dev_type == 'L':
                link_list.append(mac)
            if dev_type == 'H'  or ord(dev_type) == ord('L') + 128:
                #print(int(binascii.b2a_hex(mode),16))
                if int(binascii.b2a_hex(mode),16) > 127 :
                    bot_list.append([mac,"Turn On"])
                    bot_list.append([mac,"Turn Off"])
                    bot_list.append([mac,"Up"])
                    bot_list.append([mac,"Down"])
                else :
                    bot_list.append([mac,"Press"])
            elif dev_type == 'T':
                meter_list.append([mac, meterTemp, meterHumi])
#                meter_list = {"a":"1", "b":meterHumi}
            if ord(dev_type) == ord('L') + 128:
                enc_list.append([mac,"Press"])
        # print(bot_list)
        #print("Scan timeout.")
        return bot_list + meter_list
        pass

    def register_cb( self, fn ):
        self.cb=fn
        return

    def close(self):
        #self.con.sendcontrol('c')
        self.con.close(force=True)

def main():
    #Check bluetooth dongle
    #print('Usage: "sudo python switchbot.py [mac_addr  cmd]" or "sudo python switchbot.py"')
    connect = pexpect.spawn('hciconfig')
    pnum = connect.expect(["hci0",pexpect.EOF,pexpect.TIMEOUT])
    if pnum!=0:
        print('No bluetooth hardware, exit now')
        sys.exit()
    connect = pexpect.spawn('hciconfig hci0 up')

    if len(sys.argv) == 3 or len(sys.argv) == 4:
        dev = sys.argv[1]
        act = sys.argv[2] if len(sys.argv) < 4  else  ('Turn ' + sys.argv[3] )
        #trigger_device([dev,act])

    elif len(sys.argv) == 1:
        #Start scanning...
        scan = DevScanner()
        dev_list = scan.scan_loop()
        print(dev_list)
        dev = sys.argv[1] if len(sys.argv) > 1 else None
        dev_number = None

        if not dev_list:
            print("No SwitchBot nearby, exit")
            sys.exit()

    else :
        print('wrong cmd.')
        print('Usage: "sudo python switchbot_meter_py3.py [mac_addr  cmd]" or "sudo python switchbot_meter_py3.py"')

    #connect = pexpect.spawn('hciconfig')
    return(dev_list)
    #sys.exit()
if __name__ == "__main__":
    main()

実行結果

~/python-host $ sudo python3 SBHumiture.py
Start scanning...
[['XX:XX:XX:XX:XX:XX', 27.0, 65]]

色々余分なコードも残ってそうですが、とりあえず所望の値が帰ってきました。スキャンに失敗した際は"No SwitchBot nearby, exit"と表示されます。
毎回スキャンするのではなくアドレス指定でコマンドした方がシンプルだし省エネのような気がしますが、そこまでは出来ませんでした・・・。もし出来たら載せます!