From 4cb9c5084c250a08b8fd583a771a433669ebcd1d Mon Sep 17 00:00:00 2001 From: znetsixe Date: Tue, 12 May 2026 15:24:03 +0200 Subject: [PATCH] feat(mgc): editor defaults, compact status badge, mode-case fix, real example flows + dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Editor (mgc.html) - Drag-in defaults now expose mode (optimalControl) and scaling (normalized) via dropdowns in the edit dialog. Was: no control fields in the UI at all, so users had to send set.mode/set.scaling after deploy or live with the hidden schema defaults. Wire-up (src/nodeClass.js) - buildDomainConfig now bridges the flat editor fields (mode, scaling) into the nested schema shape (mode.current, scaling.current). Was: returned {} so the editor's mode/scaling never reached the runtime. Mode-case bug fix (src/specificClass.js) - Schema enum values are camelCase (optimalControl, priorityControl) but the runtime switch in _runDispatch matched lowercase only. With the default config, dispatch silently fell through to the warning branch and nothing ran. Normalise via String(this.mode).toLowerCase() so both forms work. Status badge (src/io/output.js) - Compacted from ~80 chars (mode | Ⓝ: πŸ’¨=Q/Qmax | ⚑=P | N machine(s)) to ~50 chars (mode | norm | Q=Q/Qmax mΒ³/h | P=P kW | active/total x). Drops emoji glyphs that rendered inconsistently across themes; uses the same dot+fill convention as pumpingStation. Output extension (src/io/output.js) - getOutput() now also emits flowCapacityMin/Max, machineCount, machineCountActive. Was: only group-level totals + dist-from-peak + mode/scaling, so dashboards couldn't show capacity / active count without subscribing to each rotatingMachine individually. Examples - Drop pre-refactor stubs (basic.flow.json, integration.flow.json, edge.flow.json). They had a single MGC + inject + debug, no children, and never dispatched anything. - 01-Basic.json: 1 MGC + 3 rotatingMachine pumps + Setup once-fires virtualControl + cmd.startup on all pumps via fan-out function. Numbered driver groups for Control mode / Scaling / Operator demand. Pumps register with MGC via Port 2 (child.register, automatic). - 02-Dashboard.json: same plumbing + FlowFuse Dashboard 2.0 page with Controls (mode + scaling buttons, demand slider 0–100, stop + init buttons), Status (7 ui-text rows), Trends (3 charts: flow + capacity, power, BEP rel %), and a raw-output ui-template dumping every Port 0 field. Fan-out function caches last-known values so deltas don't blank. Wiki + README - examples/README.md rewritten for the two-file set with canonical command surface table and "what to try" recipes. - wiki/Home.md Β§11 (Examples) updated; Β§14 #4 (TODO flow item) replaced with the actual current limitation (no per-pump fan-out on Port 0). Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/01-Basic.json | 677 ++++++++++++++++++ examples/02-Dashboard.json | 1205 ++++++++++++++++++++++++++++++++ examples/README.md | 53 +- examples/basic.flow.json | 6 - examples/edge.flow.json | 6 - examples/integration.flow.json | 6 - mgc.html | 24 +- src/io/output.js | 31 +- src/nodeClass.js | 10 +- src/specificClass.js | 5 +- wiki/Home.md | 16 +- 11 files changed, 1996 insertions(+), 43 deletions(-) create mode 100644 examples/01-Basic.json create mode 100644 examples/02-Dashboard.json delete mode 100644 examples/basic.flow.json delete mode 100644 examples/edge.flow.json delete mode 100644 examples/integration.flow.json diff --git a/examples/01-Basic.json b/examples/01-Basic.json new file mode 100644 index 0000000..c579f55 --- /dev/null +++ b/examples/01-Basic.json @@ -0,0 +1,677 @@ +[ + { + "id": "tab_mgc_basic", + "type": "tab", + "label": "MGC - Basic", + "disabled": false, + "info": "Tier 1: one machineGroupControl (MGC) coordinating three rotatingMachine pumps. Setup once-fires virtualControl + cmd.startup on all three pumps; then operator demand drives the MGC, which dispatches per-pump flow setpoints." + }, + { + "id": "grp_mgc_unit", + "type": "group", + "z": "tab_mgc_basic", + "name": "Machine Group (Unit)", + "style": { + "label": true, + "stroke": "#000000", + "fill": "#50a8d9", + "fill-opacity": "0.10" + }, + "nodes": [ + "mgc_basic_node" + ], + "x": 974, + "y": 359, + "w": 152, + "h": 122 + }, + { + "id": "grp_pump_a", + "type": "group", + "z": "tab_mgc_basic", + "name": "Pump A (Equipment)", + "style": { + "label": true, + "stroke": "#000000", + "fill": "#86bbdd", + "fill-opacity": "0.10" + }, + "nodes": [ + "rm_basic_pump_a" + ], + "x": 694, + "y": 199, + "w": 142, + "h": 82 + }, + { + "id": "grp_pump_b", + "type": "group", + "z": "tab_mgc_basic", + "name": "Pump B (Equipment)", + "style": { + "label": true, + "stroke": "#000000", + "fill": "#86bbdd", + "fill-opacity": "0.10" + }, + "nodes": [ + "rm_basic_pump_b" + ], + "x": 694, + "y": 379, + "w": 142, + "h": 82 + }, + { + "id": "grp_pump_c", + "type": "group", + "z": "tab_mgc_basic", + "name": "Pump C (Equipment)", + "style": { + "label": true, + "stroke": "#000000", + "fill": "#86bbdd", + "fill-opacity": "0.10" + }, + "nodes": [ + "rm_basic_pump_c" + ], + "x": 694, + "y": 559, + "w": 142, + "h": 82 + }, + { + "id": "grp_drv_mode", + "type": "group", + "z": "tab_mgc_basic", + "name": "1. Control mode", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "inj_mode_optimal", + "inj_mode_priority" + ], + "x": 94, + "y": 99, + "w": 312, + "h": 122 + }, + { + "id": "grp_drv_scaling", + "type": "group", + "z": "tab_mgc_basic", + "name": "2. Scaling", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "inj_scaling_norm", + "inj_scaling_abs" + ], + "x": 94, + "y": 259, + "w": 312, + "h": 122 + }, + { + "id": "grp_drv_demand", + "type": "group", + "z": "tab_mgc_basic", + "name": "3. Operator demand (% of group capacity)", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "inj_demand_25", + "inj_demand_50", + "inj_demand_75", + "inj_demand_100", + "inj_demand_0" + ], + "x": 94, + "y": 419, + "w": 312, + "h": 222 + }, + { + "id": "grp_setup", + "type": "group", + "z": "tab_mgc_basic", + "name": "Setup β€” once on deploy", + "style": { + "stroke": "#666666", + "fill": "#dddddd", + "fill-opacity": "0.20", + "label": true, + "color": "#333333" + }, + "nodes": [ + "inj_setup_start", + "fn_setup_fanout" + ], + "x": 94, + "y": 679, + "w": 532, + "h": 82 + }, + { + "id": "grp_dbg", + "type": "group", + "z": "tab_mgc_basic", + "name": "Debug outputs (sidebar)", + "style": { + "stroke": "#666666", + "fill": "#d1d1d1", + "fill-opacity": "0.2", + "label": true, + "color": "#333333" + }, + "nodes": [ + "dbg_port0", + "dbg_port1", + "dbg_port2" + ], + "x": 1234, + "y": 339, + "w": 232, + "h": 202 + }, + { + "id": "cmt_title", + "type": "comment", + "z": "tab_mgc_basic", + "name": "MGC β€” Basic (Tier 1)", + "info": "One machineGroupControl coordinating three rotatingMachine pumps.\n\nDefaults: mode=optimalControl, scaling=normalized.\n\nSETUP β€” fires once on deploy\n- Switches all 3 pumps to virtualControl mode\n- Sends cmd.startup to all 3 pumps\nPumps register with the MGC automatically via Port 2 (child.register).\n\nHOW TO USE\n1. Deploy β€” the Setup group auto-runs after ~1.5 s, putting pumps in virtual + started.\n2. Click any \"set.demand = N %\" β€” MGC dispatches per-pump flow setpoints by BEP-gravitation (default) or priority list, depending on the mode.\n3. Switch scaling to `absolute` to interpret set.demand as mΒ³/h instead of %.\n4. Switch mode to `priorityControl` for sequential equal-flow control; `optimalControl` (default) picks the best combination automatically.\n5. Send `set.demand = 0` to drain the group (turnOffAllMachines).\n\nPORTS (MGC)\n- Port 0: process output (mode, scaling, totals, dist-from-peak)\n- Port 1: InfluxDB-shaped payload\n- Port 2: parent-registration handshake (when wired into a pumpingStation)", + "x": 1100, + "y": 280, + "wires": [] + }, + { + "id": "inj_mode_optimal", + "type": "inject", + "z": "tab_mgc_basic", + "g": "grp_drv_mode", + "name": "set.mode = optimalControl", + "props": [ + { "p": "topic", "vt": "str" }, + { "p": "payload", "v": "optimalControl", "vt": "str" } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.mode", + "x": 260, + "y": 140, + "wires": [ + [ + "mgc_basic_node" + ] + ] + }, + { + "id": "inj_mode_priority", + "type": "inject", + "z": "tab_mgc_basic", + "g": "grp_drv_mode", + "name": "set.mode = priorityControl", + "props": [ + { "p": "topic", "vt": "str" }, + { "p": "payload", "v": "priorityControl", "vt": "str" } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.mode", + "x": 260, + "y": 180, + "wires": [ + [ + "mgc_basic_node" + ] + ] + }, + { + "id": "inj_scaling_norm", + "type": "inject", + "z": "tab_mgc_basic", + "g": "grp_drv_scaling", + "name": "set.scaling = normalized", + "props": [ + { "p": "topic", "vt": "str" }, + { "p": "payload", "v": "normalized", "vt": "str" } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.scaling", + "x": 260, + "y": 300, + "wires": [ + [ + "mgc_basic_node" + ] + ] + }, + { + "id": "inj_scaling_abs", + "type": "inject", + "z": "tab_mgc_basic", + "g": "grp_drv_scaling", + "name": "set.scaling = absolute", + "props": [ + { "p": "topic", "vt": "str" }, + { "p": "payload", "v": "absolute", "vt": "str" } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.scaling", + "x": 260, + "y": 340, + "wires": [ + [ + "mgc_basic_node" + ] + ] + }, + { + "id": "inj_demand_0", + "type": "inject", + "z": "tab_mgc_basic", + "g": "grp_drv_demand", + "name": "set.demand = 0 (stop)", + "props": [ + { "p": "topic", "vt": "str" }, + { "p": "payload", "v": "0", "vt": "num" } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.demand", + "x": 260, + "y": 460, + "wires": [ + [ + "mgc_basic_node" + ] + ] + }, + { + "id": "inj_demand_25", + "type": "inject", + "z": "tab_mgc_basic", + "g": "grp_drv_demand", + "name": "set.demand = 25 %", + "props": [ + { "p": "topic", "vt": "str" }, + { "p": "payload", "v": "25", "vt": "num" } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.demand", + "x": 260, + "y": 500, + "wires": [ + [ + "mgc_basic_node" + ] + ] + }, + { + "id": "inj_demand_50", + "type": "inject", + "z": "tab_mgc_basic", + "g": "grp_drv_demand", + "name": "set.demand = 50 %", + "props": [ + { "p": "topic", "vt": "str" }, + { "p": "payload", "v": "50", "vt": "num" } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.demand", + "x": 260, + "y": 540, + "wires": [ + [ + "mgc_basic_node" + ] + ] + }, + { + "id": "inj_demand_75", + "type": "inject", + "z": "tab_mgc_basic", + "g": "grp_drv_demand", + "name": "set.demand = 75 %", + "props": [ + { "p": "topic", "vt": "str" }, + { "p": "payload", "v": "75", "vt": "num" } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.demand", + "x": 260, + "y": 580, + "wires": [ + [ + "mgc_basic_node" + ] + ] + }, + { + "id": "inj_demand_100", + "type": "inject", + "z": "tab_mgc_basic", + "g": "grp_drv_demand", + "name": "set.demand = 100 %", + "props": [ + { "p": "topic", "vt": "str" }, + { "p": "payload", "v": "100", "vt": "num" } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.demand", + "x": 260, + "y": 620, + "wires": [ + [ + "mgc_basic_node" + ] + ] + }, + { + "id": "inj_setup_start", + "type": "inject", + "z": "tab_mgc_basic", + "g": "grp_setup", + "name": "Auto-start pumps", + "props": [ + { "p": "payload", "v": "go", "vt": "str" } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "1.5", + "topic": "", + "x": 220, + "y": 720, + "wires": [ + [ + "fn_setup_fanout" + ] + ] + }, + { + "id": "fn_setup_fanout", + "type": "function", + "z": "tab_mgc_basic", + "g": "grp_setup", + "name": "fan-out: virtualControl + startup β†’ A/B/C", + "func": "// Fire two messages per pump: set.mode = virtualControl, then cmd.startup.\n// Each output is a message array β€” Node-RED dispatches them sequentially.\nconst setMode = { topic: 'set.mode', payload: 'virtualControl' };\nconst startup = { topic: 'cmd.startup', payload: {} };\nreturn [\n [setMode, startup], // β†’ Pump A\n [setMode, startup], // β†’ Pump B\n [setMode, startup], // β†’ Pump C\n];\n", + "outputs": 3, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 480, + "y": 720, + "wires": [ + [ + "rm_basic_pump_a" + ], + [ + "rm_basic_pump_b" + ], + [ + "rm_basic_pump_c" + ] + ] + }, + { + "id": "rm_basic_pump_a", + "type": "rotatingMachine", + "z": "tab_mgc_basic", + "g": "grp_pump_a", + "name": "Pump A", + "speed": "1", + "startup": "2", + "warmup": "1", + "shutdown": "2", + "cooldown": "1", + "movementMode": "staticspeed", + "machineCurve": "", + "uuid": "mgc-basic-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": "info", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": "", + "distanceUnit": "m", + "distanceDescription": "", + "x": 760, + "y": 240, + "wires": [ + [], + [], + [ + "mgc_basic_node" + ] + ] + }, + { + "id": "rm_basic_pump_b", + "type": "rotatingMachine", + "z": "tab_mgc_basic", + "g": "grp_pump_b", + "name": "Pump B", + "speed": "1", + "startup": "2", + "warmup": "1", + "shutdown": "2", + "cooldown": "1", + "movementMode": "staticspeed", + "machineCurve": "", + "uuid": "mgc-basic-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": "info", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": "", + "distanceUnit": "m", + "distanceDescription": "", + "x": 760, + "y": 420, + "wires": [ + [], + [], + [ + "mgc_basic_node" + ] + ] + }, + { + "id": "rm_basic_pump_c", + "type": "rotatingMachine", + "z": "tab_mgc_basic", + "g": "grp_pump_c", + "name": "Pump C", + "speed": "1", + "startup": "2", + "warmup": "1", + "shutdown": "2", + "cooldown": "1", + "movementMode": "staticspeed", + "machineCurve": "", + "uuid": "mgc-basic-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": "info", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": "", + "distanceUnit": "m", + "distanceDescription": "", + "x": 760, + "y": 600, + "wires": [ + [], + [], + [ + "mgc_basic_node" + ] + ] + }, + { + "id": "mgc_basic_node", + "type": "machineGroupControl", + "z": "tab_mgc_basic", + "g": "grp_mgc_unit", + "name": "Machine Group", + "processOutputFormat": "process", + "dbaseOutputFormat": "influxdb", + "mode": "optimalControl", + "scaling": "normalized", + "uuid": "", + "supplier": "", + "category": "", + "assetType": "", + "model": "", + "unit": "", + "enableLog": false, + "logLevel": "info", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": "", + "distanceUnit": "m", + "distanceDescription": "", + "x": 1050, + "y": 420, + "wires": [ + [ + "dbg_port0" + ], + [ + "dbg_port1" + ], + [ + "dbg_port2" + ] + ] + }, + { + "id": "dbg_port0", + "type": "debug", + "z": "tab_mgc_basic", + "g": "grp_dbg", + "name": "Port 0: Process", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "x": 1340, + "y": 380, + "wires": [] + }, + { + "id": "dbg_port1", + "type": "debug", + "z": "tab_mgc_basic", + "g": "grp_dbg", + "name": "Port 1: InfluxDB", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "x": 1340, + "y": 440, + "wires": [] + }, + { + "id": "dbg_port2", + "type": "debug", + "z": "tab_mgc_basic", + "g": "grp_dbg", + "name": "Port 2: Parent reg", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "x": 1350, + "y": 500, + "wires": [] + }, + { + "id": "mgc_global_cfg", + "type": "global-config", + "env": [], + "modules": { + "EVOLV": "1.0.29" + } + } +] diff --git a/examples/02-Dashboard.json b/examples/02-Dashboard.json new file mode 100644 index 0000000..1b04f22 --- /dev/null +++ b/examples/02-Dashboard.json @@ -0,0 +1,1205 @@ +[ + { + "id": "tab_mgc_dash", + "type": "tab", + "label": "MGC - Dashboard", + "disabled": false, + "info": "Tier 2: one machineGroupControl coordinating three rotatingMachine pumps, driven by a FlowFuse dashboard. Mode + scaling buttons, demand slider, live status rows, three trend charts, and a raw-output table." + }, + { + "id": "grp_mgc_unit", + "type": "group", + "z": "tab_mgc_dash", + "name": "Machine Group (Unit)", + "style": { + "label": true, + "stroke": "#000000", + "fill": "#50a8d9", + "fill-opacity": "0.10" + }, + "nodes": [ + "mgc_dash_node" + ], + "x": 974, + "y": 379, + "w": 152, + "h": 122 + }, + { + "id": "grp_pump_a", + "type": "group", + "z": "tab_mgc_dash", + "name": "Pump A (Equipment)", + "style": { + "label": true, + "stroke": "#000000", + "fill": "#86bbdd", + "fill-opacity": "0.10" + }, + "nodes": [ + "rm_dash_pump_a" + ], + "x": 694, + "y": 219, + "w": 142, + "h": 82 + }, + { + "id": "grp_pump_b", + "type": "group", + "z": "tab_mgc_dash", + "name": "Pump B (Equipment)", + "style": { + "label": true, + "stroke": "#000000", + "fill": "#86bbdd", + "fill-opacity": "0.10" + }, + "nodes": [ + "rm_dash_pump_b" + ], + "x": 694, + "y": 399, + "w": 142, + "h": 82 + }, + { + "id": "grp_pump_c", + "type": "group", + "z": "tab_mgc_dash", + "name": "Pump C (Equipment)", + "style": { + "label": true, + "stroke": "#000000", + "fill": "#86bbdd", + "fill-opacity": "0.10" + }, + "nodes": [ + "rm_dash_pump_c" + ], + "x": 694, + "y": 579, + "w": 142, + "h": 82 + }, + { + "id": "grp_drv_mode", + "type": "group", + "z": "tab_mgc_dash", + "name": "1. Control mode", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "ui_btn_mode_optimal", + "ui_btn_mode_priority" + ], + "x": 94, + "y": 99, + "w": 312, + "h": 122 + }, + { + "id": "grp_drv_scaling", + "type": "group", + "z": "tab_mgc_dash", + "name": "2. Scaling", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "ui_btn_scaling_norm", + "ui_btn_scaling_abs" + ], + "x": 94, + "y": 259, + "w": 312, + "h": 122 + }, + { + "id": "grp_drv_demand", + "type": "group", + "z": "tab_mgc_dash", + "name": "3. Operator demand", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "ui_slider_demand", + "ui_btn_demand_stop" + ], + "x": 94, + "y": 419, + "w": 312, + "h": 122 + }, + { + "id": "grp_setup", + "type": "group", + "z": "tab_mgc_dash", + "name": "Setup β€” once on deploy + manual re-init", + "style": { + "stroke": "#666666", + "fill": "#dddddd", + "fill-opacity": "0.20", + "label": true, + "color": "#333333" + }, + "nodes": [ + "inj_setup_start", + "ui_btn_setup_init", + "fn_setup_fanout" + ], + "x": 94, + "y": 579, + "w": 532, + "h": 122 + }, + { + "id": "grp_status_panel", + "type": "group", + "z": "tab_mgc_dash", + "name": "Live status, trends, raw output", + "style": { + "stroke": "#666666", + "fill": "#bde0fe", + "fill-opacity": "0.20", + "label": true, + "color": "#333333" + }, + "nodes": [ + "fn_status_split", + "ui_txt_mode", + "ui_txt_scaling", + "ui_txt_flow", + "ui_txt_power", + "ui_txt_capacity", + "ui_txt_machines", + "ui_txt_bep", + "ui_chart_flow", + "ui_chart_power", + "ui_chart_bep", + "ui_tpl_raw" + ], + "x": 1234, + "y": 79, + "w": 712, + "h": 642 + }, + { + "id": "grp_dbg", + "type": "group", + "z": "tab_mgc_dash", + "name": "Debug outputs (sidebar)", + "style": { + "stroke": "#666666", + "fill": "#d1d1d1", + "fill-opacity": "0.2", + "label": true, + "color": "#333333" + }, + "nodes": [ + "dbg_port0", + "dbg_port1", + "dbg_port2" + ], + "x": 1234, + "y": 759, + "w": 252, + "h": 202 + }, + { + "id": "cmt_title", + "type": "comment", + "z": "tab_mgc_dash", + "name": "MGC β€” Dashboard (Tier 2)", + "info": "Same command surface as the Basic flow, driven by a FlowFuse dashboard.\n\nOpen /dashboard/mgc-basic after deploy.\n\nCONTROLS panel\n- Mode: optimalControl / priorityControl β†’ set.mode\n- Scaling: normalized (0–100 %) / absolute (mΒ³/h) β†’ set.scaling\n- Demand slider 0–100 β†’ set.demand (interpretation depends on scaling)\n- Stop button (set.demand = 0) and Initialize pumps button\n\nSTATUS panel\n- Mode / Scaling / Total flow / Total power / Capacity (Qmin–Qmax) / Active machines / BEP distance (rel %)\n\nTRENDS panel\n- Flow (mΒ³/h) β€” predicted aggregate vs capacity\n- Power (kW)\n- BEP distance (rel %)\n\nRAW OUTPUT panel\n- Full key/value dump of the latest MGC Port 0 cache (sorted).\n\nPORTS (preserved for inspection)\n- Port 0: process output (changed fields only)\n- Port 1: InfluxDB-shaped {measurement, fields, tags, timestamp}\n- Port 2: parent registration (when wired into a pumpingStation)", + "x": 1100, + "y": 280, + "wires": [] + }, + { + "id": "ui_base_mgc", + "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_mgc", + "type": "ui-theme", + "name": "EVOLV Basic Theme", + "colors": { + "surface": "#ffffff", + "primary": "#50a8d9", + "bgPage": "#eeeeee", + "groupBg": "#ffffff", + "groupOutline": "#cccccc" + }, + "sizes": { + "density": "default", + "pagePadding": "14px", + "groupGap": "14px", + "groupBorderRadius": "6px", + "widgetGap": "12px" + } + }, + { + "id": "ui_page_mgc", + "type": "ui-page", + "name": "MGC Basic", + "ui": "ui_base_mgc", + "path": "/mgc-basic", + "icon": "settings-input-component", + "layout": "grid", + "theme": "ui_theme_mgc", + "breakpoints": [ + { + "name": "Default", + "px": "0", + "cols": "12" + } + ], + "order": 1, + "className": "" + }, + { + "id": "ui_group_ctrl", + "type": "ui-group", + "name": "Controls", + "page": "ui_page_mgc", + "width": "6", + "height": "1", + "order": 1, + "showTitle": true, + "className": "" + }, + { + "id": "ui_group_status", + "type": "ui-group", + "name": "Status", + "page": "ui_page_mgc", + "width": "6", + "height": "1", + "order": 2, + "showTitle": true, + "className": "" + }, + { + "id": "ui_group_trends", + "type": "ui-group", + "name": "Trends", + "page": "ui_page_mgc", + "width": "12", + "height": "1", + "order": 3, + "showTitle": true, + "className": "" + }, + { + "id": "ui_group_raw", + "type": "ui-group", + "name": "Raw output (Port 0 cache)", + "page": "ui_page_mgc", + "width": "12", + "height": "1", + "order": 4, + "showTitle": true, + "className": "" + }, + { + "id": "ui_btn_mode_optimal", + "type": "ui-button", + "z": "tab_mgc_dash", + "g": "grp_drv_mode", + "group": "ui_group_ctrl", + "name": "Mode: optimalControl", + "label": "Mode: optimalControl", + "order": 1, + "width": "3", + "height": "1", + "emulateClick": false, + "tooltip": "Best-combination optimiser (BEP-Gravitation / NCog)", + "color": "", + "bgcolor": "", + "icon": "auto_fix_high", + "payload": "optimalControl", + "payloadType": "str", + "topic": "set.mode", + "topicType": "str", + "x": 260, + "y": 140, + "wires": [ + [ + "mgc_dash_node" + ] + ] + }, + { + "id": "ui_btn_mode_priority", + "type": "ui-button", + "z": "tab_mgc_dash", + "g": "grp_drv_mode", + "group": "ui_group_ctrl", + "name": "Mode: priorityControl", + "label": "Mode: priorityControl", + "order": 2, + "width": "3", + "height": "1", + "emulateClick": false, + "tooltip": "Sequential equal-flow control by priority list", + "color": "", + "bgcolor": "", + "icon": "format_list_numbered", + "payload": "priorityControl", + "payloadType": "str", + "topic": "set.mode", + "topicType": "str", + "x": 260, + "y": 180, + "wires": [ + [ + "mgc_dash_node" + ] + ] + }, + { + "id": "ui_btn_scaling_norm", + "type": "ui-button", + "z": "tab_mgc_dash", + "g": "grp_drv_scaling", + "group": "ui_group_ctrl", + "name": "Scaling: normalized", + "label": "Scaling: normalized", + "order": 3, + "width": "3", + "height": "1", + "emulateClick": false, + "tooltip": "Interpret set.demand as 0–100 % of group capacity", + "color": "", + "bgcolor": "", + "icon": "percent", + "payload": "normalized", + "payloadType": "str", + "topic": "set.scaling", + "topicType": "str", + "x": 260, + "y": 300, + "wires": [ + [ + "mgc_dash_node" + ] + ] + }, + { + "id": "ui_btn_scaling_abs", + "type": "ui-button", + "z": "tab_mgc_dash", + "g": "grp_drv_scaling", + "group": "ui_group_ctrl", + "name": "Scaling: absolute", + "label": "Scaling: absolute", + "order": 4, + "width": "3", + "height": "1", + "emulateClick": false, + "tooltip": "Interpret set.demand as mΒ³/h (capped by group min/max)", + "color": "", + "bgcolor": "", + "icon": "exposure", + "payload": "absolute", + "payloadType": "str", + "topic": "set.scaling", + "topicType": "str", + "x": 260, + "y": 340, + "wires": [ + [ + "mgc_dash_node" + ] + ] + }, + { + "id": "ui_slider_demand", + "type": "ui-slider", + "z": "tab_mgc_dash", + "g": "grp_drv_demand", + "group": "ui_group_ctrl", + "name": "Demand", + "label": "Demand", + "order": 5, + "width": "6", + "height": "1", + "topic": "set.demand", + "topicType": "str", + "icon": "speed", + "iconType": "fa", + "min": 0, + "max": 100, + "step": 1, + "thumbLabel": "always", + "tickLabel": "true", + "showTicks": false, + "passthru": true, + "outs": "all", + "className": "", + "x": 260, + "y": 460, + "wires": [ + [ + "mgc_dash_node" + ] + ] + }, + { + "id": "ui_btn_demand_stop", + "type": "ui-button", + "z": "tab_mgc_dash", + "g": "grp_drv_demand", + "group": "ui_group_ctrl", + "name": "Demand = 0 (Stop)", + "label": "Stop (demand = 0)", + "order": 6, + "width": "6", + "height": "1", + "emulateClick": false, + "tooltip": "Send set.demand = 0 β€” MGC calls turnOffAllMachines", + "color": "#ffffff", + "bgcolor": "#cc6600", + "icon": "stop", + "payload": "0", + "payloadType": "num", + "topic": "set.demand", + "topicType": "str", + "x": 260, + "y": 500, + "wires": [ + [ + "mgc_dash_node" + ] + ] + }, + { + "id": "ui_btn_setup_init", + "type": "ui-button", + "z": "tab_mgc_dash", + "g": "grp_setup", + "group": "ui_group_ctrl", + "name": "Initialize pumps", + "label": "Initialize pumps (virtualControl + startup)", + "order": 7, + "width": "12", + "height": "1", + "emulateClick": false, + "tooltip": "Re-runs the once-on-deploy setup: virtualControl mode + cmd.startup on all three pumps", + "color": "", + "bgcolor": "", + "icon": "play_arrow", + "payload": "go", + "payloadType": "str", + "topic": "", + "topicType": "str", + "x": 260, + "y": 620, + "wires": [ + [ + "fn_setup_fanout" + ] + ] + }, + { + "id": "inj_setup_start", + "type": "inject", + "z": "tab_mgc_dash", + "g": "grp_setup", + "name": "Auto-start pumps", + "props": [ + { "p": "payload", "v": "go", "vt": "str" } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "1.5", + "topic": "", + "x": 260, + "y": 660, + "wires": [ + [ + "fn_setup_fanout" + ] + ] + }, + { + "id": "fn_setup_fanout", + "type": "function", + "z": "tab_mgc_dash", + "g": "grp_setup", + "name": "fan-out: virtualControl + startup β†’ A/B/C", + "func": "// Fire two messages per pump: set.mode = virtualControl, then cmd.startup.\nconst setMode = { topic: 'set.mode', payload: 'virtualControl' };\nconst startup = { topic: 'cmd.startup', payload: {} };\nreturn [\n [setMode, startup], // β†’ Pump A\n [setMode, startup], // β†’ Pump B\n [setMode, startup], // β†’ Pump C\n];\n", + "outputs": 3, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 500, + "y": 640, + "wires": [ + [ + "rm_dash_pump_a" + ], + [ + "rm_dash_pump_b" + ], + [ + "rm_dash_pump_c" + ] + ] + }, + { + "id": "fn_status_split", + "type": "function", + "z": "tab_mgc_dash", + "g": "grp_status_panel", + "name": "fan-out Port 0 (status + charts + raw)", + "func": "// MGC Port 0 emits delta-only. Cache last-known so deltas never blank a\n// row, then fan out one msg per ui-text / ui-chart / ui-template slot.\nconst cache = context.get('cache') || {};\nconst p = msg.payload || {};\nfor (const k in p) cache[k] = p[k];\ncontext.set('cache', cache);\n\nconst num = (v, dp, unit) => {\n const n = +v;\n if (!Number.isFinite(n)) return 'β€”';\n return n.toFixed(dp) + (unit ? ' ' + unit : '');\n};\n\nconst mode = cache.mode || 'β€”';\nconst scaling = cache.scaling || 'β€”';\nconst flow = cache['atEquipment_predicted_flow'];\nconst power = cache['atEquipment_predicted_power'];\nconst qMin = cache.flowCapacityMin;\nconst qMax = cache.flowCapacityMax;\nconst nAct = cache.machineCountActive;\nconst nTot = cache.machineCount;\nconst bepRel = cache.relDistFromPeak;\n\nconst chart = (topic, v) => Number.isFinite(+v) ? { topic, payload: +v } : null;\n\nconst rawRows = Object.keys(cache).sort().map((k) => {\n const v = cache[k];\n let display;\n if (v === null || v === undefined) display = 'β€”';\n else if (typeof v === 'number') display = Number.isInteger(v) ? String(v) : v.toFixed(4);\n else display = String(v);\n return { key: k, value: display };\n});\n\nreturn [\n // 0–6: status text\n { payload: mode },\n { payload: scaling },\n { payload: num(flow, 1, 'mΒ³/h') },\n { payload: num(power, 2, 'kW') },\n { payload: Number.isFinite(+qMax) ? `${num(qMin, 1)} – ${num(qMax, 1, 'mΒ³/h')}` : 'β€”' },\n { payload: Number.isFinite(+nAct) && Number.isFinite(+nTot) ? `${nAct} / ${nTot}` : 'β€”' },\n { payload: num(bepRel, 1, '%') },\n // 7–9: charts (flow vs capacity / power / bep distance)\n chart('Flow', flow),\n chart('Capacity', qMax),\n chart('Power', power),\n chart('BEP rel%', bepRel),\n // 11: raw rows for the ui-template\n { payload: rawRows },\n];\n", + "outputs": 12, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1300, + "y": 140, + "wires": [ + [ + "ui_txt_mode" + ], + [ + "ui_txt_scaling" + ], + [ + "ui_txt_flow" + ], + [ + "ui_txt_power" + ], + [ + "ui_txt_capacity" + ], + [ + "ui_txt_machines" + ], + [ + "ui_txt_bep" + ], + [ + "ui_chart_flow" + ], + [ + "ui_chart_flow" + ], + [ + "ui_chart_power" + ], + [ + "ui_chart_bep" + ], + [ + "ui_tpl_raw" + ] + ] + }, + { + "id": "ui_txt_mode", + "type": "ui-text", + "z": "tab_mgc_dash", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 1, + "width": "6", + "height": "1", + "name": "Mode", + "label": "Mode", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#1F4E79", + "x": 1560, + "y": 100, + "wires": [] + }, + { + "id": "ui_txt_scaling", + "type": "ui-text", + "z": "tab_mgc_dash", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 2, + "width": "6", + "height": "1", + "name": "Scaling", + "label": "Scaling", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#1F4E79", + "x": 1560, + "y": 140, + "wires": [] + }, + { + "id": "ui_txt_flow", + "type": "ui-text", + "z": "tab_mgc_dash", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 3, + "width": "6", + "height": "1", + "name": "Total flow", + "label": "Total flow", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#1F4E79", + "x": 1570, + "y": 180, + "wires": [] + }, + { + "id": "ui_txt_power", + "type": "ui-text", + "z": "tab_mgc_dash", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 4, + "width": "6", + "height": "1", + "name": "Total power", + "label": "Total power", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#1F4E79", + "x": 1580, + "y": 220, + "wires": [] + }, + { + "id": "ui_txt_capacity", + "type": "ui-text", + "z": "tab_mgc_dash", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 5, + "width": "6", + "height": "1", + "name": "Capacity (Qmin–Qmax)", + "label": "Capacity", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#1F4E79", + "x": 1620, + "y": 260, + "wires": [] + }, + { + "id": "ui_txt_machines", + "type": "ui-text", + "z": "tab_mgc_dash", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 6, + "width": "6", + "height": "1", + "name": "Machines (active / total)", + "label": "Machines", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#1F4E79", + "x": 1630, + "y": 300, + "wires": [] + }, + { + "id": "ui_txt_bep", + "type": "ui-text", + "z": "tab_mgc_dash", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 7, + "width": "6", + "height": "1", + "name": "BEP distance (rel)", + "label": "BEP distance", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#7D3C98", + "x": 1600, + "y": 340, + "wires": [] + }, + { + "id": "ui_chart_flow", + "type": "ui-chart", + "z": "tab_mgc_dash", + "g": "grp_status_panel", + "group": "ui_group_trends", + "name": "Flow vs capacity", + "label": "Flow (mΒ³/h) β€” predicted vs capacity", + "order": 1, + "width": 6, + "height": 4, + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisLabel": "time", + "xAxisType": "time", + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "xAxisFormat": "", + "xAxisFormatType": "auto", + "yAxisLabel": "mΒ³/h", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "xmin": "", + "xmax": "", + "ymin": "", + "ymax": "", + "removeOlder": "15", + "removeOlderUnit": "60", + "removeOlderPoints": "", + "bins": 10, + "action": "append", + "stackSeries": false, + "pointShape": "circle", + "pointRadius": 4, + "interpolation": "linear", + "showLegend": true, + "className": "", + "colors": [ + "#0095FF", + "#cccccc", + "#FF7F0E", + "#2CA02C", + "#A347E1", + "#D62728", + "#FF9896", + "#9467BD", + "#C5B0D5" + ], + "textColor": [ + "#666666" + ], + "textColorDefault": true, + "gridColor": [ + "#e5e5e5" + ], + "gridColorDefault": true, + "x": 1600, + "y": 400, + "wires": [] + }, + { + "id": "ui_chart_power", + "type": "ui-chart", + "z": "tab_mgc_dash", + "g": "grp_status_panel", + "group": "ui_group_trends", + "name": "Power", + "label": "Power (kW)", + "order": 2, + "width": 6, + "height": 4, + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisLabel": "time", + "xAxisType": "time", + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "xAxisFormat": "", + "xAxisFormatType": "auto", + "yAxisLabel": "kW", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "xmin": "", + "xmax": "", + "ymin": "", + "ymax": "", + "removeOlder": "15", + "removeOlderUnit": "60", + "removeOlderPoints": "", + "bins": 10, + "action": "append", + "stackSeries": false, + "pointShape": "circle", + "pointRadius": 4, + "interpolation": "linear", + "showLegend": false, + "className": "", + "colors": [ + "#2CA02C", + "#FF0000", + "#FF7F0E", + "#0095FF", + "#A347E1", + "#D62728", + "#FF9896", + "#9467BD", + "#C5B0D5" + ], + "textColor": [ + "#666666" + ], + "textColorDefault": true, + "gridColor": [ + "#e5e5e5" + ], + "gridColorDefault": true, + "x": 1580, + "y": 440, + "wires": [] + }, + { + "id": "ui_chart_bep", + "type": "ui-chart", + "z": "tab_mgc_dash", + "g": "grp_status_panel", + "group": "ui_group_trends", + "name": "BEP distance (rel %)", + "label": "BEP distance (rel %)", + "order": 3, + "width": 6, + "height": 4, + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisLabel": "time", + "xAxisType": "time", + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "xAxisFormat": "", + "xAxisFormatType": "auto", + "yAxisLabel": "%", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "xmin": "", + "xmax": "", + "ymin": "", + "ymax": "", + "removeOlder": "15", + "removeOlderUnit": "60", + "removeOlderPoints": "", + "bins": 10, + "action": "append", + "stackSeries": false, + "pointShape": "circle", + "pointRadius": 4, + "interpolation": "linear", + "showLegend": false, + "className": "", + "colors": [ + "#A347E1", + "#FF0000", + "#FF7F0E", + "#2CA02C", + "#0095FF", + "#D62728", + "#FF9896", + "#9467BD", + "#C5B0D5" + ], + "textColor": [ + "#666666" + ], + "textColorDefault": true, + "gridColor": [ + "#e5e5e5" + ], + "gridColorDefault": true, + "x": 1610, + "y": 480, + "wires": [] + }, + { + "id": "ui_tpl_raw", + "type": "ui-template", + "z": "tab_mgc_dash", + "g": "grp_status_panel", + "group": "ui_group_raw", + "name": "Raw output table", + "order": 1, + "width": "12", + "height": "8", + "head": "", + "format": "\n\n\n", + "storeOutMessages": true, + "passthru": true, + "resendOnRefresh": true, + "templateScope": "local", + "className": "", + "x": 1620, + "y": 520, + "wires": [ + [] + ] + }, + { + "id": "rm_dash_pump_a", + "type": "rotatingMachine", + "z": "tab_mgc_dash", + "g": "grp_pump_a", + "name": "Pump A", + "speed": "1", + "startup": "2", + "warmup": "1", + "shutdown": "2", + "cooldown": "1", + "movementMode": "staticspeed", + "machineCurve": "", + "uuid": "mgc-dash-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": "info", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": "", + "distanceUnit": "m", + "distanceDescription": "", + "x": 760, + "y": 260, + "wires": [ + [], + [], + [ + "mgc_dash_node" + ] + ] + }, + { + "id": "rm_dash_pump_b", + "type": "rotatingMachine", + "z": "tab_mgc_dash", + "g": "grp_pump_b", + "name": "Pump B", + "speed": "1", + "startup": "2", + "warmup": "1", + "shutdown": "2", + "cooldown": "1", + "movementMode": "staticspeed", + "machineCurve": "", + "uuid": "mgc-dash-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": "info", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": "", + "distanceUnit": "m", + "distanceDescription": "", + "x": 760, + "y": 440, + "wires": [ + [], + [], + [ + "mgc_dash_node" + ] + ] + }, + { + "id": "rm_dash_pump_c", + "type": "rotatingMachine", + "z": "tab_mgc_dash", + "g": "grp_pump_c", + "name": "Pump C", + "speed": "1", + "startup": "2", + "warmup": "1", + "shutdown": "2", + "cooldown": "1", + "movementMode": "staticspeed", + "machineCurve": "", + "uuid": "mgc-dash-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": "info", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": "", + "distanceUnit": "m", + "distanceDescription": "", + "x": 760, + "y": 620, + "wires": [ + [], + [], + [ + "mgc_dash_node" + ] + ] + }, + { + "id": "mgc_dash_node", + "type": "machineGroupControl", + "z": "tab_mgc_dash", + "g": "grp_mgc_unit", + "name": "Machine Group", + "processOutputFormat": "process", + "dbaseOutputFormat": "influxdb", + "mode": "optimalControl", + "scaling": "normalized", + "uuid": "", + "supplier": "", + "category": "", + "assetType": "", + "model": "", + "unit": "", + "enableLog": false, + "logLevel": "info", + "positionVsParent": "atEquipment", + "positionIcon": "", + "hasDistance": false, + "distance": "", + "distanceUnit": "m", + "distanceDescription": "", + "x": 1050, + "y": 440, + "wires": [ + [ + "dbg_port0", + "fn_status_split" + ], + [ + "dbg_port1" + ], + [ + "dbg_port2" + ] + ] + }, + { + "id": "dbg_port0", + "type": "debug", + "z": "tab_mgc_dash", + "g": "grp_dbg", + "name": "Port 0: Process", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "x": 1340, + "y": 800, + "wires": [] + }, + { + "id": "dbg_port1", + "type": "debug", + "z": "tab_mgc_dash", + "g": "grp_dbg", + "name": "Port 1: InfluxDB", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "x": 1340, + "y": 860, + "wires": [] + }, + { + "id": "dbg_port2", + "type": "debug", + "z": "tab_mgc_dash", + "g": "grp_dbg", + "name": "Port 2: Parent reg", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "x": 1350, + "y": 920, + "wires": [] + }, + { + "id": "mgc_global_cfg", + "type": "global-config", + "env": [], + "modules": { + "EVOLV": "1.0.29" + } + } +] diff --git a/examples/README.md b/examples/README.md index f94a51a..ef00b45 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,8 +1,51 @@ -# machineGroupControl Example Flows +# machineGroupControl - Example Flows -Import-ready Node-RED examples for machineGroupControl. +Import-ready Node-RED examples for `machineGroupControl` (MGC). MGC is not a standalone node β€” it needs at least one `rotatingMachine` child to dispatch demand to. Both flows below ship three child pumps. ## Files -- basic.flow.json -- integration.flow.json -- edge.flow.json + +| File | Tier | What it shows | +|---|---|---| +| `01-Basic.json` | 1 | One MGC + three `rotatingMachine` pumps driven by inject buttons. Setup once-fires `virtualControl` + `cmd.startup` on all three pumps; mode / scaling / demand are then driven by buttons. | +| `02-Dashboard.json` | 2 | Same command surface driven by a FlowFuse Dashboard 2.0 page β€” mode + scaling buttons, demand slider, live status rows, three trend charts, and a raw-output table. | + +## Prerequisites + +- Node-RED with the EVOLV package installed (`machineGroupControl` and `rotatingMachine` registered). +- For `02-Dashboard.json`: `@flowfuse/node-red-dashboard` (Dashboard 2.0). + +## Load a flow + +```bash +curl -X POST -H 'Content-Type: application/json' \ + --data @nodes/machineGroupControl/examples/01-Basic.json \ + http://localhost:1880/flows +``` + +Or in the editor: Menu β†’ Import β†’ drag the file β†’ Import. + +## Canonical command surface + +| Topic | Aliases | Payload | What it does | +|---|---|---|---| +| `set.mode` | `setMode` | `"optimalControl"`, `"priorityControl"`, `"prioritypercentagecontrol"`, `"maintenance"` | Switch dispatch strategy | +| `set.scaling` | `setScaling` | `"normalized"`, `"absolute"` | Interpret demand as 0–100 % vs mΒ³/h | +| `set.demand` | `Qd` | number | Operator demand setpoint | +| `child.register` | `registerChild` | child node id (string) | Manually register a child (Port 2 wiring does this automatically) | + +## 01-Basic β€” what to try + +1. Deploy. After ~1.5 s the Setup group auto-fires, putting all three pumps in `virtualControl` mode + sending `cmd.startup` to each. +2. Click `set.demand = 50 %` β€” MGC's `optimalControl` picks the best pump combination by BEP-gravitation and dispatches `flowmovement` to the selected pumps. +3. Click `set.demand = 100 %` β€” MGC switches to a higher combination, possibly engaging an extra pump. +4. Switch mode to `priorityControl` and try the same demands β€” pumps now run equal-flow by priority order. +5. Switch scaling to `absolute` β€” set.demand is now interpreted as mΒ³/h (capped at the group min / max). +6. `set.demand = 0` β€” MGC calls `turnOffAllMachines`, all pumps shut down. + +## 02-Dashboard β€” what to try + +1. Deploy β†’ open `http://localhost:1880/dashboard/mgc-basic`. +2. The dashboard auto-initialises the pumps; the `Initialize pumps` button on the page re-runs the setup manually. +3. Drag the **Demand** slider β€” MGC dispatches and the Flow / Power / BEP charts react. +4. Switch modes and scalings via the buttons; the Mode / Scaling rows in the Status panel reflect the change. +5. Inspect the **Raw output** table for the full Port 0 surface (every field MGC emits, including `flowCapacityMax`, `machineCountActive`, `absDistFromPeak`, `relDistFromPeak`). diff --git a/examples/basic.flow.json b/examples/basic.flow.json deleted file mode 100644 index ad5b77f..0000000 --- a/examples/basic.flow.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - {"id":"machineGroupControl_basic_tab","type":"tab","label":"machineGroupControl basic","disabled":false,"info":"machineGroupControl basic example"}, - {"id":"machineGroupControl_basic_node","type":"machineGroupControl","z":"machineGroupControl_basic_tab","name":"machineGroupControl basic","x":420,"y":180,"wires":[["machineGroupControl_basic_dbg"]]}, - {"id":"machineGroupControl_basic_inj","type":"inject","z":"machineGroupControl_basic_tab","name":"basic trigger","props":[{"p":"topic","vt":"str"},{"p":"payload","vt":"str"}],"topic":"ping","payload":"1","payloadType":"str","x":160,"y":180,"wires":[["machineGroupControl_basic_node"]]}, - {"id":"machineGroupControl_basic_dbg","type":"debug","z":"machineGroupControl_basic_tab","name":"machineGroupControl basic debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":660,"y":180,"wires":[]} -] diff --git a/examples/edge.flow.json b/examples/edge.flow.json deleted file mode 100644 index 3f7839d..0000000 --- a/examples/edge.flow.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - {"id":"machineGroupControl_edge_tab","type":"tab","label":"machineGroupControl edge","disabled":false,"info":"machineGroupControl edge example"}, - {"id":"machineGroupControl_edge_node","type":"machineGroupControl","z":"machineGroupControl_edge_tab","name":"machineGroupControl edge","x":420,"y":180,"wires":[["machineGroupControl_edge_dbg"]]}, - {"id":"machineGroupControl_edge_inj","type":"inject","z":"machineGroupControl_edge_tab","name":"unknown topic","props":[{"p":"topic","vt":"str"},{"p":"payload","vt":"str"}],"topic":"doesNotExist","payload":"x","payloadType":"str","x":170,"y":180,"wires":[["machineGroupControl_edge_node"]]}, - {"id":"machineGroupControl_edge_dbg","type":"debug","z":"machineGroupControl_edge_tab","name":"machineGroupControl edge debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":660,"y":180,"wires":[]} -] diff --git a/examples/integration.flow.json b/examples/integration.flow.json deleted file mode 100644 index 58da112..0000000 --- a/examples/integration.flow.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - {"id":"machineGroupControl_int_tab","type":"tab","label":"machineGroupControl integration","disabled":false,"info":"machineGroupControl integration example"}, - {"id":"machineGroupControl_int_node","type":"machineGroupControl","z":"machineGroupControl_int_tab","name":"machineGroupControl integration","x":420,"y":180,"wires":[["machineGroupControl_int_dbg"]]}, - {"id":"machineGroupControl_int_inj","type":"inject","z":"machineGroupControl_int_tab","name":"registerChild","props":[{"p":"topic","vt":"str"},{"p":"payload","vt":"str"}],"topic":"registerChild","payload":"example-child-id","payloadType":"str","x":170,"y":180,"wires":[["machineGroupControl_int_node"]]}, - {"id":"machineGroupControl_int_dbg","type":"debug","z":"machineGroupControl_int_tab","name":"machineGroupControl integration debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":680,"y":180,"wires":[]} -] diff --git a/mgc.html b/mgc.html index 5a0344d..5015782 100644 --- a/mgc.html +++ b/mgc.html @@ -21,6 +21,10 @@ processOutputFormat: { value: "process" }, dbaseOutputFormat: { value: "influxdb" }, + // Control strategy + mode: { value: "optimalControl" }, // optimalControl | priorityControl | prioritypercentagecontrol | maintenance + scaling: { value: "normalized" }, // normalized (0–100 %) | absolute (mΒ³/h) + //define asset properties uuid: { value: "" }, supplier: { value: "" }, @@ -40,7 +44,7 @@ distance: { value: 0 }, distanceUnit: { value: "m" }, distanceDescription: { value: "" } - + }, inputs:1, outputs:3, @@ -84,6 +88,24 @@