電源をオンオフするテスト手順を自動化する

## 1. 低頻度で発生する不具合は何かと大変
何かと大変なので自動化できるものは自動化したいですよね。

### 1.1 起票が大変
フリーズする不具合をテストエンジニアのAさんが踏んだようです。リーダーのBさんから起票を頼まれました。

> Aさん「フリーズが発生したので同じ手順を10回試したところです。フリーズは最初の1回だけでした」
Bさん「再現するか確認したいのでもう10回お願いします」
・・・
Aさん「合計20回やってもう1回発生しました」
Bさん「再現性ありですね。2回発生／20回実行で起票してください」

必ず発生する手順が見つかれば開発者がとても助かるし再テストの工数も軽くて済みます。

> Bさん「もしできれば高頻度で再現する手順を見つけてほしいです」
Aさん「試してみます」
・・・
Aさん「思いつく範囲で試したのですがこれといってなかったです」
Bさん「わかりました、ありがとうございます」

高頻度で発生する手順は見つからなかったようです。

### 1.2 再テストも大変
起票した不具合が修正されたようです。

> Bさん「このあいだ起票したフリーズが修正されたので再テストしてください」
Aさん「2回発生／20回実行だったので20回やって一度も発生しなければOKですね」
Bさん「5回発生／20回実行だったら再テストは20回でよかったのだけど」
Aさん「高頻度の再現手順は見つからなかったんですよね」

どれくらいの確からしさで修正されたと判断するかはケースバイケースですが、例えば10%の再現率で発生する不具合が99%の確からしさで修正されたと判断するには44回試して一度も発生しない必要があります[^1]。

> Bさん「50回お願いします」

Aさんは起票で20回、再テストで50回、合計70回、同じ手順を繰り返すことになりました。

### 1.3 電源のオンオフが伴うと腕の筋肉的にも大変
ところでテストによってはコールドブート（機器の電源を完全に切った状態から起動すること）を伴うことがあります。「コンセントから電源コードを抜いて1分待つ」みたいなアレです。

回数が少なければ文字通りコンセントから電源コードを抜き差ししてできますがAさんのように何十回となるとスイッチ付きの電源タップを使ったりすると思います。とはいえ何十回もスイッチをバッチンバッチン切り替えるのはそれなりに腕が疲れてきて大変です。

## 2. 解決策
筆者はかつて組み込み系プログラマだったとき、電源タップのスイッチを一日に何度もオンオフするのが面倒になって秋月のH8/3048Fマイコンボードとリレーで電源オンオフ治具を作ってパソコンからコマンドで制御していたことがあります。ただ、今はもっと安全で安価な選択肢があり商用電源のAC100Vを自作の電源タップでオンオフするのはおすすめしません。

その後USB連動電源タップが登場しました。パソコン本体のUSBポートを監視してパソコン本体の電源オンオフに連動して周辺機器の電源をオンオフする商品です。ググるとこれとArduinoやRaspberry Piを組み合わせた製作事例がヒットします（[usb連動電源タップ arduino](https://www.google.co.jp/search?hl=ja&as_q=usb連動電源タップ+arduino)、[usb連動電源タップ Raspberry Pi](https://www.google.co.jp/search?hl=ja&as_q=usb連動電源タップ+Raspberry+Pi)）。ただ、この記事を書いている2022年11月においてUSB連動電源タップの廉価モデルは品薄のようです。

第三の選択肢として2016年ごろから出回り始めたスマートプラグを利用する方法があります。筆者が購入した[SwitchBot](https://www.switchbot.jp)の[プラグミニ](https://www.switchbot.jp/products/switchbot-plug-mini)は1個2千円くらいでBLE(Bluetooth Low Energy)で接続しPythonで制御できます。そこでこれを利用して電源をオンオフするテスト手順を自動化します。
なお、SwitchBotプラグミニは電気ストーブを始めとする危険源の接続を禁止しています。詳しくは[取扱説明書](https://drive.google.com/file/d/1ehcyqdJbvSazHQgB6rZ9zGXiei2mutz5/view?usp=sharing)をご確認ください。

https://www.switchbot.jp/products/switchbot-plug-mini

「[システムテスト自動化 標準ガイド](https://www.shoeisha.co.jp/book/detail/9784798139210)（通称：ギア本）」で自動化すべきでないテストの一つに「物理的なやりとりがあるテスト。例えば、カードリーダーにカードを通す、何かの装置の接続を外す、<b>電源をオフかオンにする</b> など」（強調筆者）が挙げられていますがこれは原著が刊行された1999年当時は安全かつ安価に自動で電源をオンオフさせるのがソフトウェアのエンジニアには難しくスケールもさせづらかったためと思います[^2]。それにギア本の著者らが「自動化すべきでない」といっても彼らが代わりにスイッチをバッチンバッチンしてくれるわけではありません。2022年のテクノロジを手にしている我々は遠慮なく自動化してしまいましょう。

## 3. プログラム
### 3.1 用意するもの
- SwitchBotプラグミニ
- SwitchBotのスマホアプリ（SwitchBotのBLE MACアドレスの調査に使用します）
- Windows PC
- Python3
- bleak

筆者の動作環境はWindows10 21H1、Python 3.10.0、bleak 0.19.1です。

```cmd:bleakのインストール手順
pip install bleak
```

### 3.2 ソースコード

```python:switchbotplugmini.py
# references
# https://qiita.com/hiratarich/items/00be23735ac6001ff74b
# https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/blob/latest/devicetypes/plugmini.md
# https://masahito.hatenablog.com/entry/2021/10/02/095828
# https://bleak.readthedocs.io/en/latest/index.html
# https://github.com/hbldh/bleak/issues/59

import binascii
import asyncio
from bleak import *

def switchbotplugmini(address, operation):
    """! switchbotplugmini brief.
    Turn Off, Turn On, Toggle, Read State the SwitchBot Plug Mini
    @param address  : BLE MAC Address
    @param operation: turnoff/turnon/toggle/readstate
    @return         : result(True/False)
                      resp(RESP message(0x0100:Off, 0x0180:On) or 0x0000)
    """

    #RX characteristic UUID of the message from the Terminal to the Device
    RX_CHARACTERISTIC_UUID = "cba20002-224d-11e6-9fb8-0002a5d5c51b"
    #TX characteristic UUID of the message from the Device to the Terminal
    TX_CHARACTERISTIC_UUID = "cba20003-224d-11e6-9fb8-0002a5d5c51b"
    
    result = True
    resp = b"\x00\x00"

    def callback(sender: int, data: bytearray):
        #print(f"{sender}: {data}")
        nonlocal resp
        resp = data

    async def run(loop):
        async with BleakClient(address, loop=loop) as client:
            await client.start_notify(TX_CHARACTERISTIC_UUID, callback)
            await client.write_gatt_char(RX_CHARACTERISTIC_UUID, bytearray(command), response=True)
            await asyncio.sleep(0.5)
            await client.stop_notify(TX_CHARACTERISTIC_UUID)

    if operation == "turnoff":
        command = b"\x57\x0f\x50\x01\x01\x00"
    elif operation == "turnon":
        command = b"\x57\x0f\x50\x01\x01\x80"
    elif operation == "toggle":
        command = b"\x57\x0f\x50\x01\x02\x80"
    elif operation == "readstate":
        command = b"\x57\x0f\x51\x01"
    else:
        print("ERROR, <turnoff/turnon/toggle/readstate>")
        return False, resp

    loop = asyncio.new_event_loop()
    try:
        asyncio.set_event_loop(loop)
        loop.run_until_complete(run(loop))
    except:
        print(sys.exc_info())
        result = False
    finally:
        asyncio.set_event_loop(None)
        loop.close()
        return result, resp

def main():
    if len(sys.argv) != 3:
        print("ERROR, python switchbotplugmini.py <BLE ADDRESS> <turnoff/turnon/toggle/readstate>")
        sys.exit(1)

    result, resp = switchbotplugmini(sys.argv[1], sys.argv[2])
    if result:
        if resp == b"\x01\x80":
            print(result, binascii.hexlify(resp), "on")
        elif resp == b"\x01\x00":
            print(result, binascii.hexlify(resp), "off")
        else:
            print(result, binascii.hexlify(resp)) 
        sys.exit(0) #result==True, exit(0)
    else:
        print(result, binascii.hexlify(resp))
        sys.exit(1)

if __name__ == "__main__":
    main()
```

### 3.3 使い方
SwitchBot Plug MiniのBLE MACアドレスはSwitchBotのスマホアプリで調べて置き換えてください。

#### 3.3.1 コマンドプロンプトから

```cmd:実行例1
>python switchbotplugmini.py 00:00:5E:00:53:00 turnon
True b'0180' on

>python switchbotplugmini.py 00:00:5E:00:53:00 readstate
True b'0180' on

>python switchbotplugmini.py 00:00:5E:00:53:00 turnoff
True b'0100' off

>python switchbotplugmini.py 00:00:5E:00:53:00 readstate
True b'0100' off

>python switchbotplugmini.py 00:00:5E:00:53:00 toggle
True b'0180' on

>python switchbotplugmini.py 00:00:5E:00:53:00 toggle
True b'0100' off
```

```cmd:実行例2：SwitchBotプラグミニが検出されない場合
>python switchbotplugmini.py 00:00:5E:00:53:00 turnon
(<class 'bleak.exc.BleakDeviceNotFoundError'>, BleakDeviceNotFoundError('Device with address 00:00:5E:00:53:00 was not found.'), <traceback object at 0x00000287FF354EC0>)
False b'0000'
```

#### 3.3.2 モジュールとして外部Pythonプログラムから呼び出す
switchbotplugmini.pyとexternal.pyを同じフォルダに配置します。

```python:external.py
from switchbotplugmini import *

address="00:00:5E:00:53:00" #replace your SwitchBot Plug Mini MAC Address.
def on():
    result, resp = switchbotplugmini(address, "turnon")
    print(result, binascii.hexlify(resp))

def off():
    result, resp = switchbotplugmini(address, "turnoff")
    print(result, binascii.hexlify(resp))

def toggle():
    result, resp = switchbotplugmini(address, "toggle")
    print(result, binascii.hexlify(resp))

def readstate():
    result, resp = switchbotplugmini(address, "readstate")
    print(result, binascii.hexlify(resp))

print("-- Turn ON --")
on()
print("-- Read State --")
readstate()

print("-- Turn OFF --")
off()
print("-- Read State --")
readstate()

print("-- Toggle --")
toggle()
print("-- Read State --")
readstate()

print("-- Toggle --")
toggle()
print("-- Read State --")
readstate()
```

```cmd:実行例
>python external.py
-- Turn ON --
True b'0180'
-- Read State --
True b'0180'
-- Turn OFF --
True b'0100'
-- Read State --
True b'0100'
-- Toggle --
True b'0180'
-- Read State --
True b'0180'
-- Toggle --
True b'0100'
-- Read State --
True b'0100'
```

## 4. まとめ
電源のオンオフをソフトウェアで制御できるようになりました。低頻度で発生する不具合の再テストに限らずいろいろ応用できそうです。
- Pythonで作成されているテストツールへの組み込み
- スモークテスト（新しいファームウェアをFlash ROMに焼いて再起動して実施する簡易チェック）の自動化
- 自動テストでテストがこけたりフリーズしたりしたときに再起動してテストを再開する[^3]

また、SwitchBotプラグミニは本体側面の電源スイッチの押し心地が軽快で、手動で電源をオンオフする場合でも伝統的なシーソースイッチの電源タップと比べて筋肉的な疲労が少なくて済みそうと思いました。QAならとりあえず1個持っていると役立つと思います。

## 付録A. 参考資料
- [1/10回で起こるバグの改修試験は何回すべきか統計学で考える | MISO](https://www.tdi.co.jp/miso/system-improvement-probability-statistics)
- [SwitchBotをWindows 10 から制御する](https://qiita.com/hiratarich/items/00be23735ac6001ff74b)
- [SwitchBotAPI-BLE/plugmini.md at latest · OpenWonderLabs/SwitchBotAPI-BLE · GitHub](https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/blob/latest/devicetypes/plugmini.md)
- [Python 3.10触ってみた(1) ~asyncio#get_event_loop - Keep on moving](https://masahito.hatenablog.com/entry/2021/10/02/095828)
- [bleak &mdash; bleak 0.20.0a1 documentation](https://bleak.readthedocs.io/en/latest/index.html)
- [write_gatt_char(..., response=True) not returning bytearray · Issue #59 · hbldh/bleak · GitHub](https://github.com/hbldh/bleak/issues/59)

[^1]: 参考：[1/10回で起こるバグの改修試験は何回すべきか統計学で考える | MISO](https://www.tdi.co.jp/miso/system-improvement-probability-statistics)
[^2]: 2022年3月に登場したSwitchBotプラグミニが1個2千円で買ってきてPythonで数十行プログラムを書けばすぐに電源をオンオフできるのに対して1999年頃はマイコンのプログラムを書くだけでは済まず電子工作やケース加工も必要で、安全かつ安価にソフトウェア制御で電源をオンオフするのは当時のソフトウェアのエンジニアにはハードルが高かったと思います。
[^3]: テスト環境のトラブルでテストが失敗した場合はテストを安全に終了させる必要があります。