Files
EVOLV/examples/pumpingstation-3pumps-dashboard/flow.json
znetsixe 0d7af6bfff
Some checks failed
CI / lint-and-test (push) Has been cancelled
refactor(examples): split pumpingstation demo across 4 concern-based tabs + add layout rule set
The demo was a single 96-node tab with everything wired directly. Now
4 tabs wired only through named link-out / link-in pairs, and a
permanent rule set for future Claude sessions to follow.

Tabs (by concern, not by data flow):

  🏭 Process Plant   only EVOLV nodes (3 pumps + MGC + PS + 6 measurements)
                     + per-node output formatters
  📊 Dashboard UI   only ui-* widgets, button/setpoint wrappers, trend
                     splitters
  🎛️ Demo Drivers   random demand generator + state holder. Removable
                     in production
  ⚙️ Setup & Init   one-shot deploy-time injects (mode, scaling,
                     auto-startup, random-on)

Cross-tab wiring uses a fixed named-channel contract (cmd:demand,
cmd:mode, cmd:setpoint-A, evt:pump-A, etc.) — multiple emitters can
target a single link-in for fan-in, e.g. both the slider and the random
generator feed cmd:demand.

Bug fixes folded in:

1. Trend chart was empty / scrambled. Root cause: the trend-feeder
   function had ONE output that wired to BOTH flow and power charts,
   so each chart received both flow and power msgs and the legend
   garbled. Now: 2 outputs (flow → flow chart, power → power chart),
   one msg per output.

2. Every ui-text and ui-chart fell on the (0, 0) corner of the editor
   canvas. Root cause: the helper functions accepted x/y parameters
   but never assigned them on the returned node dict — Node-RED
   defaulted every widget to (0, 0) and they piled on top of each
   other. The dashboard render was unaffected (it lays out by group/
   order), but the editor was unreadable. Fixed both helpers and added
   a verification step ("no node should be at (0, 0)") to the rule set.

Spacing convention (now codified):
- 6 lanes per tab at x = [120, 380, 640, 900, 1160, 1420]
- 80 px standard row pitch, 30-40 px for tight ui-text stacks
- 200 px gap between sections, with a comment header per section

New rule set: .claude/rules/node-red-flow-layout.md
- Tab boundaries by concern
- Link-channel naming convention (cmd:/evt:/setup: prefixes)
- Spacing constants
- Trend-split chart pattern
- Inject node payload typing pitfall (per-prop v/vt)
- Dashboard widget rules (every ui-* needs x/y!)
- Do/don't checklist
- Link-out/link-in JSON cheat sheet
- 5-step layout verification before declaring a flow done

CLAUDE.md updated to point at the new rule set.

Verified end-to-end on Dockerized Node-RED 2026-04-13: 168 nodes across
4 tabs, all wired via 22 link-out / 19 link-in pairs, no nodes at
(0, 0), pumps reach operational ~5 s after deploy, MGC distributes
random demand, trends populate per pump.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:13:27 +02:00

3349 lines
72 KiB
JSON

[
{
"id": "tab_process",
"type": "tab",
"label": "\ud83c\udfed Process Plant",
"disabled": false,
"info": "EVOLV plant model: pumpingStation + machineGroupControl + 3 rotatingMachines, each with upstream and downstream pressure measurements.\n\nReceives commands via link-in nodes from the Dashboard / Demo Drivers tabs. Emits per-pump status via link-out per pump.\n\nNo UI, no demo drivers, no one-shot setup logic on this tab \u2014 those live on their own tabs so this layer can be lifted into production unchanged."
},
{
"id": "c_process_title",
"type": "comment",
"z": "tab_process",
"name": "\ud83c\udfed PROCESS PLANT \u2014 EVOLV nodes only",
"info": "Per pump: 2 measurement sensors \u2192 rotatingMachine \u2192 output formatter \u2192 link-out to dashboard.\nMGC orchestrates 3 pumps. PS observes basin (manual mode for the demo).\nAll cross-tab wires are link-in / link-out by named channel.",
"x": 640,
"y": 20,
"wires": []
},
{
"id": "c_pump_a",
"type": "comment",
"z": "tab_process",
"name": "\u2500\u2500 Pump A \u2500\u2500",
"info": "Up + Dn pressure sensors register as children. rotatingMachine emits state on port 0 (formatted then link-out to UI). Port 2 emits registerChild \u2192 MGC.",
"x": 640,
"y": 100,
"wires": []
},
{
"id": "meas_pump_a_u",
"type": "measurement",
"z": "tab_process",
"name": "PT-A-Up",
"mode": "analog",
"channels": "[]",
"scaling": false,
"i_min": 0,
"i_max": 1,
"i_offset": 0,
"o_min": 50,
"o_max": 400,
"simulator": true,
"smooth_method": "mean",
"count": "5",
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"uuid": "sensor-pump_a-upstream",
"supplier": "vega",
"category": "sensor",
"assetType": "pressure",
"model": "vega-pressure-10",
"unit": "mbar",
"assetTagNumber": "PT-1-U",
"enableLog": false,
"logLevel": "warn",
"positionVsParent": "upstream",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 380,
"y": 140,
"wires": [
[],
[],
[
"pump_a"
]
]
},
{
"id": "meas_pump_a_d",
"type": "measurement",
"z": "tab_process",
"name": "PT-A-Dn",
"mode": "analog",
"channels": "[]",
"scaling": false,
"i_min": 0,
"i_max": 1,
"i_offset": 0,
"o_min": 800,
"o_max": 2200,
"simulator": true,
"smooth_method": "mean",
"count": "5",
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"uuid": "sensor-pump_a-downstream",
"supplier": "vega",
"category": "sensor",
"assetType": "pressure",
"model": "vega-pressure-10",
"unit": "mbar",
"assetTagNumber": "PT-1-D",
"enableLog": false,
"logLevel": "warn",
"positionVsParent": "downstream",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 380,
"y": 190,
"wires": [
[],
[],
[
"pump_a"
]
]
},
{
"id": "lin_setpoint_pump_a",
"type": "link in",
"z": "tab_process",
"name": "cmd:setpoint-A",
"links": [
"lout_setpoint_pump_a_dash"
],
"x": 120,
"y": 160,
"wires": [
[
"build_setpoint_pump_a"
]
]
},
{
"id": "build_setpoint_pump_a",
"type": "function",
"z": "tab_process",
"name": "build setpoint cmd (Pump A)",
"func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'GUI', action: 'execMovement', setpoint: Number(msg.payload) };\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 600,
"y": 160,
"wires": [
[
"pump_a"
]
]
},
{
"id": "lin_seq_pump_a",
"type": "link in",
"z": "tab_process",
"name": "cmd:pump-A-seq",
"links": [
"lout_seq_pump_a_dash"
],
"x": 120,
"y": 210,
"wires": [
[
"pump_a"
]
]
},
{
"id": "pump_a",
"type": "rotatingMachine",
"z": "tab_process",
"name": "Pump A",
"speed": "10",
"startup": "2",
"warmup": "1",
"shutdown": "2",
"cooldown": "1",
"movementMode": "staticspeed",
"machineCurve": "",
"uuid": "pump-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": "warn",
"positionVsParent": "atEquipment",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 900,
"y": 180,
"wires": [
[
"format_pump_a"
],
[],
[
"mgc_pumps"
]
]
},
{
"id": "format_pump_a",
"type": "function",
"z": "tab_process",
"name": "format Pump A 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 flow = find('flow.predicted.downstream.');\nconst power = find('power.predicted.atequipment.');\nconst pU = find('pressure.measured.upstream.');\nconst pD = find('pressure.measured.downstream.');\nmsg.payload = {\n state: c.state || 'idle',\n mode: c.mode || 'auto',\n ctrl: c.ctrl != null ? Number(c.ctrl).toFixed(1) + '%' : 'n/a',\n flow: flow != null ? Number(flow).toFixed(1) + ' m\u00b3/h' : 'n/a',\n power: power != null ? Number(power).toFixed(2) + ' kW' : 'n/a',\n pUp: pU != null ? Number(pU).toFixed(0) : 'n/a',\n pDn: pD != null ? Number(pD).toFixed(0) : 'n/a',\n flowNum: flow != null ? Number(flow) : null,\n powerNum: power != null ? Number(power) : null,\n};\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1160,
"y": 180,
"wires": [
[
"lout_evt_pump_a"
]
]
},
{
"id": "lout_evt_pump_a",
"type": "link out",
"z": "tab_process",
"name": "evt:pump-A",
"mode": "link",
"links": [
"lin_evt_pump_a_dash"
],
"x": 1420,
"y": 180,
"wires": []
},
{
"id": "c_pump_b",
"type": "comment",
"z": "tab_process",
"name": "\u2500\u2500 Pump B \u2500\u2500",
"info": "Up + Dn pressure sensors register as children. rotatingMachine emits state on port 0 (formatted then link-out to UI). Port 2 emits registerChild \u2192 MGC.",
"x": 640,
"y": 300,
"wires": []
},
{
"id": "meas_pump_b_u",
"type": "measurement",
"z": "tab_process",
"name": "PT-B-Up",
"mode": "analog",
"channels": "[]",
"scaling": false,
"i_min": 0,
"i_max": 1,
"i_offset": 0,
"o_min": 50,
"o_max": 400,
"simulator": true,
"smooth_method": "mean",
"count": "5",
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"uuid": "sensor-pump_b-upstream",
"supplier": "vega",
"category": "sensor",
"assetType": "pressure",
"model": "vega-pressure-10",
"unit": "mbar",
"assetTagNumber": "PT-2-U",
"enableLog": false,
"logLevel": "warn",
"positionVsParent": "upstream",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 380,
"y": 340,
"wires": [
[],
[],
[
"pump_b"
]
]
},
{
"id": "meas_pump_b_d",
"type": "measurement",
"z": "tab_process",
"name": "PT-B-Dn",
"mode": "analog",
"channels": "[]",
"scaling": false,
"i_min": 0,
"i_max": 1,
"i_offset": 0,
"o_min": 800,
"o_max": 2200,
"simulator": true,
"smooth_method": "mean",
"count": "5",
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"uuid": "sensor-pump_b-downstream",
"supplier": "vega",
"category": "sensor",
"assetType": "pressure",
"model": "vega-pressure-10",
"unit": "mbar",
"assetTagNumber": "PT-2-D",
"enableLog": false,
"logLevel": "warn",
"positionVsParent": "downstream",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 380,
"y": 390,
"wires": [
[],
[],
[
"pump_b"
]
]
},
{
"id": "lin_setpoint_pump_b",
"type": "link in",
"z": "tab_process",
"name": "cmd:setpoint-B",
"links": [
"lout_setpoint_pump_b_dash"
],
"x": 120,
"y": 360,
"wires": [
[
"build_setpoint_pump_b"
]
]
},
{
"id": "build_setpoint_pump_b",
"type": "function",
"z": "tab_process",
"name": "build setpoint cmd (Pump B)",
"func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'GUI', action: 'execMovement', setpoint: Number(msg.payload) };\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 600,
"y": 360,
"wires": [
[
"pump_b"
]
]
},
{
"id": "lin_seq_pump_b",
"type": "link in",
"z": "tab_process",
"name": "cmd:pump-B-seq",
"links": [
"lout_seq_pump_b_dash"
],
"x": 120,
"y": 410,
"wires": [
[
"pump_b"
]
]
},
{
"id": "pump_b",
"type": "rotatingMachine",
"z": "tab_process",
"name": "Pump B",
"speed": "10",
"startup": "2",
"warmup": "1",
"shutdown": "2",
"cooldown": "1",
"movementMode": "staticspeed",
"machineCurve": "",
"uuid": "pump-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": "warn",
"positionVsParent": "atEquipment",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 900,
"y": 380,
"wires": [
[
"format_pump_b"
],
[],
[
"mgc_pumps"
]
]
},
{
"id": "format_pump_b",
"type": "function",
"z": "tab_process",
"name": "format Pump B 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 flow = find('flow.predicted.downstream.');\nconst power = find('power.predicted.atequipment.');\nconst pU = find('pressure.measured.upstream.');\nconst pD = find('pressure.measured.downstream.');\nmsg.payload = {\n state: c.state || 'idle',\n mode: c.mode || 'auto',\n ctrl: c.ctrl != null ? Number(c.ctrl).toFixed(1) + '%' : 'n/a',\n flow: flow != null ? Number(flow).toFixed(1) + ' m\u00b3/h' : 'n/a',\n power: power != null ? Number(power).toFixed(2) + ' kW' : 'n/a',\n pUp: pU != null ? Number(pU).toFixed(0) : 'n/a',\n pDn: pD != null ? Number(pD).toFixed(0) : 'n/a',\n flowNum: flow != null ? Number(flow) : null,\n powerNum: power != null ? Number(power) : null,\n};\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1160,
"y": 380,
"wires": [
[
"lout_evt_pump_b"
]
]
},
{
"id": "lout_evt_pump_b",
"type": "link out",
"z": "tab_process",
"name": "evt:pump-B",
"mode": "link",
"links": [
"lin_evt_pump_b_dash"
],
"x": 1420,
"y": 380,
"wires": []
},
{
"id": "c_pump_c",
"type": "comment",
"z": "tab_process",
"name": "\u2500\u2500 Pump C \u2500\u2500",
"info": "Up + Dn pressure sensors register as children. rotatingMachine emits state on port 0 (formatted then link-out to UI). Port 2 emits registerChild \u2192 MGC.",
"x": 640,
"y": 500,
"wires": []
},
{
"id": "meas_pump_c_u",
"type": "measurement",
"z": "tab_process",
"name": "PT-C-Up",
"mode": "analog",
"channels": "[]",
"scaling": false,
"i_min": 0,
"i_max": 1,
"i_offset": 0,
"o_min": 50,
"o_max": 400,
"simulator": true,
"smooth_method": "mean",
"count": "5",
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"uuid": "sensor-pump_c-upstream",
"supplier": "vega",
"category": "sensor",
"assetType": "pressure",
"model": "vega-pressure-10",
"unit": "mbar",
"assetTagNumber": "PT-3-U",
"enableLog": false,
"logLevel": "warn",
"positionVsParent": "upstream",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 380,
"y": 540,
"wires": [
[],
[],
[
"pump_c"
]
]
},
{
"id": "meas_pump_c_d",
"type": "measurement",
"z": "tab_process",
"name": "PT-C-Dn",
"mode": "analog",
"channels": "[]",
"scaling": false,
"i_min": 0,
"i_max": 1,
"i_offset": 0,
"o_min": 800,
"o_max": 2200,
"simulator": true,
"smooth_method": "mean",
"count": "5",
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"uuid": "sensor-pump_c-downstream",
"supplier": "vega",
"category": "sensor",
"assetType": "pressure",
"model": "vega-pressure-10",
"unit": "mbar",
"assetTagNumber": "PT-3-D",
"enableLog": false,
"logLevel": "warn",
"positionVsParent": "downstream",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 380,
"y": 590,
"wires": [
[],
[],
[
"pump_c"
]
]
},
{
"id": "lin_setpoint_pump_c",
"type": "link in",
"z": "tab_process",
"name": "cmd:setpoint-C",
"links": [
"lout_setpoint_pump_c_dash"
],
"x": 120,
"y": 560,
"wires": [
[
"build_setpoint_pump_c"
]
]
},
{
"id": "build_setpoint_pump_c",
"type": "function",
"z": "tab_process",
"name": "build setpoint cmd (Pump C)",
"func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'GUI', action: 'execMovement', setpoint: Number(msg.payload) };\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 600,
"y": 560,
"wires": [
[
"pump_c"
]
]
},
{
"id": "lin_seq_pump_c",
"type": "link in",
"z": "tab_process",
"name": "cmd:pump-C-seq",
"links": [
"lout_seq_pump_c_dash"
],
"x": 120,
"y": 610,
"wires": [
[
"pump_c"
]
]
},
{
"id": "pump_c",
"type": "rotatingMachine",
"z": "tab_process",
"name": "Pump C",
"speed": "10",
"startup": "2",
"warmup": "1",
"shutdown": "2",
"cooldown": "1",
"movementMode": "staticspeed",
"machineCurve": "",
"uuid": "pump-pump_c",
"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": "warn",
"positionVsParent": "atEquipment",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 900,
"y": 580,
"wires": [
[
"format_pump_c"
],
[],
[
"mgc_pumps"
]
]
},
{
"id": "format_pump_c",
"type": "function",
"z": "tab_process",
"name": "format Pump C 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 flow = find('flow.predicted.downstream.');\nconst power = find('power.predicted.atequipment.');\nconst pU = find('pressure.measured.upstream.');\nconst pD = find('pressure.measured.downstream.');\nmsg.payload = {\n state: c.state || 'idle',\n mode: c.mode || 'auto',\n ctrl: c.ctrl != null ? Number(c.ctrl).toFixed(1) + '%' : 'n/a',\n flow: flow != null ? Number(flow).toFixed(1) + ' m\u00b3/h' : 'n/a',\n power: power != null ? Number(power).toFixed(2) + ' kW' : 'n/a',\n pUp: pU != null ? Number(pU).toFixed(0) : 'n/a',\n pDn: pD != null ? Number(pD).toFixed(0) : 'n/a',\n flowNum: flow != null ? Number(flow) : null,\n powerNum: power != null ? Number(power) : null,\n};\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1160,
"y": 580,
"wires": [
[
"lout_evt_pump_c"
]
]
},
{
"id": "lout_evt_pump_c",
"type": "link out",
"z": "tab_process",
"name": "evt:pump-C",
"mode": "link",
"links": [
"lin_evt_pump_c_dash"
],
"x": 1420,
"y": 580,
"wires": []
},
{
"id": "c_mgc",
"type": "comment",
"z": "tab_process",
"name": "\u2500\u2500 MGC \u2500\u2500 (orchestrates the 3 pumps via optimalcontrol)",
"info": "Receives Qd from cmd:demand link-in. Distributes flow across pumps.",
"x": 640,
"y": 700,
"wires": []
},
{
"id": "lin_demand_to_mgc",
"type": "link in",
"z": "tab_process",
"name": "cmd:demand",
"links": [
"lout_demand_drivers",
"lout_demand_dash"
],
"x": 120,
"y": 760,
"wires": [
[
"demand_fanout_mgc_ps"
]
]
},
{
"id": "demand_fanout_mgc_ps",
"type": "function",
"z": "tab_process",
"name": "demand \u2192 MGC + PS",
"func": "const v = Number(msg.payload);\nif (!Number.isFinite(v) || v <= 0) return null;\n// MGC accepts Qd in m3/h directly when scaling=absolute.\nconst qd = { topic: 'Qd', payload: v };\n// PS accepts q_in in m3/s (canonical) via the 'unit' field.\nconst qin = { topic: 'q_in', payload: v / 3600, unit: 'm3/s' };\nreturn [qd, qin];",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 600,
"y": 760,
"wires": [
[
"mgc_pumps"
],
[
"ps_basin"
]
]
},
{
"id": "mgc_pumps",
"type": "machineGroupControl",
"z": "tab_process",
"name": "MGC \u2014 Pump Group",
"uuid": "mgc-pump-group",
"category": "controller",
"assetType": "machinegroupcontrol",
"model": "default",
"unit": "m3/h",
"supplier": "evolv",
"enableLog": false,
"logLevel": "warn",
"positionVsParent": "atEquipment",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"x": 900,
"y": 780,
"wires": [
[
"format_mgc"
],
[],
[
"ps_basin"
]
]
},
{
"id": "format_mgc",
"type": "function",
"z": "tab_process",
"name": "format MGC 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 totalFlow = find('flow.predicted.atequipment.') ?? find('downstream_predicted_flow');\nconst totalPower = find('power.predicted.atequipment.') ?? find('atEquipment_predicted_power');\nconst eff = find('efficiency.predicted.atequipment.');\nmsg.payload = {\n totalFlow: totalFlow != null ? Number(totalFlow).toFixed(1) + ' m\u00b3/h' : 'n/a',\n totalPower: totalPower != null ? Number(totalPower).toFixed(2) + ' kW' : 'n/a',\n efficiency: eff != null ? Number(eff).toFixed(3) : 'n/a',\n totalFlowNum: totalFlow != null ? Number(totalFlow) : null,\n totalPowerNum: totalPower != null ? Number(totalPower) : null,\n};\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1160,
"y": 780,
"wires": [
[
"lout_evt_mgc"
]
]
},
{
"id": "lout_evt_mgc",
"type": "link out",
"z": "tab_process",
"name": "evt:mgc",
"mode": "link",
"links": [
"lin_evt_mgc_dash"
],
"x": 1420,
"y": 780,
"wires": []
},
{
"id": "c_ps",
"type": "comment",
"z": "tab_process",
"name": "\u2500\u2500 Pumping Station \u2500\u2500 (basin model, manual control mode)",
"info": "Receives q_in from demand fanout. Emits formatted basin state.",
"x": 640,
"y": 900,
"wires": []
},
{
"id": "ps_basin",
"type": "pumpingStation",
"z": "tab_process",
"name": "Pumping Station",
"uuid": "ps-basin-1",
"category": "station",
"assetType": "pumpingstation",
"model": "default",
"unit": "m3/s",
"supplier": "evolv",
"enableLog": false,
"logLevel": "warn",
"positionVsParent": "atEquipment",
"positionIcon": "",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"controlMode": "manual",
"basinVolume": 50,
"basinHeight": 4,
"enableDryRunProtection": false,
"enableOverfillProtection": false,
"dryRunThresholdPercent": 0,
"overfillThresholdPercent": 100,
"timeleftToFullOrEmptyThresholdSeconds": 0,
"x": 900,
"y": 980,
"wires": [
[
"format_ps"
],
[]
]
},
{
"id": "format_ps",
"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.');\nmsg.payload = {\n level: lvl != null ? Number(lvl).toFixed(2) + ' m' : 'n/a',\n volume: vol != null ? Number(vol).toFixed(1) + ' m\u00b3' : 'n/a',\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 state: c.state || c.direction || 'idle',\n levelNum: lvl != null ? Number(lvl) : null,\n volumeNum: vol != null ? Number(vol) : null,\n};\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1160,
"y": 980,
"wires": [
[
"lout_evt_ps"
]
]
},
{
"id": "lout_evt_ps",
"type": "link out",
"z": "tab_process",
"name": "evt:ps",
"mode": "link",
"links": [
"lin_evt_ps_dash"
],
"x": 1420,
"y": 980,
"wires": []
},
{
"id": "c_mode_bcast",
"type": "comment",
"z": "tab_process",
"name": "\u2500\u2500 Mode broadcast \u2500\u2500",
"info": "Single 'auto' / 'virtualControl' value fans out as setMode to all 3 pumps.",
"x": 640,
"y": 1100,
"wires": []
},
{
"id": "lin_mode",
"type": "link in",
"z": "tab_process",
"name": "cmd:mode",
"links": [
"lout_mode_dash"
],
"x": 120,
"y": 1160,
"wires": [
[
"fanout_mode"
]
]
},
{
"id": "fanout_mode",
"type": "function",
"z": "tab_process",
"name": "fan setMode \u2192 3 pumps",
"func": "msg.topic = 'setMode';\nreturn [msg, msg, msg];",
"outputs": 3,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 600,
"y": 1160,
"wires": [
[
"pump_a"
],
[
"pump_b"
],
[
"pump_c"
]
]
},
{
"id": "c_station_cmds",
"type": "comment",
"z": "tab_process",
"name": "\u2500\u2500 Station-wide commands \u2500\u2500 (Start All / Stop All / Emergency)",
"info": "Each link-in carries a fully-built msg ready for handleInput; we just fan out 3-way.",
"x": 640,
"y": 1300,
"wires": []
},
{
"id": "lin_station_start",
"type": "link in",
"z": "tab_process",
"name": "cmd:station-startup",
"links": [
"lout_cmd_station_startup_dash"
],
"x": 120,
"y": 1360,
"wires": [
[
"fan_station_start"
]
]
},
{
"id": "fan_station_start",
"type": "function",
"z": "tab_process",
"name": "fan startup \u2192 3 pumps",
"func": "return [msg, msg, msg];",
"outputs": 3,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 600,
"y": 1360,
"wires": [
[
"pump_a"
],
[
"pump_b"
],
[
"pump_c"
]
]
},
{
"id": "lin_station_stop",
"type": "link in",
"z": "tab_process",
"name": "cmd:station-shutdown",
"links": [
"lout_cmd_station_shutdown_dash"
],
"x": 120,
"y": 1420,
"wires": [
[
"fan_station_stop"
]
]
},
{
"id": "fan_station_stop",
"type": "function",
"z": "tab_process",
"name": "fan shutdown \u2192 3 pumps",
"func": "return [msg, msg, msg];",
"outputs": 3,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 600,
"y": 1420,
"wires": [
[
"pump_a"
],
[
"pump_b"
],
[
"pump_c"
]
]
},
{
"id": "lin_station_estop",
"type": "link in",
"z": "tab_process",
"name": "cmd:station-estop",
"links": [
"lout_cmd_station_estop_dash"
],
"x": 120,
"y": 1480,
"wires": [
[
"fan_station_estop"
]
]
},
{
"id": "fan_station_estop",
"type": "function",
"z": "tab_process",
"name": "fan emergency stop \u2192 3 pumps",
"func": "return [msg, msg, msg];",
"outputs": 3,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 600,
"y": 1480,
"wires": [
[
"pump_a"
],
[
"pump_b"
],
[
"pump_c"
]
]
},
{
"id": "c_setup_at_mgc",
"type": "comment",
"z": "tab_process",
"name": "\u2500\u2500 Setup feeders \u2500\u2500",
"info": "Cross-tab link from Setup tab \u2192 MGC scaling/mode init.",
"x": 640,
"y": 1500,
"wires": []
},
{
"id": "lin_setup_at_mgc",
"type": "link in",
"z": "tab_process",
"name": "setup:to-mgc",
"links": [
"lout_setup_to_mgc"
],
"x": 120,
"y": 1560,
"wires": [
[
"mgc_pumps"
]
]
},
{
"id": "tab_ui",
"type": "tab",
"label": "\ud83d\udcca Dashboard UI",
"disabled": false,
"info": "Every ui-* widget lives here. Inputs (sliders/switches/buttons) emit via link-out; status text + charts receive via link-in. No business logic on this tab."
},
{
"id": "ui_base_ps_demo",
"type": "ui-base",
"name": "EVOLV Demo",
"path": "/dashboard",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control"
],
"showPathInSidebar": true,
"headerContent": "page",
"navigationStyle": "default",
"titleBarStyle": "default"
},
{
"id": "ui_theme_ps_demo",
"type": "ui-theme",
"name": "EVOLV Theme",
"colors": {
"surface": "#ffffff",
"primary": "#0f52a5",
"bgPage": "#f4f6fa",
"groupBg": "#ffffff",
"groupOutline": "#cccccc"
},
"sizes": {
"density": "default",
"pagePadding": "12px",
"groupGap": "12px",
"groupBorderRadius": "6px",
"widgetGap": "8px"
}
},
{
"id": "ui_page_ps_demo",
"type": "ui-page",
"name": "Pumping Station \u2014 3 Pumps",
"ui": "ui_base_ps_demo",
"path": "/pumping-station-demo",
"icon": "water_pump",
"layout": "grid",
"theme": "ui_theme_ps_demo",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "12"
}
],
"order": 1,
"className": ""
},
{
"id": "ui_grp_demand",
"type": "ui-group",
"name": "1. Process Demand",
"page": "ui_page_ps_demo",
"width": "12",
"height": "1",
"order": 1,
"showTitle": true,
"className": "",
"groupType": "default",
"disabled": false,
"visible": true
},
{
"id": "ui_grp_station",
"type": "ui-group",
"name": "2. Station Controls",
"page": "ui_page_ps_demo",
"width": "12",
"height": "1",
"order": 2,
"showTitle": true,
"className": "",
"groupType": "default",
"disabled": false,
"visible": true
},
{
"id": "ui_grp_mgc",
"type": "ui-group",
"name": "3a. MGC Status",
"page": "ui_page_ps_demo",
"width": "6",
"height": "1",
"order": 3,
"showTitle": true,
"className": "",
"groupType": "default",
"disabled": false,
"visible": true
},
{
"id": "ui_grp_ps",
"type": "ui-group",
"name": "3b. Basin Status",
"page": "ui_page_ps_demo",
"width": "6",
"height": "1",
"order": 4,
"showTitle": true,
"className": "",
"groupType": "default",
"disabled": false,
"visible": true
},
{
"id": "ui_grp_pump_a",
"type": "ui-group",
"name": "4a. Pump A",
"page": "ui_page_ps_demo",
"width": "4",
"height": "1",
"order": 5,
"showTitle": true,
"className": "",
"groupType": "default",
"disabled": false,
"visible": true
},
{
"id": "ui_grp_pump_b",
"type": "ui-group",
"name": "4b. Pump B",
"page": "ui_page_ps_demo",
"width": "4",
"height": "1",
"order": 6,
"showTitle": true,
"className": "",
"groupType": "default",
"disabled": false,
"visible": true
},
{
"id": "ui_grp_pump_c",
"type": "ui-group",
"name": "4c. Pump C",
"page": "ui_page_ps_demo",
"width": "4",
"height": "1",
"order": 7,
"showTitle": true,
"className": "",
"groupType": "default",
"disabled": false,
"visible": true
},
{
"id": "ui_grp_trend",
"type": "ui-group",
"name": "5. Trends",
"page": "ui_page_ps_demo",
"width": "12",
"height": "1",
"order": 8,
"showTitle": true,
"className": "",
"groupType": "default",
"disabled": false,
"visible": true
},
{
"id": "c_ui_title",
"type": "comment",
"z": "tab_ui",
"name": "\ud83d\udcca DASHBOARD UI \u2014 only ui-* widgets here",
"info": "Layout: column 1 = inputs (sliders/switches/buttons) \u2192 link-outs.\nColumn 2 = link-ins from process \u2192 routed to text/gauge/chart widgets.",
"x": 640,
"y": 20,
"wires": []
},
{
"id": "c_ui_demand",
"type": "comment",
"z": "tab_ui",
"name": "\u2500\u2500 Process Demand \u2500\u2500",
"info": "",
"x": 640,
"y": 100,
"wires": []
},
{
"id": "ui_demand_slider",
"type": "ui-slider",
"z": "tab_ui",
"group": "ui_grp_demand",
"name": "Process demand slider",
"label": "Process Demand (m\u00b3/h)",
"tooltip": "",
"order": 1,
"width": "0",
"height": "0",
"passthru": true,
"outs": "end",
"topic": "manualDemand",
"topicType": "str",
"min": "0",
"max": "300",
"step": "5.0",
"showLabel": true,
"showValue": true,
"labelPosition": "top",
"valuePosition": "left",
"thumbLabel": false,
"iconStart": "",
"iconEnd": "",
"x": 120,
"y": 140,
"wires": [
[
"lout_demand_dash"
]
]
},
{
"id": "lout_demand_dash",
"type": "link out",
"z": "tab_ui",
"name": "cmd:demand",
"mode": "link",
"links": [
"lin_demand_to_mgc",
"lin_demand_to_text"
],
"x": 380,
"y": 140,
"wires": []
},
{
"id": "ui_random_toggle",
"type": "ui-switch",
"z": "tab_ui",
"group": "ui_grp_demand",
"name": "Random demand",
"label": "Random demand generator (auto)",
"tooltip": "",
"order": 1,
"width": "0",
"height": "0",
"passthru": true,
"decouple": "false",
"topic": "randomToggle",
"topicType": "str",
"style": "",
"className": "",
"evaluate": "true",
"onvalue": "on",
"onvalueType": "str",
"onicon": "auto_mode",
"oncolor": "#0f52a5",
"offvalue": "off",
"offvalueType": "str",
"officon": "back_hand",
"offcolor": "#888888",
"x": 120,
"y": 200,
"wires": [
[
"lout_random_dash"
]
]
},
{
"id": "lout_random_dash",
"type": "link out",
"z": "tab_ui",
"name": "cmd:randomToggle",
"mode": "link",
"links": [
"lin_random_to_drivers"
],
"x": 380,
"y": 200,
"wires": []
},
{
"id": "ui_demand_text",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_demand",
"order": 1,
"width": "0",
"height": "0",
"name": "Current demand",
"label": "Current demand",
"format": "{{msg.payload}} m\u00b3/h",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 900,
"y": 140,
"wires": []
},
{
"id": "lin_demand_to_text",
"type": "link in",
"z": "tab_ui",
"name": "cmd:demand",
"links": [
"lout_demand_dash",
"lout_demand_drivers"
],
"x": 640,
"y": 140,
"wires": [
[
"ui_demand_text"
]
]
},
{
"id": "c_ui_station",
"type": "comment",
"z": "tab_ui",
"name": "\u2500\u2500 Mode + Station-wide buttons \u2500\u2500",
"info": "",
"x": 640,
"y": 320,
"wires": []
},
{
"id": "ui_mode_toggle",
"type": "ui-switch",
"z": "tab_ui",
"group": "ui_grp_station",
"name": "Auto/Manual mode",
"label": "Mode (Auto = MGC orchestrates \u00b7 Manual = dashboard per-pump)",
"tooltip": "",
"order": 1,
"width": "0",
"height": "0",
"passthru": true,
"decouple": "false",
"topic": "setMode",
"topicType": "str",
"style": "",
"className": "",
"evaluate": "true",
"onvalue": "auto",
"onvalueType": "str",
"onicon": "auto_mode",
"oncolor": "#0f52a5",
"offvalue": "virtualControl",
"offvalueType": "str",
"officon": "back_hand",
"offcolor": "#888888",
"x": 120,
"y": 360,
"wires": [
[
"lout_mode_dash"
]
]
},
{
"id": "lout_mode_dash",
"type": "link out",
"z": "tab_ui",
"name": "cmd:mode",
"mode": "link",
"links": [
"lin_mode"
],
"x": 380,
"y": 360,
"wires": []
},
{
"id": "btn_station_0",
"type": "ui-button",
"z": "tab_ui",
"group": "ui_grp_station",
"name": "Start all pumps",
"label": "Start all pumps",
"order": 1,
"width": "0",
"height": "0",
"tooltip": "",
"color": "#ffffff",
"bgcolor": "#16a34a",
"className": "",
"icon": "play_arrow",
"iconPosition": "left",
"payload": "fired",
"payloadType": "str",
"topic": "station_0",
"topicType": "str",
"buttonType": "default",
"x": 120,
"y": 420,
"wires": [
[
"wrap_station_0"
]
]
},
{
"id": "wrap_station_0",
"type": "function",
"z": "tab_ui",
"name": "build cmd (Start all pumps)",
"func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'startup' };\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 420,
"wires": [
[
"lout_cmd_station_startup_dash"
]
]
},
{
"id": "lout_cmd_station_startup_dash",
"type": "link out",
"z": "tab_ui",
"name": "cmd:station-startup",
"mode": "link",
"links": [
"lin_station_start"
],
"x": 640,
"y": 420,
"wires": []
},
{
"id": "btn_station_1",
"type": "ui-button",
"z": "tab_ui",
"group": "ui_grp_station",
"name": "Stop all pumps",
"label": "Stop all pumps",
"order": 1,
"width": "0",
"height": "0",
"tooltip": "",
"color": "#ffffff",
"bgcolor": "#ea580c",
"className": "",
"icon": "stop",
"iconPosition": "left",
"payload": "fired",
"payloadType": "str",
"topic": "station_1",
"topicType": "str",
"buttonType": "default",
"x": 120,
"y": 480,
"wires": [
[
"wrap_station_1"
]
]
},
{
"id": "wrap_station_1",
"type": "function",
"z": "tab_ui",
"name": "build cmd (Stop all pumps)",
"func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'shutdown' };\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 480,
"wires": [
[
"lout_cmd_station_shutdown_dash"
]
]
},
{
"id": "lout_cmd_station_shutdown_dash",
"type": "link out",
"z": "tab_ui",
"name": "cmd:station-shutdown",
"mode": "link",
"links": [
"lin_station_stop"
],
"x": 640,
"y": 480,
"wires": []
},
{
"id": "btn_station_2",
"type": "ui-button",
"z": "tab_ui",
"group": "ui_grp_station",
"name": "EMERGENCY STOP",
"label": "EMERGENCY STOP",
"order": 1,
"width": "0",
"height": "0",
"tooltip": "",
"color": "#ffffff",
"bgcolor": "#dc2626",
"className": "",
"icon": "stop_circle",
"iconPosition": "left",
"payload": "fired",
"payloadType": "str",
"topic": "station_2",
"topicType": "str",
"buttonType": "default",
"x": 120,
"y": 540,
"wires": [
[
"wrap_station_2"
]
]
},
{
"id": "wrap_station_2",
"type": "function",
"z": "tab_ui",
"name": "build cmd (EMERGENCY STOP)",
"func": "msg.topic = 'emergencystop';\nmsg.payload = { source:'GUI', action:'emergencystop' };\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 540,
"wires": [
[
"lout_cmd_station_estop_dash"
]
]
},
{
"id": "lout_cmd_station_estop_dash",
"type": "link out",
"z": "tab_ui",
"name": "cmd:station-estop",
"mode": "link",
"links": [
"lin_station_estop"
],
"x": 640,
"y": 540,
"wires": []
},
{
"id": "c_ui_mgc_ps",
"type": "comment",
"z": "tab_ui",
"name": "\u2500\u2500 MGC + Basin overview \u2500\u2500",
"info": "",
"x": 640,
"y": 600,
"wires": []
},
{
"id": "lin_evt_mgc_dash",
"type": "link in",
"z": "tab_ui",
"name": "evt:mgc",
"links": [
"lout_evt_mgc"
],
"x": 120,
"y": 640,
"wires": [
[
"ui_mgc_total_flow",
"ui_mgc_total_power",
"ui_mgc_eff"
]
]
},
{
"id": "ui_mgc_total_flow",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_mgc",
"order": 1,
"width": "0",
"height": "0",
"name": "MGC total flow",
"label": "Total flow",
"format": "{{msg.payload.totalFlow}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 640,
"wires": []
},
{
"id": "ui_mgc_total_power",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_mgc",
"order": 1,
"width": "0",
"height": "0",
"name": "MGC total power",
"label": "Total power",
"format": "{{msg.payload.totalPower}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 670,
"wires": []
},
{
"id": "ui_mgc_eff",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_mgc",
"order": 1,
"width": "0",
"height": "0",
"name": "MGC efficiency",
"label": "Group efficiency",
"format": "{{msg.payload.efficiency}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 700,
"wires": []
},
{
"id": "lin_evt_ps_dash",
"type": "link in",
"z": "tab_ui",
"name": "evt:ps",
"links": [
"lout_evt_ps"
],
"x": 120,
"y": 760,
"wires": [
[
"ui_ps_state",
"ui_ps_level",
"ui_ps_volume",
"ui_ps_qin",
"ui_ps_qout"
]
]
},
{
"id": "ui_ps_state",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_ps",
"order": 1,
"width": "0",
"height": "0",
"name": "PS state",
"label": "Basin state",
"format": "{{msg.payload.state}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 760,
"wires": []
},
{
"id": "ui_ps_level",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_ps",
"order": 1,
"width": "0",
"height": "0",
"name": "PS level",
"label": "Basin level",
"format": "{{msg.payload.level}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 790,
"wires": []
},
{
"id": "ui_ps_volume",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_ps",
"order": 1,
"width": "0",
"height": "0",
"name": "PS volume",
"label": "Basin volume",
"format": "{{msg.payload.volume}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 820,
"wires": []
},
{
"id": "ui_ps_qin",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_ps",
"order": 1,
"width": "0",
"height": "0",
"name": "PS Qin",
"label": "Inflow",
"format": "{{msg.payload.qIn}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 850,
"wires": []
},
{
"id": "ui_ps_qout",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_ps",
"order": 1,
"width": "0",
"height": "0",
"name": "PS Qout",
"label": "Pumped out",
"format": "{{msg.payload.qOut}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 880,
"wires": []
},
{
"id": "c_ui_pump_a",
"type": "comment",
"z": "tab_ui",
"name": "\u2500\u2500 Pump A \u2500\u2500",
"info": "",
"x": 640,
"y": 1000,
"wires": []
},
{
"id": "lin_evt_pump_a_dash",
"type": "link in",
"z": "tab_ui",
"name": "evt:pump-A",
"links": [
"lout_evt_pump_a"
],
"x": 120,
"y": 1040,
"wires": [
[
"ui_pump_a_state",
"ui_pump_a_mode",
"ui_pump_a_ctrl",
"ui_pump_a_flow",
"ui_pump_a_power",
"ui_pump_a_pUp",
"ui_pump_a_pDn",
"trend_split_pump_a"
]
]
},
{
"id": "ui_pump_a_state",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_a",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump A State",
"label": "State",
"format": "{{msg.payload.state}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1040,
"wires": []
},
{
"id": "ui_pump_a_mode",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_a",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump A Mode",
"label": "Mode",
"format": "{{msg.payload.mode}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1070,
"wires": []
},
{
"id": "ui_pump_a_ctrl",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_a",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump A Controller %",
"label": "Controller %",
"format": "{{msg.payload.ctrl}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1100,
"wires": []
},
{
"id": "ui_pump_a_flow",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_a",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump A Flow",
"label": "Flow",
"format": "{{msg.payload.flow}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1130,
"wires": []
},
{
"id": "ui_pump_a_power",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_a",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump A Power",
"label": "Power",
"format": "{{msg.payload.power}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1160,
"wires": []
},
{
"id": "ui_pump_a_pUp",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_a",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump A p Upstream",
"label": "p Upstream",
"format": "{{msg.payload.pUp}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1190,
"wires": []
},
{
"id": "ui_pump_a_pDn",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_a",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump A p Downstream",
"label": "p Downstream",
"format": "{{msg.payload.pDn}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1220,
"wires": []
},
{
"id": "ui_pump_a_setpoint",
"type": "ui-slider",
"z": "tab_ui",
"group": "ui_grp_pump_a",
"name": "Pump A setpoint",
"label": "Setpoint % (manual mode)",
"tooltip": "",
"order": 1,
"width": "0",
"height": "0",
"passthru": true,
"outs": "end",
"topic": "setpoint_pump_a",
"topicType": "str",
"min": "0",
"max": "100",
"step": "5.0",
"showLabel": true,
"showValue": true,
"labelPosition": "top",
"valuePosition": "left",
"thumbLabel": false,
"iconStart": "",
"iconEnd": "",
"x": 120,
"y": 1280,
"wires": [
[
"lout_setpoint_pump_a_dash"
]
]
},
{
"id": "lout_setpoint_pump_a_dash",
"type": "link out",
"z": "tab_ui",
"name": "cmd:setpoint-A",
"mode": "link",
"links": [
"lin_setpoint_pump_a"
],
"x": 380,
"y": 1280,
"wires": []
},
{
"id": "btn_pump_a_start",
"type": "ui-button",
"z": "tab_ui",
"group": "ui_grp_pump_a",
"name": "Pump A startup",
"label": "Startup",
"order": 1,
"width": "0",
"height": "0",
"tooltip": "",
"color": "#ffffff",
"bgcolor": "#16a34a",
"className": "",
"icon": "play_arrow",
"iconPosition": "left",
"payload": "fired",
"payloadType": "str",
"topic": "start_pump_a",
"topicType": "str",
"buttonType": "default",
"x": 120,
"y": 1330,
"wires": [
[
"wrap_pump_a_start"
]
]
},
{
"id": "wrap_pump_a_start",
"type": "function",
"z": "tab_ui",
"name": "build start (Pump A)",
"func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'startup' };\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 1330,
"wires": [
[
"lout_seq_pump_a_dash"
]
]
},
{
"id": "btn_pump_a_stop",
"type": "ui-button",
"z": "tab_ui",
"group": "ui_grp_pump_a",
"name": "Pump A shutdown",
"label": "Shutdown",
"order": 1,
"width": "0",
"height": "0",
"tooltip": "",
"color": "#ffffff",
"bgcolor": "#ea580c",
"className": "",
"icon": "stop",
"iconPosition": "left",
"payload": "fired",
"payloadType": "str",
"topic": "stop_pump_a",
"topicType": "str",
"buttonType": "default",
"x": 120,
"y": 1380,
"wires": [
[
"wrap_pump_a_stop"
]
]
},
{
"id": "wrap_pump_a_stop",
"type": "function",
"z": "tab_ui",
"name": "build stop (Pump A)",
"func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'shutdown' };\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 1380,
"wires": [
[
"lout_seq_pump_a_dash"
]
]
},
{
"id": "lout_seq_pump_a_dash",
"type": "link out",
"z": "tab_ui",
"name": "cmd:pump-A-seq",
"mode": "link",
"links": [
"lin_seq_pump_a"
],
"x": 640,
"y": 1355,
"wires": []
},
{
"id": "trend_split_pump_a",
"type": "function",
"z": "tab_ui",
"name": "trend split (Pump A)",
"func": "const p = msg.payload || {};\nconst flowMsg = p.flowNum != null ? { topic: 'Pump A', payload: Number(p.flowNum) } : null;\nconst powerMsg = p.powerNum != null ? { topic: 'Pump A', payload: Number(p.powerNum) } : null;\nreturn [flowMsg, powerMsg];",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 900,
"y": 1080,
"wires": [
[
"trend_chart_flow"
],
[
"trend_chart_power"
]
]
},
{
"id": "c_ui_pump_b",
"type": "comment",
"z": "tab_ui",
"name": "\u2500\u2500 Pump B \u2500\u2500",
"info": "",
"x": 640,
"y": 1400,
"wires": []
},
{
"id": "lin_evt_pump_b_dash",
"type": "link in",
"z": "tab_ui",
"name": "evt:pump-B",
"links": [
"lout_evt_pump_b"
],
"x": 120,
"y": 1440,
"wires": [
[
"ui_pump_b_state",
"ui_pump_b_mode",
"ui_pump_b_ctrl",
"ui_pump_b_flow",
"ui_pump_b_power",
"ui_pump_b_pUp",
"ui_pump_b_pDn",
"trend_split_pump_b"
]
]
},
{
"id": "ui_pump_b_state",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_b",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump B State",
"label": "State",
"format": "{{msg.payload.state}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1440,
"wires": []
},
{
"id": "ui_pump_b_mode",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_b",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump B Mode",
"label": "Mode",
"format": "{{msg.payload.mode}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1470,
"wires": []
},
{
"id": "ui_pump_b_ctrl",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_b",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump B Controller %",
"label": "Controller %",
"format": "{{msg.payload.ctrl}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1500,
"wires": []
},
{
"id": "ui_pump_b_flow",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_b",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump B Flow",
"label": "Flow",
"format": "{{msg.payload.flow}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1530,
"wires": []
},
{
"id": "ui_pump_b_power",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_b",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump B Power",
"label": "Power",
"format": "{{msg.payload.power}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1560,
"wires": []
},
{
"id": "ui_pump_b_pUp",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_b",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump B p Upstream",
"label": "p Upstream",
"format": "{{msg.payload.pUp}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1590,
"wires": []
},
{
"id": "ui_pump_b_pDn",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_b",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump B p Downstream",
"label": "p Downstream",
"format": "{{msg.payload.pDn}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1620,
"wires": []
},
{
"id": "ui_pump_b_setpoint",
"type": "ui-slider",
"z": "tab_ui",
"group": "ui_grp_pump_b",
"name": "Pump B setpoint",
"label": "Setpoint % (manual mode)",
"tooltip": "",
"order": 1,
"width": "0",
"height": "0",
"passthru": true,
"outs": "end",
"topic": "setpoint_pump_b",
"topicType": "str",
"min": "0",
"max": "100",
"step": "5.0",
"showLabel": true,
"showValue": true,
"labelPosition": "top",
"valuePosition": "left",
"thumbLabel": false,
"iconStart": "",
"iconEnd": "",
"x": 120,
"y": 1680,
"wires": [
[
"lout_setpoint_pump_b_dash"
]
]
},
{
"id": "lout_setpoint_pump_b_dash",
"type": "link out",
"z": "tab_ui",
"name": "cmd:setpoint-B",
"mode": "link",
"links": [
"lin_setpoint_pump_b"
],
"x": 380,
"y": 1680,
"wires": []
},
{
"id": "btn_pump_b_start",
"type": "ui-button",
"z": "tab_ui",
"group": "ui_grp_pump_b",
"name": "Pump B startup",
"label": "Startup",
"order": 1,
"width": "0",
"height": "0",
"tooltip": "",
"color": "#ffffff",
"bgcolor": "#16a34a",
"className": "",
"icon": "play_arrow",
"iconPosition": "left",
"payload": "fired",
"payloadType": "str",
"topic": "start_pump_b",
"topicType": "str",
"buttonType": "default",
"x": 120,
"y": 1730,
"wires": [
[
"wrap_pump_b_start"
]
]
},
{
"id": "wrap_pump_b_start",
"type": "function",
"z": "tab_ui",
"name": "build start (Pump B)",
"func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'startup' };\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 1730,
"wires": [
[
"lout_seq_pump_b_dash"
]
]
},
{
"id": "btn_pump_b_stop",
"type": "ui-button",
"z": "tab_ui",
"group": "ui_grp_pump_b",
"name": "Pump B shutdown",
"label": "Shutdown",
"order": 1,
"width": "0",
"height": "0",
"tooltip": "",
"color": "#ffffff",
"bgcolor": "#ea580c",
"className": "",
"icon": "stop",
"iconPosition": "left",
"payload": "fired",
"payloadType": "str",
"topic": "stop_pump_b",
"topicType": "str",
"buttonType": "default",
"x": 120,
"y": 1780,
"wires": [
[
"wrap_pump_b_stop"
]
]
},
{
"id": "wrap_pump_b_stop",
"type": "function",
"z": "tab_ui",
"name": "build stop (Pump B)",
"func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'shutdown' };\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 1780,
"wires": [
[
"lout_seq_pump_b_dash"
]
]
},
{
"id": "lout_seq_pump_b_dash",
"type": "link out",
"z": "tab_ui",
"name": "cmd:pump-B-seq",
"mode": "link",
"links": [
"lin_seq_pump_b"
],
"x": 640,
"y": 1755,
"wires": []
},
{
"id": "trend_split_pump_b",
"type": "function",
"z": "tab_ui",
"name": "trend split (Pump B)",
"func": "const p = msg.payload || {};\nconst flowMsg = p.flowNum != null ? { topic: 'Pump B', payload: Number(p.flowNum) } : null;\nconst powerMsg = p.powerNum != null ? { topic: 'Pump B', payload: Number(p.powerNum) } : null;\nreturn [flowMsg, powerMsg];",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 900,
"y": 1480,
"wires": [
[
"trend_chart_flow"
],
[
"trend_chart_power"
]
]
},
{
"id": "c_ui_pump_c",
"type": "comment",
"z": "tab_ui",
"name": "\u2500\u2500 Pump C \u2500\u2500",
"info": "",
"x": 640,
"y": 1800,
"wires": []
},
{
"id": "lin_evt_pump_c_dash",
"type": "link in",
"z": "tab_ui",
"name": "evt:pump-C",
"links": [
"lout_evt_pump_c"
],
"x": 120,
"y": 1840,
"wires": [
[
"ui_pump_c_state",
"ui_pump_c_mode",
"ui_pump_c_ctrl",
"ui_pump_c_flow",
"ui_pump_c_power",
"ui_pump_c_pUp",
"ui_pump_c_pDn",
"trend_split_pump_c"
]
]
},
{
"id": "ui_pump_c_state",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_c",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump C State",
"label": "State",
"format": "{{msg.payload.state}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1840,
"wires": []
},
{
"id": "ui_pump_c_mode",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_c",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump C Mode",
"label": "Mode",
"format": "{{msg.payload.mode}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1870,
"wires": []
},
{
"id": "ui_pump_c_ctrl",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_c",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump C Controller %",
"label": "Controller %",
"format": "{{msg.payload.ctrl}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1900,
"wires": []
},
{
"id": "ui_pump_c_flow",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_c",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump C Flow",
"label": "Flow",
"format": "{{msg.payload.flow}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1930,
"wires": []
},
{
"id": "ui_pump_c_power",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_c",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump C Power",
"label": "Power",
"format": "{{msg.payload.power}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1960,
"wires": []
},
{
"id": "ui_pump_c_pUp",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_c",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump C p Upstream",
"label": "p Upstream",
"format": "{{msg.payload.pUp}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 1990,
"wires": []
},
{
"id": "ui_pump_c_pDn",
"type": "ui-text",
"z": "tab_ui",
"group": "ui_grp_pump_c",
"order": 1,
"width": "0",
"height": "0",
"name": "Pump C p Downstream",
"label": "p Downstream",
"format": "{{msg.payload.pDn}}",
"layout": "row-left",
"style": false,
"font": "",
"fontSize": 14,
"color": "#000000",
"x": 640,
"y": 2020,
"wires": []
},
{
"id": "ui_pump_c_setpoint",
"type": "ui-slider",
"z": "tab_ui",
"group": "ui_grp_pump_c",
"name": "Pump C setpoint",
"label": "Setpoint % (manual mode)",
"tooltip": "",
"order": 1,
"width": "0",
"height": "0",
"passthru": true,
"outs": "end",
"topic": "setpoint_pump_c",
"topicType": "str",
"min": "0",
"max": "100",
"step": "5.0",
"showLabel": true,
"showValue": true,
"labelPosition": "top",
"valuePosition": "left",
"thumbLabel": false,
"iconStart": "",
"iconEnd": "",
"x": 120,
"y": 2080,
"wires": [
[
"lout_setpoint_pump_c_dash"
]
]
},
{
"id": "lout_setpoint_pump_c_dash",
"type": "link out",
"z": "tab_ui",
"name": "cmd:setpoint-C",
"mode": "link",
"links": [
"lin_setpoint_pump_c"
],
"x": 380,
"y": 2080,
"wires": []
},
{
"id": "btn_pump_c_start",
"type": "ui-button",
"z": "tab_ui",
"group": "ui_grp_pump_c",
"name": "Pump C startup",
"label": "Startup",
"order": 1,
"width": "0",
"height": "0",
"tooltip": "",
"color": "#ffffff",
"bgcolor": "#16a34a",
"className": "",
"icon": "play_arrow",
"iconPosition": "left",
"payload": "fired",
"payloadType": "str",
"topic": "start_pump_c",
"topicType": "str",
"buttonType": "default",
"x": 120,
"y": 2130,
"wires": [
[
"wrap_pump_c_start"
]
]
},
{
"id": "wrap_pump_c_start",
"type": "function",
"z": "tab_ui",
"name": "build start (Pump C)",
"func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'startup' };\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 2130,
"wires": [
[
"lout_seq_pump_c_dash"
]
]
},
{
"id": "btn_pump_c_stop",
"type": "ui-button",
"z": "tab_ui",
"group": "ui_grp_pump_c",
"name": "Pump C shutdown",
"label": "Shutdown",
"order": 1,
"width": "0",
"height": "0",
"tooltip": "",
"color": "#ffffff",
"bgcolor": "#ea580c",
"className": "",
"icon": "stop",
"iconPosition": "left",
"payload": "fired",
"payloadType": "str",
"topic": "stop_pump_c",
"topicType": "str",
"buttonType": "default",
"x": 120,
"y": 2180,
"wires": [
[
"wrap_pump_c_stop"
]
]
},
{
"id": "wrap_pump_c_stop",
"type": "function",
"z": "tab_ui",
"name": "build stop (Pump C)",
"func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'shutdown' };\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 2180,
"wires": [
[
"lout_seq_pump_c_dash"
]
]
},
{
"id": "lout_seq_pump_c_dash",
"type": "link out",
"z": "tab_ui",
"name": "cmd:pump-C-seq",
"mode": "link",
"links": [
"lin_seq_pump_c"
],
"x": 640,
"y": 2155,
"wires": []
},
{
"id": "trend_split_pump_c",
"type": "function",
"z": "tab_ui",
"name": "trend split (Pump C)",
"func": "const p = msg.payload || {};\nconst flowMsg = p.flowNum != null ? { topic: 'Pump C', payload: Number(p.flowNum) } : null;\nconst powerMsg = p.powerNum != null ? { topic: 'Pump C', payload: Number(p.powerNum) } : null;\nreturn [flowMsg, powerMsg];",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 900,
"y": 1880,
"wires": [
[
"trend_chart_flow"
],
[
"trend_chart_power"
]
]
},
{
"id": "c_ui_trends",
"type": "comment",
"z": "tab_ui",
"name": "\u2500\u2500 Trends (shared by all pumps) \u2500\u2500",
"info": "Each chart accepts msg.topic as the series name (categoryType=msg).",
"x": 640,
"y": 2280,
"wires": []
},
{
"id": "trend_chart_flow",
"type": "ui-chart",
"z": "tab_ui",
"group": "ui_grp_trend",
"name": "Flow per pump (m\u00b3/h)",
"label": "Flow per pump",
"order": 1,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisLabel": "",
"xAxisType": "time",
"xAxisTimeFormat": "auto",
"yAxisLabel": "",
"ymin": "",
"ymax": "",
"action": "append",
"pointShape": "circle",
"pointRadius": 2,
"showLegend": true,
"removeOlder": "10",
"removeOlderUnit": "60",
"removeOlderPoints": "200",
"colors": [],
"textColor": [],
"textColorDefault": true,
"width": "0",
"height": "0",
"className": "",
"x": 900,
"y": 2320,
"wires": [
[]
]
},
{
"id": "trend_chart_power",
"type": "ui-chart",
"z": "tab_ui",
"group": "ui_grp_trend",
"name": "Power per pump (kW)",
"label": "Power per pump",
"order": 1,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisLabel": "",
"xAxisType": "time",
"xAxisTimeFormat": "auto",
"yAxisLabel": "",
"ymin": "",
"ymax": "",
"action": "append",
"pointShape": "circle",
"pointRadius": 2,
"showLegend": true,
"removeOlder": "10",
"removeOlderUnit": "60",
"removeOlderPoints": "200",
"colors": [],
"textColor": [],
"textColorDefault": true,
"width": "0",
"height": "0",
"className": "",
"x": 900,
"y": 2380,
"wires": [
[]
]
},
{
"id": "tab_drivers",
"type": "tab",
"label": "\ud83c\udf9b\ufe0f Demo Drivers",
"disabled": false,
"info": "Auto stimulus for the demo. Random demand generator + state holder for the dashboard's randomToggle switch. In production, delete this tab and feed cmd:demand from your real demand source."
},
{
"id": "c_drv_title",
"type": "comment",
"z": "tab_drivers",
"name": "\ud83c\udf9b\ufe0f DEMO DRIVERS \u2014 auto stimulus only",
"info": "Removable: in production, replace this tab with the real demand source.",
"x": 640,
"y": 20,
"wires": []
},
{
"id": "c_drv_state",
"type": "comment",
"z": "tab_drivers",
"name": "\u2500\u2500 Random toggle state \u2500\u2500",
"info": "",
"x": 640,
"y": 100,
"wires": []
},
{
"id": "lin_random_to_drivers",
"type": "link in",
"z": "tab_drivers",
"name": "cmd:randomToggle",
"links": [
"lout_random_dash"
],
"x": 120,
"y": 140,
"wires": [
[
"random_state"
]
]
},
{
"id": "random_state",
"type": "function",
"z": "tab_drivers",
"name": "store random on/off",
"func": "flow.set('randomOn', msg.payload === 'on');\nreturn null;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 380,
"y": 140,
"wires": [
[]
]
},
{
"id": "c_drv_random",
"type": "comment",
"z": "tab_drivers",
"name": "\u2500\u2500 Random demand generator \u2500\u2500 (every 3 s)",
"info": "",
"x": 640,
"y": 250,
"wires": []
},
{
"id": "rand_tick",
"type": "inject",
"z": "tab_drivers",
"name": "tick (random demand)",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "",
"vt": "date"
}
],
"topic": "randomTick",
"payload": "",
"payloadType": "date",
"repeat": "3",
"crontab": "",
"once": false,
"onceDelay": "0.5",
"x": 120,
"y": 290,
"wires": [
[
"random_demand_fn"
]
]
},
{
"id": "random_demand_fn",
"type": "function",
"z": "tab_drivers",
"name": "random demand",
"func": "if (!flow.get('randomOn')) return null;\nconst v = Math.round(40 + Math.random() * 200);\nreturn { topic: 'manualDemand', payload: v };",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 600,
"y": 290,
"wires": [
[
"lout_demand_drivers"
]
]
},
{
"id": "lout_demand_drivers",
"type": "link out",
"z": "tab_drivers",
"name": "cmd:demand",
"mode": "link",
"links": [
"lin_demand_to_mgc",
"lin_demand_to_text"
],
"x": 900,
"y": 290,
"wires": []
},
{
"id": "tab_setup",
"type": "tab",
"label": "\u2699\ufe0f Setup & Init",
"disabled": false,
"info": "One-shot deploy-time injects. Sets MGC scaling/mode, broadcasts pumps mode = auto, and auto-starts the pumps + random demand."
},
{
"id": "c_setup_title",
"type": "comment",
"z": "tab_setup",
"name": "\u2699\ufe0f SETUP & INIT \u2014 one-shot deploy-time injects",
"info": "Disable this tab in production \u2014 the runtime should be persistent.",
"x": 640,
"y": 20,
"wires": []
},
{
"id": "setup_mgc_scaling",
"type": "inject",
"z": "tab_setup",
"name": "MGC scaling = absolute",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "absolute",
"vt": "str"
}
],
"topic": "setScaling",
"payload": "absolute",
"payloadType": "str",
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "1.5",
"x": 120,
"y": 100,
"wires": [
[
"lout_setup_to_mgc"
]
]
},
{
"id": "setup_mgc_mode",
"type": "inject",
"z": "tab_setup",
"name": "MGC mode = optimalcontrol",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "optimalcontrol",
"vt": "str"
}
],
"topic": "setMode",
"payload": "optimalcontrol",
"payloadType": "str",
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "1.7",
"x": 120,
"y": 160,
"wires": [
[
"lout_setup_to_mgc"
]
]
},
{
"id": "lout_setup_to_mgc",
"type": "link out",
"z": "tab_setup",
"name": "setup:to-mgc",
"mode": "link",
"links": [
"lin_setup_at_mgc"
],
"x": 380,
"y": 130,
"wires": []
},
{
"id": "setup_pumps_mode",
"type": "inject",
"z": "tab_setup",
"name": "pumps mode = auto",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "auto",
"vt": "str"
}
],
"topic": "setMode",
"payload": "auto",
"payloadType": "str",
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "2.0",
"x": 120,
"y": 250,
"wires": [
[
"lout_mode_setup"
]
]
},
{
"id": "lout_mode_setup",
"type": "link out",
"z": "tab_setup",
"name": "cmd:mode",
"mode": "link",
"links": [
"lin_mode"
],
"x": 380,
"y": 250,
"wires": []
},
{
"id": "setup_pumps_startup",
"type": "inject",
"z": "tab_setup",
"name": "auto-startup all pumps",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "{\"source\":\"GUI\",\"action\":\"execSequence\",\"parameter\":\"startup\"}",
"vt": "json"
}
],
"topic": "execSequence",
"payload": "{\"source\":\"GUI\",\"action\":\"execSequence\",\"parameter\":\"startup\"}",
"payloadType": "json",
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "4",
"x": 120,
"y": 350,
"wires": [
[
"lout_setup_station_start"
]
]
},
{
"id": "lout_setup_station_start",
"type": "link out",
"z": "tab_setup",
"name": "cmd:station-startup",
"mode": "link",
"links": [
"lin_station_start"
],
"x": 380,
"y": 350,
"wires": []
},
{
"id": "setup_random_on",
"type": "inject",
"z": "tab_setup",
"name": "auto-enable random demand",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"v": "on",
"vt": "str"
}
],
"topic": "randomToggle",
"payload": "on",
"payloadType": "str",
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "5",
"x": 120,
"y": 450,
"wires": [
[
"lout_setup_random"
]
]
},
{
"id": "lout_setup_random",
"type": "link out",
"z": "tab_setup",
"name": "cmd:randomToggle",
"mode": "link",
"links": [
"lin_random_to_drivers"
],
"x": 380,
"y": 450,
"wires": []
}
]