rack:PD 供电信息、每日耗电量与平均功率
PD 供电(板卡页新增一格):
- 优先用遥测里的实时值 status.pd(固件 ≥0.1.17);老固件退回最近一条 PD_LIMIT 事件(快照的 pd)。
显示 20 V / 档位电流 / 整板功率上限 / 输入功率占上限的比例。
- PD_LIMIT 事件写成「PD 20 V / 3.00 A,上限 60 W」,不再把那两列当电压电流显示;arg 2 = 运行中换档。
耗电量(板卡页新增卡片):
- 新表 energy_day(永久保留):从分钟聚合滚出每日能量与有数据秒数,rail 0 = 底板 12 V 输入,1~5 = 各路主板。
每分钟重算今天 / 昨天,更早的只补缺不覆盖(分钟数据 90 天后会被清)。按服务器本地零点切日。
- GET /api/boards/{id}/energy;前端 EnergyCard:今日 / 昨日 / 近 7 天 / 近 30 天 / 累计(各带平均功率)、
每日柱状图、今日各路主板。量的是 12 V 输入,不含 PD 20→12 V 降压损耗。
后端测试 57 项全过;已部署。
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+15
-1
@@ -58,6 +58,7 @@ class Hub:
|
||||
self.bus.start()
|
||||
for kind in ("rsp", "tele", "state"):
|
||||
self.bus.subscribe("%s/+/%s" % (protocol.TOPIC_ROOT, kind), 0 if kind == "tele" else 1)
|
||||
self.db.energy_rollup(days_back=1) # 补上停机期间没滚的日子(更早的只补缺,见 Db.energy_rollup)
|
||||
self._tasks = [asyncio.ensure_future(self._flusher()), asyncio.ensure_future(self._maint())]
|
||||
|
||||
async def stop(self):
|
||||
@@ -192,6 +193,8 @@ class Hub:
|
||||
self.db.x("UPDATE boards SET ev_cursor=? WHERE id=?", (nxt, board))
|
||||
if ev:
|
||||
self.broadcast({"t": "events", "board": board, "ev": ev, "received": now})
|
||||
if any(e[3] == protocol.EV_PD_LIMIT for e in ev): # 板子重启过:供电档位可能换了
|
||||
self.broadcast({"t": "board", "board": self.snapshot(board)})
|
||||
if nxt >= rsp.get("next", nxt) or not ev:
|
||||
return
|
||||
except CmdTimeout:
|
||||
@@ -200,6 +203,16 @@ class Hub:
|
||||
self._syncing.discard(board)
|
||||
|
||||
# ---- 查 ----------------------------------------------------------------
|
||||
def pd(self, board: str) -> Optional[dict]:
|
||||
"""CH224A 协商到的供电档位。固件只在启动时读一次、记成 PD_LIMIT 事件(不进遥测),
|
||||
所以取最近一条;板子还没同步过事件是 None。充电器不断电换不了,启动那一次就是现状。"""
|
||||
rows = self.db.q("SELECT arg, mv, ua, received FROM events WHERE board_id=? AND type=? "
|
||||
"ORDER BY received DESC, seq DESC LIMIT 1", (board, protocol.EV_PD_LIMIT))
|
||||
if not rows:
|
||||
return None
|
||||
r = rows[0]
|
||||
return dict(protocol.pd_from_event(r["arg"], r["mv"], r["ua"]), ts=r["received"])
|
||||
|
||||
def snapshot(self, board: str) -> dict:
|
||||
b = self._board(board)
|
||||
meta = self.db.q("SELECT name, location FROM boards WHERE id=?", (board,))
|
||||
@@ -208,7 +221,7 @@ class Hub:
|
||||
stale = b["last_tele"] == 0 or now_ms() - b["last_tele"] > STALE_MS
|
||||
return {"id": board, "name": meta[0]["name"] if meta else "", "location": meta[0]["location"] if meta else "",
|
||||
"online": b["online"], "stale": stale, "fw": b["fw"], "ip": b["ip"],
|
||||
"last_tele": b["last_tele"], "status": b["status"],
|
||||
"last_tele": b["last_tele"], "status": b["status"], "pd": self.pd(board),
|
||||
"phones": [dict(p) for p in phones]}
|
||||
|
||||
def boards(self) -> List[dict]:
|
||||
@@ -257,6 +270,7 @@ class Hub:
|
||||
await asyncio.sleep(MAINT_S)
|
||||
try:
|
||||
self.db.downsample()
|
||||
self.db.energy_rollup() # 先滚再清:今天 / 昨天每分钟重算一遍,量是几千行
|
||||
self.db.prune()
|
||||
except Exception:
|
||||
log.exception("降采样 / 清理失败")
|
||||
|
||||
在新工单中引用
屏蔽一个用户