[ { "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": true, "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": true, "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": true, "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": true, "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" ] ], "distance": 0, "distanceUnit": "m", "distanceDescription": "reactor inlet" }, { "id": "demo_meas_do", "type": "measurement", "z": "demo_tab_treatment", "name": "DO-001 (Dissolved O2)", "scaling": true, "i_min": 0, "i_max": 20, "i_offset": 0, "o_min": 0, "o_max": 20, "smooth_method": "mean", "count": 3, "simulator": true, "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" ] ], "positionIcon": "⊥", "hasDistance": true, "distance": 15, "distanceUnit": "m", "distanceDescription": "aeration zone" }, { "id": "demo_meas_nh4", "type": "measurement", "z": "demo_tab_treatment", "name": "NH4-001 (Ammonium)", "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-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" ] ], "positionIcon": "⊥", "hasDistance": true, "distance": 35, "distanceUnit": "m", "distanceDescription": "post-aeration zone" }, { "id": "demo_reactor", "type": "reactor", "z": "demo_tab_treatment", "name": "Bioreactor R1", "reactor_type": "PFR", "volume": 2000, "length": 50, "resolution_L": 50, "alpha": 0, "n_inlets": 4, "kla": 70, "S_O_init": 2, "S_I_init": 30, "S_S_init": 100, "S_NH_init": 16, "S_N2_init": 0, "S_NO_init": 0, "S_HCO_init": 8, "X_I_init": 25, "X_S_init": 75, "X_H_init": 1500, "X_STO_init": 0, "X_A_init": 15, "X_TS_init": 2500, "timeStep": 1, "uuid": "reactor-r1-001", "enableLog": true, "logLevel": "info", "positionVsParent": "upstream", "x": 820, "y": 220, "wires": [ [ "demo_link_reactor_dash", "demo_link_process_out_treatment", "demo_link_overview_reactor_out", "demo_fn_nh4_profile_extract" ], [ "demo_link_influx_out_treatment" ], [ "demo_dbg_registration", "demo_settler" ] ], "speedUpFactor": 120, "positionIcon": "→" }, { "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": true, "onceDelay": "4", "x": 200, "y": 120, "wires": [ [ "demo_reactor" ] ], "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_link_influx_out_treatment" ], [ "demo_dbg_registration" ] ], "positionIcon": "←" }, { "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": "glsa_K6F9WJ6e6uWyGQpz0CKfHuEYepgoyIhW_9398347f", "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": "10", "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": "10", "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": "10", "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", "func": "const p = msg.payload || {};\nif (!p.C || !Array.isArray(p.C)) return null;\nconst now = Date.now();\nreturn [\n {topic:'DO (S_O)', payload: Math.round(p.C[0]*100)/100, timestamp: now},\n {topic:'NH4 (S_NH)', payload: Math.round(p.C[3]*100)/100, timestamp: now},\n {topic:'NO3 (S_NO)', payload: Math.round(p.C[5]*100)/100, timestamp: now},\n {topic:'COD (S_S)', payload: Math.round(p.C[2]*100)/100, timestamp: now},\n {topic:'TSS (X_TS)', payload: Math.round(p.C[12]*100)/100, timestamp: now},\n {payload: `DO: ${p.C[0].toFixed(1)} | NH4: ${p.C[3].toFixed(1)} | NO3: ${p.C[5].toFixed(1)} | TSS: ${p.C[12].toFixed(0)} mg/L`}\n];", "outputs": 6, "x": 250, "y": 550, "wires": [ [ "demo_chart_reactor_do" ], [ "demo_chart_reactor_n" ], [ "demo_chart_reactor_n" ], [ "demo_chart_reactor_cod" ], [ "demo_chart_reactor_cod" ], [ "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 Flow';\nelse if (topic.includes('do') || topic.includes('DO')) label = 'DO-001 Oxygen';\nelse if (topic.includes('nh4') || topic.includes('NH4')) label = 'NH4-001 Ammonium';\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 Nitrogen", "label": "Nitrogen Species (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", "#2CA02C", "#D62728" ], "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_settler", "name": "Reactor COD/TSS", "label": "COD & TSS (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": 600, "wires": [], "showLegend": true, "xAxisProperty": "", "xAxisPropertyType": "timestamp", "yAxisProperty": "payload", "yAxisPropertyType": "msg", "colors": [ "#9467BD", "#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_effluent", "name": "Reactor Status", "label": "Effluent Quality", "order": 1, "width": "6", "height": "1", "format": "{{msg.payload}}", "layout": "row-spread", "x": 510, "y": 640, "wires": [], "className": "" }, { "id": "demo_chart_meas_abs", "type": "ui-chart", "z": "demo_tab_dashboard", "group": "demo_ui_grp_measurements", "name": "Sensor Values", "label": "Sensor 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": "Sensor %", "label": "Sensor 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": true, "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": true, "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": true, "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": true, "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": true, "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": true, "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": true, "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": true, "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": true, "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": true, "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": true, "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": "\n\n\n\n", "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": "\n", "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" ] ], "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" ] ] }, { "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 as inlet 3\nif (msg.topic === 'Fluent' && msg.payload && msg.payload.inlet === 2) {\n msg.payload.inlet = 3; // reactor inlet 3 = RAS\n return msg;\n}\nreturn null;", "outputs": 1, "x": 1060, "y": 760, "wires": [ [ "demo_reactor" ] ] }, { "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';\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": "26", "x": 810, "y": 1200, "wires": [ [ "demo_dashapi" ] ], "repeatType": "none", "crontab": "", "repeat": "" }, { "id": "demo_fn_nh4_profile_extract", "type": "function", "z": "demo_tab_treatment", "name": "Extract NH4 Profile", "func": "if (msg.topic !== \"GridProfile\") return null;\nconst p = msg.payload;\nif (!p || !p.grid || !Array.isArray(p.grid)) return null;\n\nconst S_NH = 3;\nconst d_x = p.d_x;\nconst n_x = p.n_x;\nconst now = Date.now();\n\nconst positions = [0, 10, 25, 35, 50];\nconst msgs = positions.map(dist => {\n const idx = Math.min(Math.max(Math.round(dist / d_x), 0), n_x - 1);\n const val = p.grid[idx] ? p.grid[idx][S_NH] : null;\n if (val == null) return null;\n return { topic: \"NH4 @ \" + dist + \"m\", payload: Math.round(val * 100) / 100, timestamp: now };\n}).filter(Boolean);\n\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=10", "props": [ { "p": "topic", "vt": "str", "v": "Dispersion" }, { "p": "payload", "vt": "num", "v": "10" } ], "repeat": "", "crontab": "", "once": true, "onceDelay": "0.5", "topic": "Dispersion", "x": 620, "y": 160, "wires": [ [ "demo_reactor" ] ] } ]