Compare commits
3 Commits
d94d5874bc
...
65807881d5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65807881d5 | ||
|
|
f9f6e874d1 | ||
|
|
eabaa1b0bf |
@@ -17,16 +17,17 @@
|
|||||||
color: "#0c99d9", // color for the node based on the S88 schema
|
color: "#0c99d9", // color for the node based on the S88 schema
|
||||||
defaults: {
|
defaults: {
|
||||||
|
|
||||||
// Define specific properties
|
// Define station-specific properties
|
||||||
scaling: { value: false },
|
simulator: { value: false },
|
||||||
i_min: { value: 0, required: true },
|
basinVolume: { value: 1 }, // m³, total empty basin
|
||||||
i_max: { value: 0, required: true },
|
basinHeight: { value: 1 }, // m, floor to top
|
||||||
i_offset: { value: 0 },
|
heightInlet: { value: 0.8 }, // m, centre of inlet pipe above floor
|
||||||
o_min: { value: 0, required: true },
|
heightOutlet: { value: 0.2 }, // m, centre of outlet pipe above floor
|
||||||
o_max: { value: 1, required: true },
|
heightOverflow: { value: 0.9 }, // m, overflow elevation
|
||||||
simulator: { value: false },
|
|
||||||
smooth_method: { value: "" },
|
// Advanced reference information
|
||||||
count: { value: "10", required: true },
|
refHeight: { value: "NAP" }, // reference height
|
||||||
|
basinBottomRef: { value: 1 }, // absolute elevation of basin floor
|
||||||
|
|
||||||
//define asset properties
|
//define asset properties
|
||||||
uuid: { value: "" },
|
uuid: { value: "" },
|
||||||
@@ -71,89 +72,42 @@
|
|||||||
// Wait for the menu data to be ready before initializing the editor
|
// Wait for the menu data to be ready before initializing the editor
|
||||||
waitForMenuData();
|
waitForMenuData();
|
||||||
|
|
||||||
// THIS IS NODE SPECIFIC --------------- Initialize the dropdowns and other specific UI elements -------------- this should be derived from the config in the future (make config based menu)
|
// NODE SPECIFIC
|
||||||
// Populate smoothing methods dropdown
|
document.getElementById("node-input-basinVolume");
|
||||||
const smoothMethodSelect = document.getElementById('node-input-smooth_method');
|
document.getElementById("node-input-basinHeight");
|
||||||
const options = window.EVOLV?.nodes?.pumpingStation?.config?.smoothing?.smoothMethod?.rules?.values || [];
|
document.getElementById("node-input-heightInlet");
|
||||||
|
document.getElementById("node-input-heightOutlet");
|
||||||
|
document.getElementById("node-input-heightOverflow");
|
||||||
|
document.getElementById("node-input-refHeight");
|
||||||
|
document.getElementById("node-input-basinBottomRef");
|
||||||
|
|
||||||
// Clear existing options
|
const refHeightEl = document.getElementById("node-input-refHeight");
|
||||||
smoothMethodSelect.innerHTML = '';
|
if (refHeightEl) {
|
||||||
|
refHeightEl.value = this.refHeight || "NAP";
|
||||||
// Add empty option
|
|
||||||
const emptyOption = document.createElement('option');
|
|
||||||
emptyOption.value = '';
|
|
||||||
emptyOption.textContent = 'Select method...';
|
|
||||||
smoothMethodSelect.appendChild(emptyOption);
|
|
||||||
|
|
||||||
// Add smoothing method options
|
|
||||||
options.forEach(option => {
|
|
||||||
const optionElement = document.createElement('option');
|
|
||||||
optionElement.value = option.value;
|
|
||||||
optionElement.textContent = option.value;
|
|
||||||
optionElement.title = option.description; // Add tooltip with full description
|
|
||||||
smoothMethodSelect.appendChild(optionElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set current value if it exists
|
|
||||||
if (this.smooth_method) {
|
|
||||||
smoothMethodSelect.value = this.smooth_method;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Scale rows toggle ---
|
|
||||||
const chk = document.getElementById('node-input-scaling');
|
|
||||||
const rowMin = document.getElementById('row-input-i_min');
|
|
||||||
const rowMax = document.getElementById('row-input-i_max');
|
|
||||||
|
|
||||||
function toggleScalingRows() {
|
|
||||||
const show = chk.checked;
|
|
||||||
rowMin.style.display = show ? 'block' : 'none';
|
|
||||||
rowMax.style.display = show ? 'block' : 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// wire and initialize
|
|
||||||
chk.addEventListener('change', toggleScalingRows);
|
|
||||||
toggleScalingRows();
|
|
||||||
|
|
||||||
//------------------- END OF CUSTOM config UI ELEMENTS ------------------- //
|
//------------------- END OF CUSTOM config UI ELEMENTS ------------------- //
|
||||||
},
|
},
|
||||||
oneditsave: function () {
|
oneditsave: function () {
|
||||||
const node = this;
|
const node = this;
|
||||||
|
|
||||||
// Validate asset properties using the asset menu
|
//window.EVOLV?.nodes?.pumpingStation?.assetMenu?.saveEditor?.(node);
|
||||||
if (window.EVOLV?.nodes?.pumpingStation?.assetMenu?.saveEditor) {
|
window.EVOLV?.nodes?.pumpingStation?.loggerMenu?.saveEditor?.(node);
|
||||||
success = window.EVOLV.nodes.pumpingStation.assetMenu.saveEditor(this);
|
window.EVOLV?.nodes?.pumpingStation?.positionMenu?.saveEditor?.(node);
|
||||||
}
|
|
||||||
|
|
||||||
// Validate logger properties using the logger menu
|
//node specific
|
||||||
if (window.EVOLV?.nodes?.pumpingStation?.loggerMenu?.saveEditor) {
|
node.refHeight = document.getElementById("node-input-refHeight").value || "NAP";
|
||||||
success = window.EVOLV.nodes.pumpingStation.loggerMenu.saveEditor(node);
|
node.simulator = document.getElementById("node-input-simulator").checked;
|
||||||
}
|
|
||||||
|
|
||||||
// save position field
|
["basinVolume","basinHeight","heightInlet","heightOutlet","heightOverflow","basinBottomRef"]
|
||||||
if (window.EVOLV?.nodes?.pumpingStation?.positionMenu?.saveEditor) {
|
.forEach(field => {
|
||||||
window.EVOLV.nodes.pumpingStation.positionMenu.saveEditor(this);
|
node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0;
|
||||||
}
|
});
|
||||||
|
|
||||||
// Save basic properties
|
|
||||||
["smooth_method"].forEach(
|
|
||||||
(field) => (node[field] = document.getElementById(`node-input-${field}`).value || "")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save numeric and boolean properties
|
|
||||||
["scaling", "simulator"].forEach(
|
|
||||||
(field) => (node[field] = document.getElementById(`node-input-${field}`).checked)
|
|
||||||
);
|
|
||||||
|
|
||||||
["i_min", "i_max", "i_offset", "o_min", "o_max", "count"].forEach(
|
|
||||||
(field) => (node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Validation checks
|
|
||||||
if (node.scaling && (isNaN(node.i_min) || isNaN(node.i_max))) {
|
|
||||||
RED.notify("Scaling enabled, but input range is incomplete!", "error");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
node.refHeight = document.getElementById("node-input-refHeight").value || "";
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -161,72 +115,60 @@
|
|||||||
|
|
||||||
<script type="text/html" data-template-name="pumpingStation">
|
<script type="text/html" data-template-name="pumpingStation">
|
||||||
|
|
||||||
<!-- Scaling Checkbox -->
|
<!-- Simulator toggle -->
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-scaling"
|
<label for="node-input-simulator"><i class="fa fa-play-circle"></i> Simulator</label>
|
||||||
><i class="fa fa-compress"></i> Scaling</label>
|
<input type="checkbox" id="node-input-simulator" style="width:20px;vertical-align:baseline;" />
|
||||||
<input type="checkbox" id="node-input-scaling" style="width:20px; vertical-align:baseline;"/>
|
<span>Run station in simulated mode</span>
|
||||||
<span>Enable input scaling?</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Source Min/Max (only if scaling is true) -->
|
<hr>
|
||||||
<div class="form-row" id="row-input-i_min">
|
|
||||||
<label for="node-input-i_min"><i class="fa fa-arrow-down"></i> Source Min</label>
|
|
||||||
<input type="number" id="node-input-i_min" placeholder="0" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row" id="row-input-i_max">
|
<!-- Basin geometry -->
|
||||||
<label for="node-input-i_max"><i class="fa fa-arrow-up"></i> Source Max</label>
|
|
||||||
<input type="number" id="node-input-i_max" placeholder="3000" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Offset -->
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-i_offset"><i class="fa fa-adjust"></i> Input Offset</label>
|
<label for="node-input-basinVolume"><i class="fa fa-cube"></i> Basin Volume (m³)</label>
|
||||||
<input type="number" id="node-input-i_offset" placeholder="0" />
|
<input type="number" id="node-input-basinVolume" min="0" step="0.1" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Output / Process Min/Max -->
|
|
||||||
<div class="form-row">
|
|
||||||
<label for="node-input-o_min"><i class="fa fa-tag"></i> Process Min</label>
|
|
||||||
<input type="number" id="node-input-o_min" placeholder="0" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-o_max"><i class="fa fa-tag"></i> Process Max</label>
|
<label for="node-input-basinHeight"><i class="fa fa-arrows-v"></i> Basin Height (m)</label>
|
||||||
<input type="number" id="node-input-o_max" placeholder="1" />
|
<input type="number" id="node-input-basinHeight" min="0" step="0.1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Simulator Checkbox -->
|
<!-- Inlet/Outlet elevations -->
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-simulator"><i class="fa fa-cog"></i> Simulator</label>
|
<label for="node-input-heightInlet"><i class="fa fa-long-arrow-up"></i> Inlet Elevation (m)</label>
|
||||||
<input type="checkbox" id="node-input-simulator" style="width:20px; vertical-align:baseline;"/>
|
<input type="number" id="node-input-heightInlet" min="0" step="0.01" />
|
||||||
<span>Activate internal simulation?</span>
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-heightOutlet"><i class="fa fa-long-arrow-down"></i> Outlet Elevation (m)</label>
|
||||||
|
<input type="number" id="node-input-heightOutlet" min="0" step="0.01" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-heightOverflow"><i class="fa fa-tint"></i> Overflow Level (m)</label>
|
||||||
|
<input type="number" id="node-input-heightOverflow" min="0" step="0.01" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Smoothing Method -->
|
<hr>
|
||||||
|
|
||||||
|
<!-- Reference data -->
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-smooth_method"><i class="fa fa-line-chart"></i> Smoothing</label>
|
<label for="node-input-refHeight"><i class="fa fa-map-marker"></i> Reference height</label>
|
||||||
<select id="node-input-smooth_method" style="width:60%;">
|
<select id="node-input-refHeight" style="width:60%;">
|
||||||
|
<option value="NAP">NAP</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Smoothing Window -->
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-count">Window</label>
|
<label for="node-input-basinBottomRef"><i class="fa fa-level-down"></i> Basin Bottom (m Refheight)</label>
|
||||||
<input type="number" id="node-input-count" placeholder="10" style="width:60px;"/>
|
<input type="number" id="node-input-basinBottomRef" step="0.01" />
|
||||||
<div class="form-tips">Number of samples for smoothing</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Optional Extended Fields: supplier, cat, type, model, unit -->
|
<!-- Shared asset/logger/position menus -->
|
||||||
<!-- Asset fields will be injected here -->
|
|
||||||
<div id="asset-fields-placeholder"></div>
|
<div id="asset-fields-placeholder"></div>
|
||||||
|
|
||||||
<!-- loglevel checkbox -->
|
|
||||||
<div id="logger-fields-placeholder"></div>
|
<div id="logger-fields-placeholder"></div>
|
||||||
|
|
||||||
<!-- Position fields will be injected here -->
|
|
||||||
<div id="position-fields-placeholder"></div>
|
<div id="position-fields-placeholder"></div>
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,17 @@ class nodeClass {
|
|||||||
functionality: {
|
functionality: {
|
||||||
positionVsParent: uiConfig.positionVsParent,// Default to 'atEquipment' if not specified
|
positionVsParent: uiConfig.positionVsParent,// Default to 'atEquipment' if not specified
|
||||||
distance: uiConfig.hasDistance ? uiConfig.distance : undefined
|
distance: uiConfig.hasDistance ? uiConfig.distance : undefined
|
||||||
|
},
|
||||||
|
basin:{
|
||||||
|
volume: uiConfig.basinVolume,
|
||||||
|
height: uiConfig.basinHeight,
|
||||||
|
heightInlet: uiConfig.heightInlet,
|
||||||
|
heightOutlet: uiConfig.heightOutlet,
|
||||||
|
heightOverflow: uiConfig.heightOverflow,
|
||||||
|
},
|
||||||
|
hydraulics:{
|
||||||
|
refHeight: uiConfig.refHeight,
|
||||||
|
basinBottomRef: uiConfig.basinBottomRef,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -128,6 +139,12 @@ class nodeClass {
|
|||||||
this.source.handleInput(msg);
|
this.source.handleInput(msg);
|
||||||
break;
|
break;
|
||||||
*/
|
*/
|
||||||
|
case 'registerChild':
|
||||||
|
// Register this node as a child of the parent node
|
||||||
|
const childId = msg.payload;
|
||||||
|
const childObj = this.RED.nodes.getNode(childId);
|
||||||
|
this.source.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const {logger,configUtils,configManager,MeasurementContainer,coolprop} = require('generalFunctions');
|
const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop} = require('generalFunctions');
|
||||||
|
|
||||||
class pumpingStation {
|
class pumpingStation {
|
||||||
constructor(config={}) {
|
constructor(config={}) {
|
||||||
@@ -15,25 +15,20 @@ class pumpingStation {
|
|||||||
|
|
||||||
// General properties
|
// General properties
|
||||||
this.measurements = new MeasurementContainer({
|
this.measurements = new MeasurementContainer({
|
||||||
autoConvert: true,
|
autoConvert: true
|
||||||
windowSize: this.config.smoothing.smoothWindow
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// pumpingStation-specific properties
|
// init basin object in pumping station
|
||||||
this.flowrate = null; // Function to calculate flow rate based on water level rise or fall
|
this.basin = {};
|
||||||
this.timeBeforeOverflow = null; // Time before the basin overflows at current inflow rate
|
this.state = { direction:"", netDownstream:0, netUpstream:0, seconds:0}; // init state object of pumping station to see whats going on
|
||||||
this.timeBeforeEmpty = null; // Time before the basin empties at current outflow rate
|
|
||||||
this.heightInlet = null; // Height of the inlet pipe from the bottom of the basin
|
|
||||||
this.heightOutlet = null; // Height of the outlet pipe from the bottom of the basin
|
|
||||||
this.heightOverflow = null; // Height of the overflow point from the bottom of the basin
|
|
||||||
this.volume = null; // Total volume of water in the basin, calculated from water level and basin dimensions
|
|
||||||
this.emptyVolume = null; // Volume in the basin when empty (at level of outlet pipe)
|
|
||||||
this.fullVolume = null; // Volume in the basin when at level of overflow point
|
|
||||||
this.crossSectionalArea = null; // Cross-sectional area of the basin, used to calculate volume from water level
|
|
||||||
|
|
||||||
// Initialize basin-specific properties from config
|
// Initialize basin-specific properties and calculate used parameters
|
||||||
this.initBasinProperties();
|
this.initBasinProperties();
|
||||||
|
|
||||||
|
this.child = {}; // object to hold child information so we know on what to subscribe
|
||||||
|
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
|
||||||
|
|
||||||
|
this.logger.debug('pumpstation Initialized with all helpers');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*------------------- Register child events -------------------*/
|
/*------------------- Register child events -------------------*/
|
||||||
@@ -53,7 +48,7 @@ class pumpingStation {
|
|||||||
child.measurements.emitter.on(eventName, (eventData) => {
|
child.measurements.emitter.on(eventName, (eventData) => {
|
||||||
this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
|
this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
|
||||||
|
|
||||||
console.log(` Emitting... ${eventName} with data:`);
|
this.logger.debug(` Emitting... ${eventName} with data:`);
|
||||||
// Store directly in parent's measurement container
|
// Store directly in parent's measurement container
|
||||||
this.measurements
|
this.measurements
|
||||||
.type(measurementType)
|
.type(measurementType)
|
||||||
@@ -96,6 +91,7 @@ class pumpingStation {
|
|||||||
// context handler for pressure updates
|
// context handler for pressure updates
|
||||||
updateMeasuredPressure(value, position, context = {}) {
|
updateMeasuredPressure(value, position, context = {}) {
|
||||||
|
|
||||||
|
// init temp
|
||||||
let kelvinTemp = null;
|
let kelvinTemp = null;
|
||||||
|
|
||||||
//pressure updates come from pressure boxes inside the basin they get converted to a level and stored as level measured at position inlet or outlet
|
//pressure updates come from pressure boxes inside the basin they get converted to a level and stored as level measured at position inlet or outlet
|
||||||
@@ -117,24 +113,225 @@ class pumpingStation {
|
|||||||
}
|
}
|
||||||
this.logger.debug(`Using temperature: ${kelvinTemp} K for calculations`);
|
this.logger.debug(`Using temperature: ${kelvinTemp} K for calculations`);
|
||||||
const density = coolprop.PropsSI('D','T',kelvinTemp,'P',101325,'Water'); //density in kg/m3 at temp and surface pressure
|
const density = coolprop.PropsSI('D','T',kelvinTemp,'P',101325,'Water'); //density in kg/m3 at temp and surface pressure
|
||||||
const g =
|
const g = 9.80665;
|
||||||
|
const pressure_Pa = this.measurements.type("pressure").variant("measured").position(position).getCurrentValue('Pa');
|
||||||
|
const level = pressure_Pa / density * g;
|
||||||
|
|
||||||
|
this.measurements.type("level").variant("predicted").position(position).value(level);
|
||||||
|
//updatePredictedLevel(); ??
|
||||||
|
|
||||||
//calculate how muc flow went in or out based on pressure difference
|
//calculate how muc flow went in or out based on pressure difference
|
||||||
this.logger.debug(`Using pressure: ${pressure} for calculations`);
|
this.logger.debug(`Using pressure: ${value} for calculations`);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMeasuredLevel(value,position, context = {}){
|
||||||
|
// Store in parent's measurement container for the first time
|
||||||
|
this.measurements.type("level").variant("measured").position(position).value(value, context.timestamp, context.unit);
|
||||||
|
|
||||||
|
//fetch level in meter
|
||||||
|
const level = this.measurements.type("level").variant("measured").position(position).getCurrentValue('m');
|
||||||
|
//calc vol in m3
|
||||||
|
const volume = this._calcVolumeFromLevel(level);
|
||||||
|
this.measurements.type("volume").variant("measured").position("atEquipment").value(volume).unit('m3');
|
||||||
|
|
||||||
|
//calc the most important values back to determine state and net up or downstream flow
|
||||||
|
this._calcNetFlow();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_calcNetFlow() {
|
||||||
|
const { heightOverflow, heightOutlet, surfaceArea } = this.basin;
|
||||||
|
|
||||||
|
const flowBased = this._calcNetFlowFromMeasurements({
|
||||||
|
heightOverflow,
|
||||||
|
heightOutlet,
|
||||||
|
surfaceArea
|
||||||
|
});
|
||||||
|
|
||||||
|
const levelBased = this._calcNetFlowFromLevel({
|
||||||
|
heightOverflow,
|
||||||
|
heightOutlet,
|
||||||
|
surfaceArea
|
||||||
|
});
|
||||||
|
|
||||||
|
if (flowBased && levelBased) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Flow vs Level comparison | flow=${flowBased.netFlowRate.toFixed(3)} ` +
|
||||||
|
`m3/s, level=${levelBased.netFlowRate.toFixed(3)} m3/s`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const effective = flowBased || levelBased;
|
||||||
|
if (effective) {
|
||||||
|
this.state = effective.state;
|
||||||
|
this.state.netFlowSource = flowBased ? (levelBased ? "flow+level" : "flow") : "level";
|
||||||
|
this.logger.debug(`Net-flow state: ${JSON.stringify(this.state)}`);
|
||||||
|
} else {
|
||||||
|
this.logger.debug("Net-flow state: insufficient data");
|
||||||
|
}
|
||||||
|
|
||||||
|
return effective;
|
||||||
|
}
|
||||||
|
|
||||||
|
_calcNetFlowFromMeasurements({ heightOverflow, heightOutlet, surfaceArea }) {
|
||||||
|
const flowDiff = this.measurements
|
||||||
|
.type("flow")
|
||||||
|
.variant("measured")
|
||||||
|
.difference({ from: "downstream", to: "upstream", unit: "m3/s" });
|
||||||
|
|
||||||
|
const level = this.measurements
|
||||||
|
.type("level")
|
||||||
|
.variant("measured")
|
||||||
|
.position("atEquipment")
|
||||||
|
.getCurrentValue("m");
|
||||||
|
|
||||||
|
const flowUpstream = this.measurements
|
||||||
|
.type("flow")
|
||||||
|
.variant("measured")
|
||||||
|
.position("upstream")
|
||||||
|
.getCurrentValue("m3/s");
|
||||||
|
|
||||||
|
const flowDownstream = this.measurements
|
||||||
|
.type("flow")
|
||||||
|
.variant("measured")
|
||||||
|
.position("downstream")
|
||||||
|
.getCurrentValue("m3/s");
|
||||||
|
|
||||||
|
if (flowDiff === null || level === null) {
|
||||||
|
this.logger.warn(`no flowdiff ${flowDiff} or level ${level} found escaping`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const flowThreshold = 0.1; // m³/s
|
||||||
|
const state = { direction: "stable", seconds: 0, netUpstream: flowUpstream ?? 0, netDownstream: flowDownstream ?? 0 };
|
||||||
|
|
||||||
|
if (flowDiff > flowThreshold) {
|
||||||
|
state.direction = "filling";
|
||||||
|
const remainingHeight = Math.max(heightOverflow - level, 0);
|
||||||
|
state.seconds = remainingHeight * surfaceArea / flowDiff;
|
||||||
|
} else if (flowDiff < -flowThreshold) {
|
||||||
|
state.direction = "draining";
|
||||||
|
const remainingHeight = Math.max(level - heightOutlet, 0);
|
||||||
|
state.seconds = remainingHeight * surfaceArea / Math.abs(flowDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.measurements
|
||||||
|
.type("netFlowRate")
|
||||||
|
.variant("predicted")
|
||||||
|
.position("atEquipment")
|
||||||
|
.value(flowDiff)
|
||||||
|
.unit("m3/s");
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Flow-based net flow | diff=${flowDiff.toFixed(3)} m3/s, level=${level.toFixed(3)} m`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { source: "flow", netFlowRate: flowDiff, state };
|
||||||
|
}
|
||||||
|
|
||||||
|
_calcNetFlowFromLevel({ heightOverflow, heightOutlet, surfaceArea }) {
|
||||||
|
const levelObj = this.measurements
|
||||||
|
.type("level")
|
||||||
|
.variant("measured")
|
||||||
|
.position("atEquipment");
|
||||||
|
|
||||||
|
const level = levelObj.getCurrentValue("m");
|
||||||
|
const prevLevel = levelObj.getLaggedValue(2, "m"); // { value, timestamp, unit }
|
||||||
|
const measurement = levelObj.get();
|
||||||
|
const latestTimestamp = measurement?.getLatestTimestamp();
|
||||||
|
|
||||||
|
if (level === null || !prevLevel || latestTimestamp == null) {
|
||||||
|
this.logger.warn(`no flowdiff ${level}, previous level ${prevLevel}, latestTimestamp ${latestTimestamp} found escaping`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaSeconds = (latestTimestamp - prevLevel.timestamp) / 1000;
|
||||||
|
if (deltaSeconds <= 0) {
|
||||||
|
this.logger.warn(`Level fallback: invalid Δt=${deltaSeconds} , LatestTimestamp : ${latestTimestamp}, PrevTimestamp : ${prevLevel.timestamp}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lvlDiff = level - prevLevel.value;
|
||||||
|
const lvlRate = lvlDiff / deltaSeconds; // m/s
|
||||||
|
const levelRateThreshold = 0.1 / surfaceArea; // same 0.1 m³/s threshold translated to height
|
||||||
|
|
||||||
|
const state = { direction: "stable", seconds: 0, netUpstream: 0, netDownstream: 0 };
|
||||||
|
|
||||||
|
if (lvlRate > levelRateThreshold) {
|
||||||
|
state.direction = "filling";
|
||||||
|
const remainingHeight = Math.max(heightOverflow - level, 0);
|
||||||
|
state.seconds = remainingHeight / lvlRate;
|
||||||
|
} else if (lvlRate < -levelRateThreshold) {
|
||||||
|
state.direction = "draining";
|
||||||
|
const remainingHeight = Math.max(level - heightOutlet, 0);
|
||||||
|
state.seconds = remainingHeight / Math.abs(lvlRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const netFlowRate = lvlRate * surfaceArea; // m³/s inferred from level trend
|
||||||
|
|
||||||
|
this.measurements
|
||||||
|
.type("netFlowRate")
|
||||||
|
.variant("predicted")
|
||||||
|
.position("atEquipment")
|
||||||
|
.value(netFlowRate)
|
||||||
|
.unit("m3/s");
|
||||||
|
|
||||||
|
this.logger.warn(
|
||||||
|
`Level-based net flow | rate=${lvlRate.toExponential(3)} m/s, inferred=${netFlowRate.toFixed(3)} m3/s`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { source: "level", netFlowRate, state };
|
||||||
}
|
}
|
||||||
|
|
||||||
initBasinProperties() {
|
initBasinProperties() {
|
||||||
// Initialize basin-specific properties from config
|
|
||||||
this.heightInlet = this.config.basin.heightInlet || 0; // Default to 0 if not specified
|
|
||||||
this.heightOutlet = this.config.basin.heightOutlet || 0; // Default to 0 if not specified
|
// Load and calc basic params
|
||||||
this.heightOverflow = this.config.basin.heightOverflow || 0; // Default to 0 if not specified
|
const volEmptyBasin = this.config.basin.volume;
|
||||||
this.crossSectionalArea = this.config.basin.crossSectionalArea || 1; // Default to 1 m² if not specified
|
const heightBasin = this.config.basin.height;
|
||||||
|
const heightInlet = this.config.basin.heightInlet;
|
||||||
|
const heightOutlet = this.config.basin.heightOutlet;
|
||||||
|
const heightOverflow = this.config.basin.heightOverflow;
|
||||||
|
|
||||||
|
//calculated params
|
||||||
|
const surfaceArea = volEmptyBasin / heightBasin;
|
||||||
|
const maxVol = heightBasin * surfaceArea; // if Basin where to ever fill up completely this is the water volume
|
||||||
|
const maxVolOverflow = heightOverflow * surfaceArea ; // Max water volume before you start loosing water to overflow
|
||||||
|
const minVol = heightInlet * surfaceArea;
|
||||||
|
const minVolOut = heightOutlet * surfaceArea ; // this will indicate if its an open end or a closed end.
|
||||||
|
|
||||||
|
this.basin.volEmptyBasin = volEmptyBasin ;
|
||||||
|
this.basin.heightBasin = heightBasin ;
|
||||||
|
this.basin.heightInlet = heightInlet ;
|
||||||
|
this.basin.heightOutlet = heightOutlet ;
|
||||||
|
this.basin.heightOverflow = heightOverflow ;
|
||||||
|
this.basin.surfaceArea = surfaceArea ;
|
||||||
|
this.basin.maxVol = maxVol ;
|
||||||
|
this.basin.maxVolOverflow = maxVolOverflow;
|
||||||
|
this.basin.minVol = minVol ;
|
||||||
|
this.basin.minVolOut = minVolOut ;
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Basin initialized | area=${surfaceArea.toFixed(2)} m², max=${maxVol.toFixed(2)} m³, overflow=${maxVolOverflow.toFixed(2)} m³`
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_calcVolumeFromLevel(level) {
|
||||||
|
const surfaceArea = this.basin.surfaceArea;
|
||||||
|
return Math.max(level, 0) * surfaceArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
getOutput() {
|
getOutput() {
|
||||||
return {
|
return {
|
||||||
volume: this.volume,
|
volume_m3: this.measurements.type("volume").variant("measured").position("atEquipment").getCurrentValue('m3') ,
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,9 +340,9 @@ module.exports = pumpingStation;
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
// */
|
//
|
||||||
|
|
||||||
|
|
||||||
|
//coolprop example
|
||||||
(async () => {
|
(async () => {
|
||||||
const PropsSI = await coolprop.getPropsSI();
|
const PropsSI = await coolprop.getPropsSI();
|
||||||
|
|
||||||
@@ -178,3 +375,4 @@ module.exports = pumpingStation;
|
|||||||
console.error('Example known-good call:', PropsSI('D', 'T', 298.15, 'P', 101325, 'Water'));
|
console.error('Example known-good call:', PropsSI('D', 'T', 298.15, 'P', 101325, 'Water'));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user