feat(pumpingStation): realistic defaults, ramp-foot visual fix, manual-mode visibility, dashboard example

Editor + schema defaults
- pumpingStation.html: drag-in defaults now reflect a realistic basin
  (volume=50 m³, height=4 m, inflowLevel=1.5, outflowLevel=0.2,
  overflowLevel=3.8, startLevel=1, stopLevel=0.5, minLevel=0.3,
  maxLevel=3.8). Old defaults left every level field null.

Visual bug fix
- src/editor/mode-preview.js: the level-based ramp curve in the editor
  was being drawn with foot=startLevel via buildPath(start, start, max).
  The runtime in control/levelBased.js has always used inflowLevel as
  the ramp foot. Pass buildPath(start, upFoot, max) where upFoot falls
  back to start when inflowLevel is missing, matching the runtime.

Manual mode observability
- src/specificClass.js: store last forwarded demand on this._manualDemand;
  surface as `mode` and `manualDemand` in getOutput(); call
  notifyOutputChanged() on forwardDemandToChildren and on changeMode so
  Port 0/1 emit even with no children registered. Status badge compacted
  to `mode | dir% | net m³/h` + `Qd=X m³/h` in manual mode.

Examples cleanup
- Drop stale 02-Integration.json, 03-Dashboard.json, basic-dashboard.flow.json,
  standalone-demo.js.
- 01-Basic.json: numbered driver groups (1. Control mode … 4. Calibration),
  Debug-outputs group, fixed typos and HOW-TO-USE; Port 1 debug now active.
- New 02-Dashboard.json: FlowFuse Dashboard 2.0 with Controls (7 buttons),
  Status (7 ui-text rows), Trends (4 ui-charts: level / volume / volume% /
  flow in-out-net), Raw output (ui-template dumping every Port 0 field).
  Fan-out function pattern-matches the 4-segment measurement keys by
  prefix instead of hardcoding childId, converts flow m³/s → m³/h, and
  caches last-known values so deltas never blank a row.
- examples/README.md realigned to the two-file set.

Wiki
- Home.md: 5 image placeholders replaced with the provided screenshots
  (01-node-and-editor, 02-basic-flow, 03-wiring-standalone,
  04-wiring-integrated) and the demo GIF (01-basic-demo).
- Reference-Examples.md: shipped-files table reduced to 01-Basic +
  02-Dashboard, Example-01 section uses the screenshot + GIF, Example-02
  rewritten as Dashboard (kept screenshot/GIF callouts open for those
  captures), Example-03/Integration sections + their debug-recipes row
  removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-12 14:52:00 +02:00
parent 8507ee4e02
commit fe5fa3577b
17 changed files with 1649 additions and 3168 deletions

View File

@@ -1,340 +1,479 @@
[
{
"id": "ps_basic_tab",
"type": "tab",
"label": "PumpingStation - Basic",
"disabled": false,
"info": "Tier 1: single pumpingStation node driven by inject nodes only. Demonstrates the canonical Phase-2 topic API: set.mode, set.inflow, set.demand."
},
{
"id": "ps_basic_title",
"type": "comment",
"z": "ps_basic_tab",
"name": "PumpingStation - Basic\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nA 50 m³ basin (3.5 m tall, inflow at 3.0 m, outflow at 0.2 m,\noverflow at 3.2 m). controlMode = levelbased, manual demand allowed\nonly when set.mode = manual.\n\nHOW TO USE:\n 1. Deploy the flow.\n 2. Click \"set.mode = manual\" so set.demand is honoured.\n 3. Click \"set.inflow = 60 m3/h\" to push wastewater into the basin.\n 4. Watch the basin fill on Port 0 (level, volume, percControl rise).\n 5. Click \"calibrate volume 25 m3\" to jump straight to half-full.\n\nAliases (changemode, q_in, Qd, …) still work but log a deprecation\nwarning - fresh flows use the canonical names.",
"info": "",
"x": 600,
"y": 40,
"wires": []
},
{
"id": "ps_basic_inj_mode",
"type": "inject",
"z": "ps_basic_tab",
"name": "set.mode = manual",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "manual",
"vt": "str"
}
],
"topic": "set.mode",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"x": 200,
"y": 160,
"wires": [
[
"ps_basic_node"
]
]
},
{
"id": "ps_basic_inj_mode_lvl",
"type": "inject",
"z": "ps_basic_tab",
"name": "set.mode = levelbased",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "levelbased",
"vt": "str"
}
],
"topic": "set.mode",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"x": 220,
"y": 200,
"wires": [
[
"ps_basic_node"
]
]
},
{
"id": "ps_basic_inj_inflow",
"type": "inject",
"z": "ps_basic_tab",
"name": "set.inflow = 60 m3/h",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "60",
"vt": "num"
}
],
"topic": "set.inflow",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"x": 200,
"y": 260,
"wires": [
[
"ps_basic_node"
]
]
},
{
"id": "ps_basic_inj_demand",
"type": "inject",
"z": "ps_basic_tab",
"name": "set.demand = 40 %",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "40",
"vt": "num"
}
],
"topic": "set.demand",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"x": 200,
"y": 300,
"wires": [
[
"ps_basic_node"
]
]
},
{
"id": "ps_basic_inj_calvol",
"type": "inject",
"z": "ps_basic_tab",
"name": "calibrate volume 25 m3",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "25",
"vt": "num"
}
],
"topic": "cmd.calibrate.volume",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"x": 220,
"y": 360,
"wires": [
[
"ps_basic_node"
]
]
},
{
"id": "ps_basic_inj_callvl",
"type": "inject",
"z": "ps_basic_tab",
"name": "calibrate level 1.5 m",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "1.5",
"vt": "num"
}
],
"topic": "cmd.calibrate.level",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"x": 220,
"y": 400,
"wires": [
[
"ps_basic_node"
]
]
},
{
"id": "ps_basic_node",
"type": "pumpingStation",
"z": "ps_basic_tab",
"name": "Pumping Station",
"simulator": false,
"basinVolume": 50,
"basinHeight": 3.5,
"inflowLevel": 3,
"outflowLevel": 0.2,
"overflowLevel": 3.2,
"defaultFluid": "wastewater",
"inletPipeDiameter": 0.3,
"outletPipeDiameter": 0.3,
"pipelineLength": 80,
"maxDischargeHead": 24,
"staticHead": 12,
"maxInflowRate": 200,
"temperatureReferenceDegC": 15,
"timeleftToFullOrEmptyThresholdSeconds": 0,
"enableDryRunProtection": true,
"enableOverfillProtection": true,
"dryRunThresholdPercent": 2,
"overfillThresholdPercent": 98,
"minHeightBasedOn": "outlet",
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"refHeight": "NAP",
"basinBottomRef": 1,
"uuid": "example-ps-001",
"supplier": "WBD-RD",
"category": "station",
"assetType": "pumpingstation",
"model": "demo-50m3",
"unit": "m3/h",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "atEquipment",
"positionIcon": "",
"hasDistance": false,
"distance": "",
"distanceUnit": "m",
"distanceDescription": "",
"controlMode": "levelbased",
"startLevel": 1.2,
"minLevel": 0.4,
"maxLevel": 2.8,
"flowSetpoint": null,
"flowDeadband": null,
"x": 1320,
"y": 300,
"wires": [
[
"ps_basic_format"
],
[
"ps_basic_dbg_influx"
],
[
"ps_basic_dbg_parent"
]
]
},
{
"id": "ps_basic_format",
"type": "function",
"z": "ps_basic_tab",
"name": "Merge deltas + format",
"func": "const p = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst cache = context.get('c') || {};\nObject.assign(cache, p);\ncontext.set('c', cache);\nfunction pick(prefix) {\n for (const k of Object.keys(cache)) if (k === prefix || k.indexOf(prefix + '.') === 0) {\n const v = Number(cache[k]); if (Number.isFinite(v)) return v;\n } return null;\n}\nconst vol = pick('volume.predicted.atequipment');\nconst lvl = pick('level.predicted.atequipment');\nconst flIn = pick('flow.predicted.in');\nmsg.payload = {\n state: cache.state || 'unknown',\n controlMode: cache.controlMode || cache.mode || 'n/a',\n direction: cache.direction || 'n/a',\n percControl: cache.percControl != null ? Number(cache.percControl).toFixed(1) + ' %' : 'n/a',\n volume: vol != null ? vol.toFixed(2) + ' m3' : 'n/a',\n volumePercent: cache.volumePercent != null ? Number(cache.volumePercent).toFixed(1) + ' %' : 'n/a',\n level: lvl != null ? lvl.toFixed(3) + ' m' : 'n/a',\n inflow: flIn != null ? (flIn * 3600).toFixed(1) + ' m3/h' : 'n/a',\n timeToFull: cache.timeToFull != null ? Number(cache.timeToFull).toFixed(0) + ' s' : 'n/a',\n timeToEmpty: cache.timeToEmpty != null ? Number(cache.timeToEmpty).toFixed(0) + ' s' : 'n/a'\n};\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1560,
"y": 280,
"wires": [
[
"ps_basic_dbg_process"
]
]
},
{
"id": "ps_basic_dbg_process",
"type": "debug",
"z": "ps_basic_tab",
"name": "Port 0: Process",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"x": 1800,
"y": 240,
"wires": []
},
{
"id": "ps_basic_dbg_influx",
"type": "debug",
"z": "ps_basic_tab",
"name": "Port 1: InfluxDB",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 1800,
"y": 320,
"wires": []
},
{
"id": "ps_basic_dbg_parent",
"type": "debug",
"z": "ps_basic_tab",
"name": "Port 2: Parent reg",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 1800,
"y": 380,
"wires": []
},
{
"id": "grp_ps_basic",
"type": "group",
"z": "ps_basic_tab",
"name": "Pumping Station (PC)",
"style": {
"label": true,
"stroke": "#000000",
"fill": "#0c99d9",
"fill-opacity": "0.10"
{
"id": "77f00aef1c966167",
"type": "tab",
"label": "PumpingStation - Basic",
"disabled": false,
"info": "Tier 1: single pumpingStation node driven by inject nodes only. Demonstrates the canonical Phase-2 topic API: set.mode, set.inflow, set.demand."
},
"nodes": [
"ps_basic_node",
"ps_basic_format"
],
"x": 1290,
"y": 230,
"w": 500,
"h": 140
}
]
{
"id": "aa3381b896eb2cfb",
"type": "group",
"z": "77f00aef1c966167",
"name": "Pumping Station (Process Cell)",
"style": {
"label": true,
"stroke": "#000000",
"fill": "#0c99d9",
"fill-opacity": "0.10"
},
"nodes": [
"8e78b6607deb33a7"
],
"x": 534,
"y": 351.5,
"w": 232,
"h": 97
},
{
"id": "4996420d47442fad",
"type": "group",
"z": "77f00aef1c966167",
"name": "1. Control mode",
"style": {
"stroke": "#666666",
"fill": "#ffdf7f",
"fill-opacity": "0.15",
"label": true,
"color": "#333333"
},
"nodes": [
"1155bbbde7c65363",
"e9bea0f95b557f5d"
],
"x": 94,
"y": 119,
"w": 272,
"h": 122
},
{
"id": "a9f9b38b0e00c1d7",
"type": "group",
"z": "77f00aef1c966167",
"name": "2. Flow signals (inflow / outflow)",
"style": {
"stroke": "#666666",
"fill": "#ffdf7f",
"fill-opacity": "0.15",
"label": true,
"color": "#333333"
},
"nodes": [
"7b2b5eb919b1ab15",
"3350187815774b95"
],
"x": 94,
"y": 279,
"w": 262,
"h": 122
},
{
"id": "42bf82c87d05f498",
"type": "group",
"z": "77f00aef1c966167",
"name": "3. Operator demand (manual mode only)",
"style": {
"stroke": "#666666",
"fill": "#ffdf7f",
"fill-opacity": "0.15",
"label": true,
"color": "#333333"
},
"nodes": [
"48c2262c345c46b9"
],
"x": 94,
"y": 479,
"w": 261,
"h": 82
},
{
"id": "234bdce20170061a",
"type": "group",
"z": "77f00aef1c966167",
"name": "4. Calibration",
"style": {
"stroke": "#666666",
"fill": "#ffdf7f",
"fill-opacity": "0.15",
"label": true,
"color": "#333333"
},
"nodes": [
"463eefdd54df89a5",
"2e0642275899fc79"
],
"x": 94,
"y": 599,
"w": 272,
"h": 122
},
{
"id": "f4ba4542514ed853",
"type": "group",
"z": "77f00aef1c966167",
"name": "Expected outputs",
"style": {
"stroke": "#666666",
"fill": "#d1d1d1",
"fill-opacity": "0.2",
"label": true,
"color": "#333333"
},
"nodes": [
"b2450e5ee2eebfaa",
"386af1ad8aa8ed12",
"c27c2655f199b530"
],
"x": 874,
"y": 299,
"w": 252,
"h": 202
},
{
"id": "b30af582f935bcb7",
"type": "comment",
"z": "77f00aef1c966167",
"name": "PumpingStation — Basic (Tier 1)",
"info": "Single pumpingStation node driven by inject buttons. Shows the canonical msg.topic command surface.\n\nDefault controlMode = levelbased. Switch to manual to honour set.demand.\n\nHOW TO USE\n1. Deploy the flow.\n2. (optional) Click \"set.mode = manual\" if you want set.demand to forward; otherwise leave it on levelbased and the ramp drives demand from level.\n3. Click \"set.inflow = 60 m³/h\" to push wastewater into the basin.\n4. Watch the basin fill on Port 0 (level, volume rise) and Port 1 (InfluxDB-shaped payload).\n5. In manual mode: click \"set.demand = 40\" — the value surfaces as `manualDemand` on Port 0/1 and in the node status badge.\n6. Click \"calibrate volume 25 m³\" or \"calibrate level 1.5 m\" to snap the predicted-volume integrator.\n\nPORTS\n- Port 0: process output (changed fields only)\n- Port 1: InfluxDB-shaped {measurement, fields, tags, timestamp}\n- Port 2: parent registration (child handshake)",
"x": 650,
"y": 300,
"wires": []
},
{
"id": "1155bbbde7c65363",
"type": "inject",
"z": "77f00aef1c966167",
"g": "4996420d47442fad",
"name": "set.mode = manual",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "manual",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"topic": "set.mode",
"x": 230,
"y": 160,
"wires": [
[
"8e78b6607deb33a7"
]
]
},
{
"id": "e9bea0f95b557f5d",
"type": "inject",
"z": "77f00aef1c966167",
"g": "4996420d47442fad",
"name": "set.mode = levelbased",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "levelbased",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"topic": "set.mode",
"x": 240,
"y": 200,
"wires": [
[
"8e78b6607deb33a7"
]
]
},
{
"id": "7b2b5eb919b1ab15",
"type": "inject",
"z": "77f00aef1c966167",
"g": "a9f9b38b0e00c1d7",
"name": "set.inflow = 60 m3/h",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "60",
"vt": "num"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"topic": "set.inflow",
"x": 240,
"y": 360,
"wires": [
[
"8e78b6607deb33a7"
]
]
},
{
"id": "48c2262c345c46b9",
"type": "inject",
"z": "77f00aef1c966167",
"g": "42bf82c87d05f498",
"name": "set.demand = 40 %",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "40",
"vt": "num"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"topic": "set.demand",
"x": 230,
"y": 520,
"wires": [
[
"8e78b6607deb33a7"
]
]
},
{
"id": "463eefdd54df89a5",
"type": "inject",
"z": "77f00aef1c966167",
"g": "234bdce20170061a",
"name": "calibrate volume 25 m3",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "25",
"vt": "num"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"topic": "cmd.calibrate.volume",
"x": 240,
"y": 640,
"wires": [
[
"8e78b6607deb33a7"
]
]
},
{
"id": "2e0642275899fc79",
"type": "inject",
"z": "77f00aef1c966167",
"g": "234bdce20170061a",
"name": "calibrate level 1.5 m",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "1.5",
"vt": "num"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"topic": "cmd.calibrate.level",
"x": 240,
"y": 680,
"wires": [
[
"8e78b6607deb33a7"
]
]
},
{
"id": "b2450e5ee2eebfaa",
"type": "debug",
"z": "77f00aef1c966167",
"g": "f4ba4542514ed853",
"name": "Port 0: Process",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"x": 980,
"y": 340,
"wires": []
},
{
"id": "386af1ad8aa8ed12",
"type": "debug",
"z": "77f00aef1c966167",
"g": "f4ba4542514ed853",
"name": "Port 1: InfluxDB",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 980,
"y": 400,
"wires": []
},
{
"id": "c27c2655f199b530",
"type": "debug",
"z": "77f00aef1c966167",
"g": "f4ba4542514ed853",
"name": "Port 2: Parent reg",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 990,
"y": 460,
"wires": []
},
{
"id": "8e78b6607deb33a7",
"type": "pumpingStation",
"z": "77f00aef1c966167",
"g": "aa3381b896eb2cfb",
"name": "",
"simulator": false,
"basinVolume": 50,
"basinHeight": 4,
"inflowLevel": 1.5,
"outflowLevel": 0.2,
"overflowLevel": 3.8,
"defaultFluid": "wastewater",
"inletPipeDiameter": 0.3,
"outletPipeDiameter": 0.3,
"pipelineLength": 80,
"maxDischargeHead": 24,
"staticHead": 12,
"maxInflowRate": 200,
"temperatureReferenceDegC": 15,
"timeleftToFullOrEmptyThresholdSeconds": 0,
"enableDryRunProtection": true,
"enableHighVolumeSafety": true,
"enableOverfillProtection": true,
"dryRunThresholdPercent": 2,
"highVolumeSafetyThresholdPercent": 98,
"overfillThresholdPercent": 98,
"minHeightBasedOn": "outlet",
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"refHeight": "NAP",
"basinBottomRef": 1,
"uuid": "",
"supplier": "",
"category": "",
"assetType": "",
"model": "",
"unit": "",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"positionIcon": "⊥",
"hasDistance": false,
"distance": "",
"controlMode": "levelbased",
"levelCurveType": "linear",
"logCurveFactor": 9,
"enableShiftedRamp": false,
"shiftLevel": 0,
"shiftArmPercent": 95,
"startLevel": 1,
"stopLevel": 0.5,
"minLevel": 0.20400000000000001,
"maxLevel": 3.8,
"flowSetpoint": null,
"flowDeadband": null,
"x": 650,
"y": 400,
"wires": [
[
"b2450e5ee2eebfaa"
],
[
"386af1ad8aa8ed12"
],
[
"c27c2655f199b530"
]
]
},
{
"id": "3350187815774b95",
"type": "inject",
"z": "77f00aef1c966167",
"g": "a9f9b38b0e00c1d7",
"name": "set.outflow= 80 m3/h",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"topic": "set.outflow",
"payload": "80",
"payloadType": "num",
"x": 230,
"y": 320,
"wires": [
[
"8e78b6607deb33a7"
]
]
},
{
"id": "ef77c1819422a098",
"type": "global-config",
"env": [],
"modules": {
"EVOLV": "1.0.29"
}
}
]

1070
examples/02-Dashboard.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,686 +0,0 @@
[
{
"id": "ps_int_proc",
"type": "tab",
"label": "Process Plant",
"disabled": false,
"info": "Tier 2: pumpingStation + measurement child + machineGroupControl parent with two rotatingMachine pumps. Demonstrates Phase-2 parent/child handshakes and the canonical set.mode/set.inflow/set.demand topics."
},
{
"id": "ps_int_setup",
"type": "tab",
"label": "Setup",
"disabled": false,
"info": "Deploy-time once-true injects that initialise control modes on the EVOLV nodes."
},
{
"id": "ps_int_title",
"type": "comment",
"z": "ps_int_proc",
"name": "PumpingStation - Integration\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nL0 link-ins | L2 level sensor (CM) | L3 pumps (EM) | L4 MGC (UN) | L5 station (PC).\nPumps register with MGC via Port 2; MGC and the level sensor register with the station via Port 2.\nCross-tab channels: setup:* drive once-true initialisation from the Setup tab.",
"info": "",
"x": 600,
"y": 40,
"wires": []
},
{
"id": "lin_setup_mode",
"type": "link in",
"z": "ps_int_proc",
"name": "setup:to-ps-mode",
"links": [],
"x": 120,
"y": 500,
"wires": [
[
"ps_int_station"
]
]
},
{
"id": "lin_setup_inflow",
"type": "link in",
"z": "ps_int_proc",
"name": "setup:to-ps-inflow",
"links": [],
"x": 120,
"y": 560,
"wires": [
[
"ps_int_station"
]
]
},
{
"id": "lin_setup_mgcmode",
"type": "link in",
"z": "ps_int_proc",
"name": "setup:to-mgc-mode",
"links": [],
"x": 120,
"y": 360,
"wires": [
[
"ps_int_mgc"
]
]
},
{
"id": "meas_level",
"type": "measurement",
"z": "ps_int_proc",
"name": "Basin level sensor",
"mode": "analog",
"channels": "[]",
"scaling": false,
"i_min": 0,
"i_max": 0,
"i_offset": 0,
"o_min": 0,
"o_max": 1,
"simulator": true,
"smooth_method": "mean",
"count": 5,
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"uuid": "example-level-001",
"supplier": "vega",
"category": "sensor",
"assetType": "level",
"model": "VEGAPULS-31",
"unit": "m",
"assetTagNumber": "LT-001",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 600,
"y": 700,
"wires": [
[
"ps_int_dbg_level"
],
[],
[
"ps_int_station"
]
]
},
{
"id": "ps_int_inj_level",
"type": "inject",
"z": "ps_int_proc",
"name": "sim level 1.6 m",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "1.6",
"vt": "num"
}
],
"topic": "measurement",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "",
"x": 120,
"y": 700,
"wires": [
[
"meas_level"
]
]
},
{
"id": "pump_a",
"type": "rotatingMachine",
"z": "ps_int_proc",
"name": "Pump A",
"speed": "1",
"startup": "2",
"warmup": "1",
"shutdown": "2",
"cooldown": "1",
"movementMode": "staticspeed",
"machineCurve": "",
"uuid": "example-pump-a",
"supplier": "hidrostal",
"category": "pump",
"assetType": "pump-centrifugal",
"model": "hidrostal-H05K-S03R",
"unit": "m3/h",
"curvePressureUnit": "mbar",
"curveFlowUnit": "m3/h",
"curvePowerUnit": "kW",
"curveControlUnit": "%",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"positionIcon": "",
"hasDistance": false,
"distance": "",
"distanceUnit": "m",
"distanceDescription": "",
"x": 840,
"y": 320,
"wires": [
[
"ps_int_dbg_pa"
],
[],
[
"ps_int_mgc"
]
]
},
{
"id": "pump_b",
"type": "rotatingMachine",
"z": "ps_int_proc",
"name": "Pump B",
"speed": "1",
"startup": "2",
"warmup": "1",
"shutdown": "2",
"cooldown": "1",
"movementMode": "staticspeed",
"machineCurve": "",
"uuid": "example-pump-b",
"supplier": "hidrostal",
"category": "pump",
"assetType": "pump-centrifugal",
"model": "hidrostal-H05K-S03R",
"unit": "m3/h",
"curvePressureUnit": "mbar",
"curveFlowUnit": "m3/h",
"curvePowerUnit": "kW",
"curveControlUnit": "%",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"positionIcon": "",
"hasDistance": false,
"distance": "",
"distanceUnit": "m",
"distanceDescription": "",
"x": 840,
"y": 400,
"wires": [
[
"ps_int_dbg_pb"
],
[],
[
"ps_int_mgc"
]
]
},
{
"id": "ps_int_mgc",
"type": "machineGroupControl",
"z": "ps_int_proc",
"name": "Pump Group",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "atEquipment",
"positionIcon": "",
"hasDistance": false,
"distance": "",
"distanceUnit": "m",
"x": 1080,
"y": 360,
"wires": [
[
"ps_int_dbg_mgc"
],
[],
[
"ps_int_station"
]
]
},
{
"id": "ps_int_station",
"type": "pumpingStation",
"z": "ps_int_proc",
"name": "Pumping Station",
"simulator": false,
"basinVolume": 50,
"basinHeight": 3.5,
"inflowLevel": 3,
"outflowLevel": 0.2,
"overflowLevel": 3.2,
"defaultFluid": "wastewater",
"inletPipeDiameter": 0.3,
"outletPipeDiameter": 0.3,
"pipelineLength": 80,
"maxDischargeHead": 24,
"staticHead": 12,
"maxInflowRate": 200,
"temperatureReferenceDegC": 15,
"timeleftToFullOrEmptyThresholdSeconds": 0,
"enableDryRunProtection": true,
"enableOverfillProtection": true,
"dryRunThresholdPercent": 2,
"overfillThresholdPercent": 98,
"minHeightBasedOn": "outlet",
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"refHeight": "NAP",
"basinBottomRef": 1,
"uuid": "example-ps-001",
"supplier": "WBD-RD",
"category": "station",
"assetType": "pumpingstation",
"model": "demo-50m3",
"unit": "m3/h",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "atEquipment",
"positionIcon": "",
"hasDistance": false,
"distance": "",
"distanceUnit": "m",
"distanceDescription": "",
"controlMode": "levelbased",
"startLevel": 1.2,
"minLevel": 0.4,
"maxLevel": 2.8,
"flowSetpoint": null,
"flowDeadband": null,
"x": 1320,
"y": 520,
"wires": [
[
"ps_int_format"
],
[
"ps_int_dbg_influx"
],
[]
]
},
{
"id": "ps_int_format",
"type": "function",
"z": "ps_int_proc",
"name": "Merge deltas + format",
"func": "const p = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst cache = context.get('c') || {}; Object.assign(cache, p); context.set('c', cache);\nfunction pick(prefix){ for (const k of Object.keys(cache)) if (k===prefix||k.indexOf(prefix+'.')===0){ const v=Number(cache[k]); if(Number.isFinite(v)) return v;} return null; }\nconst vol=pick('volume.predicted.atequipment'), lvl=pick('level.predicted.atequipment'), flIn=pick('flow.predicted.in'), flOut=pick('flow.predicted.out');\nmsg.payload = {\n state: cache.state || 'unknown',\n controlMode: cache.controlMode || cache.mode || 'n/a',\n direction: cache.direction || 'n/a',\n percControl: cache.percControl != null ? Number(cache.percControl).toFixed(1)+' %' : 'n/a',\n volume: vol != null ? vol.toFixed(2)+' m3' : 'n/a',\n volumePercent: cache.volumePercent != null ? Number(cache.volumePercent).toFixed(1)+' %' : 'n/a',\n level: lvl != null ? lvl.toFixed(3)+' m' : 'n/a',\n inflow: flIn != null ? (flIn*3600).toFixed(1)+' m3/h' : 'n/a',\n outflow: flOut != null ? (flOut*3600).toFixed(1)+' m3/h' : 'n/a',\n childCount: cache.childCount != null ? cache.childCount : 'n/a'\n};\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1560,
"y": 520,
"wires": [
[
"ps_int_dbg_process"
]
]
},
{
"id": "ps_int_dbg_process",
"type": "debug",
"z": "ps_int_proc",
"name": "PS Port 0: Process",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"x": 1800,
"y": 480,
"wires": []
},
{
"id": "ps_int_dbg_influx",
"type": "debug",
"z": "ps_int_proc",
"name": "PS Port 1: InfluxDB",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 1800,
"y": 540,
"wires": []
},
{
"id": "ps_int_dbg_mgc",
"type": "debug",
"z": "ps_int_proc",
"name": "MGC Port 0",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"x": 1800,
"y": 360,
"wires": []
},
{
"id": "ps_int_dbg_pa",
"type": "debug",
"z": "ps_int_proc",
"name": "Pump A Port 0",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"x": 1800,
"y": 320,
"wires": []
},
{
"id": "ps_int_dbg_pb",
"type": "debug",
"z": "ps_int_proc",
"name": "Pump B Port 0",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"x": 1800,
"y": 400,
"wires": []
},
{
"id": "ps_int_dbg_level",
"type": "debug",
"z": "ps_int_proc",
"name": "Level Port 0",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"x": 1800,
"y": 700,
"wires": []
},
{
"id": "grp_pumpa",
"type": "group",
"z": "ps_int_proc",
"name": "Pump A (EM)",
"style": {
"label": true,
"stroke": "#000000",
"fill": "#86bbdd",
"fill-opacity": "0.10"
},
"nodes": [
"pump_a",
"ps_int_dbg_pa"
],
"x": 815,
"y": 275,
"w": 1210,
"h": 110
},
{
"id": "grp_pumpb",
"type": "group",
"z": "ps_int_proc",
"name": "Pump B (EM)",
"style": {
"label": true,
"stroke": "#000000",
"fill": "#86bbdd",
"fill-opacity": "0.10"
},
"nodes": [
"pump_b",
"ps_int_dbg_pb"
],
"x": 815,
"y": 355,
"w": 1210,
"h": 110
},
{
"id": "grp_mgc",
"type": "group",
"z": "ps_int_proc",
"name": "Pump Group MGC (UN)",
"style": {
"label": true,
"stroke": "#000000",
"fill": "#50a8d9",
"fill-opacity": "0.10"
},
"nodes": [
"ps_int_mgc",
"ps_int_dbg_mgc",
"lin_setup_mgcmode"
],
"x": 95,
"y": 315,
"w": 1930,
"h": 110
},
{
"id": "grp_station",
"type": "group",
"z": "ps_int_proc",
"name": "Pumping Station (PC)",
"style": {
"label": true,
"stroke": "#000000",
"fill": "#0c99d9",
"fill-opacity": "0.10"
},
"nodes": [
"ps_int_station",
"ps_int_format",
"ps_int_dbg_process",
"ps_int_dbg_influx",
"lin_setup_mode",
"lin_setup_inflow"
],
"x": 95,
"y": 435,
"w": 1930,
"h": 190
},
{
"id": "grp_level",
"type": "group",
"z": "ps_int_proc",
"name": "Level Sensor (CM)",
"style": {
"label": true,
"stroke": "#000000",
"fill": "#a9daee",
"fill-opacity": "0.10"
},
"nodes": [
"meas_level",
"ps_int_inj_level",
"ps_int_dbg_level"
],
"x": 95,
"y": 655,
"w": 1930,
"h": 110
},
{
"id": "setup_title",
"type": "comment",
"z": "ps_int_setup",
"name": "Deploy-time setup\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nFires once after each deploy: pushes the canonical set.mode / set.inflow /\nset.demand topics across cross-tab channels into the Process Plant tab.",
"info": "",
"x": 600,
"y": 40,
"wires": []
},
{
"id": "setup_inj_mode",
"type": "inject",
"z": "ps_int_setup",
"name": "set.mode = levelbased",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "levelbased",
"vt": "str"
}
],
"topic": "set.mode",
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "0.5",
"x": 120,
"y": 160,
"wires": [
[
"lout_setup_mode"
]
]
},
{
"id": "setup_inj_mgcmode",
"type": "inject",
"z": "ps_int_setup",
"name": "MGC set.mode = auto",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "auto",
"vt": "str"
}
],
"topic": "set.mode",
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "0.5",
"x": 120,
"y": 220,
"wires": [
[
"lout_setup_mgcmode"
]
]
},
{
"id": "setup_inj_inflow",
"type": "inject",
"z": "ps_int_setup",
"name": "seed inflow 60 m3/h",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "60",
"vt": "num"
}
],
"topic": "set.inflow",
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "1.0",
"x": 120,
"y": 280,
"wires": [
[
"lout_setup_inflow"
]
]
},
{
"id": "lout_setup_mode",
"type": "link out",
"z": "ps_int_setup",
"name": "setup:to-ps-mode",
"mode": "link",
"links": [
"lin_setup_mode"
],
"x": 1800,
"y": 160,
"wires": []
},
{
"id": "lout_setup_mgcmode",
"type": "link out",
"z": "ps_int_setup",
"name": "setup:to-mgc-mode",
"mode": "link",
"links": [
"lin_setup_mgcmode"
],
"x": 1800,
"y": 220,
"wires": []
},
{
"id": "lout_setup_inflow",
"type": "link out",
"z": "ps_int_setup",
"name": "setup:to-ps-inflow",
"mode": "link",
"links": [
"lin_setup_inflow"
],
"x": 1800,
"y": 280,
"wires": []
},
{
"id": "grp_setup",
"type": "group",
"z": "ps_int_setup",
"name": "Deploy-time setup",
"style": {
"label": true,
"stroke": "#000000",
"fill": "#dddddd",
"fill-opacity": "0.10"
},
"nodes": [
"setup_inj_mode",
"setup_inj_mgcmode",
"setup_inj_inflow",
"lout_setup_mode",
"lout_setup_mgcmode",
"lout_setup_inflow"
],
"x": 95,
"y": 115,
"w": 1930,
"h": 230
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
# pumpingStation - Example Flows
Three Node-RED flows demonstrating the Phase-2 pumpingStation node on the
canonical topic API (`set.mode`, `set.inflow`, `set.demand`,
Node-RED flows demonstrating the Phase-2 pumpingStation node on the
canonical topic API (`set.mode`, `set.inflow`, `set.outflow`, `set.demand`,
`cmd.calibrate.volume`, `cmd.calibrate.level`). Legacy aliases
(`changemode`, `q_in`, `Qd`, `calibratePredictedVolume`,
(`changemode`, `q_in`, `q_out`, `Qd`, `calibratePredictedVolume`,
`calibratePredictedLevel`, `registerChild`) still work but log a
one-time deprecation warning; these fresh flows use the canonical names only.
@@ -12,15 +12,14 @@ one-time deprecation warning; these fresh flows use the canonical names only.
| File | Tier | Tabs | Purpose |
|---|---|---|---|
| `01-Basic.json` | 1 | Process Plant | Single pumpingStation driven by inject nodes - no parent, no dashboard. |
| `02-Integration.json` | 2 | Process Plant + Setup | Adds a `measurement` level child and a `machineGroupControl` parent with two `rotatingMachine` pumps. Demonstrates the Phase-2 parent/child handshake. |
| `03-Dashboard.json` | 3 | Process Plant + Dashboard UI + Setup | Tier 2 plumbing plus a FlowFuse Dashboard 2.0 page with 3 charts (flow / level / volume %), text widgets, and 2 controls (mode dropdown + demand slider). |
| `02-Dashboard.json` | 2 | Process Plant + Dashboard UI | Same command surface as Basic, but driven by FlowFuse Dashboard 2.0 widgets — `ui-button` controls + `ui-text` live status panel. |
## Prerequisites
- Node-RED with the EVOLV package installed (so the `pumpingStation`,
`measurement`, `machineGroupControl`, and `rotatingMachine` node
types are registered).
- For `03-Dashboard.json`: `@flowfuse/node-red-dashboard` (Dashboard 2.0).
- For `02-Dashboard.json`: `@flowfuse/node-red-dashboard` (Dashboard 2.0).
## How to load
@@ -46,28 +45,22 @@ import into their own tabs and can be deployed immediately.
5. Inject `cmd.calibrate.volume = 25 m3` to jump the predicted-volume
integrator to half-full.
## 02-Integration - what to try
1. Deploy. The Setup tab fires `set.mode = levelbased` to the station
and `set.mode = auto` to the MGC.
2. The two pumps register with the MGC via Port 2; the MGC and the level
sensor register with the station via Port 2. Watch the registration
debug taps to confirm.
3. The level inject pushes a 1.6 m measurement so the station sees a
non-zero starting level. Setup also seeds `set.inflow = 60 m3/h`.
4. The station's `controlMode = levelbased` then drives the MGC, which
dispatches to Pump A / Pump B.
## 03-Dashboard - what to try
## 02-Dashboard - what to try
1. Deploy.
2. Open the dashboard at `http://localhost:1880/dashboard/page/pumping-station`.
3. Use the **Control mode** dropdown to switch between `manual`,
`levelbased`, `flowbased`, `none`.
4. In manual mode, drag the **Manual demand** slider - the demand cascades
to the MGC and on to the pumps.
5. The three charts (flow, level, volume %) plot live data; the four text
widgets show state, percControl, direction, and time-to-empty.
2. Open the dashboard at `http://localhost:1880/dashboard/pumpingstation-basic`.
3. Click **Mode: Manual** or **Mode: Levelbased** in the Controls panel.
4. Click **Inflow 60 m³/h** to push wastewater into the basin — the Status
panel on the right shows level / volume / volume % rising.
5. In manual mode, click **Demand 40 m³/h** — the value surfaces as
`Manual demand` in the Status panel and in the node's status badge.
6. Use **Calibrate V = 25 m³** or **Calibrate L = 1.5 m** to snap the
predicted-volume integrator.
All buttons fire the same canonical `msg.topic` as the Basic flow's inject
nodes; the only difference is the trigger. The Live status panel is fed by
Port 0 via a small fan-out function that caches last-known values so
delta-only updates never blank a row.
## Layout conventions
@@ -88,12 +81,6 @@ These flows follow the EVOLV layout rule set in
## Regenerating
These flows are generated from `tools/build-examples.js`. Edit the
generator, never the JSON, then:
```bash
node nodes/pumpingStation/tools/build-examples.js
```
The script writes `01-Basic.json`, `02-Integration.json`, and
`03-Dashboard.json` into this directory.
The current example JSON files are hand-maintained. If you re-introduce a
generator, regenerate `01-Basic.json` and `02-Dashboard.json` from it
rather than editing the JSON directly.

View File

@@ -1,589 +0,0 @@
[
{
"id": "ps_tab_basic_dashboard",
"type": "tab",
"label": "PumpingStation Dashboard",
"disabled": false,
"info": "Basic level-based pumpingStation dashboard with basin trends and safety state."
},
{
"id": "ui_base_ps_basic",
"type": "ui-base",
"name": "EVOLV Demo",
"path": "/dashboard",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control"
],
"showPathInSidebar": false,
"headerContent": "page",
"navigationStyle": "default",
"titleBarStyle": "default"
},
{
"id": "ui_theme_ps_basic",
"type": "ui-theme",
"name": "EVOLV Pumping Theme",
"colors": {
"surface": "#ffffff",
"primary": "#0c99d9",
"bgPage": "#f1f3f5",
"groupBg": "#ffffff",
"groupOutline": "#cfd7de"
},
"sizes": {
"density": "default",
"pagePadding": "14px",
"groupGap": "14px",
"groupBorderRadius": "6px",
"widgetGap": "12px"
}
},
{
"id": "ui_page_ps_basic",
"type": "ui-page",
"name": "PumpingStation",
"ui": "ui_base_ps_basic",
"path": "/pumping-station",
"icon": "water_drop",
"layout": "grid",
"theme": "ui_theme_ps_basic",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "12"
}
],
"order": 1,
"className": ""
},
{
"id": "ui_group_ps_inputs",
"type": "ui-group",
"name": "Simulation Inputs",
"page": "ui_page_ps_basic",
"width": "4",
"height": "1",
"order": 1,
"showTitle": true,
"className": ""
},
{
"id": "ui_group_ps_trends",
"type": "ui-group",
"name": "Basin Trends",
"page": "ui_page_ps_basic",
"width": "8",
"height": "1",
"order": 2,
"showTitle": true,
"className": ""
},
{
"id": "ui_group_ps_state",
"type": "ui-group",
"name": "State",
"page": "ui_page_ps_basic",
"width": "12",
"height": "1",
"order": 3,
"showTitle": true,
"className": ""
},
{
"id": "ps_node_basic",
"type": "pumpingStation",
"z": "ps_tab_basic_dashboard",
"name": "PS Dashboard Demo",
"basinVolume": 50,
"basinHeight": 5,
"inflowLevel": 3,
"outflowLevel": 0.2,
"overflowLevel": 4.5,
"defaultFluid": "wastewater",
"inletPipeDiameter": 0.4,
"outletPipeDiameter": 0.3,
"pipelineLength": 80,
"maxDischargeHead": 24,
"staticHead": 12,
"maxInflowRate": 200,
"temperatureReferenceDegC": 15,
"timeleftToFullOrEmptyThresholdSeconds": 0,
"enableDryRunProtection": true,
"enableHighVolumeSafety": true,
"enableOverfillProtection": true,
"dryRunThresholdPercent": 2,
"highVolumeSafetyThresholdPercent": 98,
"overfillThresholdPercent": 98,
"minHeightBasedOn": "outlet",
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"refHeight": "NAP",
"basinBottomRef": 0,
"unit": "m3/h",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"controlMode": "levelbased",
"levelCurveType": "linear",
"logCurveFactor": 9,
"minLevel": 1,
"startLevel": 2,
"maxLevel": 4,
"x": 720,
"y": 260,
"wires": [
[
"ps_parse_output"
],
[
"ps_debug_influx"
],
[
"ps_debug_parent"
]
]
},
{
"id": "ps_calibrate_initial",
"type": "inject",
"z": "ps_tab_basic_dashboard",
"name": "Set start level 2 m",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "0.5",
"topic": "calibratePredictedLevel",
"payload": "2",
"payloadType": "num",
"x": 180,
"y": 180,
"wires": [
[
"ps_node_basic"
]
]
},
{
"id": "ps_auto_inflow",
"type": "inject",
"z": "ps_tab_basic_dashboard",
"name": "Auto inflow 0.008 m3/s",
"props": [
{
"p": "payload"
}
],
"repeat": "1",
"crontab": "",
"once": true,
"onceDelay": "1",
"topic": "",
"payload": "0.008",
"payloadType": "num",
"x": 180,
"y": 240,
"wires": [
[
"ps_build_qin"
]
]
},
{
"id": "ps_inflow_input",
"type": "ui-number-input",
"z": "ps_tab_basic_dashboard",
"group": "ui_group_ps_inputs",
"name": "Inflow",
"label": "Inflow (m3/s)",
"order": 1,
"width": "4",
"height": "1",
"passthru": true,
"topic": "",
"min": 0,
"max": 0.05,
"step": 0.001,
"x": 190,
"y": 300,
"wires": [
[
"ps_build_qin"
]
]
},
{
"id": "ps_build_qin",
"type": "function",
"z": "ps_tab_basic_dashboard",
"name": "Build q_in",
"func": "msg.topic = 'q_in';\nmsg.unit = 'm3/s';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 440,
"y": 260,
"wires": [
[
"ps_node_basic"
]
]
},
{
"id": "ps_outflow_input",
"type": "ui-number-input",
"z": "ps_tab_basic_dashboard",
"group": "ui_group_ps_inputs",
"name": "Outflow",
"label": "Outflow (m3/s)",
"order": 2,
"width": "4",
"height": "1",
"passthru": true,
"topic": "",
"min": 0,
"max": 0.05,
"step": 0.001,
"x": 190,
"y": 360,
"wires": [
[
"ps_build_qout"
]
]
},
{
"id": "ps_build_qout",
"type": "function",
"z": "ps_tab_basic_dashboard",
"name": "Build q_out",
"func": "msg.topic = 'q_out';\nmsg.unit = 'm3/s';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 440,
"y": 360,
"wires": [
[
"ps_node_basic"
]
]
},
{
"id": "ps_parse_output",
"type": "function",
"z": "ps_tab_basic_dashboard",
"name": "Parse PS output",
"func": "// MeasurementContainer flat keys are `${type}.${variant}.${position}.${childId}`.\n// When PS writes without an explicit .child(), the childId is the literal\n// string 'default' — DON'T strip it. See generalFunctions/src/measurements/\n// MeasurementContainer.js getFlattenedOutput for details.\nconst fields = (msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst snapshot = Object.assign({}, context.get('snapshot') || {}, fields);\ncontext.set('snapshot', snapshot);\nconst firstFinite = (...keys) => {\n for (const key of keys) {\n const value = Number(snapshot[key]);\n if (Number.isFinite(value)) return value;\n }\n return null;\n};\nconst level = firstFinite('level.predicted.atequipment.default', 'level.measured.atequipment.default');\nconst volume = firstFinite('volume.predicted.atequipment.default', 'volume.measured.atequipment.default');\nconst netFlow = firstFinite('netFlowRate.predicted.atequipment.default', 'netFlowRate.measured.atequipment.default');\nconst demand = firstFinite('percControl');\nconst safety = snapshot.safetyState || 'normal';\nconst direction = snapshot.direction || 'unknown';\nconst overflow = snapshot.isOverflowing === true || snapshot.isOverflowing === 'true';\nconst timeleft = Number(snapshot.timeleft);\nconst fmt = (value, digits = 2) => Number.isFinite(value) ? value.toFixed(digits) : '-';\nreturn [\n level == null ? null : { topic: 'level', payload: level },\n volume == null ? null : { topic: 'volume', payload: volume },\n demand == null ? null : { topic: 'demand', payload: demand },\n netFlow == null ? null : { topic: 'net_flow', payload: netFlow },\n { topic: 'safety', payload: `${safety} | overflowing=${overflow}` },\n { topic: 'snapshot', payload: `level=${fmt(level)} m | volume=${fmt(volume)} m3 | demand=${fmt(demand, 0)}% | direction=${direction} | t=${Number.isFinite(timeleft) ? Math.round(timeleft) + ' s' : '-'}` }\n];",
"outputs": 6,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 980,
"y": 220,
"wires": [
[
"ps_chart_level"
],
[
"ps_chart_volume"
],
[
"ps_chart_demand"
],
[
"ps_chart_netflow"
],
[
"ps_text_safety"
],
[
"ps_text_snapshot"
]
]
},
{
"id": "ps_chart_level",
"type": "ui-chart",
"z": "ps_tab_basic_dashboard",
"group": "ui_group_ps_trends",
"name": "Level",
"label": "Level (m)",
"order": 1,
"width": 4,
"height": 4,
"chartType": "line",
"category": "topic",
"xAxisType": "time",
"yAxisLabel": "m",
"removeOlder": "15",
"removeOlderUnit": "60",
"x": 1230,
"y": 140,
"wires": [],
"showLegend": false,
"categoryType": "msg",
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"xAxisFormat": "",
"xAxisFormatType": "auto",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"xmin": "",
"xmax": "",
"ymin": "0",
"ymax": "5",
"bins": 10,
"action": "append",
"stackSeries": false,
"pointShape": "circle",
"pointRadius": 4,
"interpolation": "linear",
"className": "",
"colors": [
"#0c99d9"
],
"textColor": [
"#666666"
],
"textColorDefault": true,
"gridColor": [
"#e5e5e5"
],
"gridColorDefault": true
},
{
"id": "ps_chart_volume",
"type": "ui-chart",
"z": "ps_tab_basic_dashboard",
"group": "ui_group_ps_trends",
"name": "Volume",
"label": "Volume (m3)",
"order": 2,
"width": 4,
"height": 4,
"chartType": "line",
"category": "topic",
"xAxisType": "time",
"yAxisLabel": "m3",
"removeOlder": "15",
"removeOlderUnit": "60",
"x": 1230,
"y": 200,
"wires": [],
"showLegend": false,
"categoryType": "msg",
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"xAxisFormat": "",
"xAxisFormatType": "auto",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"xmin": "",
"xmax": "",
"ymin": "0",
"ymax": "50",
"bins": 10,
"action": "append",
"stackSeries": false,
"pointShape": "circle",
"pointRadius": 4,
"interpolation": "linear",
"className": "",
"colors": [
"#2ca02c"
],
"textColor": [
"#666666"
],
"textColorDefault": true,
"gridColor": [
"#e5e5e5"
],
"gridColorDefault": true
},
{
"id": "ps_chart_demand",
"type": "ui-chart",
"z": "ps_tab_basic_dashboard",
"group": "ui_group_ps_trends",
"name": "Demand",
"label": "Demand (%)",
"order": 3,
"width": 4,
"height": 4,
"chartType": "line",
"category": "topic",
"xAxisType": "time",
"yAxisLabel": "%",
"removeOlder": "15",
"removeOlderUnit": "60",
"x": 1230,
"y": 260,
"wires": [],
"showLegend": false,
"categoryType": "msg",
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"xAxisFormat": "",
"xAxisFormatType": "auto",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"xmin": "",
"xmax": "",
"ymin": "0",
"ymax": "120",
"bins": 10,
"action": "append",
"stackSeries": false,
"pointShape": "circle",
"pointRadius": 4,
"interpolation": "linear",
"className": "",
"colors": [
"#d68910"
],
"textColor": [
"#666666"
],
"textColorDefault": true,
"gridColor": [
"#e5e5e5"
],
"gridColorDefault": true
},
{
"id": "ps_chart_netflow",
"type": "ui-chart",
"z": "ps_tab_basic_dashboard",
"group": "ui_group_ps_trends",
"name": "Net Flow",
"label": "Net flow (m3/s)",
"order": 4,
"width": 4,
"height": 4,
"chartType": "line",
"category": "topic",
"xAxisType": "time",
"yAxisLabel": "m3/s",
"removeOlder": "15",
"removeOlderUnit": "60",
"x": 1240,
"y": 320,
"wires": [],
"showLegend": false,
"categoryType": "msg",
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"xAxisFormat": "",
"xAxisFormatType": "auto",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"xmin": "",
"xmax": "",
"ymin": "",
"ymax": "",
"bins": 10,
"action": "append",
"stackSeries": false,
"pointShape": "circle",
"pointRadius": 4,
"interpolation": "linear",
"className": "",
"colors": [
"#9467bd"
],
"textColor": [
"#666666"
],
"textColorDefault": true,
"gridColor": [
"#e5e5e5"
],
"gridColorDefault": true
},
{
"id": "ps_text_safety",
"type": "ui-text",
"z": "ps_tab_basic_dashboard",
"group": "ui_group_ps_state",
"name": "Safety",
"label": "Safety",
"order": 1,
"width": 4,
"height": 1,
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 1230,
"y": 380,
"wires": []
},
{
"id": "ps_text_snapshot",
"type": "ui-text",
"z": "ps_tab_basic_dashboard",
"group": "ui_group_ps_state",
"name": "Snapshot",
"label": "Snapshot",
"order": 2,
"width": 8,
"height": 1,
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 1240,
"y": 440,
"wires": []
},
{
"id": "ps_debug_influx",
"type": "debug",
"z": "ps_tab_basic_dashboard",
"name": "Influx output",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 980,
"y": 320,
"wires": []
},
{
"id": "ps_debug_parent",
"type": "debug",
"z": "ps_tab_basic_dashboard",
"name": "Parent output",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 980,
"y": 380,
"wires": []
}
]

View File

@@ -1,57 +0,0 @@
/**
* Standalone PumpingStation demo — run with `node examples/standalone-demo.js`.
* Builds a station + one pump, calibrates predicted volume, ticks once.
* Useful for sanity-checking the orchestrator without Node-RED.
*/
const PumpingStation = require('../src/specificClass');
const RotatingMachine = require('../../rotatingMachine/src/specificClass');
function createPumpingStationConfig(name) {
return {
general: {
logging: { enabled: true, logLevel: 'debug' },
name,
id: `${name}-${Date.now()}`,
flowThreshold: 1e-4,
},
functionality: { softwareType: 'pumpingStation', role: 'stationcontroller' },
basin: { volume: 43.75, height: 10, inflowLevel: 3, outflowLevel: 0.2, overflowLevel: 3.2 },
hydraulics: { refHeight: 'NAP', basinBottomRef: 0 },
safety: { enableDryRunProtection: false, enableOverfillProtection: false },
};
}
function createMachineConfig(name, position) {
return {
general: { name, logging: { enabled: false, logLevel: 'debug' } },
functionality: { softwareType: 'machine', positionVsParent: position },
asset: { supplier: 'Hydrostal', type: 'pump', category: 'centrifugal', model: 'hidrostal-H05K-S03R' },
};
}
function createMachineStateConfig() {
return {
general: { logging: { enabled: true, logLevel: 'debug' } },
movement: { speed: 1 },
time: { starting: 2, warmingup: 3, stopping: 2, coolingdown: 3 },
};
}
(async function demo() {
const station = new PumpingStation(createPumpingStationConfig('PumpingStationDemo'));
const pump1 = new RotatingMachine(createMachineConfig('Pump1', 'downstream'), createMachineStateConfig());
station.childRegistrationUtils.registerChild(pump1, 'machine');
setInterval(() => station.tick(), 1000);
await new Promise((resolve) => setTimeout(resolve, 10));
console.log('Initial state:', station.state);
station.setManualInflow(300, Date.now(), 'l/s');
station.calibratePredictedVolume(3.4);
console.log('Station state:', station.state);
console.log('Station output:', station.getOutput());
})().catch((err) => {
console.error('Demo failed:', err);
});