Compare commits
7 Commits
d94d5874bc
...
6e9ae9fc7e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e9ae9fc7e | ||
|
|
371f3c65e7 | ||
|
|
b8b7871e38 | ||
|
|
f29aa4f5af | ||
|
|
65807881d5 | ||
|
|
f9f6e874d1 | ||
|
|
eabaa1b0bf |
@@ -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: "" },
|
||||
@@ -57,7 +58,7 @@
|
||||
icon: "font-awesome/fa-tint",
|
||||
|
||||
label: function () {
|
||||
return this.positionIcon + " " + this.assetType || "pumpingStation";
|
||||
return this.positionIcon + " PumpingStation";
|
||||
},
|
||||
|
||||
oneditprepare: function() {
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -53,6 +53,17 @@ class nodeClass {
|
||||
functionality: {
|
||||
positionVsParent: uiConfig.positionVsParent,// Default to 'atEquipment' if not specified
|
||||
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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,10 +88,7 @@ class nodeClass {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this node as a child upstream and downstream.
|
||||
* Delayed to avoid Node-RED startup race conditions.
|
||||
*/
|
||||
// init registration msg
|
||||
_registerChild() {
|
||||
setTimeout(() => {
|
||||
this.node.send([
|
||||
@@ -91,12 +99,78 @@ class nodeClass {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the periodic tick loop to drive the Measurement class.
|
||||
*/
|
||||
_updateNodeStatus() {
|
||||
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 direction = ps.state?.direction || "unknown";
|
||||
const secondsRemaining = ps.state?.seconds ?? null;
|
||||
|
||||
const timeRemaining = secondsRemaining ? `${Math.round(secondsRemaining / 60)}` : 0 + " min";
|
||||
|
||||
// --- 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;
|
||||
}
|
||||
|
||||
// --- Status text ----------------------------------------------------------
|
||||
const textParts = [
|
||||
`${symbol} ${percentFull.toFixed(1)}%`,
|
||||
`V=${currentVolume.toFixed(2)} / ${maxVolBeforeOverflow.toFixed(2)} m³`,
|
||||
`net=${netFlowM3h.toFixed(1)} m³/h`,
|
||||
`t≈${timeRemaining}`
|
||||
];
|
||||
|
||||
return {
|
||||
fill,
|
||||
shape: "dot",
|
||||
text: textParts.join(" | ")
|
||||
};
|
||||
} catch (error) {
|
||||
this.node.error("Error in updateNodeStatus: " + error.message);
|
||||
return { fill: "red", shape: "ring", text: "Status Error" };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// any time based functions here
|
||||
_startTickLoop() {
|
||||
setTimeout(() => {
|
||||
this._tickInterval = setInterval(() => this._tick(), 1000);
|
||||
|
||||
// Update node status on nodered screen every second ( this is not the best way to do this, but it works for now)
|
||||
this._statusInterval = setInterval(() => {
|
||||
const status = this._updateNodeStatus();
|
||||
this.node.status(status);
|
||||
}, 1000);
|
||||
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
@@ -104,8 +178,9 @@ class nodeClass {
|
||||
* Execute a single tick: update measurement, format and send outputs.
|
||||
*/
|
||||
_tick() {
|
||||
//this.source.tick();
|
||||
|
||||
//pumping station needs time based ticks to recalc level when predicted
|
||||
this.source.tick();
|
||||
const raw = this.source.getOutput();
|
||||
const processMsg = this._output.formatMsg(raw, this.config, 'process');
|
||||
const influxMsg = this._output.formatMsg(raw, this.config, 'influxdb');
|
||||
@@ -128,6 +203,12 @@ class nodeClass {
|
||||
this.source.handleInput(msg);
|
||||
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();
|
||||
});
|
||||
@@ -139,7 +220,7 @@ class nodeClass {
|
||||
_attachCloseHandler() {
|
||||
this.node.on('close', (done) => {
|
||||
clearInterval(this._tickInterval);
|
||||
//clearInterval(this._statusInterval);
|
||||
clearInterval(this._statusInterval);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const EventEmitter = require('events');
|
||||
const {logger,configUtils,configManager,MeasurementContainer,coolprop} = require('generalFunctions');
|
||||
const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation} = require('generalFunctions');
|
||||
|
||||
class pumpingStation {
|
||||
constructor(config={}) {
|
||||
@@ -9,37 +9,36 @@ class pumpingStation {
|
||||
this.defaultConfig = this.configManager.getConfig('pumpingStation');
|
||||
this.configUtils = new configUtils(this.defaultConfig);
|
||||
this.config = this.configUtils.initConfig(config);
|
||||
this.interpolate = new interpolation();
|
||||
|
||||
// Init after config is set
|
||||
this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name);
|
||||
|
||||
// General properties
|
||||
this.measurements = new MeasurementContainer({
|
||||
autoConvert: true,
|
||||
windowSize: this.config.smoothing.smoothWindow
|
||||
autoConvert: true
|
||||
});
|
||||
|
||||
// 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 = {};
|
||||
this.state = { direction:"", netDownstream:0, netUpstream:0, seconds:0}; // init state object of pumping station to see whats going on
|
||||
|
||||
// Initialize basin-specific properties from config
|
||||
// Initialize basin-specific properties and calculate used parameters
|
||||
this.initBasinProperties();
|
||||
this.parent = {}; // object to hold parent information for when we follow flow directions.
|
||||
this.child = {}; // object to hold child information so we know on what to subscribe
|
||||
this.machines = {}; // object to hold child machine information
|
||||
this.stations = {}; // object to hold station information
|
||||
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
|
||||
|
||||
this.logger.debug('pumpstation Initialized with all helpers');
|
||||
}
|
||||
|
||||
/*------------------- Register child events -------------------*/
|
||||
registerChild(child, softwareType) {
|
||||
this.logger.debug('Setting up child event for softwaretype ' + softwareType);
|
||||
|
||||
//define what to do with measurements
|
||||
if(softwareType === "measurement"){
|
||||
const position = child.config.functionality.positionVsParent;
|
||||
const distance = child.config.functionality.distanceVsParent || 0;
|
||||
@@ -53,20 +52,172 @@ class pumpingStation {
|
||||
child.measurements.emitter.on(eventName, (eventData) => {
|
||||
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
|
||||
this.measurements
|
||||
.type(measurementType)
|
||||
.variant("measured")
|
||||
.position(position)
|
||||
.value(eventData.value, eventData.timestamp, eventData.unit);
|
||||
this.measurements.type(measurementType).variant("measured").position(position).value(eventData.value, eventData.timestamp, eventData.unit);
|
||||
|
||||
// Call the appropriate handler
|
||||
this._callMeasurementHandler(measurementType, eventData.value, position, eventData);
|
||||
});
|
||||
}
|
||||
|
||||
//define what to do when machines are connected
|
||||
if(softwareType == "machine"){
|
||||
// Check if the machine is already registered
|
||||
this.machines[child.config.general.id] === undefined ? this.machines[child.config.general.id] = child : this.logger.warn(`Machine ${child.config.general.id} is already registered.`);
|
||||
|
||||
//listen for machine pressure changes
|
||||
this.logger.debug(`Listening for flow changes from machine ${child.config.general.id}`);
|
||||
|
||||
switch(child.config.functionality.positionVsParent){
|
||||
case("downstream"):
|
||||
case("atequipment"): //in case of atequipment we also assume downstream seeing as it is registered at this pumpingstation as part of it.
|
||||
//for now lets focus on handling downstream predicted flow
|
||||
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
||||
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
||||
this.measurements.type('flow').variant('predicted').position('out').value(eventData.value,eventData.timestamp,eventData.unit);
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
case("upstream"):
|
||||
//check for predicted outgoing flow at the connected child pumpingsation
|
||||
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
||||
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
||||
//register this then as upstream flow that arrives at the station
|
||||
this.measurements.type('flow').variant('predicted').position('in').value(eventData.value,eventData.timestamp,eventData.unit);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.warn(`nu such position ${child.config.functionality.positionVsParent}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// add one for group later
|
||||
if( softwareType == "machineGroup" ){
|
||||
|
||||
}
|
||||
|
||||
// add one for pumping station
|
||||
if ( softwareType == "pumpingStation"){
|
||||
// Check if the machine is already registered
|
||||
this.stations[child.config.general.id] === undefined ? this.machistationsnes[child.config.general.id] = child : this.logger.warn(`Machine ${child.config.general.id} is already registered.`);
|
||||
|
||||
//listen for machine pressure changes
|
||||
this.logger.debug(`Listening for flow changes from machine ${child.config.general.id}`);
|
||||
|
||||
switch(child.config.functionality.positionVsParent){
|
||||
case("downstream"):
|
||||
//check for predicted outgoing flow at the connected child pumpingsation
|
||||
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
||||
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
||||
//register this then as upstream flow that arrives at the station
|
||||
this.measurements.type('flow').variant('predicted').position('out').value(eventData.value,eventData.timestamp,eventData.unit);
|
||||
});
|
||||
break;
|
||||
|
||||
case("upstream"):
|
||||
//check for predicted outgoing flow at the connected child pumpingsation
|
||||
child.measurements.emitter.on("flow.predicted.downstream", (eventData) => {
|
||||
this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`);
|
||||
//register this then as upstream flow that arrives at the station
|
||||
this.measurements.type('flow').variant('predicted').position('in').value(eventData.value,eventData.timestamp,eventData.unit);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
// there is no such thing as atequipment from 1 pumpingstation to another....
|
||||
this.logger.warn(`nu such position ${child.config.functionality.positionVsParent} for pumping station`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//in or outgoing flow = direction
|
||||
_updateVolumePrediction(flowDir){
|
||||
|
||||
//get downflow
|
||||
const seriesExists = this.measurements.type("flow").variant("predicted").position(flowDir).exists();
|
||||
if(!seriesExists){return};
|
||||
|
||||
const series = this.measurements.type("flow").variant("predicted").position(flowDir);
|
||||
const currFLow = series.getLaggedValue(0, "m3/s"); // { value, timestamp, unit }
|
||||
const prevFlow = series.getLaggedValue(1, "m3/s"); // { value, timestamp, unit }
|
||||
|
||||
if (!currFLow || !prevFlow) return;
|
||||
|
||||
this.logger.debug(`currDownflow = ${currFLow.value} , prevDownFlow = ${prevFlow.value}`);
|
||||
|
||||
// calc difference in time
|
||||
const deltaT = currFLow.timestamp - prevFlow.timestamp;
|
||||
const deltaSeconds = deltaT / 1000;
|
||||
|
||||
if (deltaSeconds <= 0) {
|
||||
this.logger.warn(`Flow integration aborted; invalid Δt=${deltaSeconds}s.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const avgFlow = (currFLow.value + prevFlow.value) / 2;
|
||||
const calcVol = avgFlow * deltaSeconds;
|
||||
|
||||
//substract seeing as this is downstream and is being pulled away from the pumpingstaion and keep track of status
|
||||
const currVolume = this.measurements.type('volume').variant('predicted').position('atEquipment').getCurrentValue('m3');
|
||||
let newVol = currVolume;
|
||||
|
||||
switch(flowDir){
|
||||
case("out"):
|
||||
newVol = currVolume - calcVol;
|
||||
break;
|
||||
|
||||
case("in"):
|
||||
newVol = currVolume + calcVol;
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.error('Flow must come in or out of the station!');
|
||||
}
|
||||
|
||||
|
||||
this.measurements.type('volume').variant('predicted').position('atEquipment').value(newVol).unit('m3');
|
||||
//convert to a predicted level
|
||||
const newLevel = this._calcLevelFromVolume(newVol);
|
||||
|
||||
this.measurements.type('level').variant('predicted').position('atEquipment').value(newLevel).unit('m');
|
||||
|
||||
this.logger.debug(`new predicted volume : ${newVol} new predicted level: ${newLevel} `);
|
||||
|
||||
}
|
||||
|
||||
|
||||
//trigger shutdown when level is too low and trigger no start flag for childs ?
|
||||
safetyVolCheck(){
|
||||
|
||||
}
|
||||
|
||||
|
||||
//update measured temperature to adjust density of liquid
|
||||
updateMeasuredTemperature(){
|
||||
|
||||
}
|
||||
|
||||
//update measured flow and recalc
|
||||
updateMeasuredFlow(){
|
||||
|
||||
}
|
||||
|
||||
//keep updating the volume / level when the flow is still active from a machine or machinegroup or incoming from another source
|
||||
tick(){
|
||||
//go through all the functions that require time based checks or updates
|
||||
this._updateVolumePrediction("out"); //check for changes in outgoing flow
|
||||
this._updateVolumePrediction("in"); // check for changes in incomming flow
|
||||
//calc the most important values back to determine state and net up or downstream flow
|
||||
this._calcNetFlow();
|
||||
this._calcTimeRemaining();
|
||||
|
||||
}
|
||||
|
||||
|
||||
_callMeasurementHandler(measurementType, value, position, context) {
|
||||
switch (measurementType) {
|
||||
case 'pressure':
|
||||
@@ -91,11 +242,12 @@ class pumpingStation {
|
||||
this.updatePosition();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -112,40 +264,405 @@ class pumpingStation {
|
||||
this.logger.warn(`No temperature measurement available, defaulting to 15C for pressure to level conversion.`);
|
||||
this.measurements.type("temperature").variant("assumed").position("atEquipment").value(15, Date.now(), "C");
|
||||
kelvinTemp = this.measurements.type('temperature').variant('assumed').position('atEquipment').getCurrentValue('K');
|
||||
this.logger.debug(`Temperature is : ${kelvinTemp}`);
|
||||
} else {
|
||||
kelvinTemp = mTemp;
|
||||
}
|
||||
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;
|
||||
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(); ?? OLIFANT!
|
||||
|
||||
//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.logger.debug(`basin minvol : ${this.basin.minVol}, cur volume : ${volume} / ${this.basin.maxVolOverflow}`);
|
||||
|
||||
const proc = this.interpolate.interpolate_lin_single_point(volume,this.basin.minVol,this.basin.maxVolOverflow,0,100);
|
||||
this.logger.debug(`PROC volume : ${proc}`);
|
||||
this.measurements.type("volume").variant("measured").position("atEquipment").value(volume).unit('m3');
|
||||
this.measurements.type("volume").variant("procent").position("atEquipment").value(proc);
|
||||
|
||||
}
|
||||
|
||||
_calcNetFlow() {
|
||||
let netFlow = null;
|
||||
|
||||
const netFlow_FlowSensor = Math.abs(this.measurements.type("flow").variant("measured").difference({ from: "downstream", to: "upstream", unit: "m3/s" }));
|
||||
const netFlow_LevelSensor = this._calcNetFlowFromLevelDiff();
|
||||
const netFlow_PredictedFlow = Math.abs(this.measurements.type('flow').variant('predicted').difference({ from: "in", to: "out", unit: "m3/s" }));
|
||||
|
||||
switch (true){
|
||||
//prefer flowsensor netflow
|
||||
case (netFlow_FlowSensor!=null):
|
||||
return netFlow_FlowSensor;
|
||||
//try using level difference if possible to infer netflow
|
||||
case (netFlow_LevelSensor!= null):
|
||||
return netFlow_LevelSensor;
|
||||
case (netFlow_PredictedFlow != null):
|
||||
return netFlow_PredictedFlow;
|
||||
default:
|
||||
this.logger.warn(`Can't calculate netflow without the proper measurements or predictions`);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_calcRemainingTime(level,variant){
|
||||
|
||||
const { heightOverflow, heightOutlet, surfaceArea } = this.basin;
|
||||
const flowDiff = this.measurements.type("flow").variant(variant).difference({ from: "downstream", to: "upstream", unit: "m3/s" });
|
||||
|
||||
switch(true){
|
||||
case(flowDiff>0):
|
||||
remainingHeight = Math.max(heightOverflow - level, 0);
|
||||
this.state.seconds = remainingHeight * surfaceArea / flowDiff;
|
||||
break;
|
||||
|
||||
case(flowDiff<0):
|
||||
remainingHeight = Math.max(level - heightOutlet, 0);
|
||||
this.state.seconds = remainingHeight * surfaceArea / Math.abs(flowDiff);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.debug(`doing nothing with level calc`)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_calcDirection(flowDiff){
|
||||
|
||||
let direction = null;
|
||||
|
||||
switch (true){
|
||||
case flowDiff > flowThreshold:
|
||||
direction = "filling";
|
||||
break;
|
||||
|
||||
case flowDiff < -flowThreshold:
|
||||
direction = "draining";
|
||||
break;
|
||||
|
||||
case flowDiff < flowThreshold && flowDiff > -flowThreshold:
|
||||
direction = "stable";
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.warn("Uknown state direction detected??");
|
||||
return null;
|
||||
|
||||
}
|
||||
return direction;
|
||||
}
|
||||
|
||||
_calcNetFlowFromLevelDiff() {
|
||||
const { surfaceArea } = this.basin;
|
||||
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 netFlowRate = lvlRate * surfaceArea; // m³/s inferred from level trend
|
||||
|
||||
return netFlowRate;
|
||||
}
|
||||
|
||||
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 = heightOutlet * surfaceArea;
|
||||
const minVolOut = heightInlet * 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 ;
|
||||
|
||||
//init predicted min volume to min vol in order to have a starting point
|
||||
this.measurements.type("volume").variant("predicted").position("atEquipment").value(minVol).unit('m3');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
_calcLevelFromVolume(vol){
|
||||
const surfaceArea = this.basin.surfaceArea;
|
||||
return Math.max(vol, 0) / surfaceArea;
|
||||
}
|
||||
|
||||
|
||||
getOutput() {
|
||||
// Improved output object generation
|
||||
const output = {};
|
||||
//build the output object
|
||||
this.measurements.getTypes().forEach(type => {
|
||||
this.measurements.getVariants(type).forEach(variant => {
|
||||
this.measurements.getPositions(variant).forEach(position => {
|
||||
const sample = this.measurements.type(type).variant(variant).position(position);
|
||||
output[`${type}.${variant}.${position}`] = sample.getCurrentValue();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//fill in the rest of the output object
|
||||
output["state"] = this.state;
|
||||
output["basin"] = this.basin;
|
||||
|
||||
if(this.flowDrift != null){
|
||||
const flowDrift = this.flowDrift;
|
||||
output["flowNrmse"] = flowDrift.nrmse;
|
||||
output["flowLongterNRMSD"] = flowDrift.longTermNRMSD;
|
||||
output["flowImmediateLevel"] = flowDrift.immediateLevel;
|
||||
output["flowLongTermLevel"] = flowDrift.longTermLevel;
|
||||
}
|
||||
|
||||
|
||||
getOutput() {
|
||||
return {
|
||||
volume: this.volume,
|
||||
};
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = pumpingStation;
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Example: pumping station + rotating machine + measurements (stand-alone) */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
const PumpingStation = require("./specificClass");
|
||||
const RotatingMachine = require("../../rotatingMachine/src/specificClass");
|
||||
const Measurement = require("../../measurement/src/specificClass");
|
||||
|
||||
/** Helpers ******************************************************************/
|
||||
function createPumpingStationConfig(name) {
|
||||
return {
|
||||
general: {
|
||||
logging: { enabled: true, logLevel: "debug" },
|
||||
name,
|
||||
id: `${name}-${Date.now()}`,
|
||||
unit: "m3/h"
|
||||
},
|
||||
functionality: {
|
||||
softwareType: "pumpingStation",
|
||||
role: "stationcontroller"
|
||||
},
|
||||
basin: {
|
||||
volume: 43.75,
|
||||
height: 3.5,
|
||||
heightInlet: 0.3,
|
||||
heightOutlet: 0.2,
|
||||
heightOverflow: 3.0
|
||||
},
|
||||
hydraulics: {
|
||||
refHeight: "NAP",
|
||||
basinBottomRef: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createLevelMeasurementConfig(name) {
|
||||
return {
|
||||
general: {
|
||||
logging: { enabled: true, logLevel: "debug" },
|
||||
name,
|
||||
id: `${name}-${Date.now()}`,
|
||||
unit: "m"
|
||||
},
|
||||
functionality: {
|
||||
softwareType: "measurement",
|
||||
role: "sensor",
|
||||
positionVsParent: "atEquipment"
|
||||
},
|
||||
asset: {
|
||||
category: "sensor",
|
||||
type: "level",
|
||||
model: "demo-level",
|
||||
supplier: "demoCo",
|
||||
unit: "m"
|
||||
},
|
||||
scaling: { enabled: false },
|
||||
smoothing: { smoothWindow: 5, smoothMethod: "none" }
|
||||
};
|
||||
}
|
||||
|
||||
function createFlowMeasurementConfig(name, position) {
|
||||
return {
|
||||
general: {
|
||||
logging: { enabled: true, logLevel: "debug" },
|
||||
name,
|
||||
id: `${name}-${Date.now()}`,
|
||||
unit: "m3/s"
|
||||
},
|
||||
functionality: {
|
||||
softwareType: "measurement",
|
||||
role: "sensor",
|
||||
positionVsParent: position
|
||||
},
|
||||
asset: {
|
||||
category: "sensor",
|
||||
type: "flow",
|
||||
model: "demo-flow",
|
||||
supplier: "demoCo",
|
||||
unit: "m3/s"
|
||||
},
|
||||
scaling: { enabled: false },
|
||||
smoothing: { smoothWindow: 5, smoothMethod: "none" }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function createMachineConfig(name) {
|
||||
|
||||
curve = require('C:/Users/zn375/.node-red/public/fallbackData.json');
|
||||
return {
|
||||
|
||||
general: {
|
||||
name: name,
|
||||
logging: {
|
||||
enabled: true,
|
||||
logLevel: "warn",
|
||||
}
|
||||
},
|
||||
asset: {
|
||||
supplier: "Hydrostal",
|
||||
type: "pump",
|
||||
category: "centrifugal",
|
||||
model: "hidrostal-H05K-S03R", // Ensure this field is present.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createMachineStateConfig() {
|
||||
return {
|
||||
general: {
|
||||
logging: {
|
||||
enabled: true,
|
||||
logLevel: "debug",
|
||||
},
|
||||
},
|
||||
// Your custom config here (or leave empty for defaults)
|
||||
movement: {
|
||||
speed: 1,
|
||||
},
|
||||
time: {
|
||||
starting: 2,
|
||||
warmingup: 3,
|
||||
stopping: 2,
|
||||
coolingdown: 3,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// convenience for seeding measurements
|
||||
function pushSample(measurement, type, value, unit) {
|
||||
const pos = measurement.config.functionality.positionVsParent;
|
||||
measurement.measurements
|
||||
.type(type)
|
||||
.variant("measured")
|
||||
.position(pos)
|
||||
.value(value, Date.now(), unit);
|
||||
}
|
||||
|
||||
/** Demo *********************************************************************/
|
||||
(async function demoStationWithPump() {
|
||||
const station = new PumpingStation(createPumpingStationConfig("PumpingStationDemo"));
|
||||
const pump = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig());
|
||||
|
||||
const levelSensor = new Measurement(createLevelMeasurementConfig("WetWellLevel"));
|
||||
const upstreamFlow = new Measurement(createFlowMeasurementConfig("InfluentFlow", "upstream"));
|
||||
const downstreamFlow = new Measurement(createFlowMeasurementConfig("PumpDischargeFlow", "downstream"));
|
||||
|
||||
|
||||
// station uses the sensors
|
||||
/*
|
||||
station.childRegistrationUtils.registerChild(levelSensor, levelSensor.config.functionality.softwareType);
|
||||
station.childRegistrationUtils.registerChild(upstreamFlow, upstreamFlow.config.functionality.softwareType);
|
||||
station.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.softwareType);
|
||||
*/
|
||||
|
||||
// pump owns the downstream flow sensor
|
||||
pump.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.positionVsParent);
|
||||
station.childRegistrationUtils.registerChild(pump,"downstream");
|
||||
|
||||
setInterval(() => station.tick(), 1000);
|
||||
|
||||
// seed a starting level & flow
|
||||
/*
|
||||
pushSample(levelSensor, "level", 1.8, "m");
|
||||
pushSample(upstreamFlow, "flow", 0.35, "m3/s");
|
||||
pushSample(downstreamFlow, "flow", 0.20, "m3/s");
|
||||
*/
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
|
||||
// pump increases discharge flow
|
||||
/*
|
||||
pushSample(downstreamFlow, "flow", 0.28, "m3/s");
|
||||
pushSample(upstreamFlow, "flow", 0.40, "m3/s");
|
||||
pushSample(levelSensor, "level", 1.85, "m");
|
||||
*/
|
||||
console.log("Station output:", station.getOutput());
|
||||
await pump.handleInput("parent", "execSequence", "startup");
|
||||
await pump.handleInput("parent", "execMovement", 50);
|
||||
console.log("Station state:", station.state);
|
||||
console.log("Station output:", station.getOutput());
|
||||
console.log("Pump state:", pump.state.getCurrentState());
|
||||
})();
|
||||
|
||||
|
||||
/*
|
||||
|
||||
// */
|
||||
|
||||
|
||||
//coolprop example
|
||||
(async () => {
|
||||
const PropsSI = await coolprop.getPropsSI();
|
||||
|
||||
@@ -178,3 +695,4 @@ module.exports = pumpingStation;
|
||||
console.error('Example known-good call:', PropsSI('D', 'T', 298.15, 'P', 101325, 'Water'));
|
||||
}
|
||||
})();
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user