はじめに
みなさん初めまして.今回は自分の研究室の環境で使用しているSwitchBot プラグMiniのON/OFF状態をみることができるexporterを開発したのでどんなものなのかの概要や使い方を説明していきたいと思います.
概要
SwitchBot プラグミニは,家庭の家電をカンタンにスマート化できるコンパクトなプラグ型デバイスです.できることとしてはコンセントと対象機器の間に設置し電源をONするのかOFFにするのかの切り替えができます.
私の所属する研究室の環境ではESXiがインストールされた10台の物理マシンがありそのうちの6台(Mint,Rose,Jasmine,Lotus,Violet,Plum)に取り付けられています.
6台は研究室の学生が自由にVMを作成して実験できるマシンです.使用している学生がいない時間にSwichBot プラグMiniが電源をOFFにします.
現在私たちの研究室で動いているアルゴリズムは.CDSLの山野倖平さんが作成したものになります.
詳しいアルゴリズムを知りたい方がいれば以下のURLから見てみてください!⇩
- 山野倖平, 平尾真斗, 串田高幸「VMの使用時間にもとづく作業時間の予測と物理マシンの停止による省電力化」
https://drive.google.com/file/d/1Yo_XdFur6XxWzpDo-Caxs0F6rVVXXNmI/view
今回はこのアルゴリズムが動いている前提で話を進めていきます.
環境
- Ubuntu 24.04.1 LTS
- Python 3.10.12
ライブラリ
- Flask==2.3.3
- requests==2.31.0
- prometheus_client
ファイル構成
プロジェクトのファイルは以下にあります.
https://github.com/cdsl-research/switchbot-exporter
switchbot-exporter
├── app.py →exporterの実行ファイル
├── Dockerfile
└── requirements.txt →Pythonライブラリインストール用
使い方
ファイル内は以下のようになっています.埋める箇所はswitchbot_targets
とheaders
のAPIトークンです.
from flask import Flask, Response
from prometheus_client import Gauge, generate_latest
import requests
app = Flask(__name__)
# デバイス情報(名前はユニークに)
switchbot_targets = {
"mint": "$ID",
"rose": "$ID",
"jasmine": "$ID",
"plum": "$ID",
"lotus": "$ID",
"violet": "$ID"
}
# Prometheus用のメトリクス定義
power_gauge = Gauge('switchbot_power_status', 'Power status of SwitchBot device (1=on, 0=off)', ['device_name'])
# SwitchBot APIトークン
headers = {
"Authorization": "$APIトークン"
}
def update_metrics():
for device_name, device_id in switchbot_targets.items():
url = f"https://api.switch-bot.com/v1.0/devices/{device_id}/status"
try:
response = requests.get(url, headers=headers)
body = response.json().get("body", {})
power = body.get("power", None)
if power == "on":
power_gauge.labels(device_name=device_name).set(1)
elif power == "off":
power_gauge.labels(device_name=device_name).set(0)
else:
power_gauge.labels(device_name=device_name).set(-1) # 異常系
except Exception as e:
print(f"{device_name}: Error fetching status: {e}")
power_gauge.labels(device_name=device_name).set(-1) # エラー
@app.route("/metrics")
def metrics():
update_metrics()
return Response(generate_latest(), mimetype="text/plain")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=9080)
1. cdで移動
monitoring@monitoring-master-ml:~$ cd switchbot-exporter/
monitoring@monitoring-master-ml:~/switchbot-exporter$
2. switchbot_targetsの確認
以下のようにcurlコマンドを使って出力を確認します.その際に$Token
にSwitchbotのAPI用のTokenが必要です.
monitoring@monitoring-master-ml:~/switchbot-exporter$ curl -X GET "https://api.switch-bot.com/v1.0/devices" \
-H "Authorization: $Token"
monitoring@monitoring-master-ml:~/
3. switchbot_targetsをapp.pyに入れる
先ほど調べたIDをapp.pyの以下の箇所に入れます.
switchbot_targets = {
"mint": "$ID",
"rose": "$ID",
"jasmine": "$ID",
"plum": "$ID",
"lotus": "$ID",
"violet": "$ID"
}
4. Tokenの入力
app.py内のheadersの変数の箇所にcurlでも使用したAPIトークンを入れます.
headers = {
"Authorization": "$APIトークン"
}
5. 仮想環境の作成と有効化
以下のコマンドを実行
monitoring@monitoring-master-ml:~/switchbot-exporter$ python3 -m venv switchbot-exporter
monitoring@monitoring-master-ml:~/switchbot-exporter$ source switchbot-exporter/bin/activate
(switchbot-exporter) monitoring@monitoring-master-ml:~/switchbot-exporter$
6. 必要なライブラリのインストール
pip installでrequirements.txtを指定する.
(switchbot-exporter) monitoring@monitoring-master-ml:~/switchbot-exporter$ pip install -r requirements.txt
Collecting Flask==2.3.3 (from -r requirements.txt (line 1))
Downloading flask-2.3.3-py3-none-any.whl.metadata (3.6 kB)
Collecting requests==2.31.0 (from -r requirements.txt (line 2))
Downloading requests-2.31.0-py3-none-any.whl.metadata (4.6 kB)
Collecting prometheus_client (from -r requirements.txt (line 3))
Downloading prometheus_client-0.22.1-py3-none-any.whl.metadata (1.9 kB)
Collecting Werkzeug>=2.3.7 (from Flask==2.3.3->-r requirements.txt (line 1))
Using cached werkzeug-3.1.3-py3-none-any.whl.metadata (3.7 kB)
Collecting Jinja2>=3.1.2 (from Flask==2.3.3->-r requirements.txt (line 1))
Using cached jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
Collecting itsdangerous>=2.1.2 (from Flask==2.3.3->-r requirements.txt (line 1))
Using cached itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB)
Collecting click>=8.1.3 (from Flask==2.3.3->-r requirements.txt (line 1))
Using cached click-8.2.1-py3-none-any.whl.metadata (2.5 kB)
Collecting blinker>=1.6.2 (from Flask==2.3.3->-r requirements.txt (line 1))
Using cached blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
Collecting charset-normalizer<4,>=2 (from requests==2.31.0->-r requirements.txt (line 2))
Using cached charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (35 kB)
Collecting idna<4,>=2.5 (from requests==2.31.0->-r requirements.txt (line 2))
Using cached idna-3.10-py3-none-any.whl.metadata (10 kB)
Collecting urllib3<3,>=1.21.1 (from requests==2.31.0->-r requirements.txt (line 2))
Using cached urllib3-2.5.0-py3-none-any.whl.metadata (6.5 kB)
Collecting certifi>=2017.4.17 (from requests==2.31.0->-r requirements.txt (line 2))
Using cached certifi-2025.6.15-py3-none-any.whl.metadata (2.4 kB)
Collecting MarkupSafe>=2.0 (from Jinja2>=3.1.2->Flask==2.3.3->-r requirements.txt (line 1))
Using cached MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.0 kB)
Downloading flask-2.3.3-py3-none-any.whl (96 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 96.1/96.1 kB 9.1 MB/s eta 0:00:00
Downloading requests-2.31.0-py3-none-any.whl (62 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.6/62.6 kB 15.3 MB/s eta 0:00:00
Downloading prometheus_client-0.22.1-py3-none-any.whl (58 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 58.7/58.7 kB 16.7 MB/s eta 0:00:00
Using cached blinker-1.9.0-py3-none-any.whl (8.5 kB)
Using cached certifi-2025.6.15-py3-none-any.whl (157 kB)
Using cached charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (148 kB)
Using cached click-8.2.1-py3-none-any.whl (102 kB)
Using cached idna-3.10-py3-none-any.whl (70 kB)
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Using cached jinja2-3.1.6-py3-none-any.whl (134 kB)
Using cached urllib3-2.5.0-py3-none-any.whl (129 kB)
Using cached werkzeug-3.1.3-py3-none-any.whl (224 kB)
Using cached MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (23 kB)
Installing collected packages: urllib3, prometheus_client, MarkupSafe, itsdangerous, idna, click, charset-normalizer, certifi, blinker, Werkzeug, requests, Jinja2, Flask
Successfully installed Flask-2.3.3 Jinja2-3.1.6 MarkupSafe-3.0.2 Werkzeug-3.1.3 blinker-1.9.0 certifi-2025.6.15 charset-normalizer-3.4.2 click-8.2.1 idna-3.10 itsdangerous-2.2.0 prometheus_client-0.22.1 requests-2.31.0 urllib3-2.5.0
(switchbot-exporter) monitoring@monitoring-master-ml:~/switchbot-exporter$
7. app.pyの実行
以下のコマンドを入力しapp.pyを実行
(switchbot-exporter) monitoring@monitoring-master-ml:~/switchbot-exporter$ python3 app.py
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:9080
* Running on http://192.168.100.76:9080
Press CTRL+C to quit
8. 動作確認
以下のようにswitchbot_power_status
のメトリクスが見れればOK!
(switchbot-exporter) monitoring@monitoring-master-ml:~/switchbot-exporter$ curl http://monitoring-master-ml:9080/metrics
# HELP python_gc_objects_collected_total Objects collected during gc
# TYPE python_gc_objects_collected_total counter
python_gc_objects_collected_total{generation="0"} 275.0
python_gc_objects_collected_total{generation="1"} 257.0
python_gc_objects_collected_total{generation="2"} 0.0
# HELP python_gc_objects_uncollectable_total Uncollectable objects found during GC
# TYPE python_gc_objects_uncollectable_total counter
python_gc_objects_uncollectable_total{generation="0"} 0.0
python_gc_objects_uncollectable_total{generation="1"} 0.0
python_gc_objects_uncollectable_total{generation="2"} 0.0
# HELP python_gc_collections_total Number of times this generation was collected
# TYPE python_gc_collections_total counter
python_gc_collections_total{generation="0"} 86.0
python_gc_collections_total{generation="1"} 7.0
python_gc_collections_total{generation="2"} 0.0
# HELP python_info Python platform information
# TYPE python_info gauge
python_info{implementation="CPython",major="3",minor="8",patchlevel="20",version="3.8.20"} 1.0
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 1.12848896e+08
# HELP process_resident_memory_bytes Resident memory size in bytes.
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 3.6237312e+07
# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1.75178868764e+09
# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE process_cpu_seconds_total counter
process_cpu_seconds_total 0.31
# HELP process_open_fds Number of open file descriptors.
# TYPE process_open_fds gauge
process_open_fds 6.0
# HELP process_max_fds Maximum number of open file descriptors.
# TYPE process_max_fds gauge
process_max_fds 1.048576e+06
# HELP switchbot_power_status Power status of SwitchBot device (1=on, 0=off)
# TYPE switchbot_power_status gauge
switchbot_power_status{device_name="mint"} 1.0
switchbot_power_status{device_name="rose"} 0.0
switchbot_power_status{device_name="jasmine"} 1.0
switchbot_power_status{device_name="plum"} 1.0
switchbot_power_status{device_name="lotus"} 1.0
switchbot_power_status{device_name="violet"} 1.0
(switchbot-exporter) monitoring@monitoring-master-ml:~/switchbot-exporter$
switchbot_power_statusのメトリクスの値が1.0の時は対象機器に電源が供給されていることを示します.0.0は供給されていないことを示します.
switchbot_power_status{device_name="mint"} 1.0
switchbot_power_status{device_name="rose"} 0.0
switchbot_power_status{device_name="jasmine"} 1.0
switchbot_power_status{device_name="plum"} 1.0
switchbot_power_status{device_name="lotus"} 1.0
switchbot_power_status{device_name="violet"} 1.0
終わりに
今回はSwitchBot プラグMiniの電源のON/OFF状態を見れるexporterを開発しました.まだまだ改善するところが多いので今後も修正を加えて使いやすくしていきます!