[Switchbot API v1.1が登場] APIでSwitchBotロックを遠隔操作する

# Switchbot APIのv1.1が登場
この記事を書いているのが2022/09/08ですが2日前くらいにSwitchbotのAPIがv1.0 -> v1.1に移行した模様です。

僕的に一番大きな変更点として**Smart Lockを制御できるようになった**ことです。
今までセキュリティ上の観点から公開されているAPIでは、SwitchBotロックを制御することはできませんでしたが(~~なぜか一時期使用できていましたが~~)、v1.1の登場により制御ができるようになりました。

他にも変更点があると思いますが、本記事ではSwitchBotロックに着目し、Pythonを使って制御してみようかと思います。**なおSwitchbotのSwitchBotロックを制御したりするためにはSwitchbotのHubが必要になるので注意してください。**

:::note info
Switchbotに関するコードを[GitHub](https://github.com/masaki12-s/switchbot_python)に公開しています。本記事に関するコードは`src/utils.py`,`src/lock.py`になるので良ければ参考にしてください。
:::


# 署名の追加
`v1.0`から`v1.1`への一番大きな変更は署名が必要になったことです。
この署名の追加でセキュリティ面でのリスクが減ったことにより、SwitchBotロックを制御できるようになったと考えられます。
今までSwitchbotのAPIを使用するためには、専用のAPI用の`token`が必要でした。
この`token`を使うことにより、自分が持っているデバイスの見れたり制御できたりしました。
しかしv1.1のAPIでは`token`だけでなく、`secret`キーが必要になります。
そしてこの2つを使用して作成した署名を使って制御することになります。

署名の作成は[公式ドキュメント](https://github.com/OpenWonderLabs/SwitchBotAPI)に載っていますがPython3用の署名作成コードを以下にも記述しておきます。
```python
import time
import hashlib
import hmac
import base64

# open token
token = '' # copy and paste from the SwitchBot app V6.14 or later
# secret key
secret = '' # copy and paste from the SwitchBot app V6.14 or later
nonce = ''
t = int(round(time.time() * 1000))
string_to_sign = '{}{}{}'.format(token, t, nonce)

string_to_sign = bytes(string_to_sign, 'utf-8')
secret = bytes(secret, 'utf-8')

sign = base64.b64encode(hmac.new(secret, msg=string_to_sign, digestmod=hashlib.sha256).digest())
print ('Authorization: {}'.format(token))
print ('t: {}'.format(t))
print ('sign: {}'.format(str(sign, 'utf-8')))
print ('nonce: {}'.format(nonce))
```

ここで作成した`t`,`sign`,`nonce`をAPIのheaderとして新たに追加します。
次からはこのコードを絡めたデバイス一覧の取得からSmartLockの制御まで説明していきます。

# 手順
## アプリをアップデートする
Switchbot API v1.1を使用するためには、`secret`キーが必要になります。
そのためにアプリのバージョンを`v6.14`以降にする必要があります。
まだ行っていない方はアップデートをしましょう。

## tokenとsecretを取得する
次にSwitchbotアプリ内から`token`と`secret`を取得します。
以下の手順に沿って取得しましょう。

1\. SwitchBotのアカウント登録し、ログインする
2\. アプリ内の下にあるタブからプロフィール > 設定 の順にタップ
3\. アプリバージョンを10回タップ > 開発者向けオプションが表示される
4\. 開発者向けオプションを選択
5\. 上の`token`、下の`secret`が表示されるのでコピーする

:::note alert
ここで表示される`token`と`secret`は**絶対に公開しないでください**。
他人が制御できるようになってしまいます。もし公開してしまった場合はアプリ内のボタンからキーをリセットしましょう。
:::

## デバイス一覧を取得する
v1.0でもデバイス一覧を取得することができていましたが、今回v1.1になったということで取得の仕方が変更されています。上記にもありますが、署名が必要になります。

以下に`token`,`secret`を使用して署名作成、HTTPリクエストヘッダーの作成までのコードを書いていきましょう。
```python
import os
import time
import hashlib
import hmac
import base64

def make_sign(token: str,secret: str):
    nonce = ''
    t = int(round(time.time() * 1000))
    string_to_sign = bytes(f'{token}{t}{nonce}', 'utf-8')
    secret = bytes(secret, 'utf-8')
    sign = base64.b64encode(hmac.new(secret, msg=string_to_sign, digestmod=hashlib.sha256).digest())
    return sign, str(t), nonce

def make_request_header(token: str,secret: str) -> dict:
    sign,t,nonce = make_sign(token, secret)
    headers={
            "Authorization": token,
            "sign": sign,
            "t": str(t),
            "nonce": nonce
        }
    return headers
```

署名作成は`make_sign`関数、HTTPリクエストヘッダー作成は`make_request_header`関数になります。
おそらくリクエストの度に署名を生成する必要があるため、関数化しておいた方がいいと思います。
`make_request_header`関数に引数として`token`と`secret`を入れてあげると返り値として`headers`が返ってきます。リクエストを送る際にはこの`headers`を使用するだけです。そのまま流用しても問題ないと思います。

次にこの`headers`を利用して自分のSwitchbotデバイスの一覧を取得してみましょう。

```python
import requests
import json

base_url = 'https://api.switch-bot.com'

def get_device_list(deviceListJson='deviceList.json'):
    # tokenとsecretを貼り付ける
    token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

    devices_url = base_url + "/v1.1/devices"

    headers = make_request_header(token, secret)

    try:
        # APIでデバイスの取得を試みる
        res = requests.get(devices_url, headers=headers)
        res.raise_for_status()

        print(res.text)
        deviceList = json.loads(res.text)
        # 取得データをjsonファイルに書き込み
        with open(deviceListJson, mode='wt', encoding='utf-8') as f:
            json.dump(deviceList, f, ensure_ascii=False, indent=2)

    except requests.exceptions.RequestException as e:
        print('response error:',e)

if __name__ == "__main__":
    get_device_list()
```
`GET`リクエストからデバイス一覧を取得する関数になります。
`token`と`secret`にはアプリから取得した値をそのまま貼り付けましょう。キーをどう管理するかは、各々に任せます。
リクエストが成功していればデバイス一覧が出力されます。失敗している場合、エラーメッセージが表示されると思います。
またjsonファイルにdumpされます。このjsonファイルは関数の引数で指定してあげてください。今回のコードでは`deviceList.json`がデフォルトになっています。
v1.1のAPIを試したところで次にSwitchBotロックを制御してみます。

## SwitchBotロックを制御する
それでは本題となるSwitchBotロックの制御になります。ここまで来れたらここからは難しくないと思います。
APIを使用するときは、公式のAPIドキュメントを見ましょう。APIに対してどのようなリクエストが必要か、どのようなレスポンスが返ってくるかがぎっしり書かれています。
[公式ドキュメント](https://github.com/OpenWonderLabs/SwitchBotAPI)は英語で書かれていますが、海外産のAPIを使用する機会は多いと思うので、頑張って読み解きましょう。Smart Lockを制御するときは`command`として`lock`,`unlock`を使用するそうです。

### ロックの状態を取得する
```python
def get_lock_status(deviceId: str):
    devices_url = base_url + "/v1.1/devices/" + deviceId + "/status"
    try:
        # ロックの状態を取得
        res = requests.get(devices_url, headers=headers)
        res.raise_for_status()
        return res.json()

    except requests.exceptions.RequestException as e:
        print('response error:',e)
```
`get_lock_status`関数の引数にはSmart Lockの`deviceId`を指定してあげてください。成功した場合、以下のレスポンスが表示されると思います。
```
{'statusCode': 100, 'body': {'deviceId': 'xxxxxxxxxx', 'deviceType': 'Smart Lock', 'hubDeviceId': 'xxxxxxxxxx', 'lockState': 'unlocked', 'doorState': 'opened', 'calibrate': True}, 'message': 'success'}
```
この`lockState`がロック/アンロックの状態になります。

### ロック/アンロックする
ロック/アンロック用のメソッドを用意しました。
引数には先ほどと同様、`deviceId`を指定してあげてください。
`post`リクエストをする場合は`requests`の`post`メソッドを使用します。
ロック/アンロックする場合は、パラメータとして以下の用に指定してあげる必要があります。
ロックする場合は`command`を`lock`に、アンロックする場合は`command`を`unlock`にしてあげましょう。
```python
data={
        "commandType": "command",
        "command": "lock",
        "parameter": "default",
    }
```

```python
def lock(deviceId: str):
    devices_url = base_url + "/v1.1/devices/" + deviceId + "/commands"
    data={
            "commandType": "command",
            "command": "lock",
            "parameter": "default",
        }
    try:
        # ロック
        res = requests.post(devices_url, headers=headers, json=data)
        res.raise_for_status()
        print(res.text)

    except requests.exceptions.RequestException as e:
        print('response error:',e)

def unlock(deviceId: str):
    devices_url = base_url + "/v1.1/devices/" + deviceId + "/commands"
    data={
            "commandType": "command",
            "command": "unlock",
            "parameter": "default",
        }
    try:
        # アンロック
        res = requests.post(devices_url, headers=headers, json=data)
        res.raise_for_status()
        print(res.text)
        
    except requests.exceptions.RequestException as e:
        print('response error:',e)
```
成功すれば以下のようなレスポンスが返ってくると思います。
```
{"statusCode":100,"body":{},"message":"success"}
```

# 最後に 
今回APIがアップデートされたということで早速今までAPIで制御できなかったSwitchBotロックの制御を行ってみました。セキュリティが向上したということで今後も色々と追加されていきそうですね。今後もSwitchbotに関するAPIで面白いことができそうだったら、記事をあげていこうと思います。
Switchbotに関するコードを[GitHub](https://github.com/masaki12-s/switchbot_python)に公開しています。今回紹介した部分は`src/utils.py`と`src/lock.py`になりますので、よければ参考にしてください。