【解析&Python改造編】SwitchBotをPythonから操作する

SwitchBotは目覚まし代わりに照明スイッチをONにしてくれたり、リモコン代わりにもなるのでいろいろと便利なのですが、電池を交換すると設定がリセットされてしまう場合があるのでPythonでそのあたりのBluetooth LEのパケットデータを調べてみることにしました。




【注意】以降に記載内容は安全に動作する保証がなく実験レベルのものなので、発火や故障の可能性があるため、自己責任のもと、ご参照ください。また、bluetoothなどの電波関連は技術適合品を利用しています。技術適合マークがない場合は意図せず電波を送信してしまい電波法違反の可能性があるのでご注意ください。

用意したものはadafruitさんのnRF51822 Bluefruit LE Freiend ( Amazon ) というBluetooth Low Energyを扱うツールです。主要チップはNordic SemiconductorさんのnRF51822チップです。

※個体特定防止のため、一部、白塗りしています。

手順は Introducing the Adafruit Bluefruit LE Sniffer に記載されているので、その通りやっていくだけです。まず初めに、USBデバイスが何も刺さってないことを確認して、購入したBluefruitの基板が上図のように黒であればCP2104ドライバーをインストールしてください。基板がブルーの場合はFTDIドライバーをインストールします。

どちらかのドライバーをインストールしたらBluefruitをUSBポートに刺します。次にWindowsであれば、Python2をインストールします。Python3でないのは以下の記述があったためです。

以降ではWindows 10 pro 64bitを想定してPython2の導入をしていきます。Python2を開き、「Latest Python 2 Release – Python 2.X.X」のリンクを開き、「Windows x86-64 MSI installer」をクリックします。

注意点として以下のように「Add python.exe to Path」を「Will be installed on local hard drive」に設定し、コマンド プロンプト(dos窓)からPythonを実行できるようにしておいてください。

Python2の導入が終わったらWiresharkを開き、「Windows Installer (64-bit)」をインストールします。

Wireshark導入時に、以下のように「Wireshark Desktop Icon」にチェックを入れると便利です。

次にUsing with Sniffer V2を開き、extcap.zipをダウンロードしてZIPファイルを解凍しておきます。

Wiresharkを起動し、「wiresharkについて」をクリックします。

以下のように確認して、extcapフォルダを開きます。

先ほど解凍しておいたextcap.zipの中身を上記のグローバルExtcapパスのフォルダに移動します。

次にWiresharkを起動しなおすと、以下のように「nRF Sniffer COMX」が追加されているので、そちらをクリックします。

インターフェースツールバー「nRF Sniffer」を表示します。

下図のようにインターフェースツールのnRF SnifferのDeviceのプルダウンメニューからSwitchBotのMACアドレスを探して選択します。近接するSwitchBotのMACアドレスを検索するプログラムも利用できますが、お隣のも拾う可能性があるので、電池を抜いた場合に反応しないかなどで確認してください。

最後に動作確認でフィルターを「btle.master_bd_addr == (master_bdのMACアドレス) and btle.slave_bd_addr == (SwitchBotのMACアドレス) and btatt.handle == 0x0016」を指定して、iPhoneのSwitchBotアプリのONボタンを押すと以下のように「0x570101」がSwitchBot宛てに送信されているのが確認できます。その他のボタンもすべて押してみて調べ上げてPythonのコードに反映しました。バッテリー残量やスイッチの反転や長押し設定や通常のスイッチON/OFF操作ができるように改造してあります。

アップデートしたPythonのコードはこちらです。


search_mac_addr.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# 【注意】近接するSwitchBotのMACアドレスを検索するプログラムですが
# お隣のも拾う可能性があるので、電池を抜いた場合に反応しないかなどで
# 確認してください。
# apt-get install python3-pip
# pip3 install bluepy
# chmod 755 ./search_mac_addr.py
# ./search_mac_addr.py

from bluepy import btle

UUID_SERVICE='cba20d00-224d-11e6-9fb8-0002a5d5c51b'

if __name__ == '__main__': 
  index = 0   # 0=/dev/hci0
  timeout = 3.0
  scanner = btle.Scanner(index)
  devices = scanner.scan(timeout)

  print('SwitchBot list:') 
  for dev in devices:
    for (adTypeCode, desc, value) in dev.getScanData():
      if adTypeCode == 7 and value == UUID_SERVICE:
        print(f'  MAC address={dev.addr}, Type={dev.addrType}, RSSI={dev.rssi} dB')

switchbot.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright 2017-present WonderLabs, Inc. <support@wondertechlabs.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# refs. https://github.com/OpenWonderLabs/python-host/
# refs. https://github.com/kanon700/python-host/

# apt-get install python3-pip
# pip3 install bluepy
# chmod 755 ./switchbot.py
# ./switchbot.py switch_on
# ./switchbot.py switch_off

import binascii
from bluepy import btle
import json
import requests
import sys
import traceback

MAC_ADDRESS='XX:XX:XX:XX:XX:XX'   # Check the Mac address of SwitchBot from the setting screen of the app.
COMMANDS = {
  'press'              : '5701',     # press mode
  'switch_on'          : '570101',   # switch mode
  'switch_off'         : '570102',   # switch mode
  'down'               : '570103',
  'up'                 : '570104',
  'show_settings'      : '5702',
  'set_reverse_off'    : '57036410',
  'set_reverse_on'     : '57036411',
  'set_long_press_0s'  : '570f0800',
  'set_long_press_1s'  : '570f0801',
  'set_long_press_2s'  : '570f0802',
  'set_long_press_3s'  : '570f0803',
  'set_long_press_4s'  : '570f0804',
  'set_long_press_5s'  : '570f0805',
  'set_long_press_10s' : '570f080a',
  'set_long_press_20s' : '570f0814',
  'set_long_press_30s' : '570f081e',
  'set_long_press_40s' : '570f0828',
  'set_long_press_50s' : '570f0832',
  'set_long_press_60s' : '570f083c',
}
EXIT_FAILURE=1
EXIT_SUCCESS=0
HANDLE_READ=0x13
HANDLE_WRITE=0x16
TIMEOUT_SECONDS_NOTIFICATIONS=1
UUID_SERVICE='cba20d00-224d-11e6-9fb8-0002a5d5c51b'
UUID_SPECIFIED_CHARACTERISTIC='cba20002-224d-11e6-9fb8-0002a5d5c51b'

if __name__ == '__main__':
  if len(sys.argv) != 2:
    print('Usage: ./switchbot (' + '|'.join(COMMANDS.keys()) + ')')
    sys.exit(EXIT_FAILURE)

  if sys.argv[1] not in COMMANDS:
    print('Unknown parameter.')
    sys.exit(EXIT_FAILURE)
  key=sys.argv[1]

  try:
    p = btle.Peripheral(MAC_ADDRESS, btle.ADDR_TYPE_RANDOM)
    p.waitForNotifications(TIMEOUT_SECONDS_NOTIFICATIONS)
    svc = p.getServiceByUUID(UUID_SERVICE)
    ch = svc.getCharacteristics(UUID_SPECIFIED_CHARACTERISTIC)[0]
    p.writeCharacteristic(HANDLE_WRITE, binascii.a2b_hex(COMMANDS[key]), True)
    value = p.readCharacteristic(HANDLE_READ)
    result = {}
    if key == 'show_settings':
      result['battery'] = value[1]
      result['firmware'] = value[2] / 10.0
      result['n_timers'] = value[8]
      result['dual_state_mode'] = bool(value[9] & 16)
      result['inverse_direction'] = bool(value[9] & 1)
      result['hold_seconds'] = value[10]
    else:
      result['return'] = binascii.b2a_hex(value).decode()
    print(str(result))
    p.disconnect()

  except Exception as e:
    print(traceback.format_exc())
    sys.exit(EXIT_FAILURE)

sys.exit(EXIT_SUCCESS)

コメントを残す

メールアドレスが公開されることはありません。