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)} 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; } 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')); } })(); */