🥚

Python(Flask)からSwitchBotAPIを叩いてみる

2024/02/25に公開

1. 環境想定

  • macOS Sonoma 14.3.1
  • Python 3.12.1
  • pipenv version 2023.12.0
  • SwitchBotAPI 1.1

2. 事前準備

Python

  • 下記のコマンドを実行する。
pip install --upgrade pip
pip install pipenv
pipenv install flask flask-restful requests python-dotenv

SwitchBot

  • SwitchBotアプリをインストールする
  • アプリを起動してSwitchBotアカウントでログインする
  • 「プロフィール > 設定」から、「アプリバージョン」を数回タップする
    • 「開発者向けオプション」が表示されればOK
  • 「開発者向けオプション」を開き、「トークンを取得」をタップする
    • トークン値とクライアントシークレット値が表示される

3. Flask準備

  • ディレクトリ想定

    .
    ├── Pipfile
    ├── Pipfile.lock
    ├── api.py
    ├── config.py
    ├── main.py
    └── templates
        └── index.html
    
  • FlaskでAPI結果を取得できるように準備する

    main.py
    from flask import Flask, render_template
    from api import api_bp
    
    app = Flask(__name__)
    app.register_blueprint(api_bp)
    
    @app.route("/")
    def index():
        return render_template("index.html")
    
    if __name__ == "__main__":
        app.run(debug=True)
    
    config.py
    from dotenv import load_dotenv
    
    import os
    
    load_dotenv()
    
    # .envファイルの値を取得
    TOKEN = os.getenv("TOKEN")
    CLIENT_SECRET = os.getenv("CLIENT_SECRET")
    
    .env
    TOKEN=<TOKEN>
    CLIENT_SECRET=<CLIENT_SECRET>
    
    api.py
    from flask import Blueprint
    from flask_restful import Api, Resource
    
    import requests
    
    import json
    import time
    import hashlib
    import hmac
    import base64
    import uuid
    
    import config
    
    # config.pyから環境変数値を取得
    token = config.TOKEN
    client_secret = config.CLIENT_SECRET
    
        # Flask Appの分割モジュール
    api_bp = Blueprint("api", __name__, url_prefix="/api")
    
    # SwhichBotAPIの共通設定
    secret = bytes(client_secret, "utf-8")
    nonce = uuid.uuid4()
    charset = "utf8"
    domain = "https://api.switch-bot.com/v1.1"
    
    class Devices(Resource):
        # Get device status
        def get(self):
            t = int(round(time.time() * 1000))
            string_to_sign = f"{token}{t}{nonce}"
            string_to_sign = bytes(string_to_sign, "utf-8")
            sign = base64.b64encode(hmac.new(secret, msg=string_to_sign, digestmod=hashlib.sha256).digest())
    
            headers = {
                "Authorization": token,
                "charset": charset,
                "t": str(t),
                "sign": str(sign, "utf-8"),
                "nonce": str(nonce)
            }
            res = requests.get(f"{domain}/devices", headers=headers)
    
            return res.json()
    
    # Send device control commands
    class Commands(Resource):
        def post(self):
            t = int(round(time.time() * 1000))
            string_to_sign = f"{token}{t}{nonce}"
            string_to_sign = bytes(string_to_sign, "utf-8")
            sign = base64.b64encode(hmac.new(secret, msg=string_to_sign, digestmod=hashlib.sha256).digest())
    
            headers = {
                "Content-Type": "application/json",
                "Authorization": token,
                "charset": charset,
                "t": str(t),
                "sign": str(sign, "utf-8"),
                "nonce": str(nonce)
            }
            # Color BulbをturnOnにする例
            body = {
                "command": "turnOn",
                "commandType": "command"
            }
            
            res = requests.post(f"{domain}/devices/<deviceID>/commands", headers=headers, json=body)
            return res.json()
    
    api = Api(api_bp)
    api.add_resource(Devices, "/devices")
    api.add_resource(Commands, "/commands")
    
    index.html
    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>TESTページ</title>
    </head>
    <body>
        <ul>
            <li><a href="/api/devices">GET /devices</a></li>
            <form method="POST" name="form1" action="/api/commands"></form>
            <li><a href="javascript:form1.submit()">POST /commands</a></li>
        </ul>
    </body>
    </html>
    

4. 結果

GET /devices

{
    "statusCode": 100,
    "body": {
        "deviceList": [
            {
                "deviceId": "XXXXXXXXXXXX",
                "deviceName": "<device name>",
                "deviceType": "Color Bulb",
                "enableCloudService": true,
                "hubDeviceId": "YYYYYYYYYYYY"
            },
            …(略)…
        ],
        "infraredRemoteList": [
            {
                …(略)…
            }
        ]
    },
    "message": "success"
}

POST /commands

{
    "statusCode": 100,
    "body": {
        "items": [
            {
                "deviceID": "XXXXXXXXXXXX",
                "code": 100,
                "status": {
                    "isDelay": false,
                    "color": "0:0:0",
                    "dayLightMin": <Min>,
                    "dayLightWh": <Wh>,
                    "isPreset": false,
                    "weekLightMin": <weekLightMin>,
                    "colorTemperature": <colorTemperature>,
                    "presetIdx": -1,
                    "brightness": 100,
                    "dynamicSpeed": 100,
                    "weekLightWh": <weekLightWh>,
                    "power": "on",
                    "presetMode": "white",
                    "signal": 0,
                    "monthLightWh": <monthLightWh>,
                    "monthLightMin": <monthLightMin>
                },
                "message": "success"
            }
        ]
    },
    "message": "success"
}

参考

Discussion