TAPO PDU PROJECT VOL.1
Tapo P110M × python-kasaで自作PDUを作る【環境構築・Unsupported device(TPAP)エラー解決編】
こんにちは。ニンジンです🥕
電力モニタリング機能付きのスマートプラグ「TP-Link Tapo P110M」を2個購入しました。Tapo純正アプリでもON/OFFや電力量の確認はできるのですが、複数台をまとめて操作したり、電力データを自分の好きな形で記録・可視化したりするには、公式アプリだけでは物足りません。
そこで、PythonでTapo P110Mを制御する「自作PDU(Power Distribution Unit)」システムを作ることにしました。この記事はその第1回で、環境構築から、実際にハマった「Unsupported device」エラーの原因究明、基本的なON/OFF制御の実装までを紹介します。
この記事で分かること
- Tapo P110MをPythonから制御するための環境構築(
python-kasa) - デバイスのIPアドレスを固定化しなくても運用できる設計
kasa discoverで「Unsupported device」encrypt_type='TPAP'エラーが出たときの原因と対処法- MACアドレスを使って、IPが変わっても壊れないデバイス管理コードの書き方
- 個別ON/OFF・一括ON/OFFを実装するPythonコード
こんな人におすすめ
- Tapo P110M(またはP110/P105などの同シリーズ)をPythonで自動化したい人
kasa discoverで「Unsupported device」と表示されて困っている人- スマートプラグを使った自作PDU・電力モニタリングシステムに興味がある人
Tapo P110Mをpython-kasaで制御する環境を作る
TapoデバイスをPythonから操作するライブラリには python-kasa と PyP110 の2つの選択肢があります。今回は python-kasa を選びました。もともとTP-Link Kasaシリーズ向けに作られたライブラリですが、Tapoシリーズにも対応が進んでおり、discover(ネットワーク上のデバイス自動検出)やコマンドラインツールが標準で付いてくる点が決め手でした。
pip install python-kasa動作確認したPython環境は次の通りです。
- Python 3.13.12 / pip 25.3
- Windows環境(Windowsストア版Python)
インストールが終わると、kasaというコマンドラインツールも付属しますが、この記事では扱いやすさを優先してPythonスクリプトから使う方法だけを紹介します。
デバイスをMACアドレスで管理する方針にした理由
Tapoデバイスをプログラムから操作するには、まずデバイスを見分ける手がかりが必要です。ふつうはIPアドレスを使い、ルーターのDHCP予約機能で「このMACアドレスには常にこのIPを割り当てる」と固定してしまうのが定石です。
ところが、私が使っているネットワークはモバイルルーター(ZEUS WiFi HWR0001A)でした。これはブラウザからアクセスできる管理画面を持たないタイプの端末で、ポート80/443自体は開いているもののHTTPリクエストへの応答がなく、DHCP予約はおろかルーターの詳細設定そのものに手が届きませんでした。
最初は「困ったな、IP固定化できないと運用が不安定になる」と思っていたのですが、よく考えるとpython-kasaにはdiscoverというネットワークブロードキャストでTapoデバイスを自動検出する機能があります。IPアドレスの代わりにMACアドレス(DHCPで変わらない固定の識別子)をキーにしてデバイスを管理すれば、スクリプトを実行するたびにその時点のIPアドレスを自動で調べ直せるので、そもそもIPアドレスを固定する必要がなくなります。
振り返ってみると、これは結果的に良い設計でした。IP固定化ができない環境でも動きますし、読者の方が同じコードを使う場合も「ルーターの管理画面でMACアドレスを調べてIPを固定する」という一手間が丸ごと不要になります。たまたま環境の制約で選んだ方法が、そのまま汎用性の高い設計につながった格好です。
discover_devices.pyを作ってTapo P110Mを検出する
ブロードキャストアドレスを確認する
discoverはネットワーク全体に向けてブロードキャストを送り、応答があったデバイスを見つける仕組みです。自分のブロードキャストアドレスは、ipconfigコマンドで自分のPCのIPv4アドレスを確認し(例: 192.168.1.15)、最後の数字を255に置き換える(192.168.1.255)ことで求められます。この記事のコード例に出てくる192.168.43.255は筆者の環境のものなので、実行する際はご自身の環境の値に置き換えてください。
認証情報を.envに用意する
discover_devices.pyの中では、TP-Linkアカウントのメールアドレスとパスワードを使ってTapoデバイスに認証します(ローカルAPI制御にもアカウント情報が必要になる仕様のためです)。パスワードをコードに直接書きたくないので、先に.envファイルに分けて保存しておきます。
TAPO_EMAIL=your_email@example.com
TAPO_PASSWORD=your_password.envは.gitignoreに追加し、コードとは別で管理する運用にしています。この後登場するPythonファイルは、すべて同じフォルダに置く必要があります(お互いをimportしたり、同じ.envを読み込んだりするためです)。VSCodeのエクスプローラーで見ると、こんな配置になっています。
discover_devices.pyを作って実行する
以上を踏まえて作ったのがdiscover_devices.pyです。こちらも同じフォルダに置きます。
discover自体は動作しましたが、環境によって挙動が変わる可能性があるため、うまくいかない場合はネットワークプロファイルも確認してみてください。前提として、Tapoアプリ側では2台ともWi-Fi接続・アカウント紐付けまでのセットアップは完了させておいてください。
import asyncio
import os
from pathlib import Path
from dotenv import load_dotenv
from kasa import Discover
load_dotenv(Path(__file__).parent / ".env")
async def main():
email = os.environ["TAPO_EMAIL"]
password = os.environ["TAPO_PASSWORD"]
devices = await Discover.discover(
target="192.168.43.255", # 自分のネットワークのブロードキャストアドレス
username=email,
password=password,
discovery_timeout=8,
)
if not devices:
print("デバイスが見つかりませんでした。")
return
try:
for ip, dev in devices.items():
await dev.update()
mac = getattr(dev, "mac", "unknown")
print(f"IP: {ip} / alias: {dev.alias} / model: {dev.model} / MAC: {mac}")
finally:
for dev in devices.values():
await dev.disconnect()
if __name__ == "__main__":
asyncio.run(main())実行してみると、次のようになりました。
「デバイスが見つかりません」の原因はencrypt_type=’TPAP’
ネットワーク上にデバイス自体は存在するのに、見つかりません(デバッグログを見ると、encrypt_type='TPAP'という暗号化方式が原因で「Unsupported device」の状態になっていました)。実はこれ、ライブラリの不具合ではなくTapoアプリ側の設定が原因でした。P110Mのファームウェア1.4.1以降では、Tapoアプリ側で明示的に許可しない限り、サードパーティ製ツールからのローカルAPI制御をブロックする仕様に変更されています。これを解除する手順は次の通りです。
- Tapoアプリを起動し、右下の「私」タブを開く
- 「サービス」欄の「音声アシスタント」を開く(バージョンによっては「Tapoラボ」という名称の場合もあります)
- 一覧の下にある「サードパーティ連携」を開き、トグルをONにする
- P110M 1台ごとに個別にこの操作が必要です
▲ 「私」タブの設定画面。「音声アシスタント」の少し上に「Tapo と Kasa を連携」が並んでいる
python-kasaというライブラリ名との類似も単なる偶然です)。「音声アシスタント」の方を選んでください。トグルをONにすると、「有効にすると、スマートデバイスがHome Assistantなどサードパーティのエコシステムとシームレスに統合され、スマートホームの接続性が向上します。免責事項: Home Assistantなどのオープンソースプラットフォームとの連携のためにこの機能を有効にすると、特定のプライバシーリスクが生じる可能性があります。」という説明文が表示されます。個人利用のPoCとしては許容範囲だと判断しましたが、念のため頭に入れておいてください。
2台とも設定した後、改めてdiscover_devices.pyを実行すると、無事にデバイス情報を取得できるようになりました。
ちょうどこの出力にMACアドレスも表示されているので(MAC: 18:69:45:CF:E5:6Aのような部分)、次のセクションで使うのでメモしておいてください。Tapoアプリのデバイス設定画面(デバイス情報)からも確認できますが、どのみちdiscover_devices.pyは使うことになるので、この出力からメモしておくのが手っ取り早いです。
デバイス管理モジュール(tapo_devices.py)を作る
discover_devices.pyで動作確認はできましたが、このあと個別ON/OFF・一括ON/OFFなど複数のスクリプトを作っていきます。MACアドレスとデバイスの対応関係、discover処理を毎回書き直さなくていいように、tapo_devices.pyという共通モジュールに切り出しておきます。これも同じフォルダに置きます。
import asyncio
import os
from pathlib import Path
from dotenv import load_dotenv
from kasa import Discover
load_dotenv(Path(__file__).parent / ".env")
BROADCAST_TARGET = "192.168.43.255"
DISCOVERY_TIMEOUT = 8
DEVICES = {
"plug1": {"mac": "18:69:45:CF:E5:6A", "label": "Tapo P110M 1"},
"plug2": {"mac": "18:69:45:CF:D8:62", "label": "Tapo P110M 2"},
}
def _credentials():
return os.environ["TAPO_EMAIL"], os.environ["TAPO_PASSWORD"]
async def discover_known_devices(names=None):
"""指定した名前(省略時は全台)のデバイスをMACアドレスで照合して返す。"""
email, password = _credentials()
target_names = names or list(DEVICES.keys())
target_macs = {DEVICES[n]["mac"].upper(): n for n in target_names}
found = await Discover.discover(
target=BROADCAST_TARGET,
username=email,
password=password,
discovery_timeout=DISCOVERY_TIMEOUT,
)
resolved = {}
for dev in found.values():
mac = (getattr(dev, "mac", "") or "").upper()
if mac in target_macs:
resolved[target_macs[mac]] = dev
else:
await dev.disconnect()
missing = set(target_names) - set(resolved.keys())
for name in missing:
print(f"警告: {DEVICES[name]['label']} ({name}) が見つかりませんでした。")
return resolved
async def disconnect_all(devices):
for dev in devices.values():
await dev.disconnect()discoverでネットワーク全体から見つかったデバイスのうち、DEVICES辞書に登録しておいたMACアドレスと一致するものだけを、"plug1"のような扱いやすい名前に変換して返す、というシンプルな作りです。見つからなかったデバイスは警告を出すようにしています。認証情報は、先ほど作った.envから読み込んでいます。
tapo_devices.pyは他のスクリプトからimportされて使われる部品なので、これ単体を実行しても何も起こりません。VSCode上ではこのように見えます。
discoverが1回で成功しないことがある
実際に運用してみると、discoverがまれに1回目は失敗し、2回目のリトライで成功する、という現象がありました。原因ははっきり特定できていませんが(モバイルルーター特有のブロードキャスト遅延を疑っています)、実用上は単純なリトライで十分回避できたので、tapo_devices.pyに次のようなリトライ処理を追加しています。
async def discover_with_retry(names=None, retries=3, delay_sec=3):
"""discoverが一部/全部失敗することがあるため、対象デバイスがそろうまでリトライする。"""
target_names = names or list(DEVICES.keys())
for attempt in range(1, retries + 1):
devices = await discover_known_devices(target_names)
if len(devices) == len(target_names):
return devices
print(f"デバイスの検出に一部失敗しました(試行{attempt}/{retries})。再試行します...")
await disconnect_all(devices)
await asyncio.sleep(delay_sec)
raise RuntimeError("デバイスの検出に失敗しました。ネットワーク接続を確認してください。")個別ON/OFF・一括ON/OFFを実装する
土台ができたので、実際にプラグを操作するスクリプトを作ります。
個別ON/OFF(individual_control.py)
import asyncio
import sys
from tapo_devices import DEVICES, discover_with_retry, disconnect_all
async def main():
names = "|".join(DEVICES.keys())
if len(sys.argv) != 3 or sys.argv[1] not in DEVICES or sys.argv[2] not in ("on", "off"):
print(f"使い方: python individual_control.py [{names}] [on|off]")
return
name, action = sys.argv[1], sys.argv[2]
devices = await discover_with_retry([name])
try:
dev = devices.get(name)
if dev is None:
return
await dev.update()
if action == "on":
await dev.turn_on()
else:
await dev.turn_off()
await dev.update()
print(f"{dev.alias} を {action} にしました。(現在の状態: {'ON' if dev.is_on else 'OFF'})")
finally:
await disconnect_all(devices)
if __name__ == "__main__":
asyncio.run(main())python individual_control.py plug1 onのように実行すると、plug1(Tapo P110M 1)だけをONにできます。
python ファイル名.pyを実行するだけなので、plug1やonのような引数を渡せません。このスクリプトは必ずターミナルからコマンドで実行してください。引数を付けずに実行すると使い方が表示され、正しく引数を付けると実際にデバイスが操作されます。
1点ハマったポイントとして、Discover.discover()が返すDeviceオブジェクトは、直後の時点ではalias(デバイス名)などの情報がまだ反映されていないことがあります。
# 一見動きそうだが、aliasがNoneのまま表示されてしまう
dev = devices["plug1"]
print(dev.alias) # Noneawait dev.update()を呼んでから参照する必要があります。地味にハマりやすいポイントなので、ON/OFF操作をした後も、状態表示のために再度update()を呼んでいます。
一括ON/OFF(bulk_control.py)
import asyncio
import sys
from tapo_devices import discover_with_retry, disconnect_all
async def main():
if len(sys.argv) != 2 or sys.argv[1] not in ("on", "off"):
print("使い方: python bulk_control.py [on|off]")
return
action = sys.argv[1]
devices = await discover_with_retry()
try:
for dev in devices.values():
await dev.update()
if action == "on":
await dev.turn_on()
else:
await dev.turn_off()
await dev.update()
print(f"{dev.alias} を {action} にしました。(現在の状態: {'ON' if dev.is_on else 'OFF'})")
finally:
await disconnect_all(devices)
if __name__ == "__main__":
asyncio.run(main())discover_with_retry()を引数なしで呼ぶと、DEVICESに登録した全台が対象になります。
python bulk_control.py onこれで2台のTapo P110Mを一括ONできます。
実機で個別・一括それぞれのON/OFFが正しく動作することを確認しました。
まとめ・次回予告
今回は、Tapo P110Mをpython-kasaで制御するための環境構築から、実際にハマった「Unsupported device(TPAP)」エラーの原因究明、MACアドレスベースのデバイス管理、個別/一括ON-OFFの実装までを紹介しました。
今回のポイント
- IPアドレスを固定化しなくても、
discoverをMACアドレスと組み合わせれば安定して運用できる - 「Unsupported device」
encrypt_type='TPAP'エラーは、Tapoアプリ側の「サードパーティ連携」設定が原因 Discover.discover()直後のDeviceオブジェクトはupdate()するまで情報が反映されない
次回は、この基盤の上に電力データの記録(SQLite)と、ブラウザから一括/個別ON-OFF・消費電力を確認できるWebダッシュボード(FastAPI)を構築していきます。
よくある質問
Qpython-kasaとPyP110、どちらを使うべきですか?
AどちらもTapoデバイスのローカル制御に対応していますが、今回は discover 機能やコマンドラインツールが標準で使える点を重視して python-kasa を選びました。すでにPyP110で構築済みの環境がある場合は、無理に乗り換える必要はありません。
Q「サードパーティ連携」を有効にするとセキュリティ的に問題ないですか?
A設定画面には「Home Assistantなどのオープンソースプラットフォームとの連携のためにこの機能を有効にすると、特定のプライバシーリスクが生じる可能性があります」という免責事項が表示されます。自宅の個人利用ネットワークであれば実用上のリスクは低いと考えていますが、法人利用や不特定多数がアクセスできるネットワークでは慎重に検討してください。
QIPアドレスを固定化した方が確実ではないですか?
AルーターがDHCP予約に対応していれば、固定化してもちろん問題ありません。今回はモバイルルーターの制約でできなかったのですが、結果的に「固定化しなくても動く」設計にしたことで、読者の方が別のネットワーク環境でそのまま使い回せるコードになりました。
Qdiscoverで自分のデバイスがどうしても見つかりません。
Aまず target にデフォルトの 255.255.255.255 ではなく、自分のネットワークのブロードキャストアドレス(例: 192.168.1.255)を明示的に指定してみてください。それでも見つからない場合は、Tapoアプリ側の「サードパーティ連携」がONになっているか確認してください。
Q.envファイルに書いた認証情報は安全ですか?
A.env ファイルは .gitignore に登録し、Gitリポジトリには含めない運用にしています。ファイル自体もローカルPC上にのみ保存し、外部には共有していません。
Series Navigation
このシリーズの記事一覧
Follow me on X
本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕
@ninjin_py_vba をフォローシリーズ記事一覧
電子工作・自動化ツールの製作記録をシリーズごとにまとめています。気になるテーマからお読みください。
-
FILE.01 — IoT
Raspberry Pi Pico W × GASラズパイPico W×GASで自作!「LINEで動く温湿度&スマートリモコン」完全ロードマップ
「外出中のペットの室温が心配…」そんな思いからスタートしたIoT自作プロジェクトの総集編。温湿度監視からエアコン遠隔操作まで、仕組みをゼロから作りたい方のための全5回の開発記録です。
シリーズを読む -
FILE.02 — CNC
Arduino × レーザー刻印CNCCNC自作シリーズ
高精度な加工を目指し、本格的な自作CNC製作に挑戦中。現在は基幹パーツであるオリエンタルモーターの納品を待つ「設計・準備編」を公開。ハードとソフトの両面から、理想のマシンを形にする過程をリアルタイムにお届けします。
シリーズを読む -
FILE.03 — XY軸制御
Raspberry Pi × ステッピングモーターXYテーブルシリーズ
Raspberry Piとステッパモーターを使い、ゼロから2軸制御に挑む記録。OS設定から回路設計、多軸制御のPythonコードまで、躓きやすいポイントを徹底図解。電子工作初心者が「動く感動」を味わうための実戦ガイドです。
シリーズを読む -
FILE.04 — 業務自動化
Python × Excel × CustomTkinter【Python開発記】NINJIN Mail制作記
PythonとExcelを連携させ、実務で即戦力となるメール送信ツールを開発。CustomTkinterによるUI構築や、ミスを防ぐテンプレート活用術など、現場の「痒い所に手が届く」自動化ノウハウを細部まで丁寧に解説します。
シリーズを読む -
FILE.05 — 業務自動化
Python × Excel × GUIアプリ化Invoice Maker(請求書・見積書自動作成)
手作業の請求書作成から卒業!PythonでExcelデータを読み込み一括PDF化する「Invoice_Maker」の作り方を全3回で解説。基本ロジックからGUIアプリ化まで、実務で役立つ自動化ノウハウが満載です。
シリーズを読む -
FILE.06 — 業務自動化
Access × VBA【Access VBA開発記】対応履歴管理データベース
問い合わせ・トラブル対応履歴がExcelでバラバラに散らばる悩みから、Access+VBAで検索性の高い管理データベースを自作。テンプレート配布から設計解説、Win32 API実装で味わった失敗談まで、生成AI時代の検証記録を全3回でお届けします。
シリーズを読む



コメント