Compare commits

..

24 Commits

Author SHA1 Message Date
d0db5c8999 Merge pull request 'dev-Rene' (#1) from dev-Rene into main
Reviewed-on: #1
2025-12-08 10:00:06 +00:00
znetsixe
7efd3b0a07 bug fixes 2025-11-30 20:13:21 +01:00
znetsixe
c81ee1b470 fixed change mode and control logic method 2025-11-30 17:46:07 +01:00
znetsixe
955c17a466 bug fixes 2025-11-30 09:24:18 +01:00
Rene De ren
052ded7b6e fixes 2025-11-28 16:29:05 +01:00
znetsixe
321ea33bf7 rebuilding pumping station NOT WORKING 2025-11-28 09:59:16 +01:00
znetsixe
288bd244dd updating to corrospend with reality 2025-11-27 17:46:24 +01:00
znetsixe
d91609b3a4 updates to safety features 2025-11-25 14:57:39 +01:00
znetsixe
5a575a29fe updated pumpingstation 2025-11-20 12:15:46 +01:00
znetsixe
0a6c7ee2e1 Further bug fixes and optimized level control for groups and machines alike 2025-11-13 19:37:41 +01:00
znetsixe
4cc529b1c2 Fixes next idle machine for level control 2025-11-12 17:37:09 +01:00
znetsixe
fbfcec4b47 Added simpel case for level control 2025-11-10 16:20:23 +01:00
znetsixe
43eb97407f added safeguarding when vol gets too low for machines, 2025-11-07 15:07:56 +01:00
znetsixe
9e4b149b64 fixed multiple children being able to pull and push to pumpingstation 2025-11-06 16:46:54 +01:00
znetsixe
1848486f1c bug fixes output formatting 2025-11-06 11:19:20 +01:00
znetsixe
d44cbc978b updates visual 2025-11-03 09:17:22 +01:00
znetsixe
f243761f00 Updated node status 2025-11-03 07:42:51 +01:00
znetsixe
2a31c7ec69 working pumpingstation with machines 2025-10-28 17:04:26 +01:00
znetsixe
69f68adffe testing codex 2025-10-27 19:55:48 +01:00
znetsixe
5a1eff37d7 Need to remove wobble on level only 2025-10-27 17:45:48 +01:00
znetsixe
e8f9207a92 some major design choises updated 2025-10-27 16:39:06 +01:00
znetsixe
6e9ae9fc7e Need to stich everything together then V1.0 is done. 2025-10-23 18:04:18 +02:00
znetsixe
371f3c65e7 updated retrieval mechanism 2025-10-23 09:51:54 +02:00
znetsixe
b8b7871e38 update before closing 2025-10-21 13:44:31 +02:00
3 changed files with 1004 additions and 360 deletions

View File

@@ -24,6 +24,12 @@
heightInlet: { value: 0.8 }, // m, centre of inlet pipe above floor heightInlet: { value: 0.8 }, // m, centre of inlet pipe above floor
heightOutlet: { value: 0.2 }, // m, centre of outlet pipe above floor heightOutlet: { value: 0.2 }, // m, centre of outlet pipe above floor
heightOverflow: { value: 0.9 }, // m, overflow elevation heightOverflow: { value: 0.9 }, // m, overflow elevation
timeleftToFullOrEmptyThresholdSeconds:{value:0}, // time threshold to safeguard starting or stopping pumps in seconds
enableDryRunProtection: { value: true },
enableOverfillProtection: { value: true },
dryRunThresholdPercent: { value: 2 },
overfillThresholdPercent: { value: 98 },
minHeightBasedOn: { value: "outlet" }, // basis for minimum height check: inlet or outlet
// Advanced reference information // Advanced reference information
refHeight: { value: "NAP" }, // reference height refHeight: { value: "NAP" }, // reference height
@@ -47,7 +53,16 @@
hasDistance: { value: false }, hasDistance: { value: false },
distance: { value: 0 }, distance: { value: 0 },
distanceUnit: { value: "m" }, distanceUnit: { value: "m" },
distanceDescription: { value: "" } distanceDescription: { value: "" },
// control strategy
controlMode: { value: "none" },
startLevel: { value: null },
stopLevel: { value: null },
minFlowLevel: { value: null },
maxFlowLevel: { value: null },
flowSetpoint: { value: null },
flowDeadband: { value: null }
}, },
@@ -86,6 +101,68 @@
refHeightEl.value = this.refHeight || "NAP"; refHeightEl.value = this.refHeight || "NAP";
} }
const minHeightBasedOnEl = document.getElementById("node-input-minHeightBasedOn");
if (minHeightBasedOnEl) {
minHeightBasedOnEl.value = this.minHeightBasedOn;
}
const dryRunToggle = document.getElementById("node-input-enableDryRunProtection");
const dryRunPercent = document.getElementById("node-input-dryRunThresholdPercent");
const overfillToggle = document.getElementById("node-input-enableOverfillProtection");
const overfillPercent = document.getElementById("node-input-overfillThresholdPercent");
const toggleInput = (toggleEl, inputEl) => {
if (!toggleEl || !inputEl) { return; }
inputEl.disabled = !toggleEl.checked;
inputEl.parentElement.classList.toggle('disabled', inputEl.disabled);
};
if (dryRunToggle && dryRunPercent) {
dryRunToggle.checked = !!this.enableDryRunProtection;
dryRunPercent.value = Number.isFinite(this.dryRunThresholdPercent) ? this.dryRunThresholdPercent : 2;
dryRunToggle.addEventListener('change', () => toggleInput(dryRunToggle, dryRunPercent));
toggleInput(dryRunToggle, dryRunPercent);
}
if (overfillToggle && overfillPercent) {
overfillToggle.checked = !!this.enableOverfillProtection;
overfillPercent.value = Number.isFinite(this.overfillThresholdPercent) ? this.overfillThresholdPercent : 98;
overfillToggle.addEventListener('change', () => toggleInput(overfillToggle, overfillPercent));
toggleInput(overfillToggle, overfillPercent);
}
const timeLeftInput = document.getElementById("node-input-timeleftToFullOrEmptyThresholdSeconds");
if (timeLeftInput) {
timeLeftInput.value = Number.isFinite(this.timeleftToFullOrEmptyThresholdSeconds)
? this.timeleftToFullOrEmptyThresholdSeconds
: 0;
}
// control mode toggle UI
const toggleModeSections = (val) => {
document.querySelectorAll('.ps-mode-section').forEach((el) => el.style.display = 'none');
const active = document.getElementById(`ps-mode-${val}`);
if (active) active.style.display = '';
};
const modeSelect = document.getElementById('node-input-controlMode');
if (modeSelect) {
modeSelect.value = this.controlMode || 'none';
toggleModeSections(modeSelect.value);
modeSelect.addEventListener('change', (e) => toggleModeSections(e.target.value));
}
const setNumberField = (id, val) => {
const el = document.getElementById(id);
if (el) el.value = Number.isFinite(val) ? val : '';
};
setNumberField('node-input-startLevel', this.startLevel);
setNumberField('node-input-stopLevel', this.stopLevel);
setNumberField('node-input-minFlowLevel', this.minFlowLevel);
setNumberField('node-input-maxFlowLevel', this.maxFlowLevel);
setNumberField('node-input-flowSetpoint', this.flowSetpoint);
setNumberField('node-input-flowDeadband', this.flowDeadband);
//------------------- END OF CUSTOM config UI ELEMENTS ------------------- // //------------------- END OF CUSTOM config UI ELEMENTS ------------------- //
}, },
@@ -98,14 +175,29 @@
//node specific //node specific
node.refHeight = document.getElementById("node-input-refHeight").value || "NAP"; node.refHeight = document.getElementById("node-input-refHeight").value || "NAP";
node.minHeightBasedOn = document.getElementById("node-input-minHeightBasedOn").value || "outlet";
node.simulator = document.getElementById("node-input-simulator").checked; node.simulator = document.getElementById("node-input-simulator").checked;
["basinVolume","basinHeight","heightInlet","heightOutlet","heightOverflow","basinBottomRef"] ["basinVolume","basinHeight","heightInlet","heightOutlet","heightOverflow","basinBottomRef","timeleftToFullOrEmptyThresholdSeconds","dryRunThresholdPercent","overfillThresholdPercent"]
.forEach(field => { .forEach(field => {
node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0; node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0;
}); });
node.refHeight = document.getElementById("node-input-refHeight").value || ""; node.refHeight = document.getElementById("node-input-refHeight").value || "";
node.enableDryRunProtection = document.getElementById("node-input-enableDryRunProtection").checked;
node.enableOverfillProtection = document.getElementById("node-input-enableOverfillProtection").checked;
// control strategy
node.controlMode = document.getElementById('node-input-controlMode').value || 'none';
const parseNum = (id) => parseFloat(document.getElementById(id)?.value);
node.startLevel = parseNum('node-input-startLevel');
node.stopLevel = parseNum('node-input-stopLevel');
node.minFlowLevel = parseNum('node-input-minFlowLevel');
node.maxFlowLevel = parseNum('node-input-maxFlowLevel');
node.flowSetpoint = parseNum('node-input-flowSetpoint');
node.flowDeadband = parseNum('node-input-flowDeadband');
}, },
}); });
@@ -115,7 +207,7 @@
<script type="text/html" data-template-name="pumpingStation"> <script type="text/html" data-template-name="pumpingStation">
<!-- Simulator toggle --> <h4>Simulation</h4>
<div class="form-row"> <div class="form-row">
<label for="node-input-simulator"><i class="fa fa-play-circle"></i> Simulator</label> <label for="node-input-simulator"><i class="fa fa-play-circle"></i> Simulator</label>
<input type="checkbox" id="node-input-simulator" style="width:20px;vertical-align:baseline;" /> <input type="checkbox" id="node-input-simulator" style="width:20px;vertical-align:baseline;" />
@@ -123,8 +215,8 @@
</div> </div>
<hr> <hr>
<!-- Basin geometry --> <h4>Basin Geometry</h4>
<div class="form-row"> <div class="form-row">
<label for="node-input-basinVolume"><i class="fa fa-cube"></i> Basin Volume (m³)</label> <label for="node-input-basinVolume"><i class="fa fa-cube"></i> Basin Volume (m³)</label>
<input type="number" id="node-input-basinVolume" min="0" step="0.1" /> <input type="number" id="node-input-basinVolume" min="0" step="0.1" />
@@ -150,7 +242,58 @@
<hr> <hr>
<h4>Control Strategy</h4>
<div class="form-row">
<label for="node-input-controlMode"><i class="fa fa-sliders"></i> Control mode</label>
<select id="node-input-controlMode">
<option value="none">None / Manual</option>
<option value="levelbased">Level-based</option>
<option value="flowbased">Flow-based</option>
</select>
</div>
<div id="ps-mode-levelbased" class="ps-mode-section">
<div class="form-row">
<label for="node-input-startLevel">startLevel</label>
<input type="number" id="node-input-startLevel" placeholder="m" />
</div>
<div class="form-row">
<label for="node-input-stopLevel">stopLevel</label>
<input type="number" id="node-input-stopLevel" placeholder="m" />
</div>
<div class="form-row">
<label for="node-input-minFlowLevel">Min flow (m)</label>
<input type="number" id="node-input-minFlowLevel" placeholder="m" />
</div>
<div class="form-row">
<label for="node-input-maxFlowLevel">Max flow (m)</label>
<input type="number" id="node-input-maxFlowLevel" placeholder="m" />
</div>
</div>
<div id="ps-mode-flowbased" class="ps-mode-section" style="display:none">
<div class="form-row">
<label for="node-input-flowSetpoint">Flow setpoint</label>
<input type="number" id="node-input-flowSetpoint" placeholder="m3/h" />
</div>
<div class="form-row">
<label for="node-input-flowDeadband">Deadband</label>
<input type="number" id="node-input-flowDeadband" placeholder="m3/h" />
</div>
</div>
<hr>
<h4>Reference</h4>
<!-- Reference data --> <!-- Reference data -->
<div class="form-row">
<label for="node-input-minHeightBasedOn"><i class="fa fa-arrows-v"></i> Minimum Height Based On</label>
<select id="node-input-minHeightBasedOn" style="width:60%;">
<option value="inlet">Inlet Elevation</option>
<option value="outlet">Outlet Elevation</option>
</select>
</div>
<div class="form-row"> <div class="form-row">
<label for="node-input-refHeight"><i class="fa fa-map-marker"></i> Reference height</label> <label for="node-input-refHeight"><i class="fa fa-map-marker"></i> Reference height</label>
<select id="node-input-refHeight" style="width:60%;"> <select id="node-input-refHeight" style="width:60%;">
@@ -163,6 +306,40 @@
<input type="number" id="node-input-basinBottomRef" step="0.01" /> <input type="number" id="node-input-basinBottomRef" step="0.01" />
</div> </div>
<hr>
<h4>Safety</h4>
<!-- Safety settings -->
<div class="form-row">
<label for="node-input-timeleftToFullOrEmptyThresholdSeconds"><i class="fa fa-clock-o"></i> Time To Empty/Full (s)</label>
<input type="number" id="node-input-timeleftToFullOrEmptyThresholdSeconds" min="0" step="1" />
</div>
<div class="form-row">
<label for="node-input-enableDryRunProtection">
<i class="fa fa-shield"></i> Dry-run Protection
</label>
<input type="checkbox" id="node-input-enableDryRunProtection" style="width:20px;vertical-align:baseline;" />
<span>Prevent pumps from running on low volume</span>
</div>
<div class="form-row">
<label for="node-input-dryRunThresholdPercent" style="padding-left:20px;">Low Volume Threshold (%)</label>
<input type="number" id="node-input-dryRunThresholdPercent" min="0" max="100" step="0.1" />
</div>
<div class="form-row">
<label for="node-input-enableOverfillProtection">
<i class="fa fa-exclamation-triangle"></i> Overfill Protection
</label>
<input type="checkbox" id="node-input-enableOverfillProtection" style="width:20px;vertical-align:baseline;" />
<span>Stop filling when approaching overflow</span>
</div>
<div class="form-row">
<label for="node-input-overfillThresholdPercent" style="padding-left:20px;">High Volume Threshold (%)</label>
<input type="number" id="node-input-overfillThresholdPercent" min="0" max="100" step="0.1" />
</div>
<hr>
<!-- Shared asset/logger/position menus --> <!-- Shared asset/logger/position menus -->
<div id="asset-fields-placeholder"></div> <div id="asset-fields-placeholder"></div>
<div id="logger-fields-placeholder"></div> <div id="logger-fields-placeholder"></div>

View File

@@ -63,7 +63,24 @@ class nodeClass {
}, },
hydraulics:{ hydraulics:{
refHeight: uiConfig.refHeight, refHeight: uiConfig.refHeight,
minHeightBasedOn: uiConfig.minHeightBasedOn,
basinBottomRef: uiConfig.basinBottomRef, basinBottomRef: uiConfig.basinBottomRef,
},
control:{
mode: uiConfig.controlMode,
levelbased:{
startLevel:uiConfig.startLevel,
stopLevel:uiConfig.stopLevel,
minFlowLevel:uiConfig.minFlowLevel,
maxFlowLevel:uiConfig.maxFlowLevel
}
},
safety:{
enableDryRunProtection: uiConfig.enableDryRunProtection,
dryRunThresholdPercent: uiConfig.dryRunThresholdPercent,
enableOverfillProtection: uiConfig.enableOverfillProtection,
overfillThresholdPercent: uiConfig.overfillThresholdPercent,
timeleftToFullOrEmptyThresholdSeconds: uiConfig.timeleftToFullOrEmptyThresholdSeconds
} }
}; };
@@ -101,65 +118,60 @@ class nodeClass {
_updateNodeStatus() { _updateNodeStatus() {
const ps = this.source; const ps = this.source;
try {
// --- Basin & measurements -------------------------------------------------
const maxVolBeforeOverflow = ps.basin?.maxVolOverflow ?? ps.basin?.maxVol ?? 0;
const volumeMeasurement = ps.measurements.type("volume").variant("measured").position("atEquipment");
const currentVolume = volumeMeasurement.getCurrentValue("m3") ?? 0;
const netFlowMeasurement = ps.measurements.type("netFlowRate").variant("predicted").position("atEquipment");
const netFlowM3s = netFlowMeasurement?.getCurrentValue("m3/s") ?? 0;
const netFlowM3h = netFlowM3s * 3600;
const percentFull = ps.measurements.type("volume").variant("procent").position("atEquipment").getCurrentValue() ?? 0;
// --- State information ---------------------------------------------------- const pickVariant = (type, prefer = ['measured', 'predicted'], position = 'atEquipment', unit) => {
const direction = ps.state?.direction || "unknown"; for (const variant of prefer) {
const secondsRemaining = ps.state?.seconds ?? null; const chain = ps.measurements.type(type).variant(variant).position(position);
const value = unit ? chain.getCurrentValue(unit) : chain.getCurrentValue();
const timeRemaining = secondsRemaining ? `${Math.round(secondsRemaining / 60)}` : 0 + " min"; if (value != null) return { value, variant };
// --- Icon / colour selection ---------------------------------------------
let symbol = "❔";
let fill = "grey";
switch (direction) {
case "filling":
symbol = "⬆️";
fill = "blue";
break;
case "draining":
symbol = "⬇️";
fill = "orange";
break;
case "stable":
symbol = "⏸️";
fill = "green";
break;
default:
symbol = "❔";
fill = "grey";
break;
} }
return { value: null, variant: null };
};
// --- Status text ---------------------------------------------------------- const vol = pickVariant('volume', ['measured', 'predicted'], 'atEquipment', 'm3');
const textParts = [ const volPercent = pickVariant('volumePercent', ['measured','predicted'], 'atEquipment'); // already unitless
`${symbol} ${percentFull.toFixed(1)}%`, const level = pickVariant('level', ['measured', 'predicted'], 'atEquipment', 'm');
`V=${currentVolume.toFixed(2)} / ${maxVolBeforeOverflow.toFixed(2)}`, const netFlow = pickVariant('netFlowRate', ['measured', 'predicted'], 'atEquipment', 'm3/h');
`net=${netFlowM3h.toFixed(1)} m³/h`,
`t≈${timeRemaining}`
];
return { const maxVolBeforeOverflow = ps.basin?.maxVolOverflow ?? ps.basin?.maxVol ?? 0;
fill, const currentVolume = vol.value ?? 0;
shape: "dot", const currentvolPercent = volPercent.value ?? 0;
text: textParts.join(" | ") const netFlowM3h = netFlow.value ?? 0;
};
} catch (error) { const direction = ps.state?.direction ?? 'unknown';
this.node.error("Error in updateNodeStatus: " + error.message); const secondsRemaining = ps.state?.seconds ?? null;
return { fill: "red", shape: "ring", text: "Status Error" }; const timeRemainingMinutes = secondsRemaining != null ? Math.round(secondsRemaining / 60) : null;
const badgePieces = [];
badgePieces.push(`${currentvolPercent.toFixed(1)}% `);
badgePieces.push(
`V=${currentVolume.toFixed(2)} / ${maxVolBeforeOverflow.toFixed(2)}`
);
badgePieces.push(`net: ${netFlowM3h.toFixed(0)} m³/h`);
if (timeRemainingMinutes != null) {
badgePieces.push(`t≈${timeRemainingMinutes} min)`);
} }
const { symbol, fill } = (() => {
switch (direction) {
case 'filling': return { symbol: '⬆️', fill: 'blue' };
case 'draining': return { symbol: '⬇️', fill: 'orange' };
case 'steady': return { symbol: '⏸️', fill: 'green' };
default: return { symbol: '❔', fill: 'grey' };
}
})();
badgePieces[0] = `${symbol} ${badgePieces[0]}`;
return {
fill,
shape: 'dot',
text: badgePieces.join(' | ')
};
} }
// any time based functions here // any time based functions here
_startTickLoop() { _startTickLoop() {
setTimeout(() => { setTimeout(() => {
@@ -178,11 +190,12 @@ class nodeClass {
* Execute a single tick: update measurement, format and send outputs. * Execute a single tick: update measurement, format and send outputs.
*/ */
_tick() { _tick() {
//this.source.tick();
//pumping station needs time based ticks to recalc level when predicted
this.source.tick();
const raw = this.source.getOutput(); const raw = this.source.getOutput();
const processMsg = this._output.formatMsg(raw, this.config, 'process'); const processMsg = this._output.formatMsg(raw, this.source.config, 'process');
const influxMsg = this._output.formatMsg(raw, this.config, 'influxdb'); const influxMsg = this._output.formatMsg(raw, this.source.config, 'influxdb');
// Send only updated outputs on ports 0 & 1 // Send only updated outputs on ports 0 & 1
this.node.send([processMsg, influxMsg]); this.node.send([processMsg, influxMsg]);
@@ -195,19 +208,33 @@ class nodeClass {
this.node.on('input', (msg, send, done) => { this.node.on('input', (msg, send, done) => {
switch (msg.topic) { switch (msg.topic) {
//example //example
/*case 'simulator': case 'changemode':
this.source.toggleSimulation(); this.source.changeMode(msg.payload);
break; break;
default:
this.source.handleInput(msg);
break;
*/
case 'registerChild': case 'registerChild':
// Register this node as a child of the parent node // Register this node as a child of the parent node
const childId = msg.payload; const childId = msg.payload;
const childObj = this.RED.nodes.getNode(childId); const childObj = this.RED.nodes.getNode(childId);
this.source.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent); this.source.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
break; break;
case 'calibratePredictedVolume':
const injectedVol = parseFloat(msg.payload);
this.source.calibratePredictedVolume(injectedVol);
break;
case 'calibratePredictedLevel':
const injectedLevel = parseFloat(msg.payload);
this.source.calibratePredictedLevel(injectedLevel);
break;
case 'q_in': {
// payload can be number or { value, unit, timestamp }
const val = Number(msg.payload);
const unit = msg?.unit;
const ts = msg?.timestamp || Date.now();
this.source.setManualInflow(val, ts, unit);
break;
}
} }
done(); done();
}); });

File diff suppressed because it is too large Load Diff