#!/usr/bin/env node /** * Patch demo-flow.json: * Phase A: Add 4 NH4 measurement nodes + ui-group + ui-chart * Phase B: Add influent composer function node + wire merge collector * Phase C: Fix biomass init on reactor * Phase D: Add RAS pump, flow sensor, 2 injects, filter function + wiring */ const fs = require('fs'); const path = require('path'); const flowPath = path.join(__dirname, '..', 'docker', 'demo-flow.json'); const flow = JSON.parse(fs.readFileSync(flowPath, 'utf8')); // Helper: find node by id const findNode = (id) => flow.find(n => n.id === id); // ============================================================ // PHASE A: Add 4 NH4 measurement nodes + ui-group + ui-chart // ============================================================ const nh4Measurements = [ { id: 'demo_meas_nh4_in', name: 'NH4-IN (Ammonium Inlet)', uuid: 'nh4-in-001', assetTagNumber: 'NH4-IN', distance: 0, distanceDescription: 'reactor inlet', y: 280 }, { id: 'demo_meas_nh4_a', name: 'NH4-A (Early Aeration)', uuid: 'nh4-a-001', assetTagNumber: 'NH4-A', distance: 10, distanceDescription: 'early aeration zone', y: 320 }, { id: 'demo_meas_nh4_b', name: 'NH4-B (Mid-Reactor)', uuid: 'nh4-b-001', assetTagNumber: 'NH4-B', distance: 25, distanceDescription: 'mid-reactor', y: 360 }, { id: 'demo_meas_nh4_c', name: 'NH4-C (Near Outlet)', uuid: 'nh4-c-001', assetTagNumber: 'NH4-C', distance: 45, distanceDescription: 'near outlet', y: 400 } ]; for (const m of nh4Measurements) { flow.push({ id: m.id, type: 'measurement', z: 'demo_tab_treatment', name: m.name, 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: m.uuid, supplier: 'Hach', category: 'sensor', assetType: 'ammonium', model: 'Amtax-sc', unit: 'mg/L', assetTagNumber: m.assetTagNumber, enableLog: false, logLevel: 'error', positionVsParent: 'atEquipment', x: 400, y: m.y, wires: [ ['demo_link_meas_dash', 'demo_link_process_out_treatment'], ['demo_link_influx_out_treatment'], ['demo_reactor'] ], positionIcon: '⊥', hasDistance: true, distance: m.distance, distanceUnit: 'm', distanceDescription: m.distanceDescription }); } // NH4 profile ui-group flow.push({ 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: '' }); // NH4 profile chart flow.push({ 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: '' }); // Link out + link in for NH4 profile chart flow.push({ 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: 620, y: 340 }); flow.push({ 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_fn_nh4_profile_parse']] }); // Parse function for NH4 profile chart flow.push({ id: 'demo_fn_nh4_profile_parse', type: 'function', z: 'demo_tab_dashboard', name: 'Parse NH4 Profile', func: `const p = msg.payload || {}; const topic = msg.topic || ''; const now = Date.now(); const val = Number(p.mAbs); if (!Number.isFinite(val)) return null; let label = topic; if (topic.includes('NH4-IN')) label = 'NH4-IN (0m)'; else if (topic.includes('NH4-A')) label = 'NH4-A (10m)'; else if (topic.includes('NH4-B')) label = 'NH4-B (25m)'; else if (topic.includes('NH4-001')) label = 'NH4-001 (35m)'; else if (topic.includes('NH4-C')) label = 'NH4-C (45m)'; return { topic: label, payload: Math.round(val * 100) / 100, timestamp: now };`, outputs: 1, x: 280, y: 1060, wires: [['demo_chart_nh4_profile']] }); // Wire existing NH4-001 and new NH4 measurements to the profile link out const existingNh4 = findNode('demo_meas_nh4'); if (existingNh4) { if (!existingNh4.wires[0].includes('demo_link_nh4_profile_dash')) { existingNh4.wires[0].push('demo_link_nh4_profile_dash'); } } for (const m of nh4Measurements) { const node = findNode(m.id); if (node && !node.wires[0].includes('demo_link_nh4_profile_dash')) { node.wires[0].push('demo_link_nh4_profile_dash'); } } console.log('Phase A: Added 4 NH4 measurements + ui-group + chart + wiring'); // ============================================================ // PHASE B: Add influent composer + wire merge collector // ============================================================ flow.push({ id: 'demo_fn_influent_compose', type: 'function', z: 'demo_tab_treatment', name: 'Influent Composer', func: `// Convert merge collector output to Fluent messages for reactor // 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] const p = msg.payload || {}; const MUNICIPAL = [0.5, 30, 200, 40, 0, 0, 5, 25, 150, 30, 0, 0, 200]; const INDUSTRIAL = [0.5, 40, 300, 25, 0, 0, 4, 30, 100, 20, 0, 0, 150]; const RESIDENTIAL = [0.5, 25, 180, 45, 0, 0, 5, 20, 130, 25, 0, 0, 175]; const Fw = (p.west?.netFlow || 0) * 24; // m3/h -> m3/d const Fn = (p.north?.netFlow || 0) * 24; const Fs = (p.south?.netFlow || 0) * 24; const msgs = []; if (Fw > 0) msgs.push({ topic: 'Fluent', payload: { inlet: 0, F: Fw, C: MUNICIPAL }}); if (Fn > 0) msgs.push({ topic: 'Fluent', payload: { inlet: 1, F: Fn, C: INDUSTRIAL }}); if (Fs > 0) msgs.push({ topic: 'Fluent', payload: { inlet: 2, F: Fs, C: RESIDENTIAL }}); return [msgs];`, outputs: 1, x: 480, y: 1040, wires: [['demo_reactor']] }); // Wire merge collector → influent composer (add to existing wires) const mergeCollect = findNode('demo_fn_merge_collect'); if (mergeCollect) { if (!mergeCollect.wires[0].includes('demo_fn_influent_compose')) { mergeCollect.wires[0].push('demo_fn_influent_compose'); } console.log('Phase B: Wired merge collector → influent composer → reactor'); } else { console.error('Phase B: ERROR — demo_fn_merge_collect not found!'); } // ============================================================ // PHASE C: Fix biomass initialization // ============================================================ const reactor = findNode('demo_reactor'); if (reactor) { reactor.X_A_init = 300; reactor.X_H_init = 1500; reactor.X_TS_init = 2500; reactor.S_HCO_init = 8; console.log('Phase C: Updated reactor biomass init values'); } else { console.error('Phase C: ERROR — demo_reactor not found!'); } // ============================================================ // PHASE D: Return Activated Sludge // ============================================================ // D1: RAS pump flow.push({ 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-RAS', unit: 'm3/h', enableLog: true, logLevel: 'info', positionVsParent: 'downstream', positionIcon: '←', hasDistance: false, distance: 0, distanceUnit: 'm', distanceDescription: '', x: 1000, y: 380, wires: [ ['demo_link_process_out_treatment'], ['demo_link_influx_out_treatment'], ['demo_settler'] ], curveFlowUnit: 'l/s', curvePressureUnit: 'mbar', curvePowerUnit: 'kW' }); // D2: RAS flow sensor flow.push({ 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: 1200, y: 380, wires: [ ['demo_link_process_out_treatment'], ['demo_link_influx_out_treatment'], ['demo_pump_ras'] ] }); // D3: Inject to set pump mode flow.push({ 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: 1000, y: 440, wires: [['demo_pump_ras']], repeatType: 'none', crontab: '', repeat: '' }); // D3: Inject to set pump speed flow.push({ 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: 1000, y: 480, wires: [['demo_pump_ras']], repeatType: 'none', crontab: '', repeat: '' }); // D4: RAS filter function flow.push({ 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 if (msg.topic === 'Fluent' && msg.payload && msg.payload.inlet === 2) { msg.payload.inlet = 3; // reactor inlet 3 = RAS return msg; } return null;`, outputs: 1, x: 1000, y: 320, wires: [['demo_reactor']] }); // D5: Wire settler Port 0 → RAS filter const settler = findNode('demo_settler'); if (settler) { if (!settler.wires[0].includes('demo_fn_ras_filter')) { settler.wires[0].push('demo_fn_ras_filter'); } console.log('Phase D: Wired settler → RAS filter → reactor'); } else { console.error('Phase D: ERROR — demo_settler not found!'); } // D5: Update reactor n_inlets: 3 → 4 if (reactor) { reactor.n_inlets = 4; console.log('Phase D: Updated reactor n_inlets to 4'); } console.log('Phase D: Added RAS pump, flow sensor, 2 injects, filter function'); // ============================================================ // WRITE OUTPUT // ============================================================ fs.writeFileSync(flowPath, JSON.stringify(flow, null, 2) + '\n', 'utf8'); console.log(`\nDone. Wrote ${flow.length} nodes to ${flowPath}`);