TAPO PDU PROJECT VOL.3
Tapo P110Mの消費電力をExcelレポート化する【年次集計・電気代試算・セキュリティ判断編】
こんにちは。ニンジンです🥕
前回は、Tapo P110Mの消費電力データをSQLiteに記録する仕組みと、FastAPI + Uvicornで一括/個別ON-OFF・電力監視ができるWebダッシュボードの構築、通信失敗時の自動再discoverまでを紹介しました。
今回はその記録データを活用して、消費電力履歴をExcelでダウンロードできるようにし、月別・年次の集計レポート、参考単価による電気代の試算機能まで作っていきます。あわせて、「スマホなど別端末からもダッシュボードを見たい」という要望を検討した結果、あえて見送った判断についても紹介します。
Series Navigation
このシリーズの記事一覧(全3回)
この記事で分かること
- FastAPIで
.xlsxファイルをダウンロードさせる方法(openpyxl+StreamingResponse) - 15秒間隔でポーリングした生ログをそのまま月別タブに出力すると起きる「データ量問題」の試算
energy_today_kwh(本日の積算電力量)のMAX値を使った、壊れにくい日次集計クエリの設計- 1〜12月のタブを持つ年次レポートをopenpyxlで生成する方法
- 参考単価(円/kWh)を使って電気代を試算し、注記付きでExcelに表示する方法
- 「スマホなど別端末からダッシュボードを見たい」を、認証なしダッシュボードのセキュリティリスクを理由にあえて見送った判断
こんな人におすすめ
- 前回・前々回の記事からの続きで読んでいる人
- 自作の監視ツールに「Excelでダウンロードできる集計レポート」を追加したい人
- 個人開発のWebアプリを外部公開するかどうかで悩んでいる人
消費電力履歴を/api/exportでExcelダウンロードする
まずはシンプルに、SQLiteに溜まった生ログをそのままExcel形式でダウンロードできるようにします。CSVでも良かったのですが、あとで見出しの体裁を整えたり複数タブに分けたりすることを見据えて、最初からopenpyxlで.xlsxを組み立てることにしました。
from io import BytesIO
from openpyxl import Workbook
from fastapi.responses import StreamingResponse
@app.get("/api/export")
async def export_excel(device: str | None = None):
"""消費電力量の記録をExcel(.xlsx)としてダウンロードする。省エネ参考資料としての利用を想定。"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
if device:
if device not in DEVICES:
raise HTTPException(status_code=404, detail="unknown device")
rows = conn.execute(
"SELECT * FROM power_readings WHERE device_name = ? ORDER BY id", (device,)
).fetchall()
else:
rows = conn.execute("SELECT * FROM power_readings ORDER BY id").fetchall()
conn.close()
wb = Workbook()
ws = wb.active
ws.title = "消費電力履歴"
headers = ["日時", "デバイス", "瞬間電力(W)", "本日の電力量(kWh)", "当月の電力量(kWh)", "電圧(V)", "電流(A)"]
ws.append(headers)
for row in rows:
label = DEVICES.get(row["device_name"], {}).get("label", row["device_name"])
ws.append([
row["timestamp"], label, row["power_w"],
row["energy_today_kwh"], row["energy_month_kwh"],
row["voltage_v"], row["current_a"],
])
for col, width in zip("ABCDEFG", [20, 16, 14, 18, 18, 12, 12]):
ws.column_dimensions[col].width = width
ws.freeze_panes = "A2"
buf = BytesIO()
wb.save(buf)
buf.seek(0)
filename = f"tapo_power_history_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
return StreamingResponse(
buf,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)ポイントは、ファイルをディスクに書き出さずにBytesIO上でExcelを組み立て、StreamingResponseでそのままダウンロードさせているところです。Content-Dispositionヘッダーでファイル名を指定すれば、ブラウザ側は普通のファイルダウンロードとして扱ってくれます。
ダッシュボードの各デバイスカードに「Excel」ボタンを追加し、クリックするとwindow.location.hrefでこのエンドポイントに遷移させるだけで、ダウンロードが始まります。
生ログをそのまま月別タブにすると何が起きるか
生データのダウンロード自体はこれで動くのですが、「1年分をまとめて見たい」という用途には向きません。今回のダッシュボードは15秒間隔でポーリングしているため、1台あたりの記録件数を計算してみると次のようになります。
- 1時間あたり:
60分 × 60秒 ÷ 15秒= 240件 - 1日あたり:
240件 × 24時間= 5,760件 - 1年あたり:
5,760件 × 365日≈ 210万件
これをそのまま「1月タブ、2月タブ…」と月別に流し込むと、1タブに数十万行のデータが並ぶことになり、Excelで開くだけで重くなりますし、そもそも人間が眺めて傾向を掴める形ではありません。永久保存(削除・アーカイブなしの設計)にしているぶん、この問題は時間が経つほど深刻になります。
そこで、生データのダウンロードとは別に、日単位に丸めた集計版を作ることにしました。
MAX(energy_today_kwh)を使った日次集計クエリを設計する
「その日1日でどれだけ電力を使ったか」を求めるとき、素朴には「瞬間電力(power_w)を全部足し合わせる」方法を考えがちです。しかし、これは以下の理由で採用しませんでした。
- 瞬間電力はあくまで「その瞬間」の値であり、単純に
SUMしても正しい電力量(kWh)にはならない(ポーリング間隔を掛けて積分する必要があり、通信失敗によるポーリング抜けがあると誤差が乗る) - 一方、Tapo P110M自身が計算している
energy_today_kwh(本日の積算電力量)は、その日のうちにデバイス内部でリセットされながら増え続ける値なので、その日の最後に記録された値(=最大値)がそのまま「その日の合計」になる
そのため、日次集計は素直にMAX(energy_today_kwh)を使うのが一番正確、という結論になりました。
query = """
SELECT
date(timestamp) AS day,
device_name,
MAX(energy_today_kwh) AS daily_kwh,
AVG(power_w) AS avg_w,
MAX(power_w) AS max_w
FROM power_readings
WHERE strftime('%Y', timestamp) = ? AND strftime('%m', timestamp) = ?
GROUP BY day, device_name
ORDER BY day, device_name
"""avg_w(平均電力)・max_w(最大電力)は参考情報として、同じクエリの中で一緒に集計しています。/api/export/yearlyで1〜12月タブの年次レポートを生成する
集計クエリができたので、指定した年の1〜12月ぶんのシートを持つExcelを組み立てます。
DEFAULT_UNIT_PRICE_YEN = 31.0
@app.get("/api/export/yearly")
async def export_yearly(year: int, device: str | None = None, unit_price: float = DEFAULT_UNIT_PRICE_YEN):
"""指定した年の1〜12月タブを持つExcelを生成する。各タブは日次集計(電力量合計・平均/最大電力・参考金額)。"""
if device is not None and device not in DEVICES:
raise HTTPException(status_code=404, detail="unknown device")
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
wb = Workbook()
wb.remove(wb.active)
note = (
f"※参考単価: 1kWhあたり{unit_price:g}円(目安)で試算した金額です。"
"実際の電気料金は契約している電力会社・プランにより異なります。"
)
headers = ["日付", "デバイス", "電力量合計(kWh)", "平均電力(W)", "最大電力(W)", "参考金額(円)"]
for month in range(1, 13):
sheet = wb.create_sheet(title=f"{year}年{month}月")
sheet.append([note])
sheet.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(headers))
sheet.append(headers)
# (このあと日次集計クエリを実行し、1日1行ずつ書き込む)
sheet.freeze_panes = "A3"
conn.close()
# ...(Excelをメモリ上に保存してダウンロードさせる処理は/api/exportと同様)データがまだない月(未来の月、あるいは単に記録がない月)は、ヘッダー行だけの空タブになる仕様にしています。1年分の見通しを最初から確保しておいたほうが、あとで見返すときに月をまたいで比較しやすいと考えたためです。
参考単価31円/kWhで電気代を試算する
省エネの参考資料として使うなら、電力量(kWh)だけでなく「だいたい何円くらいか」も見えたほうが実感が湧きます。ただし電気料金は契約している電力会社・プランによって単価が変わるため、実際の請求額ではなく、あくまで参考金額として試算する形にしました。
既定の単価は31円/kWhにしています。これは2026年時点で家庭用電気料金の全国平均目安としてよく参照される値をWeb検索で確認して採用したもので、地域や契約プランによって実際の単価は変動します。この前提が伝わるよう、各月シートの1行目に注記(結合セル)として明記しています。
daily_kwh = round(row["daily_kwh"], 3) if row["daily_kwh"] is not None else None
sheet.append([
row["day"], label, daily_kwh,
round(row["avg_w"], 2) if row["avg_w"] is not None else None,
round(row["max_w"], 2) if row["max_w"] is not None else None,
round(daily_kwh * unit_price) if daily_kwh is not None else None,
])なお、この「参考金額」列は日次集計版(/api/export/yearly)だけに追加しています。生ログの瞬間電力(W)に単価を掛けても意味のある金額にはならないため、全期間の生データダンプ(/api/export)には追加していません。
ダッシュボード側にも、年の入力欄・参考単価の入力欄(既定31円)・「年別レポート(日次集計・全12ヶ月タブ)」ボタンを追加し、生データの「全期間の生データをエクスポート」ボタンと並べて2階建てにしました。
<input type="number" id="yearInput">
<label>参考単価
<input type="number" id="unitPriceInput" value="31" step="0.1"> 円/kWh
</label>
<button onclick="exportYearly()">年別レポート(日次集計・全12ヶ月タブ)</button>
<button onclick="exportExcel()">全期間の生データをエクスポート</button>スマホなど他端末からダッシュボードにアクセスしたいと思ったら
「スマホなど別端末からもダッシュボードを見たい」という気持ちもあったのですが、今回はセキュリティ上の理由から見送りました。今のダッシュボードには認証機能が一切なく、ON/OFFの制御APIも消費電力データの閲覧も誰でも操作できてしまう状態なのと、このPCのセキュリティ性を考えたため、127.0.0.1(このPC上のみ)での運用に留めています。マルチデバイス対応が必要になったときは、次に予定しているRaspberry Pi 4Bへのサーバー移行のタイミングで、認証機能の追加とあわせて改めて設計し直す方針です。
よくある質問
Q生ログをそのまま月別タブに出力しなかったのはなぜですか?
A15秒間隔のポーリングだと1台あたり年間約210万件に達する試算になり、1タブに数十万行を詰め込むとExcelで開くだけで重くなるうえ、人間が眺めて傾向を掴める形にもならないためです。日単位に丸めた集計版(/api/export/yearly)を別途用意することで解決しました。
Q参考単価はどうやって決めましたか? 実際の電気代とずれませんか?
A2026年時点で家庭用電気料金の全国平均目安としてよく参照される31円/kWhをWeb検索で確認して既定値にしていますが、実際の単価は契約している電力会社・プランによって変わります。そのためあくまで「参考金額」と明記し、unit_priceパラメータやダッシュボードの入力欄で利用者が自分の契約単価に合わせて変更できるようにしています。
Qなぜスマホからも見られるようにしなかったのですか?
A今のダッシュボードには認証機能が一切なく、ON/OFF制御まで含めて誰でも操作できてしまう状態のため、セキュリティ上の理由から今回は見送りました。127.0.0.1(PC専用)運用に留め、マルチデバイス対応は次に検討しているRaspberry Pi 4Bサーバー化のタイミングで、認証機能とあわせて設計し直す予定です。
Q生ログはこの先も無限に増え続けるのですか?
A現状の実装では削除・アーカイブ処理を持たないため、記録し続ける限りDBは肥大化していきます。将来的には「直近数ヶ月ぶんだけ生データを保持し、それより古いものは日次集計に丸めて元データを削除する」といった保持ポリシーの導入が望ましいと考えていますが、今回はまだ実装せず今後の課題としています。
QCSVではなくExcel(.xlsx)出力にしたのはなぜですか?
A1つのファイルに1〜12月ぶんの複数タブを持たせたかったため、openpyxlで.xlsxを組み立てる形にしました。CSVは1ファイル1テーブルが基本のため、月ごとにファイルを分けるか1ファイルに全月を詰め込むかの選択になり、どちらも今回の「月ごとにタブを切り替えて見る」という使い方には合いませんでした。
まとめ・次回予告
今回は、Tapo P110Mの消費電力履歴をExcelでダウンロードできるようにし、生ログのままでは扱いづらいという課題から日次集計の年次レポートを設計・実装、参考単価による電気代の試算まで紹介しました。
- openpyxl+StreamingResponseでディスクに書き出さずにExcelをダウンロードさせる方法
- 15秒間隔ポーリングの生ログをそのまま月別タブにすると年間約210万件に達してしまう問題と、MAX(energy_today_kwh)を使った壊れにくい日次集計の設計
- 1〜12月タブの年次レポートに、契約により変動する旨を明記したうえで参考単価による電気代試算を追加
- 「スマホからも見たい」という要望を、認証機能のないダッシュボードを公開するリスクを理由にあえて見送り、127.0.0.1運用に留めた判断
Tapo P110M PDUシステムのPoCとしての3部作はこれで一区切りです。今後、24時間稼働の実運用を見据えてRaspberry Pi 4Bをサーバーとして常時稼働させる続編も検討していますので、実現したらまたこのブログで紹介したいと思います。
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回でお届けします。
シリーズを読む


コメント