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

## 1. はじめに
「[電源をオンオフするテスト手順を自動化する](https://qiita.com/pbjpkas/items/e96bf25ea2af901e31ad)」の続編です。前編も併せてご覧ください。

Raspberry Piで電源オンオフができると自動テストでできることが増え、Windows PCが必須でなくなることで機材面の柔軟性も増します。Raspberry PiからSwitchBot Plug Miniをオンオフするのは[SwitchBotのGitHub](https://github.com/OpenWonderLabs)で公開されているRaspberry Pi向けPythonプログラムにコマンドを追加すればサクッとできそうと思ったものの意外とハマったのでまとめます。
![embedded_ci6b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/d963288e-51b1-3b2e-c7a5-3bf74a9b7c80.png)


## 2. 環境
- Raspberry Pi Zero WH Rev 1.1
- Raspberry Pi OS Lite(32-bit)
  - A port of Debian Bullseye with no desktop environment
  - リリース日時：2022-09-22
- Python 3.9.2(Raspberry Pi OS Liteにプリインストールされているもの)

## 3. 環境構築
[OpenWonderLabs/python-host](https://github.com/OpenWonderLabs/python-host)のInstallationの手順をアレンジして実行します。#1、#2は補足の章をご覧ください。

```
sudo apt-get update
sudo apt-get install python3-pip                              #1
sudo pip3 install pexpect                                     #1
sudo apt-get install libusb-dev libdbus-1-dev libglib2.0-dev
sudo reboot                                                   #2
sudo apt-get install libudev-dev libical-dev libreadline-dev
sudo pip3 install bluepy
```

筆者の環境にインストールされたバージョンを以下に示します。
```
python3-pip:all/bullseye 20.3.4-4+rpt1+deb11u1 uptodate
libdbus-1-dev:armhf/bullseye 1.12.24-0+deb11u1 uptodate
libglib2.0-dev:armhf/bullseye 2.66.8-1 uptodate
libusb-dev:armhf/bullseye 2:0.1.12-32 uptodate
libical-dev:armhf/bullseye 3.0.9-2 uptodate
libreadline-dev:armhf/bullseye 8.1-1 uptodate
libudev-dev:armhf/bullseye 247.3-7+rpi1+deb11u1 uptodate

pexpect       4.8.0
bluepy        1.3.0
```

## 4. ソースコード
SwitchBotのGitHubで公開されている[switchbot_py2topy3.py](https://github.com/OpenWonderLabs/python-host/blob/master/switchbot_py2topy3.py)をベースにPlug Mini対応を行います。
- LE address typeをpublicにする（-t randomを削除する）[^1]
- SwitchBotプラグミニが検出されないときにExceptionを吐いて死なないように修正
- Plug Miniの制御コマンドを追加
- Usageのメッセージ文を変更
- Plug Miniの制御に不要なコードの削除

```python:switchbot_py2topy3_plugmini.py
#!/usr/bin/env python3
# switchbot_py2topy3_plugmini.py by ka's
# Apache License, Version 2.0
# based on https://github.com/OpenWonderLabs/python-host/blob/master/switchbot_py2topy3.py
# ----------------------------------------------------------------------
# (switchbot_py2topy3.py)
# 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.
import pexpect
import sys


def trigger_device(device):
    '''! trigger_device brief.
    turnoff, turnon, toggle, readstate the SwitchBot Plug Mini
    @return : '0000':Error, '0001':Timeout, '0100':Off, '0180':On
    '''
    [mac, dev_type, act] = device
    # print 'Start to control'
    con = pexpect.spawn('gatttool -b ' + mac + ' -I') # remove "-t random" for Plug Mini
    con.expect('\[LE\]>')
    # print('Preparing to connect.')
    retry = 3
    index = 0
    while retry > 0 and 0 == index:
        con.sendline('connect')
        # To compatible with different Bluez versions
        index = con.expect(
            ['Error', '\[CON\]', 'Connection successful.*\[LE\]>', pexpect.exceptions.TIMEOUT])
        retry -= 1
    if 0 == index:
        print('Connection error.')
        return '0000'
    elif 3 == index:
        print('Connection timeout.')
        return '0001'

    # print('Connection successful.')
    con.sendline('char-desc')
    con.expect(['\[CON\]', 'cba20002-224d-11e6-9fb8-0002a5d5c51b'])
    cmd_handle = con.before.decode('utf-8').split('\n')[-1].split()[2].strip(',')
    if dev_type == 'PlugMini':
        if act == 'turnoff':
            con.sendline('char-write-cmd ' + cmd_handle + ' 570f50010100')
        elif act == 'turnon':
            con.sendline('char-write-cmd ' + cmd_handle + ' 570f50010180')
        elif act == 'toggle':
            con.sendline('char-write-cmd ' + cmd_handle + ' 570f50010280')
        elif act == 'readstate':
            con.sendline('char-write-cmd ' + cmd_handle + ' 570f5101')
        else:
            print('Unsupported cmd')
            return '0000'

        con.expect('\[LE\]>')
        con.sendline('char-read-uuid cba20003-224d-11e6-9fb8-0002a5d5c51b')
        index = con.expect(['value:[0-9a-fA-F ]+', 'Error'])
        if index == 0:
            data = con.after.decode('utf-8').split(':')[1].replace(' ', '')
        else:
            data = '0000'
            print('Error!')
    else:
        print('Unsupported operations')
        return '0000'
    con.expect('\[LE\]>')
    con.sendline('quit')
    # print('Complete')
    return data


def main():
    # Check bluetooth dongle
    # print(
    #    'Usage: "sudo python3 switchbot_py2topy3_plugmini.py [mac dev_type cmd]"')
    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')

    # print(sys.argv, len(sys.argv))

    if len(sys.argv) == 4:
        dev = sys.argv[1]
        dev_type = sys.argv[2]
        act = sys.argv[3]
        result = trigger_device([dev, dev_type, act])
        if result == '0000':
            print(result, 'Error')
        elif result == '0001':
            print(result, 'Timeout')
        elif result == '0100':
            print(result, 'off')
        elif result == '0180':
            print(result, 'on')
        else:
            print(result, 'unknown')

    else:
        print('Wrong cmd!')
        print(
            'Usage: "sudo python3 switchbot_py2topy3_plugmini.py [mac dev_type cmd]"')

    connect = pexpect.spawn('hciconfig')

    sys.exit()


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

## 5. 使い方
### 5.1 コマンドラインから
sudoで実行します。SwitchBot Plug MiniのBLE MACアドレスはSwitchBotのスマホアプリで調べて置き換えてください。

```sh:実行例1
$ sudo python3 switchbot_py2topy3_plugmini.py 00:00:5E:00:53:00 PlugMini turnon
0180 on
$ sudo python3 switchbot_py2topy3_plugmini.py 00:00:5E:00:53:00 PlugMini readstate
0180 on
$ sudo python3 switchbot_py2topy3_plugmini.py 00:00:5E:00:53:00 PlugMini turnoff
0100 off
$ sudo python3 switchbot_py2topy3_plugmini.py 00:00:5E:00:53:00 PlugMini readstate
0100 off
$ sudo python3 switchbot_py2topy3_plugmini.py 00:00:5E:00:53:00 PlugMini toggle
0180 on
$ sudo python3 switchbot_py2topy3_plugmini.py 00:00:5E:00:53:00 PlugMini toggle
0100 off
```

```sh:実行例2：SwitchBotプラグミニが検出されない場合
$ sudo python3 switchbot_py2topy3_plugmini.py 00:00:5E:00:53:00 PlugMini turnon
Connection timeout.
0001 Timeout
```

### 5.2 モジュールとして外部Pythonプログラムから呼び出す
switchbot_py2topy3_plugmini.pyとexternal_switchbot_py2topy3_plugmini.pyを同じディレクトリに配置します。

```python:external_switchbot_py2topy3_plugmini.py
from switchbot_py2topy3_plugmini import *

mac      = "00:00:5E:00:53:00" #replace your SwitchBot Plug Mini MAC Address.
dev_type = "PlugMini"

def on():
    result = trigger_device([mac, dev_type, "turnon"])
    print(result)

def off():
    result = trigger_device([mac, dev_type, "turnoff"])
    print(result)

def toggle():
    result = trigger_device([mac, dev_type, "toggle"])
    print(result)

def readstate():
    result = trigger_device([mac, dev_type, "readstate"])
    print(result)

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()
```

```python:実行例
$ sudo python3 external_switchbot_py2topy3_plugmini.py
-- Turn ON --
0180
-- Read State --
0180
-- Turn OFF --
0100
-- Read State --
0100
-- Toggle --
0180
-- Read State --
0180
-- Toggle --
0100
-- Read State --
0100
```

## 6. おわりに
- Windowsに加えてRaspberry PiもSwitchBot Plug Miniをオンオフできるようになり、テストツールの選択肢が広がりました。
- Windows PCや上位機種のRaspberry Piと比べて安価で低スペックな機種でできたことで、テスト自動化のハードルが下がったと思います。

## 補足1. pexpectのインストール
「sudo apt-get install python-pexpect」をRaspberry Pi OS Bullseyeで実行するとエラーになります。
>sudo apt-get install python-pexpect
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
Package python-pexpect is not available, but is referred to by another package.
This may mean that the package is missing, has been obsoleted, or
is only available from another source
However the following packages replace it:
  python-pexpect-doc
>
>E: Package 'python-pexpect' has no installation candidate

## 補足2. reboot
libdbus-1-devのインストールで再起動が要求されます。
>Setting up dbus (1.12.24-0+deb11u1) ...
A reboot is required to replace the running dbus-daemon.
Please reboot the system when convenient.

## 補足3. 電源オンオフ以外のテスト自動化
「[リモートワークでテストを行うためのヒント](https://qiita.com/pbjpkas/items/6b2f08273aa9a6e90d0f)」に筆者のテスト自動化の記事一覧があります。ご参考になれば幸いです。

[^1]: もともとのswitchbot_py2topy3.pyが対応しているBot、Meter、CurtainとPlug MiniでLE address typeが異なっているのは意外でした。なんで機器と接続できないのだろうとしばらくハマっていました。