feat(dashboard): add basin fill gauge, countdown, and basin trend charts
Some checks failed
CI / lint-and-test (push) Has been cancelled
Some checks failed
CI / lint-and-test (push) Has been cancelled
PS control page now shows 7 fields instead of 5:
- Direction (filling/draining/steady)
- Basin level (m)
- Basin volume (m³)
- Fill level (%)
- Net flow (m³/h, signed)
- Time to full/empty (countdown in min or s)
- Inflow (m³/h)
Two new trend pages per time window (short 10 min / long 1 hour):
- Basin chart: 3 series (Basin fill %, Basin level m, Net flow m³/h)
on both Trends 10 min and Trends 1 hour pages.
PS formatter now extracts direction, netFlow, seconds from the delta-
compressed port 0 cache and computes fillPct from vol/maxVol. Dispatcher
sends 10 outputs (7 text + 3 trend numerics to both short+long basin
charts).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -611,14 +611,26 @@ def build_process_tab():
|
||||
"const vol = find('volume.predicted.');\n"
|
||||
"const qIn = find('flow.measured.upstream.') || find('flow.measured.in.');\n"
|
||||
"const qOut = find('flow.measured.downstream.') || find('flow.measured.out.');\n"
|
||||
"// Compute derived metrics\n"
|
||||
"const maxVol = 9.33; // must match basinVolume * basinHeight / basinHeight = basinVolume / surfaceArea * height\n"
|
||||
"const fillPct = vol != null ? Math.round(Number(vol) / maxVol * 100) : null;\n"
|
||||
"const netM3h = (c.netFlow != null) ? Number(c.netFlow) * 3600 : null;\n"
|
||||
"const seconds = (c.seconds != null && Number.isFinite(Number(c.seconds))) ? Number(c.seconds) : null;\n"
|
||||
"const timeStr = seconds != null ? (seconds > 60 ? Math.round(seconds/60) + ' min' : Math.round(seconds) + ' s') : 'n/a';\n"
|
||||
"msg.payload = {\n"
|
||||
" level: lvl != null ? Number(lvl).toFixed(2) + ' m' : 'n/a',\n"
|
||||
" volume: vol != null ? Number(vol).toFixed(1) + ' m³' : 'n/a',\n"
|
||||
" qIn: qIn != null ? (Number(qIn) * 3600).toFixed(0) + ' m³/h' : 'n/a',\n"
|
||||
" qOut: qOut != null ? (Number(qOut) * 3600).toFixed(0) + ' m³/h' : 'n/a',\n"
|
||||
" state: c.state || c.direction || 'idle',\n"
|
||||
" levelNum: lvl != null ? Number(lvl) : null,\n"
|
||||
" volumeNum: vol != null ? Number(vol) : null,\n"
|
||||
" direction: c.direction || 'steady',\n"
|
||||
" level: lvl != null ? Number(lvl).toFixed(2) + ' m' : 'n/a',\n"
|
||||
" volume: vol != null ? Number(vol).toFixed(1) + ' m³' : 'n/a',\n"
|
||||
" fillPct: fillPct != null ? fillPct + '%' : 'n/a',\n"
|
||||
" netFlow: netM3h != null ? netM3h.toFixed(0) + ' m³/h' : 'n/a',\n"
|
||||
" timeLeft: timeStr,\n"
|
||||
" qIn: qIn != null ? (Number(qIn) * 3600).toFixed(0) + ' m³/h' : 'n/a',\n"
|
||||
" qOut: qOut != null ? (Number(qOut) * 3600).toFixed(0) + ' m³/h' : 'n/a',\n"
|
||||
" // Numerics for trends\n"
|
||||
" levelNum: lvl != null ? Number(lvl) : null,\n"
|
||||
" volumeNum: vol != null ? Number(vol) : null,\n"
|
||||
" fillPctNum: fillPct,\n"
|
||||
" netFlowNum: netM3h,\n"
|
||||
"};\n"
|
||||
"return msg;",
|
||||
outputs=1, wires=[["lout_evt_ps"]],
|
||||
@@ -719,10 +731,12 @@ def build_ui_tab():
|
||||
ui_group(g_pump_b, "4b. Pump B", PG, width=4, order=6),
|
||||
ui_group(g_pump_c, "4c. Pump C", PG, width=4, order=7),
|
||||
# Trends on separate pages
|
||||
ui_group(g_trend_short_flow, "Flow (10 min)", PG_SHORT, width=12, order=1),
|
||||
ui_group(g_trend_short_power, "Power (10 min)", PG_SHORT, width=12, order=2),
|
||||
ui_group(g_trend_long_flow, "Flow (1 hour)", PG_LONG, width=12, order=1),
|
||||
ui_group(g_trend_long_power, "Power (1 hour)", PG_LONG, width=12, order=2),
|
||||
ui_group(g_trend_short_flow, "Flow (10 min)", PG_SHORT, width=12, order=1),
|
||||
ui_group(g_trend_short_power, "Power (10 min)", PG_SHORT, width=12, order=2),
|
||||
ui_group("ui_grp_trend_short_basin", "Basin (10 min)", PG_SHORT, width=12, order=3),
|
||||
ui_group(g_trend_long_flow, "Flow (1 hour)", PG_LONG, width=12, order=1),
|
||||
ui_group(g_trend_long_power, "Power (1 hour)", PG_LONG, width=12, order=2),
|
||||
ui_group("ui_grp_trend_long_basin", "Basin (1 hour)", PG_LONG, width=12, order=3),
|
||||
]
|
||||
|
||||
nodes.append(comment("c_ui_title", TAB_UI, LANE_X[2], 20,
|
||||
@@ -863,30 +877,56 @@ def build_ui_tab():
|
||||
CH_PS_EVT, source_out_ids=["lout_evt_ps"],
|
||||
downstream=["dispatch_ps"]
|
||||
))
|
||||
# PS dispatcher: 10 outputs — 7 text fields + 3 trend numerics
|
||||
nodes.append(function_node(
|
||||
"dispatch_ps", TAB_UI, LANE_X[1], y + 160,
|
||||
"dispatch PS",
|
||||
"const p = msg.payload || {};\n"
|
||||
"const ts = Date.now();\n"
|
||||
"return [\n"
|
||||
" {payload: String(p.state || 'idle')},\n"
|
||||
" {payload: String(p.level || 'n/a')},\n"
|
||||
" {payload: String(p.volume || 'n/a')},\n"
|
||||
" {payload: String(p.qIn || 'n/a')},\n"
|
||||
" {payload: String(p.qOut || 'n/a')},\n"
|
||||
" {payload: String(p.direction || 'steady')},\n"
|
||||
" {payload: String(p.level || 'n/a')},\n"
|
||||
" {payload: String(p.volume || 'n/a')},\n"
|
||||
" {payload: String(p.fillPct || 'n/a')},\n"
|
||||
" {payload: String(p.netFlow || 'n/a')},\n"
|
||||
" {payload: String(p.timeLeft || 'n/a')},\n"
|
||||
" {payload: String(p.qIn || 'n/a')},\n"
|
||||
" // Trend numerics\n"
|
||||
" p.fillPctNum != null ? {topic: 'Basin fill', payload: p.fillPctNum, timestamp: ts} : null,\n"
|
||||
" p.levelNum != null ? {topic: 'Basin level', payload: p.levelNum, timestamp: ts} : null,\n"
|
||||
" p.netFlowNum != null ? {topic: 'Net flow', payload: p.netFlowNum,timestamp: ts} : null,\n"
|
||||
"];",
|
||||
outputs=5,
|
||||
wires=[["ui_ps_state"], ["ui_ps_level"], ["ui_ps_volume"], ["ui_ps_qin"], ["ui_ps_qout"]],
|
||||
outputs=10,
|
||||
wires=[
|
||||
["ui_ps_direction"],
|
||||
["ui_ps_level"],
|
||||
["ui_ps_volume"],
|
||||
["ui_ps_fill"],
|
||||
["ui_ps_netflow"],
|
||||
["ui_ps_timeleft"],
|
||||
["ui_ps_qin"],
|
||||
# Trend outputs → both short + long charts
|
||||
["trend_short_basin", "trend_long_basin"], # fill %
|
||||
["trend_short_basin", "trend_long_basin"], # level
|
||||
["trend_short_basin", "trend_long_basin"], # net flow
|
||||
],
|
||||
))
|
||||
nodes.append(ui_text("ui_ps_state", TAB_UI, LANE_X[2], y + 160, g_ps,
|
||||
"PS state", "Basin state", "{{msg.payload}}"))
|
||||
nodes.append(ui_text("ui_ps_level", TAB_UI, LANE_X[2], y + 200, g_ps,
|
||||
"PS level", "Basin level", "{{msg.payload}}"))
|
||||
nodes.append(ui_text("ui_ps_volume", TAB_UI, LANE_X[2], y + 240, g_ps,
|
||||
"PS volume","Basin volume", "{{msg.payload}}"))
|
||||
nodes.append(ui_text("ui_ps_qin", TAB_UI, LANE_X[2], y + 280, g_ps,
|
||||
"PS Qin", "Inflow", "{{msg.payload}}"))
|
||||
nodes.append(ui_text("ui_ps_qout", TAB_UI, LANE_X[2], y + 320, g_ps,
|
||||
"PS Qout", "Pumped out", "{{msg.payload}}"))
|
||||
|
||||
# PS text widgets
|
||||
nodes.append(ui_text("ui_ps_direction", TAB_UI, LANE_X[2], y + 160, g_ps,
|
||||
"PS direction", "Direction", "{{msg.payload}}"))
|
||||
nodes.append(ui_text("ui_ps_level", TAB_UI, LANE_X[2], y + 200, g_ps,
|
||||
"PS level", "Basin level", "{{msg.payload}}"))
|
||||
nodes.append(ui_text("ui_ps_volume", TAB_UI, LANE_X[2], y + 240, g_ps,
|
||||
"PS volume", "Basin volume", "{{msg.payload}}"))
|
||||
nodes.append(ui_text("ui_ps_fill", TAB_UI, LANE_X[2], y + 280, g_ps,
|
||||
"PS fill %", "Fill level", "{{msg.payload}}"))
|
||||
nodes.append(ui_text("ui_ps_netflow", TAB_UI, LANE_X[2], y + 320, g_ps,
|
||||
"PS net flow", "Net flow", "{{msg.payload}}"))
|
||||
nodes.append(ui_text("ui_ps_timeleft", TAB_UI, LANE_X[2], y + 360, g_ps,
|
||||
"PS time left", "Time to full/empty", "{{msg.payload}}"))
|
||||
nodes.append(ui_text("ui_ps_qin", TAB_UI, LANE_X[2], y + 400, g_ps,
|
||||
"PS Qin", "Inflow", "{{msg.payload}}"))
|
||||
|
||||
# ===== SECTION: Per-pump panels =====
|
||||
y_pumps_start = 1000
|
||||
@@ -1046,6 +1086,26 @@ def build_ui_tab():
|
||||
order=1,
|
||||
))
|
||||
|
||||
# ===== Basin charts (fill %, level, net flow) =====
|
||||
# Short-term
|
||||
nodes.append(ui_chart(
|
||||
"trend_short_basin", TAB_UI, LANE_X[3], y_charts + 360,
|
||||
"ui_grp_trend_short_basin",
|
||||
"Basin — 10 min", "Basin metrics",
|
||||
width=12, height=8,
|
||||
remove_older="10", remove_older_unit="60", remove_older_points="300",
|
||||
y_axis_label="", order=1,
|
||||
))
|
||||
# Long-term
|
||||
nodes.append(ui_chart(
|
||||
"trend_long_basin", TAB_UI, LANE_X[3], y_charts + 440,
|
||||
"ui_grp_trend_long_basin",
|
||||
"Basin — 1 hour", "Basin metrics",
|
||||
width=12, height=8,
|
||||
remove_older="60", remove_older_unit="60", remove_older_points="1800",
|
||||
y_axis_label="", order=1,
|
||||
))
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user