Compare commits

..

2 Commits

Author SHA1 Message Date
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
2 changed files with 302 additions and 32 deletions

View File

@@ -178,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');

View File

@@ -25,8 +25,9 @@ class pumpingStation {
// 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.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
this.logger.debug('pumpstation Initialized with all helpers');
@@ -36,6 +37,7 @@ class pumpingStation {
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;
@@ -51,18 +53,103 @@ class pumpingStation {
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}`);
//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('atEquipment').value(eventData.value,eventData.timestamp,eventData.unit);
});
}
// add one for group later
if( softwareType == "machineGroup" ){
}
}
//update prediction in outgoing downstream flow
_updateDownstreamFlowPrediction(){
//get downflow
const downFlowExists = this.measurements.type("flow").variant("predicted").position("atEquipment").exists();
if(!downFlowExists){return};
const downFlow = this.measurements.type("flow").variant("predicted").position("atEquipment");
const currDownFlow = downFlow.getLaggedValue(0, "m3/s"); // { value, timestamp, unit }
const prevDownFlow = downFlow.getLaggedValue(1, "m3/s"); // { value, timestamp, unit }
if (!currDownFlow || !prevDownFlow) return;
this.logger.debug(`currDownflow = ${currDownFlow.value} , prevDownFlow = ${prevDownFlow.value}`);
// calc difference in time
const deltaT = currDownFlow.timestamp - prevDownFlow.timestamp;
const deltaSeconds = deltaT / 1000;
if (deltaSeconds <= 0) {
this.logger.warn(`Flow integration aborted; invalid Δt=${deltaSeconds}s.`);
return;
}
const avgFlow = (currDownFlow.value + prevDownFlow.value) / 2;
const volumeSubstracted = 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');
const newVol = currVolume - volumeSubstracted;
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} `);
}
//update prediction in incomming upstream flow
_updateUpstreamFlowPrediction(){
}
//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._updateDownstreamFlowPrediction();
}
_callMeasurementHandler(measurementType, value, position, context) {
switch (measurementType) {
case 'pressure':
@@ -87,7 +174,7 @@ class pumpingStation {
this.updatePosition();
break;
}
}
}
// context handler for pressure updates
updateMeasuredPressure(value, position, context = {}) {
@@ -109,6 +196,7 @@ 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;
}
@@ -137,9 +225,9 @@ class pumpingStation {
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.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)
this.measurements.type("volume").variant("procent").position("atEquipment").value(proc);
//calc the most important values back to determine state and net up or downstream flow
@@ -147,8 +235,6 @@ this.logger.debug(`PROC volume : ${proc}`);
}
_calcNetFlow() {
const { heightOverflow, heightOutlet, surfaceArea } = this.basin;
@@ -166,7 +252,7 @@ this.logger.debug(`PROC volume : ${proc}`);
if (flowBased && levelBased) {
this.logger.debug(
`Flow vs Level comparison | flow=${flowBased.netFlowRate.toFixed(3)} ` +
`Flow vs Level comparison | flow=${flowBased.netFlowRate.value.toFixed(3)} ` +
`m3/s, level=${levelBased.netFlowRate.toFixed(3)} m3/s`
);
}
@@ -211,18 +297,14 @@ this.logger.debug(`PROC volume : ${proc}`);
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`
`Flow-based net flow | diff=${flowDiff.value.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 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();
@@ -257,12 +339,7 @@ this.logger.debug(`PROC volume : ${proc}`);
const netFlowRate = lvlRate * surfaceArea; // m³/s inferred from level trend
this.measurements
.type("netFlowRate")
.variant("predicted")
.position("atEquipment")
.value(netFlowRate)
.unit("m3/s");
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`
@@ -298,9 +375,14 @@ this.logger.debug(`PROC volume : ${proc}`);
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)}`
);
//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)}`
);
}
@@ -310,6 +392,11 @@ _calcVolumeFromLevel(level) {
return Math.max(level, 0) * surfaceArea;
}
_calcLevelFromVolume(vol){
const surfaceArea = this.basin.surfaceArea;
return Math.max(vol, 0) / surfaceArea;
}
getOutput() {
return {
@@ -321,10 +408,192 @@ _calcVolumeFromLevel(level) {
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");
*/
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();