0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FastAPIのlifespanで起動時discover→バックグラウンド定期ポーリングを回す(python-kasa / Tapo P110M)

0
Posted at

概要

スマートプラグ Tapo P110M の消費電力を、FastAPI 製のダッシュボードで監視する構成です。python-kasa の API はすべて async で、しかも discover は数秒かかります。そこで アプリ起動時に 1 回だけ discover して接続を使い回し、以降はバックグラウンドタスクで一定間隔ポーリングして SQLite に記録 します。この記事では FastAPI の lifespan の書き方と、通信断からデバイス単位で復旧する部分だけ抜き出します。

ブラウザで開いた消費電力監視ダッシュボード

つまずいたポイント

最初はエンドポイントの中で毎回 Discover.discover() を呼んでいましたが、1 リクエストに数秒かかるうえ、デバイスにも余計な負荷がかかります。

接続は「起動時に 1 回」に寄せたいのですが、FastAPI 単体だと「起動時に 1 回だけ実行して、その結果をエンドポイント全体で共有する」置き場所に迷います。ここで使うのが lifespan(旧 @app.on_event("startup") の後継)です。起動時に discover → asyncio.create_task() でポーリングループを開始し、終了時にタスクをキャンセルして切断します。

コア1: lifespanで1回だけdiscoverし、ポーリングタスクを起動する

import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI

from tapo_devices import discover_with_retry, disconnect_all

devices: dict = {}          # 検出したデバイスを全体で共有する

@asynccontextmanager
async def lifespan(app: FastAPI):
    global devices
    devices = await discover_with_retry()      # 起動時に1回だけ
    for dev in devices.values():
        await dev.update()
    task = asyncio.create_task(poll_loop())    # バックグラウンドで回し続ける
    try:
        yield                                  # ここでアプリが動く
    finally:
        task.cancel()                          # 終了時に後片付け
        await disconnect_all(devices)

app = FastAPI(lifespan=lifespan)

エンドポイント側は devices や、ポーリングが更新する latest_status を参照するだけで済みます。

コア2: 通信失敗をデバイス単位で数えて、閾値で再discoverする

Wi-Fi 環境では dev.update() がたまに失敗します。全体を作り直すと重いので、デバイスごとに連続失敗回数を数え、閾値(既定 2 回)を超えたそのデバイスだけ discover し直します。失敗中も直前の値に online: False を上書きして残し、画面の OFFLINE 表示に使います。

POLL_INTERVAL_SEC = 15
REDISCOVER_AFTER_FAILURES = 2

latest_status: dict = {}
consecutive_failures: dict = {}

async def poll_loop():
    conn = init_db()
    while True:
        for name, dev in list(devices.items()):
            try:
                await dev.update()
                log_reading(conn, name, dev)
                latest_status[name] = status_of(dev, online=True)
                consecutive_failures[name] = 0
            except Exception as exc:
                consecutive_failures[name] = consecutive_failures.get(name, 0) + 1
                latest_status[name] = {**latest_status.get(name, {}),
                                       "online": False, "error": str(exc)}
                if consecutive_failures[name] >= REDISCOVER_AFTER_FAILURES:
                    await rediscover_one(name)   # この1台だけ discover し直す
        await asyncio.sleep(POLL_INTERVAL_SEC)

なお Tapo P110M の Energy モジュールは consumption_today / consumption_this_month は取れましたが、consumption_total(生涯積算)は実機で常に None でした。

完全なソースコードと手順はブログにまとめています

power_readings テーブルの設計、dev.modules["Energy"] から取れる値・取れない値の一覧、/api/status/api/history・ON/OFF API の実装、バニラ JS のフロント、ポーリング間隔を環境変数にした理由、常駐化を見送った判断まで、以下の記事に書いています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?