[ { "id": "tab_process", "type": "tab", "label": "\ud83c\udfed Process Plant", "disabled": false, "info": "EVOLV plant model: 3 rotatingMachines (each with 4 measurement nodes \u2014 upstream P, downstream P, flow, power), MGC, PS.\n\nPer pump there is a 'physics' function node that consumes the pump's own port-0 stream PLUS PS port-0 (basin level) and drives all 4 measurement nodes with physically-coupled values (upstream P from basin head; downstream P from pump state + flow; flow/power mirror predicted with Gaussian noise). This lives on this tab so the plant model is self-contained.\n\nAll cross-tab wires use named link-in / link-out channels." }, { "id": "c_process_title", "type": "comment", "z": "tab_process", "name": "\ud83c\udfed PROCESS PLANT \u2014 EVOLV nodes + per-pump physics feeders", "info": "", "x": 640, "y": 20, "wires": [] }, { "id": "c_pump_a", "type": "comment", "z": "tab_process", "name": "\u2500\u2500 Pump A \u2500\u2500 (pump + 4 sensors + physics feeder)", "info": "Up/Dn pressure + flow + power sensors register as children of the pump. The physics_ function takes the pump's own port-0 stream and PS port-0 (basin level) and drives all 4 sensors with physically-coupled values.", "x": 640, "y": 80, "wires": [] }, { "id": "meas_pump_a_u", "type": "measurement", "z": "tab_process", "name": "A-Up", "mode": "analog", "channels": "[]", "scaling": false, "i_min": 0, "i_max": 1, "i_offset": 0, "o_min": 0, "o_max": 4000, "simulator": false, "smooth_method": "mean", "count": "3", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "uuid": "sensor-pump_a-u", "supplier": "vega", "category": "sensor", "assetType": "pressure", "model": "vega-pressure-10", "unit": "mbar", "assetTagNumber": "A-U", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "upstream", "positionIcon": "\u2192", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 380, "y": 120, "wires": [ [], [ "lout_tlm_meas_pump_a_u" ], [ "pump_a" ] ] }, { "id": "lout_tlm_meas_pump_a_u", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 580, "y": 120, "wires": [] }, { "id": "meas_pump_a_d", "type": "measurement", "z": "tab_process", "name": "A-Dn", "mode": "analog", "channels": "[]", "scaling": false, "i_min": 0, "i_max": 1, "i_offset": 0, "o_min": 0, "o_max": 4000, "simulator": false, "smooth_method": "mean", "count": "3", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "uuid": "sensor-pump_a-d", "supplier": "vega", "category": "sensor", "assetType": "pressure", "model": "vega-pressure-10", "unit": "mbar", "assetTagNumber": "A-D", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "downstream", "positionIcon": "\u2190", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 380, "y": 155, "wires": [ [], [ "lout_tlm_meas_pump_a_d" ], [ "pump_a" ] ] }, { "id": "lout_tlm_meas_pump_a_d", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 580, "y": 155, "wires": [] }, { "id": "meas_pump_a_f", "type": "measurement", "z": "tab_process", "name": "A-Flow", "mode": "analog", "channels": "[]", "scaling": false, "i_min": 0, "i_max": 1, "i_offset": 0, "o_min": 0, "o_max": 250, "simulator": false, "smooth_method": "mean", "count": "3", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "uuid": "sensor-pump_a-f", "supplier": "endress", "category": "sensor", "assetType": "flow", "model": "endress-promag-50", "unit": "m3/h", "assetTagNumber": "A-F", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "downstream", "positionIcon": "\u2190", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 380, "y": 190, "wires": [ [], [ "lout_tlm_meas_pump_a_f" ], [ "pump_a" ] ] }, { "id": "lout_tlm_meas_pump_a_f", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 580, "y": 190, "wires": [] }, { "id": "meas_pump_a_p", "type": "measurement", "z": "tab_process", "name": "A-Pwr", "mode": "analog", "channels": "[]", "scaling": false, "i_min": 0, "i_max": 1, "i_offset": 0, "o_min": 0, "o_max": 30, "simulator": false, "smooth_method": "mean", "count": "3", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "uuid": "sensor-pump_a-p", "supplier": "siemens", "category": "sensor", "assetType": "power", "model": "siemens-sentron-pac4200", "unit": "kW", "assetTagNumber": "A-P", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "atEquipment", "positionIcon": "\u22a5", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 380, "y": 225, "wires": [ [], [ "lout_tlm_meas_pump_a_p" ], [ "pump_a" ] ] }, { "id": "lout_tlm_meas_pump_a_p", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 580, "y": 225, "wires": [] }, { "id": "pump_a", "type": "rotatingMachine", "z": "tab_process", "name": "Pump A", "speed": "200", "startup": "2", "warmup": "1", "shutdown": "2", "cooldown": "1", "movementMode": "staticspeed", "machineCurve": "", "uuid": "pump-pump_a", "supplier": "hidrostal", "category": "pump", "assetType": "pump-centrifugal", "model": "hidrostal-H05K-S03R", "unit": "m3/h", "curvePressureUnit": "mbar", "curveFlowUnit": "m3/h", "curvePowerUnit": "kW", "curveControlUnit": "%", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "atEquipment", "positionIcon": "\u22a5", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 900, "y": 170, "wires": [ [ "format_pump_a", "physics_pump_a" ], [ "lout_tlm_pump_a" ], [ "mgc_pumps" ] ] }, { "id": "lout_tlm_pump_a", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 900, "y": 210, "wires": [] }, { "id": "format_pump_a", "type": "function", "z": "tab_process", "name": "format Pump A port 0", "func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\n// Throttle dashboard fan-out to \u2264 2 Hz. The pump emits on\n// every state change (multiple per sec while cycling); the\n// dashboard doesn't need that resolution and the websocket\n// fan-out chokes the browser.\nconst now = Date.now();\nconst last = context.get('_lastEmit') || 0;\nif (now - last < 1000) return null;\ncontext.set('_lastEmit', now);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst flow = find('flow.predicted.downstream.');\nconst power = find('power.predicted.atequipment.');\nconst ctrl = find('ctrl.predicted.atequipment.');\nconst pUp = find('pressure.measured.upstream.');\nconst pDn = find('pressure.measured.downstream.');\nmsg.payload = {\n state: c.state || 'idle',\n mode: c.mode || 'auto',\n ctrl: ctrl != null ? Number(ctrl ).toFixed(1) + '%' : 'n/a',\n flow: flow != null ? Number(flow ).toFixed(1) + ' m\u00b3/h' : 'n/a',\n power: power != null ? Number(power).toFixed(2) + ' kW' : 'n/a',\n pUp: pUp != null ? Number(pUp ).toFixed(0) + ' mbar' : 'n/a',\n pDn: pDn != null ? Number(pDn ).toFixed(0) + ' mbar' : 'n/a',\n ctrlNum: ctrl != null ? Number(ctrl ) : null,\n flowNum: flow != null ? Number(flow ) : null,\n powerNum: power != null ? Number(power) : null,\n pUpNum: pUp != null ? Number(pUp ) : null,\n pDnNum: pDn != null ? Number(pDn ) : null,\n // Pump is moving water any time it's between startup and shutdown, not\n // just during steady operational. accelerate/decelerate/warmup count.\n isRunning: ['operational','starting','warmingup','accelerating','decelerating','stopping'].includes(c.state),\n};\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1160, "y": 170, "wires": [ [ "lout_evt_pump_a" ] ] }, { "id": "lout_evt_pump_a", "type": "link out", "z": "tab_process", "name": "evt:pump-A", "mode": "link", "links": [ "lin_evt_pump_a_dash" ], "x": 1420, "y": 170, "wires": [] }, { "id": "physics_pump_a", "type": "function", "z": "tab_process", "name": "physics Pump A \u2192 4 sensors", "func": "const c = context.get('c') || {};\nfunction find(o, prefix) {\n for (const k in o) { if (k.indexOf(prefix) === 0) return o[k]; }\n return null;\n}\nfunction gauss(sigma) {\n let s = 0;\n for (let i = 0; i < 12; i++) s += Math.random();\n return (s - 6) * sigma;\n}\n\nif (msg.from === 'ps') {\n const psSnap = c.ps || {};\n Object.assign(psSnap, msg.payload || {});\n c.ps = psSnap;\n const lvl = find(psSnap, 'level.predicted.atequipment.')\n ?? find(psSnap, 'level.measured.atequipment.');\n if (lvl != null) c.basinLevel = Number(lvl);\n context.set('c', c);\n return null;\n}\n\nconst pumpSnap = c.pump || {};\nObject.assign(pumpSnap, msg.payload || {});\nc.pump = pumpSnap;\ncontext.set('c', c);\n// Throttle: 1 Hz sensor updates are plenty for the demo; the\n// pump emits on every state change (5+/sec while cycling).\nconst _now = Date.now();\nconst _last = context.get('_lastEmit') || 0;\nif (_now - _last < 1000) return null;\ncontext.set('_lastEmit', _now);\n\nconst state = pumpSnap.state || 'idle';\n// 'isRunning' = the rotor is spinning (any non-idle, non-cooled state).\n// MGC retargets flow on every tick, so the pump spends most of its\n// time in 'accelerating' or 'decelerating', not 'operational'. Those\n// transient states are still moving water \u2014 flow/power sensors must\n// publish non-zero values during them or the measurement nodes go\n// quiet (formatMsg skips emits on no-diff).\nconst isRunning = ['operational','starting','warmingup','accelerating','decelerating','stopping'].includes(state);\n// 'pumpFlow' (not 'flow') \u2014 `flow` is the Node-RED flow-context object.\nconst pumpFlow = Number(find(pumpSnap, 'flow.predicted.downstream.'));\nconst pumpPower = Number(find(pumpSnap, 'power.predicted.atequipment.'));\nconst basinLevel = c.basinLevel != null ? Number(c.basinLevel) : 0;\n\n// Publish this pump's contribution to the flow-context shared\n// header so the other physics feeders can compute total flow.\nflow.set('pump_flow_a', isRunning && Number.isFinite(pumpFlow) ? pumpFlow : 0);\nflow.set('pump_flow_a_state', state);\nconst flowA = Number(flow.get('pump_flow_a') || 0);\nconst flowB = Number(flow.get('pump_flow_b') || 0);\nconst flowC = Number(flow.get('pump_flow_c') || 0);\nconst totalFlow = flowA + flowB + flowC;\n\nconst HEAD_M = Math.max(0, basinLevel - 0.3);\n// Suction (basin) header pressure \u2014 same physical value for all\n// pumps; per-pump sensor noise added independently.\nconst p_upstream_clean = 98.1 * HEAD_M;\nlet p_upstream = Math.max(0, p_upstream_clean + gauss(2.5));\n\n// Discharge (header) pressure \u2014 driven by TOTAL flow leaving the\n// manifold, NOT this pump's individual flow. Static head 12 m\n// + quadratic system curve scaled so totalFlow=300 m\u00b3/h gives\n// ~full dynamic head.\nconst STATIC_MBAR = 12 * 98.1;\nconst DYN_MBAR_MAX = 12 * 98.1;\nconst TOTAL_FLOW_MAX = 300;\nconst ratio = Math.min(1, totalFlow / TOTAL_FLOW_MAX);\nconst p_downstream_header = STATIC_MBAR + ratio * ratio * DYN_MBAR_MAX;\n// Publish the clean header value to flow context so the MGC's\n// header-pressure measurement child can read it.\nflow.set('header_p_downstream', p_downstream_header);\nflow.set('header_p_upstream', p_upstream_clean);\n// Per-pump downstream sensor: header value with local sensor noise.\nlet p_downstream = Math.max(0, p_downstream_header + gauss(8));\n\nconst flowMeas = (isRunning && Number.isFinite(pumpFlow))\n ? Math.max(0, pumpFlow + gauss(Math.max(0.5, pumpFlow * 0.01)))\n : 0;\n\nconst powerMeas = (isRunning && Number.isFinite(pumpPower))\n ? Math.max(0, pumpPower + gauss(Math.max(0.05, pumpPower * 0.005)))\n : 0;\n\nreturn [\n { topic: 'measurement', payload: p_upstream },\n { topic: 'measurement', payload: p_downstream },\n { topic: 'measurement', payload: flowMeas },\n { topic: 'measurement', payload: powerMeas },\n];\n", "outputs": 4, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1160, "y": 240, "wires": [ [ "meas_pump_a_u" ], [ "meas_pump_a_d" ], [ "meas_pump_a_f" ], [ "meas_pump_a_p" ] ] }, { "id": "lin_setpoint_pump_a", "type": "link in", "z": "tab_process", "name": "cmd:setpoint-A", "links": [ "lout_setpoint_pump_a_dash" ], "x": 120, "y": 140, "wires": [ [ "build_setpoint_pump_a" ] ] }, { "id": "build_setpoint_pump_a", "type": "function", "z": "tab_process", "name": "build setpoint cmd (Pump A)", "func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'GUI', action: 'execMovement', setpoint: Number(msg.payload) };\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 600, "y": 140, "wires": [ [ "pump_a" ] ] }, { "id": "lin_seq_pump_a", "type": "link in", "z": "tab_process", "name": "cmd:pump-A-seq", "links": [ "lout_seq_pump_a_dash" ], "x": 120, "y": 190, "wires": [ [ "pump_a" ] ] }, { "id": "c_pump_b", "type": "comment", "z": "tab_process", "name": "\u2500\u2500 Pump B \u2500\u2500 (pump + 4 sensors + physics feeder)", "info": "Up/Dn pressure + flow + power sensors register as children of the pump. The physics_ function takes the pump's own port-0 stream and PS port-0 (basin level) and drives all 4 sensors with physically-coupled values.", "x": 640, "y": 360, "wires": [] }, { "id": "meas_pump_b_u", "type": "measurement", "z": "tab_process", "name": "B-Up", "mode": "analog", "channels": "[]", "scaling": false, "i_min": 0, "i_max": 1, "i_offset": 0, "o_min": 0, "o_max": 4000, "simulator": false, "smooth_method": "mean", "count": "3", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "uuid": "sensor-pump_b-u", "supplier": "vega", "category": "sensor", "assetType": "pressure", "model": "vega-pressure-10", "unit": "mbar", "assetTagNumber": "B-U", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "upstream", "positionIcon": "\u2192", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 380, "y": 400, "wires": [ [], [ "lout_tlm_meas_pump_b_u" ], [ "pump_b" ] ] }, { "id": "lout_tlm_meas_pump_b_u", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 580, "y": 400, "wires": [] }, { "id": "meas_pump_b_d", "type": "measurement", "z": "tab_process", "name": "B-Dn", "mode": "analog", "channels": "[]", "scaling": false, "i_min": 0, "i_max": 1, "i_offset": 0, "o_min": 0, "o_max": 4000, "simulator": false, "smooth_method": "mean", "count": "3", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "uuid": "sensor-pump_b-d", "supplier": "vega", "category": "sensor", "assetType": "pressure", "model": "vega-pressure-10", "unit": "mbar", "assetTagNumber": "B-D", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "downstream", "positionIcon": "\u2190", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 380, "y": 435, "wires": [ [], [ "lout_tlm_meas_pump_b_d" ], [ "pump_b" ] ] }, { "id": "lout_tlm_meas_pump_b_d", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 580, "y": 435, "wires": [] }, { "id": "meas_pump_b_f", "type": "measurement", "z": "tab_process", "name": "B-Flow", "mode": "analog", "channels": "[]", "scaling": false, "i_min": 0, "i_max": 1, "i_offset": 0, "o_min": 0, "o_max": 250, "simulator": false, "smooth_method": "mean", "count": "3", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "uuid": "sensor-pump_b-f", "supplier": "endress", "category": "sensor", "assetType": "flow", "model": "endress-promag-50", "unit": "m3/h", "assetTagNumber": "B-F", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "downstream", "positionIcon": "\u2190", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 380, "y": 470, "wires": [ [], [ "lout_tlm_meas_pump_b_f" ], [ "pump_b" ] ] }, { "id": "lout_tlm_meas_pump_b_f", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 580, "y": 470, "wires": [] }, { "id": "meas_pump_b_p", "type": "measurement", "z": "tab_process", "name": "B-Pwr", "mode": "analog", "channels": "[]", "scaling": false, "i_min": 0, "i_max": 1, "i_offset": 0, "o_min": 0, "o_max": 30, "simulator": false, "smooth_method": "mean", "count": "3", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "uuid": "sensor-pump_b-p", "supplier": "siemens", "category": "sensor", "assetType": "power", "model": "siemens-sentron-pac4200", "unit": "kW", "assetTagNumber": "B-P", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "atEquipment", "positionIcon": "\u22a5", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 380, "y": 505, "wires": [ [], [ "lout_tlm_meas_pump_b_p" ], [ "pump_b" ] ] }, { "id": "lout_tlm_meas_pump_b_p", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 580, "y": 505, "wires": [] }, { "id": "pump_b", "type": "rotatingMachine", "z": "tab_process", "name": "Pump B", "speed": "200", "startup": "2", "warmup": "1", "shutdown": "2", "cooldown": "1", "movementMode": "staticspeed", "machineCurve": "", "uuid": "pump-pump_b", "supplier": "hidrostal", "category": "pump", "assetType": "pump-centrifugal", "model": "hidrostal-H05K-S03R", "unit": "m3/h", "curvePressureUnit": "mbar", "curveFlowUnit": "m3/h", "curvePowerUnit": "kW", "curveControlUnit": "%", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "atEquipment", "positionIcon": "\u22a5", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 900, "y": 450, "wires": [ [ "format_pump_b", "physics_pump_b" ], [ "lout_tlm_pump_b" ], [ "mgc_pumps" ] ] }, { "id": "lout_tlm_pump_b", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 900, "y": 490, "wires": [] }, { "id": "format_pump_b", "type": "function", "z": "tab_process", "name": "format Pump B port 0", "func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\n// Throttle dashboard fan-out to \u2264 2 Hz. The pump emits on\n// every state change (multiple per sec while cycling); the\n// dashboard doesn't need that resolution and the websocket\n// fan-out chokes the browser.\nconst now = Date.now();\nconst last = context.get('_lastEmit') || 0;\nif (now - last < 1000) return null;\ncontext.set('_lastEmit', now);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst flow = find('flow.predicted.downstream.');\nconst power = find('power.predicted.atequipment.');\nconst ctrl = find('ctrl.predicted.atequipment.');\nconst pUp = find('pressure.measured.upstream.');\nconst pDn = find('pressure.measured.downstream.');\nmsg.payload = {\n state: c.state || 'idle',\n mode: c.mode || 'auto',\n ctrl: ctrl != null ? Number(ctrl ).toFixed(1) + '%' : 'n/a',\n flow: flow != null ? Number(flow ).toFixed(1) + ' m\u00b3/h' : 'n/a',\n power: power != null ? Number(power).toFixed(2) + ' kW' : 'n/a',\n pUp: pUp != null ? Number(pUp ).toFixed(0) + ' mbar' : 'n/a',\n pDn: pDn != null ? Number(pDn ).toFixed(0) + ' mbar' : 'n/a',\n ctrlNum: ctrl != null ? Number(ctrl ) : null,\n flowNum: flow != null ? Number(flow ) : null,\n powerNum: power != null ? Number(power) : null,\n pUpNum: pUp != null ? Number(pUp ) : null,\n pDnNum: pDn != null ? Number(pDn ) : null,\n // Pump is moving water any time it's between startup and shutdown, not\n // just during steady operational. accelerate/decelerate/warmup count.\n isRunning: ['operational','starting','warmingup','accelerating','decelerating','stopping'].includes(c.state),\n};\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1160, "y": 450, "wires": [ [ "lout_evt_pump_b" ] ] }, { "id": "lout_evt_pump_b", "type": "link out", "z": "tab_process", "name": "evt:pump-B", "mode": "link", "links": [ "lin_evt_pump_b_dash" ], "x": 1420, "y": 450, "wires": [] }, { "id": "physics_pump_b", "type": "function", "z": "tab_process", "name": "physics Pump B \u2192 4 sensors", "func": "const c = context.get('c') || {};\nfunction find(o, prefix) {\n for (const k in o) { if (k.indexOf(prefix) === 0) return o[k]; }\n return null;\n}\nfunction gauss(sigma) {\n let s = 0;\n for (let i = 0; i < 12; i++) s += Math.random();\n return (s - 6) * sigma;\n}\n\nif (msg.from === 'ps') {\n const psSnap = c.ps || {};\n Object.assign(psSnap, msg.payload || {});\n c.ps = psSnap;\n const lvl = find(psSnap, 'level.predicted.atequipment.')\n ?? find(psSnap, 'level.measured.atequipment.');\n if (lvl != null) c.basinLevel = Number(lvl);\n context.set('c', c);\n return null;\n}\n\nconst pumpSnap = c.pump || {};\nObject.assign(pumpSnap, msg.payload || {});\nc.pump = pumpSnap;\ncontext.set('c', c);\n// Throttle: 1 Hz sensor updates are plenty for the demo; the\n// pump emits on every state change (5+/sec while cycling).\nconst _now = Date.now();\nconst _last = context.get('_lastEmit') || 0;\nif (_now - _last < 1000) return null;\ncontext.set('_lastEmit', _now);\n\nconst state = pumpSnap.state || 'idle';\n// 'isRunning' = the rotor is spinning (any non-idle, non-cooled state).\n// MGC retargets flow on every tick, so the pump spends most of its\n// time in 'accelerating' or 'decelerating', not 'operational'. Those\n// transient states are still moving water \u2014 flow/power sensors must\n// publish non-zero values during them or the measurement nodes go\n// quiet (formatMsg skips emits on no-diff).\nconst isRunning = ['operational','starting','warmingup','accelerating','decelerating','stopping'].includes(state);\n// 'pumpFlow' (not 'flow') \u2014 `flow` is the Node-RED flow-context object.\nconst pumpFlow = Number(find(pumpSnap, 'flow.predicted.downstream.'));\nconst pumpPower = Number(find(pumpSnap, 'power.predicted.atequipment.'));\nconst basinLevel = c.basinLevel != null ? Number(c.basinLevel) : 0;\n\n// Publish this pump's contribution to the flow-context shared\n// header so the other physics feeders can compute total flow.\nflow.set('pump_flow_b', isRunning && Number.isFinite(pumpFlow) ? pumpFlow : 0);\nflow.set('pump_flow_b_state', state);\nconst flowA = Number(flow.get('pump_flow_a') || 0);\nconst flowB = Number(flow.get('pump_flow_b') || 0);\nconst flowC = Number(flow.get('pump_flow_c') || 0);\nconst totalFlow = flowA + flowB + flowC;\n\nconst HEAD_M = Math.max(0, basinLevel - 0.3);\n// Suction (basin) header pressure \u2014 same physical value for all\n// pumps; per-pump sensor noise added independently.\nconst p_upstream_clean = 98.1 * HEAD_M;\nlet p_upstream = Math.max(0, p_upstream_clean + gauss(2.5));\n\n// Discharge (header) pressure \u2014 driven by TOTAL flow leaving the\n// manifold, NOT this pump's individual flow. Static head 12 m\n// + quadratic system curve scaled so totalFlow=300 m\u00b3/h gives\n// ~full dynamic head.\nconst STATIC_MBAR = 12 * 98.1;\nconst DYN_MBAR_MAX = 12 * 98.1;\nconst TOTAL_FLOW_MAX = 300;\nconst ratio = Math.min(1, totalFlow / TOTAL_FLOW_MAX);\nconst p_downstream_header = STATIC_MBAR + ratio * ratio * DYN_MBAR_MAX;\n// Publish the clean header value to flow context so the MGC's\n// header-pressure measurement child can read it.\nflow.set('header_p_downstream', p_downstream_header);\nflow.set('header_p_upstream', p_upstream_clean);\n// Per-pump downstream sensor: header value with local sensor noise.\nlet p_downstream = Math.max(0, p_downstream_header + gauss(8));\n\nconst flowMeas = (isRunning && Number.isFinite(pumpFlow))\n ? Math.max(0, pumpFlow + gauss(Math.max(0.5, pumpFlow * 0.01)))\n : 0;\n\nconst powerMeas = (isRunning && Number.isFinite(pumpPower))\n ? Math.max(0, pumpPower + gauss(Math.max(0.05, pumpPower * 0.005)))\n : 0;\n\nreturn [\n { topic: 'measurement', payload: p_upstream },\n { topic: 'measurement', payload: p_downstream },\n { topic: 'measurement', payload: flowMeas },\n { topic: 'measurement', payload: powerMeas },\n];\n", "outputs": 4, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1160, "y": 520, "wires": [ [ "meas_pump_b_u" ], [ "meas_pump_b_d" ], [ "meas_pump_b_f" ], [ "meas_pump_b_p" ] ] }, { "id": "lin_setpoint_pump_b", "type": "link in", "z": "tab_process", "name": "cmd:setpoint-B", "links": [ "lout_setpoint_pump_b_dash" ], "x": 120, "y": 420, "wires": [ [ "build_setpoint_pump_b" ] ] }, { "id": "build_setpoint_pump_b", "type": "function", "z": "tab_process", "name": "build setpoint cmd (Pump B)", "func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'GUI', action: 'execMovement', setpoint: Number(msg.payload) };\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 600, "y": 420, "wires": [ [ "pump_b" ] ] }, { "id": "lin_seq_pump_b", "type": "link in", "z": "tab_process", "name": "cmd:pump-B-seq", "links": [ "lout_seq_pump_b_dash" ], "x": 120, "y": 470, "wires": [ [ "pump_b" ] ] }, { "id": "c_pump_c", "type": "comment", "z": "tab_process", "name": "\u2500\u2500 Pump C \u2500\u2500 (pump + 4 sensors + physics feeder)", "info": "Up/Dn pressure + flow + power sensors register as children of the pump. The physics_ function takes the pump's own port-0 stream and PS port-0 (basin level) and drives all 4 sensors with physically-coupled values.", "x": 640, "y": 640, "wires": [] }, { "id": "meas_pump_c_u", "type": "measurement", "z": "tab_process", "name": "C-Up", "mode": "analog", "channels": "[]", "scaling": false, "i_min": 0, "i_max": 1, "i_offset": 0, "o_min": 0, "o_max": 4000, "simulator": false, "smooth_method": "mean", "count": "3", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "uuid": "sensor-pump_c-u", "supplier": "vega", "category": "sensor", "assetType": "pressure", "model": "vega-pressure-10", "unit": "mbar", "assetTagNumber": "C-U", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "upstream", "positionIcon": "\u2192", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 380, "y": 680, "wires": [ [], [ "lout_tlm_meas_pump_c_u" ], [ "pump_c" ] ] }, { "id": "lout_tlm_meas_pump_c_u", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 580, "y": 680, "wires": [] }, { "id": "meas_pump_c_d", "type": "measurement", "z": "tab_process", "name": "C-Dn", "mode": "analog", "channels": "[]", "scaling": false, "i_min": 0, "i_max": 1, "i_offset": 0, "o_min": 0, "o_max": 4000, "simulator": false, "smooth_method": "mean", "count": "3", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "uuid": "sensor-pump_c-d", "supplier": "vega", "category": "sensor", "assetType": "pressure", "model": "vega-pressure-10", "unit": "mbar", "assetTagNumber": "C-D", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "downstream", "positionIcon": "\u2190", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 380, "y": 715, "wires": [ [], [ "lout_tlm_meas_pump_c_d" ], [ "pump_c" ] ] }, { "id": "lout_tlm_meas_pump_c_d", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 580, "y": 715, "wires": [] }, { "id": "meas_pump_c_f", "type": "measurement", "z": "tab_process", "name": "C-Flow", "mode": "analog", "channels": "[]", "scaling": false, "i_min": 0, "i_max": 1, "i_offset": 0, "o_min": 0, "o_max": 250, "simulator": false, "smooth_method": "mean", "count": "3", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "uuid": "sensor-pump_c-f", "supplier": "endress", "category": "sensor", "assetType": "flow", "model": "endress-promag-50", "unit": "m3/h", "assetTagNumber": "C-F", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "downstream", "positionIcon": "\u2190", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 380, "y": 750, "wires": [ [], [ "lout_tlm_meas_pump_c_f" ], [ "pump_c" ] ] }, { "id": "lout_tlm_meas_pump_c_f", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 580, "y": 750, "wires": [] }, { "id": "meas_pump_c_p", "type": "measurement", "z": "tab_process", "name": "C-Pwr", "mode": "analog", "channels": "[]", "scaling": false, "i_min": 0, "i_max": 1, "i_offset": 0, "o_min": 0, "o_max": 30, "simulator": false, "smooth_method": "mean", "count": "3", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "uuid": "sensor-pump_c-p", "supplier": "siemens", "category": "sensor", "assetType": "power", "model": "siemens-sentron-pac4200", "unit": "kW", "assetTagNumber": "C-P", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "atEquipment", "positionIcon": "\u22a5", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 380, "y": 785, "wires": [ [], [ "lout_tlm_meas_pump_c_p" ], [ "pump_c" ] ] }, { "id": "lout_tlm_meas_pump_c_p", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 580, "y": 785, "wires": [] }, { "id": "pump_c", "type": "rotatingMachine", "z": "tab_process", "name": "Pump C", "speed": "200", "startup": "2", "warmup": "1", "shutdown": "2", "cooldown": "1", "movementMode": "staticspeed", "machineCurve": "", "uuid": "pump-pump_c", "supplier": "hidrostal", "category": "pump", "assetType": "pump-centrifugal", "model": "hidrostal-H05K-S03R", "unit": "m3/h", "curvePressureUnit": "mbar", "curveFlowUnit": "m3/h", "curvePowerUnit": "kW", "curveControlUnit": "%", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "atEquipment", "positionIcon": "\u22a5", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "x": 900, "y": 730, "wires": [ [ "format_pump_c", "physics_pump_c" ], [ "lout_tlm_pump_c" ], [ "mgc_pumps" ] ] }, { "id": "lout_tlm_pump_c", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 900, "y": 770, "wires": [] }, { "id": "format_pump_c", "type": "function", "z": "tab_process", "name": "format Pump C port 0", "func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\n// Throttle dashboard fan-out to \u2264 2 Hz. The pump emits on\n// every state change (multiple per sec while cycling); the\n// dashboard doesn't need that resolution and the websocket\n// fan-out chokes the browser.\nconst now = Date.now();\nconst last = context.get('_lastEmit') || 0;\nif (now - last < 1000) return null;\ncontext.set('_lastEmit', now);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst flow = find('flow.predicted.downstream.');\nconst power = find('power.predicted.atequipment.');\nconst ctrl = find('ctrl.predicted.atequipment.');\nconst pUp = find('pressure.measured.upstream.');\nconst pDn = find('pressure.measured.downstream.');\nmsg.payload = {\n state: c.state || 'idle',\n mode: c.mode || 'auto',\n ctrl: ctrl != null ? Number(ctrl ).toFixed(1) + '%' : 'n/a',\n flow: flow != null ? Number(flow ).toFixed(1) + ' m\u00b3/h' : 'n/a',\n power: power != null ? Number(power).toFixed(2) + ' kW' : 'n/a',\n pUp: pUp != null ? Number(pUp ).toFixed(0) + ' mbar' : 'n/a',\n pDn: pDn != null ? Number(pDn ).toFixed(0) + ' mbar' : 'n/a',\n ctrlNum: ctrl != null ? Number(ctrl ) : null,\n flowNum: flow != null ? Number(flow ) : null,\n powerNum: power != null ? Number(power) : null,\n pUpNum: pUp != null ? Number(pUp ) : null,\n pDnNum: pDn != null ? Number(pDn ) : null,\n // Pump is moving water any time it's between startup and shutdown, not\n // just during steady operational. accelerate/decelerate/warmup count.\n isRunning: ['operational','starting','warmingup','accelerating','decelerating','stopping'].includes(c.state),\n};\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1160, "y": 730, "wires": [ [ "lout_evt_pump_c" ] ] }, { "id": "lout_evt_pump_c", "type": "link out", "z": "tab_process", "name": "evt:pump-C", "mode": "link", "links": [ "lin_evt_pump_c_dash" ], "x": 1420, "y": 730, "wires": [] }, { "id": "physics_pump_c", "type": "function", "z": "tab_process", "name": "physics Pump C \u2192 4 sensors", "func": "const c = context.get('c') || {};\nfunction find(o, prefix) {\n for (const k in o) { if (k.indexOf(prefix) === 0) return o[k]; }\n return null;\n}\nfunction gauss(sigma) {\n let s = 0;\n for (let i = 0; i < 12; i++) s += Math.random();\n return (s - 6) * sigma;\n}\n\nif (msg.from === 'ps') {\n const psSnap = c.ps || {};\n Object.assign(psSnap, msg.payload || {});\n c.ps = psSnap;\n const lvl = find(psSnap, 'level.predicted.atequipment.')\n ?? find(psSnap, 'level.measured.atequipment.');\n if (lvl != null) c.basinLevel = Number(lvl);\n context.set('c', c);\n return null;\n}\n\nconst pumpSnap = c.pump || {};\nObject.assign(pumpSnap, msg.payload || {});\nc.pump = pumpSnap;\ncontext.set('c', c);\n// Throttle: 1 Hz sensor updates are plenty for the demo; the\n// pump emits on every state change (5+/sec while cycling).\nconst _now = Date.now();\nconst _last = context.get('_lastEmit') || 0;\nif (_now - _last < 1000) return null;\ncontext.set('_lastEmit', _now);\n\nconst state = pumpSnap.state || 'idle';\n// 'isRunning' = the rotor is spinning (any non-idle, non-cooled state).\n// MGC retargets flow on every tick, so the pump spends most of its\n// time in 'accelerating' or 'decelerating', not 'operational'. Those\n// transient states are still moving water \u2014 flow/power sensors must\n// publish non-zero values during them or the measurement nodes go\n// quiet (formatMsg skips emits on no-diff).\nconst isRunning = ['operational','starting','warmingup','accelerating','decelerating','stopping'].includes(state);\n// 'pumpFlow' (not 'flow') \u2014 `flow` is the Node-RED flow-context object.\nconst pumpFlow = Number(find(pumpSnap, 'flow.predicted.downstream.'));\nconst pumpPower = Number(find(pumpSnap, 'power.predicted.atequipment.'));\nconst basinLevel = c.basinLevel != null ? Number(c.basinLevel) : 0;\n\n// Publish this pump's contribution to the flow-context shared\n// header so the other physics feeders can compute total flow.\nflow.set('pump_flow_c', isRunning && Number.isFinite(pumpFlow) ? pumpFlow : 0);\nflow.set('pump_flow_c_state', state);\nconst flowA = Number(flow.get('pump_flow_a') || 0);\nconst flowB = Number(flow.get('pump_flow_b') || 0);\nconst flowC = Number(flow.get('pump_flow_c') || 0);\nconst totalFlow = flowA + flowB + flowC;\n\nconst HEAD_M = Math.max(0, basinLevel - 0.3);\n// Suction (basin) header pressure \u2014 same physical value for all\n// pumps; per-pump sensor noise added independently.\nconst p_upstream_clean = 98.1 * HEAD_M;\nlet p_upstream = Math.max(0, p_upstream_clean + gauss(2.5));\n\n// Discharge (header) pressure \u2014 driven by TOTAL flow leaving the\n// manifold, NOT this pump's individual flow. Static head 12 m\n// + quadratic system curve scaled so totalFlow=300 m\u00b3/h gives\n// ~full dynamic head.\nconst STATIC_MBAR = 12 * 98.1;\nconst DYN_MBAR_MAX = 12 * 98.1;\nconst TOTAL_FLOW_MAX = 300;\nconst ratio = Math.min(1, totalFlow / TOTAL_FLOW_MAX);\nconst p_downstream_header = STATIC_MBAR + ratio * ratio * DYN_MBAR_MAX;\n// Publish the clean header value to flow context so the MGC's\n// header-pressure measurement child can read it.\nflow.set('header_p_downstream', p_downstream_header);\nflow.set('header_p_upstream', p_upstream_clean);\n// Per-pump downstream sensor: header value with local sensor noise.\nlet p_downstream = Math.max(0, p_downstream_header + gauss(8));\n\nconst flowMeas = (isRunning && Number.isFinite(pumpFlow))\n ? Math.max(0, pumpFlow + gauss(Math.max(0.5, pumpFlow * 0.01)))\n : 0;\n\nconst powerMeas = (isRunning && Number.isFinite(pumpPower))\n ? Math.max(0, pumpPower + gauss(Math.max(0.05, pumpPower * 0.005)))\n : 0;\n\nreturn [\n { topic: 'measurement', payload: p_upstream },\n { topic: 'measurement', payload: p_downstream },\n { topic: 'measurement', payload: flowMeas },\n { topic: 'measurement', payload: powerMeas },\n];\n", "outputs": 4, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1160, "y": 800, "wires": [ [ "meas_pump_c_u" ], [ "meas_pump_c_d" ], [ "meas_pump_c_f" ], [ "meas_pump_c_p" ] ] }, { "id": "lin_setpoint_pump_c", "type": "link in", "z": "tab_process", "name": "cmd:setpoint-C", "links": [ "lout_setpoint_pump_c_dash" ], "x": 120, "y": 700, "wires": [ [ "build_setpoint_pump_c" ] ] }, { "id": "build_setpoint_pump_c", "type": "function", "z": "tab_process", "name": "build setpoint cmd (Pump C)", "func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'GUI', action: 'execMovement', setpoint: Number(msg.payload) };\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 600, "y": 700, "wires": [ [ "pump_c" ] ] }, { "id": "lin_seq_pump_c", "type": "link in", "z": "tab_process", "name": "cmd:pump-C-seq", "links": [ "lout_seq_pump_c_dash" ], "x": 120, "y": 750, "wires": [ [ "pump_c" ] ] }, { "id": "c_mgc", "type": "comment", "z": "tab_process", "name": "\u2500\u2500 MGC \u2500\u2500 (orchestrates the 3 pumps via optimalcontrol)", "info": "", "x": 640, "y": 920, "wires": [] }, { "id": "mgc_pumps", "type": "machineGroupControl", "z": "tab_process", "name": "MGC \u2014 Pump Group", "uuid": "mgc-pump-group", "category": "controller", "assetType": "machinegroupcontrol", "model": "default", "unit": "m3/h", "supplier": "evolv", "enableLog": true, "logLevel": "debug", "tickIntervalMs": 2000, "positionVsParent": "atEquipment", "positionIcon": "\u22a5", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "x": 900, "y": 1000, "wires": [ [ "format_mgc" ], [ "lout_tlm_mgc" ], [ "ps_basin" ] ] }, { "id": "lout_tlm_mgc", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 900, "y": 1040, "wires": [] }, { "id": "format_mgc", "type": "function", "z": "tab_process", "name": "format MGC port 0", "func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\n// Throttle: MGC fires on every distribution change.\nconst now = Date.now();\nconst last = context.get('_lastEmit') || 0;\nif (now - last < 1000) return null;\ncontext.set('_lastEmit', now);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst totalFlow = find('flow.predicted.atequipment.') ?? find('downstream_predicted_flow');\nconst totalPower = find('power.predicted.atequipment.') ?? find('atEquipment_predicted_power');\nconst eff = find('efficiency.predicted.atequipment.');\nmsg.payload = {\n totalFlow: totalFlow != null ? Number(totalFlow ).toFixed(1) + ' m\u00b3/h' : 'n/a',\n totalPower: totalPower != null ? Number(totalPower).toFixed(2) + ' kW' : 'n/a',\n efficiency: eff != null ? Number(eff).toFixed(3) : 'n/a',\n totalFlowNum: totalFlow != null ? Number(totalFlow ) : null,\n totalPowerNum: totalPower != null ? Number(totalPower) : null,\n efficiencyNum: eff != null ? Number(eff) : null,\n};\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1160, "y": 1000, "wires": [ [ "lout_evt_mgc" ] ] }, { "id": "lout_evt_mgc", "type": "link out", "z": "tab_process", "name": "evt:mgc", "mode": "link", "links": [ "lin_evt_mgc_dash" ], "x": 1420, "y": 1000, "wires": [] }, { "id": "c_ps", "type": "comment", "z": "tab_process", "name": "\u2500\u2500 Pumping Station \u2500\u2500 (basin model, levelbased control)", "info": "", "x": 640, "y": 1200, "wires": [] }, { "id": "lin_qin_at_ps", "type": "link in", "z": "tab_process", "name": "cmd:q_in", "links": [ "lout_qin_drivers" ], "x": 120, "y": 1240, "wires": [ [ "ps_basin" ] ] }, { "id": "lin_qd_at_ps", "type": "link in", "z": "tab_process", "name": "cmd:Qd", "links": [ "lout_qd_dash" ], "x": 120, "y": 1280, "wires": [ [ "qd_to_ps_wrap" ] ] }, { "id": "qd_to_ps_wrap", "type": "function", "z": "tab_process", "name": "wrap slider \u2192 PS Qd", "func": "msg.topic = 'Qd';\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 380, "y": 1280, "wires": [ [ "ps_basin" ] ] }, { "id": "lin_ps_mode_at_ps", "type": "link in", "z": "tab_process", "name": "cmd:ps-mode", "links": [ "lout_ps_mode_dash" ], "x": 120, "y": 1320, "wires": [ [ "ps_basin" ] ] }, { "id": "ps_basin", "type": "pumpingStation", "z": "tab_process", "name": "Pumping Station", "uuid": "ps-basin-1", "category": "station", "assetType": "pumpingstation", "model": "default", "unit": "m3/s", "supplier": "evolv", "enableLog": false, "logLevel": "warn", "tickIntervalMs": 2000, "positionVsParent": "atEquipment", "positionIcon": "\u22a5", "hasDistance": false, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", "controlMode": "levelbased", "basinVolume": 50.0, "basinHeight": 4.0, "inflowLevel": 2.5, "outflowLevel": 0.3, "overflowLevel": 3.8, "inletPipeDiameter": 0.3, "outletPipeDiameter": 0.3, "minLevel": 0.5, "startLevel": 2.5, "stopLevel": 2.0, "deadZoneKeepAlivePercent": 1, "maxLevel": 3.5, "refHeight": "NAP", "minHeightBasedOn": "outlet", "basinBottomRef": 0, "staticHead": 12, "maxDischargeHead": 24, "pipelineLength": 80, "defaultFluid": "wastewater", "temperatureReferenceDegC": 15, "maxInflowRate": 200, "enableDryRunProtection": true, "enableOverfillProtection": true, "dryRunThresholdPercent": 5, "overfillThresholdPercent": 95, "timeleftToFullOrEmptyThresholdSeconds": 0, "x": 900, "y": 1280, "wires": [ [ "format_ps", "ps_to_physics" ], [ "lout_tlm_ps" ] ] }, { "id": "lout_tlm_ps", "type": "link out", "z": "tab_process", "name": "evt:tlm", "mode": "link", "links": [ "lin_tlm" ], "x": 900, "y": 1320, "wires": [] }, { "id": "ps_to_physics", "type": "function", "z": "tab_process", "name": "ps \u2192 fan basin level to 3 physics feeders", "func": "const out = { from: 'ps', payload: msg.payload };\nreturn [out, out, out];", "outputs": 3, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1160, "y": 1330, "wires": [ [ "physics_pump_a" ], [ "physics_pump_b" ], [ "physics_pump_c" ] ] }, { "id": "format_ps", "type": "function", "z": "tab_process", "name": "format PS port 0", "func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\n// Throttle: PS emits frequently in levelbased mode.\nconst now = Date.now();\nconst last = context.get('_lastEmit') || 0;\nif (now - last < 1000) return null;\ncontext.set('_lastEmit', now);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst MAX_VOL = 50.0;\nconst lvl = find('level.predicted.');\nconst vol = find('volume.predicted.');\nconst qIn = find('flow.predicted.in.');\nconst qOut = find('flow.predicted.out.');\nconst netFlowRate = find('netFlowRate.predicted.');\nconst fillPct = vol != null\n ? Math.min(100, Math.max(0, Math.round(Number(vol) / MAX_VOL * 100)))\n : null;\nconst netM3h = netFlowRate != null ? Number(netFlowRate) * 3600 : null;\nconst seconds = (c.timeleft != null && Number.isFinite(Number(c.timeleft)))\n ? Number(c.timeleft) : null;\nconst timeStr = seconds != null\n ? (seconds > 60 ? Math.round(seconds/60) + ' min'\n : Math.round(seconds) + ' s')\n : 'n/a';\nmsg.payload = {\n direction: c.direction || 'steady',\n level: lvl != null ? Number(lvl).toFixed(2) + ' m' : 'n/a',\n volume: vol != null ? Number(vol).toFixed(1) + ' m\u00b3' : 'n/a',\n fillPct: fillPct != null ? fillPct + '%' : 'n/a',\n netFlow: netM3h != null ? netM3h.toFixed(0) + ' m\u00b3/h' : 'n/a',\n timeLeft: timeStr,\n qIn: qIn != null ? (Number(qIn ) * 3600).toFixed(0) + ' m\u00b3/h' : 'n/a',\n qOut: qOut != null ? (Number(qOut) * 3600).toFixed(0) + ' m\u00b3/h' : 'n/a',\n levelNum: lvl != null ? Number(lvl) : null,\n volumeNum: vol != null ? Number(vol) : null,\n fillPctNum: fillPct,\n netFlowNum: netM3h,\n percControl: c.percControl != null ? Number(c.percControl) : null,\n qInNum: qIn != null ? Number(qIn ) * 3600 : null,\n qOutNum: qOut != null ? Number(qOut) * 3600 : null,\n safetyState: c.safetyState || 'normal',\n};\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1160, "y": 1280, "wires": [ [ "lout_evt_ps" ] ] }, { "id": "lout_evt_ps", "type": "link out", "z": "tab_process", "name": "evt:ps", "mode": "link", "links": [ "lin_evt_ps_dash" ], "x": 1420, "y": 1280, "wires": [] }, { "id": "c_mode_bcast", "type": "comment", "z": "tab_process", "name": "\u2500\u2500 Mode broadcast \u2500\u2500", "info": "", "x": 640, "y": 1420, "wires": [] }, { "id": "lin_mode", "type": "link in", "z": "tab_process", "name": "cmd:mode", "links": [ "lout_mode_setup" ], "x": 120, "y": 1480, "wires": [ [ "fanout_mode" ] ] }, { "id": "fanout_mode", "type": "function", "z": "tab_process", "name": "fan setMode \u2192 3 pumps", "func": "msg.topic = 'setMode';\nreturn [msg, msg, msg];", "outputs": 3, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 600, "y": 1480, "wires": [ [ "pump_a" ], [ "pump_b" ], [ "pump_c" ] ] }, { "id": "c_station_cmds", "type": "comment", "z": "tab_process", "name": "\u2500\u2500 Station-wide commands \u2500\u2500", "info": "", "x": 640, "y": 1620, "wires": [] }, { "id": "lin_station_start", "type": "link in", "z": "tab_process", "name": "cmd:station-startup", "links": [ "lout_cmd_station_startup_dash" ], "x": 120, "y": 1680, "wires": [ [ "fan_station_start" ] ] }, { "id": "fan_station_start", "type": "function", "z": "tab_process", "name": "fan startup \u2192 3 pumps", "func": "return [msg, msg, msg];", "outputs": 3, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 600, "y": 1680, "wires": [ [ "pump_a" ], [ "pump_b" ], [ "pump_c" ] ] }, { "id": "lin_station_stop", "type": "link in", "z": "tab_process", "name": "cmd:station-shutdown", "links": [ "lout_cmd_station_shutdown_dash" ], "x": 120, "y": 1740, "wires": [ [ "fan_station_stop" ] ] }, { "id": "fan_station_stop", "type": "function", "z": "tab_process", "name": "fan shutdown \u2192 3 pumps", "func": "return [msg, msg, msg];", "outputs": 3, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 600, "y": 1740, "wires": [ [ "pump_a" ], [ "pump_b" ], [ "pump_c" ] ] }, { "id": "lin_station_estop", "type": "link in", "z": "tab_process", "name": "cmd:station-estop", "links": [ "lout_cmd_station_estop_dash" ], "x": 120, "y": 1800, "wires": [ [ "fan_station_estop" ] ] }, { "id": "fan_station_estop", "type": "function", "z": "tab_process", "name": "fan emergency stop \u2192 3 pumps", "func": "return [msg, msg, msg];", "outputs": 3, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 600, "y": 1800, "wires": [ [ "pump_a" ], [ "pump_b" ], [ "pump_c" ] ] }, { "id": "c_setup_at_mgc", "type": "comment", "z": "tab_process", "name": "\u2500\u2500 Setup feeders \u2500\u2500", "info": "", "x": 640, "y": 1900, "wires": [] }, { "id": "lin_setup_at_mgc", "type": "link in", "z": "tab_process", "name": "setup:to-mgc", "links": [ "lout_setup_to_mgc" ], "x": 120, "y": 1960, "wires": [ [ "mgc_pumps" ] ] }, { "id": "lin_setup_calibrate_ps", "type": "link in", "z": "tab_process", "name": "setup:calibrate-ps", "links": [ "lout_setup_calibrate" ], "x": 120, "y": 2020, "wires": [ [ "ps_basin" ] ] }, { "id": "tab_ui", "type": "tab", "label": "\ud83d\udcca Dashboard UI", "disabled": false, "info": "All FlowFuse ui-* widgets. Two pages:\n /dashboard/realtime \u2014 gauges + per-pump status (no time history)\n /dashboard/trends \u2014 line charts, 1 hour rolling window\n\nAll inputs leave via link-out; all process state arrives via link-in." }, { "id": "ui_base", "type": "ui-base", "name": "EVOLV Pumping", "path": "/dashboard", "appIcon": "", "includeClientData": true, "acceptsClientConfig": [ "ui-notification", "ui-control" ], "showPathInSidebar": true, "headerContent": "page", "navigationStyle": "default", "titleBarStyle": "default" }, { "id": "ui_theme", "type": "ui-theme", "name": "EVOLV Theme", "colors": { "surface": "#ffffff", "primary": "#0c99d9", "bgPage": "#f4f6fa", "groupBg": "#ffffff", "groupOutline": "#cccccc" }, "sizes": { "density": "default", "pagePadding": "12px", "groupGap": "12px", "groupBorderRadius": "6px", "widgetGap": "8px" } }, { "id": "ui_page_realtime", "type": "ui-page", "name": "Realtime", "ui": "ui_base", "path": "/realtime", "icon": "speed", "layout": "grid", "theme": "ui_theme", "breakpoints": [ { "name": "Default", "px": "0", "cols": "12" } ], "order": 1, "className": "" }, { "id": "ui_page_trends", "type": "ui-page", "name": "Trends \u2014 1 hour", "ui": "ui_base", "path": "/trends", "icon": "show_chart", "layout": "grid", "theme": "ui_theme", "breakpoints": [ { "name": "Default", "px": "0", "cols": "12" } ], "order": 2, "className": "" }, { "id": "ui_grp_inflow", "type": "ui-group", "name": "1. Inflow (operator input)", "page": "ui_page_realtime", "width": "12", "height": "1", "order": 1, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_station", "type": "ui-group", "name": "2. Station Mode + Commands", "page": "ui_page_realtime", "width": "12", "height": "1", "order": 2, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_basin", "type": "ui-group", "name": "3. Basin Realtime", "page": "ui_page_realtime", "width": "6", "height": "1", "order": 3, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_mgc", "type": "ui-group", "name": "4. Pump Group (MGC)", "page": "ui_page_realtime", "width": "6", "height": "1", "order": 4, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_pump_a", "type": "ui-group", "name": "5a. Pump A", "page": "ui_page_realtime", "width": "4", "height": "1", "order": 5, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_pump_b", "type": "ui-group", "name": "5b. Pump B", "page": "ui_page_realtime", "width": "4", "height": "1", "order": 6, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_pump_c", "type": "ui-group", "name": "5c. Pump C", "page": "ui_page_realtime", "width": "4", "height": "1", "order": 7, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_tr_basin", "type": "ui-group", "name": "Basin level + fill (1h)", "page": "ui_page_trends", "width": "12", "height": "1", "order": 1, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_tr_demand", "type": "ui-group", "name": "Process demand \u2014 PS percControl (1h)", "page": "ui_page_trends", "width": "12", "height": "1", "order": 2, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_tr_dq", "type": "ui-group", "name": "\u0394Q = inflow \u2212 outflow (m\u00b3/h, +fill / \u2212drain)", "page": "ui_page_trends", "width": "12", "height": "1", "order": 3, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_tr_states", "type": "ui-group", "name": "Pump state timeline (gantt)", "page": "ui_page_trends", "width": "12", "height": "1", "order": 4, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_tr_flow", "type": "ui-group", "name": "Inflow / Outflow / Per-pump flow (1h)", "page": "ui_page_trends", "width": "12", "height": "1", "order": 5, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_tr_power", "type": "ui-group", "name": "Per-pump power (1h)", "page": "ui_page_trends", "width": "12", "height": "1", "order": 6, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "ui_grp_tr_press", "type": "ui-group", "name": "Per-pump pressures (1h)", "page": "ui_page_trends", "width": "12", "height": "1", "order": 7, "showTitle": true, "className": "", "groupType": "default", "disabled": false, "visible": true }, { "id": "c_ui_title", "type": "comment", "z": "tab_ui", "name": "\ud83d\udcca DASHBOARD UI \u2014 only ui-* widgets here", "info": "", "x": 640, "y": 20, "wires": [] }, { "id": "c_ui_inflow", "type": "comment", "z": "tab_ui", "name": "\u2500\u2500 Operator inflow input \u2500\u2500", "info": "", "x": 640, "y": 80, "wires": [] }, { "id": "ui_inflow_slider", "type": "ui-slider", "z": "tab_ui", "group": "ui_grp_inflow", "name": "Inflow baseline", "label": "Inflow baseline (m\u00b3/h) \u2014 scenarios modulate around this value", "tooltip": "", "order": 1, "width": "0", "height": "0", "passthru": true, "outs": "end", "topic": "inflowBaseline", "topicType": "str", "min": "0", "max": "250", "step": "5.0", "showLabel": true, "showValue": true, "labelPosition": "top", "valuePosition": "left", "thumbLabel": false, "iconStart": "", "iconEnd": "", "x": 120, "y": 120, "wires": [ [ "lout_inflow_baseline" ] ] }, { "id": "lout_inflow_baseline", "type": "link out", "z": "tab_ui", "name": "cmd:inflow-baseline", "mode": "link", "links": [ "lin_inflow_baseline" ], "x": 380, "y": 120, "wires": [] }, { "id": "btn_scn_constant", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_inflow", "name": "Scenario Constant", "label": "Constant", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#0c99d9", "className": "", "icon": "horizontal_rule", "iconPosition": "left", "payload": "constant", "payloadType": "str", "topic": "scenario", "topicType": "str", "buttonType": "default", "x": 120, "y": 180, "wires": [ [ "wrap_scn_constant" ] ] }, { "id": "wrap_scn_constant", "type": "function", "z": "tab_ui", "name": "build scenario constant", "func": "msg.payload = 'constant';\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 180, "wires": [ [ "lout_inflow_scenario" ] ] }, { "id": "btn_scn_sine", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_inflow", "name": "Scenario Sine wave", "label": "Sine wave", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#16a34a", "className": "", "icon": "show_chart", "iconPosition": "left", "payload": "sine", "payloadType": "str", "topic": "scenario", "topicType": "str", "buttonType": "default", "x": 120, "y": 230, "wires": [ [ "wrap_scn_sine" ] ] }, { "id": "wrap_scn_sine", "type": "function", "z": "tab_ui", "name": "build scenario sine", "func": "msg.payload = 'sine';\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 230, "wires": [ [ "lout_inflow_scenario" ] ] }, { "id": "btn_scn_diurnal", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_inflow", "name": "Scenario Diurnal", "label": "Diurnal", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#f59e0b", "className": "", "icon": "schedule", "iconPosition": "left", "payload": "diurnal", "payloadType": "str", "topic": "scenario", "topicType": "str", "buttonType": "default", "x": 120, "y": 280, "wires": [ [ "wrap_scn_diurnal" ] ] }, { "id": "wrap_scn_diurnal", "type": "function", "z": "tab_ui", "name": "build scenario diurnal", "func": "msg.payload = 'diurnal';\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 280, "wires": [ [ "lout_inflow_scenario" ] ] }, { "id": "btn_scn_storm", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_inflow", "name": "Scenario Storm", "label": "Storm", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#dc2626", "className": "", "icon": "thunderstorm", "iconPosition": "left", "payload": "storm", "payloadType": "str", "topic": "scenario", "topicType": "str", "buttonType": "default", "x": 120, "y": 330, "wires": [ [ "wrap_scn_storm" ] ] }, { "id": "wrap_scn_storm", "type": "function", "z": "tab_ui", "name": "build scenario storm", "func": "msg.payload = 'storm';\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 330, "wires": [ [ "lout_inflow_scenario" ] ] }, { "id": "lout_inflow_scenario", "type": "link out", "z": "tab_ui", "name": "cmd:inflow-scenario", "mode": "link", "links": [ "lin_inflow_scenario" ], "x": 640, "y": 180, "wires": [] }, { "id": "lin_evt_inflow", "type": "link in", "z": "tab_ui", "name": "evt:inflow", "links": [ "lout_evt_inflow" ], "x": 900, "y": 120, "wires": [ [ "dispatch_inflow" ] ] }, { "id": "dispatch_inflow", "type": "function", "z": "tab_ui", "name": "dispatch inflow", "func": "const p = msg.payload || {};\nconst ts = Date.now();\nreturn [\n { payload: (p.scenario || 'constant').toUpperCase() },\n { payload: p.q_h != null ? Number(p.q_h).toFixed(1) + ' m\u00b3/h' : 'n/a' },\n p.q_h != null ? { topic: 'Inflow', payload: Number(p.q_h), timestamp: ts } : null,\n];", "outputs": 3, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1160, "y": 120, "wires": [ [ "ui_inflow_scn_text" ], [ "ui_inflow_value_text" ], [ "chart_trend_flow" ] ] }, { "id": "ui_inflow_scn_text", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_inflow", "order": 1, "width": "0", "height": "0", "name": "Active scenario", "label": "Active scenario", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 1420, "y": 120, "wires": [] }, { "id": "ui_inflow_value_text", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_inflow", "order": 1, "width": "0", "height": "0", "name": "Live inflow", "label": "Live inflow", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 1420, "y": 160, "wires": [] }, { "id": "c_ui_station", "type": "comment", "z": "tab_ui", "name": "\u2500\u2500 Mode + Station-wide buttons \u2500\u2500", "info": "", "x": 640, "y": 380, "wires": [] }, { "id": "ui_mode_toggle", "type": "ui-switch", "z": "tab_ui", "group": "ui_grp_station", "name": "Station mode", "label": "Station mode (Auto = level-based \u00b7 Manual = slider Qd)", "tooltip": "", "order": 1, "width": "0", "height": "0", "passthru": true, "decouple": "false", "topic": "changemode", "topicType": "str", "style": "", "className": "", "evaluate": "true", "onvalue": "levelbased", "onvalueType": "str", "onicon": "auto_mode", "oncolor": "#0c99d9", "offvalue": "manual", "offvalueType": "str", "officon": "back_hand", "offcolor": "#888888", "x": 120, "y": 420, "wires": [ [ "lout_ps_mode_dash" ] ] }, { "id": "lout_ps_mode_dash", "type": "link out", "z": "tab_ui", "name": "cmd:ps-mode", "mode": "link", "links": [ "lin_ps_mode_at_ps" ], "x": 380, "y": 420, "wires": [] }, { "id": "ui_qd_slider", "type": "ui-slider", "z": "tab_ui", "group": "ui_grp_station", "name": "Manual Qd", "label": "Manual Qd (m\u00b3/h, manual mode only)", "tooltip": "", "order": 1, "width": "0", "height": "0", "passthru": true, "outs": "end", "topic": "manualDemand", "topicType": "str", "min": "0", "max": "600", "step": "5.0", "showLabel": true, "showValue": true, "labelPosition": "top", "valuePosition": "left", "thumbLabel": false, "iconStart": "", "iconEnd": "", "x": 120, "y": 470, "wires": [ [ "lout_qd_dash" ] ] }, { "id": "lout_qd_dash", "type": "link out", "z": "tab_ui", "name": "cmd:Qd", "mode": "link", "links": [ "lin_qd_at_ps" ], "x": 380, "y": 470, "wires": [] }, { "id": "btn_station_0", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_station", "name": "Start all pumps", "label": "Start all pumps", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#16a34a", "className": "", "icon": "play_arrow", "iconPosition": "left", "payload": "fired", "payloadType": "str", "topic": "station_0", "topicType": "str", "buttonType": "default", "x": 120, "y": 530, "wires": [ [ "wrap_station_0" ] ] }, { "id": "wrap_station_0", "type": "function", "z": "tab_ui", "name": "build cmd (Start all pumps)", "func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'startup' };\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 530, "wires": [ [ "lout_cmd_station_startup_dash" ] ] }, { "id": "lout_cmd_station_startup_dash", "type": "link out", "z": "tab_ui", "name": "cmd:station-startup", "mode": "link", "links": [ "lin_station_start" ], "x": 640, "y": 530, "wires": [] }, { "id": "btn_station_1", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_station", "name": "Stop all pumps", "label": "Stop all pumps", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#ea580c", "className": "", "icon": "stop", "iconPosition": "left", "payload": "fired", "payloadType": "str", "topic": "station_1", "topicType": "str", "buttonType": "default", "x": 120, "y": 580, "wires": [ [ "wrap_station_1" ] ] }, { "id": "wrap_station_1", "type": "function", "z": "tab_ui", "name": "build cmd (Stop all pumps)", "func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'shutdown' };\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 580, "wires": [ [ "lout_cmd_station_shutdown_dash" ] ] }, { "id": "lout_cmd_station_shutdown_dash", "type": "link out", "z": "tab_ui", "name": "cmd:station-shutdown", "mode": "link", "links": [ "lin_station_stop" ], "x": 640, "y": 580, "wires": [] }, { "id": "btn_station_2", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_station", "name": "EMERGENCY STOP", "label": "EMERGENCY STOP", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#dc2626", "className": "", "icon": "stop_circle", "iconPosition": "left", "payload": "fired", "payloadType": "str", "topic": "station_2", "topicType": "str", "buttonType": "default", "x": 120, "y": 630, "wires": [ [ "wrap_station_2" ] ] }, { "id": "wrap_station_2", "type": "function", "z": "tab_ui", "name": "build cmd (EMERGENCY STOP)", "func": "msg.topic = 'emergencystop';\nmsg.payload = { source:'GUI', action:'emergencystop' };\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 630, "wires": [ [ "lout_cmd_station_estop_dash" ] ] }, { "id": "lout_cmd_station_estop_dash", "type": "link out", "z": "tab_ui", "name": "cmd:station-estop", "mode": "link", "links": [ "lin_station_estop" ], "x": 640, "y": 630, "wires": [] }, { "id": "c_ui_basin", "type": "comment", "z": "tab_ui", "name": "\u2500\u2500 Basin realtime (gauges + text) \u2500\u2500", "info": "", "x": 640, "y": 700, "wires": [] }, { "id": "lin_evt_ps_dash", "type": "link in", "z": "tab_ui", "name": "evt:ps", "links": [ "lout_evt_ps" ], "x": 120, "y": 740, "wires": [ [ "dispatch_ps" ] ] }, { "id": "dispatch_ps", "type": "function", "z": "tab_ui", "name": "dispatch PS", "func": "const p = msg.payload || {};\nconst ts = Date.now();\n// \u0394Q = inflow \u2212 outflow in m\u00b3/h (positive = filling).\nconst dQ = (p.qInNum != null && p.qOutNum != null)\n ? p.qInNum - p.qOutNum : null;\n// Demand text formatting.\nconst demandStr = p.percControl != null\n ? Number(p.percControl).toFixed(0) + '%' : 'n/a';\nreturn [\n { payload: String(p.direction || 'steady') },\n { payload: String(p.level || 'n/a') },\n { payload: String(p.volume || 'n/a') },\n { payload: String(p.fillPct || 'n/a') },\n { payload: String(p.netFlow || 'n/a') },\n { payload: String(p.timeLeft || 'n/a') },\n { payload: String(p.qIn || 'n/a') },\n { payload: String(p.qOut || 'n/a') },\n { payload: String(p.safetyState || 'normal') },\n { payload: demandStr },\n p.levelNum != null ? { payload: p.levelNum } : null,\n p.fillPctNum != null ? { payload: p.fillPctNum } : null,\n p.percControl != null ? { payload: p.percControl } : null,\n p.levelNum != null ? { topic: 'Basin level', payload: p.levelNum, timestamp: ts } : null,\n p.fillPctNum != null ? { topic: 'Fill %', payload: p.fillPctNum, timestamp: ts } : null,\n p.qOutNum != null ? { topic: 'Outflow', payload: p.qOutNum, timestamp: ts } : null,\n p.percControl != null ? { topic: 'PS demand', payload: p.percControl, timestamp: ts } : null,\n dQ != null ? { topic: '\u0394Q', payload: dQ, timestamp: ts } : null,\n];", "outputs": 18, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 380, "y": 740, "wires": [ [ "ui_ps_direction" ], [ "ui_ps_level" ], [ "ui_ps_volume" ], [ "ui_ps_fill" ], [ "ui_ps_netflow" ], [ "ui_ps_timeleft" ], [ "ui_ps_qin" ], [ "ui_ps_qout" ], [ "ui_ps_safety" ], [ "ui_ps_demand" ], [ "gauge_basin_level" ], [ "gauge_basin_fill" ], [ "gauge_ps_demand" ], [ "chart_trend_basin" ], [ "chart_trend_basin" ], [ "chart_trend_flow" ], [ "chart_trend_demand" ], [ "chart_trend_dq" ] ] }, { "id": "ui_ps_direction", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_basin", "order": 1, "width": "0", "height": "0", "name": "Direction", "label": "Direction", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 740, "wires": [] }, { "id": "ui_ps_level", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_basin", "order": 1, "width": "0", "height": "0", "name": "Basin level", "label": "Basin level", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 770, "wires": [] }, { "id": "ui_ps_volume", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_basin", "order": 1, "width": "0", "height": "0", "name": "Basin volume", "label": "Basin volume", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 800, "wires": [] }, { "id": "ui_ps_fill", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_basin", "order": 1, "width": "0", "height": "0", "name": "Fill %", "label": "Fill %", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 830, "wires": [] }, { "id": "ui_ps_netflow", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_basin", "order": 1, "width": "0", "height": "0", "name": "Net flow", "label": "Net flow", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 860, "wires": [] }, { "id": "ui_ps_timeleft", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_basin", "order": 1, "width": "0", "height": "0", "name": "Time left", "label": "Time to full/empty", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 890, "wires": [] }, { "id": "ui_ps_qin", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_basin", "order": 1, "width": "0", "height": "0", "name": "Inflow", "label": "Inflow", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 920, "wires": [] }, { "id": "ui_ps_qout", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_basin", "order": 1, "width": "0", "height": "0", "name": "Outflow", "label": "Outflow", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 950, "wires": [] }, { "id": "ui_ps_safety", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_basin", "order": 1, "width": "0", "height": "0", "name": "Safety", "label": "Safety state", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 980, "wires": [] }, { "id": "ui_ps_demand", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_basin", "order": 1, "width": "0", "height": "0", "name": "PS demand", "label": "Process demand", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1010, "wires": [] }, { "id": "gauge_basin_level", "type": "ui-gauge", "z": "tab_ui", "group": "ui_grp_basin", "name": "Basin level gauge", "gtype": "gauge-tank", "gstyle": "Rounded", "title": "Level", "units": "m", "prefix": "", "suffix": " m", "min": 0, "max": 4.0, "segments": [ { "color": "#f44336", "from": 0 }, { "color": "#ff9800", "from": 1.0 }, { "color": "#2196f3", "from": 2.0 }, { "color": "#ff9800", "from": 3.5 }, { "color": "#f44336", "from": 3.8 } ], "width": 3, "height": 4, "order": 10, "icon": "", "sizeGauge": 20, "sizeGap": 2, "sizeSegments": 10, "x": 900, "y": 740, "wires": [] }, { "id": "gauge_basin_fill", "type": "ui-gauge", "z": "tab_ui", "group": "ui_grp_basin", "name": "Basin fill gauge", "gtype": "gauge-34", "gstyle": "Rounded", "title": "Fill", "units": "%", "prefix": "", "suffix": "%", "min": 0, "max": 100, "segments": [ { "color": "#f44336", "from": 0 }, { "color": "#ff9800", "from": 10 }, { "color": "#4caf50", "from": 30 }, { "color": "#ff9800", "from": 80 }, { "color": "#f44336", "from": 95 } ], "width": 3, "height": 4, "order": 11, "icon": "water_drop", "sizeGauge": 20, "sizeGap": 2, "sizeSegments": 10, "x": 900, "y": 800, "wires": [] }, { "id": "gauge_ps_demand", "type": "ui-gauge", "z": "tab_ui", "group": "ui_grp_basin", "name": "PS demand gauge", "gtype": "gauge-34", "gstyle": "Rounded", "title": "PS demand", "units": "%", "prefix": "", "suffix": "%", "min": 0, "max": 100, "segments": [ { "color": "#cccccc", "from": 0 }, { "color": "#0c99d9", "from": 5 }, { "color": "#16a34a", "from": 30 }, { "color": "#f59e0b", "from": 70 }, { "color": "#dc2626", "from": 95 } ], "width": 3, "height": 4, "order": 12, "icon": "speed", "sizeGauge": 20, "sizeGap": 2, "sizeSegments": 10, "x": 900, "y": 860, "wires": [] }, { "id": "c_ui_mgc", "type": "comment", "z": "tab_ui", "name": "\u2500\u2500 MGC realtime \u2500\u2500", "info": "", "x": 640, "y": 1080, "wires": [] }, { "id": "lin_evt_mgc_dash", "type": "link in", "z": "tab_ui", "name": "evt:mgc", "links": [ "lout_evt_mgc" ], "x": 120, "y": 1120, "wires": [ [ "dispatch_mgc" ] ] }, { "id": "dispatch_mgc", "type": "function", "z": "tab_ui", "name": "dispatch MGC", "func": "const p = msg.payload || {};\nreturn [\n { payload: String(p.totalFlow || 'n/a') },\n { payload: String(p.totalPower || 'n/a') },\n { payload: String(p.efficiency || 'n/a') },\n p.totalFlowNum != null ? { payload: p.totalFlowNum } : null,\n p.totalPowerNum != null ? { payload: p.totalPowerNum } : null,\n];", "outputs": 5, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 380, "y": 1120, "wires": [ [ "ui_mgc_total_flow" ], [ "ui_mgc_total_power" ], [ "ui_mgc_eff" ], [ "gauge_mgc_flow" ], [ "gauge_mgc_power" ] ] }, { "id": "ui_mgc_total_flow", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_mgc", "order": 1, "width": "0", "height": "0", "name": "MGC total flow", "label": "Total flow", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1120, "wires": [] }, { "id": "ui_mgc_total_power", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_mgc", "order": 1, "width": "0", "height": "0", "name": "MGC total power", "label": "Total power", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1150, "wires": [] }, { "id": "ui_mgc_eff", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_mgc", "order": 1, "width": "0", "height": "0", "name": "MGC efficiency", "label": "Group efficiency", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1180, "wires": [] }, { "id": "gauge_mgc_flow", "type": "ui-gauge", "z": "tab_ui", "group": "ui_grp_mgc", "name": "MGC total flow gauge", "gtype": "gauge-34", "gstyle": "Rounded", "title": "Total flow", "units": "m\u00b3/h", "prefix": "", "suffix": " m\u00b3/h", "min": 0, "max": 600, "segments": [ { "color": "#cccccc", "from": 0 }, { "color": "#0c99d9", "from": 50 }, { "color": "#16a34a", "from": 200 }, { "color": "#f59e0b", "from": 500 } ], "width": 3, "height": 4, "order": 10, "icon": "", "sizeGauge": 20, "sizeGap": 2, "sizeSegments": 10, "x": 900, "y": 1120, "wires": [] }, { "id": "gauge_mgc_power", "type": "ui-gauge", "z": "tab_ui", "group": "ui_grp_mgc", "name": "MGC total power gauge", "gtype": "gauge-34", "gstyle": "Rounded", "title": "Total power", "units": "kW", "prefix": "", "suffix": " kW", "min": 0, "max": 30, "segments": [ { "color": "#cccccc", "from": 0 }, { "color": "#0c99d9", "from": 1 }, { "color": "#16a34a", "from": 5 }, { "color": "#f59e0b", "from": 20 } ], "width": 3, "height": 4, "order": 11, "icon": "", "sizeGauge": 20, "sizeGap": 2, "sizeSegments": 10, "x": 900, "y": 1180, "wires": [] }, { "id": "c_ui_pump_a", "type": "comment", "z": "tab_ui", "name": "\u2500\u2500 Pump A \u2500\u2500", "info": "", "x": 640, "y": 1340, "wires": [] }, { "id": "lin_evt_pump_a_dash", "type": "link in", "z": "tab_ui", "name": "evt:pump-A", "links": [ "lout_evt_pump_a" ], "x": 120, "y": 1380, "wires": [ [ "dispatch_pump_a" ] ] }, { "id": "dispatch_pump_a", "type": "function", "z": "tab_ui", "name": "dispatch Pump A", "func": "const p = msg.payload || {};\nconst ts = Date.now();\nconst OFF = 0;\nfunction stateNum(s) {\n switch (s) {\n case 'operational': return OFF + 2;\n case 'starting':\n case 'warmingup': return OFF + 1;\n case 'stopping': return OFF + 1.5;\n case 'coolingdown': return OFF + 0.5;\n default: return OFF;\n }\n}\nconst sNum = p.state ? stateNum(p.state) : null;\nreturn [\n {payload: String(p.state || 'idle')},\n {payload: String(p.mode || 'auto')},\n {payload: String(p.ctrl || 'n/a')},\n {payload: String(p.flow || 'n/a')},\n {payload: String(p.power || 'n/a')},\n {payload: String(p.pUp || 'n/a')},\n {payload: String(p.pDn || 'n/a')},\n p.flowNum != null ? {topic: 'Pump A', payload: p.flowNum, timestamp: ts} : null,\n p.powerNum != null ? {topic: 'Pump A', payload: p.powerNum, timestamp: ts} : null,\n p.pUpNum != null ? {topic: 'Pump A up', payload: p.pUpNum, timestamp: ts} : null,\n p.pDnNum != null ? {topic: 'Pump A dn', payload: p.pDnNum, timestamp: ts} : null,\n sNum != null ? {topic: 'Pump A state', payload: sNum, timestamp: ts} : null,\n];", "outputs": 12, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 380, "y": 1380, "wires": [ [ "ui_pump_a_state" ], [ "ui_pump_a_mode" ], [ "ui_pump_a_ctrl" ], [ "ui_pump_a_flow" ], [ "ui_pump_a_power" ], [ "ui_pump_a_pUp" ], [ "ui_pump_a_pDn" ], [ "chart_trend_flow" ], [ "chart_trend_power" ], [ "chart_trend_pressure" ], [ "chart_trend_pressure" ], [ "chart_trend_states" ] ] }, { "id": "ui_pump_a_state", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_a", "order": 1, "width": "0", "height": "0", "name": "Pump A State", "label": "State", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1380, "wires": [] }, { "id": "ui_pump_a_mode", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_a", "order": 1, "width": "0", "height": "0", "name": "Pump A Mode", "label": "Mode", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1410, "wires": [] }, { "id": "ui_pump_a_ctrl", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_a", "order": 1, "width": "0", "height": "0", "name": "Pump A Controller %", "label": "Controller %", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1440, "wires": [] }, { "id": "ui_pump_a_flow", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_a", "order": 1, "width": "0", "height": "0", "name": "Pump A Flow", "label": "Flow", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1470, "wires": [] }, { "id": "ui_pump_a_power", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_a", "order": 1, "width": "0", "height": "0", "name": "Pump A Power", "label": "Power", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1500, "wires": [] }, { "id": "ui_pump_a_pUp", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_a", "order": 1, "width": "0", "height": "0", "name": "Pump A p Upstream", "label": "p Upstream", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1530, "wires": [] }, { "id": "ui_pump_a_pDn", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_a", "order": 1, "width": "0", "height": "0", "name": "Pump A p Downstream", "label": "p Downstream", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1560, "wires": [] }, { "id": "ui_pump_a_setpoint", "type": "ui-slider", "z": "tab_ui", "group": "ui_grp_pump_a", "name": "Pump A setpoint", "label": "Setpoint % (manual mode)", "tooltip": "", "order": 1, "width": "0", "height": "0", "passthru": true, "outs": "end", "topic": "setpoint_pump_a", "topicType": "str", "min": "0", "max": "100", "step": "5.0", "showLabel": true, "showValue": true, "labelPosition": "top", "valuePosition": "left", "thumbLabel": false, "iconStart": "", "iconEnd": "", "x": 120, "y": 1620, "wires": [ [ "lout_setpoint_pump_a_dash" ] ] }, { "id": "lout_setpoint_pump_a_dash", "type": "link out", "z": "tab_ui", "name": "cmd:setpoint-A", "mode": "link", "links": [ "lin_setpoint_pump_a" ], "x": 380, "y": 1620, "wires": [] }, { "id": "btn_pump_a_start", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_pump_a", "name": "Pump A startup", "label": "Startup", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#16a34a", "className": "", "icon": "play_arrow", "iconPosition": "left", "payload": "fired", "payloadType": "str", "topic": "start_pump_a", "topicType": "str", "buttonType": "default", "x": 120, "y": 1670, "wires": [ [ "wrap_pump_a_start" ] ] }, { "id": "wrap_pump_a_start", "type": "function", "z": "tab_ui", "name": "build start (Pump A)", "func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'startup' };\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 1670, "wires": [ [ "lout_seq_pump_a_dash" ] ] }, { "id": "btn_pump_a_stop", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_pump_a", "name": "Pump A shutdown", "label": "Shutdown", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#ea580c", "className": "", "icon": "stop", "iconPosition": "left", "payload": "fired", "payloadType": "str", "topic": "stop_pump_a", "topicType": "str", "buttonType": "default", "x": 120, "y": 1720, "wires": [ [ "wrap_pump_a_stop" ] ] }, { "id": "wrap_pump_a_stop", "type": "function", "z": "tab_ui", "name": "build stop (Pump A)", "func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'shutdown' };\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 1720, "wires": [ [ "lout_seq_pump_a_dash" ] ] }, { "id": "lout_seq_pump_a_dash", "type": "link out", "z": "tab_ui", "name": "cmd:pump-A-seq", "mode": "link", "links": [ "lin_seq_pump_a" ], "x": 640, "y": 1695, "wires": [] }, { "id": "c_ui_pump_b", "type": "comment", "z": "tab_ui", "name": "\u2500\u2500 Pump B \u2500\u2500", "info": "", "x": 640, "y": 1820, "wires": [] }, { "id": "lin_evt_pump_b_dash", "type": "link in", "z": "tab_ui", "name": "evt:pump-B", "links": [ "lout_evt_pump_b" ], "x": 120, "y": 1860, "wires": [ [ "dispatch_pump_b" ] ] }, { "id": "dispatch_pump_b", "type": "function", "z": "tab_ui", "name": "dispatch Pump B", "func": "const p = msg.payload || {};\nconst ts = Date.now();\nconst OFF = 3;\nfunction stateNum(s) {\n switch (s) {\n case 'operational': return OFF + 2;\n case 'starting':\n case 'warmingup': return OFF + 1;\n case 'stopping': return OFF + 1.5;\n case 'coolingdown': return OFF + 0.5;\n default: return OFF;\n }\n}\nconst sNum = p.state ? stateNum(p.state) : null;\nreturn [\n {payload: String(p.state || 'idle')},\n {payload: String(p.mode || 'auto')},\n {payload: String(p.ctrl || 'n/a')},\n {payload: String(p.flow || 'n/a')},\n {payload: String(p.power || 'n/a')},\n {payload: String(p.pUp || 'n/a')},\n {payload: String(p.pDn || 'n/a')},\n p.flowNum != null ? {topic: 'Pump B', payload: p.flowNum, timestamp: ts} : null,\n p.powerNum != null ? {topic: 'Pump B', payload: p.powerNum, timestamp: ts} : null,\n p.pUpNum != null ? {topic: 'Pump B up', payload: p.pUpNum, timestamp: ts} : null,\n p.pDnNum != null ? {topic: 'Pump B dn', payload: p.pDnNum, timestamp: ts} : null,\n sNum != null ? {topic: 'Pump B state', payload: sNum, timestamp: ts} : null,\n];", "outputs": 12, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 380, "y": 1860, "wires": [ [ "ui_pump_b_state" ], [ "ui_pump_b_mode" ], [ "ui_pump_b_ctrl" ], [ "ui_pump_b_flow" ], [ "ui_pump_b_power" ], [ "ui_pump_b_pUp" ], [ "ui_pump_b_pDn" ], [ "chart_trend_flow" ], [ "chart_trend_power" ], [ "chart_trend_pressure" ], [ "chart_trend_pressure" ], [ "chart_trend_states" ] ] }, { "id": "ui_pump_b_state", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_b", "order": 1, "width": "0", "height": "0", "name": "Pump B State", "label": "State", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1860, "wires": [] }, { "id": "ui_pump_b_mode", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_b", "order": 1, "width": "0", "height": "0", "name": "Pump B Mode", "label": "Mode", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1890, "wires": [] }, { "id": "ui_pump_b_ctrl", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_b", "order": 1, "width": "0", "height": "0", "name": "Pump B Controller %", "label": "Controller %", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1920, "wires": [] }, { "id": "ui_pump_b_flow", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_b", "order": 1, "width": "0", "height": "0", "name": "Pump B Flow", "label": "Flow", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1950, "wires": [] }, { "id": "ui_pump_b_power", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_b", "order": 1, "width": "0", "height": "0", "name": "Pump B Power", "label": "Power", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 1980, "wires": [] }, { "id": "ui_pump_b_pUp", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_b", "order": 1, "width": "0", "height": "0", "name": "Pump B p Upstream", "label": "p Upstream", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 2010, "wires": [] }, { "id": "ui_pump_b_pDn", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_b", "order": 1, "width": "0", "height": "0", "name": "Pump B p Downstream", "label": "p Downstream", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 2040, "wires": [] }, { "id": "ui_pump_b_setpoint", "type": "ui-slider", "z": "tab_ui", "group": "ui_grp_pump_b", "name": "Pump B setpoint", "label": "Setpoint % (manual mode)", "tooltip": "", "order": 1, "width": "0", "height": "0", "passthru": true, "outs": "end", "topic": "setpoint_pump_b", "topicType": "str", "min": "0", "max": "100", "step": "5.0", "showLabel": true, "showValue": true, "labelPosition": "top", "valuePosition": "left", "thumbLabel": false, "iconStart": "", "iconEnd": "", "x": 120, "y": 2100, "wires": [ [ "lout_setpoint_pump_b_dash" ] ] }, { "id": "lout_setpoint_pump_b_dash", "type": "link out", "z": "tab_ui", "name": "cmd:setpoint-B", "mode": "link", "links": [ "lin_setpoint_pump_b" ], "x": 380, "y": 2100, "wires": [] }, { "id": "btn_pump_b_start", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_pump_b", "name": "Pump B startup", "label": "Startup", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#16a34a", "className": "", "icon": "play_arrow", "iconPosition": "left", "payload": "fired", "payloadType": "str", "topic": "start_pump_b", "topicType": "str", "buttonType": "default", "x": 120, "y": 2150, "wires": [ [ "wrap_pump_b_start" ] ] }, { "id": "wrap_pump_b_start", "type": "function", "z": "tab_ui", "name": "build start (Pump B)", "func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'startup' };\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 2150, "wires": [ [ "lout_seq_pump_b_dash" ] ] }, { "id": "btn_pump_b_stop", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_pump_b", "name": "Pump B shutdown", "label": "Shutdown", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#ea580c", "className": "", "icon": "stop", "iconPosition": "left", "payload": "fired", "payloadType": "str", "topic": "stop_pump_b", "topicType": "str", "buttonType": "default", "x": 120, "y": 2200, "wires": [ [ "wrap_pump_b_stop" ] ] }, { "id": "wrap_pump_b_stop", "type": "function", "z": "tab_ui", "name": "build stop (Pump B)", "func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'shutdown' };\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 2200, "wires": [ [ "lout_seq_pump_b_dash" ] ] }, { "id": "lout_seq_pump_b_dash", "type": "link out", "z": "tab_ui", "name": "cmd:pump-B-seq", "mode": "link", "links": [ "lin_seq_pump_b" ], "x": 640, "y": 2175, "wires": [] }, { "id": "c_ui_pump_c", "type": "comment", "z": "tab_ui", "name": "\u2500\u2500 Pump C \u2500\u2500", "info": "", "x": 640, "y": 2300, "wires": [] }, { "id": "lin_evt_pump_c_dash", "type": "link in", "z": "tab_ui", "name": "evt:pump-C", "links": [ "lout_evt_pump_c" ], "x": 120, "y": 2340, "wires": [ [ "dispatch_pump_c" ] ] }, { "id": "dispatch_pump_c", "type": "function", "z": "tab_ui", "name": "dispatch Pump C", "func": "const p = msg.payload || {};\nconst ts = Date.now();\nconst OFF = 6;\nfunction stateNum(s) {\n switch (s) {\n case 'operational': return OFF + 2;\n case 'starting':\n case 'warmingup': return OFF + 1;\n case 'stopping': return OFF + 1.5;\n case 'coolingdown': return OFF + 0.5;\n default: return OFF;\n }\n}\nconst sNum = p.state ? stateNum(p.state) : null;\nreturn [\n {payload: String(p.state || 'idle')},\n {payload: String(p.mode || 'auto')},\n {payload: String(p.ctrl || 'n/a')},\n {payload: String(p.flow || 'n/a')},\n {payload: String(p.power || 'n/a')},\n {payload: String(p.pUp || 'n/a')},\n {payload: String(p.pDn || 'n/a')},\n p.flowNum != null ? {topic: 'Pump C', payload: p.flowNum, timestamp: ts} : null,\n p.powerNum != null ? {topic: 'Pump C', payload: p.powerNum, timestamp: ts} : null,\n p.pUpNum != null ? {topic: 'Pump C up', payload: p.pUpNum, timestamp: ts} : null,\n p.pDnNum != null ? {topic: 'Pump C dn', payload: p.pDnNum, timestamp: ts} : null,\n sNum != null ? {topic: 'Pump C state', payload: sNum, timestamp: ts} : null,\n];", "outputs": 12, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 380, "y": 2340, "wires": [ [ "ui_pump_c_state" ], [ "ui_pump_c_mode" ], [ "ui_pump_c_ctrl" ], [ "ui_pump_c_flow" ], [ "ui_pump_c_power" ], [ "ui_pump_c_pUp" ], [ "ui_pump_c_pDn" ], [ "chart_trend_flow" ], [ "chart_trend_power" ], [ "chart_trend_pressure" ], [ "chart_trend_pressure" ], [ "chart_trend_states" ] ] }, { "id": "ui_pump_c_state", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_c", "order": 1, "width": "0", "height": "0", "name": "Pump C State", "label": "State", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 2340, "wires": [] }, { "id": "ui_pump_c_mode", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_c", "order": 1, "width": "0", "height": "0", "name": "Pump C Mode", "label": "Mode", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 2370, "wires": [] }, { "id": "ui_pump_c_ctrl", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_c", "order": 1, "width": "0", "height": "0", "name": "Pump C Controller %", "label": "Controller %", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 2400, "wires": [] }, { "id": "ui_pump_c_flow", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_c", "order": 1, "width": "0", "height": "0", "name": "Pump C Flow", "label": "Flow", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 2430, "wires": [] }, { "id": "ui_pump_c_power", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_c", "order": 1, "width": "0", "height": "0", "name": "Pump C Power", "label": "Power", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 2460, "wires": [] }, { "id": "ui_pump_c_pUp", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_c", "order": 1, "width": "0", "height": "0", "name": "Pump C p Upstream", "label": "p Upstream", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 2490, "wires": [] }, { "id": "ui_pump_c_pDn", "type": "ui-text", "z": "tab_ui", "group": "ui_grp_pump_c", "order": 1, "width": "0", "height": "0", "name": "Pump C p Downstream", "label": "p Downstream", "format": "{{msg.payload}}", "layout": "row-spread", "style": false, "font": "", "fontSize": 14, "color": "#000000", "x": 640, "y": 2520, "wires": [] }, { "id": "ui_pump_c_setpoint", "type": "ui-slider", "z": "tab_ui", "group": "ui_grp_pump_c", "name": "Pump C setpoint", "label": "Setpoint % (manual mode)", "tooltip": "", "order": 1, "width": "0", "height": "0", "passthru": true, "outs": "end", "topic": "setpoint_pump_c", "topicType": "str", "min": "0", "max": "100", "step": "5.0", "showLabel": true, "showValue": true, "labelPosition": "top", "valuePosition": "left", "thumbLabel": false, "iconStart": "", "iconEnd": "", "x": 120, "y": 2580, "wires": [ [ "lout_setpoint_pump_c_dash" ] ] }, { "id": "lout_setpoint_pump_c_dash", "type": "link out", "z": "tab_ui", "name": "cmd:setpoint-C", "mode": "link", "links": [ "lin_setpoint_pump_c" ], "x": 380, "y": 2580, "wires": [] }, { "id": "btn_pump_c_start", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_pump_c", "name": "Pump C startup", "label": "Startup", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#16a34a", "className": "", "icon": "play_arrow", "iconPosition": "left", "payload": "fired", "payloadType": "str", "topic": "start_pump_c", "topicType": "str", "buttonType": "default", "x": 120, "y": 2630, "wires": [ [ "wrap_pump_c_start" ] ] }, { "id": "wrap_pump_c_start", "type": "function", "z": "tab_ui", "name": "build start (Pump C)", "func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'startup' };\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 2630, "wires": [ [ "lout_seq_pump_c_dash" ] ] }, { "id": "btn_pump_c_stop", "type": "ui-button", "z": "tab_ui", "group": "ui_grp_pump_c", "name": "Pump C shutdown", "label": "Shutdown", "order": 1, "width": "0", "height": "0", "tooltip": "", "color": "#ffffff", "bgcolor": "#ea580c", "className": "", "icon": "stop", "iconPosition": "left", "payload": "fired", "payloadType": "str", "topic": "stop_pump_c", "topicType": "str", "buttonType": "default", "x": 120, "y": 2680, "wires": [ [ "wrap_pump_c_stop" ] ] }, { "id": "wrap_pump_c_stop", "type": "function", "z": "tab_ui", "name": "build stop (Pump C)", "func": "msg.topic = 'execSequence';\nmsg.payload = { source:'GUI', action:'execSequence', parameter:'shutdown' };\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 2680, "wires": [ [ "lout_seq_pump_c_dash" ] ] }, { "id": "lout_seq_pump_c_dash", "type": "link out", "z": "tab_ui", "name": "cmd:pump-C-seq", "mode": "link", "links": [ "lin_seq_pump_c" ], "x": 640, "y": 2655, "wires": [] }, { "id": "c_ui_trends", "type": "comment", "z": "tab_ui", "name": "\u2500\u2500 Trend charts (1h rolling) \u2500\u2500", "info": "", "x": 640, "y": 2840, "wires": [] }, { "id": "chart_trend_basin", "type": "ui-chart", "z": "tab_ui", "group": "ui_grp_tr_basin", "name": "Basin level + fill %", "label": "Basin level + fill", "order": 1, "chartType": "line", "interpolation": "linear", "category": "topic", "categoryType": "msg", "xAxisLabel": "", "xAxisType": "time", "xAxisProperty": "", "xAxisPropertyType": "timestamp", "xAxisFormat": "", "xAxisFormatType": "auto", "xmin": "", "xmax": "", "yAxisLabel": "m / %", "yAxisProperty": "payload", "yAxisPropertyType": "msg", "ymin": "", "ymax": "", "removeOlder": "60", "removeOlderUnit": "60", "removeOlderPoints": "3600", "action": "append", "stackSeries": false, "pointShape": "circle", "pointRadius": 4, "showLegend": true, "bins": 10, "colors": [ "#0095FF", "#FF0000", "#FF7F0E", "#2CA02C", "#A347E1", "#D62728", "#FF9896", "#9467BD", "#C5B0D5" ], "textColor": [ "#666666" ], "textColorDefault": true, "gridColor": [ "#e5e5e5" ], "gridColorDefault": true, "width": 12, "height": 8, "className": "", "x": 900, "y": 2880, "wires": [ [] ] }, { "id": "chart_trend_demand", "type": "ui-chart", "z": "tab_ui", "group": "ui_grp_tr_demand", "name": "PS process demand %", "label": "PS demand", "order": 1, "chartType": "line", "interpolation": "linear", "category": "topic", "categoryType": "msg", "xAxisLabel": "", "xAxisType": "time", "xAxisProperty": "", "xAxisPropertyType": "timestamp", "xAxisFormat": "", "xAxisFormatType": "auto", "xmin": "", "xmax": "", "yAxisLabel": "%", "yAxisProperty": "payload", "yAxisPropertyType": "msg", "ymin": "0", "ymax": "110", "removeOlder": "60", "removeOlderUnit": "60", "removeOlderPoints": "3600", "action": "append", "stackSeries": false, "pointShape": "circle", "pointRadius": 4, "showLegend": true, "bins": 10, "colors": [ "#0095FF", "#FF0000", "#FF7F0E", "#2CA02C", "#A347E1", "#D62728", "#FF9896", "#9467BD", "#C5B0D5" ], "textColor": [ "#666666" ], "textColorDefault": true, "gridColor": [ "#e5e5e5" ], "gridColorDefault": true, "width": 12, "height": 6, "className": "", "x": 900, "y": 2920, "wires": [ [] ] }, { "id": "chart_trend_dq", "type": "ui-chart", "z": "tab_ui", "group": "ui_grp_tr_dq", "name": "\u0394Q \u2014 inflow \u2212 outflow", "label": "\u0394Q", "order": 1, "chartType": "line", "interpolation": "linear", "category": "topic", "categoryType": "msg", "xAxisLabel": "", "xAxisType": "time", "xAxisProperty": "", "xAxisPropertyType": "timestamp", "xAxisFormat": "", "xAxisFormatType": "auto", "xmin": "", "xmax": "", "yAxisLabel": "m\u00b3/h", "yAxisProperty": "payload", "yAxisPropertyType": "msg", "ymin": "", "ymax": "", "removeOlder": "60", "removeOlderUnit": "60", "removeOlderPoints": "3600", "action": "append", "stackSeries": false, "pointShape": "circle", "pointRadius": 4, "showLegend": true, "bins": 10, "colors": [ "#0095FF", "#FF0000", "#FF7F0E", "#2CA02C", "#A347E1", "#D62728", "#FF9896", "#9467BD", "#C5B0D5" ], "textColor": [ "#666666" ], "textColorDefault": true, "gridColor": [ "#e5e5e5" ], "gridColorDefault": true, "width": 12, "height": 6, "className": "", "x": 900, "y": 2940, "wires": [ [] ] }, { "id": "chart_trend_states", "type": "ui-chart", "z": "tab_ui", "group": "ui_grp_tr_states", "name": "Pump state timeline", "label": "Pump states (A=0-2, B=3-5, C=6-8)", "order": 1, "chartType": "line", "interpolation": "step", "category": "topic", "categoryType": "msg", "xAxisLabel": "", "xAxisType": "time", "xAxisProperty": "", "xAxisPropertyType": "timestamp", "xAxisFormat": "", "xAxisFormatType": "auto", "xmin": "", "xmax": "", "yAxisLabel": "A B C tracks", "yAxisProperty": "payload", "yAxisPropertyType": "msg", "ymin": "-0.5", "ymax": "8.5", "removeOlder": "60", "removeOlderUnit": "60", "removeOlderPoints": "3600", "action": "append", "stackSeries": false, "pointShape": "circle", "pointRadius": 4, "showLegend": true, "bins": 10, "colors": [ "#0095FF", "#FF0000", "#FF7F0E", "#2CA02C", "#A347E1", "#D62728", "#FF9896", "#9467BD", "#C5B0D5" ], "textColor": [ "#666666" ], "textColorDefault": true, "gridColor": [ "#e5e5e5" ], "gridColorDefault": true, "width": 12, "height": 6, "className": "", "x": 900, "y": 2960, "wires": [ [] ] }, { "id": "chart_trend_flow", "type": "ui-chart", "z": "tab_ui", "group": "ui_grp_tr_flow", "name": "Inflow / Outflow / Per-pump flow", "label": "Flows", "order": 1, "chartType": "line", "interpolation": "linear", "category": "topic", "categoryType": "msg", "xAxisLabel": "", "xAxisType": "time", "xAxisProperty": "", "xAxisPropertyType": "timestamp", "xAxisFormat": "", "xAxisFormatType": "auto", "xmin": "", "xmax": "", "yAxisLabel": "m\u00b3/h", "yAxisProperty": "payload", "yAxisPropertyType": "msg", "ymin": "", "ymax": "", "removeOlder": "60", "removeOlderUnit": "60", "removeOlderPoints": "3600", "action": "append", "stackSeries": false, "pointShape": "circle", "pointRadius": 4, "showLegend": true, "bins": 10, "colors": [ "#0095FF", "#FF0000", "#FF7F0E", "#2CA02C", "#A347E1", "#D62728", "#FF9896", "#9467BD", "#C5B0D5" ], "textColor": [ "#666666" ], "textColorDefault": true, "gridColor": [ "#e5e5e5" ], "gridColorDefault": true, "width": 12, "height": 8, "className": "", "x": 900, "y": 2960, "wires": [ [] ] }, { "id": "chart_trend_power", "type": "ui-chart", "z": "tab_ui", "group": "ui_grp_tr_power", "name": "Per-pump power", "label": "Power", "order": 1, "chartType": "line", "interpolation": "linear", "category": "topic", "categoryType": "msg", "xAxisLabel": "", "xAxisType": "time", "xAxisProperty": "", "xAxisPropertyType": "timestamp", "xAxisFormat": "", "xAxisFormatType": "auto", "xmin": "", "xmax": "", "yAxisLabel": "kW", "yAxisProperty": "payload", "yAxisPropertyType": "msg", "ymin": "", "ymax": "", "removeOlder": "60", "removeOlderUnit": "60", "removeOlderPoints": "3600", "action": "append", "stackSeries": false, "pointShape": "circle", "pointRadius": 4, "showLegend": true, "bins": 10, "colors": [ "#0095FF", "#FF0000", "#FF7F0E", "#2CA02C", "#A347E1", "#D62728", "#FF9896", "#9467BD", "#C5B0D5" ], "textColor": [ "#666666" ], "textColorDefault": true, "gridColor": [ "#e5e5e5" ], "gridColorDefault": true, "width": 12, "height": 8, "className": "", "x": 900, "y": 3040, "wires": [ [] ] }, { "id": "chart_trend_pressure", "type": "ui-chart", "z": "tab_ui", "group": "ui_grp_tr_press", "name": "Per-pump up/dn pressure", "label": "Pressure", "order": 1, "chartType": "line", "interpolation": "linear", "category": "topic", "categoryType": "msg", "xAxisLabel": "", "xAxisType": "time", "xAxisProperty": "", "xAxisPropertyType": "timestamp", "xAxisFormat": "", "xAxisFormatType": "auto", "xmin": "", "xmax": "", "yAxisLabel": "mbar", "yAxisProperty": "payload", "yAxisPropertyType": "msg", "ymin": "", "ymax": "", "removeOlder": "60", "removeOlderUnit": "60", "removeOlderPoints": "3600", "action": "append", "stackSeries": false, "pointShape": "circle", "pointRadius": 4, "showLegend": true, "bins": 10, "colors": [ "#0095FF", "#FF0000", "#FF7F0E", "#2CA02C", "#A347E1", "#D62728", "#FF9896", "#9467BD", "#C5B0D5" ], "textColor": [ "#666666" ], "textColorDefault": true, "gridColor": [ "#e5e5e5" ], "gridColorDefault": true, "width": 12, "height": 8, "className": "", "x": 900, "y": 3120, "wires": [ [] ] }, { "id": "tab_drivers", "type": "tab", "label": "\ud83c\udf9b\ufe0f Demo Drivers", "disabled": false, "info": "Inflow generator. The operator picks a SCENARIO (Constant / Sine / Diurnal / Storm) on the dashboard and sets a BASELINE m\u00b3/h value. Every second this generator emits q_in to the PS based on the active scenario + baseline.\n\nOutflow is implicit: the pumps drain the basin via MGC." }, { "id": "c_drv_title", "type": "comment", "z": "tab_drivers", "name": "\ud83c\udf9b\ufe0f DEMO DRIVERS \u2014 operator-driven inflow generator", "info": "", "x": 640, "y": 20, "wires": [] }, { "id": "lin_inflow_scenario", "type": "link in", "z": "tab_drivers", "name": "cmd:inflow-scenario", "links": [ "lout_inflow_scenario", "lout_setup_inflow_scn" ], "x": 120, "y": 100, "wires": [ [ "inflow_state" ] ] }, { "id": "lin_inflow_baseline", "type": "link in", "z": "tab_drivers", "name": "cmd:inflow-baseline", "links": [ "lout_inflow_baseline", "lout_setup_inflow_baseline" ], "x": 120, "y": 140, "wires": [ [ "inflow_state" ] ] }, { "id": "inflow_tick", "type": "inject", "z": "tab_drivers", "name": "tick (1 Hz)", "props": [ { "p": "topic", "vt": "str" }, { "p": "payload", "v": "", "vt": "date" } ], "topic": "tick", "payload": "", "payloadType": "date", "repeat": "1", "crontab": "", "once": false, "onceDelay": "0.5", "x": 120, "y": 200, "wires": [ [ "inflow_state" ] ] }, { "id": "inflow_state", "type": "function", "z": "tab_drivers", "name": "inflow scenario engine", "func": "let scenario = context.get('scenario') || 'constant';\nlet baseline = context.get('baseline');\nif (baseline == null) baseline = 60;\n\nif (msg.topic === 'inflowBaseline') {\n const v = Number(msg.payload);\n if (Number.isFinite(v) && v >= 0) {\n baseline = v;\n context.set('baseline', baseline);\n }\n return null;\n}\nif (msg.topic === 'scenario') {\n const s = String(msg.payload || '').toLowerCase();\n if (['constant','sine','diurnal','storm'].includes(s)) {\n scenario = s;\n context.set('scenario', scenario);\n }\n return null;\n}\nconst t = Date.now() / 1000;\nlet q_h;\nswitch (scenario) {\n case 'sine': {\n q_h = baseline * (1 + 0.5 * Math.sin(2 * Math.PI * t / 240));\n break;\n }\n case 'diurnal': {\n q_h = baseline * (1 + 0.6 * Math.sin(2 * Math.PI * t / 480 - Math.PI/2));\n break;\n }\n case 'storm': {\n const phase = (t % 240) / 240;\n let factor;\n if (phase < 0.15) factor = 1 + (4 / 0.15) * phase;\n else factor = Math.max(1, 5 - (4 / 0.85) * (phase - 0.15));\n q_h = baseline * factor;\n break;\n }\n case 'constant':\n default:\n q_h = baseline;\n}\nq_h = Math.max(0, q_h);\nconst q_s = q_h / 3600;\nreturn [\n { topic: 'q_in', payload: q_s, unit: 'm3/s', timestamp: Date.now() },\n { payload: { scenario, baseline, q_h, q_s, ts: Date.now() } },\n];", "outputs": 2, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 640, "y": 160, "wires": [ [ "lout_qin_drivers" ], [ "lout_evt_inflow" ] ] }, { "id": "lout_qin_drivers", "type": "link out", "z": "tab_drivers", "name": "cmd:q_in", "mode": "link", "links": [ "lin_qin_at_ps" ], "x": 900, "y": 140, "wires": [] }, { "id": "lout_evt_inflow", "type": "link out", "z": "tab_drivers", "name": "evt:inflow", "mode": "link", "links": [ "lin_evt_inflow" ], "x": 900, "y": 180, "wires": [] }, { "id": "tab_setup", "type": "tab", "label": "\u2699\ufe0f Setup & Init", "disabled": false, "info": "One-shot deploy-time injects:\n \u2022 MGC scaling = normalized + mode = optimalcontrol\n \u2022 all pumps mode = auto\n \u2022 initial inflow baseline + scenario\n\nDisable this tab in production." }, { "id": "c_setup_title", "type": "comment", "z": "tab_setup", "name": "\u2699\ufe0f SETUP & INIT \u2014 one-shot deploy-time injects", "info": "", "x": 640, "y": 20, "wires": [] }, { "id": "setup_mgc_scaling", "type": "inject", "z": "tab_setup", "name": "MGC scaling = normalized", "props": [ { "p": "topic", "vt": "str" }, { "p": "payload", "v": "normalized", "vt": "str" } ], "topic": "setScaling", "payload": "normalized", "payloadType": "str", "repeat": "", "crontab": "", "once": true, "onceDelay": "1.5", "x": 120, "y": 100, "wires": [ [ "lout_setup_to_mgc" ] ] }, { "id": "setup_mgc_mode", "type": "inject", "z": "tab_setup", "name": "MGC mode = optimalcontrol", "props": [ { "p": "topic", "vt": "str" }, { "p": "payload", "v": "optimalcontrol", "vt": "str" } ], "topic": "setMode", "payload": "optimalcontrol", "payloadType": "str", "repeat": "", "crontab": "", "once": true, "onceDelay": "1.7", "x": 120, "y": 160, "wires": [ [ "lout_setup_to_mgc" ] ] }, { "id": "lout_setup_to_mgc", "type": "link out", "z": "tab_setup", "name": "setup:to-mgc", "mode": "link", "links": [ "lin_setup_at_mgc" ], "x": 380, "y": 130, "wires": [] }, { "id": "setup_pumps_mode", "type": "inject", "z": "tab_setup", "name": "pumps mode = auto", "props": [ { "p": "topic", "vt": "str" }, { "p": "payload", "v": "auto", "vt": "str" } ], "topic": "setMode", "payload": "auto", "payloadType": "str", "repeat": "", "crontab": "", "once": true, "onceDelay": "2.0", "x": 120, "y": 240, "wires": [ [ "lout_mode_setup" ] ] }, { "id": "lout_mode_setup", "type": "link out", "z": "tab_setup", "name": "cmd:mode", "mode": "link", "links": [ "lin_mode" ], "x": 380, "y": 240, "wires": [] }, { "id": "setup_inflow_baseline", "type": "inject", "z": "tab_setup", "name": "inflow baseline = 25 m\u00b3/h (nominal)", "props": [ { "p": "topic", "vt": "str" }, { "p": "payload", "v": "25", "vt": "num" } ], "topic": "inflowBaseline", "payload": "25", "payloadType": "num", "repeat": "", "crontab": "", "once": true, "onceDelay": "2.5", "x": 120, "y": 320, "wires": [ [ "lout_setup_inflow_baseline" ] ] }, { "id": "lout_setup_inflow_baseline", "type": "link out", "z": "tab_setup", "name": "cmd:inflow-baseline", "mode": "link", "links": [ "lin_inflow_baseline" ], "x": 380, "y": 320, "wires": [] }, { "id": "setup_inflow_scenario", "type": "inject", "z": "tab_setup", "name": "inflow scenario = sine", "props": [ { "p": "topic", "vt": "str" }, { "p": "payload", "v": "sine", "vt": "str" } ], "topic": "scenario", "payload": "sine", "payloadType": "str", "repeat": "", "crontab": "", "once": true, "onceDelay": "2.7", "x": 120, "y": 380, "wires": [ [ "lout_setup_inflow_scn" ] ] }, { "id": "lout_setup_inflow_scn", "type": "link out", "z": "tab_setup", "name": "cmd:inflow-scenario", "mode": "link", "links": [ "lin_inflow_scenario" ], "x": 380, "y": 380, "wires": [] }, { "id": "setup_calibrate_level", "type": "inject", "z": "tab_setup", "name": "[manual] calibrate basin = 1.0 m (click to reset)", "props": [ { "p": "topic", "vt": "str" }, { "p": "payload", "v": "1.0", "vt": "num" } ], "topic": "calibratePredictedLevel", "payload": "1.0", "payloadType": "num", "repeat": "", "crontab": "", "once": false, "onceDelay": "0.5", "x": 120, "y": 460, "wires": [ [ "lout_setup_calibrate" ] ] }, { "id": "lout_setup_calibrate", "type": "link out", "z": "tab_setup", "name": "setup:calibrate-ps", "mode": "link", "links": [ "lin_setup_calibrate_ps" ], "x": 380, "y": 460, "wires": [] }, { "id": "tab_telemetry", "type": "tab", "label": "\ud83d\udcc8 Telemetry", "disabled": false, "info": "InfluxDB writer: every EVOLV node's port-1 telemetry is fanned in via the evt:tlm link channel, converted to line protocol, and POSTed to InfluxDB v2 (org=evolv, bucket=telemetry).\n\nPattern adapted from docker/demo-flow.json." }, { "id": "c_tlm_title", "type": "comment", "z": "tab_telemetry", "name": "\ud83d\udcc8 TELEMETRY \u2014 InfluxDB writer", "info": "", "x": 640, "y": 20, "wires": [] }, { "id": "lin_tlm", "type": "link in", "z": "tab_telemetry", "name": "evt:tlm", "links": [ "lout_tlm_pump_a", "lout_tlm_meas_pump_a_u", "lout_tlm_meas_pump_a_d", "lout_tlm_meas_pump_a_f", "lout_tlm_meas_pump_a_p", "lout_tlm_pump_b", "lout_tlm_meas_pump_b_u", "lout_tlm_meas_pump_b_d", "lout_tlm_meas_pump_b_f", "lout_tlm_meas_pump_b_p", "lout_tlm_pump_c", "lout_tlm_meas_pump_c_u", "lout_tlm_meas_pump_c_d", "lout_tlm_meas_pump_c_f", "lout_tlm_meas_pump_c_p", "lout_tlm_mgc", "lout_tlm_ps" ], "x": 120, "y": 100, "wires": [ [ "fn_tlm_to_lp" ] ] }, { "id": "fn_tlm_to_lp", "type": "function", "z": "tab_telemetry", "name": "\u2192 InfluxDB line protocol", "func": "const p = msg.payload;\nif (!p || !p.measurement || !p.fields) return null;\nconst esc = (s) => String(s)\n .replace(/,/g, '\\\\,').replace(/ /g, '\\\\ ').replace(/=/g, '\\\\=');\nconst tags = Object.entries(p.tags || {})\n .filter(([k, v]) => v !== undefined && v !== null && v !== '')\n .map(([k, v]) => `${esc(k)}=${esc(v)}`).join(',');\nconst fieldPairs = Object.entries(p.fields)\n .filter(([k, v]) => v !== undefined && v !== null)\n .map(([k, v]) => {\n if (typeof v === 'number' && Number.isFinite(v)) return `${esc(k)}=${v}`;\n if (typeof v === 'boolean') return `${esc(k)}=${v}`;\n return `${esc(k)}=\"${String(v).replace(/\"/g, '\\\\\"')}\"`;\n });\nif (fieldPairs.length === 0) return null;\nconst ts = Date.now() * 1000000;\nmsg.payload = `${esc(p.measurement)}${tags ? ',' + tags : ''} `\n + `${fieldPairs.join(',')} ${ts}`;\n// Hint the join node to fire on size or timeout.\nmsg.topic = 'tlm';\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 640, "y": 100, "wires": [ [ "join_tlm" ] ] }, { "id": "join_tlm", "type": "join", "z": "tab_telemetry", "name": "batch (200 lines / 2 s)", "mode": "custom", "build": "string", "property": "payload", "propertyType": "msg", "key": "topic", "joiner": "\\n", "joinerType": "str", "accumulate": false, "timeout": "2", "count": "200", "reduceRight": false, "reduceExp": "", "reduceInit": "", "reduceInitType": "", "reduceFixup": "", "x": 900, "y": 100, "wires": [ [ "fn_tlm_post" ] ] }, { "id": "fn_tlm_post", "type": "function", "z": "tab_telemetry", "name": "wrap as InfluxDB POST", "func": "// Count lines for status reporting.\nconst body = String(msg.payload || '');\nconst lineCount = body ? body.split('\\n').length : 0;\nif (lineCount === 0) return null;\nmsg.lineCount = lineCount;\nmsg.headers = {\n 'Authorization': 'Token evolv-dev-token',\n 'Content-Type': 'text/plain'\n};\nmsg.url = 'http://influxdb:8086/api/v2/write?org=evolv&bucket=telemetry&precision=ns';\nmsg.method = 'POST';\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1100, "y": 100, "wires": [ [ "http_tlm" ] ] }, { "id": "http_tlm", "type": "http request", "z": "tab_telemetry", "name": "Write InfluxDB", "method": "use", "ret": "txt", "paytoqs": "ignore", "url": "", "tls": "", "persist": false, "proxy": "", "authType": "", "senderr": false, "x": 1240, "y": 100, "wires": [ [ "fn_tlm_count" ] ] }, { "id": "fn_tlm_count", "type": "function", "z": "tab_telemetry", "name": "Count writes", "func": "const lines = Number(msg.lineCount) || 0;\nconst writes = (global.get('influx_writes') || 0) + 1;\nconst totalLines = (global.get('influx_lines') || 0) + lines;\nglobal.set('influx_writes', writes);\nglobal.set('influx_lines', totalLines);\nconst errors = global.get('influx_errors') || 0;\nif (msg.statusCode && msg.statusCode >= 400) {\n global.set('influx_errors', errors + 1);\n node.status({fill:'red', shape:'ring',\n text:`ERR ${errors+1}: ${msg.statusCode}`});\n} else {\n node.status({fill:'green', shape:'dot',\n text:`${writes} POSTs \u00b7 ${totalLines} lines (${errors} err)`});\n}\nreturn null;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1420, "y": 100, "wires": [ [] ] } ]