Files
pumpingStation/src/specificClass.js
2025-10-27 19:55:48 +01:00

844 lines
31 KiB
JavaScript

const EventEmitter = require('events');
const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation} = require('generalFunctions');
class pumpingStation {
constructor(config={}) {
this.emitter = new EventEmitter(); // Own EventEmitter
this.configManager = new configManager();
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
});
// init basin object in pumping station
this.basin = {};
this.state = {
direction: "steady",
netFlow: 0,
flowSource: null,
seconds: null,
remainingSource: null
}; // init state object of pumping station to see whats going on
// 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;
const measurementType = child.config.asset.type;
const key = `${measurementType}_${position}`;
//rebuild to measurementype.variant no position and then switch based on values not strings or names.
const eventName = `${measurementType}.measured.${position}`;
this.logger.debug(`Setting up listener for ${eventName} from child ${child.config.general.name}`);
// Register event listener for measurement updates
child.measurements.emitter.on(eventName, (eventData) => {
this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
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);
// 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(`Flowdir ${flowDir} => currFlow ${currFLow.value} , prevflow = ${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();
const {time:timeleft, source:variant} = this._calcTimeRemaining();
this.logger.debug(`Remaining time ~${Math.round(timeleft/60/60*10)/10} h, based on variant ${variant} `);
}
_calcTimeRemaining(){
//init timeRemaining
const winningTime = {time:0,source:""};
//calculate time left prioritise flow based variant
const { time: flowTime, variant: flowVariant } = this._selectBestRemainingTimeFlowVariant();
//if flow doesnt work then use level based varianti to calc timeleft
if(flowVariant == null){
const {time: levelTime, variant: levelVariant} = this._selectBestRemainingTimeLevelVariant();
winningTime.time = levelTime;
winningTime.source = levelVariant;
if(levelVariant == null){
winningTime.time = null;
winningTime.source = null;
}
}
else{
winningTime.time = flowTime;
winningTime.source = flowVariant;
}
return winningTime;
}
// Select remaining time based on flow + level variation measured or predicted and give back {time:0,variant:null};
_selectBestRemainingTimeFlowVariant(){
//define variants
const remainingTimeVariants = [
{ flowVariant: "measured", levelVariant: "measured" },
{ flowVariant: "measured", levelVariant: "predicted" },
{ flowVariant: "predicted", levelVariant: "measured" },
{ flowVariant: "predicted", levelVariant: "predicted" }
];
let remainingT = null;
for (const variant of remainingTimeVariants) {
const candidate = this._calcRemainingTimeBasedOnFlow(variant);
if (candidate != null) {
remainingT = candidate;
return {time:remainingT,variant:variant};
}
}
return {time:0,variant:null};
}
// Select remaining time based only on level variation measured or predicted and give back {time:0,variant:null};
_selectBestRemainingTimeLevelVariant(){
//define variants (in sequence of priority first measured then predicted etc...)
const remainingTimeVariants = ["measured","predicted"];
let remainingT = null;
for (const variant of remainingTimeVariants) {
const candidate = this._calcRemainingTimeBasedOnLevel(variant);
if (candidate != null) {
remainingT = candidate;
return {time:remainingT,variant:variant};
}
}
return {time:0,variant:null};
}
_callMeasurementHandler(measurementType, value, position, context) {
switch (measurementType) {
case 'pressure':
this.updateMeasuredPressure(value, position, context);
break;
case 'flow':
this.updateMeasuredFlow(value, position, context);
break;
case 'temperature':
this.updateMeasuredTemperature(value, position, context);
break;
case 'level':
this.updateMeasuredLevel(value, position, context);
break;
default:
this.logger.warn(`No handler for measurement type: ${measurementType}`);
// Generic handler - just update position
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
this.logger.debug(`Pressure update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`);
// Store in parent's measurement container for the first time
this.measurements.type("pressure").variant("measured").position(position).value(value, context.timestamp, context.unit);
//convert pressure to level based on density of water and height of pressure sensor
const mTemp = this.measurements.type("temperature").variant("measured").position("atEquipment").getCurrentValue('K'); //default to 20C if no temperature measurement
//prefer measured temp but otherwise assume nominal temp for wastewater
if(mTemp === null){
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 = 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: ${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("measured");
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;
}
}
//@params : params : example {flowVariant: "predicted",levelVariant: "measured"};
_calcRemainingTimeBasedOnFlow(params){
const {flowVariant,levelVariant} = params;
this.logger.debug(`${flowVariant} - ${levelVariant} `);
if( flowVariant === null || levelVariant === null ){
this.logger.warn(`Cant calculate remaining time without needed variants`);
return 0;
}
const { heightOverflow, heightOutlet, surfaceArea } = this.basin;
const levelexists = this.measurements.type("level").variant(levelVariant).exists({ position: "atEquipment", requireValues: true });
const flowOutExists = this.measurements.type("flow").variant(flowVariant).exists({ position: "out", requireValues: true });
const flowInExists = this.measurements.type("flow").variant(flowVariant).exists({ position: "in", requireValues: true });
let secondsRemaining = 0;
if( ! flowOutExists || ! flowInExists || ! levelexists){
this.logger.warn(`Cant calculate remaining time without needed parameters ${flowOutExists} , ${flowInExists} , ${levelexists}`);
return null;
}
const flowDiff = this.measurements.type("flow").variant(flowVariant).difference({ from: "downstream", to: "upstream", unit: "m3/s" });
const level = this.measurements.type("level").variant(levelVariant).type('atEquipment').getCurrentValue('m');
let remainingHeight = 0;
switch(true){
case(flowDiff>0):
remainingHeight = Math.max(heightOverflow - level, 0);
secondsRemaining = remainingHeight * surfaceArea / flowDiff;
return secondsRemaining;
case(flowDiff<0):
remainingHeight = Math.max(level - heightOutlet, 0);
secondsRemaining = remainingHeight * surfaceArea / Math.abs(flowDiff);
return secondsRemaining;
default:
this.logger.debug(`Flowdiff is 0 not doing anything.`);
return secondsRemaining;
}
}
//@params : variant : example "predicted","measured"
_calcRemainingTimeBasedOnLevel(variant){
const {heightOverflow,heightOutlet} = this.basin;
const levelObj = this.measurements.type("level").variant(variant).position("atEquipment");
const level = levelObj.getCurrentValue("m");
const prevLevelSample = levelObj.getLaggedSample(2, "m"); // { value, timestamp, unit }
const measurement = levelObj.get();
const latestTimestamp = measurement?.getLatestTimestamp();
if (level === null || prevLevelSample == null || latestTimestamp == null) {
this.logger.warn(`no flowdiff ${level}, previous level ${prevLevelSample}, latestTimestamp ${latestTimestamp} found escaping`);
return null;
}
const deltaSeconds = (latestTimestamp - prevLevelSample.timestamp) / 1000;
if (deltaSeconds <= 0) {
this.logger.warn(`Level fallback: invalid Δt=${deltaSeconds} , LatestTimestamp : ${latestTimestamp}, PrevTimestamp : ${prevLevelSample.value}`);
return null;
}
const lvlDiff = level - prevLevelSample.value;
const lvlRate = lvlDiff / deltaSeconds; // m/s
let secondsRemaining = 0;
let remainingHeight = 0;
switch(true){
case(lvlRate>0):
remainingHeight = Math.max(heightOverflow - level, 0);
secondsRemaining = remainingHeight / Math.abs(lvlRate); // seconds
return secondsRemaining;
case(lvlRate<0):
remainingHeight = Math.max(level - heightOutlet, 0);
secondsRemaining = remainingHeight / Math.abs(lvlRate);
return secondsRemaining;
default:
this.logger.debug(`Flowdiff is 0 not doing anything.`);
return secondsRemaining;
}
}
//Give a flowDifference and calculate direction => spits out filling , draining or stable
_calcDirectionBasedOnFlow(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(variant) {
const { surfaceArea } = this.basin;
const levelObj = this.measurements.type("level").variant(variant).position("atEquipment");
const level = levelObj.getCurrentValue("m");
const prevLevelSample = levelObj.getLaggedSample(2, "m"); // { value, timestamp, unit }
const measurement = levelObj.get();
const latestTimestamp = measurement?.getLatestTimestamp();
if (level === null || prevLevelSample == null || latestTimestamp == null) {
this.logger.warn(`no flowdiff ${level}, previous level ${prevLevelSample}, latestTimestamp ${latestTimestamp} found escaping`);
return null;
}
const deltaSeconds = (latestTimestamp - prevLevelSample.timestamp) / 1000;
if (deltaSeconds <= 0) {
this.logger.warn(`Level fallback: invalid Δt=${deltaSeconds} , LatestTimestamp : ${latestTimestamp}, PrevTimestamp : ${prevLevelSample.timestamp}`);
return null;
}
const lvlDiff = level - prevLevelSample.value;
const lvlRate = lvlDiff / deltaSeconds; // m/s
const netFlowRate = lvlRate * surfaceArea; // m³/s inferred from level trend
return netFlowRate;
}
initBasinProperties() {
// 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.measurements.type("volume").variant("predicted").position("atEquipment").value(maxVol).unit('m3');
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;
}
_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;
}
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 pump1 = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig());
const pump2 = new RotatingMachine(createMachineConfig("Pump2"), 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(pump1,"downstream");
station.childRegistrationUtils.registerChild(pump2,"upstream");
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 pump1.handleInput("parent", "execSequence", "startup");
await pump2.handleInput("parent", "execSequence", "startup");
await pump1.handleInput("parent", "execMovement", 5);
await pump2.handleInput("parent", "execMovement", 5);
console.log("Station state:", station.state);
console.log("Station output:", station.getOutput());
console.log("Pump state:", pump1.state.getCurrentState());
console.log("Pump state:", pump2.state.getCurrentState());
})();
/*
//coolprop example
(async () => {
const PropsSI = await coolprop.getPropsSI();
// 👇 replace these with your real inputs
const tC_input = 25; // °C
const pPa_input = 101325; // Pa
// Sanitize & convert
const T = Number(tC_input) + 273.15; // K
const P = Number(pPa_input); // Pa
const fluid = 'Water';
// Preconditions
if (!Number.isFinite(T) || !Number.isFinite(P)) {
throw new Error(`Bad inputs: T=${T} K, P=${P} Pa`);
}
if (T <= 0) throw new Error(`Temperature must be in Kelvin (>0). Got ${T}.`);
if (P <= 0) throw new Error(`Pressure must be >0 Pa. Got ${P}.`);
// Try T,P order
let rho = PropsSI('D', 'T', T, 'P', P, fluid);
// Fallback: P,T order (should be equivalent)
if (!Number.isFinite(rho)) rho = PropsSI('D', 'P', P, 'T', T, fluid);
console.log({ T, P, rho });
if (!Number.isFinite(rho)) {
console.error('Still Infinity. Extra checks:');
console.error('typeof T:', typeof T, 'typeof P:', typeof P);
console.error('Example known-good call:', PropsSI('D', 'T', 298.15, 'P', 101325, 'Water'));
}
})();
*/