SwitchBotアプリが使いにくいので自作してみる~API利用~

IoTスマートホームデバイスと言えばSwitchBot
自分も、SwitchBotのカーテンの開閉を自動化するデバイスなどいくつか購入し愛用しています。

基本的には素晴らしい製品群なのですが、それらのコントロールを行う、SwitchBotアプリがまぁ使いにくい…(皆様はいかがでしょうか)

特に、複数個のスマート電球の操作が非常に扱いにくいと感じています。
電球の状態を取得するまでの時間が長すぎたり、電源入れているのに接続できなかったり、グループ化機能もイマイチかゆいところに手が届かないというか。

何とかできないものかと思っていたところ、SwitchBotが開発者向けにAPIを公開していることを知り、じゃ、いっその事操作アプリを自作してみるかと思い至りました。

RaspberryPiなどと組み合わせれば、自作の人感センサー+SwitchBot製品で自動ライトON/OFFなどスマートホーム化の幅が広がりそうです。
センサー系などは自作してしまった方が安上がりかもしれませんね

アプリを作り上げてから記事にすると時間がかかり過ぎてしまうということと、
完成してからだと膨大な情報量に圧倒され、それらを記事にまとめる気が起きなくなるということから、
開発過程の中で学んだことなどをこまめに記事にしていこうと思います

走り書きな所もありますがご了承ください

なお、以下に紹介するソースコードはこちらにpushしています

アクセストークンと秘密鍵の取得

SwitchBotが公開しているAPIと、利用開始までの手順は以下で紹介されています。

簡単にまとめると

  1. アプリを開く

  2. プロフィール → 設定 → 基本データを開く

  3. アプリバージョンが記載されている箇所を10回タップする

これにて、隠されていた開発者向けオプションが見えるようになります。
なんかこういうのってゲームの隠しコマンド思い出してワクワクしますね

開発者向けオプションから、アクセストークンと、秘密鍵を取得できます。
これらを用いてプログラムを組むことで様々な機能にアクセスできます。

認証

SwitchBotAPIを利用するには、特定のフォーマットに従ったAPIヘッダーを添える必要があります。

公式githubでそのサンプルが公開されているので、それを参考に組んでみました

import time
import hashlib
import hmac
import base64
import uuid
import os


def create_header():
    token = os.getenv("SWITCHBOT_TOKEN")
    time_stamp = int(round(time.time() * 1000))
    nonce = uuid.uuid4()
    message = bytes(f"{token}{time_stamp}{nonce}", "utf-8")

    secret_key = os.getenv("SWITCHBOT_SECRET_KEY")
    secret_key = bytes(secret_key, "utf-8")
    sign = base64.b64encode(
        hmac.new(secret_key, msg=message, digestmod=hashlib.sha256).digest()
    )

    return {
        "Authorization": token,
        "Content-Type": "application/json",
        "charset": "utf8",
        "t": str(time_stamp),
        "sign": str(sign, "utf-8"),
        "nonce": str(nonce),
    }

簡単に行っていることをまとめます。
技術的詳細に興味のない方は読み飛ばしてください。

アクセストークンと秘密鍵を環境変数から読み出し

アプリより取得できるアクセストークンと秘密鍵ですが、これらは絶対に他者には教えないでください。
何者かに家電を好き勝手操作される恐れがあります。

今回、githubを利用してソースコードを管理し、また知識共有のためこれを公開しています。

アクセストークンと秘密鍵を、ソースコードにべた書きしてしまうと、githubを介して世界中から私の家電が操作される恐れがあるので、
アクセストークンと、秘密鍵はあらかじめ環境変数として登録しておいて、
ソースコードでは環境変数からそれらを取得するようにしました。

Nonce認証

SwitchBotAPIとの通信ではNonce認証というものを行います。

SwitchBotと私と攻撃者の3人がいるとしましょう。
SwitchBotと私はそれぞれ秘密鍵を知っていて、攻撃者は秘密鍵を知らないとします。

SwitchBotは私に秘密鍵を要求します。
そこで私は秘密鍵をハッシュ化してSwitchBotに送ります
SwitchBotはハッシュ値を貰い、SwitchBotが知っている私用の秘密鍵をハッシュ化して一致するか確認します。
ここで一致すれば、正しい秘密鍵が送られたものとして、私にアクセスを許可します。

この通信を攻撃者が傍受したとしましょう。
攻撃者は秘密鍵自体は知りませんが、秘密鍵のハッシュ値を知ることが出来ます。
そこで、秘密鍵のハッシュ値をSwitchBotに送ると、SwitchBotは適切な秘密鍵を送ったとして、その攻撃者を私とみなしアクセスを許可してしまいます。

この様な事態(リプレイ攻撃)を避けるために、Nonce認証という方法が用いられます。

Nonce認証とは、一回限りのランダムな値(Nonce)を用いた認証です。
ハッシュ関数の特性を利用して攻撃を防ぎます。
※実際のSwitchBotとの通信ではタイムスタンプも使っていますが、以下の説明では省きます

SwitchBotは私に秘密鍵を要求します。
私はNonceを生成し、秘密鍵とNonceを連結した文字列をハッシュ化して、SwitchBotにハッシュ値とNonce値を送ります。
SwitchBotはハッシュ値を貰い、SwitchBotが知っている私用の秘密鍵と、貰ったNonce値をハッシュ化して一致するか確認します。
ここで一致すれば、正しい秘密鍵が送られたものとして、私にアクセスを許可します。
ここでNonce値は捨てられ、次回認証時には別のNonce値を生成することとなります。
つまり、私はSwitchBotに毎回異なるハッシュ値を送ることになります。

攻撃者がこの通信を傍受し、SwitchBotに同じハッシュ値とNonce値を送っても、すでに1度認証が行われたNonce値であることからアクセスを許可しません。
攻撃者は秘密鍵を知らないので、適当なハッシュ値を送っても認証は通りません。

コード中に用いられているuuidとは、偶然的に一致する確率が非常に低いランダムな値を生成するモジュールです。
これをNonce値として利用しています。

Base64形式

Base64というのは、エンコード方式の一つです。
いかなるデータも、6bit(64通り)の印字可能な文字のみで表現する方式です。

バイナリデータを直接扱えないとか、7bitで表現しきれる英数字しか扱えないと言うようなサーバーを介した通信を行う際の都合を考えて考案されたものの様です。

詳細は以下が参考になります

送ろうとしているデータを6bitごとに区切り、対応表から6bitにつき英数字を1文字割り当てます。
そうして、最終的に得られるのは、64個の英数字(一部記号もある)で表現される文字列です。
これを送り、受信側は逆の手順を踏んで元のデータを得ます。

ハッシュ化された値の送受信する際の中間表現的なものとして利用されているのだと思います。

保有デバイス一覧の取得

先ほどの認証用ヘッダーを用いて、API仕様書から適切なGETリクエストを送れば保有デバイス一覧を取得できます。
以下はそのサンプルコードです

import json
import requests
from header_creator import create_header

api_header = create_header()
response = requests.get("https://api.switch-bot.com/v1.1/devices", headers=api_header)
response_data = response.json()

if not response_data.get("statusCode") == 100:
    print(f"デバイスリストの取得に失敗 statusCode:{response_data.get("statusCode")}")
    exit()

device_list = response_data.get("body", {}).get("deviceList", [])
output_file = "devices.json"
with open(output_file, "w") as f:
    json.dump(device_list, f, indent=4)

APIリクエストは1日1000回までという上限が設けられており、保有デバイスは大して変動が無いので、一度取得したらファイル保存するという方法を取ることにしました。

ここから、デバイスごとのIDなどを取得できます。
このIDを用いて特定デバイスの操作をAPI越しに行えます

電球の調色操作

デバイスIDを取得できたので、それを用いて操作してみます。
今回は、電球の操作です。

SwitchBotの電球(Color Bulb)に対して行える操作は以下の通りです。

ON、OFF、トグル(ONならOFF, OFFならON)、明るさ調整、色調整、色温度調整。全部できます。

以下は、色調整のサンプルコードです。(すでにスイッチONを想定していますが、API仕様書に従って、同様の方法でON/OFFも可能です)

import requests
import pprint
import json
from header_creator import create_header

device_file = "devices.json"
with open(device_file, "r", encoding="utf-8") as f:
    devices = json.load(f)

color_bulb = None
for device in devices:
    if device.get("deviceType") == "Color Bulb":
        color_bulb = device.get("deviceId")
        break

api_header = create_header()
params = {
    "commandType": "command",
    "command": "setColor",
    "parameter": "64:00:00",
}
json_params = json.dumps(params)
response = requests.post(
    f"https://api.switch-bot.com/v1.1/devices/{color_bulb}/commands",
    headers=api_header,
    data=json_params,
)

今回の記事はここまでです。
API通信の基本から簡単な操作まで扱いました。
次回からは構造も見直しつつ、アプリとしての体裁を作っていこうと思います。 


いいなと思ったら応援しよう!