writing core class

This commit is contained in:
znetsixe
2025-10-14 16:32:44 +02:00
parent d94d5874bc
commit eabaa1b0bf
2 changed files with 115 additions and 141 deletions

View File

@@ -17,16 +17,17 @@
color: "#0c99d9", // color for the node based on the S88 schema
defaults: {
// Define specific properties
scaling: { value: false },
i_min: { value: 0, required: true },
i_max: { value: 0, required: true },
i_offset: { value: 0 },
o_min: { value: 0, required: true },
o_max: { value: 1, required: true },
// Define station-specific properties
simulator: { value: false },
smooth_method: { value: "" },
count: { value: "10", required: true },
basinVolume: { value: 1 }, // m³, total empty basin
basinHeight: { value: 1 }, // m, floor to top
heightInlet: { value: 0.8 }, // m, centre of inlet pipe above floor
heightOutlet: { value: 0.2 }, // m, centre of outlet pipe above floor
heightOverflow: { value: 0.9 }, // m, overflow elevation
// Advanced reference information
refHeight: { value: "NAP" }, // reference height
basinBottomRef: { value: 1 }, // absolute elevation of basin floor
//define asset properties
uuid: { value: "" },
@@ -71,89 +72,42 @@
// Wait for the menu data to be ready before initializing the editor
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)
// Populate smoothing methods dropdown
const smoothMethodSelect = document.getElementById('node-input-smooth_method');
const options = window.EVOLV?.nodes?.pumpingStation?.config?.smoothing?.smoothMethod?.rules?.values || [];
// NODE SPECIFIC
document.getElementById("node-input-basinVolume");
document.getElementById("node-input-basinHeight");
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
smoothMethodSelect.innerHTML = '';
// 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;
const refHeightEl = document.getElementById("node-input-refHeight");
if (refHeightEl) {
refHeightEl.value = this.refHeight || "NAP";
}
// --- 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 ------------------- //
},
oneditsave: function () {
const node = this;
// Validate asset properties using the asset menu
if (window.EVOLV?.nodes?.pumpingStation?.assetMenu?.saveEditor) {
success = window.EVOLV.nodes.pumpingStation.assetMenu.saveEditor(this);
}
//window.EVOLV?.nodes?.pumpingStation?.assetMenu?.saveEditor?.(node);
window.EVOLV?.nodes?.pumpingStation?.loggerMenu?.saveEditor?.(node);
window.EVOLV?.nodes?.pumpingStation?.positionMenu?.saveEditor?.(node);
// Validate logger properties using the logger menu
if (window.EVOLV?.nodes?.pumpingStation?.loggerMenu?.saveEditor) {
success = window.EVOLV.nodes.pumpingStation.loggerMenu.saveEditor(node);
}
//node specific
node.refHeight = document.getElementById("node-input-refHeight").value || "NAP";
node.simulator = document.getElementById("node-input-simulator").checked;
// save position field
if (window.EVOLV?.nodes?.pumpingStation?.positionMenu?.saveEditor) {
window.EVOLV.nodes.pumpingStation.positionMenu.saveEditor(this);
}
// 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");
}
["basinVolume","basinHeight","heightInlet","heightOutlet","heightOverflow","basinBottomRef"]
.forEach(field => {
node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0;
});
node.refHeight = document.getElementById("node-input-refHeight").value || "";
},
});
</script>
@@ -161,72 +115,60 @@
<script type="text/html" data-template-name="pumpingStation">
<!-- Scaling Checkbox -->
<!-- Simulator toggle -->
<div class="form-row">
<label for="node-input-scaling"
><i class="fa fa-compress"></i> Scaling</label>
<input type="checkbox" id="node-input-scaling" style="width:20px; vertical-align:baseline;"/>
<span>Enable input scaling?</span>
<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;" />
<span>Run station in simulated mode</span>
</div>
<!-- Source Min/Max (only if scaling is true) -->
<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>
<hr>
<div class="form-row" id="row-input-i_max">
<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 -->
<!-- Basin geometry -->
<div class="form-row">
<label for="node-input-i_offset"><i class="fa fa-adjust"></i> Input Offset</label>
<input type="number" id="node-input-i_offset" placeholder="0" />
</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" />
<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" />
</div>
<div class="form-row">
<label for="node-input-o_max"><i class="fa fa-tag"></i> Process Max</label>
<input type="number" id="node-input-o_max" placeholder="1" />
<label for="node-input-basinHeight"><i class="fa fa-arrows-v"></i> Basin Height (m)</label>
<input type="number" id="node-input-basinHeight" min="0" step="0.1" />
</div>
<!-- Simulator Checkbox -->
<!-- Inlet/Outlet elevations -->
<div class="form-row">
<label for="node-input-simulator"><i class="fa fa-cog"></i> Simulator</label>
<input type="checkbox" id="node-input-simulator" style="width:20px; vertical-align:baseline;"/>
<span>Activate internal simulation?</span>
<label for="node-input-heightInlet"><i class="fa fa-long-arrow-up"></i> Inlet Elevation (m)</label>
<input type="number" id="node-input-heightInlet" min="0" step="0.01" />
</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>
<!-- Smoothing Method -->
<hr>
<!-- Reference data -->
<div class="form-row">
<label for="node-input-smooth_method"><i class="fa fa-line-chart"></i> Smoothing</label>
<select id="node-input-smooth_method" style="width:60%;">
<label for="node-input-refHeight"><i class="fa fa-map-marker"></i> Reference height</label>
<select id="node-input-refHeight" style="width:60%;">
<option value="NAP">NAP</option>
</select>
</div>
<!-- Smoothing Window -->
<div class="form-row">
<label for="node-input-count">Window</label>
<input type="number" id="node-input-count" placeholder="10" style="width:60px;"/>
<div class="form-tips">Number of samples for smoothing</div>
<label for="node-input-basinBottomRef"><i class="fa fa-level-down"></i> Basin Bottom (m Refheight)</label>
<input type="number" id="node-input-basinBottomRef" step="0.01" />
</div>
<!-- Optional Extended Fields: supplier, cat, type, model, unit -->
<!-- Asset fields will be injected here -->
<!-- Shared asset/logger/position menus -->
<div id="asset-fields-placeholder"></div>
<!-- loglevel checkbox -->
<div id="logger-fields-placeholder"></div>
<!-- Position fields will be injected here -->
<div id="position-fields-placeholder"></div>
</script>

View File

@@ -19,19 +19,20 @@ class pumpingStation {
windowSize: this.config.smoothing.smoothWindow
});
// pumpingStation-specific properties
this.flowrate = null; // Function to calculate flow rate based on water level rise or fall
this.timeBeforeOverflow = null; // Time before the basin overflows at current inflow rate
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
// init basin object in pumping station
this.basin = {
volumeWater : null,// Total volume of water in the basin, calculated from water level and basin di
emptyVolume : null,// Volume in the basin when empty (at level of outlet pipe)
fullVolume : null,// Volume in the basin when at level of overflow point
crossSectionalArea: null,// Cross-sectional area of the basin, used to calculate volume from water level
};
// Initialize basin-specific properties from config
// pumping station specifics
this.calculatedFlowrate = null,// Function to calculate flow rate based on water level rise or fall NO MEASUREMENT this is the predicted value which should match a flowrate if we have it and we have to check mass balance ? Look at the pumps connected to the group controller or directly to this node and check incoming vs outgoing?
this.timeBeforeOverflow = null,// Time before the basin overflows at current inflow rate at level of heightOutlet
this.timeBeforeEmpty = null,// Time before the basin empties at current outflow rate at level of heightInlet
// Initialize basin-specific properties and calculate used parameters
this.initBasinProperties();
}
@@ -96,6 +97,7 @@ class pumpingStation {
// context handler for pressure updates
updateMeasuredPressure(value, position, context = {}) {
// init temp
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
@@ -117,20 +119,50 @@ class pumpingStation {
}
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 g =
const g = 9.80665;
//calculate how muc flow went in or out based on pressure difference
this.logger.debug(`Using pressure: ${pressure} for calculations`);
}
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
this.heightOverflow = this.config.basin.heightOverflow || 0; // Default to 0 if not specified
this.crossSectionalArea = this.config.basin.crossSectionalArea || 1; // Default to 1 m² if not specified
// Load and calc basic params
const volEmptyBasin = this.config.basin.volume;
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)}`
);
}
_calcVolumeFromLevel(level) {
const surfaceArea = this.basin.surfaceArea;
return Math.max(level, 0) * surfaceArea;
}
getOutput() {
return {