diff --git a/examples/01-Basic.json b/examples/01-Basic.json index 56a4223..4bb75bd 100644 --- a/examples/01-Basic.json +++ b/examples/01-Basic.json @@ -1,340 +1,479 @@ [ - { - "id": "ps_basic_tab", - "type": "tab", - "label": "PumpingStation - Basic", - "disabled": false, - "info": "Tier 1: single pumpingStation node driven by inject nodes only. Demonstrates the canonical Phase-2 topic API: set.mode, set.inflow, set.demand." - }, - { - "id": "ps_basic_title", - "type": "comment", - "z": "ps_basic_tab", - "name": "PumpingStation - Basic\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nA 50 m³ basin (3.5 m tall, inflow at 3.0 m, outflow at 0.2 m,\noverflow at 3.2 m). controlMode = levelbased, manual demand allowed\nonly when set.mode = manual.\n\nHOW TO USE:\n 1. Deploy the flow.\n 2. Click \"set.mode = manual\" so set.demand is honoured.\n 3. Click \"set.inflow = 60 m3/h\" to push wastewater into the basin.\n 4. Watch the basin fill on Port 0 (level, volume, percControl rise).\n 5. Click \"calibrate volume 25 m3\" to jump straight to half-full.\n\nAliases (changemode, q_in, Qd, …) still work but log a deprecation\nwarning - fresh flows use the canonical names.", - "info": "", - "x": 600, - "y": 40, - "wires": [] - }, - { - "id": "ps_basic_inj_mode", - "type": "inject", - "z": "ps_basic_tab", - "name": "set.mode = manual", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "manual", - "vt": "str" - } - ], - "topic": "set.mode", - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": "", - "x": 200, - "y": 160, - "wires": [ - [ - "ps_basic_node" - ] - ] - }, - { - "id": "ps_basic_inj_mode_lvl", - "type": "inject", - "z": "ps_basic_tab", - "name": "set.mode = levelbased", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "levelbased", - "vt": "str" - } - ], - "topic": "set.mode", - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": "", - "x": 220, - "y": 200, - "wires": [ - [ - "ps_basic_node" - ] - ] - }, - { - "id": "ps_basic_inj_inflow", - "type": "inject", - "z": "ps_basic_tab", - "name": "set.inflow = 60 m3/h", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "60", - "vt": "num" - } - ], - "topic": "set.inflow", - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": "", - "x": 200, - "y": 260, - "wires": [ - [ - "ps_basic_node" - ] - ] - }, - { - "id": "ps_basic_inj_demand", - "type": "inject", - "z": "ps_basic_tab", - "name": "set.demand = 40 %", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "40", - "vt": "num" - } - ], - "topic": "set.demand", - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": "", - "x": 200, - "y": 300, - "wires": [ - [ - "ps_basic_node" - ] - ] - }, - { - "id": "ps_basic_inj_calvol", - "type": "inject", - "z": "ps_basic_tab", - "name": "calibrate volume 25 m3", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "25", - "vt": "num" - } - ], - "topic": "cmd.calibrate.volume", - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": "", - "x": 220, - "y": 360, - "wires": [ - [ - "ps_basic_node" - ] - ] - }, - { - "id": "ps_basic_inj_callvl", - "type": "inject", - "z": "ps_basic_tab", - "name": "calibrate level 1.5 m", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "1.5", - "vt": "num" - } - ], - "topic": "cmd.calibrate.level", - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": "", - "x": 220, - "y": 400, - "wires": [ - [ - "ps_basic_node" - ] - ] - }, - { - "id": "ps_basic_node", - "type": "pumpingStation", - "z": "ps_basic_tab", - "name": "Pumping Station", - "simulator": false, - "basinVolume": 50, - "basinHeight": 3.5, - "inflowLevel": 3, - "outflowLevel": 0.2, - "overflowLevel": 3.2, - "defaultFluid": "wastewater", - "inletPipeDiameter": 0.3, - "outletPipeDiameter": 0.3, - "pipelineLength": 80, - "maxDischargeHead": 24, - "staticHead": 12, - "maxInflowRate": 200, - "temperatureReferenceDegC": 15, - "timeleftToFullOrEmptyThresholdSeconds": 0, - "enableDryRunProtection": true, - "enableOverfillProtection": true, - "dryRunThresholdPercent": 2, - "overfillThresholdPercent": 98, - "minHeightBasedOn": "outlet", - "processOutputFormat": "process", - "dbaseOutputFormat": "influxdb", - "refHeight": "NAP", - "basinBottomRef": 1, - "uuid": "example-ps-001", - "supplier": "WBD-RD", - "category": "station", - "assetType": "pumpingstation", - "model": "demo-50m3", - "unit": "m3/h", - "enableLog": true, - "logLevel": "info", - "positionVsParent": "atEquipment", - "positionIcon": "", - "hasDistance": false, - "distance": "", - "distanceUnit": "m", - "distanceDescription": "", - "controlMode": "levelbased", - "startLevel": 1.2, - "minLevel": 0.4, - "maxLevel": 2.8, - "flowSetpoint": null, - "flowDeadband": null, - "x": 1320, - "y": 300, - "wires": [ - [ - "ps_basic_format" - ], - [ - "ps_basic_dbg_influx" - ], - [ - "ps_basic_dbg_parent" - ] - ] - }, - { - "id": "ps_basic_format", - "type": "function", - "z": "ps_basic_tab", - "name": "Merge deltas + format", - "func": "const p = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst cache = context.get('c') || {};\nObject.assign(cache, p);\ncontext.set('c', cache);\nfunction pick(prefix) {\n for (const k of Object.keys(cache)) if (k === prefix || k.indexOf(prefix + '.') === 0) {\n const v = Number(cache[k]); if (Number.isFinite(v)) return v;\n } return null;\n}\nconst vol = pick('volume.predicted.atequipment');\nconst lvl = pick('level.predicted.atequipment');\nconst flIn = pick('flow.predicted.in');\nmsg.payload = {\n state: cache.state || 'unknown',\n controlMode: cache.controlMode || cache.mode || 'n/a',\n direction: cache.direction || 'n/a',\n percControl: cache.percControl != null ? Number(cache.percControl).toFixed(1) + ' %' : 'n/a',\n volume: vol != null ? vol.toFixed(2) + ' m3' : 'n/a',\n volumePercent: cache.volumePercent != null ? Number(cache.volumePercent).toFixed(1) + ' %' : 'n/a',\n level: lvl != null ? lvl.toFixed(3) + ' m' : 'n/a',\n inflow: flIn != null ? (flIn * 3600).toFixed(1) + ' m3/h' : 'n/a',\n timeToFull: cache.timeToFull != null ? Number(cache.timeToFull).toFixed(0) + ' s' : 'n/a',\n timeToEmpty: cache.timeToEmpty != null ? Number(cache.timeToEmpty).toFixed(0) + ' s' : 'n/a'\n};\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 1560, - "y": 280, - "wires": [ - [ - "ps_basic_dbg_process" - ] - ] - }, - { - "id": "ps_basic_dbg_process", - "type": "debug", - "z": "ps_basic_tab", - "name": "Port 0: Process", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "payload", - "targetType": "msg", - "x": 1800, - "y": 240, - "wires": [] - }, - { - "id": "ps_basic_dbg_influx", - "type": "debug", - "z": "ps_basic_tab", - "name": "Port 1: InfluxDB", - "active": false, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "true", - "targetType": "full", - "x": 1800, - "y": 320, - "wires": [] - }, - { - "id": "ps_basic_dbg_parent", - "type": "debug", - "z": "ps_basic_tab", - "name": "Port 2: Parent reg", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "true", - "targetType": "full", - "x": 1800, - "y": 380, - "wires": [] - }, - { - "id": "grp_ps_basic", - "type": "group", - "z": "ps_basic_tab", - "name": "Pumping Station (PC)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#0c99d9", - "fill-opacity": "0.10" + { + "id": "77f00aef1c966167", + "type": "tab", + "label": "PumpingStation - Basic", + "disabled": false, + "info": "Tier 1: single pumpingStation node driven by inject nodes only. Demonstrates the canonical Phase-2 topic API: set.mode, set.inflow, set.demand." }, - "nodes": [ - "ps_basic_node", - "ps_basic_format" - ], - "x": 1290, - "y": 230, - "w": 500, - "h": 140 - } -] + { + "id": "aa3381b896eb2cfb", + "type": "group", + "z": "77f00aef1c966167", + "name": "Pumping Station (Process Cell)", + "style": { + "label": true, + "stroke": "#000000", + "fill": "#0c99d9", + "fill-opacity": "0.10" + }, + "nodes": [ + "8e78b6607deb33a7" + ], + "x": 534, + "y": 351.5, + "w": 232, + "h": 97 + }, + { + "id": "4996420d47442fad", + "type": "group", + "z": "77f00aef1c966167", + "name": "1. Control mode", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "1155bbbde7c65363", + "e9bea0f95b557f5d" + ], + "x": 94, + "y": 119, + "w": 272, + "h": 122 + }, + { + "id": "a9f9b38b0e00c1d7", + "type": "group", + "z": "77f00aef1c966167", + "name": "2. Flow signals (inflow / outflow)", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "7b2b5eb919b1ab15", + "3350187815774b95" + ], + "x": 94, + "y": 279, + "w": 262, + "h": 122 + }, + { + "id": "42bf82c87d05f498", + "type": "group", + "z": "77f00aef1c966167", + "name": "3. Operator demand (manual mode only)", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "48c2262c345c46b9" + ], + "x": 94, + "y": 479, + "w": 261, + "h": 82 + }, + { + "id": "234bdce20170061a", + "type": "group", + "z": "77f00aef1c966167", + "name": "4. Calibration", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "463eefdd54df89a5", + "2e0642275899fc79" + ], + "x": 94, + "y": 599, + "w": 272, + "h": 122 + }, + { + "id": "f4ba4542514ed853", + "type": "group", + "z": "77f00aef1c966167", + "name": "Expected outputs", + "style": { + "stroke": "#666666", + "fill": "#d1d1d1", + "fill-opacity": "0.2", + "label": true, + "color": "#333333" + }, + "nodes": [ + "b2450e5ee2eebfaa", + "386af1ad8aa8ed12", + "c27c2655f199b530" + ], + "x": 874, + "y": 299, + "w": 252, + "h": 202 + }, + { + "id": "b30af582f935bcb7", + "type": "comment", + "z": "77f00aef1c966167", + "name": "PumpingStation — Basic (Tier 1)", + "info": "Single pumpingStation node driven by inject buttons. Shows the canonical msg.topic command surface.\n\nDefault controlMode = levelbased. Switch to manual to honour set.demand.\n\nHOW TO USE\n1. Deploy the flow.\n2. (optional) Click \"set.mode = manual\" if you want set.demand to forward; otherwise leave it on levelbased and the ramp drives demand from level.\n3. Click \"set.inflow = 60 m³/h\" to push wastewater into the basin.\n4. Watch the basin fill on Port 0 (level, volume rise) and Port 1 (InfluxDB-shaped payload).\n5. In manual mode: click \"set.demand = 40\" — the value surfaces as `manualDemand` on Port 0/1 and in the node status badge.\n6. Click \"calibrate volume 25 m³\" or \"calibrate level 1.5 m\" to snap the predicted-volume integrator.\n\nPORTS\n- Port 0: process output (changed fields only)\n- Port 1: InfluxDB-shaped {measurement, fields, tags, timestamp}\n- Port 2: parent registration (child handshake)", + "x": 650, + "y": 300, + "wires": [] + }, + { + "id": "1155bbbde7c65363", + "type": "inject", + "z": "77f00aef1c966167", + "g": "4996420d47442fad", + "name": "set.mode = manual", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "v": "manual", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.mode", + "x": 230, + "y": 160, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "e9bea0f95b557f5d", + "type": "inject", + "z": "77f00aef1c966167", + "g": "4996420d47442fad", + "name": "set.mode = levelbased", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "v": "levelbased", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.mode", + "x": 240, + "y": 200, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "7b2b5eb919b1ab15", + "type": "inject", + "z": "77f00aef1c966167", + "g": "a9f9b38b0e00c1d7", + "name": "set.inflow = 60 m3/h", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "v": "60", + "vt": "num" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.inflow", + "x": 240, + "y": 360, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "48c2262c345c46b9", + "type": "inject", + "z": "77f00aef1c966167", + "g": "42bf82c87d05f498", + "name": "set.demand = 40 %", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "v": "40", + "vt": "num" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.demand", + "x": 230, + "y": 520, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "463eefdd54df89a5", + "type": "inject", + "z": "77f00aef1c966167", + "g": "234bdce20170061a", + "name": "calibrate volume 25 m3", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "v": "25", + "vt": "num" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "cmd.calibrate.volume", + "x": 240, + "y": 640, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "2e0642275899fc79", + "type": "inject", + "z": "77f00aef1c966167", + "g": "234bdce20170061a", + "name": "calibrate level 1.5 m", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "v": "1.5", + "vt": "num" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "cmd.calibrate.level", + "x": 240, + "y": 680, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "b2450e5ee2eebfaa", + "type": "debug", + "z": "77f00aef1c966167", + "g": "f4ba4542514ed853", + "name": "Port 0: Process", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "x": 980, + "y": 340, + "wires": [] + }, + { + "id": "386af1ad8aa8ed12", + "type": "debug", + "z": "77f00aef1c966167", + "g": "f4ba4542514ed853", + "name": "Port 1: InfluxDB", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "x": 980, + "y": 400, + "wires": [] + }, + { + "id": "c27c2655f199b530", + "type": "debug", + "z": "77f00aef1c966167", + "g": "f4ba4542514ed853", + "name": "Port 2: Parent reg", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "x": 990, + "y": 460, + "wires": [] + }, + { + "id": "8e78b6607deb33a7", + "type": "pumpingStation", + "z": "77f00aef1c966167", + "g": "aa3381b896eb2cfb", + "name": "", + "simulator": false, + "basinVolume": 50, + "basinHeight": 4, + "inflowLevel": 1.5, + "outflowLevel": 0.2, + "overflowLevel": 3.8, + "defaultFluid": "wastewater", + "inletPipeDiameter": 0.3, + "outletPipeDiameter": 0.3, + "pipelineLength": 80, + "maxDischargeHead": 24, + "staticHead": 12, + "maxInflowRate": 200, + "temperatureReferenceDegC": 15, + "timeleftToFullOrEmptyThresholdSeconds": 0, + "enableDryRunProtection": true, + "enableHighVolumeSafety": true, + "enableOverfillProtection": true, + "dryRunThresholdPercent": 2, + "highVolumeSafetyThresholdPercent": 98, + "overfillThresholdPercent": 98, + "minHeightBasedOn": "outlet", + "processOutputFormat": "process", + "dbaseOutputFormat": "influxdb", + "refHeight": "NAP", + "basinBottomRef": 1, + "uuid": "", + "supplier": "", + "category": "", + "assetType": "", + "model": "", + "unit": "", + "enableLog": false, + "logLevel": "error", + "positionVsParent": "atEquipment", + "positionIcon": "⊥", + "hasDistance": false, + "distance": "", + "controlMode": "levelbased", + "levelCurveType": "linear", + "logCurveFactor": 9, + "enableShiftedRamp": false, + "shiftLevel": 0, + "shiftArmPercent": 95, + "startLevel": 1, + "stopLevel": 0.5, + "minLevel": 0.20400000000000001, + "maxLevel": 3.8, + "flowSetpoint": null, + "flowDeadband": null, + "x": 650, + "y": 400, + "wires": [ + [ + "b2450e5ee2eebfaa" + ], + [ + "386af1ad8aa8ed12" + ], + [ + "c27c2655f199b530" + ] + ] + }, + { + "id": "3350187815774b95", + "type": "inject", + "z": "77f00aef1c966167", + "g": "a9f9b38b0e00c1d7", + "name": "set.outflow= 80 m3/h", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": "", + "topic": "set.outflow", + "payload": "80", + "payloadType": "num", + "x": 230, + "y": 320, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "ef77c1819422a098", + "type": "global-config", + "env": [], + "modules": { + "EVOLV": "1.0.29" + } + } +] \ No newline at end of file diff --git a/examples/02-Dashboard.json b/examples/02-Dashboard.json new file mode 100644 index 0000000..ab52731 --- /dev/null +++ b/examples/02-Dashboard.json @@ -0,0 +1,1070 @@ +[ + { + "id": "77f00aef1c966167", + "type": "tab", + "label": "PumpingStation - Dashboard", + "disabled": false, + "info": "Tier 2: single pumpingStation node driven by a FlowFuse dashboard. Same command surface as the Basic flow, plus live status rows, four trend charts, and a raw-output table." + }, + { + "id": "aa3381b896eb2cfb", + "type": "group", + "z": "77f00aef1c966167", + "name": "Pumping Station (Process Cell)", + "style": { + "label": true, + "stroke": "#000000", + "fill": "#0c99d9", + "fill-opacity": "0.10" + }, + "nodes": [ + "8e78b6607deb33a7" + ], + "x": 534, + "y": 371.5, + "w": 232, + "h": 97 + }, + { + "id": "4996420d47442fad", + "type": "group", + "z": "77f00aef1c966167", + "name": "1. Control mode", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "ui_btn_mode_manual", + "ui_btn_mode_lvl" + ], + "x": 94, + "y": 119, + "w": 272, + "h": 122 + }, + { + "id": "a9f9b38b0e00c1d7", + "type": "group", + "z": "77f00aef1c966167", + "name": "2. Flow signals (inflow / outflow)", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "ui_btn_inflow", + "ui_btn_outflow" + ], + "x": 94, + "y": 279, + "w": 272, + "h": 122 + }, + { + "id": "42bf82c87d05f498", + "type": "group", + "z": "77f00aef1c966167", + "name": "3. Operator demand (manual mode only)", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "ui_btn_demand" + ], + "x": 94, + "y": 459, + "w": 261, + "h": 82 + }, + { + "id": "234bdce20170061a", + "type": "group", + "z": "77f00aef1c966167", + "name": "4. Calibration", + "style": { + "stroke": "#666666", + "fill": "#ffdf7f", + "fill-opacity": "0.15", + "label": true, + "color": "#333333" + }, + "nodes": [ + "ui_btn_cal_vol", + "ui_btn_cal_lvl" + ], + "x": 94, + "y": 579, + "w": 272, + "h": 122 + }, + { + "id": "grp_status_panel", + "type": "group", + "z": "77f00aef1c966167", + "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_direction", + "ui_txt_level", + "ui_txt_volume", + "ui_txt_volpct", + "ui_txt_pct", + "ui_txt_demand", + "ui_chart_level", + "ui_chart_volume", + "ui_chart_volpct", + "ui_chart_flow", + "ui_tpl_raw" + ], + "x": 854, + "y": 99, + "w": 712, + "h": 642 + }, + { + "id": "f4ba4542514ed853", + "type": "group", + "z": "77f00aef1c966167", + "name": "Debug outputs (sidebar)", + "style": { + "stroke": "#666666", + "fill": "#d1d1d1", + "fill-opacity": "0.2", + "label": true, + "color": "#333333" + }, + "nodes": [ + "b2450e5ee2eebfaa", + "386af1ad8aa8ed12", + "c27c2655f199b530" + ], + "x": 854, + "y": 779, + "w": 252, + "h": 202 + }, + { + "id": "b30af582f935bcb7", + "type": "comment", + "z": "77f00aef1c966167", + "name": "PumpingStation — Dashboard (Tier 2)", + "info": "Same command surface as the Basic example, driven by a FlowFuse dashboard.\n\nOpen /dashboard/pumpingstation-basic after deploy.\n\nCONTROLS panel\n- Mode buttons → set.mode (manual / levelbased)\n- Inflow / Outflow buttons → set.inflow / set.outflow (60 / 80 m³/h)\n- Demand button → set.demand (40 m³/h, manual mode only)\n- Calibrate buttons → cmd.calibrate.volume / cmd.calibrate.level\n\nSTATUS panel\n- 7 text rows: Mode, Direction, Level, Volume, Volume %, percControl, Manual demand\n\nTRENDS panel\n- 4 charts: Level (m), Volume (m³), Volume %, Flow (in/out/net m³/h)\n\nRAW OUTPUT panel\n- Full key/value dump of the latest Port 0 cache (sorted). Shows every field the node emits including basin geometry, safety thresholds, predicted overflow/underflow.\n\nThe fan-out function caches last-known values so delta-only Port 0 updates never blank a row.", + "x": 660, + "y": 320, + "wires": [] + }, + { + "id": "ui_base_ps", + "type": "ui-base", + "name": "EVOLV Demo", + "path": "/dashboard", + "appIcon": "", + "includeClientData": true, + "acceptsClientConfig": [ + "ui-notification", + "ui-control" + ], + "showPathInSidebar": false, + "headerContent": "page", + "navigationStyle": "default", + "titleBarStyle": "default" + }, + { + "id": "ui_theme_ps", + "type": "ui-theme", + "name": "EVOLV Basic Theme", + "colors": { + "surface": "#ffffff", + "primary": "#0c99d9", + "bgPage": "#eeeeee", + "groupBg": "#ffffff", + "groupOutline": "#cccccc" + }, + "sizes": { + "density": "default", + "pagePadding": "14px", + "groupGap": "14px", + "groupBorderRadius": "6px", + "widgetGap": "12px" + } + }, + { + "id": "ui_page_ps", + "type": "ui-page", + "name": "PumpingStation Basic", + "ui": "ui_base_ps", + "path": "/pumpingstation-basic", + "icon": "water-pump", + "layout": "grid", + "theme": "ui_theme_ps", + "breakpoints": [ + { + "name": "Default", + "px": "0", + "cols": "12" + } + ], + "order": 1, + "className": "" + }, + { + "id": "ui_group_ctrl", + "type": "ui-group", + "name": "Controls", + "page": "ui_page_ps", + "width": "6", + "height": "1", + "order": 1, + "showTitle": true, + "className": "" + }, + { + "id": "ui_group_status", + "type": "ui-group", + "name": "Status", + "page": "ui_page_ps", + "width": "6", + "height": "1", + "order": 2, + "showTitle": true, + "className": "" + }, + { + "id": "ui_group_trends", + "type": "ui-group", + "name": "Trends", + "page": "ui_page_ps", + "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_ps", + "width": "12", + "height": "1", + "order": 4, + "showTitle": true, + "className": "" + }, + { + "id": "ui_btn_mode_manual", + "type": "ui-button", + "z": "77f00aef1c966167", + "g": "4996420d47442fad", + "group": "ui_group_ctrl", + "name": "Mode: Manual", + "label": "Mode: Manual", + "order": 1, + "width": "3", + "height": "1", + "emulateClick": false, + "tooltip": "Switch control mode to manual (set.demand is honoured)", + "color": "", + "bgcolor": "", + "icon": "pan_tool", + "payload": "manual", + "payloadType": "str", + "topic": "set.mode", + "topicType": "str", + "x": 230, + "y": 160, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "ui_btn_mode_lvl", + "type": "ui-button", + "z": "77f00aef1c966167", + "g": "4996420d47442fad", + "group": "ui_group_ctrl", + "name": "Mode: Levelbased", + "label": "Mode: Levelbased", + "order": 2, + "width": "3", + "height": "1", + "emulateClick": false, + "tooltip": "Switch control mode to levelbased (ramp drives demand from level)", + "color": "", + "bgcolor": "", + "icon": "stacked_line_chart", + "payload": "levelbased", + "payloadType": "str", + "topic": "set.mode", + "topicType": "str", + "x": 240, + "y": 200, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "ui_btn_inflow", + "type": "ui-button", + "z": "77f00aef1c966167", + "g": "a9f9b38b0e00c1d7", + "group": "ui_group_ctrl", + "name": "Inflow 60 m³/h", + "label": "Inflow 60 m³/h", + "order": 3, + "width": "3", + "height": "1", + "emulateClick": false, + "tooltip": "Push a measured inflow of 60 m³/h into the basin balance", + "color": "", + "bgcolor": "", + "icon": "south", + "payload": "60", + "payloadType": "num", + "topic": "set.inflow", + "topicType": "str", + "x": 240, + "y": 320, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "ui_btn_outflow", + "type": "ui-button", + "z": "77f00aef1c966167", + "g": "a9f9b38b0e00c1d7", + "group": "ui_group_ctrl", + "name": "Outflow 80 m³/h", + "label": "Outflow 80 m³/h", + "order": 4, + "width": "3", + "height": "1", + "emulateClick": false, + "tooltip": "Push a measured outflow of 80 m³/h into the basin balance", + "color": "", + "bgcolor": "", + "icon": "north", + "payload": "80", + "payloadType": "num", + "topic": "set.outflow", + "topicType": "str", + "x": 240, + "y": 360, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "ui_btn_demand", + "type": "ui-button", + "z": "77f00aef1c966167", + "g": "42bf82c87d05f498", + "group": "ui_group_ctrl", + "name": "Demand 40 m³/h", + "label": "Demand 40 m³/h (manual)", + "order": 5, + "width": "6", + "height": "1", + "emulateClick": false, + "tooltip": "Operator outflow demand — only forwarded when mode = manual", + "color": "", + "bgcolor": "", + "icon": "speed", + "payload": "40", + "payloadType": "num", + "topic": "set.demand", + "topicType": "str", + "x": 240, + "y": 500, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "ui_btn_cal_vol", + "type": "ui-button", + "z": "77f00aef1c966167", + "g": "234bdce20170061a", + "group": "ui_group_ctrl", + "name": "Calibrate V=25 m³", + "label": "Calibrate V = 25 m³", + "order": 6, + "width": "3", + "height": "1", + "emulateClick": false, + "tooltip": "Snap the predicted-volume integrator to 25 m³", + "color": "", + "bgcolor": "", + "icon": "tune", + "payload": "25", + "payloadType": "num", + "topic": "cmd.calibrate.volume", + "topicType": "str", + "x": 240, + "y": 620, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "ui_btn_cal_lvl", + "type": "ui-button", + "z": "77f00aef1c966167", + "g": "234bdce20170061a", + "group": "ui_group_ctrl", + "name": "Calibrate L=1.5 m", + "label": "Calibrate L = 1.5 m", + "order": 7, + "width": "3", + "height": "1", + "emulateClick": false, + "tooltip": "Snap the predicted-volume integrator to a known level of 1.5 m", + "color": "", + "bgcolor": "", + "icon": "tune", + "payload": "1.5", + "payloadType": "num", + "topic": "cmd.calibrate.level", + "topicType": "str", + "x": 240, + "y": 660, + "wires": [ + [ + "8e78b6607deb33a7" + ] + ] + }, + { + "id": "fn_status_split", + "type": "function", + "z": "77f00aef1c966167", + "g": "grp_status_panel", + "name": "fan-out Port 0 (status + charts + raw)", + "func": "// Port 0 emits delta-only — cache last-known so deltas never blank a row.\n// Keys with dots use the runtime childId (= node id), so we pattern-match\n// by prefix rather than hardcoding.\nconst cache = context.get('cache') || {};\nconst p = msg.payload || {};\nfor (const k in p) cache[k] = p[k];\ncontext.set('cache', cache);\n\nconst findByPrefix = (prefix) => {\n for (const k of Object.keys(cache)) if (k.startsWith(prefix)) return cache[k];\n return null;\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 level = findByPrefix('level.predicted.atequipment.');\nconst volume = findByPrefix('volume.predicted.atequipment.');\nconst volPct = findByPrefix('volumePercent.predicted.atequipment.');\nconst qInS = findByPrefix('flow.predicted.in.');\nconst qOutS = findByPrefix('flow.predicted.out.');\nconst qNetS = findByPrefix('netFlowRate.predicted.atequipment.');\nconst qInH = Number.isFinite(+qInS) ? +qInS * 3600 : null;\nconst qOutH = Number.isFinite(+qOutS) ? +qOutS * 3600 : null;\nconst qNetH = Number.isFinite(+qNetS) ? +qNetS * 3600 : null;\nconst pct = cache.percControl;\nconst dem = cache.manualDemand;\nconst mode = cache.mode || '—';\nconst dir = cache.direction || '—';\n\nconst chart = (topic, v) => Number.isFinite(+v) ? { topic, payload: +v } : null;\n\n// Raw view: every cached key, sorted, with values prettified for display.\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 widgets\n { payload: mode },\n { payload: dir },\n { payload: num(level, 2, 'm') },\n { payload: num(volume, 2, 'm³') },\n { payload: num(volPct, 2, '%') },\n { payload: num(pct, 1, '%') },\n { payload: mode === 'manual'\n ? (Number.isFinite(+dem) ? num(dem, 1, 'm³/h') : 'not set')\n : '—' },\n // 7–9: single-series charts\n chart('Level', level),\n chart('Volume', volume),\n chart('Volume %', volPct),\n // 10–12: flow chart (three series share the same chart node)\n chart('Inflow', qInH),\n chart('Outflow', qOutH),\n chart('Net', qNetH),\n // 13: raw key/value rows for the ui-template\n { payload: rawRows },\n];\n", + "outputs": 14, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 980, + "y": 140, + "wires": [ + [ + "ui_txt_mode" + ], + [ + "ui_txt_direction" + ], + [ + "ui_txt_level" + ], + [ + "ui_txt_volume" + ], + [ + "ui_txt_volpct" + ], + [ + "ui_txt_pct" + ], + [ + "ui_txt_demand" + ], + [ + "ui_chart_level" + ], + [ + "ui_chart_volume" + ], + [ + "ui_chart_volpct" + ], + [ + "ui_chart_flow" + ], + [ + "ui_chart_flow" + ], + [ + "ui_chart_flow" + ], + [ + "ui_tpl_raw" + ] + ] + }, + { + "id": "ui_txt_mode", + "type": "ui-text", + "z": "77f00aef1c966167", + "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": 1240, + "y": 100, + "wires": [] + }, + { + "id": "ui_txt_direction", + "type": "ui-text", + "z": "77f00aef1c966167", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 2, + "width": "6", + "height": "1", + "name": "Direction", + "label": "Direction", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#1F4E79", + "x": 1250, + "y": 140, + "wires": [] + }, + { + "id": "ui_txt_level", + "type": "ui-text", + "z": "77f00aef1c966167", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 3, + "width": "6", + "height": "1", + "name": "Level", + "label": "Level", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#1F4E79", + "x": 1240, + "y": 180, + "wires": [] + }, + { + "id": "ui_txt_volume", + "type": "ui-text", + "z": "77f00aef1c966167", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 4, + "width": "6", + "height": "1", + "name": "Volume", + "label": "Volume", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#1F4E79", + "x": 1250, + "y": 220, + "wires": [] + }, + { + "id": "ui_txt_volpct", + "type": "ui-text", + "z": "77f00aef1c966167", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 5, + "width": "6", + "height": "1", + "name": "Volume %", + "label": "Volume %", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#1F4E79", + "x": 1250, + "y": 260, + "wires": [] + }, + { + "id": "ui_txt_pct", + "type": "ui-text", + "z": "77f00aef1c966167", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 6, + "width": "6", + "height": "1", + "name": "percControl", + "label": "percControl", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#1F4E79", + "x": 1260, + "y": 300, + "wires": [] + }, + { + "id": "ui_txt_demand", + "type": "ui-text", + "z": "77f00aef1c966167", + "g": "grp_status_panel", + "group": "ui_group_status", + "order": 7, + "width": "6", + "height": "1", + "name": "Manual demand", + "label": "Manual demand", + "format": "{{msg.payload}}", + "layout": "row-spread", + "style": false, + "font": "", + "fontSize": 14, + "color": "#7D3C98", + "x": 1270, + "y": 340, + "wires": [] + }, + { + "id": "ui_chart_level", + "type": "ui-chart", + "z": "77f00aef1c966167", + "g": "grp_status_panel", + "group": "ui_group_trends", + "name": "Level (m)", + "label": "Level (m)", + "order": 1, + "width": 6, + "height": 4, + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisLabel": "time", + "xAxisType": "time", + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "xAxisFormat": "", + "xAxisFormatType": "auto", + "yAxisLabel": "m", + "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": [ + "#0095FF", + "#FF0000", + "#FF7F0E", + "#2CA02C", + "#A347E1", + "#D62728", + "#FF9896", + "#9467BD", + "#C5B0D5" + ], + "textColor": [ + "#666666" + ], + "textColorDefault": true, + "gridColor": [ + "#e5e5e5" + ], + "gridColorDefault": true, + "x": 1240, + "y": 400, + "wires": [] + }, + { + "id": "ui_chart_volume", + "type": "ui-chart", + "z": "77f00aef1c966167", + "g": "grp_status_panel", + "group": "ui_group_trends", + "name": "Volume (m³)", + "label": "Volume (m³)", + "order": 2, + "width": 6, + "height": 4, + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisLabel": "time", + "xAxisType": "time", + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "xAxisFormat": "", + "xAxisFormatType": "auto", + "yAxisLabel": "m³", + "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": 1250, + "y": 440, + "wires": [] + }, + { + "id": "ui_chart_volpct", + "type": "ui-chart", + "z": "77f00aef1c966167", + "g": "grp_status_panel", + "group": "ui_group_trends", + "name": "Volume %", + "label": "Volume %", + "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": "0", + "ymax": "100", + "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": 1240, + "y": 480, + "wires": [] + }, + { + "id": "ui_chart_flow", + "type": "ui-chart", + "z": "77f00aef1c966167", + "g": "grp_status_panel", + "group": "ui_group_trends", + "name": "Flow (m³/h)", + "label": "Flow (m³/h) — Inflow / Outflow / Net", + "order": 4, + "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", + "#FF7F0E", + "#2CA02C", + "#FF0000", + "#A347E1", + "#D62728", + "#FF9896", + "#9467BD", + "#C5B0D5" + ], + "textColor": [ + "#666666" + ], + "textColorDefault": true, + "gridColor": [ + "#e5e5e5" + ], + "gridColorDefault": true, + "x": 1250, + "y": 520, + "wires": [] + }, + { + "id": "ui_tpl_raw", + "type": "ui-template", + "z": "77f00aef1c966167", + "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": 1260, + "y": 580, + "wires": [ + [] + ] + }, + { + "id": "b2450e5ee2eebfaa", + "type": "debug", + "z": "77f00aef1c966167", + "g": "f4ba4542514ed853", + "name": "Port 0: Process", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "x": 980, + "y": 820, + "wires": [] + }, + { + "id": "386af1ad8aa8ed12", + "type": "debug", + "z": "77f00aef1c966167", + "g": "f4ba4542514ed853", + "name": "Port 1: InfluxDB", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "x": 980, + "y": 880, + "wires": [] + }, + { + "id": "c27c2655f199b530", + "type": "debug", + "z": "77f00aef1c966167", + "g": "f4ba4542514ed853", + "name": "Port 2: Parent reg", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "targetType": "full", + "x": 990, + "y": 940, + "wires": [] + }, + { + "id": "8e78b6607deb33a7", + "type": "pumpingStation", + "z": "77f00aef1c966167", + "g": "aa3381b896eb2cfb", + "name": "", + "simulator": false, + "basinVolume": 50, + "basinHeight": 4, + "inflowLevel": 1.5, + "outflowLevel": 0.2, + "overflowLevel": 3.8, + "defaultFluid": "wastewater", + "inletPipeDiameter": 0.3, + "outletPipeDiameter": 0.3, + "pipelineLength": 80, + "maxDischargeHead": 24, + "staticHead": 12, + "maxInflowRate": 200, + "temperatureReferenceDegC": 15, + "timeleftToFullOrEmptyThresholdSeconds": 0, + "enableDryRunProtection": true, + "enableHighVolumeSafety": true, + "enableOverfillProtection": true, + "dryRunThresholdPercent": 2, + "highVolumeSafetyThresholdPercent": 98, + "overfillThresholdPercent": 98, + "minHeightBasedOn": "outlet", + "processOutputFormat": "process", + "dbaseOutputFormat": "influxdb", + "refHeight": "NAP", + "basinBottomRef": 1, + "uuid": "", + "supplier": "", + "category": "", + "assetType": "", + "model": "", + "unit": "", + "enableLog": false, + "logLevel": "error", + "positionVsParent": "atEquipment", + "positionIcon": "⊥", + "hasDistance": false, + "distance": "", + "controlMode": "levelbased", + "levelCurveType": "linear", + "logCurveFactor": 9, + "enableShiftedRamp": false, + "shiftLevel": 0, + "shiftArmPercent": 95, + "startLevel": 1, + "stopLevel": 0.5, + "minLevel": 0.3, + "maxLevel": 3.8, + "flowSetpoint": null, + "flowDeadband": null, + "x": 650, + "y": 420, + "wires": [ + [ + "b2450e5ee2eebfaa", + "fn_status_split" + ], + [ + "386af1ad8aa8ed12" + ], + [ + "c27c2655f199b530" + ] + ] + }, + { + "id": "ef77c1819422a098", + "type": "global-config", + "env": [], + "modules": { + "EVOLV": "1.0.29" + } + } +] diff --git a/examples/02-Integration.json b/examples/02-Integration.json deleted file mode 100644 index ac386dc..0000000 --- a/examples/02-Integration.json +++ /dev/null @@ -1,686 +0,0 @@ -[ - { - "id": "ps_int_proc", - "type": "tab", - "label": "Process Plant", - "disabled": false, - "info": "Tier 2: pumpingStation + measurement child + machineGroupControl parent with two rotatingMachine pumps. Demonstrates Phase-2 parent/child handshakes and the canonical set.mode/set.inflow/set.demand topics." - }, - { - "id": "ps_int_setup", - "type": "tab", - "label": "Setup", - "disabled": false, - "info": "Deploy-time once-true injects that initialise control modes on the EVOLV nodes." - }, - { - "id": "ps_int_title", - "type": "comment", - "z": "ps_int_proc", - "name": "PumpingStation - Integration\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nL0 link-ins | L2 level sensor (CM) | L3 pumps (EM) | L4 MGC (UN) | L5 station (PC).\nPumps register with MGC via Port 2; MGC and the level sensor register with the station via Port 2.\nCross-tab channels: setup:* drive once-true initialisation from the Setup tab.", - "info": "", - "x": 600, - "y": 40, - "wires": [] - }, - { - "id": "lin_setup_mode", - "type": "link in", - "z": "ps_int_proc", - "name": "setup:to-ps-mode", - "links": [], - "x": 120, - "y": 500, - "wires": [ - [ - "ps_int_station" - ] - ] - }, - { - "id": "lin_setup_inflow", - "type": "link in", - "z": "ps_int_proc", - "name": "setup:to-ps-inflow", - "links": [], - "x": 120, - "y": 560, - "wires": [ - [ - "ps_int_station" - ] - ] - }, - { - "id": "lin_setup_mgcmode", - "type": "link in", - "z": "ps_int_proc", - "name": "setup:to-mgc-mode", - "links": [], - "x": 120, - "y": 360, - "wires": [ - [ - "ps_int_mgc" - ] - ] - }, - { - "id": "meas_level", - "type": "measurement", - "z": "ps_int_proc", - "name": "Basin level sensor", - "mode": "analog", - "channels": "[]", - "scaling": false, - "i_min": 0, - "i_max": 0, - "i_offset": 0, - "o_min": 0, - "o_max": 1, - "simulator": true, - "smooth_method": "mean", - "count": 5, - "processOutputFormat": "process", - "dbaseOutputFormat": "influxdb", - "uuid": "example-level-001", - "supplier": "vega", - "category": "sensor", - "assetType": "level", - "model": "VEGAPULS-31", - "unit": "m", - "assetTagNumber": "LT-001", - "enableLog": false, - "logLevel": "error", - "positionVsParent": "atEquipment", - "positionIcon": "", - "hasDistance": false, - "distance": 0, - "distanceUnit": "m", - "distanceDescription": "", - "x": 600, - "y": 700, - "wires": [ - [ - "ps_int_dbg_level" - ], - [], - [ - "ps_int_station" - ] - ] - }, - { - "id": "ps_int_inj_level", - "type": "inject", - "z": "ps_int_proc", - "name": "sim level 1.6 m", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "1.6", - "vt": "num" - } - ], - "topic": "measurement", - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": "", - "x": 120, - "y": 700, - "wires": [ - [ - "meas_level" - ] - ] - }, - { - "id": "pump_a", - "type": "rotatingMachine", - "z": "ps_int_proc", - "name": "Pump A", - "speed": "1", - "startup": "2", - "warmup": "1", - "shutdown": "2", - "cooldown": "1", - "movementMode": "staticspeed", - "machineCurve": "", - "uuid": "example-pump-a", - "supplier": "hidrostal", - "category": "pump", - "assetType": "pump-centrifugal", - "model": "hidrostal-H05K-S03R", - "unit": "m3/h", - "curvePressureUnit": "mbar", - "curveFlowUnit": "m3/h", - "curvePowerUnit": "kW", - "curveControlUnit": "%", - "enableLog": false, - "logLevel": "error", - "positionVsParent": "atEquipment", - "positionIcon": "", - "hasDistance": false, - "distance": "", - "distanceUnit": "m", - "distanceDescription": "", - "x": 840, - "y": 320, - "wires": [ - [ - "ps_int_dbg_pa" - ], - [], - [ - "ps_int_mgc" - ] - ] - }, - { - "id": "pump_b", - "type": "rotatingMachine", - "z": "ps_int_proc", - "name": "Pump B", - "speed": "1", - "startup": "2", - "warmup": "1", - "shutdown": "2", - "cooldown": "1", - "movementMode": "staticspeed", - "machineCurve": "", - "uuid": "example-pump-b", - "supplier": "hidrostal", - "category": "pump", - "assetType": "pump-centrifugal", - "model": "hidrostal-H05K-S03R", - "unit": "m3/h", - "curvePressureUnit": "mbar", - "curveFlowUnit": "m3/h", - "curvePowerUnit": "kW", - "curveControlUnit": "%", - "enableLog": false, - "logLevel": "error", - "positionVsParent": "atEquipment", - "positionIcon": "", - "hasDistance": false, - "distance": "", - "distanceUnit": "m", - "distanceDescription": "", - "x": 840, - "y": 400, - "wires": [ - [ - "ps_int_dbg_pb" - ], - [], - [ - "ps_int_mgc" - ] - ] - }, - { - "id": "ps_int_mgc", - "type": "machineGroupControl", - "z": "ps_int_proc", - "name": "Pump Group", - "enableLog": true, - "logLevel": "info", - "positionVsParent": "atEquipment", - "positionIcon": "", - "hasDistance": false, - "distance": "", - "distanceUnit": "m", - "x": 1080, - "y": 360, - "wires": [ - [ - "ps_int_dbg_mgc" - ], - [], - [ - "ps_int_station" - ] - ] - }, - { - "id": "ps_int_station", - "type": "pumpingStation", - "z": "ps_int_proc", - "name": "Pumping Station", - "simulator": false, - "basinVolume": 50, - "basinHeight": 3.5, - "inflowLevel": 3, - "outflowLevel": 0.2, - "overflowLevel": 3.2, - "defaultFluid": "wastewater", - "inletPipeDiameter": 0.3, - "outletPipeDiameter": 0.3, - "pipelineLength": 80, - "maxDischargeHead": 24, - "staticHead": 12, - "maxInflowRate": 200, - "temperatureReferenceDegC": 15, - "timeleftToFullOrEmptyThresholdSeconds": 0, - "enableDryRunProtection": true, - "enableOverfillProtection": true, - "dryRunThresholdPercent": 2, - "overfillThresholdPercent": 98, - "minHeightBasedOn": "outlet", - "processOutputFormat": "process", - "dbaseOutputFormat": "influxdb", - "refHeight": "NAP", - "basinBottomRef": 1, - "uuid": "example-ps-001", - "supplier": "WBD-RD", - "category": "station", - "assetType": "pumpingstation", - "model": "demo-50m3", - "unit": "m3/h", - "enableLog": true, - "logLevel": "info", - "positionVsParent": "atEquipment", - "positionIcon": "", - "hasDistance": false, - "distance": "", - "distanceUnit": "m", - "distanceDescription": "", - "controlMode": "levelbased", - "startLevel": 1.2, - "minLevel": 0.4, - "maxLevel": 2.8, - "flowSetpoint": null, - "flowDeadband": null, - "x": 1320, - "y": 520, - "wires": [ - [ - "ps_int_format" - ], - [ - "ps_int_dbg_influx" - ], - [] - ] - }, - { - "id": "ps_int_format", - "type": "function", - "z": "ps_int_proc", - "name": "Merge deltas + format", - "func": "const p = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst cache = context.get('c') || {}; Object.assign(cache, p); context.set('c', cache);\nfunction pick(prefix){ for (const k of Object.keys(cache)) if (k===prefix||k.indexOf(prefix+'.')===0){ const v=Number(cache[k]); if(Number.isFinite(v)) return v;} return null; }\nconst vol=pick('volume.predicted.atequipment'), lvl=pick('level.predicted.atequipment'), flIn=pick('flow.predicted.in'), flOut=pick('flow.predicted.out');\nmsg.payload = {\n state: cache.state || 'unknown',\n controlMode: cache.controlMode || cache.mode || 'n/a',\n direction: cache.direction || 'n/a',\n percControl: cache.percControl != null ? Number(cache.percControl).toFixed(1)+' %' : 'n/a',\n volume: vol != null ? vol.toFixed(2)+' m3' : 'n/a',\n volumePercent: cache.volumePercent != null ? Number(cache.volumePercent).toFixed(1)+' %' : 'n/a',\n level: lvl != null ? lvl.toFixed(3)+' m' : 'n/a',\n inflow: flIn != null ? (flIn*3600).toFixed(1)+' m3/h' : 'n/a',\n outflow: flOut != null ? (flOut*3600).toFixed(1)+' m3/h' : 'n/a',\n childCount: cache.childCount != null ? cache.childCount : 'n/a'\n};\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 1560, - "y": 520, - "wires": [ - [ - "ps_int_dbg_process" - ] - ] - }, - { - "id": "ps_int_dbg_process", - "type": "debug", - "z": "ps_int_proc", - "name": "PS Port 0: Process", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "payload", - "targetType": "msg", - "x": 1800, - "y": 480, - "wires": [] - }, - { - "id": "ps_int_dbg_influx", - "type": "debug", - "z": "ps_int_proc", - "name": "PS Port 1: InfluxDB", - "active": false, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "true", - "targetType": "full", - "x": 1800, - "y": 540, - "wires": [] - }, - { - "id": "ps_int_dbg_mgc", - "type": "debug", - "z": "ps_int_proc", - "name": "MGC Port 0", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "payload", - "targetType": "msg", - "x": 1800, - "y": 360, - "wires": [] - }, - { - "id": "ps_int_dbg_pa", - "type": "debug", - "z": "ps_int_proc", - "name": "Pump A Port 0", - "active": false, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "payload", - "targetType": "msg", - "x": 1800, - "y": 320, - "wires": [] - }, - { - "id": "ps_int_dbg_pb", - "type": "debug", - "z": "ps_int_proc", - "name": "Pump B Port 0", - "active": false, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "payload", - "targetType": "msg", - "x": 1800, - "y": 400, - "wires": [] - }, - { - "id": "ps_int_dbg_level", - "type": "debug", - "z": "ps_int_proc", - "name": "Level Port 0", - "active": false, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "payload", - "targetType": "msg", - "x": 1800, - "y": 700, - "wires": [] - }, - { - "id": "grp_pumpa", - "type": "group", - "z": "ps_int_proc", - "name": "Pump A (EM)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#86bbdd", - "fill-opacity": "0.10" - }, - "nodes": [ - "pump_a", - "ps_int_dbg_pa" - ], - "x": 815, - "y": 275, - "w": 1210, - "h": 110 - }, - { - "id": "grp_pumpb", - "type": "group", - "z": "ps_int_proc", - "name": "Pump B (EM)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#86bbdd", - "fill-opacity": "0.10" - }, - "nodes": [ - "pump_b", - "ps_int_dbg_pb" - ], - "x": 815, - "y": 355, - "w": 1210, - "h": 110 - }, - { - "id": "grp_mgc", - "type": "group", - "z": "ps_int_proc", - "name": "Pump Group MGC (UN)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#50a8d9", - "fill-opacity": "0.10" - }, - "nodes": [ - "ps_int_mgc", - "ps_int_dbg_mgc", - "lin_setup_mgcmode" - ], - "x": 95, - "y": 315, - "w": 1930, - "h": 110 - }, - { - "id": "grp_station", - "type": "group", - "z": "ps_int_proc", - "name": "Pumping Station (PC)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#0c99d9", - "fill-opacity": "0.10" - }, - "nodes": [ - "ps_int_station", - "ps_int_format", - "ps_int_dbg_process", - "ps_int_dbg_influx", - "lin_setup_mode", - "lin_setup_inflow" - ], - "x": 95, - "y": 435, - "w": 1930, - "h": 190 - }, - { - "id": "grp_level", - "type": "group", - "z": "ps_int_proc", - "name": "Level Sensor (CM)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#a9daee", - "fill-opacity": "0.10" - }, - "nodes": [ - "meas_level", - "ps_int_inj_level", - "ps_int_dbg_level" - ], - "x": 95, - "y": 655, - "w": 1930, - "h": 110 - }, - { - "id": "setup_title", - "type": "comment", - "z": "ps_int_setup", - "name": "Deploy-time setup\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nFires once after each deploy: pushes the canonical set.mode / set.inflow /\nset.demand topics across cross-tab channels into the Process Plant tab.", - "info": "", - "x": 600, - "y": 40, - "wires": [] - }, - { - "id": "setup_inj_mode", - "type": "inject", - "z": "ps_int_setup", - "name": "set.mode = levelbased", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "levelbased", - "vt": "str" - } - ], - "topic": "set.mode", - "repeat": "", - "crontab": "", - "once": true, - "onceDelay": "0.5", - "x": 120, - "y": 160, - "wires": [ - [ - "lout_setup_mode" - ] - ] - }, - { - "id": "setup_inj_mgcmode", - "type": "inject", - "z": "ps_int_setup", - "name": "MGC set.mode = auto", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "auto", - "vt": "str" - } - ], - "topic": "set.mode", - "repeat": "", - "crontab": "", - "once": true, - "onceDelay": "0.5", - "x": 120, - "y": 220, - "wires": [ - [ - "lout_setup_mgcmode" - ] - ] - }, - { - "id": "setup_inj_inflow", - "type": "inject", - "z": "ps_int_setup", - "name": "seed inflow 60 m3/h", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "60", - "vt": "num" - } - ], - "topic": "set.inflow", - "repeat": "", - "crontab": "", - "once": true, - "onceDelay": "1.0", - "x": 120, - "y": 280, - "wires": [ - [ - "lout_setup_inflow" - ] - ] - }, - { - "id": "lout_setup_mode", - "type": "link out", - "z": "ps_int_setup", - "name": "setup:to-ps-mode", - "mode": "link", - "links": [ - "lin_setup_mode" - ], - "x": 1800, - "y": 160, - "wires": [] - }, - { - "id": "lout_setup_mgcmode", - "type": "link out", - "z": "ps_int_setup", - "name": "setup:to-mgc-mode", - "mode": "link", - "links": [ - "lin_setup_mgcmode" - ], - "x": 1800, - "y": 220, - "wires": [] - }, - { - "id": "lout_setup_inflow", - "type": "link out", - "z": "ps_int_setup", - "name": "setup:to-ps-inflow", - "mode": "link", - "links": [ - "lin_setup_inflow" - ], - "x": 1800, - "y": 280, - "wires": [] - }, - { - "id": "grp_setup", - "type": "group", - "z": "ps_int_setup", - "name": "Deploy-time setup", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#dddddd", - "fill-opacity": "0.10" - }, - "nodes": [ - "setup_inj_mode", - "setup_inj_mgcmode", - "setup_inj_inflow", - "lout_setup_mode", - "lout_setup_mgcmode", - "lout_setup_inflow" - ], - "x": 95, - "y": 115, - "w": 1930, - "h": 230 - } -] diff --git a/examples/03-Dashboard.json b/examples/03-Dashboard.json deleted file mode 100644 index 6a486e5..0000000 --- a/examples/03-Dashboard.json +++ /dev/null @@ -1,1325 +0,0 @@ -[ - { - "id": "ps_dash_proc", - "type": "tab", - "label": "Process Plant", - "disabled": false, - "info": "Tier 3: full station with measurement + MGC + 2 pumps, formatted for live dashboard." - }, - { - "id": "ps_dash_ui", - "type": "tab", - "label": "Dashboard UI", - "disabled": false, - "info": "FlowFuse dashboard 2.0: 3 charts (flow / level / volumePercent), text widgets and 2 sliders." - }, - { - "id": "ps_dash_setup", - "type": "tab", - "label": "Setup", - "disabled": false, - "info": "Once-true injects: initial mode + initial inflow seed." - }, - { - "id": "ps_dash_base", - "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": "ps_dash_theme", - "type": "ui-theme", - "name": "EVOLV Theme", - "colors": { - "surface": "#ffffff", - "primary": "#0c99d9", - "bgPage": "#eeeeee", - "groupBg": "#ffffff", - "groupOutline": "#cccccc" - }, - "sizes": { - "density": "default", - "pagePadding": "14px", - "groupGap": "14px", - "groupBorderRadius": "6px", - "widgetGap": "12px" - } - }, - { - "id": "ps_dash_page", - "type": "ui-page", - "name": "PumpingStation Demo", - "ui": "ps_dash_base", - "path": "/pumping-station", - "icon": "water", - "layout": "grid", - "theme": "ps_dash_theme", - "breakpoints": [ - { - "name": "Default", - "px": "0", - "cols": "12" - } - ], - "order": 1, - "className": "" - }, - { - "id": "ps_dash_grp_ctrl", - "type": "ui-group", - "name": "Controls", - "page": "ps_dash_page", - "width": 6, - "height": 1, - "order": 1, - "showTitle": true, - "className": "" - }, - { - "id": "ps_dash_grp_status", - "type": "ui-group", - "name": "Operator Status", - "page": "ps_dash_page", - "width": 6, - "height": 1, - "order": 2, - "showTitle": true, - "className": "" - }, - { - "id": "ps_dash_grp_trend", - "type": "ui-group", - "name": "Live Trends", - "page": "ps_dash_page", - "width": 12, - "height": 1, - "order": 3, - "showTitle": true, - "className": "" - }, - { - "id": "ps_dash_proc_title", - "type": "comment", - "z": "ps_dash_proc", - "name": "Process Plant\n━━━━━━━━━━━━━━━━━\nFull station with parent (MGC) and 2 pump children.\nEvents go to Dashboard UI through evt:ps; commands come back through cmd:ps-mode and cmd:ps-demand.", - "info": "", - "x": 600, - "y": 40, - "wires": [] - }, - { - "id": "lin_proc_mode", - "type": "link in", - "z": "ps_dash_proc", - "name": "cmd:ps-mode", - "links": [], - "x": 120, - "y": 480, - "wires": [ - [ - "ps_dash_station" - ] - ] - }, - { - "id": "lin_proc_demand", - "type": "link in", - "z": "ps_dash_proc", - "name": "cmd:ps-demand", - "links": [], - "x": 120, - "y": 540, - "wires": [ - [ - "ps_dash_station" - ] - ] - }, - { - "id": "lin_proc_setupmode", - "type": "link in", - "z": "ps_dash_proc", - "name": "setup:to-ps-mode", - "links": [], - "x": 120, - "y": 420, - "wires": [ - [ - "ps_dash_station" - ] - ] - }, - { - "id": "lin_proc_setupinflow", - "type": "link in", - "z": "ps_dash_proc", - "name": "setup:to-ps-inflow", - "links": [], - "x": 120, - "y": 600, - "wires": [ - [ - "ps_dash_station" - ] - ] - }, - { - "id": "ps_dash_meas_level", - "type": "measurement", - "z": "ps_dash_proc", - "name": "Basin level sensor", - "mode": "analog", - "channels": "[]", - "scaling": false, - "i_min": 0, - "i_max": 0, - "i_offset": 0, - "o_min": 0, - "o_max": 1, - "simulator": true, - "smooth_method": "mean", - "count": 5, - "processOutputFormat": "process", - "dbaseOutputFormat": "influxdb", - "uuid": "example-level-001", - "supplier": "vega", - "category": "sensor", - "assetType": "level", - "model": "VEGAPULS-31", - "unit": "m", - "assetTagNumber": "LT-001", - "enableLog": false, - "logLevel": "error", - "positionVsParent": "atEquipment", - "positionIcon": "", - "hasDistance": false, - "distance": 0, - "distanceUnit": "m", - "distanceDescription": "", - "x": 600, - "y": 700, - "wires": [ - [], - [], - [ - "ps_dash_station" - ] - ] - }, - { - "id": "ps_dash_inj_level", - "type": "inject", - "z": "ps_dash_proc", - "name": "sim level 1.6 m", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "1.6", - "vt": "num" - } - ], - "topic": "measurement", - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": "", - "x": 120, - "y": 700, - "wires": [ - [ - "ps_dash_meas_level" - ] - ] - }, - { - "id": "ps_dash_pump_a", - "type": "rotatingMachine", - "z": "ps_dash_proc", - "name": "Pump A", - "speed": "1", - "startup": "2", - "warmup": "1", - "shutdown": "2", - "cooldown": "1", - "movementMode": "staticspeed", - "machineCurve": "", - "uuid": "example-pump-a", - "supplier": "hidrostal", - "category": "pump", - "assetType": "pump-centrifugal", - "model": "hidrostal-H05K-S03R", - "unit": "m3/h", - "curvePressureUnit": "mbar", - "curveFlowUnit": "m3/h", - "curvePowerUnit": "kW", - "curveControlUnit": "%", - "enableLog": false, - "logLevel": "error", - "positionVsParent": "atEquipment", - "positionIcon": "", - "hasDistance": false, - "distance": "", - "distanceUnit": "m", - "distanceDescription": "", - "x": 840, - "y": 320, - "wires": [ - [], - [], - [ - "ps_dash_mgc" - ] - ] - }, - { - "id": "ps_dash_pump_b", - "type": "rotatingMachine", - "z": "ps_dash_proc", - "name": "Pump B", - "speed": "1", - "startup": "2", - "warmup": "1", - "shutdown": "2", - "cooldown": "1", - "movementMode": "staticspeed", - "machineCurve": "", - "uuid": "example-pump-b", - "supplier": "hidrostal", - "category": "pump", - "assetType": "pump-centrifugal", - "model": "hidrostal-H05K-S03R", - "unit": "m3/h", - "curvePressureUnit": "mbar", - "curveFlowUnit": "m3/h", - "curvePowerUnit": "kW", - "curveControlUnit": "%", - "enableLog": false, - "logLevel": "error", - "positionVsParent": "atEquipment", - "positionIcon": "", - "hasDistance": false, - "distance": "", - "distanceUnit": "m", - "distanceDescription": "", - "x": 840, - "y": 400, - "wires": [ - [], - [], - [ - "ps_dash_mgc" - ] - ] - }, - { - "id": "ps_dash_mgc", - "type": "machineGroupControl", - "z": "ps_dash_proc", - "name": "Pump Group", - "enableLog": true, - "logLevel": "info", - "positionVsParent": "atEquipment", - "positionIcon": "", - "hasDistance": false, - "distance": "", - "distanceUnit": "m", - "x": 1080, - "y": 360, - "wires": [ - [], - [], - [ - "ps_dash_station" - ] - ] - }, - { - "id": "ps_dash_station", - "type": "pumpingStation", - "z": "ps_dash_proc", - "name": "Pumping Station", - "simulator": false, - "basinVolume": 50, - "basinHeight": 3.5, - "inflowLevel": 3, - "outflowLevel": 0.2, - "overflowLevel": 3.2, - "defaultFluid": "wastewater", - "inletPipeDiameter": 0.3, - "outletPipeDiameter": 0.3, - "pipelineLength": 80, - "maxDischargeHead": 24, - "staticHead": 12, - "maxInflowRate": 200, - "temperatureReferenceDegC": 15, - "timeleftToFullOrEmptyThresholdSeconds": 0, - "enableDryRunProtection": true, - "enableOverfillProtection": true, - "dryRunThresholdPercent": 2, - "overfillThresholdPercent": 98, - "minHeightBasedOn": "outlet", - "processOutputFormat": "process", - "dbaseOutputFormat": "influxdb", - "refHeight": "NAP", - "basinBottomRef": 1, - "uuid": "example-ps-001", - "supplier": "WBD-RD", - "category": "station", - "assetType": "pumpingstation", - "model": "demo-50m3", - "unit": "m3/h", - "enableLog": true, - "logLevel": "info", - "positionVsParent": "atEquipment", - "positionIcon": "", - "hasDistance": false, - "distance": "", - "distanceUnit": "m", - "distanceDescription": "", - "controlMode": "levelbased", - "startLevel": 1.2, - "minLevel": 0.4, - "maxLevel": 2.8, - "flowSetpoint": null, - "flowDeadband": null, - "x": 1320, - "y": 520, - "wires": [ - [ - "ps_dash_trend_split" - ], - [], - [] - ] - }, - { - "id": "ps_dash_trend_split", - "type": "function", - "z": "ps_dash_proc", - "name": "Trend split + status", - "func": "const p = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst cache = context.get('c') || {}; Object.assign(cache, p); context.set('c', cache);\nfunction pick(prefix){ for (const k of Object.keys(cache)) if (k===prefix||k.indexOf(prefix+'.')===0){ const v=Number(cache[k]); if(Number.isFinite(v)) return v;} return null; }\nconst flowIn = pick('flow.predicted.in');\nconst flowOut = pick('flow.predicted.out');\nconst level = pick('level.predicted.atequipment');\nconst volPct = Number(cache.volumePercent);\nconst ts = Date.now();\nconst flowMsgs = [];\nif (flowIn != null) flowMsgs.push({ topic: 'Inflow', payload: flowIn * 3600, timestamp: ts });\nif (flowOut != null) flowMsgs.push({ topic: 'Outflow', payload: flowOut * 3600, timestamp: ts });\nconst flowOut1 = flowMsgs.length ? flowMsgs : null;\nconst levelOut = level != null ? { topic: 'Level', payload: level, timestamp: ts } : null;\nconst volOut = Number.isFinite(volPct) ? { topic: 'Volume%', payload: volPct, timestamp: ts } : null;\nconst stateStr = `state=${cache.state||'?'} | mode=${cache.controlMode||cache.mode||'?'}`;\nconst percStr = cache.percControl != null ? Number(cache.percControl).toFixed(1) + ' %' : 'n/a';\nconst dirStr = cache.direction || 'n/a';\nconst tEmpty = cache.timeToEmpty != null ? Number(cache.timeToEmpty).toFixed(0) + ' s' : 'n/a';\nreturn [\n flowOut1,\n levelOut,\n volOut,\n { payload: stateStr },\n { payload: percStr },\n { payload: dirStr },\n { payload: tEmpty }\n];", - "outputs": 7, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 1560, - "y": 520, - "wires": [ - [ - "lout_evt_flow" - ], - [ - "lout_evt_level" - ], - [ - "lout_evt_volpct" - ], - [ - "lout_evt_state" - ], - [ - "lout_evt_perc" - ], - [ - "lout_evt_dir" - ], - [ - "lout_evt_tempty" - ] - ] - }, - { - "id": "lout_evt_flow", - "type": "link out", - "z": "ps_dash_proc", - "name": "evt:flow", - "mode": "link", - "links": [ - "lin_ui_flow" - ], - "x": 1800, - "y": 420, - "wires": [] - }, - { - "id": "lout_evt_level", - "type": "link out", - "z": "ps_dash_proc", - "name": "evt:level", - "mode": "link", - "links": [ - "lin_ui_level" - ], - "x": 1800, - "y": 460, - "wires": [] - }, - { - "id": "lout_evt_volpct", - "type": "link out", - "z": "ps_dash_proc", - "name": "evt:volpct", - "mode": "link", - "links": [ - "lin_ui_volpct" - ], - "x": 1800, - "y": 500, - "wires": [] - }, - { - "id": "lout_evt_state", - "type": "link out", - "z": "ps_dash_proc", - "name": "evt:state", - "mode": "link", - "links": [ - "lin_ui_state" - ], - "x": 1800, - "y": 540, - "wires": [] - }, - { - "id": "lout_evt_perc", - "type": "link out", - "z": "ps_dash_proc", - "name": "evt:perc", - "mode": "link", - "links": [ - "lin_ui_perc" - ], - "x": 1800, - "y": 580, - "wires": [] - }, - { - "id": "lout_evt_dir", - "type": "link out", - "z": "ps_dash_proc", - "name": "evt:dir", - "mode": "link", - "links": [ - "lin_ui_dir" - ], - "x": 1800, - "y": 620, - "wires": [] - }, - { - "id": "lout_evt_tempty", - "type": "link out", - "z": "ps_dash_proc", - "name": "evt:tempty", - "mode": "link", - "links": [ - "lin_ui_tempty" - ], - "x": 1800, - "y": 660, - "wires": [] - }, - { - "id": "ps_dash_grp_station", - "type": "group", - "z": "ps_dash_proc", - "name": "Pumping Station (PC)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#0c99d9", - "fill-opacity": "0.10" - }, - "nodes": [ - "ps_dash_station", - "ps_dash_trend_split", - "lin_proc_mode", - "lin_proc_demand", - "lin_proc_setupmode", - "lin_proc_setupinflow", - "lout_evt_flow", - "lout_evt_level", - "lout_evt_volpct", - "lout_evt_state", - "lout_evt_perc", - "lout_evt_dir", - "lout_evt_tempty" - ], - "x": 95, - "y": 375, - "w": 1930, - "h": 350 - }, - { - "id": "ps_dash_grp_pa", - "type": "group", - "z": "ps_dash_proc", - "name": "Pump A (EM)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#86bbdd", - "fill-opacity": "0.10" - }, - "nodes": [ - "ps_dash_pump_a" - ], - "x": 815, - "y": 275, - "w": 250, - "h": 110 - }, - { - "id": "ps_dash_grp_pb", - "type": "group", - "z": "ps_dash_proc", - "name": "Pump B (EM)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#86bbdd", - "fill-opacity": "0.10" - }, - "nodes": [ - "ps_dash_pump_b" - ], - "x": 815, - "y": 355, - "w": 250, - "h": 110 - }, - { - "id": "ps_dash_grp_mgc", - "type": "group", - "z": "ps_dash_proc", - "name": "Pump Group MGC (UN)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#50a8d9", - "fill-opacity": "0.10" - }, - "nodes": [ - "ps_dash_mgc" - ], - "x": 1055, - "y": 315, - "w": 250, - "h": 110 - }, - { - "id": "ps_dash_grp_level", - "type": "group", - "z": "ps_dash_proc", - "name": "Level Sensor (CM)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#a9daee", - "fill-opacity": "0.10" - }, - "nodes": [ - "ps_dash_meas_level", - "ps_dash_inj_level" - ], - "x": 95, - "y": 655, - "w": 730, - "h": 110 - }, - { - "id": "ps_dash_ui_title", - "type": "comment", - "z": "ps_dash_ui", - "name": "Dashboard UI\n━━━━━━━━━━━━━━━\nLink-ins on L0 receive evt:* from Process Plant.\nSliders on L2 emit cmd:* back to Process Plant.\nCharts use the trend-split pattern: one chart per metric, series labelled by msg.topic.", - "info": "", - "x": 600, - "y": 40, - "wires": [] - }, - { - "id": "lin_ui_flow", - "type": "link in", - "z": "ps_dash_ui", - "name": "evt:flow", - "links": [], - "x": 120, - "y": 220, - "wires": [ - [ - "ui_chart_flow" - ] - ] - }, - { - "id": "lin_ui_level", - "type": "link in", - "z": "ps_dash_ui", - "name": "evt:level", - "links": [], - "x": 120, - "y": 320, - "wires": [ - [ - "ui_chart_level" - ] - ] - }, - { - "id": "lin_ui_volpct", - "type": "link in", - "z": "ps_dash_ui", - "name": "evt:volpct", - "links": [], - "x": 120, - "y": 420, - "wires": [ - [ - "ui_chart_volpct" - ] - ] - }, - { - "id": "lin_ui_state", - "type": "link in", - "z": "ps_dash_ui", - "name": "evt:state", - "links": [], - "x": 120, - "y": 520, - "wires": [ - [ - "ui_text_state" - ] - ] - }, - { - "id": "lin_ui_perc", - "type": "link in", - "z": "ps_dash_ui", - "name": "evt:perc", - "links": [], - "x": 120, - "y": 560, - "wires": [ - [ - "ui_text_perc" - ] - ] - }, - { - "id": "lin_ui_dir", - "type": "link in", - "z": "ps_dash_ui", - "name": "evt:dir", - "links": [], - "x": 120, - "y": 600, - "wires": [ - [ - "ui_text_dir" - ] - ] - }, - { - "id": "lin_ui_tempty", - "type": "link in", - "z": "ps_dash_ui", - "name": "evt:tempty", - "links": [], - "x": 120, - "y": 640, - "wires": [ - [ - "ui_text_tempty" - ] - ] - }, - { - "id": "ui_chart_flow", - "type": "ui-chart", - "z": "ps_dash_ui", - "group": "ps_dash_grp_trend", - "name": "Flow trend", - "label": "Flow (m³/h)", - "order": 1, - "width": 12, - "height": 6, - "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": "", - "bins": 10, - "action": "append", - "stackSeries": false, - "pointShape": "circle", - "pointRadius": 4, - "interpolation": "linear", - "showLegend": true, - "className": "", - "removeOlder": "15", - "removeOlderUnit": "60", - "removeOlderPoints": "200", - "colors": [ - "#0095FF", - "#FF0000", - "#FF7F0E", - "#2CA02C", - "#A347E1", - "#D62728", - "#FF9896", - "#9467BD", - "#C5B0D5" - ], - "textColor": [ - "#666666" - ], - "textColorDefault": true, - "gridColor": [ - "#e5e5e5" - ], - "gridColorDefault": true, - "x": 1080, - "y": 220, - "wires": [] - }, - { - "id": "ui_chart_level", - "type": "ui-chart", - "z": "ps_dash_ui", - "group": "ps_dash_grp_trend", - "name": "Level trend", - "label": "Level (m)", - "order": 2, - "width": 12, - "height": 6, - "chartType": "line", - "category": "topic", - "categoryType": "msg", - "xAxisLabel": "time", - "xAxisType": "time", - "xAxisProperty": "", - "xAxisPropertyType": "timestamp", - "xAxisFormat": "", - "xAxisFormatType": "auto", - "yAxisLabel": "m", - "yAxisProperty": "payload", - "yAxisPropertyType": "msg", - "xmin": "", - "xmax": "", - "ymin": "", - "ymax": "", - "bins": 10, - "action": "append", - "stackSeries": false, - "pointShape": "circle", - "pointRadius": 4, - "interpolation": "linear", - "showLegend": true, - "className": "", - "removeOlder": "15", - "removeOlderUnit": "60", - "removeOlderPoints": "200", - "colors": [ - "#0095FF", - "#FF0000", - "#FF7F0E", - "#2CA02C", - "#A347E1", - "#D62728", - "#FF9896", - "#9467BD", - "#C5B0D5" - ], - "textColor": [ - "#666666" - ], - "textColorDefault": true, - "gridColor": [ - "#e5e5e5" - ], - "gridColorDefault": true, - "x": 1080, - "y": 320, - "wires": [] - }, - { - "id": "ui_chart_volpct", - "type": "ui-chart", - "z": "ps_dash_ui", - "group": "ps_dash_grp_trend", - "name": "Volume %", - "label": "Volume (%)", - "order": 3, - "width": 12, - "height": 6, - "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": "", - "bins": 10, - "action": "append", - "stackSeries": false, - "pointShape": "circle", - "pointRadius": 4, - "interpolation": "linear", - "showLegend": true, - "className": "", - "removeOlder": "15", - "removeOlderUnit": "60", - "removeOlderPoints": "200", - "colors": [ - "#0095FF", - "#FF0000", - "#FF7F0E", - "#2CA02C", - "#A347E1", - "#D62728", - "#FF9896", - "#9467BD", - "#C5B0D5" - ], - "textColor": [ - "#666666" - ], - "textColorDefault": true, - "gridColor": [ - "#e5e5e5" - ], - "gridColorDefault": true, - "x": 1080, - "y": 420, - "wires": [] - }, - { - "id": "ui_text_state", - "type": "ui-text", - "z": "ps_dash_ui", - "group": "ps_dash_grp_status", - "name": "State", - "label": "Station state", - "order": 1, - "width": 4, - "height": 1, - "format": "{{msg.payload}}", - "layout": "row-spread", - "x": 1080, - "y": 520, - "wires": [] - }, - { - "id": "ui_text_perc", - "type": "ui-text", - "z": "ps_dash_ui", - "group": "ps_dash_grp_status", - "name": "percControl", - "label": "Control %", - "order": 2, - "width": 4, - "height": 1, - "format": "{{msg.payload}}", - "layout": "row-spread", - "x": 1080, - "y": 560, - "wires": [] - }, - { - "id": "ui_text_dir", - "type": "ui-text", - "z": "ps_dash_ui", - "group": "ps_dash_grp_status", - "name": "direction", - "label": "Direction", - "order": 3, - "width": 4, - "height": 1, - "format": "{{msg.payload}}", - "layout": "row-spread", - "x": 1080, - "y": 600, - "wires": [] - }, - { - "id": "ui_text_tempty", - "type": "ui-text", - "z": "ps_dash_ui", - "group": "ps_dash_grp_status", - "name": "timeToEmpty", - "label": "Time to empty", - "order": 4, - "width": 4, - "height": 1, - "format": "{{msg.payload}}", - "layout": "row-spread", - "x": 1080, - "y": 640, - "wires": [] - }, - { - "id": "ui_dd_mode", - "type": "ui-dropdown", - "z": "ps_dash_ui", - "group": "ps_dash_grp_ctrl", - "name": "Mode", - "label": "Control mode", - "order": 1, - "width": 6, - "height": 1, - "passthru": true, - "multiple": false, - "options": [ - { - "label": "manual", - "value": "manual", - "type": "str" - }, - { - "label": "levelbased", - "value": "levelbased", - "type": "str" - }, - { - "label": "flowbased", - "value": "flowbased", - "type": "str" - }, - { - "label": "none", - "value": "none", - "type": "str" - } - ], - "payload": "", - "topic": "set.mode", - "topicType": "str", - "x": 600, - "y": 160, - "wires": [ - [ - "ui_wrap_mode" - ] - ] - }, - { - "id": "ui_sl_demand", - "type": "ui-slider", - "z": "ps_dash_ui", - "group": "ps_dash_grp_ctrl", - "name": "Demand", - "label": "Manual demand (m³/h)", - "order": 2, - "width": 6, - "height": 1, - "passthru": true, - "outs": "end", - "topic": "set.demand", - "topicType": "str", - "min": 0, - "max": 200, - "step": 5, - "icon": "", - "thumbLabel": "always", - "showValue": true, - "className": "", - "x": 600, - "y": 220, - "wires": [ - [ - "ui_wrap_demand" - ] - ] - }, - { - "id": "ui_wrap_mode", - "type": "function", - "z": "ps_dash_ui", - "name": "topic=set.mode", - "func": "msg.topic = 'set.mode';\nmsg.payload = String(msg.payload || 'manual');\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 1080, - "y": 160, - "wires": [ - [ - "lout_cmd_mode" - ] - ] - }, - { - "id": "ui_wrap_demand", - "type": "function", - "z": "ps_dash_ui", - "name": "topic=set.demand", - "func": "msg.topic = 'set.demand';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 1080, - "y": 220, - "wires": [ - [ - "lout_cmd_demand" - ] - ] - }, - { - "id": "lout_cmd_mode", - "type": "link out", - "z": "ps_dash_ui", - "name": "cmd:ps-mode", - "mode": "link", - "links": [ - "lin_proc_mode" - ], - "x": 1800, - "y": 160, - "wires": [] - }, - { - "id": "lout_cmd_demand", - "type": "link out", - "z": "ps_dash_ui", - "name": "cmd:ps-demand", - "mode": "link", - "links": [ - "lin_proc_demand" - ], - "x": 1800, - "y": 220, - "wires": [] - }, - { - "id": "grp_ui_ctrl", - "type": "group", - "z": "ps_dash_ui", - "name": "Controls (PC)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#0c99d9", - "fill-opacity": "0.10" - }, - "nodes": [ - "ui_dd_mode", - "ui_sl_demand", - "ui_wrap_mode", - "ui_wrap_demand", - "lout_cmd_mode", - "lout_cmd_demand" - ], - "x": 575, - "y": 115, - "w": 1450, - "h": 170 - }, - { - "id": "grp_ui_status", - "type": "group", - "z": "ps_dash_ui", - "name": "Operator status (PC)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#0c99d9", - "fill-opacity": "0.10" - }, - "nodes": [ - "ui_text_state", - "ui_text_perc", - "ui_text_dir", - "ui_text_tempty", - "lin_ui_state", - "lin_ui_perc", - "lin_ui_dir", - "lin_ui_tempty" - ], - "x": 95, - "y": 475, - "w": 1210, - "h": 230 - }, - { - "id": "grp_ui_trend", - "type": "group", - "z": "ps_dash_ui", - "name": "Live trends (PC)", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#0c99d9", - "fill-opacity": "0.10" - }, - "nodes": [ - "ui_chart_flow", - "ui_chart_level", - "ui_chart_volpct", - "lin_ui_flow", - "lin_ui_level", - "lin_ui_volpct" - ], - "x": 95, - "y": 175, - "w": 1210, - "h": 310 - }, - { - "id": "ps_dash_setup_title", - "type": "comment", - "z": "ps_dash_setup", - "name": "Deploy-time setup\n━━━━━━━━━━━━━━━━━━━\nInitialises set.mode = levelbased and seeds an inflow at deploy time.", - "info": "", - "x": 600, - "y": 40, - "wires": [] - }, - { - "id": "ps_dash_setup_mode", - "type": "inject", - "z": "ps_dash_setup", - "name": "set.mode = levelbased", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "levelbased", - "vt": "str" - } - ], - "topic": "set.mode", - "repeat": "", - "crontab": "", - "once": true, - "onceDelay": "0.5", - "x": 120, - "y": 160, - "wires": [ - [ - "ps_dash_lout_setup_mode" - ] - ] - }, - { - "id": "ps_dash_setup_inflow", - "type": "inject", - "z": "ps_dash_setup", - "name": "seed inflow 60 m3/h", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "60", - "vt": "num" - } - ], - "topic": "set.inflow", - "repeat": "", - "crontab": "", - "once": true, - "onceDelay": "1.0", - "x": 120, - "y": 220, - "wires": [ - [ - "ps_dash_lout_setup_inflow" - ] - ] - }, - { - "id": "ps_dash_lout_setup_mode", - "type": "link out", - "z": "ps_dash_setup", - "name": "setup:to-ps-mode", - "mode": "link", - "links": [ - "lin_proc_setupmode" - ], - "x": 1800, - "y": 160, - "wires": [] - }, - { - "id": "ps_dash_lout_setup_inflow", - "type": "link out", - "z": "ps_dash_setup", - "name": "setup:to-ps-inflow", - "mode": "link", - "links": [ - "lin_proc_setupinflow" - ], - "x": 1800, - "y": 220, - "wires": [] - }, - { - "id": "ps_dash_grp_setup", - "type": "group", - "z": "ps_dash_setup", - "name": "Deploy-time setup", - "style": { - "label": true, - "stroke": "#000000", - "fill": "#dddddd", - "fill-opacity": "0.10" - }, - "nodes": [ - "ps_dash_setup_mode", - "ps_dash_setup_inflow", - "ps_dash_lout_setup_mode", - "ps_dash_lout_setup_inflow" - ], - "x": 95, - "y": 115, - "w": 1930, - "h": 170 - } -] diff --git a/examples/README.md b/examples/README.md index 593d26f..4e1e480 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,9 +1,9 @@ # pumpingStation - Example Flows -Three Node-RED flows demonstrating the Phase-2 pumpingStation node on the -canonical topic API (`set.mode`, `set.inflow`, `set.demand`, +Node-RED flows demonstrating the Phase-2 pumpingStation node on the +canonical topic API (`set.mode`, `set.inflow`, `set.outflow`, `set.demand`, `cmd.calibrate.volume`, `cmd.calibrate.level`). Legacy aliases -(`changemode`, `q_in`, `Qd`, `calibratePredictedVolume`, +(`changemode`, `q_in`, `q_out`, `Qd`, `calibratePredictedVolume`, `calibratePredictedLevel`, `registerChild`) still work but log a one-time deprecation warning; these fresh flows use the canonical names only. @@ -12,15 +12,14 @@ one-time deprecation warning; these fresh flows use the canonical names only. | File | Tier | Tabs | Purpose | |---|---|---|---| | `01-Basic.json` | 1 | Process Plant | Single pumpingStation driven by inject nodes - no parent, no dashboard. | -| `02-Integration.json` | 2 | Process Plant + Setup | Adds a `measurement` level child and a `machineGroupControl` parent with two `rotatingMachine` pumps. Demonstrates the Phase-2 parent/child handshake. | -| `03-Dashboard.json` | 3 | Process Plant + Dashboard UI + Setup | Tier 2 plumbing plus a FlowFuse Dashboard 2.0 page with 3 charts (flow / level / volume %), text widgets, and 2 controls (mode dropdown + demand slider). | +| `02-Dashboard.json` | 2 | Process Plant + Dashboard UI | Same command surface as Basic, but driven by FlowFuse Dashboard 2.0 widgets — `ui-button` controls + `ui-text` live status panel. | ## Prerequisites - Node-RED with the EVOLV package installed (so the `pumpingStation`, `measurement`, `machineGroupControl`, and `rotatingMachine` node types are registered). -- For `03-Dashboard.json`: `@flowfuse/node-red-dashboard` (Dashboard 2.0). +- For `02-Dashboard.json`: `@flowfuse/node-red-dashboard` (Dashboard 2.0). ## How to load @@ -46,28 +45,22 @@ import into their own tabs and can be deployed immediately. 5. Inject `cmd.calibrate.volume = 25 m3` to jump the predicted-volume integrator to half-full. -## 02-Integration - what to try - -1. Deploy. The Setup tab fires `set.mode = levelbased` to the station - and `set.mode = auto` to the MGC. -2. The two pumps register with the MGC via Port 2; the MGC and the level - sensor register with the station via Port 2. Watch the registration - debug taps to confirm. -3. The level inject pushes a 1.6 m measurement so the station sees a - non-zero starting level. Setup also seeds `set.inflow = 60 m3/h`. -4. The station's `controlMode = levelbased` then drives the MGC, which - dispatches to Pump A / Pump B. - -## 03-Dashboard - what to try +## 02-Dashboard - what to try 1. Deploy. -2. Open the dashboard at `http://localhost:1880/dashboard/page/pumping-station`. -3. Use the **Control mode** dropdown to switch between `manual`, - `levelbased`, `flowbased`, `none`. -4. In manual mode, drag the **Manual demand** slider - the demand cascades - to the MGC and on to the pumps. -5. The three charts (flow, level, volume %) plot live data; the four text - widgets show state, percControl, direction, and time-to-empty. +2. Open the dashboard at `http://localhost:1880/dashboard/pumpingstation-basic`. +3. Click **Mode: Manual** or **Mode: Levelbased** in the Controls panel. +4. Click **Inflow 60 m³/h** to push wastewater into the basin — the Status + panel on the right shows level / volume / volume % rising. +5. In manual mode, click **Demand 40 m³/h** — the value surfaces as + `Manual demand` in the Status panel and in the node's status badge. +6. Use **Calibrate V = 25 m³** or **Calibrate L = 1.5 m** to snap the + predicted-volume integrator. + +All buttons fire the same canonical `msg.topic` as the Basic flow's inject +nodes; the only difference is the trigger. The Live status panel is fed by +Port 0 via a small fan-out function that caches last-known values so +delta-only updates never blank a row. ## Layout conventions @@ -88,12 +81,6 @@ These flows follow the EVOLV layout rule set in ## Regenerating -These flows are generated from `tools/build-examples.js`. Edit the -generator, never the JSON, then: - -```bash -node nodes/pumpingStation/tools/build-examples.js -``` - -The script writes `01-Basic.json`, `02-Integration.json`, and -`03-Dashboard.json` into this directory. +The current example JSON files are hand-maintained. If you re-introduce a +generator, regenerate `01-Basic.json` and `02-Dashboard.json` from it +rather than editing the JSON directly. diff --git a/examples/basic-dashboard.flow.json b/examples/basic-dashboard.flow.json deleted file mode 100644 index 9eaddea..0000000 --- a/examples/basic-dashboard.flow.json +++ /dev/null @@ -1,589 +0,0 @@ -[ - { - "id": "ps_tab_basic_dashboard", - "type": "tab", - "label": "PumpingStation Dashboard", - "disabled": false, - "info": "Basic level-based pumpingStation dashboard with basin trends and safety state." - }, - { - "id": "ui_base_ps_basic", - "type": "ui-base", - "name": "EVOLV Demo", - "path": "/dashboard", - "appIcon": "", - "includeClientData": true, - "acceptsClientConfig": [ - "ui-notification", - "ui-control" - ], - "showPathInSidebar": false, - "headerContent": "page", - "navigationStyle": "default", - "titleBarStyle": "default" - }, - { - "id": "ui_theme_ps_basic", - "type": "ui-theme", - "name": "EVOLV Pumping Theme", - "colors": { - "surface": "#ffffff", - "primary": "#0c99d9", - "bgPage": "#f1f3f5", - "groupBg": "#ffffff", - "groupOutline": "#cfd7de" - }, - "sizes": { - "density": "default", - "pagePadding": "14px", - "groupGap": "14px", - "groupBorderRadius": "6px", - "widgetGap": "12px" - } - }, - { - "id": "ui_page_ps_basic", - "type": "ui-page", - "name": "PumpingStation", - "ui": "ui_base_ps_basic", - "path": "/pumping-station", - "icon": "water_drop", - "layout": "grid", - "theme": "ui_theme_ps_basic", - "breakpoints": [ - { - "name": "Default", - "px": "0", - "cols": "12" - } - ], - "order": 1, - "className": "" - }, - { - "id": "ui_group_ps_inputs", - "type": "ui-group", - "name": "Simulation Inputs", - "page": "ui_page_ps_basic", - "width": "4", - "height": "1", - "order": 1, - "showTitle": true, - "className": "" - }, - { - "id": "ui_group_ps_trends", - "type": "ui-group", - "name": "Basin Trends", - "page": "ui_page_ps_basic", - "width": "8", - "height": "1", - "order": 2, - "showTitle": true, - "className": "" - }, - { - "id": "ui_group_ps_state", - "type": "ui-group", - "name": "State", - "page": "ui_page_ps_basic", - "width": "12", - "height": "1", - "order": 3, - "showTitle": true, - "className": "" - }, - { - "id": "ps_node_basic", - "type": "pumpingStation", - "z": "ps_tab_basic_dashboard", - "name": "PS Dashboard Demo", - "basinVolume": 50, - "basinHeight": 5, - "inflowLevel": 3, - "outflowLevel": 0.2, - "overflowLevel": 4.5, - "defaultFluid": "wastewater", - "inletPipeDiameter": 0.4, - "outletPipeDiameter": 0.3, - "pipelineLength": 80, - "maxDischargeHead": 24, - "staticHead": 12, - "maxInflowRate": 200, - "temperatureReferenceDegC": 15, - "timeleftToFullOrEmptyThresholdSeconds": 0, - "enableDryRunProtection": true, - "enableHighVolumeSafety": true, - "enableOverfillProtection": true, - "dryRunThresholdPercent": 2, - "highVolumeSafetyThresholdPercent": 98, - "overfillThresholdPercent": 98, - "minHeightBasedOn": "outlet", - "processOutputFormat": "process", - "dbaseOutputFormat": "influxdb", - "refHeight": "NAP", - "basinBottomRef": 0, - "unit": "m3/h", - "enableLog": false, - "logLevel": "error", - "positionVsParent": "atEquipment", - "positionIcon": "", - "hasDistance": false, - "distance": 0, - "distanceUnit": "m", - "distanceDescription": "", - "controlMode": "levelbased", - "levelCurveType": "linear", - "logCurveFactor": 9, - "minLevel": 1, - "startLevel": 2, - "maxLevel": 4, - "x": 720, - "y": 260, - "wires": [ - [ - "ps_parse_output" - ], - [ - "ps_debug_influx" - ], - [ - "ps_debug_parent" - ] - ] - }, - { - "id": "ps_calibrate_initial", - "type": "inject", - "z": "ps_tab_basic_dashboard", - "name": "Set start level 2 m", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload" - } - ], - "repeat": "", - "crontab": "", - "once": true, - "onceDelay": "0.5", - "topic": "calibratePredictedLevel", - "payload": "2", - "payloadType": "num", - "x": 180, - "y": 180, - "wires": [ - [ - "ps_node_basic" - ] - ] - }, - { - "id": "ps_auto_inflow", - "type": "inject", - "z": "ps_tab_basic_dashboard", - "name": "Auto inflow 0.008 m3/s", - "props": [ - { - "p": "payload" - } - ], - "repeat": "1", - "crontab": "", - "once": true, - "onceDelay": "1", - "topic": "", - "payload": "0.008", - "payloadType": "num", - "x": 180, - "y": 240, - "wires": [ - [ - "ps_build_qin" - ] - ] - }, - { - "id": "ps_inflow_input", - "type": "ui-number-input", - "z": "ps_tab_basic_dashboard", - "group": "ui_group_ps_inputs", - "name": "Inflow", - "label": "Inflow (m3/s)", - "order": 1, - "width": "4", - "height": "1", - "passthru": true, - "topic": "", - "min": 0, - "max": 0.05, - "step": 0.001, - "x": 190, - "y": 300, - "wires": [ - [ - "ps_build_qin" - ] - ] - }, - { - "id": "ps_build_qin", - "type": "function", - "z": "ps_tab_basic_dashboard", - "name": "Build q_in", - "func": "msg.topic = 'q_in';\nmsg.unit = 'm3/s';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 440, - "y": 260, - "wires": [ - [ - "ps_node_basic" - ] - ] - }, - { - "id": "ps_outflow_input", - "type": "ui-number-input", - "z": "ps_tab_basic_dashboard", - "group": "ui_group_ps_inputs", - "name": "Outflow", - "label": "Outflow (m3/s)", - "order": 2, - "width": "4", - "height": "1", - "passthru": true, - "topic": "", - "min": 0, - "max": 0.05, - "step": 0.001, - "x": 190, - "y": 360, - "wires": [ - [ - "ps_build_qout" - ] - ] - }, - { - "id": "ps_build_qout", - "type": "function", - "z": "ps_tab_basic_dashboard", - "name": "Build q_out", - "func": "msg.topic = 'q_out';\nmsg.unit = 'm3/s';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 440, - "y": 360, - "wires": [ - [ - "ps_node_basic" - ] - ] - }, - { - "id": "ps_parse_output", - "type": "function", - "z": "ps_tab_basic_dashboard", - "name": "Parse PS output", - "func": "// MeasurementContainer flat keys are `${type}.${variant}.${position}.${childId}`.\n// When PS writes without an explicit .child(), the childId is the literal\n// string 'default' — DON'T strip it. See generalFunctions/src/measurements/\n// MeasurementContainer.js getFlattenedOutput for details.\nconst fields = (msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst snapshot = Object.assign({}, context.get('snapshot') || {}, fields);\ncontext.set('snapshot', snapshot);\nconst firstFinite = (...keys) => {\n for (const key of keys) {\n const value = Number(snapshot[key]);\n if (Number.isFinite(value)) return value;\n }\n return null;\n};\nconst level = firstFinite('level.predicted.atequipment.default', 'level.measured.atequipment.default');\nconst volume = firstFinite('volume.predicted.atequipment.default', 'volume.measured.atequipment.default');\nconst netFlow = firstFinite('netFlowRate.predicted.atequipment.default', 'netFlowRate.measured.atequipment.default');\nconst demand = firstFinite('percControl');\nconst safety = snapshot.safetyState || 'normal';\nconst direction = snapshot.direction || 'unknown';\nconst overflow = snapshot.isOverflowing === true || snapshot.isOverflowing === 'true';\nconst timeleft = Number(snapshot.timeleft);\nconst fmt = (value, digits = 2) => Number.isFinite(value) ? value.toFixed(digits) : '-';\nreturn [\n level == null ? null : { topic: 'level', payload: level },\n volume == null ? null : { topic: 'volume', payload: volume },\n demand == null ? null : { topic: 'demand', payload: demand },\n netFlow == null ? null : { topic: 'net_flow', payload: netFlow },\n { topic: 'safety', payload: `${safety} | overflowing=${overflow}` },\n { topic: 'snapshot', payload: `level=${fmt(level)} m | volume=${fmt(volume)} m3 | demand=${fmt(demand, 0)}% | direction=${direction} | t=${Number.isFinite(timeleft) ? Math.round(timeleft) + ' s' : '-'}` }\n];", - "outputs": 6, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 980, - "y": 220, - "wires": [ - [ - "ps_chart_level" - ], - [ - "ps_chart_volume" - ], - [ - "ps_chart_demand" - ], - [ - "ps_chart_netflow" - ], - [ - "ps_text_safety" - ], - [ - "ps_text_snapshot" - ] - ] - }, - { - "id": "ps_chart_level", - "type": "ui-chart", - "z": "ps_tab_basic_dashboard", - "group": "ui_group_ps_trends", - "name": "Level", - "label": "Level (m)", - "order": 1, - "width": 4, - "height": 4, - "chartType": "line", - "category": "topic", - "xAxisType": "time", - "yAxisLabel": "m", - "removeOlder": "15", - "removeOlderUnit": "60", - "x": 1230, - "y": 140, - "wires": [], - "showLegend": false, - "categoryType": "msg", - "xAxisProperty": "", - "xAxisPropertyType": "timestamp", - "xAxisFormat": "", - "xAxisFormatType": "auto", - "yAxisProperty": "payload", - "yAxisPropertyType": "msg", - "xmin": "", - "xmax": "", - "ymin": "0", - "ymax": "5", - "bins": 10, - "action": "append", - "stackSeries": false, - "pointShape": "circle", - "pointRadius": 4, - "interpolation": "linear", - "className": "", - "colors": [ - "#0c99d9" - ], - "textColor": [ - "#666666" - ], - "textColorDefault": true, - "gridColor": [ - "#e5e5e5" - ], - "gridColorDefault": true - }, - { - "id": "ps_chart_volume", - "type": "ui-chart", - "z": "ps_tab_basic_dashboard", - "group": "ui_group_ps_trends", - "name": "Volume", - "label": "Volume (m3)", - "order": 2, - "width": 4, - "height": 4, - "chartType": "line", - "category": "topic", - "xAxisType": "time", - "yAxisLabel": "m3", - "removeOlder": "15", - "removeOlderUnit": "60", - "x": 1230, - "y": 200, - "wires": [], - "showLegend": false, - "categoryType": "msg", - "xAxisProperty": "", - "xAxisPropertyType": "timestamp", - "xAxisFormat": "", - "xAxisFormatType": "auto", - "yAxisProperty": "payload", - "yAxisPropertyType": "msg", - "xmin": "", - "xmax": "", - "ymin": "0", - "ymax": "50", - "bins": 10, - "action": "append", - "stackSeries": false, - "pointShape": "circle", - "pointRadius": 4, - "interpolation": "linear", - "className": "", - "colors": [ - "#2ca02c" - ], - "textColor": [ - "#666666" - ], - "textColorDefault": true, - "gridColor": [ - "#e5e5e5" - ], - "gridColorDefault": true - }, - { - "id": "ps_chart_demand", - "type": "ui-chart", - "z": "ps_tab_basic_dashboard", - "group": "ui_group_ps_trends", - "name": "Demand", - "label": "Demand (%)", - "order": 3, - "width": 4, - "height": 4, - "chartType": "line", - "category": "topic", - "xAxisType": "time", - "yAxisLabel": "%", - "removeOlder": "15", - "removeOlderUnit": "60", - "x": 1230, - "y": 260, - "wires": [], - "showLegend": false, - "categoryType": "msg", - "xAxisProperty": "", - "xAxisPropertyType": "timestamp", - "xAxisFormat": "", - "xAxisFormatType": "auto", - "yAxisProperty": "payload", - "yAxisPropertyType": "msg", - "xmin": "", - "xmax": "", - "ymin": "0", - "ymax": "120", - "bins": 10, - "action": "append", - "stackSeries": false, - "pointShape": "circle", - "pointRadius": 4, - "interpolation": "linear", - "className": "", - "colors": [ - "#d68910" - ], - "textColor": [ - "#666666" - ], - "textColorDefault": true, - "gridColor": [ - "#e5e5e5" - ], - "gridColorDefault": true - }, - { - "id": "ps_chart_netflow", - "type": "ui-chart", - "z": "ps_tab_basic_dashboard", - "group": "ui_group_ps_trends", - "name": "Net Flow", - "label": "Net flow (m3/s)", - "order": 4, - "width": 4, - "height": 4, - "chartType": "line", - "category": "topic", - "xAxisType": "time", - "yAxisLabel": "m3/s", - "removeOlder": "15", - "removeOlderUnit": "60", - "x": 1240, - "y": 320, - "wires": [], - "showLegend": false, - "categoryType": "msg", - "xAxisProperty": "", - "xAxisPropertyType": "timestamp", - "xAxisFormat": "", - "xAxisFormatType": "auto", - "yAxisProperty": "payload", - "yAxisPropertyType": "msg", - "xmin": "", - "xmax": "", - "ymin": "", - "ymax": "", - "bins": 10, - "action": "append", - "stackSeries": false, - "pointShape": "circle", - "pointRadius": 4, - "interpolation": "linear", - "className": "", - "colors": [ - "#9467bd" - ], - "textColor": [ - "#666666" - ], - "textColorDefault": true, - "gridColor": [ - "#e5e5e5" - ], - "gridColorDefault": true - }, - { - "id": "ps_text_safety", - "type": "ui-text", - "z": "ps_tab_basic_dashboard", - "group": "ui_group_ps_state", - "name": "Safety", - "label": "Safety", - "order": 1, - "width": 4, - "height": 1, - "format": "{{msg.payload}}", - "layout": "row-spread", - "x": 1230, - "y": 380, - "wires": [] - }, - { - "id": "ps_text_snapshot", - "type": "ui-text", - "z": "ps_tab_basic_dashboard", - "group": "ui_group_ps_state", - "name": "Snapshot", - "label": "Snapshot", - "order": 2, - "width": 8, - "height": 1, - "format": "{{msg.payload}}", - "layout": "row-spread", - "x": 1240, - "y": 440, - "wires": [] - }, - { - "id": "ps_debug_influx", - "type": "debug", - "z": "ps_tab_basic_dashboard", - "name": "Influx output", - "active": false, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "true", - "targetType": "full", - "x": 980, - "y": 320, - "wires": [] - }, - { - "id": "ps_debug_parent", - "type": "debug", - "z": "ps_tab_basic_dashboard", - "name": "Parent output", - "active": false, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "true", - "targetType": "full", - "x": 980, - "y": 380, - "wires": [] - } -] diff --git a/examples/standalone-demo.js b/examples/standalone-demo.js deleted file mode 100644 index 08a1839..0000000 --- a/examples/standalone-demo.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Standalone PumpingStation demo — run with `node examples/standalone-demo.js`. - * Builds a station + one pump, calibrates predicted volume, ticks once. - * Useful for sanity-checking the orchestrator without Node-RED. - */ -const PumpingStation = require('../src/specificClass'); -const RotatingMachine = require('../../rotatingMachine/src/specificClass'); - -function createPumpingStationConfig(name) { - return { - general: { - logging: { enabled: true, logLevel: 'debug' }, - name, - id: `${name}-${Date.now()}`, - flowThreshold: 1e-4, - }, - functionality: { softwareType: 'pumpingStation', role: 'stationcontroller' }, - basin: { volume: 43.75, height: 10, inflowLevel: 3, outflowLevel: 0.2, overflowLevel: 3.2 }, - hydraulics: { refHeight: 'NAP', basinBottomRef: 0 }, - safety: { enableDryRunProtection: false, enableOverfillProtection: false }, - }; -} - -function createMachineConfig(name, position) { - return { - general: { name, logging: { enabled: false, logLevel: 'debug' } }, - functionality: { softwareType: 'machine', positionVsParent: position }, - asset: { supplier: 'Hydrostal', type: 'pump', category: 'centrifugal', model: 'hidrostal-H05K-S03R' }, - }; -} - -function createMachineStateConfig() { - return { - general: { logging: { enabled: true, logLevel: 'debug' } }, - movement: { speed: 1 }, - time: { starting: 2, warmingup: 3, stopping: 2, coolingdown: 3 }, - }; -} - -(async function demo() { - const station = new PumpingStation(createPumpingStationConfig('PumpingStationDemo')); - const pump1 = new RotatingMachine(createMachineConfig('Pump1', 'downstream'), createMachineStateConfig()); - - station.childRegistrationUtils.registerChild(pump1, 'machine'); - - setInterval(() => station.tick(), 1000); - await new Promise((resolve) => setTimeout(resolve, 10)); - - console.log('Initial state:', station.state); - station.setManualInflow(300, Date.now(), 'l/s'); - station.calibratePredictedVolume(3.4); - - console.log('Station state:', station.state); - console.log('Station output:', station.getOutput()); -})().catch((err) => { - console.error('Demo failed:', err); -}); diff --git a/pumpingStation.html b/pumpingStation.html index 29b2e60..3255d05 100644 --- a/pumpingStation.html +++ b/pumpingStation.html @@ -29,11 +29,11 @@ // Define station-specific properties simulator: { value: false }, - basinVolume: { value: 1 }, // m³, total empty basin - basinHeight: { value: 1 }, // m, floor to top - inflowLevel: { value: 0.8 }, // m, bottom/invert of inlet pipe above floor + basinVolume: { value: 50 }, // m³, total empty basin + basinHeight: { value: 4 }, // m, floor to top + inflowLevel: { value: 1.5 }, // m, bottom/invert of inlet pipe above floor outflowLevel: { value: 0.2 }, // m, top of outlet/suction pipe above floor - overflowLevel: { value: 0.9 }, // m, overflow elevation + overflowLevel: { value: 3.8 }, // m, overflow elevation defaultFluid: { value: "wastewater" }, inletPipeDiameter: { value: 0.3 }, // m outletPipeDiameter: { value: 0.3 }, // m @@ -84,10 +84,10 @@ enableShiftedRamp: { value: false }, shiftLevel: { value: 0 }, shiftArmPercent: { value: 95 }, - startLevel: { value: null }, - stopLevel: { value: null }, - minLevel: { value: null }, - maxLevel: { value: null }, + startLevel: { value: 1 }, // m, pump-on threshold (engagement edge) + stopLevel: { value: 0.5 }, // m, pump-off threshold (hysteresis fall-back) + minLevel: { value: 0.3 }, // m, hard-stop (just above outflow pipe top) + maxLevel: { value: 3.8 }, // m, 100% demand saturation flowSetpoint: { value: null }, flowDeadband: { value: null } diff --git a/src/editor/mode-preview.js b/src/editor/mode-preview.js index 65d5783..be793ba 100644 --- a/src/editor/mode-preview.js +++ b/src/editor/mode-preview.js @@ -90,14 +90,20 @@ return pts.join(' '); }; - // Up curve. Foot is startLevel (the configured pump-on threshold and - // ramp foot per the runtime in _controlLevelBased). The OFF baseline - // is drawn for level < startLevel; at startLevel demand jumps from - // OFF to 0 % and ramps up to 100 % at maxLevel. + // Up curve. Engagement edge is startLevel (pump-on threshold); the + // ramp foot is inflowLevel — matching the runtime in + // _controlLevelBased, which scales demand over [inflowLevel, maxLevel]. + // The OFF baseline is drawn for level < startLevel; between startLevel + // and inflowLevel demand sits flat at 0 % (system armed but not yet + // ramping); from inflowLevel demand ramps to 100 % at maxLevel. const up = document.getElementById('ps-mode-curve-up'); const down = document.getElementById('ps-mode-curve-down'); const downLabel = document.getElementById('ps-mode-curve-down-label'); - if (up) up.setAttribute('points', buildPath(start, start, max)); + // Runtime falls back to startLevel when inflowLevel is missing + // (basin?.inflowLevel ?? cfg.inflowLevel ?? startLevel); mirror that + // in the preview so the curve is still drawn instead of blank. + const upFoot = Number.isFinite(inlet) && inlet > start ? inlet : start; + if (up) up.setAttribute('points', buildPath(start, upFoot, max)); // Shifted-DOWN curve (only when shift enabled): represents the // worst-case held-then-ramp path drawn for hold=100 % (the SVG diff --git a/src/specificClass.js b/src/specificClass.js index 6162dab..fc46a2d 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -44,6 +44,12 @@ class PumpingStation extends BaseDomain { this.controlState = { percControl: 0 }; this.state = { direction: 'steady', netFlow: 0, flowSource: null, seconds: null, remainingSource: null }; + // Last operator demand from set.demand in manual mode. Stored on the + // host so getOutput()/status reflect it even when no children are + // registered yet (otherwise forwardDemand is invisible on Port 0/1). + // Cleared on mode change away from manual. + this._manualDemand = null; + // Level-armed hysteresis state — ported from basin-docs `_controlLevelBased`. // Exposed as instance fields because the e2e/basic tests assert on them // directly. levelBased strategy reads/writes via the same names. @@ -172,6 +178,8 @@ class PumpingStation extends BaseDomain { if (this.config.control.allowedModes?.has?.(newMode)) { this.logger.info(`Control mode changing from ${this.mode} to ${newMode}`); this.mode = newMode; + if (newMode !== 'manual') this._manualDemand = null; + this.notifyOutputChanged(); } else { this.logger.warn(`Attempted to change to unsupported control mode: ${newMode}`); } @@ -183,7 +191,11 @@ class PumpingStation extends BaseDomain { setManualInflow(value, ts = Date.now(), unit) { calibration.setManualInflow(this, value, ts, unit); } setManualOutflow(value, ts = Date.now(), unit) { calibration.setManualOutflow(this, value, ts, unit); } - forwardDemandToChildren(demand) { return control.manual.forwardDemand(this.context(), demand); } + forwardDemandToChildren(demand) { + this._manualDemand = Number.isFinite(demand) ? demand : null; + this.notifyOutputChanged(); + return control.manual.forwardDemand(this.context(), demand); + } // Direct delegations preserved so existing tests can drive the strategy // without re-mocking the dispatch layer. @@ -220,6 +232,8 @@ class PumpingStation extends BaseDomain { out.flowSource = this.state.flowSource; out.timeleft = this.state.seconds; out.percControl = this.controlState.percControl; + out.mode = this.mode; + out.manualDemand = this._manualDemand; // Derived safety thresholds — exposed so editor + dashboards can show // the dryRunLevel and highVolumeSafetyLevel without recomputing. @@ -247,15 +261,14 @@ class PumpingStation extends BaseDomain { steady: { arrow: '⏸️', fill: 'green' }, }; const { arrow = '❔', fill = 'grey' } = STYLES[this.state?.direction] || {}; - const vol = this.measurements.type('volume').variant('predicted').position('atequipment').getCurrentValue('m3') ?? 0; const pct = this.measurements.type('volumePercent').variant('predicted').position('atequipment').getCurrentValue() ?? 0; - const maxVol = this.basin?.maxVolAtOverflow ?? 0; const netFlowM3h = (this.state?.netFlow ?? 0) * 3600; - const seconds = this.state?.seconds; - const tStr = seconds != null ? `t≈${Math.round(seconds / 60)} min` : null; + const mode = this.mode || '?'; + const manualPart = this.mode === 'manual' && Number.isFinite(this._manualDemand) + ? `Qd=${this._manualDemand.toFixed(0)} m³/h` : null; return statusBadge.compose( - [`${arrow} ${pct.toFixed(1)}%`, `V=${vol.toFixed(2)} / ${maxVol.toFixed(2)} m³`, `net: ${netFlowM3h.toFixed(0)} m³/h`, tStr], + [mode, `${arrow} ${pct.toFixed(1)}%`, `net: ${netFlowM3h.toFixed(0)} m³/h`, manualPart], { fill, shape: 'dot' } ); } diff --git a/wiki/Home.md b/wiki/Home.md index 9e9a85f..f4a0fcb 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -20,17 +20,7 @@ A `pumpingStation` models a wet-well lift station: one basin with sensors, and o ## How it looks in Node-RED -> [!IMPORTANT] -> **Screenshot needed.** Drop a `pumpingStation` node onto a fresh Node-RED canvas and capture: -> - The node tile itself (its colour, badge text, label). -> - The full edit dialog when you double-click it (basin geometry section visible). -> -> Save as `wiki/_partial-screenshots/pumpingStation/01-node-and-editor.png` (PNG, target 1200×800, optimise to ≤ 200 KB). -> Then replace this callout with: -> -> ```markdown -> ![pumpingStation node and edit dialog](_partial-screenshots/pumpingStation/01-node-and-editor.png) -> ``` +![pumpingStation node and edit dialog](_partial-screenshots/pumpingStation/01-node-and-editor.png) --- @@ -62,15 +52,7 @@ curl -X POST -H 'Content-Type: application/json' \ http://localhost:1880/flow ``` -> [!IMPORTANT] -> **Flow screenshot needed.** Open the imported `01-Basic.json` flow in the Node-RED editor and capture the whole tab. The inject row should be visible on the left, the pumpingStation in the middle, the debug taps on the right. -> -> Save as `wiki/_partial-screenshots/pumpingStation/02-basic-flow.png` (PNG, target 1600×900, optimise to ≤ 250 KB). -> Replace this callout with: -> -> ```markdown -> ![Basic example flow in Node-RED editor](_partial-screenshots/pumpingStation/02-basic-flow.png) -> ``` +![Basic example flow in Node-RED editor](_partial-screenshots/pumpingStation/02-basic-flow.png) What to click in the dashboard after deploy: @@ -79,21 +61,7 @@ What to click in the dashboard after deploy: 3. `cmd.calibrate.level = 1.5 m` → the volume integrator syncs to a known level. 4. Watch Port 0 in the debug pane: level rises, predicted volume integrates, demand follows the curve. -> [!IMPORTANT] -> **GIF needed.** Record the dashboard reacting to the four clicks above. 15–25 seconds is enough. Use `peek` (Linux), LICEcap (Win/Mac), or any screen recorder; convert to GIF and optimise: -> -> ```bash -> # if you started from an mp4: -> ffmpeg -i raw.mp4 -vf "fps=15,scale=720:-1" -loop 0 stage.gif -> gifsicle -O3 --lossy=80 stage.gif -o final.gif -> ``` -> -> Save as `wiki/_partial-gifs/pumpingStation/01-basic-demo.gif` (target ≤ 1 MB). -> Replace this callout with: -> -> ```markdown -> ![Basic demo — level rises, demand follows](_partial-gifs/pumpingStation/01-basic-demo.gif) -> ``` +![Basic demo — level rises, demand follows](_partial-gifs/pumpingStation/01-basic-demo.gif) --- @@ -103,27 +71,11 @@ The two patterns you'll see most. ### Standalone (`01-Basic.json`) -> [!IMPORTANT] -> **Screenshot needed.** From the imported `01-Basic.json`, crop a tight view of just the inject column → pumpingStation → debug nodes. Skip the comment header. -> -> Save as `wiki/_partial-screenshots/pumpingStation/03-wiring-standalone.png` (PNG, target 1400×700). -> Replace this callout with: -> -> ```markdown -> ![Standalone wiring — inject buttons → pumpingStation → debug](_partial-screenshots/pumpingStation/03-wiring-standalone.png) -> ``` +![Standalone wiring — inject buttons → pumpingStation → debug](_partial-screenshots/pumpingStation/03-wiring-standalone.png) -### With a measurement child and an MGC parent (`02-Integration.json`) +### With a measurement child and an MGC parent -> [!IMPORTANT] -> **Screenshot needed.** From the imported `02-Integration.json`, capture the whole tab. The measurement node feeding the pumpingStation should be visible on the left; the MGC with its two `rotatingMachine` pumps on the right. -> -> Save as `wiki/_partial-screenshots/pumpingStation/04-wiring-integrated.png` (PNG, target 1600×900). -> Replace this callout with: -> -> ```markdown -> ![Integrated wiring — measurement → pumpingStation → MGC → 2 pumps](_partial-screenshots/pumpingStation/04-wiring-integrated.png) -> ``` +![Integrated wiring — measurement → pumpingStation → MGC → 2 pumps](_partial-screenshots/pumpingStation/04-wiring-integrated.png) --- diff --git a/wiki/Reference-Examples.md b/wiki/Reference-Examples.md index 00d2ee2..6950529 100644 --- a/wiki/Reference-Examples.md +++ b/wiki/Reference-Examples.md @@ -9,12 +9,10 @@ ## Shipped examples -| File | Tier | Tabs | What it shows | -|:---|:---:|:---|:---| -| `examples/01-Basic.json` | 1 | Process Plant | Single pumpingStation driven by inject nodes — no parent, no dashboard. | -| `examples/02-Integration.json` | 2 | Process Plant + Setup | Adds a `measurement` level child and a `machineGroupControl` parent with two `rotatingMachine` pumps. Demonstrates the Phase-2 parent / child handshake. | -| `examples/03-Dashboard.json` | 3 | Process Plant + Dashboard + Setup | Tier-2 plumbing plus a FlowFuse Dashboard 2.0 page with 3 charts (flow / level / volume %), text widgets, and 2 controls (mode dropdown + demand slider). | -| `examples/basic-dashboard.flow.json` | legacy | mixed | Pre-refactor flow kept for reference. Use `03-Dashboard.json` instead. | +| File | Tier | What it shows | +|:---|:---:|:---| +| `examples/01-Basic.json` | 1 | Single pumpingStation driven by inject nodes — no parent, no dashboard. Numbered driver groups for Mode / Flow signals / Operator demand / Calibration. | +| `examples/02-Dashboard.json` | 2 | Same command surface as Basic, driven by a FlowFuse Dashboard 2.0 page (Controls + live Status rows + 4 trend charts + raw-output table). | --- @@ -39,93 +37,67 @@ curl -X POST -H 'Content-Type: application/json' \ ## Example 01 — Basic standalone -> [!IMPORTANT] -> **Screenshot needed.** After importing `01-Basic.json`, capture the full Process Plant tab. -> -> Save as `wiki/_partial-screenshots/pumpingStation/05-ex01-basic.png`. -> Replace this callout with the image link. +![Basic example flow in Node-RED editor](_partial-screenshots/pumpingStation/02-basic-flow.png) ### Nodes on the tab | Type | Purpose | |:---|:---| | `comment` | Tab header / instructions | -| `inject` × 6 | Buttons to send `set.mode`, `set.inflow`, `set.demand`, `cmd.calibrate.volume`, `cmd.calibrate.level` | +| `inject` × 7 | Buttons to send `set.mode` (manual / levelbased), `set.inflow`, `set.outflow`, `set.demand`, `cmd.calibrate.volume`, `cmd.calibrate.level` | | `pumpingStation` | The unit under test | -| `function` | Merge Port-0 deltas into a single rolling snapshot | | `debug` × 3 | Port 0 (process), Port 1 (InfluxDB), Port 2 (parent reg) | +Driver injects are wrapped in four numbered groups: **1. Control mode**, **2. Flow signals (inflow / outflow)**, **3. Operator demand (manual mode only)**, **4. Calibration**. Debug nodes sit in a separate **Debug outputs (sidebar)** group on the right. + ### What to do after deploy -1. Click `set.mode = levelbased`. -2. Click `cmd.calibrate.level = 1.5 m` to anchor the volume integrator. -3. Click `set.inflow = 60 m³/h`. -4. Watch the Port-0 debug pane: `direction` flips to `filling`, `level` rises, `demand` follows the level curve, `etaSeconds` decreases. -5. Click `set.demand = 40 %` (only honoured in manual mode — for level-based, the controller decides demand from level). +1. (optional) Click `set.mode = manual` if you want `set.demand` to forward; otherwise leave it on the default `levelbased` and the ramp drives demand from level. +2. Click `set.inflow = 60 m³/h` — the basin starts filling. Watch Port 0 in the debug pane: `direction` flips to `filling`, `level` rises, predicted volume integrates. +3. In manual mode: click `set.demand = 40` — the value surfaces as `manualDemand` on Port 0/1 and in the node status badge. +4. Click `cmd.calibrate.volume = 25 m³` (or `cmd.calibrate.level = 1.5 m`) to snap the predicted-volume integrator. -> [!IMPORTANT] -> **GIF needed.** Record steps 1–4. Target 15–25 s, ≤ 1 MB after `gifsicle -O3 --lossy=80`. -> -> Save as `wiki/_partial-gifs/pumpingStation/02-ex01-demo.gif`. -> Replace this callout with the image link. +![Basic demo — level rises, demand follows](_partial-gifs/pumpingStation/01-basic-demo.gif) --- -## Example 02 — Integration with parent + children +## Example 02 — Dashboard > [!IMPORTANT] -> **Screenshot needed.** After importing `02-Integration.json`, capture the full Process Plant tab. +> **Screenshot needed.** Two captures from `02-Dashboard.json`: +> 1. The editor tab (left controls column + pumpingStation + Live-status group on the right). +> 2. The rendered dashboard at `http://localhost:1880/dashboard/pumpingstation-basic`. > -> Save as `wiki/_partial-screenshots/pumpingStation/06-ex02-integration.png`. -> Replace this callout with the image link. +> Save as `wiki/_partial-screenshots/pumpingStation/05-ex02-editor.png` and `06-ex02-dashboard.png`. +> Replace this callout with both image links. ### What it adds vs Example 01 | Addition | Why | |:---|:---| -| `measurement` node feeding `level` | Replaces the inject-driven level path with a real measurement child | -| `machineGroupControl` (MGC) parent | Demand goes upward to the MGC instead of being applied directly | -| Two `rotatingMachine` pumps under the MGC | The MGC load-shares demand across them | -| `Setup` tab | Initial calibration injects fire once via `once: true` | +| FlowFuse `ui-base` + `ui-theme` + `ui-page` setup | One dashboard page hosting four widget groups | +| `ui-button` × 7 (Controls group) | Replace the inject buttons one-for-one — each carries the canonical `msg.topic` directly | +| `ui-text` × 7 (Status group) | Live readouts: Mode, Direction, Level, Volume, Volume %, percControl, Manual demand | +| `ui-chart` × 4 (Trends group) | Level (m), Volume (m³), Volume % (0–100), Flow (m³/h, multi-series Inflow / Outflow / Net) | +| `ui-template` (Raw output group) | Full key/value table of the latest Port 0 cache — every field the node emits, sorted | +| Fan-out function | Caches last-known values so delta-only Port 0 updates never blank a status row, and forwards numeric values to the charts | -This exercises the Phase-2 parent / child handshake: `child.register` is sent on Port 2 of each child to its parent, and the parent's `commandRegistry` dispatches into `ChildRouter.onRegister(...)`. - -### What to do after deploy - -1. Setup tab fires once, calibrating volume and setting mode. -2. The MGC reports its predicted flow back to the pumpingStation. -3. Click any inject in the Process Plant tab to perturb the basin. -4. Watch all three Port-0 debug taps: PS, MGC, both pumps. - ---- - -## Example 03 — Dashboard - -> [!IMPORTANT] -> **Screenshot needed.** Two captures from `03-Dashboard.json`: -> 1. The editor tab (Dashboard UI) showing the dashboard widgets and trend-feeder functions. -> 2. The rendered dashboard at `http://localhost:1880/dashboard`. -> -> Save as `wiki/_partial-screenshots/pumpingStation/07-ex03-editor.png` and `08-ex03-dashboard.png`. -> Replace this callout with both image links. - -### What it adds vs Example 02 - -| Addition | Why | -|:---|:---| -| FlowFuse ui-base + ui-page + ui-group setup | One page, multiple grouped widgets | -| 3 ui-chart widgets | flow / level / volume % trends | -| ui-text widgets | live mode, demand, direction display | -| ui-dropdown for mode | operator-facing mode switch | -| ui-slider for demand | manual setpoint | -| Trend-feeder function | splits Port-0 deltas into one msg per chart with `msg.topic` set as series label | +The buttons fire the **same canonical `msg.topic`** as the inject nodes in Example 01 — there is no separate dashboard command surface to learn. Required: `@flowfuse/node-red-dashboard` (Dashboard 2.0) installed in the Node-RED instance. +### What to do after deploy + +1. Open `http://localhost:1880/dashboard/pumpingstation-basic`. +2. Click `Mode: Manual` or `Mode: Levelbased`. +3. Click `Inflow 60 m³/h` — Status panel level / volume / vol% rise; the Level / Volume / Flow charts plot the trends. +4. In manual mode click `Demand 40 m³/h` — `Manual demand` row updates, node badge appends `Qd=40 m³/h`. +5. Inspect the **Raw output** table at the bottom of the page for the full Port 0 surface (basin geometry, dryRunLevel, highVolumeSafetyLevel, predictedOverflowVolume, …). + > [!IMPORTANT] -> **GIF needed.** Slide the demand control and watch the trend charts react. 20–30 s is enough. +> **GIF needed.** Capture clicking through Mode → Inflow → Demand and the charts reacting. 20–30 s is enough. > -> Save as `wiki/_partial-gifs/pumpingStation/03-ex03-dashboard.gif`. +> Save as `wiki/_partial-gifs/pumpingStation/02-ex02-dashboard.gif`. > Replace this callout with the image link. --- @@ -159,7 +131,6 @@ Full file: [EVOLV/docker-compose.yml](https://gitea.wbd-rd.nl/RnD/EVOLV/src/bran | Level rises but `volume` stays at `minVol` | Volume integrator hasn't been calibrated. Send `cmd.calibrate.level = ` once. | | Demand stays at 0 % even though level is high | Mode might be `manual` — check `set.mode`. Or the safety layer is blocking (look at `safety.blocked` on Port 0). | | Predicted volume drifts | Net-flow source is wrong. Look at `flowSource` on Port 0; it should match the highest-level aggregator you have wired in. | -| MGC and pumps don't see demand | `02-Integration.json` requires the MGC to register **before** the pumps. The Setup tab handles ordering. | | `enableLog: 'debug'` floods the container log | Toggle it off in the node's config. Never ship a demo with debug logging enabled. | --- diff --git a/wiki/_partial-gifs/pumpingStation/01-basic-demo.gif b/wiki/_partial-gifs/pumpingStation/01-basic-demo.gif new file mode 100644 index 0000000..1c2aadc Binary files /dev/null and b/wiki/_partial-gifs/pumpingStation/01-basic-demo.gif differ diff --git a/wiki/_partial-screenshots/pumpingStation/01-node-and-editor.png b/wiki/_partial-screenshots/pumpingStation/01-node-and-editor.png new file mode 100644 index 0000000..2122af1 Binary files /dev/null and b/wiki/_partial-screenshots/pumpingStation/01-node-and-editor.png differ diff --git a/wiki/_partial-screenshots/pumpingStation/02-basic-flow.png b/wiki/_partial-screenshots/pumpingStation/02-basic-flow.png new file mode 100644 index 0000000..58037b0 Binary files /dev/null and b/wiki/_partial-screenshots/pumpingStation/02-basic-flow.png differ diff --git a/wiki/_partial-screenshots/pumpingStation/03-wiring-standalone.png b/wiki/_partial-screenshots/pumpingStation/03-wiring-standalone.png new file mode 100644 index 0000000..65d83a2 Binary files /dev/null and b/wiki/_partial-screenshots/pumpingStation/03-wiring-standalone.png differ diff --git a/wiki/_partial-screenshots/pumpingStation/04-wiring-integrated.png b/wiki/_partial-screenshots/pumpingStation/04-wiring-integrated.png new file mode 100644 index 0000000..a6b8c03 Binary files /dev/null and b/wiki/_partial-screenshots/pumpingStation/04-wiring-integrated.png differ