Files
EVOLV/docker/demo-flow.json

7231 lines
180 KiB
JSON

[
{
"id": "demo_tab_wwtp",
"type": "tab",
"label": "Telemetry / InfluxDB",
"disabled": false,
"info": "InfluxDB write chain, process debug, Grafana dashboard API, shared infrastructure"
},
{
"id": "demo_tab_dashboard",
"type": "tab",
"label": "EVOLV Dashboard",
"disabled": false,
"info": "FlowFuse real-time dashboard for process monitoring"
},
{
"id": "demo_ui_base",
"type": "ui-base",
"name": "EVOLV WWTP",
"path": "/dashboard",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control"
],
"showPathInSidebar": false,
"headerContent": "page",
"navigationStyle": "default",
"titleBarStyle": "default"
},
{
"id": "demo_ui_theme",
"type": "ui-theme",
"name": "EVOLV Theme",
"colors": {
"surface": "#1a1a2e",
"primary": "#0094ce",
"bgPage": "#16213e",
"groupBg": "#1a1a2e",
"groupOutline": "#0f3460"
},
"sizes": {
"density": "default",
"pagePadding": "12px",
"groupGap": "12px",
"groupBorderRadius": "8px",
"widgetGap": "10px"
}
},
{
"id": "demo_ui_page_influent",
"type": "ui-page",
"name": "Influent Stations",
"ui": "demo_ui_base",
"path": "/influent",
"icon": "water_drop",
"layout": "grid",
"theme": "demo_ui_theme",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "12"
}
],
"order": 1,
"className": ""
},
{
"id": "demo_ui_page_treatment",
"type": "ui-page",
"name": "Biological Treatment",
"ui": "demo_ui_base",
"path": "/treatment",
"icon": "science",
"layout": "grid",
"theme": "demo_ui_theme",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "12"
}
],
"order": 5,
"className": ""
},
{
"id": "demo_ui_page_telemetry",
"type": "ui-page",
"name": "Telemetry Status",
"ui": "demo_ui_base",
"path": "/telemetry",
"icon": "analytics",
"layout": "grid",
"theme": "demo_ui_theme",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "12"
}
],
"order": 6,
"className": ""
},
{
"id": "demo_ui_grp_ps_west",
"type": "ui-group",
"name": "PS West (Urban)",
"page": "demo_ui_page_influent",
"width": "4",
"height": "1",
"order": 1,
"showTitle": true,
"className": ""
},
{
"id": "demo_ui_grp_ps_north",
"type": "ui-group",
"name": "PS North (Industrial)",
"page": "demo_ui_page_influent",
"width": "4",
"height": "1",
"order": 2,
"showTitle": true,
"className": ""
},
{
"id": "demo_ui_grp_ps_south",
"type": "ui-group",
"name": "PS South (Residential)",
"page": "demo_ui_page_influent",
"width": "4",
"height": "1",
"order": 3,
"showTitle": true,
"className": ""
},
{
"id": "demo_ui_grp_flows",
"type": "ui-group",
"name": "Combined Influent Flows",
"page": "demo_ui_page_influent",
"width": "12",
"height": "1",
"order": 4,
"showTitle": true,
"className": ""
},
{
"id": "demo_ui_grp_reactor",
"type": "ui-group",
"name": "Bioreactor (CSTR)",
"page": "demo_ui_page_treatment",
"width": "6",
"height": "1",
"order": 1,
"showTitle": true,
"className": ""
},
{
"id": "demo_ui_grp_measurements",
"type": "ui-group",
"name": "Process Measurements",
"page": "demo_ui_page_treatment",
"width": "6",
"height": "1",
"order": 2,
"showTitle": true,
"className": ""
},
{
"id": "demo_ui_grp_settler",
"type": "ui-group",
"name": "Secondary Clarifier",
"page": "demo_ui_page_treatment",
"width": "6",
"height": "1",
"order": 3,
"showTitle": true,
"className": ""
},
{
"id": "demo_ui_grp_effluent",
"type": "ui-group",
"name": "Effluent Quality",
"page": "demo_ui_page_treatment",
"width": "6",
"height": "1",
"order": 4,
"showTitle": true,
"className": ""
},
{
"id": "demo_ui_grp_influx_status",
"type": "ui-group",
"name": "InfluxDB Telemetry",
"page": "demo_ui_page_telemetry",
"width": "12",
"height": "1",
"order": 1,
"showTitle": true,
"className": ""
},
{
"id": "demo_ui_grp_debug",
"type": "ui-group",
"name": "Debug Messages",
"page": "demo_ui_page_telemetry",
"width": "12",
"height": "1",
"order": 2,
"showTitle": true,
"className": ""
},
{
"id": "demo_comment_ps",
"type": "comment",
"z": "demo_tab_ps_west",
"name": "=== PUMPING STATION WEST (Urban Catchment - 2 pumps, Level-based) ===",
"info": "",
"x": 340,
"y": 40
},
{
"id": "demo_ps_west",
"type": "pumpingStation",
"z": "demo_tab_ps_west",
"name": "PS West",
"simulator": false,
"basinVolume": 500,
"basinHeight": 4,
"heightInlet": 3.2,
"heightOutlet": 0.3,
"heightOverflow": 3.8,
"timeleftToFullOrEmptyThresholdSeconds": 300,
"enableDryRunProtection": true,
"enableOverfillProtection": true,
"dryRunThresholdPercent": 5,
"overfillThresholdPercent": 95,
"minHeightBasedOn": "outlet",
"refHeight": "NAP",
"basinBottomRef": -2.5,
"controlMode": "levelbased",
"startLevel": "2.5",
"stopLevel": "1.0",
"minFlowLevel": "",
"maxFlowLevel": "",
"flowSetpoint": "",
"flowDeadband": "",
"uuid": "ps-west-001",
"supplier": "Hidrostal",
"category": "pumpingStation",
"assetType": "influent-ps",
"model": "ps-urban-500",
"unit": "m3/h",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "upstream",
"positionIcon": "→",
"hasDistance": true,
"distance": 2400,
"distanceUnit": "m",
"distanceDescription": "from WWTP inlet",
"x": 860,
"y": 220,
"wires": [
[
"demo_link_ps_west_dash",
"demo_link_process_out_west",
"demo_fn_level_to_pressure_w",
"demo_mon_west",
"demo_link_merge_west_out",
"demo_ctrl_west_link_detail_data_out"
],
[
"demo_link_influx_out_west"
],
[
"demo_dbg_registration"
]
]
},
{
"id": "demo_pump_w1",
"type": "rotatingMachine",
"z": "demo_tab_ps_west",
"name": "Pump W1",
"speed": "1",
"startup": "5",
"warmup": "3",
"shutdown": "4",
"cooldown": "2",
"movementMode": "dynspeed",
"machineCurve": "",
"uuid": "pump-w1-001",
"supplier": "hidrostal",
"category": "machine",
"assetType": "pump-centrifugal",
"model": "hidrostal-H05K-S03R",
"unit": "m3/h",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"positionIcon": "⊥",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 600,
"y": 120,
"wires": [
[
"demo_link_process_out_west"
],
[
"demo_link_influx_out_west"
],
[
"demo_mgc_west"
]
],
"curveFlowUnit": "l/s",
"curvePressureUnit": "mbar",
"curvePowerUnit": "kW"
},
{
"id": "demo_pump_w2",
"type": "rotatingMachine",
"z": "demo_tab_ps_west",
"name": "Pump W2",
"speed": "1",
"startup": "5",
"warmup": "3",
"shutdown": "4",
"cooldown": "2",
"movementMode": "dynspeed",
"machineCurve": "",
"uuid": "pump-w2-001",
"supplier": "hidrostal",
"category": "machine",
"assetType": "pump-centrifugal",
"model": "hidrostal-H05K-S03R",
"unit": "m3/h",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"positionIcon": "⊥",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 600,
"y": 320,
"wires": [
[
"demo_link_process_out_west"
],
[
"demo_link_influx_out_west"
],
[
"demo_mgc_west"
]
],
"curveFlowUnit": "l/s",
"curvePressureUnit": "mbar",
"curvePowerUnit": "kW"
},
{
"id": "demo_inj_west_mode",
"type": "inject",
"z": "demo_tab_ps_west",
"name": "PS West → levelbased",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"vt": "str"
}
],
"topic": "setMode",
"payload": "levelbased",
"payloadType": "str",
"once": true,
"onceDelay": "0.5",
"x": 120,
"y": 200,
"wires": [
[
"demo_ps_west"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_inj_west_flow",
"type": "inject",
"z": "demo_tab_ps_west",
"name": "West q_in (3s)",
"props": [
{
"p": "payload"
}
],
"payload": "",
"payloadType": "date",
"repeat": "3",
"once": true,
"onceDelay": "2",
"x": 120,
"y": 240,
"wires": [
[
"demo_fn_west_flow_sim"
]
],
"repeatType": "interval",
"crontab": ""
},
{
"id": "demo_fn_west_flow_sim",
"type": "function",
"z": "demo_tab_ps_west",
"name": "Simulate urban flow",
"func": "// Urban catchment: strong diurnal pattern + rain events\nconst hour = new Date().getHours() + new Date().getMinutes()/60;\nconst diurnal = Math.sin((hour - 6) * Math.PI / 12) * 0.35 + 0.65;\nconst noise = (Math.random() - 0.5) * 0.15;\n\n// Occasional rain burst (5% chance)\nconst rain = Math.random() < 0.05 ? Math.random() * 80 : 0;\n\nconst baseFlow = 150;\nconst flow = Math.max(20, baseFlow * (diurnal + noise) + rain);\n\nmsg.topic = 'q_in';\nmsg.payload = { value: Math.round(flow * 10) / 10, unit: 'm3/h' };\nreturn msg;",
"outputs": 1,
"x": 360,
"y": 240,
"wires": [
[
"demo_ps_west"
]
]
},
{
"id": "demo_inj_w1_mode",
"type": "inject",
"z": "demo_tab_ps_west",
"name": "W1 → auto",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"vt": "str"
}
],
"topic": "setMode",
"payload": "auto",
"payloadType": "str",
"once": true,
"onceDelay": "0.3",
"x": 120,
"y": 120,
"wires": [
[
"demo_pump_w1"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_inj_w2_mode",
"type": "inject",
"z": "demo_tab_ps_west",
"name": "W2 → auto",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"vt": "str"
}
],
"topic": "setMode",
"payload": "auto",
"payloadType": "str",
"once": true,
"onceDelay": "0.3",
"x": 120,
"y": 320,
"wires": [
[
"demo_pump_w2"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_comment_ps_north",
"type": "comment",
"z": "demo_tab_ps_north",
"name": "=== PUMPING STATION NORTH (Industrial - 1 pump, Flow-based) ===",
"info": "",
"x": 340,
"y": 40
},
{
"id": "demo_ps_north",
"type": "pumpingStation",
"z": "demo_tab_ps_north",
"name": "PS North",
"simulator": false,
"basinVolume": 200,
"basinHeight": 3,
"heightInlet": 2.4,
"heightOutlet": 0.2,
"heightOverflow": 2.8,
"timeleftToFullOrEmptyThresholdSeconds": 180,
"enableDryRunProtection": true,
"enableOverfillProtection": true,
"dryRunThresholdPercent": 5,
"overfillThresholdPercent": 95,
"minHeightBasedOn": "outlet",
"refHeight": "NAP",
"basinBottomRef": -1.8,
"controlMode": "flowbased",
"startLevel": "",
"stopLevel": "",
"minFlowLevel": "",
"maxFlowLevel": "",
"flowSetpoint": "60",
"flowDeadband": "5",
"flowPidKp": 1.5,
"flowPidKi": 0.05,
"flowPidKd": 0.01,
"flowPidDerivativeFilter": 0.2,
"flowPidRateUp": 30,
"flowPidRateDown": 40,
"uuid": "ps-north-001",
"supplier": "Flygt",
"category": "pumpingStation",
"assetType": "influent-ps",
"model": "ps-industrial-200",
"unit": "m3/h",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "upstream",
"positionIcon": "→",
"hasDistance": true,
"distance": 1800,
"distanceUnit": "m",
"distanceDescription": "industrial zone",
"x": 860,
"y": 200,
"wires": [
[
"demo_link_ps_north_dash",
"demo_link_process_out_north",
"demo_fn_level_to_pressure_n",
"demo_mon_north",
"demo_link_merge_north_out",
"demo_ctrl_north_link_detail_data_out"
],
[
"demo_link_influx_out_north"
],
[
"demo_dbg_registration"
]
]
},
{
"id": "demo_pump_n1",
"type": "rotatingMachine",
"z": "demo_tab_ps_north",
"name": "Pump N1",
"speed": "1",
"startup": "4",
"warmup": "2",
"shutdown": "3",
"cooldown": "2",
"movementMode": "dynspeed",
"machineCurve": "",
"uuid": "pump-n1-001",
"supplier": "hidrostal",
"category": "machine",
"assetType": "pump-centrifugal",
"model": "hidrostal-C5-D03R-SHN1",
"unit": "m3/h",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"x": 600,
"y": 120,
"wires": [
[
"demo_link_process_out_north"
],
[
"demo_link_influx_out_north"
],
[
"demo_ps_north"
]
],
"curveFlowUnit": "l/s",
"curvePressureUnit": "mbar",
"curvePowerUnit": "kW",
"positionIcon": "⊥"
},
{
"id": "demo_inj_north_mode",
"type": "inject",
"z": "demo_tab_ps_north",
"name": "PS North → flowbased",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"vt": "str"
}
],
"topic": "setMode",
"payload": "flowbased",
"payloadType": "str",
"once": true,
"onceDelay": "0.5",
"x": 120,
"y": 200,
"wires": [
[
"demo_ps_north"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_inj_north_flow",
"type": "inject",
"z": "demo_tab_ps_north",
"name": "North q_in (3s)",
"props": [
{
"p": "payload"
}
],
"payload": "",
"payloadType": "date",
"repeat": "3",
"once": true,
"onceDelay": "2",
"x": 120,
"y": 240,
"wires": [
[
"demo_fn_north_flow_sim"
]
],
"repeatType": "interval",
"crontab": ""
},
{
"id": "demo_fn_north_flow_sim",
"type": "function",
"z": "demo_tab_ps_north",
"name": "Simulate industrial flow",
"func": "// Industrial: more constant but with shift changes\nconst hour = new Date().getHours();\nconst shift = (hour >= 6 && hour < 22) ? 1.0 : 0.3; // day vs night\nconst noise = (Math.random() - 0.5) * 0.1;\nconst baseFlow = 65;\nconst flow = Math.max(5, baseFlow * (shift + noise));\n\nmsg.topic = 'q_in';\nmsg.payload = { value: Math.round(flow * 10) / 10, unit: 'm3/h' };\nreturn msg;",
"outputs": 1,
"x": 360,
"y": 240,
"wires": [
[
"demo_ps_north"
]
]
},
{
"id": "demo_inj_n1_mode",
"type": "inject",
"z": "demo_tab_ps_north",
"name": "N1 → auto",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"vt": "str"
}
],
"topic": "setMode",
"payload": "auto",
"payloadType": "str",
"once": true,
"onceDelay": "0.3",
"x": 120,
"y": 120,
"wires": [
[
"demo_pump_n1"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_comment_ps_south",
"type": "comment",
"z": "demo_tab_ps_south",
"name": "=== PUMPING STATION SOUTH (Residential - 1 pump, Manual) ===",
"info": "",
"x": 340,
"y": 40
},
{
"id": "demo_ps_south",
"type": "pumpingStation",
"z": "demo_tab_ps_south",
"name": "PS South",
"simulator": false,
"basinVolume": 100,
"basinHeight": 2.5,
"heightInlet": 2,
"heightOutlet": 0.2,
"heightOverflow": 2.3,
"timeleftToFullOrEmptyThresholdSeconds": 120,
"enableDryRunProtection": true,
"enableOverfillProtection": true,
"dryRunThresholdPercent": 3,
"overfillThresholdPercent": 97,
"minHeightBasedOn": "outlet",
"refHeight": "NAP",
"basinBottomRef": -1.2,
"controlMode": "manual",
"startLevel": "",
"stopLevel": "",
"uuid": "ps-south-001",
"supplier": "Grundfos",
"category": "pumpingStation",
"assetType": "influent-ps",
"model": "ps-residential-100",
"unit": "m3/h",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "upstream",
"positionIcon": "→",
"hasDistance": true,
"distance": 800,
"distanceUnit": "m",
"distanceDescription": "residential area",
"x": 860,
"y": 200,
"wires": [
[
"demo_link_ps_south_dash",
"demo_link_process_out_south",
"demo_fn_level_to_pressure_s",
"demo_mon_south",
"demo_link_merge_south_out",
"demo_ctrl_south_link_detail_data_out"
],
[
"demo_link_influx_out_south"
],
[
"demo_dbg_registration"
]
]
},
{
"id": "demo_pump_s1",
"type": "rotatingMachine",
"z": "demo_tab_ps_south",
"name": "Pump S1",
"speed": "1",
"startup": "3",
"warmup": "2",
"shutdown": "3",
"cooldown": "1",
"movementMode": "dynspeed",
"machineCurve": "",
"uuid": "pump-s1-001",
"supplier": "hidrostal",
"category": "machine",
"assetType": "pump-centrifugal",
"model": "hidrostal-C5-D03R-SHN1",
"unit": "m3/h",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"x": 600,
"y": 120,
"wires": [
[
"demo_link_process_out_south"
],
[
"demo_link_influx_out_south"
],
[
"demo_ps_south"
]
],
"curveFlowUnit": "l/s",
"curvePressureUnit": "mbar",
"curvePowerUnit": "kW",
"positionIcon": "⊥"
},
{
"id": "demo_inj_south_mode",
"type": "inject",
"z": "demo_tab_ps_south",
"name": "PS South → manual",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"vt": "str"
}
],
"topic": "setMode",
"payload": "manual",
"payloadType": "str",
"once": true,
"onceDelay": "0.5",
"x": 120,
"y": 200,
"wires": [
[
"demo_ps_south"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_inj_south_flow",
"type": "inject",
"z": "demo_tab_ps_south",
"name": "South q_in (4s)",
"props": [
{
"p": "payload"
}
],
"payload": "",
"payloadType": "date",
"repeat": "4",
"once": true,
"onceDelay": "2",
"x": 120,
"y": 240,
"wires": [
[
"demo_fn_south_flow_sim"
]
],
"repeatType": "interval",
"crontab": ""
},
{
"id": "demo_fn_south_flow_sim",
"type": "function",
"z": "demo_tab_ps_south",
"name": "Simulate residential flow",
"func": "// Residential: morning/evening peaks, low at night\nconst hour = new Date().getHours() + new Date().getMinutes()/60;\nconst morning = Math.exp(-Math.pow(hour - 8, 2) / 4) * 0.4;\nconst evening = Math.exp(-Math.pow(hour - 19, 2) / 6) * 0.35;\nconst base = 0.25;\nconst noise = (Math.random() - 0.5) * 0.1;\nconst baseFlow = 40;\nconst flow = Math.max(3, baseFlow * (base + morning + evening + noise));\n\nmsg.topic = 'q_in';\nmsg.payload = { value: Math.round(flow * 10) / 10, unit: 'm3/h' };\nreturn msg;",
"outputs": 1,
"x": 360,
"y": 240,
"wires": [
[
"demo_ps_south"
]
]
},
{
"id": "demo_inj_s1_mode",
"type": "inject",
"z": "demo_tab_ps_south",
"name": "S1 → auto",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"vt": "str"
}
],
"topic": "setMode",
"payload": "auto",
"payloadType": "str",
"once": true,
"onceDelay": "0.3",
"x": 120,
"y": 120,
"wires": [
[
"demo_pump_s1"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_comment_treatment",
"type": "comment",
"z": "demo_tab_treatment",
"name": "=== BIOLOGICAL TREATMENT ===",
"info": "",
"x": 80,
"y": 40
},
{
"id": "demo_meas_flow",
"type": "measurement",
"z": "demo_tab_treatment",
"name": "FT-001 (Influent Flow)",
"scaling": true,
"i_min": 0,
"i_max": 500,
"i_offset": 0,
"o_min": 0,
"o_max": 500,
"smooth_method": "mean",
"count": 5,
"simulator": false,
"uuid": "ft-001",
"supplier": "Endress+Hauser",
"category": "sensor",
"assetType": "flow",
"model": "Promag-W400",
"unit": "m3/h",
"assetTagNumber": "FT-001",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "upstream",
"positionIcon": "→",
"hasDistance": true,
"x": 420,
"y": 100,
"wires": [
[
"demo_link_meas_dash",
"demo_link_process_out_treatment"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_reactor_z1"
]
],
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "reactor inlet"
},
{
"id": "demo_meas_do",
"type": "measurement",
"z": "demo_tab_treatment",
"name": "DO-001 (Zone 2 Mid Probe)",
"scaling": true,
"i_min": 0,
"i_max": 20,
"i_offset": 0,
"o_min": 0,
"o_max": 20,
"smooth_method": "mean",
"count": 3,
"simulator": false,
"uuid": "do-001",
"supplier": "Hach",
"category": "sensor",
"assetType": "dissolved-oxygen",
"model": "LDO2",
"unit": "mg/L",
"assetTagNumber": "DO-001",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"x": 420,
"y": 160,
"wires": [
[
"demo_link_meas_dash",
"demo_link_process_out_treatment"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_reactor_z2"
]
],
"positionIcon": "⊥",
"hasDistance": true,
"distance": 6.25,
"distanceUnit": "m",
"distanceDescription": "zone 2 mid probe"
},
{
"id": "demo_meas_nh4",
"type": "measurement",
"z": "demo_tab_treatment",
"name": "NH4-001 (Zone 4 Effluent Probe)",
"scaling": true,
"i_min": 0,
"i_max": 50,
"i_offset": 0,
"o_min": 0,
"o_max": 50,
"smooth_method": "mean",
"count": 3,
"simulator": false,
"uuid": "nh4-001",
"supplier": "Hach",
"category": "sensor",
"assetType": "ammonium",
"model": "Amtax-sc",
"unit": "mg/L",
"assetTagNumber": "NH4-001",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"x": 420,
"y": 400,
"wires": [
[
"demo_link_meas_dash",
"demo_link_process_out_treatment"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_reactor_z4"
]
],
"positionIcon": "⊥",
"hasDistance": true,
"distance": 6.25,
"distanceUnit": "m",
"distanceDescription": "zone 4 effluent probe"
},
{
"id": "demo_reactor_z1",
"type": "reactor",
"z": "demo_tab_treatment",
"name": "Bioreactor Z1",
"reactor_type": "PFR",
"volume": 500,
"length": 12.5,
"resolution_L": 140,
"alpha": 0,
"n_inlets": 4,
"kla": "",
"S_O_init": 0.8,
"S_I_init": 30,
"S_S_init": 120,
"S_NH_init": 16,
"S_N2_init": 0,
"S_NO_init": 0.5,
"S_HCO_init": 8,
"X_I_init": 25,
"X_S_init": 75,
"X_H_init": 1500,
"X_STO_init": 0,
"X_A_init": 18,
"X_TS_init": 2500,
"timeStep": 2,
"uuid": "reactor-z1-001",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "upstream",
"x": 820,
"y": 220,
"wires": [
[
"demo_fn_reactor_z1_tag"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_dbg_registration"
]
],
"speedUpFactor": 45,
"positionIcon": "→"
},
{
"id": "demo_reactor_z2",
"type": "reactor",
"z": "demo_tab_treatment",
"name": "Bioreactor Z2",
"reactor_type": "PFR",
"volume": 500,
"length": 12.5,
"resolution_L": 140,
"alpha": 0,
"n_inlets": 1,
"kla": "",
"S_O_init": 1,
"S_I_init": 30,
"S_S_init": 90,
"S_NH_init": 10,
"S_N2_init": 0,
"S_NO_init": 1.5,
"S_HCO_init": 8,
"X_I_init": 25,
"X_S_init": 75,
"X_H_init": 1500,
"X_STO_init": 0,
"X_A_init": 18,
"X_TS_init": 2500,
"timeStep": 2,
"uuid": "reactor-z2-001",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "upstream",
"x": 1040,
"y": 220,
"wires": [
[
"demo_fn_reactor_z2_tag"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_dbg_registration"
]
],
"speedUpFactor": 45,
"positionIcon": "→"
},
{
"id": "demo_reactor_z3",
"type": "reactor",
"z": "demo_tab_treatment",
"name": "Bioreactor Z3",
"reactor_type": "PFR",
"volume": 500,
"length": 12.5,
"resolution_L": 140,
"alpha": 0,
"n_inlets": 1,
"kla": "",
"S_O_init": 1.4,
"S_I_init": 30,
"S_S_init": 60,
"S_NH_init": 5,
"S_N2_init": 0,
"S_NO_init": 3.5,
"S_HCO_init": 8,
"X_I_init": 25,
"X_S_init": 75,
"X_H_init": 1500,
"X_STO_init": 0,
"X_A_init": 18,
"X_TS_init": 2500,
"timeStep": 2,
"uuid": "reactor-z3-001",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "upstream",
"x": 1260,
"y": 220,
"wires": [
[
"demo_fn_reactor_z3_tag"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_dbg_registration"
]
],
"speedUpFactor": 45,
"positionIcon": "→"
},
{
"id": "demo_reactor_z4",
"type": "reactor",
"z": "demo_tab_treatment",
"name": "Bioreactor Z4",
"reactor_type": "PFR",
"volume": 500,
"length": 12.5,
"resolution_L": 140,
"alpha": 0,
"n_inlets": 1,
"kla": "",
"S_O_init": 1.8,
"S_I_init": 30,
"S_S_init": 35,
"S_NH_init": 2.5,
"S_N2_init": 0,
"S_NO_init": 5.5,
"S_HCO_init": 8,
"X_I_init": 25,
"X_S_init": 75,
"X_H_init": 1500,
"X_STO_init": 0,
"X_A_init": 18,
"X_TS_init": 2500,
"timeStep": 2,
"uuid": "reactor-z4-001",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "upstream",
"x": 1480,
"y": 220,
"wires": [
[
"demo_fn_reactor_z4_tag"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_dbg_registration",
"demo_settler"
]
],
"speedUpFactor": 45,
"positionIcon": "→"
},
{
"id": "demo_fn_reactor_z1_tag",
"type": "function",
"z": "demo_tab_treatment",
"name": "Tag Reactor Z1",
"func": "const clone = (input) => {\n const out = { ...input };\n if (input && typeof input.payload === 'object' && input.payload !== null) {\n out.payload = JSON.parse(JSON.stringify(input.payload));\n }\n return out;\n};\nconst tagged = clone(msg);\ntagged.zoneLabel = 'Z1';\ntagged.zoneVolume = 500;\ntagged.zoneIndex = 1;\nif (msg.topic === 'Fluent') {\n return [tagged, clone(msg)];\n}\nreturn [tagged, null];",
"outputs": 2,
"x": 1080,
"y": 160,
"wires": [
[
"demo_link_reactor_dash",
"demo_link_process_out_treatment",
"demo_fn_nh4_profile_extract"
],
[
"demo_reactor_z2"
]
]
},
{
"id": "demo_fn_reactor_z2_tag",
"type": "function",
"z": "demo_tab_treatment",
"name": "Tag Reactor Z2",
"func": "const clone = (input) => {\n const out = { ...input };\n if (input && typeof input.payload === 'object' && input.payload !== null) {\n out.payload = JSON.parse(JSON.stringify(input.payload));\n }\n return out;\n};\nconst tagged = clone(msg);\ntagged.zoneLabel = 'Z2';\ntagged.zoneVolume = 500;\ntagged.zoneIndex = 2;\nif (msg.topic === 'Fluent') {\n return [tagged, clone(msg), clone(msg)];\n}\nreturn [tagged, null, null];",
"outputs": 3,
"x": 1300,
"y": 160,
"wires": [
[
"demo_link_reactor_dash",
"demo_link_process_out_treatment",
"demo_fn_nh4_profile_extract"
],
[
"demo_reactor_z3"
],
[
"demo_fn_sample_z2_probe"
]
]
},
{
"id": "demo_fn_reactor_z3_tag",
"type": "function",
"z": "demo_tab_treatment",
"name": "Tag Reactor Z3",
"func": "const clone = (input) => {\n const out = { ...input };\n if (input && typeof input.payload === 'object' && input.payload !== null) {\n out.payload = JSON.parse(JSON.stringify(input.payload));\n }\n return out;\n};\nconst tagged = clone(msg);\ntagged.zoneLabel = 'Z3';\ntagged.zoneVolume = 500;\ntagged.zoneIndex = 3;\nif (msg.topic === 'Fluent') {\n return [tagged, clone(msg)];\n}\nreturn [tagged, null];",
"outputs": 2,
"x": 1520,
"y": 160,
"wires": [
[
"demo_link_reactor_dash",
"demo_link_process_out_treatment",
"demo_fn_nh4_profile_extract"
],
[
"demo_reactor_z4"
]
]
},
{
"id": "demo_fn_reactor_z4_tag",
"type": "function",
"z": "demo_tab_treatment",
"name": "Tag Reactor Z4",
"func": "const clone = (input) => {\n const out = { ...input };\n if (input && typeof input.payload === 'object' && input.payload !== null) {\n out.payload = JSON.parse(JSON.stringify(input.payload));\n }\n return out;\n};\nconst tagged = clone(msg);\ntagged.zoneLabel = 'Z4';\ntagged.zoneVolume = 500;\ntagged.zoneIndex = 4;\nif (msg.topic === 'Fluent') {\n return [tagged, clone(msg)];\n}\nreturn [tagged, null];",
"outputs": 2,
"x": 1740,
"y": 160,
"wires": [
[
"demo_link_reactor_dash",
"demo_link_process_out_treatment",
"demo_link_overview_reactor_out",
"demo_fn_nh4_profile_extract",
"demo_fn_aeration_stage"
],
[
"demo_fn_sample_z4_probe"
]
]
},
{
"id": "demo_fn_sample_z2_probe",
"type": "function",
"z": "demo_tab_treatment",
"name": "Sample Z2 DO Probe",
"func": "if (msg.topic !== 'Fluent' || !Array.isArray(msg.payload?.C)) return null;\nconst value = Number(msg.payload.C[0]);\nif (!Number.isFinite(value)) return null;\nreturn { topic: 'measurement', payload: value, timestamp: msg.timestamp || Date.now() };",
"outputs": 1,
"x": 1320,
"y": 100,
"wires": [
[
"demo_meas_do"
]
]
},
{
"id": "demo_fn_sample_z4_probe",
"type": "function",
"z": "demo_tab_treatment",
"name": "Sample Z4 NH4 Probe",
"func": "if (msg.topic !== 'Fluent' || !Array.isArray(msg.payload?.C)) return null;\nconst value = Number(msg.payload.C[3]);\nif (!Number.isFinite(value)) return null;\nreturn { topic: 'measurement', payload: value, timestamp: msg.timestamp || Date.now() };",
"outputs": 1,
"x": 1760,
"y": 100,
"wires": [
[
"demo_meas_nh4"
]
]
},
{
"id": "demo_fn_aeration_stage",
"type": "function",
"z": "demo_tab_treatment",
"name": "Stage Aeration By NH4",
"func": "if (msg.topic !== 'Fluent' || !Array.isArray(msg.payload?.C)) return null;\nconst now = Number(msg.timestamp || Date.now());\nconst nh4 = Number(msg.payload.C[3]);\nconst NH4_HIGH = 2.0;\nconst NH4_LOW = 1.2;\nconst UP_DELAY_MS = 30 * 60 * 1000;\nconst DOWN_DELAY_MS = 60 * 60 * 1000;\n// Balanced staged aeration: Z4 carries the base nitrification load, Z3 only assists once Z4 NH4 stays high.\nconst stages = [\n [0, 40],\n [0, 55],\n [10, 70],\n [20, 85],\n];\nlet state = context.get('aerationState') || { stage: 0, highSince: null, lowSince: null };\nif (Number.isFinite(nh4) && nh4 > NH4_HIGH) {\n state.lowSince = null;\n state.highSince = state.highSince ?? now;\n if (state.stage < stages.length - 1 && now - state.highSince >= UP_DELAY_MS) {\n state.stage += 1;\n state.highSince = now;\n }\n} else if (Number.isFinite(nh4) && nh4 < NH4_LOW) {\n state.highSince = null;\n state.lowSince = state.lowSince ?? now;\n if (state.stage > 0 && now - state.lowSince >= DOWN_DELAY_MS) {\n state.stage -= 1;\n state.lowSince = now;\n }\n} else {\n state.highSince = null;\n state.lowSince = null;\n}\ncontext.set('aerationState', state);\nconst [z3Flow, z4Flow] = stages[state.stage];\nconst status = `Z4 NH4 ${nh4.toFixed(2)} mg/L -> diffuser stage ${state.stage + 1}/${stages.length} | Z3 ${z3Flow} | Z4 ${z4Flow}`;\nnode.status({ fill: state.stage > 1 ? 'yellow' : 'green', shape: 'dot', text: status });\nreturn [\n { topic: 'air_flow', payload: z3Flow, zoneLabel: 'Z3', diffuserStage: state.stage + 1, timestamp: now },\n { topic: 'air_flow', payload: z4Flow, zoneLabel: 'Z4', diffuserStage: state.stage + 1, timestamp: now },\n];",
"outputs": 2,
"x": 1760,
"y": 160,
"wires": [
[
"demo_diffuser_z3"
],
[
"demo_diffuser_z4"
]
]
},
{
"id": "demo_diffuser_z3",
"type": "diffuser",
"z": "demo_tab_treatment",
"name": "Diffuser Z3",
"number": 1,
"i_elements": 4,
"i_diff_density": 2.4,
"i_m_water": 4.5,
"alfaf": 0.7,
"i_zone_volume": 500,
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"enableLog": false,
"logLevel": "error",
"x": 2020,
"y": 120,
"wires": [
[
"demo_link_process_out_treatment"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_reactor_z3"
],
[]
]
},
{
"id": "demo_diffuser_z4",
"type": "diffuser",
"z": "demo_tab_treatment",
"name": "Diffuser Z4",
"number": 1,
"i_elements": 4,
"i_diff_density": 2.4,
"i_m_water": 4.5,
"alfaf": 0.7,
"i_zone_volume": 500,
"processOutputFormat": "process",
"dbaseOutputFormat": "influxdb",
"enableLog": false,
"logLevel": "error",
"x": 2020,
"y": 200,
"wires": [
[
"demo_link_process_out_treatment"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_reactor_z4"
],
[]
]
},
{
"id": "demo_inj_reactor_tick",
"type": "inject",
"z": "demo_tab_treatment",
"name": "Reactor tick (2s)",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"vt": "str"
}
],
"topic": "clock",
"payload": "",
"payloadType": "date",
"repeat": "2",
"once": false,
"onceDelay": "4",
"x": 200,
"y": 120,
"wires": [
[
"demo_reactor_z1",
"demo_reactor_z2",
"demo_reactor_z3",
"demo_reactor_z4"
]
],
"repeatType": "interval",
"crontab": ""
},
{
"id": "demo_settler",
"type": "settler",
"z": "demo_tab_treatment",
"name": "Settler S1",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "downstream",
"x": 1060,
"y": 320,
"wires": [
[
"demo_link_process_out_treatment",
"demo_fn_ras_filter",
"demo_fn_sample_effluent_probes"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_dbg_registration"
]
],
"positionIcon": "←"
},
{
"id": "demo_fn_sample_effluent_probes",
"type": "function",
"z": "demo_tab_treatment",
"name": "Sample Effluent Probes",
"func": "if (msg.topic !== 'Fluent' || !Array.isArray(msg.payload?.C)) return null;\nconst C = msg.payload.C;\nconst now = msg.timestamp || Date.now();\nconst sample = (index) => {\n const value = Number(C[index]);\n return Number.isFinite(value) ? { topic: 'measurement', payload: value, timestamp: now } : null;\n};\nreturn [sample(0), sample(3), sample(5), sample(12)];",
"outputs": 4,
"x": 1250,
"y": 320,
"wires": [
[
"demo_meas_eff_do"
],
[
"demo_meas_eff_nh4"
],
[
"demo_meas_eff_no3"
],
[
"demo_meas_eff_tss"
]
]
},
{
"id": "demo_monster",
"type": "monster",
"z": "demo_tab_treatment",
"name": "Monster Effluent",
"samplingtime": 24,
"minvolume": 5,
"maxweight": 23,
"emptyWeightBucket": 8.3,
"nominalFlowMin": 100,
"flowMax": 500,
"maxRainRef": 10,
"minSampleIntervalSec": 60,
"aquon_sample_name": "EVOLV-DEMO",
"uuid": "monster-eff-001",
"supplier": "Hach",
"category": "sampler",
"assetType": "flow-proportional",
"model": "AS950",
"unit": "m3/h",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "downstream",
"x": 1060,
"y": 440,
"wires": [
[
"demo_link_process_out_treatment"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_dbg_registration"
]
],
"positionIcon": "←"
},
{
"id": "demo_inj_monster_flow",
"type": "inject",
"z": "demo_tab_treatment",
"name": "Monster flow (10s)",
"props": [
{
"p": "payload"
}
],
"payload": "",
"payloadType": "date",
"repeat": "",
"once": false,
"onceDelay": "6",
"x": 200,
"y": 560,
"wires": [
[
"demo_fn_monster_flow"
]
],
"repeatType": "interval",
"crontab": "",
"d": true
},
{
"id": "demo_fn_monster_flow",
"type": "function",
"z": "demo_tab_treatment",
"name": "Sim effluent flow",
"func": "const flow = 200 + (Math.random() - 0.5) * 60;\nmsg.topic = 'input_q';\nmsg.payload = { value: Math.round(flow), unit: 'm3/h' };\nreturn msg;",
"outputs": 1,
"x": 400,
"y": 560,
"wires": [
[
"demo_monster"
]
],
"d": true
},
{
"id": "demo_dashapi",
"type": "dashboardapi",
"z": "demo_tab_wwtp",
"name": "Dashboard Generator",
"protocol": "http",
"host": "grafana",
"port": 3000,
"bearerToken": "",
"defaultBucket": "telemetry",
"enableLog": true,
"logLevel": "info",
"x": 1100,
"y": 1100,
"wires": [
[
"demo_fn_prep_grafana"
],
[],
[]
]
},
{
"id": "demo_http_grafana",
"type": "http request",
"z": "demo_tab_wwtp",
"name": "POST to Grafana",
"method": "use",
"ret": "txt",
"paytoqs": false,
"url": "",
"x": 1350,
"y": 1100,
"wires": [
[
"demo_dbg_grafana"
]
],
"persist": false
},
{
"id": "demo_dbg_grafana",
"type": "debug",
"z": "demo_tab_wwtp",
"name": "Grafana response",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "statusCode",
"statusType": "msg",
"x": 1500,
"y": 1100,
"wires": []
},
{
"id": "demo_comment_telemetry",
"type": "comment",
"z": "demo_tab_wwtp",
"name": "=== TELEMETRY (InfluxDB via HTTP) ===",
"info": "",
"x": 1300,
"y": 40
},
{
"id": "demo_link_influx_out",
"type": "link out",
"z": "demo_tab_wwtp",
"name": "→ InfluxDB",
"mode": "link",
"links": [
"demo_link_influx_in"
],
"x": 1135,
"y": 500
},
{
"id": "demo_link_influx_in",
"type": "link in",
"z": "demo_tab_wwtp",
"name": "← InfluxDB",
"links": [
"demo_link_influx_out_west",
"demo_link_influx_out_north",
"demo_link_influx_out_south",
"demo_link_influx_out_treatment",
"demo_link_influx_out"
],
"x": 1175,
"y": 100,
"wires": [
[
"demo_fn_influx_convert"
]
]
},
{
"id": "demo_fn_influx_convert",
"type": "function",
"z": "demo_tab_wwtp",
"name": "→ InfluxDB Line Protocol",
"func": "// Convert EVOLV Port 1 telemetry to InfluxDB v2 line protocol\nconst p = msg.payload;\nif (!p || !p.measurement || !p.fields) return null;\n\n// Escape special chars for line protocol\nconst esc = (s) => String(s).replace(/,/g, '\\\\,').replace(/ /g, '\\\\ ').replace(/=/g, '\\\\=');\n\n// Build tag string\nconst tags = Object.entries(p.tags || {})\n .filter(([k, v]) => v !== undefined && v !== null && v !== '')\n .map(([k, v]) => `${esc(k)}=${esc(v)}`)\n .join(',');\n\n// Build field string\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 });\n\nif (fieldPairs.length === 0) return null;\n\nconst measurement = esc(p.measurement);\nconst tagPart = tags ? `,${tags}` : '';\nconst timestamp = Date.now() * 1000000; // nanoseconds\n\nmsg.payload = `${measurement}${tagPart} ${fieldPairs.join(',')} ${timestamp}`;\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,
"x": 1350,
"y": 100,
"wires": [
[
"demo_http_influx"
]
]
},
{
"id": "demo_http_influx",
"type": "http request",
"z": "demo_tab_wwtp",
"name": "Write InfluxDB",
"method": "use",
"ret": "txt",
"paytoqs": false,
"url": "",
"x": 1560,
"y": 100,
"wires": [
[
"demo_fn_influx_count"
]
]
},
{
"id": "demo_fn_influx_count",
"type": "function",
"z": "demo_tab_wwtp",
"name": "Count writes",
"func": "const count = (global.get('influx_writes') || 0) + 1;\nglobal.set('influx_writes', count);\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',text:`ERR ${errors+1} / ${count} writes`});\n} else {\n node.status({fill:'green',shape:'dot',text:`${count} writes (${errors} errors)`});\n}\nreturn null;",
"outputs": 1,
"x": 1740,
"y": 100,
"wires": [
[]
]
},
{
"id": "demo_comment_process_out",
"type": "comment",
"z": "demo_tab_wwtp",
"name": "=== PROCESS DEBUG ===",
"info": "",
"x": 1300,
"y": 160
},
{
"id": "demo_link_process_out",
"type": "link out",
"z": "demo_tab_wwtp",
"name": "→ Process debug",
"mode": "link",
"links": [
"demo_link_process_in"
],
"x": 1135,
"y": 540
},
{
"id": "demo_link_process_in",
"type": "link in",
"z": "demo_tab_wwtp",
"name": "← Process data",
"links": [
"demo_link_process_out_west",
"demo_link_process_out_north",
"demo_link_process_out_south",
"demo_link_process_out_treatment",
"demo_link_process_out"
],
"x": 1175,
"y": 200,
"wires": [
[
"demo_dbg_process"
]
]
},
{
"id": "demo_dbg_process",
"type": "debug",
"z": "demo_tab_wwtp",
"name": "All Process Data",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "topic",
"statusType": "msg",
"x": 1360,
"y": 200,
"wires": []
},
{
"id": "demo_dbg_registration",
"type": "debug",
"z": "demo_tab_wwtp",
"name": "Registration events",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "msg",
"x": 1370,
"y": 240,
"wires": []
},
{
"id": "demo_link_ps_west_dash",
"type": "link out",
"z": "demo_tab_ps_west",
"name": "→ PS West Dashboard",
"mode": "link",
"links": [
"demo_link_ps_west_dash_in"
],
"x": 1080,
"y": 220
},
{
"id": "demo_link_ps_north_dash",
"type": "link out",
"z": "demo_tab_ps_north",
"name": "→ PS North Dashboard",
"mode": "link",
"links": [
"demo_link_ps_north_dash_in"
],
"x": 1080,
"y": 200
},
{
"id": "demo_link_ps_south_dash",
"type": "link out",
"z": "demo_tab_ps_south",
"name": "→ PS South Dashboard",
"mode": "link",
"links": [
"demo_link_ps_south_dash_in"
],
"x": 1080,
"y": 200
},
{
"id": "demo_link_ps_west_dash_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← PS West Data",
"links": [
"demo_link_ps_west_dash"
],
"x": 75,
"y": 100,
"wires": [
[
"demo_fn_ps_west_parse"
]
]
},
{
"id": "demo_link_ps_north_dash_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← PS North Data",
"links": [
"demo_link_ps_north_dash"
],
"x": 75,
"y": 200,
"wires": [
[
"demo_fn_ps_north_parse"
]
]
},
{
"id": "demo_link_ps_south_dash_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← PS South Data",
"links": [
"demo_link_ps_south_dash"
],
"x": 75,
"y": 300,
"wires": [
[
"demo_fn_ps_south_parse"
]
]
},
{
"id": "demo_fn_ps_west_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse PS West",
"func": "const p = msg.payload || {};\nconst cache = context.get('c') || {};\nconst keys = Object.keys(p);\nconst pickPrefix = (prefixes) => { for (const pfx of prefixes) { const k = keys.find(k => k.startsWith(pfx)); if (k) { const v = Number(p[k]); if (Number.isFinite(v)) return v; } } return null; };\nconst pickStr = (k, fb) => p[k] !== undefined ? String(p[k]) : fb;\n\nconst level = pickPrefix(['level.predicted.atequipment','level.measured.atequipment']);\nconst volume = pickPrefix(['volume.predicted.atequipment']);\nconst netFlow = pickPrefix(['netFlowRate.predicted.atequipment']);\nconst fillPct = pickPrefix(['volumePercent.predicted.atequipment']);\nconst direction = pickStr('direction', cache.direction || '?');\nconst timeleft = pickPrefix(['timeleft']);\n\nif (level !== null) cache.level = level;\nif (volume !== null) cache.volume = volume;\nif (netFlow !== null) cache.netFlow = netFlow;\nif (fillPct !== null) cache.fillPct = fillPct;\nif (timeleft !== null) cache.timeleft = timeleft;\ncache.direction = direction;\ncontext.set('c', cache);\n\nconst dirArrow = cache.direction === 'filling' ? '\\u2191' : cache.direction === 'emptying' ? '\\u2193' : '\\u2014';\nconst flowStr = cache.netFlow !== undefined ? Math.abs(cache.netFlow).toFixed(0) + ' m\\u00b3/h' : '';\nconst timeStr = cache.timeleft !== undefined && cache.timeleft > 0 ? Math.round(cache.timeleft) + ' min' : '';\nconst parts = [dirArrow + ' ' + (cache.direction || ''), flowStr, timeStr].filter(s => s && s.trim());\n\nconst now = Date.now();\nreturn [\n cache.level !== undefined ? {topic:'PS West Level', payload: cache.level, timestamp: now} : null,\n cache.volume !== undefined ? {topic:'PS West Volume', payload: cache.volume, timestamp: now} : null,\n cache.netFlow !== undefined ? {topic:'PS West Net Flow', payload: cache.netFlow, timestamp: now} : null,\n {topic:'PS West Status', payload: parts.join(' | ')},\n cache.level !== undefined ? {payload: Number(cache.level.toFixed(2))} : null,\n cache.fillPct !== undefined ? {payload: Number(cache.fillPct.toFixed(1))} : null\n];",
"outputs": 6,
"x": 250,
"y": 100,
"wires": [
[
"demo_chart_ps_level"
],
[
"demo_chart_ps_volume"
],
[
"demo_chart_ps_flow"
],
[
"demo_text_ps_west"
],
[
"demo_gauge_tank_west"
],
[
"demo_gauge_fill_west"
]
]
},
{
"id": "demo_fn_ps_north_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse PS North",
"func": "const p = msg.payload || {};\nconst cache = context.get('c') || {};\nconst keys = Object.keys(p);\nconst pickPrefix = (prefixes) => { for (const pfx of prefixes) { const k = keys.find(k => k.startsWith(pfx)); if (k) { const v = Number(p[k]); if (Number.isFinite(v)) return v; } } return null; };\nconst pickStr = (k, fb) => p[k] !== undefined ? String(p[k]) : fb;\n\nconst level = pickPrefix(['level.predicted.atequipment','level.measured.atequipment']);\nconst volume = pickPrefix(['volume.predicted.atequipment']);\nconst netFlow = pickPrefix(['netFlowRate.predicted.atequipment']);\nconst fillPct = pickPrefix(['volumePercent.predicted.atequipment']);\nconst direction = pickStr('direction', cache.direction || '?');\nconst timeleft = pickPrefix(['timeleft']);\n\nif (level !== null) cache.level = level;\nif (volume !== null) cache.volume = volume;\nif (netFlow !== null) cache.netFlow = netFlow;\nif (fillPct !== null) cache.fillPct = fillPct;\nif (timeleft !== null) cache.timeleft = timeleft;\ncache.direction = direction;\ncontext.set('c', cache);\n\nconst dirArrow = cache.direction === 'filling' ? '\\u2191' : cache.direction === 'emptying' ? '\\u2193' : '\\u2014';\nconst flowStr = cache.netFlow !== undefined ? Math.abs(cache.netFlow).toFixed(0) + ' m\\u00b3/h' : '';\nconst timeStr = cache.timeleft !== undefined && cache.timeleft > 0 ? Math.round(cache.timeleft) + ' min' : '';\nconst parts = [dirArrow + ' ' + (cache.direction || ''), flowStr, timeStr].filter(s => s && s.trim());\n\nconst now = Date.now();\nreturn [\n cache.level !== undefined ? {topic:'PS North Level', payload: cache.level, timestamp: now} : null,\n cache.volume !== undefined ? {topic:'PS North Volume', payload: cache.volume, timestamp: now} : null,\n cache.netFlow !== undefined ? {topic:'PS North Net Flow', payload: cache.netFlow, timestamp: now} : null,\n {topic:'PS North Status', payload: parts.join(' | ')},\n cache.level !== undefined ? {payload: Number(cache.level.toFixed(2))} : null,\n cache.fillPct !== undefined ? {payload: Number(cache.fillPct.toFixed(1))} : null\n];",
"outputs": 6,
"x": 250,
"y": 200,
"wires": [
[
"demo_chart_ps_level"
],
[
"demo_chart_ps_volume"
],
[
"demo_chart_ps_flow"
],
[
"demo_text_ps_north"
],
[
"demo_gauge_tank_north"
],
[
"demo_gauge_fill_north"
]
]
},
{
"id": "demo_fn_ps_south_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse PS South",
"func": "const p = msg.payload || {};\nconst cache = context.get('c') || {};\nconst keys = Object.keys(p);\nconst pickPrefix = (prefixes) => { for (const pfx of prefixes) { const k = keys.find(k => k.startsWith(pfx)); if (k) { const v = Number(p[k]); if (Number.isFinite(v)) return v; } } return null; };\nconst pickStr = (k, fb) => p[k] !== undefined ? String(p[k]) : fb;\n\nconst level = pickPrefix(['level.predicted.atequipment','level.measured.atequipment']);\nconst volume = pickPrefix(['volume.predicted.atequipment']);\nconst netFlow = pickPrefix(['netFlowRate.predicted.atequipment']);\nconst fillPct = pickPrefix(['volumePercent.predicted.atequipment']);\nconst direction = pickStr('direction', cache.direction || '?');\nconst timeleft = pickPrefix(['timeleft']);\n\nif (level !== null) cache.level = level;\nif (volume !== null) cache.volume = volume;\nif (netFlow !== null) cache.netFlow = netFlow;\nif (fillPct !== null) cache.fillPct = fillPct;\nif (timeleft !== null) cache.timeleft = timeleft;\ncache.direction = direction;\ncontext.set('c', cache);\n\nconst dirArrow = cache.direction === 'filling' ? '\\u2191' : cache.direction === 'emptying' ? '\\u2193' : '\\u2014';\nconst flowStr = cache.netFlow !== undefined ? Math.abs(cache.netFlow).toFixed(0) + ' m\\u00b3/h' : '';\nconst timeStr = cache.timeleft !== undefined && cache.timeleft > 0 ? Math.round(cache.timeleft) + ' min' : '';\nconst parts = [dirArrow + ' ' + (cache.direction || ''), flowStr, timeStr].filter(s => s && s.trim());\n\nconst now = Date.now();\nreturn [\n cache.level !== undefined ? {topic:'PS South Level', payload: cache.level, timestamp: now} : null,\n cache.volume !== undefined ? {topic:'PS South Volume', payload: cache.volume, timestamp: now} : null,\n cache.netFlow !== undefined ? {topic:'PS South Net Flow', payload: cache.netFlow, timestamp: now} : null,\n {topic:'PS South Status', payload: parts.join(' | ')},\n cache.level !== undefined ? {payload: Number(cache.level.toFixed(2))} : null,\n cache.fillPct !== undefined ? {payload: Number(cache.fillPct.toFixed(1))} : null\n];",
"outputs": 6,
"x": 250,
"y": 300,
"wires": [
[
"demo_chart_ps_level"
],
[
"demo_chart_ps_volume"
],
[
"demo_chart_ps_flow"
],
[
"demo_text_ps_south"
],
[
"demo_gauge_tank_south"
],
[
"demo_gauge_fill_south"
]
]
},
{
"id": "demo_chart_ps_level",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_flows",
"name": "Basin Levels",
"label": "Basin Level (m)",
"order": 1,
"width": "4",
"height": "5",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "m",
"removeOlder": "2",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 510,
"y": 100,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#0094ce",
"#FF7F0E",
"#2CA02C",
"#D62728",
"#A347E1",
"#D62728",
"#FF9896",
"#9467BD",
"#C5B0D5"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_chart_ps_volume",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_flows",
"name": "Basin Volumes",
"label": "Volume (m³)",
"order": 2,
"width": "4",
"height": "5",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "m³",
"removeOlder": "2",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 510,
"y": 200,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#0094ce",
"#FF7F0E",
"#2CA02C",
"#D62728",
"#A347E1",
"#D62728",
"#FF9896",
"#9467BD",
"#C5B0D5"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_chart_ps_flow",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_flows",
"name": "Net Flow Rates",
"label": "Net Flow (m³/h)",
"order": 3,
"width": "4",
"height": "5",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "m³/h",
"removeOlder": "2",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 510,
"y": 300,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#0094ce",
"#FF7F0E",
"#2CA02C",
"#D62728",
"#A347E1",
"#D62728",
"#FF9896",
"#9467BD",
"#C5B0D5"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_text_ps_west",
"type": "ui-text",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_ps_west",
"name": "West Status",
"label": "Status",
"order": 1,
"width": "4",
"height": "1",
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 510,
"y": 400,
"wires": [],
"className": ""
},
{
"id": "demo_text_ps_north",
"type": "ui-text",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_ps_north",
"name": "North Status",
"label": "Status",
"order": 1,
"width": "4",
"height": "1",
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 510,
"y": 440,
"wires": [],
"className": ""
},
{
"id": "demo_text_ps_south",
"type": "ui-text",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_ps_south",
"name": "South Status",
"label": "Status",
"order": 1,
"width": "4",
"height": "1",
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 510,
"y": 480,
"wires": [],
"className": ""
},
{
"id": "demo_link_reactor_dash",
"type": "link out",
"z": "demo_tab_treatment",
"name": "→ Reactor Dashboard",
"mode": "link",
"links": [
"demo_link_reactor_dash_in"
],
"x": 1020,
"y": 180
},
{
"id": "demo_link_meas_dash",
"type": "link out",
"z": "demo_tab_treatment",
"name": "→ Measurements Dashboard",
"mode": "link",
"links": [
"demo_link_meas_dash_in"
],
"x": 640,
"y": 130
},
{
"id": "demo_link_reactor_dash_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← Reactor Data",
"links": [
"demo_link_reactor_dash"
],
"x": 75,
"y": 550,
"wires": [
[
"demo_fn_reactor_parse"
]
]
},
{
"id": "demo_link_meas_dash_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← Measurements Data",
"links": [
"demo_link_meas_dash"
],
"x": 75,
"y": 700,
"wires": [
[
"demo_fn_meas_parse"
]
]
},
{
"id": "demo_fn_reactor_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse Reactor Zones",
"func": "if (msg.topic !== 'Fluent') return null;\nconst p = msg.payload || {};\nif (!Array.isArray(p.C)) return null;\nconst zone = msg.zoneLabel || 'Zone';\nconst volume = Number(msg.zoneVolume || 0);\nconst flow = Number(p.F || 0);\nconst hrtHours = flow > 0 ? (volume / flow) * 24 : 0;\nconst now = Date.now();\nconst round = (value, digits = 2) => {\n const n = Number(value);\n if (!Number.isFinite(n)) return null;\n const factor = 10 ** digits;\n return Math.round(n * factor) / factor;\n};\nconst doVal = round(p.C[0]);\nconst nh4Val = round(p.C[3]);\nconst no3Val = round(p.C[5]);\nconst ssVal = round(p.C[2]);\nconst tssVal = round(p.C[12]);\nconst xhVal = round(p.C[9]);\nconst xaVal = round(p.C[11]);\nconst flowVal = round(flow, 1);\nconst hrtVal = round(hrtHours, 2);\nconst build = (topic, payload) => payload == null ? null : { topic, payload, timestamp: now };\nconst summary = `${zone}: NH4 ${nh4Val?.toFixed(2) ?? 'n/a'} mg/L | NO3 ${no3Val?.toFixed(2) ?? 'n/a'} mg/L | DO ${doVal?.toFixed(2) ?? 'n/a'} mg/L | Flow ${flowVal?.toFixed(0) ?? 'n/a'} m3/d | HRT ${hrtVal?.toFixed(2) ?? 'n/a'} h`;\nreturn [\n build(`${zone} DO`, doVal),\n build(`${zone} NH4`, nh4Val),\n build(`${zone} NO3`, no3Val),\n build(`${zone} Readily COD`, ssVal),\n build(`${zone} TSS`, tssVal),\n build(`${zone} Heterotrophs`, xhVal),\n build(`${zone} Nitrifiers`, xaVal),\n build(`${zone} HRT`, hrtVal),\n build(`${zone} Flow`, flowVal),\n { payload: summary }\n];",
"outputs": 10,
"x": 250,
"y": 550,
"wires": [
[
"demo_chart_reactor_do"
],
[
"demo_chart_reactor_n"
],
[
"demo_chart_reactor_no3"
],
[
"demo_chart_reactor_cod"
],
[
"demo_chart_reactor_cod"
],
[
"demo_chart_reactor_biomass"
],
[
"demo_chart_reactor_biomass"
],
[
"demo_chart_reactor_flow"
],
[
"demo_chart_reactor_flow"
],
[
"demo_text_reactor"
]
]
},
{
"id": "demo_fn_meas_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse Measurements",
"func": "const p = msg.payload || {};\nconst topic = msg.topic || '';\nconst now = Date.now();\nconst val = Number(p.mAbs);\nif (!Number.isFinite(val)) return null;\n\nlet label = topic;\nif (topic.includes('flow')) label = 'FT-001 Influent Probe';\nelse if (topic.includes('do') || topic.includes('DO')) label = 'DO Probe Z2 Mid';\nelse if (topic.includes('nh4') || topic.includes('NH4')) label = 'NH4 Probe Z4 Effluent';\n\nreturn [\n {topic: label, payload: Math.round(val*100)/100, timestamp: now},\n {topic: label + ' %', payload: Math.round(Number(p.mPercent)*10)/10, timestamp: now}\n];",
"outputs": 2,
"x": 250,
"y": 700,
"wires": [
[
"demo_chart_meas_abs"
],
[
"demo_chart_meas_pct"
]
]
},
{
"id": "demo_chart_reactor_do",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_reactor",
"name": "Reactor DO",
"label": "Dissolved Oxygen (mg/L)",
"order": 1,
"width": "6",
"height": "4",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "mg/L",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 510,
"y": 520,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#0094ce",
"#FF7F0E",
"#2CA02C"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_chart_reactor_n",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_reactor",
"name": "Reactor NH4",
"label": "Zone NH4 (mg/L)",
"order": 2,
"width": "6",
"height": "4",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "mg/L",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 510,
"y": 560,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#FF7F0E",
"#D62728",
"#9467BD",
"#8C564B"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_chart_reactor_no3",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_reactor",
"name": "Reactor NO3",
"label": "Zone NO3 (mg/L)",
"order": 3,
"width": "6",
"height": "4",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "mg/L",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 510,
"y": 600,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#2CA02C",
"#1F77B4",
"#17BECF",
"#BCBD22"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_chart_reactor_cod",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_reactor",
"name": "Reactor Carbon/Solids",
"label": "Carbon & Solids (mg/L)",
"order": 4,
"width": "6",
"height": "4",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "mg/L",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 510,
"y": 640,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#9467BD",
"#D62728"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_chart_reactor_biomass",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_reactor",
"name": "Reactor Biomass",
"label": "Biomass (mg/L)",
"order": 5,
"width": "6",
"height": "4",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "mg/L",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 520,
"y": 680,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#8C564B",
"#17BECF"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_chart_reactor_flow",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_reactor",
"name": "Reactor Flow/HRT",
"label": "Zone Flow and HRT",
"order": 6,
"width": "6",
"height": "4",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "value",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 520,
"y": 720,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#1F77B4",
"#FF7F0E",
"#2CA02C",
"#D62728"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_text_reactor",
"type": "ui-text",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_reactor",
"name": "Reactor Zone Status",
"label": "Zone Summary",
"order": 7,
"width": "12",
"height": "2",
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 510,
"y": 760,
"wires": [],
"className": ""
},
{
"id": "demo_chart_meas_abs",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_measurements",
"name": "Probe Values",
"label": "Local Probe Readings",
"order": 1,
"width": "6",
"height": "4",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 510,
"y": 700,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#0094ce",
"#FF7F0E",
"#2CA02C"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_chart_meas_pct",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_measurements",
"name": "Probe Signal %",
"label": "Local Probe Range (%)",
"order": 2,
"width": "6",
"height": "4",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "%",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 510,
"y": 740,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#0094ce",
"#FF7F0E",
"#2CA02C"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_text_influx_status",
"type": "ui-text",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_influx_status",
"name": "InfluxDB Status",
"label": "Telemetry Pipeline",
"order": 1,
"width": "12",
"height": "1",
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 510,
"y": 800,
"wires": [],
"className": ""
},
{
"id": "demo_link_influx_status_out",
"type": "link out",
"z": "demo_tab_wwtp",
"name": "→ InfluxDB Status Dashboard",
"mode": "link",
"links": [
"demo_link_influx_status_in"
],
"x": 1940,
"y": 100
},
{
"id": "demo_link_influx_status_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← InfluxDB Status",
"links": [
"demo_link_influx_status_out"
],
"x": 75,
"y": 800,
"wires": [
[
"demo_fn_influx_status_parse"
]
]
},
{
"id": "demo_fn_influx_status_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse InfluxDB Status",
"func": "const writes = global.get('influx_writes') || 0;\nconst errors = global.get('influx_errors') || 0;\nmsg.payload = `Writes: ${writes} | Errors: ${errors} | Rate: ~${Math.round(writes / Math.max(1, (Date.now() - (global.get('influx_start') || Date.now())) / 1000))}/s`;\nif (!global.get('influx_start')) global.set('influx_start', Date.now());\nreturn msg;",
"outputs": 1,
"x": 280,
"y": 800,
"wires": [
[
"demo_text_influx_status"
]
]
},
{
"id": "demo_inj_influx_status",
"type": "inject",
"z": "demo_tab_dashboard",
"name": "Status poll (5s)",
"props": [
{
"p": "payload"
}
],
"payload": "",
"payloadType": "date",
"repeat": "5",
"once": false,
"onceDelay": "10",
"repeatType": "interval",
"crontab": "",
"x": 100,
"y": 840,
"wires": [
[
"demo_fn_influx_status_parse"
]
]
},
{
"id": "demo_comment_north_outflow",
"type": "comment",
"z": "demo_tab_ps_north",
"name": "--- PS North Outflow Measurement ---",
"info": "",
"x": 200,
"y": 320
},
{
"id": "demo_meas_ft_n1",
"type": "measurement",
"z": "demo_tab_ps_north",
"name": "FT-N1 (North Outflow)",
"scaling": true,
"i_min": 0,
"i_max": 200,
"i_offset": 0,
"o_min": 0,
"o_max": 200,
"smooth_method": "mean",
"count": 3,
"simulator": false,
"uuid": "ft-n1-001",
"supplier": "Endress+Hauser",
"category": "sensor",
"assetType": "flow",
"model": "Promag-W400",
"unit": "m3/h",
"assetTagNumber": "FT-N1",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "downstream",
"positionIcon": "←",
"hasDistance": false,
"x": 560,
"y": 340,
"wires": [
[
"demo_link_process_out_north"
],
[
"demo_link_influx_out_north"
],
[
"demo_ps_north"
]
]
},
{
"id": "demo_comment_effluent_meas",
"type": "comment",
"z": "demo_tab_treatment",
"name": "=== EFFLUENT MEASUREMENTS (downstream of settler) ===",
"info": "",
"x": 80,
"y": 520
},
{
"id": "demo_meas_eff_flow",
"type": "measurement",
"z": "demo_tab_treatment",
"name": "FT-002 (Effluent Flow)",
"scaling": true,
"i_min": 0,
"i_max": 500,
"i_offset": 0,
"o_min": 0,
"o_max": 500,
"smooth_method": "mean",
"count": 5,
"simulator": false,
"uuid": "ft-002",
"supplier": "Endress+Hauser",
"category": "sensor",
"assetType": "flow",
"model": "Promag-W400",
"unit": "m3/h",
"assetTagNumber": "FT-002",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "downstream",
"positionIcon": "←",
"hasDistance": false,
"x": 1280,
"y": 320,
"wires": [
[
"demo_link_eff_meas_dash",
"demo_link_process_out_treatment"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_monster"
]
]
},
{
"id": "demo_meas_eff_do",
"type": "measurement",
"z": "demo_tab_treatment",
"name": "DO-002 (Effluent DO)",
"scaling": true,
"i_min": 0,
"i_max": 20,
"i_offset": 0,
"o_min": 0,
"o_max": 20,
"smooth_method": "mean",
"count": 3,
"simulator": false,
"uuid": "do-002",
"supplier": "Hach",
"category": "sensor",
"assetType": "dissolved-oxygen",
"model": "LDO2",
"unit": "mg/L",
"assetTagNumber": "DO-002",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "downstream",
"positionIcon": "←",
"hasDistance": false,
"x": 1280,
"y": 380,
"wires": [
[
"demo_link_eff_meas_dash",
"demo_link_process_out_treatment"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_settler"
]
]
},
{
"id": "demo_meas_eff_nh4",
"type": "measurement",
"z": "demo_tab_treatment",
"name": "NH4-002 (Effluent NH4)",
"scaling": true,
"i_min": 0,
"i_max": 50,
"i_offset": 0,
"o_min": 0,
"o_max": 50,
"smooth_method": "mean",
"count": 3,
"simulator": false,
"uuid": "nh4-002",
"supplier": "Hach",
"category": "sensor",
"assetType": "ammonium",
"model": "Amtax-sc",
"unit": "mg/L",
"assetTagNumber": "NH4-002",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "downstream",
"positionIcon": "←",
"hasDistance": false,
"x": 1280,
"y": 440,
"wires": [
[
"demo_link_eff_meas_dash",
"demo_link_process_out_treatment",
"demo_link_overview_eff_out"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_settler"
]
]
},
{
"id": "demo_meas_eff_no3",
"type": "measurement",
"z": "demo_tab_treatment",
"name": "NO3-001 (Effluent NO3)",
"scaling": true,
"i_min": 0,
"i_max": 30,
"i_offset": 0,
"o_min": 0,
"o_max": 30,
"smooth_method": "mean",
"count": 3,
"simulator": false,
"uuid": "no3-001",
"supplier": "Hach",
"category": "sensor",
"assetType": "nitrate",
"model": "Nitratax-sc",
"unit": "mg/L",
"assetTagNumber": "NO3-001",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "downstream",
"positionIcon": "←",
"hasDistance": false,
"x": 1280,
"y": 500,
"wires": [
[
"demo_link_eff_meas_dash",
"demo_link_process_out_treatment"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_settler"
]
]
},
{
"id": "demo_meas_eff_tss",
"type": "measurement",
"z": "demo_tab_treatment",
"name": "TSS-001 (Effluent TSS)",
"scaling": true,
"i_min": 0,
"i_max": 100,
"i_offset": 0,
"o_min": 0,
"o_max": 100,
"smooth_method": "mean",
"count": 3,
"simulator": false,
"uuid": "tss-001",
"supplier": "Hach",
"category": "sensor",
"assetType": "tss",
"model": "Solitax-sc",
"unit": "mg/L",
"assetTagNumber": "TSS-001",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "downstream",
"positionIcon": "←",
"hasDistance": false,
"x": 1280,
"y": 560,
"wires": [
[
"demo_link_eff_meas_dash",
"demo_link_process_out_treatment",
"demo_link_overview_eff_out"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_settler"
]
]
},
{
"id": "demo_ui_grp_eff_measurements",
"type": "ui-group",
"name": "Effluent Measurements",
"page": "demo_ui_page_treatment",
"width": "6",
"height": "1",
"order": 5,
"showTitle": true,
"className": ""
},
{
"id": "demo_link_eff_meas_dash",
"type": "link out",
"z": "demo_tab_treatment",
"name": "→ Effluent Meas Dashboard",
"mode": "link",
"links": [
"demo_link_eff_meas_dash_in"
],
"x": 1500,
"y": 440
},
{
"id": "demo_link_eff_meas_dash_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← Effluent Measurements",
"links": [
"demo_link_eff_meas_dash"
],
"x": 75,
"y": 900,
"wires": [
[
"demo_fn_eff_meas_parse"
]
]
},
{
"id": "demo_fn_eff_meas_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse Effluent Meas",
"func": "const p = msg.payload || {};\nconst topic = msg.topic || '';\nconst now = Date.now();\nconst val = Number(p.mAbs);\nif (!Number.isFinite(val)) return null;\n\nlet label = topic;\nif (topic.includes('FT-002') || topic.includes('flow')) label = 'FT-002 Eff Flow';\nelse if (topic.includes('DO-002') || topic.includes('dissolved')) label = 'DO-002 Eff DO';\nelse if (topic.includes('NH4-002') || topic.includes('ammonium')) label = 'NH4-002 Eff NH4';\nelse if (topic.includes('NO3-001') || topic.includes('nitrate')) label = 'NO3-001 Eff NO3';\nelse if (topic.includes('TSS-001') || topic.includes('tss')) label = 'TSS-001 Eff TSS';\n\nreturn [\n {topic: label, payload: Math.round(val*100)/100, timestamp: now},\n {topic: label + ' %', payload: Math.round(Number(p.mPercent)*10)/10, timestamp: now}\n];",
"outputs": 2,
"x": 280,
"y": 900,
"wires": [
[
"demo_chart_eff_abs"
],
[
"demo_chart_eff_pct"
]
]
},
{
"id": "demo_chart_eff_abs",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_eff_measurements",
"name": "Effluent Quality",
"label": "Effluent Quality",
"order": 1,
"width": "6",
"height": "4",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 510,
"y": 880,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#0094ce",
"#FF7F0E",
"#2CA02C",
"#D62728",
"#9467BD"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_chart_eff_pct",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_eff_measurements",
"name": "Effluent Range %",
"label": "Effluent Range (%)",
"order": 2,
"width": "6",
"height": "4",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "%",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 510,
"y": 920,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#0094ce",
"#FF7F0E",
"#2CA02C",
"#D62728",
"#9467BD"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_comment_pressure",
"type": "comment",
"z": "demo_tab_treatment",
"name": "=== PRESSURE MEASUREMENTS (per pumping station) ===",
"info": "",
"x": 80,
"y": 740
},
{
"id": "demo_fn_level_to_pressure_w",
"type": "function",
"z": "demo_tab_ps_west",
"name": "Level→Pressure (West)",
"func": "// Extract basin level from PS output and convert to hydrostatic pressure (mbar)\n// P = rho * g * h, rho=1000 kg/m3, g=9.81 m/s2\nconst p = msg.payload || {};\nconst keys = Object.keys(p);\nconst levelKey = keys.find(k => k.startsWith(\"level.predicted.atequipment\") || k.startsWith(\"level.measured.atequipment\"));\nif (!levelKey) return null;\nconst h = Number(p[levelKey]);\nif (!Number.isFinite(h)) return null;\nmsg.topic = \"measurement\";\nmsg.payload = Math.round(h * 98.1 * 10) / 10; // mbar\nreturn msg;",
"outputs": 1,
"x": 360,
"y": 420,
"wires": [
[
"demo_meas_pt_w_up"
]
]
},
{
"id": "demo_meas_pt_w_up",
"type": "measurement",
"z": "demo_tab_ps_west",
"name": "PT-W-UP (West Upstream)",
"scaling": true,
"i_offset": 0,
"smooth_method": "mean",
"count": 3,
"category": "sensor",
"assetType": "pressure",
"enableLog": false,
"logLevel": "error",
"positionIcon": "→",
"hasDistance": false,
"i_min": 0,
"i_max": 500,
"o_min": 0,
"o_max": 500,
"simulator": false,
"uuid": "pt-w-up-001",
"supplier": "Endress+Hauser",
"model": "Cerabar-PMC51",
"unit": "mbar",
"assetTagNumber": "PT-W-UP",
"positionVsParent": "upstream",
"x": 560,
"y": 420,
"wires": [
[
"demo_link_process_out_west"
],
[
"demo_link_influx_out_west"
],
[
"demo_pump_w1",
"demo_pump_w2"
]
]
},
{
"id": "demo_meas_pt_w_down",
"type": "measurement",
"z": "demo_tab_ps_west",
"name": "PT-W-DN (West Downstream)",
"scaling": true,
"i_offset": 0,
"smooth_method": "mean",
"count": 3,
"category": "sensor",
"assetType": "pressure",
"enableLog": false,
"logLevel": "error",
"positionIcon": "←",
"hasDistance": false,
"i_min": 800,
"i_max": 1500,
"o_min": 800,
"o_max": 1500,
"simulator": false,
"uuid": "pt-w-dn-001",
"supplier": "Endress+Hauser",
"model": "Cerabar-PMC51",
"unit": "mbar",
"assetTagNumber": "PT-W-DN",
"positionVsParent": "downstream",
"x": 560,
"y": 480,
"wires": [
[
"demo_link_process_out_west"
],
[
"demo_link_influx_out_west"
],
[
"demo_pump_w1",
"demo_pump_w2"
]
]
},
{
"id": "demo_fn_level_to_pressure_n",
"type": "function",
"z": "demo_tab_ps_north",
"name": "Level→Pressure (North)",
"func": "// Extract basin level from PS output and convert to hydrostatic pressure (mbar)\n// P = rho * g * h, rho=1000 kg/m3, g=9.81 m/s2\nconst p = msg.payload || {};\nconst keys = Object.keys(p);\nconst levelKey = keys.find(k => k.startsWith(\"level.predicted.atequipment\") || k.startsWith(\"level.measured.atequipment\"));\nif (!levelKey) return null;\nconst h = Number(p[levelKey]);\nif (!Number.isFinite(h)) return null;\nmsg.topic = \"measurement\";\nmsg.payload = Math.round(h * 98.1 * 10) / 10; // mbar\nreturn msg;",
"outputs": 1,
"x": 360,
"y": 420,
"wires": [
[
"demo_meas_pt_n_up"
]
]
},
{
"id": "demo_meas_pt_n_up",
"type": "measurement",
"z": "demo_tab_ps_north",
"name": "PT-N-UP (North Upstream)",
"scaling": true,
"i_offset": 0,
"smooth_method": "mean",
"count": 3,
"category": "sensor",
"assetType": "pressure",
"enableLog": false,
"logLevel": "error",
"positionIcon": "→",
"hasDistance": false,
"i_min": 0,
"i_max": 400,
"o_min": 0,
"o_max": 400,
"simulator": false,
"uuid": "pt-n-up-001",
"supplier": "Endress+Hauser",
"model": "Cerabar-PMC51",
"unit": "mbar",
"assetTagNumber": "PT-N-UP",
"positionVsParent": "upstream",
"x": 560,
"y": 420,
"wires": [
[
"demo_link_process_out_north"
],
[
"demo_link_influx_out_north"
],
[
"demo_pump_n1"
]
]
},
{
"id": "demo_meas_pt_n_down",
"type": "measurement",
"z": "demo_tab_ps_north",
"name": "PT-N-DN (North Downstream)",
"scaling": true,
"i_offset": 0,
"smooth_method": "mean",
"count": 3,
"category": "sensor",
"assetType": "pressure",
"enableLog": false,
"logLevel": "error",
"positionIcon": "←",
"hasDistance": false,
"i_min": 600,
"i_max": 1200,
"o_min": 600,
"o_max": 1200,
"simulator": true,
"uuid": "pt-n-dn-001",
"supplier": "Endress+Hauser",
"model": "Cerabar-PMC51",
"unit": "mbar",
"assetTagNumber": "PT-N-DN",
"positionVsParent": "downstream",
"x": 560,
"y": 480,
"wires": [
[
"demo_link_process_out_north"
],
[
"demo_link_influx_out_north"
],
[
"demo_pump_n1"
]
]
},
{
"id": "demo_fn_level_to_pressure_s",
"type": "function",
"z": "demo_tab_ps_south",
"name": "Level→Pressure (South)",
"func": "// Extract basin level from PS output and convert to hydrostatic pressure (mbar)\n// P = rho * g * h, rho=1000 kg/m3, g=9.81 m/s2\nconst p = msg.payload || {};\nconst keys = Object.keys(p);\nconst levelKey = keys.find(k => k.startsWith(\"level.predicted.atequipment\") || k.startsWith(\"level.measured.atequipment\"));\nif (!levelKey) return null;\nconst h = Number(p[levelKey]);\nif (!Number.isFinite(h)) return null;\nmsg.topic = \"measurement\";\nmsg.payload = Math.round(h * 98.1 * 10) / 10; // mbar\nreturn msg;",
"outputs": 1,
"x": 360,
"y": 380,
"wires": [
[
"demo_meas_pt_s_up"
]
]
},
{
"id": "demo_meas_pt_s_up",
"type": "measurement",
"z": "demo_tab_ps_south",
"name": "PT-S-UP (South Upstream)",
"scaling": true,
"i_offset": 0,
"smooth_method": "mean",
"count": 3,
"category": "sensor",
"assetType": "pressure",
"enableLog": false,
"logLevel": "error",
"positionIcon": "→",
"hasDistance": false,
"i_min": 0,
"i_max": 300,
"o_min": 0,
"o_max": 300,
"simulator": false,
"uuid": "pt-s-up-001",
"supplier": "Endress+Hauser",
"model": "Cerabar-PMC51",
"unit": "mbar",
"assetTagNumber": "PT-S-UP",
"positionVsParent": "upstream",
"x": 560,
"y": 380,
"wires": [
[
"demo_link_process_out_south"
],
[
"demo_link_influx_out_south"
],
[
"demo_pump_s1"
]
]
},
{
"id": "demo_meas_pt_s_down",
"type": "measurement",
"z": "demo_tab_ps_south",
"name": "PT-S-DN (South Downstream)",
"scaling": true,
"i_offset": 0,
"smooth_method": "mean",
"count": 3,
"category": "sensor",
"assetType": "pressure",
"enableLog": false,
"logLevel": "error",
"positionIcon": "←",
"hasDistance": false,
"i_min": 500,
"i_max": 1000,
"o_min": 500,
"o_max": 1000,
"simulator": true,
"uuid": "pt-s-dn-001",
"supplier": "Endress+Hauser",
"model": "Cerabar-PMC51",
"unit": "mbar",
"assetTagNumber": "PT-S-DN",
"positionVsParent": "downstream",
"x": 560,
"y": 440,
"wires": [
[
"demo_link_process_out_south"
],
[
"demo_link_influx_out_south"
],
[
"demo_pump_s1"
]
]
},
{
"id": "demo_mgc_west",
"type": "machineGroupControl",
"z": "demo_tab_ps_west",
"name": "MGC West (W1+W2)",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "atEquipment",
"positionIcon": "⊥",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 600,
"y": 220,
"wires": [
[
"demo_link_process_out_west"
],
[
"demo_link_influx_out_west"
],
[
"demo_ps_west"
]
]
},
{
"id": "demo_inj_calib_west",
"type": "inject",
"z": "demo_tab_ps_west",
"name": "Cal: PS West → 200m3",
"props": [
{
"p": "payload",
"vt": "num"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "0.5",
"topic": "calibratePredictedVolume",
"payload": "200",
"payloadType": "num",
"x": 120,
"y": 80,
"wires": [
[
"demo_ps_west"
]
]
},
{
"id": "demo_inj_calib_north",
"type": "inject",
"z": "demo_tab_ps_north",
"name": "Cal: PS North → 100m3",
"props": [
{
"p": "payload",
"vt": "num"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "0.5",
"topic": "calibratePredictedVolume",
"payload": "100",
"payloadType": "num",
"x": 120,
"y": 80,
"wires": [
[
"demo_ps_north"
]
]
},
{
"id": "demo_inj_calib_south",
"type": "inject",
"z": "demo_tab_ps_south",
"name": "Cal: PS South → 50m3",
"props": [
{
"p": "payload",
"vt": "num"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "0.5",
"topic": "calibratePredictedVolume",
"payload": "50",
"payloadType": "num",
"x": 120,
"y": 80,
"wires": [
[
"demo_ps_south"
]
]
},
{
"id": "demo_mon_west",
"type": "function",
"z": "demo_tab_ps_west",
"name": "Monitor PS West",
"func": "// Extract key metrics from PS process output\nconst p = msg.payload || {};\n\n// Keys have .default suffix in PS output format\nconst vol = p[\"volume.predicted.atequipment.default\"];\nconst level = p[\"level.predicted.atequipment.default\"];\nconst netFlow = p[\"netFlowRate.predicted.atequipment.default\"];\nconst volPct = p[\"volumePercent.predicted.atequipment.default\"];\n\n// Only log when we have volume data\nif (vol !== null && vol !== undefined) {\n const ctx = context.get(\"tickCount\") || 0;\n context.set(\"tickCount\", ctx + 1);\n\n // Log every 10 ticks\n if (ctx % 10 === 0) {\n const fmt = (v, dec) => typeof v === \"number\" ? v.toFixed(dec) : String(v);\n const parts = [\"vol=\" + fmt(vol, 1) + \"m3\"];\n if (level !== null && level !== undefined) parts.push(\"lvl=\" + fmt(level, 3) + \"m\");\n if (volPct !== null && volPct !== undefined) parts.push(\"fill=\" + fmt(volPct, 1) + \"%\");\n if (netFlow !== null && netFlow !== undefined) parts.push(\"net=\" + fmt(netFlow, 1) + \"m3/h\");\n\n node.warn(parts.join(\" | \"));\n }\n}\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1080,
"y": 160,
"wires": [
[]
]
},
{
"id": "demo_mon_north",
"type": "function",
"z": "demo_tab_ps_north",
"name": "Monitor PS North",
"func": "// Extract key metrics from PS process output\nconst p = msg.payload || {};\n\n// Keys have .default suffix in PS output format\nconst vol = p[\"volume.predicted.atequipment.default\"];\nconst level = p[\"level.predicted.atequipment.default\"];\nconst netFlow = p[\"netFlowRate.predicted.atequipment.default\"];\nconst volPct = p[\"volumePercent.predicted.atequipment.default\"];\n\n// Only log when we have volume data\nif (vol !== null && vol !== undefined) {\n const ctx = context.get(\"tickCount\") || 0;\n context.set(\"tickCount\", ctx + 1);\n\n // Log every 10 ticks\n if (ctx % 10 === 0) {\n const fmt = (v, dec) => typeof v === \"number\" ? v.toFixed(dec) : String(v);\n const parts = [\"vol=\" + fmt(vol, 1) + \"m3\"];\n if (level !== null && level !== undefined) parts.push(\"lvl=\" + fmt(level, 3) + \"m\");\n if (volPct !== null && volPct !== undefined) parts.push(\"fill=\" + fmt(volPct, 1) + \"%\");\n if (netFlow !== null && netFlow !== undefined) parts.push(\"net=\" + fmt(netFlow, 1) + \"m3/h\");\n\n node.warn(parts.join(\" | \"));\n }\n}\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1080,
"y": 140,
"wires": [
[]
]
},
{
"id": "demo_mon_south",
"type": "function",
"z": "demo_tab_ps_south",
"name": "Monitor PS South",
"func": "// Extract key metrics from PS process output\nconst p = msg.payload || {};\n\n// Keys have .default suffix in PS output format\nconst vol = p[\"volume.predicted.atequipment.default\"];\nconst level = p[\"level.predicted.atequipment.default\"];\nconst netFlow = p[\"netFlowRate.predicted.atequipment.default\"];\nconst volPct = p[\"volumePercent.predicted.atequipment.default\"];\n\n// Only log when we have volume data\nif (vol !== null && vol !== undefined) {\n const ctx = context.get(\"tickCount\") || 0;\n context.set(\"tickCount\", ctx + 1);\n\n // Log every 10 ticks\n if (ctx % 10 === 0) {\n const fmt = (v, dec) => typeof v === \"number\" ? v.toFixed(dec) : String(v);\n const parts = [\"vol=\" + fmt(vol, 1) + \"m3\"];\n if (level !== null && level !== undefined) parts.push(\"lvl=\" + fmt(level, 3) + \"m\");\n if (volPct !== null && volPct !== undefined) parts.push(\"fill=\" + fmt(volPct, 1) + \"%\");\n if (netFlow !== null && netFlow !== undefined) parts.push(\"net=\" + fmt(netFlow, 1) + \"m3/h\");\n\n node.warn(parts.join(\" | \"));\n }\n}\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1080,
"y": 140,
"wires": [
[]
]
},
{
"id": "demo_gauge_tank_west",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_ps_west",
"name": "West Tank Level",
"gtype": "gauge-tank",
"gstyle": "Rounded",
"title": "Level",
"units": "m",
"prefix": "",
"suffix": "m",
"min": 0,
"max": 4,
"segments": [
{
"color": "#f44336",
"from": 0
},
{
"color": "#ff9800",
"from": 0.3
},
{
"color": "#2196f3",
"from": 1
},
{
"color": "#ff9800",
"from": 2.5
},
{
"color": "#f44336",
"from": 3.2
}
],
"width": 2,
"height": 4,
"order": 2,
"className": "",
"x": 700,
"y": 400,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_gauge_tank_north",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_ps_north",
"name": "North Tank Level",
"gtype": "gauge-tank",
"gstyle": "Rounded",
"title": "Level",
"units": "m",
"prefix": "",
"suffix": "m",
"min": 0,
"max": 3,
"segments": [
{
"color": "#f44336",
"from": 0
},
{
"color": "#ff9800",
"from": 0.2
},
{
"color": "#2196f3",
"from": 0.6
},
{
"color": "#ff9800",
"from": 2
},
{
"color": "#f44336",
"from": 2.4
}
],
"width": 2,
"height": 4,
"order": 2,
"className": "",
"x": 700,
"y": 440,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_gauge_tank_south",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_ps_south",
"name": "South Tank Level",
"gtype": "gauge-tank",
"gstyle": "Rounded",
"title": "Level",
"units": "m",
"prefix": "",
"suffix": "m",
"min": 0,
"max": 2.5,
"segments": [
{
"color": "#f44336",
"from": 0
},
{
"color": "#ff9800",
"from": 0.2
},
{
"color": "#2196f3",
"from": 0.5
},
{
"color": "#ff9800",
"from": 1.6
},
{
"color": "#f44336",
"from": 2
}
],
"width": 2,
"height": 4,
"order": 2,
"className": "",
"x": 700,
"y": 480,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_gauge_fill_west",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_ps_west",
"name": "West Fill %",
"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": 25
},
{
"color": "#ff9800",
"from": 75
},
{
"color": "#f44336",
"from": 90
}
],
"width": 2,
"height": 3,
"order": 3,
"className": "hide-limits",
"x": 900,
"y": 400,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_gauge_fill_north",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_ps_north",
"name": "North Fill %",
"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": 25
},
{
"color": "#ff9800",
"from": 75
},
{
"color": "#f44336",
"from": 90
}
],
"width": 2,
"height": 3,
"order": 3,
"className": "hide-limits",
"x": 900,
"y": 440,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_gauge_fill_south",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_ps_south",
"name": "South Fill %",
"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": 25
},
{
"color": "#ff9800",
"from": 75
},
{
"color": "#f44336",
"from": 90
}
],
"width": 2,
"height": 3,
"order": 3,
"className": "hide-limits",
"x": 900,
"y": 480,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_tab_ps_west",
"type": "tab",
"label": "PS West",
"disabled": false,
"info": "Pumping Station West (Urban Catchment - 2 pumps, Level-based)"
},
{
"id": "demo_tab_ps_north",
"type": "tab",
"label": "PS North",
"disabled": false,
"info": "Pumping Station North (Industrial - 1 pump, Flow-based)"
},
{
"id": "demo_tab_ps_south",
"type": "tab",
"label": "PS South",
"disabled": false,
"info": "Pumping Station South (Residential - 1 pump, Manual)"
},
{
"id": "demo_tab_treatment",
"type": "tab",
"label": "Biological Treatment",
"disabled": false,
"info": "Merge point, Reactor, Settler, Effluent Measurements"
},
{
"id": "demo_link_influx_out_west",
"type": "link out",
"z": "demo_tab_ps_west",
"name": "→ InfluxDB",
"mode": "link",
"links": [
"demo_link_influx_in"
],
"x": 1080,
"y": 280
},
{
"id": "demo_link_influx_out_north",
"type": "link out",
"z": "demo_tab_ps_north",
"name": "→ InfluxDB",
"mode": "link",
"links": [
"demo_link_influx_in"
],
"x": 1080,
"y": 260
},
{
"id": "demo_link_influx_out_south",
"type": "link out",
"z": "demo_tab_ps_south",
"name": "→ InfluxDB",
"mode": "link",
"links": [
"demo_link_influx_in"
],
"x": 1080,
"y": 260
},
{
"id": "demo_link_influx_out_treatment",
"type": "link out",
"z": "demo_tab_treatment",
"name": "→ InfluxDB",
"mode": "link",
"links": [
"demo_link_influx_in"
],
"x": 1020,
"y": 260
},
{
"id": "demo_link_process_out_west",
"type": "link out",
"z": "demo_tab_ps_west",
"name": "→ Process debug",
"mode": "link",
"links": [
"demo_link_process_in"
],
"x": 1080,
"y": 320
},
{
"id": "demo_link_process_out_north",
"type": "link out",
"z": "demo_tab_ps_north",
"name": "→ Process debug",
"mode": "link",
"links": [
"demo_link_process_in"
],
"x": 1080,
"y": 300
},
{
"id": "demo_link_process_out_south",
"type": "link out",
"z": "demo_tab_ps_south",
"name": "→ Process debug",
"mode": "link",
"links": [
"demo_link_process_in"
],
"x": 1080,
"y": 300
},
{
"id": "demo_link_process_out_treatment",
"type": "link out",
"z": "demo_tab_treatment",
"name": "→ Process debug",
"mode": "link",
"links": [
"demo_link_process_in"
],
"x": 1020,
"y": 300
},
{
"id": "demo_link_merge_west_out",
"type": "link out",
"z": "demo_tab_ps_west",
"name": "→ Merge (West)",
"mode": "link",
"links": [
"demo_link_merge_west_in"
],
"x": 1080,
"y": 360
},
{
"id": "demo_link_merge_north_out",
"type": "link out",
"z": "demo_tab_ps_north",
"name": "→ Merge (North)",
"mode": "link",
"links": [
"demo_link_merge_north_in"
],
"x": 1080,
"y": 340
},
{
"id": "demo_link_merge_south_out",
"type": "link out",
"z": "demo_tab_ps_south",
"name": "→ Merge (South)",
"mode": "link",
"links": [
"demo_link_merge_south_in"
],
"x": 1080,
"y": 340
},
{
"id": "demo_link_merge_west_in",
"type": "link in",
"z": "demo_tab_treatment",
"name": "← PS West",
"links": [
"demo_link_merge_west_out"
],
"x": 100,
"y": 1000,
"wires": [
[
"demo_fn_tag_west"
]
]
},
{
"id": "demo_link_merge_north_in",
"type": "link in",
"z": "demo_tab_treatment",
"name": "← PS North",
"links": [
"demo_link_merge_north_out"
],
"x": 100,
"y": 1060,
"wires": [
[
"demo_fn_tag_north"
]
]
},
{
"id": "demo_link_merge_south_in",
"type": "link in",
"z": "demo_tab_treatment",
"name": "← PS South",
"links": [
"demo_link_merge_south_out"
],
"x": 100,
"y": 1120,
"wires": [
[
"demo_fn_tag_south"
]
]
},
{
"id": "demo_fn_tag_west",
"type": "function",
"z": "demo_tab_treatment",
"name": "Tag: west",
"func": "msg._psSource = 'west';\nreturn msg;",
"outputs": 1,
"x": 300,
"y": 1000,
"wires": [
[
"demo_fn_merge_collect"
]
]
},
{
"id": "demo_fn_tag_north",
"type": "function",
"z": "demo_tab_treatment",
"name": "Tag: north",
"func": "msg._psSource = 'north';\nreturn msg;",
"outputs": 1,
"x": 300,
"y": 1060,
"wires": [
[
"demo_fn_merge_collect"
]
]
},
{
"id": "demo_fn_tag_south",
"type": "function",
"z": "demo_tab_treatment",
"name": "Tag: south",
"func": "msg._psSource = 'south';\nreturn msg;",
"outputs": 1,
"x": 300,
"y": 1120,
"wires": [
[
"demo_fn_merge_collect"
]
]
},
{
"id": "demo_fn_merge_collect",
"type": "function",
"z": "demo_tab_treatment",
"name": "Merge Collector",
"func": "// Cache each PS output by _psSource tag, compute totals\nconst p = msg.payload || {};\nconst ps = msg._psSource;\nconst cache = flow.get('merge_cache') || { west: {}, north: {}, south: {} };\nconst keys = Object.keys(p);\nconst pick = (prefix) => { const k = keys.find(k => k.startsWith(prefix)); return k ? Number(p[k]) : null; };\n\nif (ps && cache[ps]) {\n const nf = pick('netFlowRate.predicted'); if (nf !== null) cache[ps].netFlow = nf;\n const fp = pick('volumePercent.predicted'); if (fp !== null) cache[ps].fillPct = fp;\n cache[ps].direction = p.direction || cache[ps].direction;\n cache[ps].ts = Date.now();\n}\nflow.set('merge_cache', cache);\n\nconst totalFlow = (cache.west.netFlow||0) + (cache.north.netFlow||0) + (cache.south.netFlow||0);\nconst avgFill = ((cache.west.fillPct||0) + (cache.north.fillPct||0) + (cache.south.fillPct||0)) / 3;\n\nreturn {\n topic: 'merge_combined_influent',\n payload: { totalInfluentFlow: +totalFlow.toFixed(1), avgFillPercent: +avgFill.toFixed(1),\n west: cache.west, north: cache.north, south: cache.south }\n};",
"outputs": 1,
"x": 520,
"y": 1060,
"wires": [
[
"demo_link_merge_dash",
"demo_fn_influent_compose"
]
]
},
{
"id": "demo_link_merge_dash",
"type": "link out",
"z": "demo_tab_treatment",
"name": "→ Merge Dashboard",
"mode": "link",
"links": [
"demo_link_merge_dash_in"
],
"x": 720,
"y": 1020
},
{
"id": "demo_comment_merge",
"type": "comment",
"z": "demo_tab_treatment",
"name": "=== MERGE COLLECTION POINT ===",
"info": "Combines output from all 3 pumping stations",
"x": 80,
"y": 960
},
{
"id": "demo_ui_page_overview",
"type": "ui-page",
"name": "Plant Overview",
"ui": "demo_ui_base",
"path": "/overview",
"icon": "dashboard",
"layout": "grid",
"theme": "demo_ui_theme",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "12"
}
],
"order": 0,
"className": ""
},
{
"id": "demo_ui_grp_overview_chain",
"type": "ui-group",
"name": "Process Chain",
"page": "demo_ui_page_overview",
"width": "12",
"height": "1",
"order": 1,
"showTitle": true,
"className": ""
},
{
"id": "demo_ui_grp_overview_kpi",
"type": "ui-group",
"name": "Key Indicators",
"page": "demo_ui_page_overview",
"width": "12",
"height": "1",
"order": 2,
"showTitle": true,
"className": ""
},
{
"id": "demo_link_merge_dash_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← Merge Data",
"links": [
"demo_link_merge_dash"
],
"x": 75,
"y": 960,
"wires": [
[
"demo_fn_overview_parse"
]
]
},
{
"id": "demo_link_overview_reactor_out",
"type": "link out",
"z": "demo_tab_treatment",
"name": "→ Overview (Reactor)",
"mode": "link",
"links": [
"demo_link_overview_reactor_in"
],
"x": 1020,
"y": 220
},
{
"id": "demo_link_overview_reactor_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← Reactor (Overview)",
"links": [
"demo_link_overview_reactor_out"
],
"x": 75,
"y": 1020,
"wires": [
[
"demo_fn_overview_reactor_parse"
]
]
},
{
"id": "demo_link_overview_eff_out",
"type": "link out",
"z": "demo_tab_treatment",
"name": "→ Overview (Effluent)",
"mode": "link",
"links": [
"demo_link_overview_eff_in"
],
"x": 1500,
"y": 500
},
{
"id": "demo_link_overview_eff_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← Effluent (Overview)",
"links": [
"demo_link_overview_eff_out"
],
"x": 75,
"y": 1080,
"wires": [
[
"demo_fn_overview_eff_parse"
]
]
},
{
"id": "demo_fn_overview_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse Overview (Merge)",
"func": "const p = msg.payload || {};\nconst now = Date.now();\n\n// Store in flow context for the template\nflow.set('overview_merge', p);\n\n// Output 1: chain vis data, Output 2: total flow gauge\nreturn [\n { topic: 'overview_chain', payload: p },\n p.totalInfluentFlow !== undefined ? { topic: 'Total Influent Flow', payload: p.totalInfluentFlow } : null\n];",
"outputs": 2,
"x": 280,
"y": 960,
"wires": [
[
"demo_overview_template"
],
[
"demo_gauge_overview_flow"
]
]
},
{
"id": "demo_fn_overview_reactor_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse Overview (Reactor)",
"func": "const p = msg.payload || {};\nif (!p.C || !Array.isArray(p.C)) return null;\n\nflow.set('overview_reactor', p);\n\n// Output 1: DO gauge, Output 2: to chain template\nconst doVal = Math.round(p.C[0]*100)/100;\nreturn [\n { topic: 'Reactor DO', payload: doVal },\n { topic: 'Reactor DO', payload: doVal }\n];",
"outputs": 2,
"x": 280,
"y": 1020,
"wires": [
[
"demo_gauge_overview_do"
],
[
"demo_overview_template"
]
]
},
{
"id": "demo_fn_overview_eff_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse Overview (Effluent)",
"func": "const p = msg.payload || {};\nconst topic = msg.topic || '';\nconst val = Number(p.mAbs);\nif (!Number.isFinite(val)) return null;\n\nconst rounded = Math.round(val*100)/100;\n\n// Route to appropriate gauge + template based on measurement type\nconst t = topic.toLowerCase();\nif (t.includes('tss')) {\n return [{ topic: 'Effluent TSS', payload: rounded }, null, { topic: 'Effluent TSS', payload: rounded }];\n}\nif (t.includes('nh4') || t.includes('ammonium')) {\n return [null, { topic: 'Effluent NH4', payload: rounded }, { topic: 'Effluent NH4', payload: rounded }];\n}\nreturn [null, null, null];",
"outputs": 3,
"x": 280,
"y": 1080,
"wires": [
[
"demo_gauge_overview_tss"
],
[
"demo_gauge_overview_nh4"
],
[
"demo_overview_template"
]
]
},
{
"id": "demo_overview_template",
"type": "ui-template",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_overview_chain",
"name": "Process Chain Diagram",
"order": 1,
"width": "12",
"height": "4",
"head": "",
"format": "<template>\n <div class=\"pchain\">\n <div class=\"pchain-row pchain-ps\">\n <v-card v-for=\"ps in psList\" :key=\"ps.key\" class=\"pchain-card\" :color=\"blockColor(merge?.[ps.key])\" variant=\"outlined\" density=\"compact\" @click=\"navigateTo(ps.path)\">\n <div class=\"pchain-card-inner\">\n <div class=\"pchain-label\">{{ ps.label }}</div>\n <div class=\"pchain-val\">{{ formatPct(merge?.[ps.key]?.fillPct) }}</div>\n <div class=\"pchain-sub\">{{ formatDir(merge?.[ps.key]?.direction) }}</div>\n </div>\n </v-card>\n </div>\n\n <div class=\"pchain-connector\">\n <v-icon size=\"18\" color=\"#4fc3f7\">mdi-arrow-down</v-icon>\n <v-icon size=\"18\" color=\"#4fc3f7\">mdi-arrow-down</v-icon>\n <v-icon size=\"18\" color=\"#4fc3f7\">mdi-arrow-down</v-icon>\n </div>\n\n <div class=\"pchain-row pchain-process\">\n <v-card class=\"pchain-card\" color=\"#0f3460\" variant=\"outlined\" density=\"compact\">\n <div class=\"pchain-card-inner\">\n <v-icon size=\"14\" color=\"#4fc3f7\" class=\"pchain-icon\">mdi-call-merge</v-icon>\n <div class=\"pchain-label\">Merge</div>\n <div class=\"pchain-val\">{{ formatFlow(merge?.totalInfluentFlow) }}</div>\n <div class=\"pchain-sub\">m\\u00b3/h</div>\n </div>\n </v-card>\n\n <v-icon size=\"16\" color=\"#4fc3f7\" class=\"pchain-arrow\">mdi-arrow-right</v-icon>\n\n <v-card class=\"pchain-card\" :color=\"reactorColor\" variant=\"outlined\" density=\"compact\" @click=\"navigateTo('/treatment')\">\n <div class=\"pchain-card-inner\">\n <v-icon size=\"14\" color=\"#4fc3f7\" class=\"pchain-icon\">mdi-flask-outline</v-icon>\n <div class=\"pchain-label\">Reactor</div>\n <div class=\"pchain-val\">DO: {{ reactorDO }}</div>\n <div class=\"pchain-sub\">mg/L</div>\n </div>\n </v-card>\n\n <v-icon size=\"16\" color=\"#4fc3f7\" class=\"pchain-arrow\">mdi-arrow-right</v-icon>\n\n <v-card class=\"pchain-card\" color=\"#0f3460\" variant=\"outlined\" density=\"compact\" @click=\"navigateTo('/treatment')\">\n <div class=\"pchain-card-inner\">\n <v-icon size=\"14\" color=\"#4fc3f7\" class=\"pchain-icon\">mdi-filter-variant</v-icon>\n <div class=\"pchain-label\">Settler</div>\n <div class=\"pchain-val\">TSS: {{ effTSS }}</div>\n <div class=\"pchain-sub\">mg/L</div>\n </div>\n </v-card>\n\n <v-icon size=\"16\" color=\"#4fc3f7\" class=\"pchain-arrow\">mdi-arrow-right</v-icon>\n\n <v-card class=\"pchain-card\" :color=\"effluentColor\" variant=\"outlined\" density=\"compact\">\n <div class=\"pchain-card-inner\">\n <v-icon size=\"14\" color=\"#4fc3f7\" class=\"pchain-icon\">mdi-water-check</v-icon>\n <div class=\"pchain-label\">Effluent</div>\n <div class=\"pchain-val\">NH4: {{ effNH4 }}</div>\n <div class=\"pchain-sub\">mg/L</div>\n </div>\n </v-card>\n </div>\n </div>\n</template>\n\n<script>\n export default {\n data() {\n return {\n merge: null,\n reactorDO: '--',\n effTSS: '--',\n effNH4: '--',\n psList: [\n { key: 'west', label: 'PS West', path: '/ps-west' },\n { key: 'north', label: 'PS North', path: '/ps-north' },\n { key: 'south', label: 'PS South', path: '/ps-south' }\n ]\n }\n },\n computed: {\n reactorColor() {\n const d = parseFloat(this.reactorDO);\n if (isNaN(d)) return '#0f3460';\n if (d < 1) return '#b71c1c';\n if (d < 2) return '#e65100';\n return '#1b5e20';\n },\n effluentColor() {\n const n = parseFloat(this.effNH4);\n if (isNaN(n)) return '#0f3460';\n if (n > 10) return '#b71c1c';\n if (n > 5) return '#e65100';\n return '#1b5e20';\n }\n },\n watch: {\n msg(val) {\n if (!val) return;\n const t = val.topic || '';\n if (t === 'overview_chain') {\n this.merge = val.payload;\n } else if (t === 'Reactor DO') {\n this.reactorDO = val.payload?.toFixed(1) || '--';\n } else if (t === 'Effluent TSS') {\n this.effTSS = val.payload?.toFixed(1) || '--';\n } else if (t === 'Effluent NH4') {\n this.effNH4 = val.payload?.toFixed(1) || '--';\n }\n }\n },\n methods: {\n navigateTo(path) {\n this.$router.push('/dashboard' + path);\n },\n blockColor(ps) {\n if (!ps || ps.fillPct === undefined) return '#0f3460';\n if (ps.fillPct > 90) return '#b71c1c';\n if (ps.fillPct > 75) return '#e65100';\n if (ps.fillPct < 10) return '#b71c1c';\n return '#0f3460';\n },\n formatPct(v) { return v != null ? v.toFixed(0) + '%' : '--'; },\n formatFlow(v) { return v != null ? v.toFixed(0) : '--'; },\n formatDir(d) { return d === 'filling' ? '\\u2191 filling' : d === 'emptying' ? '\\u2193 emptying' : '--'; }\n }\n }\n</script>\n\n<style scoped>\n .pchain { display: flex; flex-direction: column; gap: 4px; padding: 4px; }\n .pchain-row { display: flex; align-items: center; justify-content: center; gap: 6px; flex-wrap: wrap; }\n .pchain-ps { gap: 8px; }\n .pchain-connector { display: flex; justify-content: center; gap: 60px; }\n .pchain-card { cursor: pointer; min-width: 90px; flex: 0 1 120px; border-color: rgba(79,195,247,0.25) !important; }\n .pchain-card:hover { border-color: #4fc3f7 !important; }\n .pchain-card-inner { padding: 6px 8px; text-align: center; }\n .pchain-icon { margin-bottom: 2px; }\n .pchain-label { font-size: 0.72rem; font-weight: 600; color: #e0e0e0; letter-spacing: 0.02em; }\n .pchain-val { font-size: 0.8rem; font-weight: 700; color: #4fc3f7; margin-top: 1px; }\n .pchain-sub { font-size: 0.62rem; color: #78909c; margin-top: 1px; }\n .pchain-arrow { flex: 0 0 auto; opacity: 0.7; }\n</style>",
"templateScope": "local",
"className": "",
"x": 510,
"y": 960,
"wires": [
[]
]
},
{
"id": "demo_gauge_overview_flow",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_overview_kpi",
"name": "Total Influent Flow",
"gtype": "gauge-34",
"gstyle": "Rounded",
"title": "Influent Flow",
"units": "m³/h",
"prefix": "",
"suffix": "m³/h",
"min": 0,
"max": 500,
"segments": [
{
"color": "#2196f3",
"from": 0
},
{
"color": "#4caf50",
"from": 50
},
{
"color": "#ff9800",
"from": 350
},
{
"color": "#f44336",
"from": 450
}
],
"width": 3,
"height": 3,
"order": 1,
"className": "hide-limits",
"x": 510,
"y": 1020,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_gauge_overview_do",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_overview_kpi",
"name": "Reactor DO",
"gtype": "gauge-34",
"gstyle": "Rounded",
"title": "Reactor DO",
"units": "mg/L",
"prefix": "",
"suffix": "mg/L",
"min": 0,
"max": 10,
"segments": [
{
"color": "#f44336",
"from": 0
},
{
"color": "#ff9800",
"from": 1
},
{
"color": "#4caf50",
"from": 2
},
{
"color": "#ff9800",
"from": 6
},
{
"color": "#f44336",
"from": 8
}
],
"width": 3,
"height": 3,
"order": 2,
"className": "hide-limits",
"x": 510,
"y": 1060,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_gauge_overview_tss",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_overview_kpi",
"name": "Effluent TSS",
"gtype": "gauge-34",
"gstyle": "Rounded",
"title": "Effluent TSS",
"units": "mg/L",
"prefix": "",
"suffix": "mg/L",
"min": 0,
"max": 50,
"segments": [
{
"color": "#4caf50",
"from": 0
},
{
"color": "#ff9800",
"from": 25
},
{
"color": "#f44336",
"from": 40
}
],
"width": 3,
"height": 3,
"order": 3,
"className": "hide-limits",
"x": 510,
"y": 1100,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_gauge_overview_nh4",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_overview_kpi",
"name": "Effluent NH4",
"gtype": "gauge-34",
"gstyle": "Rounded",
"title": "Effluent NH4",
"units": "mg/L",
"prefix": "",
"suffix": "mg/L",
"min": 0,
"max": 20,
"segments": [
{
"color": "#4caf50",
"from": 0
},
{
"color": "#ff9800",
"from": 5
},
{
"color": "#f44336",
"from": 10
}
],
"width": 3,
"height": 3,
"order": 4,
"className": "hide-limits",
"x": 510,
"y": 1140,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_ui_page_ps_west_detail",
"type": "ui-page",
"name": "PS West Detail",
"ui": "demo_ui_base",
"path": "/ps-west",
"icon": "water_drop",
"layout": "grid",
"theme": "demo_ui_theme",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "12"
}
],
"order": 2,
"className": ""
},
{
"id": "demo_ctrl_west_grp_controls",
"type": "ui-group",
"name": "PS West Controls",
"page": "demo_ui_page_ps_west_detail",
"width": "6",
"height": "1",
"order": 1,
"showTitle": true,
"className": ""
},
{
"id": "demo_ctrl_west_grp_monitoring",
"type": "ui-group",
"name": "PS West Monitoring",
"page": "demo_ui_page_ps_west_detail",
"width": "6",
"height": "1",
"order": 2,
"showTitle": true,
"className": ""
},
{
"id": "demo_ctrl_west_grp_charts",
"type": "ui-group",
"name": "PS West Trends",
"page": "demo_ui_page_ps_west_detail",
"width": "12",
"height": "1",
"order": 3,
"showTitle": true,
"className": ""
},
{
"id": "demo_ctrl_west_mode",
"type": "ui-button-group",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_west_grp_controls",
"name": "PS West Mode",
"label": "Station Mode",
"tooltip": "",
"order": 1,
"width": "6",
"height": "1",
"passthru": false,
"options": [
{
"label": "Level",
"value": "levelbased",
"valueType": "str"
},
{
"label": "Flow",
"value": "flowbased",
"valueType": "str"
},
{
"label": "Manual",
"value": "manual",
"valueType": "str"
}
],
"x": 120,
"y": 700,
"wires": [
[
"demo_ctrl_west_fn_mode"
]
],
"rounded": true,
"useThemeColors": true,
"topic": "topic",
"topicType": "msg",
"className": ""
},
{
"id": "demo_ctrl_west_fn_mode",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt PS West Mode",
"func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 700,
"wires": [
[
"demo_ctrl_west_link_cmd_out"
]
]
},
{
"id": "demo_ctrl_west_flow",
"type": "ui-slider",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_west_grp_controls",
"name": "PS West Flow",
"label": "Manual Flow (m³/h)",
"tooltip": "",
"order": 2,
"width": "6",
"height": "1",
"passthru": false,
"outs": "end",
"min": 0,
"max": 300,
"step": 1,
"x": 120,
"y": 740,
"wires": [
[
"demo_ctrl_west_fn_flow"
]
],
"topic": "topic",
"topicType": "msg",
"thumbLabel": true,
"showTicks": "always",
"className": "",
"iconPrepend": "",
"iconAppend": "",
"color": "",
"colorTrack": "",
"colorThumb": "",
"showTextField": false
},
{
"id": "demo_ctrl_west_fn_flow",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt PS West Flow",
"func": "msg.topic = 'q_in';\nmsg.payload = { value: Number(msg.payload), unit: 'm3/h' };\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 740,
"wires": [
[
"demo_ctrl_west_link_cmd_out"
]
]
},
{
"id": "demo_ctrl_west_pump_w1_mode",
"type": "ui-button-group",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_west_grp_controls",
"name": "W1 Mode",
"label": "W1 Mode",
"tooltip": "",
"order": 3,
"width": "3",
"height": "1",
"passthru": false,
"options": [
{
"label": "Auto",
"value": "auto",
"valueType": "str"
},
{
"label": "Virtual",
"value": "virtualControl",
"valueType": "str"
},
{
"label": "Physical",
"value": "fysicalControl",
"valueType": "str"
}
],
"x": 120,
"y": 780,
"wires": [
[
"demo_ctrl_west_fn_pump_w1_mode"
]
],
"rounded": true,
"useThemeColors": true,
"topic": "topic",
"topicType": "msg",
"className": ""
},
{
"id": "demo_ctrl_west_fn_pump_w1_mode",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt W1 Mode",
"func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nmsg._targetNode = 'demo_pump_w1';\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 780,
"wires": [
[
"demo_ctrl_west_link_pump_w1_out"
]
]
},
{
"id": "demo_ctrl_west_pump_w1_speed",
"type": "ui-slider",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_west_grp_controls",
"name": "W1 Speed",
"label": "W1 Speed (%)",
"tooltip": "",
"order": 4,
"width": "3",
"height": "1",
"passthru": false,
"outs": "end",
"min": 0,
"max": 100,
"step": 1,
"x": 120,
"y": 820,
"wires": [
[
"demo_ctrl_west_fn_pump_w1_speed"
]
],
"topic": "topic",
"topicType": "msg",
"thumbLabel": true,
"showTicks": "always",
"className": "",
"iconPrepend": "",
"iconAppend": "",
"color": "",
"colorTrack": "",
"colorThumb": "",
"showTextField": false
},
{
"id": "demo_ctrl_west_fn_pump_w1_speed",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt W1 Speed",
"func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'dashboard', action: 'setpoint', setpoint: Number(msg.payload) };\nmsg._targetNode = 'demo_pump_w1';\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 820,
"wires": [
[
"demo_ctrl_west_link_pump_w1_out"
]
]
},
{
"id": "demo_ctrl_west_link_pump_w1_out",
"type": "link out",
"z": "demo_tab_dashboard",
"name": "→ W1 Cmd",
"mode": "link",
"links": [
"demo_ctrl_west_link_pump_w1_in"
],
"x": 520,
"y": 800
},
{
"id": "demo_ctrl_west_link_pump_w1_in",
"type": "link in",
"z": "demo_tab_ps_west",
"name": "← W1 Cmd",
"links": [
"demo_ctrl_west_link_pump_w1_out"
],
"x": 120,
"y": 540,
"wires": [
[
"demo_pump_w1"
]
]
},
{
"id": "demo_ctrl_west_pump_w2_mode",
"type": "ui-button-group",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_west_grp_controls",
"name": "W2 Mode",
"label": "W2 Mode",
"tooltip": "",
"order": 5,
"width": "3",
"height": "1",
"passthru": false,
"options": [
{
"label": "Auto",
"value": "auto",
"valueType": "str"
},
{
"label": "Virtual",
"value": "virtualControl",
"valueType": "str"
},
{
"label": "Physical",
"value": "fysicalControl",
"valueType": "str"
}
],
"x": 120,
"y": 860,
"wires": [
[
"demo_ctrl_west_fn_pump_w2_mode"
]
],
"rounded": true,
"useThemeColors": true,
"topic": "topic",
"topicType": "msg",
"className": ""
},
{
"id": "demo_ctrl_west_fn_pump_w2_mode",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt W2 Mode",
"func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nmsg._targetNode = 'demo_pump_w2';\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 860,
"wires": [
[
"demo_ctrl_west_link_pump_w2_out"
]
]
},
{
"id": "demo_ctrl_west_pump_w2_speed",
"type": "ui-slider",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_west_grp_controls",
"name": "W2 Speed",
"label": "W2 Speed (%)",
"tooltip": "",
"order": 6,
"width": "3",
"height": "1",
"passthru": false,
"outs": "end",
"min": 0,
"max": 100,
"step": 1,
"x": 120,
"y": 900,
"wires": [
[
"demo_ctrl_west_fn_pump_w2_speed"
]
],
"topic": "topic",
"topicType": "msg",
"thumbLabel": true,
"showTicks": "always",
"className": "",
"iconPrepend": "",
"iconAppend": "",
"color": "",
"colorTrack": "",
"colorThumb": "",
"showTextField": false
},
{
"id": "demo_ctrl_west_fn_pump_w2_speed",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt W2 Speed",
"func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'dashboard', action: 'setpoint', setpoint: Number(msg.payload) };\nmsg._targetNode = 'demo_pump_w2';\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 900,
"wires": [
[
"demo_ctrl_west_link_pump_w2_out"
]
]
},
{
"id": "demo_ctrl_west_link_pump_w2_out",
"type": "link out",
"z": "demo_tab_dashboard",
"name": "→ W2 Cmd",
"mode": "link",
"links": [
"demo_ctrl_west_link_pump_w2_in"
],
"x": 520,
"y": 880
},
{
"id": "demo_ctrl_west_link_pump_w2_in",
"type": "link in",
"z": "demo_tab_ps_west",
"name": "← W2 Cmd",
"links": [
"demo_ctrl_west_link_pump_w2_out"
],
"x": 120,
"y": 600,
"wires": [
[
"demo_pump_w2"
]
]
},
{
"id": "demo_ctrl_west_link_cmd_out",
"type": "link out",
"z": "demo_tab_dashboard",
"name": "→ PS West Cmd",
"mode": "link",
"links": [
"demo_ctrl_west_link_cmd_in"
],
"x": 520,
"y": 720
},
{
"id": "demo_ctrl_west_link_cmd_in",
"type": "link in",
"z": "demo_tab_ps_west",
"name": "← PS West Cmd",
"links": [
"demo_ctrl_west_link_cmd_out"
],
"x": 120,
"y": 480,
"wires": [
[
"demo_ps_west"
]
]
},
{
"id": "demo_ctrl_west_link_detail_data_out",
"type": "link out",
"z": "demo_tab_ps_west",
"name": "→ PS West Detail",
"mode": "link",
"links": [
"demo_ctrl_west_link_detail_data_in"
],
"x": 1080,
"y": 400
},
{
"id": "demo_ctrl_west_link_detail_data_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← PS West Detail",
"links": [
"demo_ctrl_west_link_detail_data_out"
],
"x": 75,
"y": 650,
"wires": [
[
"demo_ctrl_west_fn_detail_parse"
]
]
},
{
"id": "demo_ctrl_west_fn_detail_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse PS West Detail",
"func": "const p = msg.payload || {};\nconst cache = context.get('c') || {};\nconst keys = Object.keys(p);\nconst pick = (prefixes) => { for (const pfx of prefixes) { const k = keys.find(k => k.startsWith(pfx)); if (k) { const v = Number(p[k]); if (Number.isFinite(v)) return v; } } return null; };\n\nconst level = pick(['level.predicted.atequipment','level.measured.atequipment']);\nconst volume = pick(['volume.predicted.atequipment']);\nconst netFlow = pick(['netFlowRate.predicted.atequipment']);\nconst fillPct = pick(['volumePercent.predicted.atequipment']);\nconst direction = p.direction || cache.direction || '?';\n\nif (level !== null) cache.level = level;\nif (volume !== null) cache.volume = volume;\nif (netFlow !== null) cache.netFlow = netFlow;\nif (fillPct !== null) cache.fillPct = fillPct;\ncache.direction = direction;\ncontext.set('c', cache);\n\nconst now = Date.now();\nconst dirArrow = cache.direction === 'filling' ? '\\u2191' : cache.direction === 'emptying' ? '\\u2193' : '\\u2014';\nconst status = [\n dirArrow + ' ' + (cache.direction || ''),\n cache.netFlow !== undefined ? Math.abs(cache.netFlow).toFixed(0) + ' m\\u00b3/h' : '',\n].filter(s => s.trim()).join(' | ');\n\nreturn [\n cache.level !== undefined ? {topic:'PS West Level', payload: cache.level, timestamp: now} : null,\n cache.netFlow !== undefined ? {topic:'PS West Flow', payload: cache.netFlow, timestamp: now} : null,\n {topic:'PS West Status', payload: status},\n cache.fillPct !== undefined ? {payload: Number(cache.fillPct.toFixed(1))} : null,\n cache.level !== undefined ? {payload: Number(cache.level.toFixed(2))} : null\n];",
"outputs": 5,
"x": 280,
"y": 650,
"wires": [
[
"demo_ctrl_west_chart_level"
],
[
"demo_ctrl_west_chart_flow"
],
[
"demo_ctrl_west_text_status"
],
[
"demo_ctrl_west_gauge_fill"
],
[
"demo_ctrl_west_gauge_tank"
]
]
},
{
"id": "demo_ctrl_west_chart_level",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_west_grp_charts",
"name": "PS West Level",
"label": "Basin Level (m)",
"order": 1,
"width": "6",
"height": "5",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "m",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#0094ce",
"#FF7F0E",
"#2CA02C"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"x": 510,
"y": 630,
"wires": [],
"className": ""
},
{
"id": "demo_ctrl_west_chart_flow",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_west_grp_charts",
"name": "PS West Flow",
"label": "Net Flow (m³/h)",
"order": 2,
"width": "6",
"height": "5",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "m³/h",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#4fc3f7",
"#FF7F0E",
"#2CA02C"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"x": 510,
"y": 660,
"wires": [],
"className": ""
},
{
"id": "demo_ctrl_west_text_status",
"type": "ui-text",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_west_grp_monitoring",
"name": "PS West Status",
"label": "Status",
"order": 1,
"width": "6",
"height": "1",
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 510,
"y": 680,
"wires": [],
"className": ""
},
{
"id": "demo_ctrl_west_gauge_fill",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_west_grp_monitoring",
"name": "PS West Fill",
"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": 25
},
{
"color": "#ff9800",
"from": 75
},
{
"color": "#f44336",
"from": 90
}
],
"width": 3,
"height": 3,
"order": 2,
"className": "hide-limits",
"x": 700,
"y": 680,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_ctrl_west_gauge_tank",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_west_grp_monitoring",
"name": "PS West Tank",
"gtype": "gauge-tank",
"gstyle": "Rounded",
"title": "Level",
"units": "m",
"prefix": "",
"suffix": "m",
"min": 0,
"max": 4,
"segments": [
{
"color": "#f44336",
"from": 0
},
{
"color": "#ff9800",
"from": 0.32
},
{
"color": "#2196f3",
"from": 1
},
{
"color": "#ff9800",
"from": 2.48
},
{
"color": "#f44336",
"from": 3.2
}
],
"width": 3,
"height": 4,
"order": 3,
"className": "",
"x": 700,
"y": 640,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_ui_page_ps_north_detail",
"type": "ui-page",
"name": "PS North Detail",
"ui": "demo_ui_base",
"path": "/ps-north",
"icon": "water_drop",
"layout": "grid",
"theme": "demo_ui_theme",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "12"
}
],
"order": 3,
"className": ""
},
{
"id": "demo_ctrl_north_grp_controls",
"type": "ui-group",
"name": "PS North Controls",
"page": "demo_ui_page_ps_north_detail",
"width": "6",
"height": "1",
"order": 1,
"showTitle": true,
"className": ""
},
{
"id": "demo_ctrl_north_grp_monitoring",
"type": "ui-group",
"name": "PS North Monitoring",
"page": "demo_ui_page_ps_north_detail",
"width": "6",
"height": "1",
"order": 2,
"showTitle": true,
"className": ""
},
{
"id": "demo_ctrl_north_grp_charts",
"type": "ui-group",
"name": "PS North Trends",
"page": "demo_ui_page_ps_north_detail",
"width": "12",
"height": "1",
"order": 3,
"showTitle": true,
"className": ""
},
{
"id": "demo_ctrl_north_mode",
"type": "ui-button-group",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_north_grp_controls",
"name": "PS North Mode",
"label": "Station Mode",
"tooltip": "",
"order": 1,
"width": "6",
"height": "1",
"passthru": false,
"options": [
{
"label": "Level",
"value": "levelbased",
"valueType": "str"
},
{
"label": "Flow",
"value": "flowbased",
"valueType": "str"
},
{
"label": "Manual",
"value": "manual",
"valueType": "str"
}
],
"x": 120,
"y": 1000,
"wires": [
[
"demo_ctrl_north_fn_mode"
]
],
"rounded": true,
"useThemeColors": true,
"topic": "topic",
"topicType": "msg",
"className": ""
},
{
"id": "demo_ctrl_north_fn_mode",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt PS North Mode",
"func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 1000,
"wires": [
[
"demo_ctrl_north_link_cmd_out"
]
]
},
{
"id": "demo_ctrl_north_flow",
"type": "ui-slider",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_north_grp_controls",
"name": "PS North Flow",
"label": "Manual Flow (m³/h)",
"tooltip": "",
"order": 2,
"width": "6",
"height": "1",
"passthru": false,
"outs": "end",
"min": 0,
"max": 200,
"step": 1,
"x": 120,
"y": 1040,
"wires": [
[
"demo_ctrl_north_fn_flow"
]
],
"topic": "topic",
"topicType": "msg",
"thumbLabel": true,
"showTicks": "always",
"className": "",
"iconPrepend": "",
"iconAppend": "",
"color": "",
"colorTrack": "",
"colorThumb": "",
"showTextField": false
},
{
"id": "demo_ctrl_north_fn_flow",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt PS North Flow",
"func": "msg.topic = 'q_in';\nmsg.payload = { value: Number(msg.payload), unit: 'm3/h' };\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 1040,
"wires": [
[
"demo_ctrl_north_link_cmd_out"
]
]
},
{
"id": "demo_ctrl_north_pump_n1_mode",
"type": "ui-button-group",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_north_grp_controls",
"name": "N1 Mode",
"label": "N1 Mode",
"tooltip": "",
"order": 3,
"width": "3",
"height": "1",
"passthru": false,
"options": [
{
"label": "Auto",
"value": "auto",
"valueType": "str"
},
{
"label": "Virtual",
"value": "virtualControl",
"valueType": "str"
},
{
"label": "Physical",
"value": "fysicalControl",
"valueType": "str"
}
],
"x": 120,
"y": 1080,
"wires": [
[
"demo_ctrl_north_fn_pump_n1_mode"
]
],
"rounded": true,
"useThemeColors": true,
"topic": "topic",
"topicType": "msg",
"className": ""
},
{
"id": "demo_ctrl_north_fn_pump_n1_mode",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt N1 Mode",
"func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nmsg._targetNode = 'demo_pump_n1';\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 1080,
"wires": [
[
"demo_ctrl_north_link_pump_n1_out"
]
]
},
{
"id": "demo_ctrl_north_pump_n1_speed",
"type": "ui-slider",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_north_grp_controls",
"name": "N1 Speed",
"label": "N1 Speed (%)",
"tooltip": "",
"order": 4,
"width": "3",
"height": "1",
"passthru": false,
"outs": "end",
"min": 0,
"max": 100,
"step": 1,
"x": 120,
"y": 1120,
"wires": [
[
"demo_ctrl_north_fn_pump_n1_speed"
]
],
"topic": "topic",
"topicType": "msg",
"thumbLabel": true,
"showTicks": "always",
"className": "",
"iconPrepend": "",
"iconAppend": "",
"color": "",
"colorTrack": "",
"colorThumb": "",
"showTextField": false
},
{
"id": "demo_ctrl_north_fn_pump_n1_speed",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt N1 Speed",
"func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'dashboard', action: 'setpoint', setpoint: Number(msg.payload) };\nmsg._targetNode = 'demo_pump_n1';\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 1120,
"wires": [
[
"demo_ctrl_north_link_pump_n1_out"
]
]
},
{
"id": "demo_ctrl_north_link_pump_n1_out",
"type": "link out",
"z": "demo_tab_dashboard",
"name": "→ N1 Cmd",
"mode": "link",
"links": [
"demo_ctrl_north_link_pump_n1_in"
],
"x": 520,
"y": 1100
},
{
"id": "demo_ctrl_north_link_pump_n1_in",
"type": "link in",
"z": "demo_tab_ps_north",
"name": "← N1 Cmd",
"links": [
"demo_ctrl_north_link_pump_n1_out"
],
"x": 120,
"y": 540,
"wires": [
[
"demo_pump_n1"
]
]
},
{
"id": "demo_ctrl_north_link_cmd_out",
"type": "link out",
"z": "demo_tab_dashboard",
"name": "→ PS North Cmd",
"mode": "link",
"links": [
"demo_ctrl_north_link_cmd_in"
],
"x": 520,
"y": 1020
},
{
"id": "demo_ctrl_north_link_cmd_in",
"type": "link in",
"z": "demo_tab_ps_north",
"name": "← PS North Cmd",
"links": [
"demo_ctrl_north_link_cmd_out"
],
"x": 120,
"y": 480,
"wires": [
[
"demo_ps_north"
]
]
},
{
"id": "demo_ctrl_north_link_detail_data_out",
"type": "link out",
"z": "demo_tab_ps_north",
"name": "→ PS North Detail",
"mode": "link",
"links": [
"demo_ctrl_north_link_detail_data_in"
],
"x": 1080,
"y": 400
},
{
"id": "demo_ctrl_north_link_detail_data_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← PS North Detail",
"links": [
"demo_ctrl_north_link_detail_data_out"
],
"x": 75,
"y": 950,
"wires": [
[
"demo_ctrl_north_fn_detail_parse"
]
]
},
{
"id": "demo_ctrl_north_fn_detail_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse PS North Detail",
"func": "const p = msg.payload || {};\nconst cache = context.get('c') || {};\nconst keys = Object.keys(p);\nconst pick = (prefixes) => { for (const pfx of prefixes) { const k = keys.find(k => k.startsWith(pfx)); if (k) { const v = Number(p[k]); if (Number.isFinite(v)) return v; } } return null; };\n\nconst level = pick(['level.predicted.atequipment','level.measured.atequipment']);\nconst volume = pick(['volume.predicted.atequipment']);\nconst netFlow = pick(['netFlowRate.predicted.atequipment']);\nconst fillPct = pick(['volumePercent.predicted.atequipment']);\nconst direction = p.direction || cache.direction || '?';\n\nif (level !== null) cache.level = level;\nif (volume !== null) cache.volume = volume;\nif (netFlow !== null) cache.netFlow = netFlow;\nif (fillPct !== null) cache.fillPct = fillPct;\ncache.direction = direction;\ncontext.set('c', cache);\n\nconst now = Date.now();\nconst dirArrow = cache.direction === 'filling' ? '\\u2191' : cache.direction === 'emptying' ? '\\u2193' : '\\u2014';\nconst status = [\n dirArrow + ' ' + (cache.direction || ''),\n cache.netFlow !== undefined ? Math.abs(cache.netFlow).toFixed(0) + ' m\\u00b3/h' : '',\n].filter(s => s.trim()).join(' | ');\n\nreturn [\n cache.level !== undefined ? {topic:'PS North Level', payload: cache.level, timestamp: now} : null,\n cache.netFlow !== undefined ? {topic:'PS North Flow', payload: cache.netFlow, timestamp: now} : null,\n {topic:'PS North Status', payload: status},\n cache.fillPct !== undefined ? {payload: Number(cache.fillPct.toFixed(1))} : null,\n cache.level !== undefined ? {payload: Number(cache.level.toFixed(2))} : null\n];",
"outputs": 5,
"x": 280,
"y": 950,
"wires": [
[
"demo_ctrl_north_chart_level"
],
[
"demo_ctrl_north_chart_flow"
],
[
"demo_ctrl_north_text_status"
],
[
"demo_ctrl_north_gauge_fill"
],
[
"demo_ctrl_north_gauge_tank"
]
]
},
{
"id": "demo_ctrl_north_chart_level",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_north_grp_charts",
"name": "PS North Level",
"label": "Basin Level (m)",
"order": 1,
"width": "6",
"height": "5",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "m",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#0094ce",
"#FF7F0E",
"#2CA02C"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"x": 510,
"y": 930,
"wires": [],
"className": ""
},
{
"id": "demo_ctrl_north_chart_flow",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_north_grp_charts",
"name": "PS North Flow",
"label": "Net Flow (m³/h)",
"order": 2,
"width": "6",
"height": "5",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "m³/h",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#4fc3f7",
"#FF7F0E",
"#2CA02C"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"x": 510,
"y": 960,
"wires": [],
"className": ""
},
{
"id": "demo_ctrl_north_text_status",
"type": "ui-text",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_north_grp_monitoring",
"name": "PS North Status",
"label": "Status",
"order": 1,
"width": "6",
"height": "1",
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 510,
"y": 980,
"wires": [],
"className": ""
},
{
"id": "demo_ctrl_north_gauge_fill",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_north_grp_monitoring",
"name": "PS North Fill",
"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": 25
},
{
"color": "#ff9800",
"from": 75
},
{
"color": "#f44336",
"from": 90
}
],
"width": 3,
"height": 3,
"order": 2,
"className": "hide-limits",
"x": 700,
"y": 980,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_ctrl_north_gauge_tank",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_north_grp_monitoring",
"name": "PS North Tank",
"gtype": "gauge-tank",
"gstyle": "Rounded",
"title": "Level",
"units": "m",
"prefix": "",
"suffix": "m",
"min": 0,
"max": 3,
"segments": [
{
"color": "#f44336",
"from": 0
},
{
"color": "#ff9800",
"from": 0.24
},
{
"color": "#2196f3",
"from": 0.75
},
{
"color": "#ff9800",
"from": 1.8599999999999999
},
{
"color": "#f44336",
"from": 2.4000000000000004
}
],
"width": 3,
"height": 4,
"order": 3,
"className": "",
"x": 700,
"y": 940,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_ui_page_ps_south_detail",
"type": "ui-page",
"name": "PS South Detail",
"ui": "demo_ui_base",
"path": "/ps-south",
"icon": "water_drop",
"layout": "grid",
"theme": "demo_ui_theme",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "12"
}
],
"order": 4,
"className": ""
},
{
"id": "demo_ctrl_south_grp_controls",
"type": "ui-group",
"name": "PS South Controls",
"page": "demo_ui_page_ps_south_detail",
"width": "6",
"height": "1",
"order": 1,
"showTitle": true,
"className": ""
},
{
"id": "demo_ctrl_south_grp_monitoring",
"type": "ui-group",
"name": "PS South Monitoring",
"page": "demo_ui_page_ps_south_detail",
"width": "6",
"height": "1",
"order": 2,
"showTitle": true,
"className": ""
},
{
"id": "demo_ctrl_south_grp_charts",
"type": "ui-group",
"name": "PS South Trends",
"page": "demo_ui_page_ps_south_detail",
"width": "12",
"height": "1",
"order": 3,
"showTitle": true,
"className": ""
},
{
"id": "demo_ctrl_south_mode",
"type": "ui-button-group",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_south_grp_controls",
"name": "PS South Mode",
"label": "Station Mode",
"tooltip": "",
"order": 1,
"width": "6",
"height": "1",
"passthru": false,
"options": [
{
"label": "Level",
"value": "levelbased",
"valueType": "str"
},
{
"label": "Flow",
"value": "flowbased",
"valueType": "str"
},
{
"label": "Manual",
"value": "manual",
"valueType": "str"
}
],
"x": 120,
"y": 1300,
"wires": [
[
"demo_ctrl_south_fn_mode"
]
],
"rounded": true,
"useThemeColors": true,
"topic": "topic",
"topicType": "msg",
"className": ""
},
{
"id": "demo_ctrl_south_fn_mode",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt PS South Mode",
"func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 1300,
"wires": [
[
"demo_ctrl_south_link_cmd_out"
]
]
},
{
"id": "demo_ctrl_south_flow",
"type": "ui-slider",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_south_grp_controls",
"name": "PS South Flow",
"label": "Manual Flow (m³/h)",
"tooltip": "",
"order": 2,
"width": "6",
"height": "1",
"passthru": false,
"outs": "end",
"min": 0,
"max": 100,
"step": 1,
"x": 120,
"y": 1340,
"wires": [
[
"demo_ctrl_south_fn_flow"
]
],
"topic": "topic",
"topicType": "msg",
"thumbLabel": true,
"showTicks": "always",
"className": "",
"iconPrepend": "",
"iconAppend": "",
"color": "",
"colorTrack": "",
"colorThumb": "",
"showTextField": false
},
{
"id": "demo_ctrl_south_fn_flow",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt PS South Flow",
"func": "msg.topic = 'q_in';\nmsg.payload = { value: Number(msg.payload), unit: 'm3/h' };\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 1340,
"wires": [
[
"demo_ctrl_south_link_cmd_out"
]
]
},
{
"id": "demo_ctrl_south_pump_s1_mode",
"type": "ui-button-group",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_south_grp_controls",
"name": "S1 Mode",
"label": "S1 Mode",
"tooltip": "",
"order": 3,
"width": "3",
"height": "1",
"passthru": false,
"options": [
{
"label": "Auto",
"value": "auto",
"valueType": "str"
},
{
"label": "Virtual",
"value": "virtualControl",
"valueType": "str"
},
{
"label": "Physical",
"value": "fysicalControl",
"valueType": "str"
}
],
"x": 120,
"y": 1380,
"wires": [
[
"demo_ctrl_south_fn_pump_s1_mode"
]
],
"rounded": true,
"useThemeColors": true,
"topic": "topic",
"topicType": "msg",
"className": ""
},
{
"id": "demo_ctrl_south_fn_pump_s1_mode",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt S1 Mode",
"func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nmsg._targetNode = 'demo_pump_s1';\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 1380,
"wires": [
[
"demo_ctrl_south_link_pump_s1_out"
]
]
},
{
"id": "demo_ctrl_south_pump_s1_speed",
"type": "ui-slider",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_south_grp_controls",
"name": "S1 Speed",
"label": "S1 Speed (%)",
"tooltip": "",
"order": 4,
"width": "3",
"height": "1",
"passthru": false,
"outs": "end",
"min": 0,
"max": 100,
"step": 1,
"x": 120,
"y": 1420,
"wires": [
[
"demo_ctrl_south_fn_pump_s1_speed"
]
],
"topic": "topic",
"topicType": "msg",
"thumbLabel": true,
"showTicks": "always",
"className": "",
"iconPrepend": "",
"iconAppend": "",
"color": "",
"colorTrack": "",
"colorThumb": "",
"showTextField": false
},
{
"id": "demo_ctrl_south_fn_pump_s1_speed",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Fmt S1 Speed",
"func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'dashboard', action: 'setpoint', setpoint: Number(msg.payload) };\nmsg._targetNode = 'demo_pump_s1';\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 1420,
"wires": [
[
"demo_ctrl_south_link_pump_s1_out"
]
]
},
{
"id": "demo_ctrl_south_link_pump_s1_out",
"type": "link out",
"z": "demo_tab_dashboard",
"name": "→ S1 Cmd",
"mode": "link",
"links": [
"demo_ctrl_south_link_pump_s1_in"
],
"x": 520,
"y": 1400
},
{
"id": "demo_ctrl_south_link_pump_s1_in",
"type": "link in",
"z": "demo_tab_ps_south",
"name": "← S1 Cmd",
"links": [
"demo_ctrl_south_link_pump_s1_out"
],
"x": 120,
"y": 540,
"wires": [
[
"demo_pump_s1"
]
]
},
{
"id": "demo_ctrl_south_link_cmd_out",
"type": "link out",
"z": "demo_tab_dashboard",
"name": "→ PS South Cmd",
"mode": "link",
"links": [
"demo_ctrl_south_link_cmd_in"
],
"x": 520,
"y": 1320
},
{
"id": "demo_ctrl_south_link_cmd_in",
"type": "link in",
"z": "demo_tab_ps_south",
"name": "← PS South Cmd",
"links": [
"demo_ctrl_south_link_cmd_out"
],
"x": 120,
"y": 480,
"wires": [
[
"demo_ps_south"
]
]
},
{
"id": "demo_ctrl_south_link_detail_data_out",
"type": "link out",
"z": "demo_tab_ps_south",
"name": "→ PS South Detail",
"mode": "link",
"links": [
"demo_ctrl_south_link_detail_data_in"
],
"x": 1080,
"y": 400
},
{
"id": "demo_ctrl_south_link_detail_data_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← PS South Detail",
"links": [
"demo_ctrl_south_link_detail_data_out"
],
"x": 75,
"y": 1250,
"wires": [
[
"demo_ctrl_south_fn_detail_parse"
]
]
},
{
"id": "demo_ctrl_south_fn_detail_parse",
"type": "function",
"z": "demo_tab_dashboard",
"name": "Parse PS South Detail",
"func": "const p = msg.payload || {};\nconst cache = context.get('c') || {};\nconst keys = Object.keys(p);\nconst pick = (prefixes) => { for (const pfx of prefixes) { const k = keys.find(k => k.startsWith(pfx)); if (k) { const v = Number(p[k]); if (Number.isFinite(v)) return v; } } return null; };\n\nconst level = pick(['level.predicted.atequipment','level.measured.atequipment']);\nconst volume = pick(['volume.predicted.atequipment']);\nconst netFlow = pick(['netFlowRate.predicted.atequipment']);\nconst fillPct = pick(['volumePercent.predicted.atequipment']);\nconst direction = p.direction || cache.direction || '?';\n\nif (level !== null) cache.level = level;\nif (volume !== null) cache.volume = volume;\nif (netFlow !== null) cache.netFlow = netFlow;\nif (fillPct !== null) cache.fillPct = fillPct;\ncache.direction = direction;\ncontext.set('c', cache);\n\nconst now = Date.now();\nconst dirArrow = cache.direction === 'filling' ? '\\u2191' : cache.direction === 'emptying' ? '\\u2193' : '\\u2014';\nconst status = [\n dirArrow + ' ' + (cache.direction || ''),\n cache.netFlow !== undefined ? Math.abs(cache.netFlow).toFixed(0) + ' m\\u00b3/h' : '',\n].filter(s => s.trim()).join(' | ');\n\nreturn [\n cache.level !== undefined ? {topic:'PS South Level', payload: cache.level, timestamp: now} : null,\n cache.netFlow !== undefined ? {topic:'PS South Flow', payload: cache.netFlow, timestamp: now} : null,\n {topic:'PS South Status', payload: status},\n cache.fillPct !== undefined ? {payload: Number(cache.fillPct.toFixed(1))} : null,\n cache.level !== undefined ? {payload: Number(cache.level.toFixed(2))} : null\n];",
"outputs": 5,
"x": 280,
"y": 1250,
"wires": [
[
"demo_ctrl_south_chart_level"
],
[
"demo_ctrl_south_chart_flow"
],
[
"demo_ctrl_south_text_status"
],
[
"demo_ctrl_south_gauge_fill"
],
[
"demo_ctrl_south_gauge_tank"
]
]
},
{
"id": "demo_ctrl_south_chart_level",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_south_grp_charts",
"name": "PS South Level",
"label": "Basin Level (m)",
"order": 1,
"width": "6",
"height": "5",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "m",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#0094ce",
"#FF7F0E",
"#2CA02C"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"x": 510,
"y": 1230,
"wires": [],
"className": ""
},
{
"id": "demo_ctrl_south_chart_flow",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_south_grp_charts",
"name": "PS South Flow",
"label": "Net Flow (m³/h)",
"order": 2,
"width": "6",
"height": "5",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "m³/h",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#4fc3f7",
"#FF7F0E",
"#2CA02C"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"x": 510,
"y": 1260,
"wires": [],
"className": ""
},
{
"id": "demo_ctrl_south_text_status",
"type": "ui-text",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_south_grp_monitoring",
"name": "PS South Status",
"label": "Status",
"order": 1,
"width": "6",
"height": "1",
"format": "{{msg.payload}}",
"layout": "row-spread",
"x": 510,
"y": 1280,
"wires": [],
"className": ""
},
{
"id": "demo_ctrl_south_gauge_fill",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_south_grp_monitoring",
"name": "PS South Fill",
"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": 25
},
{
"color": "#ff9800",
"from": 75
},
{
"color": "#f44336",
"from": 90
}
],
"width": 3,
"height": 3,
"order": 2,
"className": "hide-limits",
"x": 700,
"y": 1280,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_ctrl_south_gauge_tank",
"type": "ui-gauge",
"z": "demo_tab_dashboard",
"group": "demo_ctrl_south_grp_monitoring",
"name": "PS South Tank",
"gtype": "gauge-tank",
"gstyle": "Rounded",
"title": "Level",
"units": "m",
"prefix": "",
"suffix": "m",
"min": 0,
"max": 2.5,
"segments": [
{
"color": "#f44336",
"from": 0
},
{
"color": "#ff9800",
"from": 0.2
},
{
"color": "#2196f3",
"from": 0.625
},
{
"color": "#ff9800",
"from": 1.55
},
{
"color": "#f44336",
"from": 2
}
],
"width": 3,
"height": 4,
"order": 3,
"className": "",
"x": 700,
"y": 1240,
"wires": [],
"value": "payload",
"valueType": "msg",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"alwaysShowTitle": false,
"floatingTitlePosition": "top-left",
"icon": ""
},
{
"id": "demo_css_gauge_fixes",
"type": "ui-template",
"z": "demo_tab_dashboard",
"group": "",
"page": "",
"ui": "demo_ui_base",
"name": "Gauge CSS Fixes",
"order": 0,
"width": 0,
"height": 0,
"head": "",
"format": "<template><div></div></template>\n<style>\n /* Hide min/max limit labels on gauge-34 to prevent\n numeric 'ghost' values behind the dynamic readout */\n .hide-limits #limits {\n display: none !important;\n }\n</style>",
"templateScope": "site:style",
"className": "",
"x": 510,
"y": 920,
"wires": [
[]
]
},
{
"id": "demo_meas_nh4_in",
"type": "measurement",
"z": "demo_tab_treatment",
"name": "NH4-IN (Ammonium Inlet)",
"scaling": true,
"i_min": 0,
"i_max": 50,
"i_offset": 0,
"o_min": 0,
"o_max": 50,
"smooth_method": "mean",
"count": 3,
"simulator": true,
"uuid": "nh4-in-001",
"supplier": "Hach",
"category": "sensor",
"assetType": "ammonium",
"model": "Amtax-sc",
"unit": "mg/L",
"assetTagNumber": "NH4-IN",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"x": 420,
"y": 220,
"wires": [
[
"demo_link_process_out_treatment"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_reactor_z1"
]
],
"positionIcon": "⊥",
"hasDistance": true,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "reactor inlet"
},
{
"id": "demo_ui_grp_nh4_profile",
"type": "ui-group",
"name": "NH4 Profile Along Reactor",
"page": "demo_ui_page_treatment",
"width": "6",
"height": "1",
"order": 6,
"showTitle": true,
"className": ""
},
{
"id": "demo_chart_nh4_profile",
"type": "ui-chart",
"z": "demo_tab_dashboard",
"group": "demo_ui_grp_nh4_profile",
"name": "NH4 Profile",
"label": "NH4 Along Reactor (mg/L)",
"order": 1,
"width": "6",
"height": "5",
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisType": "time",
"yAxisLabel": "mg/L",
"removeOlder": "10",
"removeOlderUnit": "60",
"action": "append",
"pointShape": "false",
"pointRadius": 0,
"interpolation": "linear",
"x": 510,
"y": 1060,
"wires": [],
"showLegend": true,
"xAxisProperty": "",
"xAxisPropertyType": "timestamp",
"yAxisProperty": "payload",
"yAxisPropertyType": "msg",
"colors": [
"#0094ce",
"#FF7F0E",
"#2CA02C",
"#D62728",
"#A347E1",
"#D62728",
"#FF9896",
"#9467BD",
"#C5B0D5"
],
"textColor": [
"#aaaaaa"
],
"textColorDefault": false,
"gridColor": [
"#333333"
],
"gridColorDefault": false,
"className": ""
},
{
"id": "demo_link_nh4_profile_dash",
"type": "link out",
"z": "demo_tab_treatment",
"name": "→ NH4 Profile Dashboard",
"mode": "link",
"links": [
"demo_link_nh4_profile_dash_in"
],
"x": 640,
"y": 340
},
{
"id": "demo_link_nh4_profile_dash_in",
"type": "link in",
"z": "demo_tab_dashboard",
"name": "← NH4 Profile",
"links": [
"demo_link_nh4_profile_dash"
],
"x": 75,
"y": 1060,
"wires": [
[
"demo_chart_nh4_profile"
]
]
},
{
"id": "demo_fn_influent_compose",
"type": "function",
"z": "demo_tab_treatment",
"name": "Influent Composer",
"func": "// Convert merge collector output to Fluent messages for reactor\n// ASM3: [S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS]\nconst p = msg.payload || {};\nconst MUNICIPAL = [0.5, 30, 200, 40, 0, 0, 5, 25, 150, 30, 0, 0, 200];\nconst INDUSTRIAL = [0.5, 40, 300, 25, 0, 0, 4, 30, 100, 20, 0, 0, 150];\nconst RESIDENTIAL = [0.5, 25, 180, 45, 0, 0, 5, 20, 130, 25, 0, 0, 175];\n\nconst Fw = (p.west?.netFlow || 0) * 24; // m3/h -> m3/d\nconst Fn = (p.north?.netFlow || 0) * 24;\nconst Fs = (p.south?.netFlow || 0) * 24;\n\nconst msgs = [];\nif (Fw > 0) msgs.push({ topic: 'Fluent', payload: { inlet: 0, F: Fw, C: MUNICIPAL }});\nif (Fn > 0) msgs.push({ topic: 'Fluent', payload: { inlet: 1, F: Fn, C: INDUSTRIAL }});\nif (Fs > 0) msgs.push({ topic: 'Fluent', payload: { inlet: 2, F: Fs, C: RESIDENTIAL }});\nreturn [msgs];",
"outputs": 1,
"x": 720,
"y": 1100,
"wires": [
[
"demo_reactor_z1"
]
]
},
{
"id": "demo_pump_ras",
"type": "rotatingMachine",
"z": "demo_tab_treatment",
"name": "RAS Pump",
"speed": "1",
"startup": "5",
"warmup": "3",
"shutdown": "4",
"cooldown": "2",
"movementMode": "dynspeed",
"machineCurve": "",
"uuid": "pump-ras-001",
"supplier": "hidrostal",
"category": "machine",
"assetType": "pump-centrifugal",
"model": "hidrostal-C5-D03R-SHN1",
"unit": "m3/h",
"enableLog": true,
"logLevel": "info",
"positionVsParent": "downstream",
"positionIcon": "←",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 1280,
"y": 760,
"wires": [
[
"demo_link_process_out_treatment"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_settler"
]
],
"curveFlowUnit": "l/s",
"curvePressureUnit": "mbar",
"curvePowerUnit": "kW"
},
{
"id": "demo_meas_ft_ras",
"type": "measurement",
"z": "demo_tab_treatment",
"name": "FT-RAS (RAS Flow)",
"scaling": true,
"i_min": 20,
"i_max": 80,
"i_offset": 0,
"o_min": 20,
"o_max": 80,
"smooth_method": "mean",
"count": 3,
"simulator": true,
"uuid": "ft-ras-001",
"supplier": "Endress+Hauser",
"category": "sensor",
"assetType": "flow",
"model": "Promag-W400",
"unit": "m3/h",
"assetTagNumber": "FT-RAS",
"enableLog": false,
"logLevel": "error",
"positionVsParent": "atEquipment",
"positionIcon": "⊥",
"hasDistance": false,
"distance": 0,
"distanceUnit": "m",
"distanceDescription": "",
"x": 1500,
"y": 760,
"wires": [
[
"demo_link_process_out_treatment"
],
[
"demo_link_influx_out_treatment"
],
[
"demo_pump_ras"
]
]
},
{
"id": "demo_inj_ras_mode",
"type": "inject",
"z": "demo_tab_treatment",
"name": "RAS → virtualControl",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"vt": "str"
}
],
"topic": "setMode",
"payload": "virtualControl",
"payloadType": "str",
"once": true,
"onceDelay": "3",
"x": 1280,
"y": 820,
"wires": [
[
"demo_pump_ras"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_inj_ras_speed",
"type": "inject",
"z": "demo_tab_treatment",
"name": "RAS speed → 50%",
"props": [
{
"p": "topic",
"vt": "str"
},
{
"p": "payload",
"vt": "json"
}
],
"topic": "execMovement",
"payload": "{\"source\":\"auto\",\"action\":\"setpoint\",\"setpoint\":50}",
"payloadType": "json",
"once": true,
"onceDelay": "4",
"x": 1280,
"y": 880,
"wires": [
[
"demo_pump_ras"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_fn_ras_filter",
"type": "function",
"z": "demo_tab_treatment",
"name": "RAS Filter",
"func": "// Only pass RAS (inlet 2) from settler to reactor zone 1 as inlet 3\nif (msg.topic === 'Fluent' && msg.payload && msg.payload.inlet === 2) {\n msg.payload.inlet = 3;\n return msg;\n}\nreturn null;",
"outputs": 1,
"x": 1060,
"y": 760,
"wires": [
[
"demo_reactor_z1"
]
]
},
{
"id": "demo_fn_prep_grafana",
"type": "function",
"z": "demo_tab_wwtp",
"name": "Prep Grafana Request",
"func": "// Stringify payload for http request node (Node-RED 4.x requires string body)\nmsg.payload = JSON.stringify(msg.payload);\nmsg.headers = msg.headers || {};\nmsg.headers['Content-Type'] = 'application/json';\n// Force local dev auth so stale bearer/api-key headers cannot downgrade requests to anonymous.\nmsg.headers.Authorization = 'Basic ' + Buffer.from('admin:evolv').toString('base64');\nreturn msg;",
"outputs": 1,
"x": 1100,
"y": 1100,
"wires": [
[
"demo_http_grafana"
]
]
},
{
"id": "demo_inj_dashapi_west",
"type": "inject",
"z": "demo_tab_wwtp",
"name": "Gen dashboards: PS West",
"props": [
{
"p": "topic",
"v": "registerChild",
"vt": "str"
},
{
"p": "payload",
"v": "demo_ps_west",
"vt": "str"
},
{
"p": "includeChildren",
"v": "true",
"vt": "bool"
}
],
"once": true,
"onceDelay": "20",
"x": 810,
"y": 1080,
"wires": [
[
"demo_dashapi"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_inj_dashapi_north",
"type": "inject",
"z": "demo_tab_wwtp",
"name": "Gen dashboards: PS North",
"props": [
{
"p": "topic",
"v": "registerChild",
"vt": "str"
},
{
"p": "payload",
"v": "demo_ps_north",
"vt": "str"
},
{
"p": "includeChildren",
"v": "true",
"vt": "bool"
}
],
"once": true,
"onceDelay": "22",
"x": 810,
"y": 1120,
"wires": [
[
"demo_dashapi"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_inj_dashapi_south",
"type": "inject",
"z": "demo_tab_wwtp",
"name": "Gen dashboards: PS South",
"props": [
{
"p": "topic",
"v": "registerChild",
"vt": "str"
},
{
"p": "payload",
"v": "demo_ps_south",
"vt": "str"
},
{
"p": "includeChildren",
"v": "true",
"vt": "bool"
}
],
"once": true,
"onceDelay": "24",
"x": 810,
"y": 1160,
"wires": [
[
"demo_dashapi"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_inj_dashapi_settler",
"type": "inject",
"z": "demo_tab_wwtp",
"name": "Gen dashboards: Settler S1",
"props": [
{
"p": "topic",
"v": "registerChild",
"vt": "str"
},
{
"p": "payload",
"v": "demo_settler",
"vt": "str"
},
{
"p": "includeChildren",
"v": "true",
"vt": "bool"
}
],
"once": true,
"onceDelay": "34",
"x": 810,
"y": 1200,
"wires": [
[
"demo_dashapi"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_inj_dashapi_reactor_z1",
"type": "inject",
"z": "demo_tab_wwtp",
"name": "Gen dashboards: Reactor Z1",
"props": [
{
"p": "topic",
"v": "registerChild",
"vt": "str"
},
{
"p": "payload",
"v": "demo_reactor_z1",
"vt": "str"
},
{
"p": "includeChildren",
"v": "true",
"vt": "bool"
}
],
"once": true,
"onceDelay": "26",
"x": 810,
"y": 1200,
"wires": [
[
"demo_dashapi"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_inj_dashapi_reactor_z2",
"type": "inject",
"z": "demo_tab_wwtp",
"name": "Gen dashboards: Reactor Z2",
"props": [
{
"p": "topic",
"v": "registerChild",
"vt": "str"
},
{
"p": "payload",
"v": "demo_reactor_z2",
"vt": "str"
},
{
"p": "includeChildren",
"v": "true",
"vt": "bool"
}
],
"once": true,
"onceDelay": "28",
"x": 810,
"y": 1240,
"wires": [
[
"demo_dashapi"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_inj_dashapi_reactor_z3",
"type": "inject",
"z": "demo_tab_wwtp",
"name": "Gen dashboards: Reactor Z3",
"props": [
{
"p": "topic",
"v": "registerChild",
"vt": "str"
},
{
"p": "payload",
"v": "demo_reactor_z3",
"vt": "str"
},
{
"p": "includeChildren",
"v": "true",
"vt": "bool"
}
],
"once": true,
"onceDelay": "30",
"x": 810,
"y": 1280,
"wires": [
[
"demo_dashapi"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_inj_dashapi_reactor_z4",
"type": "inject",
"z": "demo_tab_wwtp",
"name": "Gen dashboards: Reactor Z4",
"props": [
{
"p": "topic",
"v": "registerChild",
"vt": "str"
},
{
"p": "payload",
"v": "demo_reactor_z4",
"vt": "str"
},
{
"p": "includeChildren",
"v": "true",
"vt": "bool"
}
],
"once": true,
"onceDelay": "32",
"x": 810,
"y": 1320,
"wires": [
[
"demo_dashapi"
]
],
"repeatType": "none",
"crontab": "",
"repeat": ""
},
{
"id": "demo_fn_nh4_profile_extract",
"type": "function",
"z": "demo_tab_treatment",
"name": "Extract Zone NH4 Profiles",
"func": "if (msg.topic !== 'GridProfile') return null;\nconst p = msg.payload;\nif (!p || !Array.isArray(p.grid) || !p.grid.length) return null;\nconst zone = msg.zoneLabel || 'Zone';\nconst now = Date.now();\nconst S_NH = 3;\nconst indices = [\n { label: 'In', index: 0 },\n { label: 'Mid', index: Math.max(0, Math.round((p.n_x - 1) / 2)) },\n { label: 'Out', index: p.n_x - 1 },\n];\nconst msgs = indices.map(({ label, index }) => {\n const row = p.grid[index];\n if (!Array.isArray(row)) return null;\n return { topic: `${zone} NH4 ${label}`, payload: Math.round(Number(row[S_NH]) * 100) / 100, timestamp: now };\n}).filter(Boolean);\nreturn [msgs];",
"outputs": 1,
"x": 640,
"y": 280,
"wires": [
[
"demo_link_nh4_profile_dash"
]
]
},
{
"id": "demo_inj_reactor_dispersion",
"type": "inject",
"z": "demo_tab_treatment",
"name": "Reactor Dispersion D=25",
"props": [
{
"p": "topic",
"vt": "str",
"v": "Dispersion"
},
{
"p": "payload",
"vt": "num",
"v": "25"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "0.5",
"topic": "Dispersion",
"x": 620,
"y": 160,
"wires": [
[
"demo_reactor_z1",
"demo_reactor_z2",
"demo_reactor_z3",
"demo_reactor_z4"
]
]
}
]