Files
EVOLV/scripts/patch-flow.js
znetsixe 6a6c04d34b Migrate to new Gitea instance (gitea.wbd-rd.nl)
- Update all submodule URLs from gitea.centraal.wbd-rd.nl to gitea.wbd-rd.nl
- Add settler as proper submodule in .gitmodules
- Add agent skills, function anchors, decisions, and improvements
- Add Docker configuration and scripts
- Add manuals and third_party docs
- Update .gitignore with secrets and build artifacts
- Remove stale .tgz build artifact

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:07:04 +01:00

456 lines
12 KiB
JavaScript

#!/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}`);