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>
This commit is contained in:
455
scripts/patch-flow.js
Normal file
455
scripts/patch-flow.js
Normal file
@@ -0,0 +1,455 @@
|
||||
#!/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}`);
|
||||
Reference in New Issue
Block a user