PR

Tapo P110M × python-kasaで自作PDUを作る【環境構築・Unsupported device(TPAP)エラー解決編】

ハードウェア制御 / 電子工作

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(電力モニタリング機能付き)

Tapo P110M(電力モニタリング機能付き)

今回自作PDU化に使ったのはこちら。瞬間電力・積算電力量をアプリ上で確認できるほか、今回のようにPythonから直接データを取得することもできます。

Tapo P105(電力モニタリングなし・2個パック)

Tapo P105(電力モニタリングなし・2個パック)

電力モニタリングが不要で、単純なON/OFF制御(スケジュール運転や外出先からの遠隔操作)だけで十分という場合は、こちらの方が安価です。この記事のコードはpython-kasaが対応しているシリーズであれば基本的に同じ考え方で動きます。


スポンサーリンク

Tapo P110Mをpython-kasaで制御する環境を作る

TapoデバイスをPythonから操作するライブラリには python-kasaPyP110 の2つの選択肢があります。今回は python-kasa を選びました。もともとTP-Link Kasaシリーズ向けに作られたライブラリですが、Tapoシリーズにも対応が進んでおり、discover(ネットワーク上のデバイス自動検出)やコマンドラインツールが標準で付いてくる点が決め手でした。

PowerShellターミナル
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ファイルに分けて保存しておきます。

📄.envVSCode
TAPO_EMAIL=your_email@example.com
TAPO_PASSWORD=your_password

.env.gitignoreに追加し、コードとは別で管理する運用にしています。この後登場するPythonファイルは、すべて同じフォルダに置く必要があります(お互いをimportしたり、同じ.envを読み込んだりするためです)。VSCodeのエクスプローラーで見ると、こんな配置になっています。

VSCodeのエクスプローラー。tapo-pduフォルダの中に.envファイルがある

▲ .envも他のPythonファイルと同じフォルダに置く

discover_devices.pyを作って実行する

以上を踏まえて作ったのがdiscover_devices.pyです。こちらも同じフォルダに置きます。

VSCodeのエクスプローラー。tapo-pduフォルダの中にdiscover_devices.pyファイルがある

▲ discover_devices.pyも同じフォルダに置く

作業を始める前に1点確認してください。このスクリプトは同一ネットワーク(同じWi-Fi/ルーター配下)にあるTapo P110Mしか検出できません。PCとP110Mを同じネットワークに接続した状態で作業してください。また、Windowsのネットワークプロファイルが「パブリック」に設定されていると、ファイアウォールの影響で通信がうまくいかない場合があります。筆者の環境ではパブリックのままでもdiscover自体は動作しましたが、環境によって挙動が変わる可能性があるため、うまくいかない場合はネットワークプロファイルも確認してみてください。

前提として、Tapoアプリ側では2台ともWi-Fi接続・アカウント紐付けまでのセットアップは完了させておいてください。

Tapoアプリのマイホーム画面。2台のP110Mが登録済みの状態

▲ Tapoアプリの「マイホーム」画面。2台のP110Mが登録済みの状態

📄discover_devices.pyVSCode
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())

実行してみると、次のようになりました。

discover_devices.pyを実行し、デバイスが見つからなかった画面

▲ discover_devices.pyを実行 →「デバイスが見つかりませんでした。」

「デバイスが見つかりません」の原因はencrypt_type=’TPAP’

ネットワーク上にデバイス自体は存在するのに、見つかりません(デバッグログを見ると、encrypt_type='TPAP'という暗号化方式が原因で「Unsupported device」の状態になっていました)。実はこれ、ライブラリの不具合ではなくTapoアプリ側の設定が原因でした。P110Mのファームウェア1.4.1以降では、Tapoアプリ側で明示的に許可しない限り、サードパーティ製ツールからのローカルAPI制御をブロックする仕様に変更されています。これを解除する手順は次の通りです。

  • Tapoアプリを起動し、右下の「私」タブを開く
  • 「サービス」欄の「音声アシスタント」を開く(バージョンによっては「Tapoラボ」という名称の場合もあります)
  • 一覧の下にある「サードパーティ連携」を開き、トグルをONにする
  • P110M 1台ごとに個別にこの操作が必要です
「私」タブの設定画面

▲ 「私」タブの設定画面。「音声アシスタント」の少し上に「Tapo と Kasa を連携」が並んでいる

注意: この「音声アシスタント」のすぐ上に「Tapo と Kasa を連携」という似た名前の項目が並んでいますが、これは全くの別機能です。旧ブランド「Kasa」シリーズのデバイスをTapoアプリに統合するためのアカウント連携機能で、今回のエラーとは無関係です(そもそもpython-kasaというライブラリ名との類似も単なる偶然です)。「音声アシスタント」の方を選んでください。
「音声アシスタント」画面。一番下にサードパーティ連携がある

▲ 「音声アシスタント」画面。一番下に「サードパーティ連携」がある(この時点で既にオン)

「サードパーティ連携」のトグルをONにした状態

▲ 「サードパーティ連携」のトグルをONにした状態。免責事項が表示される

トグルをONにすると、「有効にすると、スマートデバイスがHome Assistantなどサードパーティのエコシステムとシームレスに統合され、スマートホームの接続性が向上します。免責事項: Home Assistantなどのオープンソースプラットフォームとの連携のためにこの機能を有効にすると、特定のプライバシーリスクが生じる可能性があります。」という説明文が表示されます。個人利用のPoCとしては許容範囲だと判断しましたが、念のため頭に入れておいてください。

2台とも設定した後、改めてdiscover_devices.pyを実行すると、無事にデバイス情報を取得できるようになりました。

サードパーティ連携ONの状態でdiscover_devices.pyを実行し、2台のデバイスが見つかった画面

▲ サードパーティ連携ONにしてから再実行 → 2台とも発見できた

ちょうどこの出力に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という共通モジュールに切り出しておきます。これも同じフォルダに置きます。

VSCodeのエクスプローラー。tapo-pduフォルダの中にtapo_devices.pyファイルがある

▲ tapo_devices.pyも同じフォルダに置く

📄tapo_devices.pyVSCode
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上ではこのように見えます。

VSCodeでtapo_devices.pyを開いた画面。エクスプローラーに他のファイルも並んでいる

▲ VSCodeでtapo_devices.pyを開いた状態

discoverが1回で成功しないことがある

実際に運用してみると、discoverがまれに1回目は失敗し、2回目のリトライで成功する、という現象がありました。原因ははっきり特定できていませんが(モバイルルーター特有のブロードキャスト遅延を疑っています)、実用上は単純なリトライで十分回避できたので、tapo_devices.pyに次のようなリトライ処理を追加しています。

📄tapo_devices.pyVSCode
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)

VSCodeのエクスプローラー。tapo-pduフォルダの中にindividual_control.pyファイルがある

▲ individual_control.pyも同じフォルダに置く

📄individual_control.pyVSCode
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())
PowerShellターミナル
python individual_control.py plug1 on

のように実行すると、plug1(Tapo P110M 1)だけをONにできます。

VSCodeの▶(実行)ボタンは引数なしでpython ファイル名.pyを実行するだけなので、plug1onのような引数を渡せません。このスクリプトは必ずターミナルからコマンドで実行してください。

引数を付けずに実行すると使い方が表示され、正しく引数を付けると実際にデバイスが操作されます。

VSCodeでindividual_control.pyを実行した画面。ターミナルに使い方エラーと成功結果が表示されている

▲ 引数なしで実行→使い方が表示 / plug1 onを付けて実行→ONに成功

1点ハマったポイントとして、Discover.discover()が返すDeviceオブジェクトは、直後の時点ではalias(デバイス名)などの情報がまだ反映されていないことがあります。

📄PythonVSCode
# 一見動きそうだが、aliasがNoneのまま表示されてしまう
dev = devices["plug1"]
print(dev.alias)  # None

await dev.update()を呼んでから参照する必要があります。地味にハマりやすいポイントなので、ON/OFF操作をした後も、状態表示のために再度update()を呼んでいます。

一括ON/OFF(bulk_control.py)

VSCodeのエクスプローラー。tapo-pduフォルダの中にbulk_control.pyファイルがある

▲ bulk_control.pyも同じフォルダに置く

📄bulk_control.pyVSCode
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に登録した全台が対象になります。

PowerShellターミナル
python bulk_control.py on

これで2台のTapo P110Mを一括ONできます。

VSCodeでbulk_control.pyを実行した画面。ターミナルに使い方エラーと成功結果が表示されている

▲ 引数なしで実行→使い方が表示 / onを付けて実行→2台とも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上にのみ保存し、外部には共有していません。



ニンジン🥕アイコン

Follow me on X

本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕

@ninjin_py_vba をフォロー

この記事が役に立ったら、シェアしてもらえると嬉しいです🥕

Xでポスト はてブ LINE Pocket

PROJECT LOG — SERIES INDEX
スポンサーリンク

シリーズ記事一覧

電子工作・自動化ツールの製作記録をシリーズごとにまとめています。気になるテーマからお読みください。


Tapo P110Mの消費電力をExcelレポート化する【年次集計・電気代試算・セキュリティ判断編】
FastAPI+openpyxlでTapo P110Mの消費電力をExcelレポート化。15秒間隔の生ログを日次集計する設計や、31円/kWhでの電気代試算、外部公開を見送った理由まで解説します。
Tapo P110M × FastAPIで電力監視ダッシュボードを作る【SQLite記録・リアルタイム堅牢化編】
Tapo P110MのデータをSQLiteに記録し、FastAPI+Uvicornでリアルタイム電力監視ダッシュボードを作る方法を解説。通信失敗時の自動復旧など堅牢化のポイントも紹介します。
Tapo P110M × python-kasaで自作PDUを作る【環境構築・Unsupported device(TPAP)エラー解決編】
Tapo P110Mをpython-kasaで自作PDU化。「Unsupported device」TPAPエラーの原因と対処法、MACアドレスでIP変動に強くする設計、個別/一括ON-OFFの実装コードまで解説します。
Raspberry Pi Pico Wで作る熱中症警戒アラート表示器
Raspberry Pi Pico WとDHT11、0.96インチOLEDで熱中症警戒アラート表示器を自作。気温・湿度から簡易的な暑さ指数を計算し、5段階の顔アイコンで危険度を表示する作り方をコード付きで解説します。
CustomTkinterでオリジナルのメニューバー(File・表示・設定・Help)を自作する
CustomTkinterには無いメニューバー(File・表示・設定・Help)を、CTkFrameとCTkButtonで自作する方法を、5段階のサンプルコードとダウンロード付きで解説します。

コメント