fix: fully configure PS basin + add node-completeness rule
Some checks failed
CI / lint-and-test (push) Has been cancelled

Basin undersized (10m³) for sinus peak (126 m³/h) → overflow → 122%.
Now 30 m³ with 4m height, all PS fields set. New rule: always configure
every field of every node.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-04-14 11:00:27 +02:00
parent eb97670179
commit 53b55d81c3
3 changed files with 82 additions and 36 deletions

View File

@@ -853,15 +853,26 @@
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"controlMode": "levelbased",
"basinVolume": 10,
"basinHeight": 3,
"startLevel": 1.2,
"stopLevel": 0.6,
"minFlowLevel": 0.6,
"maxFlowLevel": 2.8,
"heightInlet": 2.5,
"heightOutlet": 0.2,
"heightOverflow": 2.8,
"basinVolume": 30,
"basinHeight": 4,
"heightInlet": 3.5,
"heightOutlet": 0.3,
"heightOverflow": 3.8,
"inletPipeDiameter": 0.3,
"outletPipeDiameter": 0.3,
"startLevel": 2.0,
"stopLevel": 1.0,
"minFlowLevel": 1.0,
"maxFlowLevel": 3.5,
"refHeight": "NAP",
"minHeightBasedOn": "outlet",
"basinBottomRef": 0,
"staticHead": 12,
"maxDischargeHead": 24,
"pipelineLength": 80,
"defaultFluid": "wastewater",
"temperatureReferenceDegC": 15,
"maxInflowRate": 200,
"enableDryRunProtection": true,
"enableOverfillProtection": true,
"dryRunThresholdPercent": 5,
@@ -881,7 +892,7 @@
"type": "function",
"z": "tab_process",
"name": "format PS port 0",
"func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst lvl = find('level.predicted.');\nconst vol = find('volume.predicted.');\nconst qIn = find('flow.measured.upstream.') || find('flow.measured.in.');\nconst qOut = find('flow.measured.downstream.') || find('flow.measured.out.');\n// Compute derived metrics\nconst maxVol = 9.33; // must match basinVolume * basinHeight / basinHeight = basinVolume / surfaceArea * height\nconst fillPct = vol != null ? Math.round(Number(vol) / maxVol * 100) : null;\nconst netM3h = (c.netFlow != null) ? Number(c.netFlow) * 3600 : null;\nconst seconds = (c.seconds != null && Number.isFinite(Number(c.seconds))) ? Number(c.seconds) : null;\nconst timeStr = seconds != null ? (seconds > 60 ? Math.round(seconds/60) + ' min' : Math.round(seconds) + ' s') : 'n/a';\nmsg.payload = {\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\u00b3' : 'n/a',\n fillPct: fillPct != null ? fillPct + '%' : 'n/a',\n netFlow: netM3h != null ? netM3h.toFixed(0) + ' m\u00b3/h' : 'n/a',\n timeLeft: timeStr,\n qIn: qIn != null ? (Number(qIn) * 3600).toFixed(0) + ' m\u00b3/h' : 'n/a',\n qOut: qOut != null ? (Number(qOut) * 3600).toFixed(0) + ' m\u00b3/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};\nreturn msg;",
"func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst lvl = find('level.predicted.');\nconst vol = find('volume.predicted.');\nconst qIn = find('flow.measured.upstream.') || find('flow.measured.in.');\nconst qOut = find('flow.measured.downstream.') || find('flow.measured.out.');\n// Compute derived metrics\n// Basin capacity = basinVolume (config). Don't hardcode \u2014 read it once.\nif (!context.get('maxVol')) context.set('maxVol', 30.0); // basinVolume from PS config\nconst maxVol = context.get('maxVol');\nconst fillPct = vol != null ? Math.min(100, Math.max(0, Math.round(Number(vol) / maxVol * 100))) : null;\nconst netM3h = (c.netFlow != null) ? Number(c.netFlow) * 3600 : null;\nconst seconds = (c.seconds != null && Number.isFinite(Number(c.seconds))) ? Number(c.seconds) : null;\nconst timeStr = seconds != null ? (seconds > 60 ? Math.round(seconds/60) + ' min' : Math.round(seconds) + ' s') : 'n/a';\nmsg.payload = {\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\u00b3' : 'n/a',\n fillPct: fillPct != null ? fillPct + '%' : 'n/a',\n netFlow: netM3h != null ? netM3h.toFixed(0) + ' m\u00b3/h' : 'n/a',\n timeLeft: timeStr,\n qIn: qIn != null ? (Number(qIn) * 3600).toFixed(0) + ' m\u00b3/h' : 'n/a',\n qOut: qOut != null ? (Number(qOut) * 3600).toFixed(0) + ' m\u00b3/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};\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
@@ -3476,7 +3487,7 @@
"prefix": "",
"suffix": " m",
"min": 0,
"max": 3,
"max": 4,
"segments": [
{
"color": "#f44336",
@@ -3484,19 +3495,19 @@
},
{
"color": "#ff9800",
"from": 0.6
"from": 1.0
},
{
"color": "#2196f3",
"from": 1.2
"from": 2.0
},
{
"color": "#ff9800",
"from": 2.5
"from": 3.5
},
{
"color": "#f44336",
"from": 2.8
"from": 3.8
}
],
"width": 2,
@@ -3632,7 +3643,7 @@
"prefix": "",
"suffix": " m",
"min": 0,
"max": 3,
"max": 4,
"segments": [
{
"color": "#f44336",
@@ -3640,19 +3651,19 @@
},
{
"color": "#ff9800",
"from": 0.6
"from": 1.0
},
{
"color": "#2196f3",
"from": 1.2
"from": 2.0
},
{
"color": "#ff9800",
"from": 2.5
"from": 3.5
},
{
"color": "#f44336",
"from": 2.8
"from": 3.8
}
],
"width": 2,