diff --git a/src/nodeClass.js b/src/nodeClass.js index 07cc409..d0e82b6 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -228,7 +228,7 @@ class nodeClass { case 'q_in': { // payload can be number or { value, unit, timestamp } const val = Number(msg.payload); - const unit = msg?.unit || 'l/s'; + const unit = msg?.unit; const ts = msg?.timestamp || Date.now(); this.source.setManualInflow(val, ts, unit); break; diff --git a/src/specificClass.js b/src/specificClass.js index 5578628..ed4372b 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -1,5 +1,13 @@ const EventEmitter = require('events'); -const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation,convert} = require('generalFunctions'); +const { + logger, + configUtils, + configManager, + childRegistrationUtils, + MeasurementContainer, + coolprop, + interpolation +} = require('generalFunctions'); class PumpingStation { constructor(config = {}) { @@ -16,39 +24,30 @@ class PumpingStation { preferredUnits: { flow: 'm3/s', netFlowRate: 'm3/s', level: 'm', volume: 'm3' } }); - this.childRegistrationUtils = new childRegistrationUtils(this); - this.machines = {}; - this.stations = {}; - this.machineGroups = {}; + this.machines = {}; + this.stations = {}; + this.machineGroups = {}; + this.predictedFlowChildren = new Map(); - //fetch control mode from config by default - this.mode = this.config.control.mode; - this._levelState = { crossed: new Set(), dwellUntil: null }; - - //variants in determining what gets priority this.flowVariants = ['measured', 'predicted']; this.levelVariants = ['measured', 'predicted']; this.volVariants = ['measured', 'predicted']; this.flowPositions = { inflow: ['in', 'upstream'], outflow: ['out', 'downstream'] }; - this.predictedFlowChildren = new Map(); // childId -> { in: 0, out: 0 } - this.basin = {}; - this.state = { - direction: 'steady', - netFlow: 0, - flowSource: null, - seconds: null, - remainingSource: null - }; + this.mode = this.config.control.mode; + this._levelState = { crossed: new Set(), dwellUntil: null }; + this.state = { direction: 'steady', netFlow: 0, flowSource: null, seconds: null, remainingSource: null }; const thresholdFromConfig = Number(this.config.general?.flowThreshold); this.flowThreshold = Number.isFinite(thresholdFromConfig) ? thresholdFromConfig : 1e-4; this.initBasinProperties(); - this.logger.debug('PumpingStationV2 initialized'); + this.logger.debug('PumpingStation initialized'); } + /* --------------------------- Registration --------------------------- */ + registerChild(child, softwareType) { this.logger.debug(`Registering child (${softwareType}) "${child.config.general.name}"`); @@ -57,48 +56,440 @@ class PumpingStation { return; } - //for machines register them for control - if(softwareType === 'machine'){ - const childId = child.config.general.id; - this.machines[childId] = child; - this.logger.debug(`Registered machine child "${child.config.general.name}" with id "${childId}"`); - } - - // for pumping stations register them for control - if(softwareType === 'pumpingstation'){ - const childId = child.config.general.id; - this.stations[childId] = child; - this.logger.debug(`Registered pumping station child "${child.config.general.name}" with id "${childId}"`); - } - - // for machine group controllers register them for control - if(softwareType === 'machinegroup'){ - const childId = child.config.general.id; - this.machineGroups[childId] = child; + if (softwareType === 'machine') { + this.machines[child.config.general.id] = child; + } else if (softwareType === 'pumpingstation') { + this.stations[child.config.general.id] = child; + } else if (softwareType === 'machinegroup') { + this.machineGroups[child.config.general.id] = child; this._registerPredictedFlowChild(child); - this.logger.debug(`Registered machine group child "${child.config.general.name}" with id "${childId}"`); } - //for all childs that can provide predicted flow data if (softwareType === 'machine' || softwareType === 'pumpingstation' || softwareType === 'machinegroup') { - this.logger.debug(`Registering predicted flow child ${child.config.general.name} with software type: ${softwareType}"`); this._registerPredictedFlowChild(child); } - - //this.logger.warn(`Unsupported child software type: ${softwareType}`); } - _safetyController(remainingTime, direction){ + _registerMeasurementChild(child) { + const position = child.config.functionality.positionVsParent; + const measurementType = child.config.asset.type; + const eventName = `${measurementType}.measured.${position}`; + child.measurements.emitter.on(eventName, (eventData = {}) => { + this.logger.debug( + `Measurement update ${eventName} <- ${eventData.childName || child.config.general.name}: ${eventData.value} ${eventData.unit}` + ); + + this.measurements + .type(measurementType) + .variant('measured') + .position(position) + .value(eventData.value, eventData.timestamp, eventData.unit); + + this._handleMeasurement(measurementType, eventData.value, position, eventData); + }); + } + + _registerPredictedFlowChild(child) { + const position = (child.config.functionality.positionVsParent || '').toLowerCase(); + const childName = child.config.general.name; + const childId = child.config.general.id ?? childName; + + let posKey; + let eventNames; + switch (position) { + case 'downstream': + case 'out': + case 'atequipment': + posKey = 'out'; + eventNames = ['flow.predicted.downstream', 'flow.predicted.atequipment']; + break; + case 'upstream': + case 'in': + posKey = 'in'; + eventNames = ['flow.predicted.upstream', 'flow.predicted.atequipment']; + break; + default: + this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`); + return; + } + + if (!this.predictedFlowChildren.has(childId)) { + this.predictedFlowChildren.set(childId, { in: 0, out: 0 }); + } + + const handler = (eventData = {}) => { + const unit = eventData.unit || child.config?.general?.unit; + const ts = eventData.timestamp || Date.now(); + + this.logger.debug(`Emitting for child ${unit} `); + this.measurements + .type('flow') + .variant('predicted') + .position(posKey) + .child(childId) + .value(eventData.value, ts, unit); + }; + + eventNames.forEach((ev) => child.measurements.emitter.on(ev, handler)); + } + + /* --------------------------- Calibration --------------------------- */ + + calibratePredictedVolume(calibratedVol, timestamp = Date.now()) { + const volumeChain = this.measurements.type('volume').variant('predicted').position('atequipment'); + const levelChain = this.measurements.type('level').variant('predicted').position('atequipment'); + + const volumeMeasurement = volumeChain.exists() ? volumeChain.get() : null; + if (volumeMeasurement) { + volumeMeasurement.values = []; + volumeMeasurement.timestamps = []; + } + + const levelMeasurement = levelChain.exists() ? levelChain.get() : null; + if (levelMeasurement) { + levelMeasurement.values = []; + levelMeasurement.timestamps = []; + } + + volumeChain.value(calibratedVol, timestamp, 'm3').unit('m3'); + levelChain.value(this._calcLevelFromVolume(calibratedVol), timestamp, 'm'); + + this._predictedFlowState = { inflow: 0, outflow: 0, lastTimestamp: timestamp }; + } + + calibratePredictedLevel(val, timestamp = Date.now(), unit = 'm') { + const volumeChain = this.measurements.type('volume').variant('predicted').position('atequipment'); + const levelChain = this.measurements.type('level').variant('predicted').position('atequipment'); + + const volumeMeasurement = volumeChain.exists() ? volumeChain.get() : null; + if (volumeMeasurement) { + volumeMeasurement.values = []; + volumeMeasurement.timestamps = []; + } + + const levelMeasurement = levelChain.exists() ? levelChain.get() : null; + if (levelMeasurement) { + levelMeasurement.values = []; + levelMeasurement.timestamps = []; + } + + levelChain.value(val, timestamp).unit(unit); + volumeChain.value(this._calcVolumeFromLevel(val), timestamp, 'm3'); + + this._predictedFlowState = { inflow: 0, outflow: 0, lastTimestamp: timestamp }; + } + + setManualInflow(value, timestamp = Date.now(), unit) { + const num = Number(value); + this.measurements.type('flow').variant('predicted').position('in').child('manual-qin').value(num, timestamp, unit); + } + + /* --------------------------- Tick / Control --------------------------- */ + + tick() { + this._updatePredictedVolume(); + + const netFlow = this._selectBestNetFlow(); + const remaining = this._computeRemainingTime(netFlow); + + this._safetyController(remaining.seconds, netFlow.direction); + if (this.safetyControllerActive) return; + + this._controlLogic(netFlow.direction); + + this.state = { + direction: netFlow.direction, + netFlow: netFlow.value, + flowSource: netFlow.source, + seconds: remaining.seconds, + remainingSource: remaining.source + }; + + this.logger.debug(`netflow = ${JSON.stringify(netFlow)}`); + this.logger.debug( + `Height : ${this.measurements.type('level').variant('predicted').position('atequipment').getCurrentValue('m')} m` + ); + } + + _controlLogic(direction) { + switch (this.mode) { + case 'levelbased': + this._controlLevelBased(direction); + break; + case 'flowbased': + this._controlFlowBased?.(); + break; + case 'manual': + break; + default: + this.logger.warn(`Unsupported control mode: ${this.mode}`); + } + } + + async _controlLevelBased(direction) { + const { startLevel, stopLevel } = this.config.control.levelbased; + const flowUnit = this.measurements.getUnit('flow'); + const levelUnit = this.measurements.getUnit('level'); + + const level = this._pickVariant('level', this.levelVariants, 'atequipment', levelUnit); + if (level == null) { + this.logger.warn('No valid level found'); + return; + } + + if (level > startLevel && direction === 'filling') { + const percControl = this._scaleLevelToFlowPercent(level); + this.logger.debug(`Controllevel based => Level ${level} control applying to pump : ${percControl}`); + await this._applyMachineLevelControl(percControl); + } + + if (level < stopLevel && direction === 'draining') { + Object.values(this.machines).forEach((machine) => { + const pos = machine?.config?.functionality?.positionVsParent; + if ((pos === 'downstream' || pos === 'atequipment') && machine._isOperationalState()) { + machine.handleInput('parent', 'execSequence', 'shutdown'); + } + }); + Object.values(this.stations).forEach((station) => station.handleInput('parent', 'execSequence', 'shutdown')); + Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines()); + } + } + + _controlFlowBased() { + // placeholder for flow-based logic + } + + async _applyMachineGroupLevelControl(percentControl) { + if (!this.machineGroups || Object.keys(this.machineGroups).length === 0) return; + await Promise.all( + Object.values(this.machineGroups).map((group) => + group.handleInput('parent', percentControl).catch((err) => { + this.logger.error(`Failed to send level control to group "${group.config.general.name}": ${err.message}`); + }) + ) + ); + } + + async _applyMachineLevelControl(percentControl) { + const machines = Object.values(this.machines).filter((machine) => { + const pos = machine?.config?.functionality?.positionVsParent; + return (pos === 'downstream' || pos === 'atequipment'); + }); + + if (!machines.length) return; + + const perMachine = percentControl / machines.length; + for (const machine of machines) { + try { + await machine.handleInput('parent', 'execSequence', 'startup'); + await machine.handleInput('parent', 'execMovement', perMachine); + } catch (err) { + this.logger.error(`Failed to start machine "${machine.config.general.name}": ${err.message}`); + } + } + } + + /* --------------------------- Measurements --------------------------- */ + + _handleMeasurement(measurementType, value, position, context) { + switch (measurementType) { + case 'level': + this._onLevelMeasurement(position, value, context); + break; + case 'pressure': + this._onPressureMeasurement(position, value, context); + break; + default: + break; + } + } + + _onLevelMeasurement(position, value, context = {}) { + this.measurements.type('level').variant('measured').position(position).value(value).unit(context.unit); + const levelSeries = this.measurements.type('level').variant('measured').position(position); + const levelMeters = levelSeries.getCurrentValue('m'); + if (levelMeters == null) return; + + const volume = this._calcVolumeFromLevel(levelMeters); + const percent = this.interpolate.interpolate_lin_single_point( + volume, + this.basin.minVol, + this.basin.maxVolOverflow, + 0, + 100 + ); + + this.measurements.type('volume').variant('measured').position('atequipment').value(volume, context.timestamp, 'm3'); + this.measurements + .type('volumePercent') + .variant('measured') + .position('atequipment') + .value(percent, context.timestamp, '%'); + } + + _onPressureMeasurement(position, value, context = {}) { + let kelvinTemp = + this.measurements.type('temperature').variant('measured').position('atequipment').getCurrentValue('K') ?? null; + + if (kelvinTemp === null) { + this.logger.warn('No temperature measurement; assuming 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'); + } + + if (kelvinTemp == null) return; + + const density = coolprop.PropsSI('D', 'T', kelvinTemp, 'P', 101325, 'Water'); + const pressurePa = this.measurements.type('pressure').variant('measured').position(position).getCurrentValue('Pa'); + if (!Number.isFinite(pressurePa) || !Number.isFinite(density)) return; + + const g = 9.80665; + const level = pressurePa / (density * g); + this.measurements.type('level').variant('predicted').position(position).value(level, context.timestamp, 'm'); + } + + /* --------------------------- Core Calculations --------------------------- */ + + _pickVariant(type, variants, position, unit) { + for (const variant of variants) { + const val = this.measurements.type(type).variant(variant).position(position).getCurrentValue(unit); + if (!Number.isFinite(val)) continue; + return val; + } + return null; + } + + _scaleLevelToFlowPercent(level) { + const { minFlowLevel, maxFlowLevel } = this.config.control.levelbased; + this.logger.debug(`Scaling minflow level : ${minFlowLevel} and maxflowLevel : ${maxFlowLevel}`); + return this.interpolate.interpolate_lin_single_point(level, minFlowLevel, maxFlowLevel, 0, 100); + } + + _levelRate(variant) { + const chain = this.measurements.type('level').variant(variant).position('atequipment'); + if (!chain.exists({ requireValues: true })) return null; + const m = chain.get(); + const current = m?.getLaggedSample?.(0); + const previous = m?.getLaggedSample?.(1); + if (!current || !previous || previous.timestamp == null) return null; + const dt = (current.timestamp - previous.timestamp) / 1000; + if (!Number.isFinite(dt) || dt <= 0) return null; + return (current.value - previous.value) / dt; + } + + _updatePredictedVolume() { + const flowUnit = 'm3/s'; // this has to be in m3/s for the actions below + const now = Date.now(); + + const inflow = this.measurements.sum('flow', 'predicted', this.flowPositions.inflow, flowUnit) || 0; + const outflow = this.measurements.sum('flow', 'predicted', this.flowPositions.outflow, flowUnit) || 0; + + if (!this._predictedFlowState) { + this._predictedFlowState = { inflow, outflow, lastTimestamp: now }; + } + + const timestampPrev = this._predictedFlowState.lastTimestamp ?? now; + const deltaSeconds = Math.max((now - timestampPrev) / 1000, 0); + const netVolumeChange = deltaSeconds > 0 ? (inflow - outflow) * deltaSeconds : 0; + + const volumeSeries = this.measurements.type('volume').variant('predicted').position('atequipment'); + const currentVolume = volumeSeries.getCurrentValue('m3'); + + const nextVolume = currentVolume + netVolumeChange; + const writeTimestamp = timestampPrev + deltaSeconds * 1000; + + volumeSeries.value(nextVolume, writeTimestamp, 'm3').unit('m3'); //olifant + + const nextLevel = this._calcLevelFromVolume(nextVolume); + this.measurements + .type('level') + .variant('predicted') + .position('atequipment') + .value(nextLevel, writeTimestamp, 'm') + .unit('m'); + + const percent = this.interpolate.interpolate_lin_single_point( + nextVolume, + this.basin.minVol, + this.basin.maxVolOverflow, + 0, + 100 + ); + + this.measurements + .type('volumePercent') + .variant('predicted') + .position('atequipment') + .value(percent, writeTimestamp, '%'); + + this._predictedFlowState = { inflow, outflow, lastTimestamp: writeTimestamp }; + } + + _selectBestNetFlow() { + const type = 'flow'; + const unit = this.measurements.getUnit(type) || 'm3/s'; + + for (const variant of this.flowVariants) { + const bucket = this.measurements.measurements?.[type]?.[variant]; + if (!bucket || Object.keys(bucket).length === 0) continue; + + const inflow = this.measurements.sum(type, variant, this.flowPositions.inflow, unit) || 0; + const outflow = this.measurements.sum(type, variant, this.flowPositions.outflow, unit) || 0; + if (Math.abs(inflow) < this.flowThreshold && Math.abs(outflow) < this.flowThreshold) continue; + + const net = inflow - outflow; + this.measurements.type('netFlowRate').variant(variant).position('atequipment').value(net, Date.now(), unit); + return { value: net, source: variant, direction: this._deriveDirection(net) }; + } + + // Fallback: level trend + for (const variant of this.levelVariants) { + const rate = this._levelRate(variant); + if (!Number.isFinite(rate)) continue; + const netFlow = rate * this.basin.surfaceArea; + return { value: netFlow, source: `level:${variant}`, direction: this._deriveDirection(netFlow) }; + } + + this.logger.warn('No usable measurements to compute net flow; assuming steady.'); + return { value: 0, source: null, direction: 'steady' }; + } + + _computeRemainingTime(netFlow) { + if (!netFlow || Math.abs(netFlow.value) < this.flowThreshold) return { seconds: null, source: null }; + + const { heightOverflow, heightOutlet, surfaceArea } = this.basin; + if (!Number.isFinite(surfaceArea) || surfaceArea <= 0) return { seconds: null, source: null }; + + for (const variant of this.levelVariants) { + const lvl = this.measurements.type('level').variant(variant).position('atequipment').getCurrentValue('m'); + if (!Number.isFinite(lvl)) continue; + + const remainingHeight = netFlow.value > 0 ? Math.max(heightOverflow - lvl, 0) : Math.max(lvl - heightOutlet, 0); + const seconds = (remainingHeight * surfaceArea) / Math.abs(netFlow.value); + if (!Number.isFinite(seconds)) continue; + + return { seconds, source: `${netFlow.source}/${variant}` }; + } + + return { seconds: null, source: netFlow.source }; + } + + _deriveDirection(netFlow) { + if (netFlow > this.flowThreshold) return 'filling'; + if (netFlow < -this.flowThreshold) return 'draining'; + return 'steady'; + } + + /* --------------------------- Safety --------------------------- */ + + _safetyController(remainingTime, direction) { this.safetyControllerActive = false; - const vol = this._resolveVolume(); - - if(vol == null){ - //if we cant get a volume we cant control blind turn all pumps off. - Object.entries(this.machines).forEach(([machineId, machine]) => { - machine.handleInput('parent', 'execSequence', 'shutdown'); - }); + const volUnit = this.measurements.getUnit('volume'); + const vol = this._pickVariant('volume', this.volVariants, 'atequipment', volUnit); + if (vol == null) { + Object.values(this.machines).forEach((machine) => machine.handleInput('parent', 'execSequence', 'shutdown')); this.logger.warn('No volume data available to safe guard system; shutting down all machines.'); this.safetyControllerActive = true; return; @@ -118,822 +509,61 @@ class PumpingStation { const triggerHighVol = this.basin.maxVolOverflow * ((Number(overfillThresholdPercent) || 0) / 100); const triggerLowVol = this.basin.minVol * (1 + ((Number(dryRunThresholdPercent) || 0) / 100)); - // trigger conditions for draining - if(direction == "draining"){ - this.logger.debug( - `Safe-guard (draining): vol=${vol != null ? vol.toFixed(2) + ' m3' : 'N/A'}; ` + - `remainingTime=${Number.isFinite(remainingTime) ? remainingTime.toFixed(1) + ' s' : 'N/A'}; ` + - `direction=${String(direction)}; triggerLowVol=${Number.isFinite(triggerLowVol) ? triggerLowVol.toFixed(2) + ' m3' : 'N/A'}` - ); - + if (direction === 'draining') { const timeTriggered = timeProtectionEnabled && remainingTime != null && remainingTime < timeleftToFullOrEmptyThresholdSeconds; const dryRunTriggered = dryRunEnabled && vol < triggerLowVol; - - if (timeTriggered || dryRunTriggered) { - //shut down all downstream or atequipment machines,pumping stations and machine groups - Object.entries(this.machines).forEach(([machineId, machine]) => { - const position = machine?.config?.functionality?.positionVsParent; - if ((position === 'downstream' || position === 'atEquipment') && machine._isOperationalState()) { - machine.handleInput('parent', 'execSequence', 'shutdown'); - this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down machine "${machineId}"`); - } - }); - Object.entries(this.stations).forEach(([stationId, station]) => { - station.handleInput('parent', 'execSequence', 'shutdown'); - this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down station "${stationId}"`); - }); - Object.entries(this.machineGroups).forEach(([groupId, group]) => { - group.turnOffAllMachines(); - this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down machine group "${groupId}"`); - }); - this.safetyControllerActive = true; - } - } - - if(direction == "filling"){ - this.logger.debug(`Safe-guard (filling): vol=${vol != null ? vol.toFixed(2) + ' m3' : 'N/A'}; ` + - `remainingTime=${Number.isFinite(remainingTime) ? remainingTime.toFixed(1) + ' s' : 'N/A'}; ` + - `direction=${String(direction)}; triggerHighVol=${Number.isFinite(triggerHighVol) ? triggerHighVol.toFixed(2) + ' m3' : 'N/A'}` - ); - - const timeTriggered =timeProtectionEnabled &&remainingTime != null && remainingTime < timeleftToFullOrEmptyThresholdSeconds; - const overfillTriggered = overfillEnabled && vol > triggerHighVol; - - if (timeTriggered || overfillTriggered) { - //shut down all upstream machines,pumping stations and machine groups - Object.entries(this.machines).forEach(([machineId, machine]) => { - const position = machine?.config?.functionality?.positionVsParent; - if ((position === 'upstream' ) && machine._isOperationalState()) { + if (timeTriggered || dryRunTriggered) { + Object.values(this.machines).forEach((machine) => { + const pos = machine?.config?.functionality?.positionVsParent; + if ((pos === 'downstream' || pos === 'atequipment') && machine._isOperationalState()) { machine.handleInput('parent', 'execSequence', 'shutdown'); } }); - Object.entries(this.machineGroups).forEach(([groupId, group]) => { - group.turnOffAllMachines(); - }); - Object.entries(this.stations).forEach(([stationId, station]) => { - station.handleInput('parent', 'execSequence', 'shutdown'); - }); - - this.logger.warn(`Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down all upstream machines/stations/groups`); - this.safetyControllerActive = true; - } - } - - } - - changeMode(newMode){ - if ( this.config.control.allowedModes.has(newMode) ){ - const currentMode = this.mode; - this.logger.info(`Control mode changing from ${currentMode} to ${newMode}`); - this.mode = newMode; - } - else{ - this.logger.warn(`Attempted to change to unsupported control mode: ${newMode}`); - } - - } - - _scaleLevelToFlowPercent(level,minflow,maxflow) { - const { minFlowLevel, maxFlowLevel } = this.config.control.levelbased; - const output = this.interpolate.interpolate_lin_single_point(level,minFlowLevel,maxFlowLevel,minflow,maxflow); - return output; - } - - - async _controlLevelBased(direction) { - const { startLevel, stopLevel } = this.config.control.levelbased; - - const flowUnit = this.measurements.getUnit('flow'); // use container as source of truth - const levelunit = this.measurements.getUnit('level'); // use container as source of truth - - let percControl = 0; - - for (const variant of this.levelVariants) { - - const level = this.measurements.type('level').variant(variant).postition('atEquipment').getCurrentValue(levelunit); - - if(!level) continue; - - } - - - const levelVal = level(snapshot); - if (levelVal == null || !Number.isFinite(levelVal)) { - this.logger.warn('No valid level found'); - return; - } - - if (levelVal > startLevel && direction === 'filling') { - let sumFlow = 0; - let minTotalFlow = Infinity; - Object.values(this.machines).forEach((m) => { - const max = m.measurements.type('flow').variant('predicted').position('max').getCurrentValue(flowUnit); - const min = m.measurements.type('flow').variant('predicted').position('min').getCurrentValue(flowUnit); - if (Number.isFinite(max)) sumFlow += max; - if (Number.isFinite(min) && min < minTotalFlow) minTotalFlow = min; - }); - - this.logger.debug(`showing level : ${levelVal}, minTotalFlow: ${minTotalFlow}, sumFlow ${sumFlow}`); - percControl = this._scaleLevelToFlowPercent(levelVal, minTotalFlow, sumFlow); - await this._applyIdleMachineLevelControl(percControl); - } - - if (levelVal < stopLevel && direction === 'draining') { - Object.entries(this.machines).forEach(([machineId, machine]) => { - const position = machine?.config?.functionality?.positionVsParent; - if ((position === 'downstream' || position === 'atEquipment') && machine._isOperationalState()) { - machine.handleInput('parent', 'execSequence', 'shutdown'); - } - }); - Object.entries(this.stations).forEach(([stationId, station]) => { - station.handleInput('parent', 'execSequence', 'shutdown'); - }); - Object.entries(this.machineGroups).forEach(([groupId, group]) => { - group.turnOffAllMachines(); - }); - } - } - - async _applyMachineGroupLevelControl(percentControl) { - this.logger.debug(`Applying level control to machine groups: ${percentControl.toFixed(1)}% displaying machine groups ${Object.keys(this.machineGroups).join(', ')}`); - if (!this.machineGroups || Object.keys(this.machineGroups).length === 0) return; - - await Promise.all( - Object.values(this.machineGroups).map(async (group) => { - try { - await group.handleInput('parent', percentControl); - } catch (err) { - this.logger.error(`Failed to send level control to group "${group.config.general.name}": ${err.message}`); - } - }) - ); - } - - async _applyIdleMachineLevelControl(percentControl) { - const idleMachines = Object.values(this.machines).filter((machine) => { - const pos = machine?.config?.functionality?.positionVsParent; - return (pos === 'downstream' || pos === 'atEquipment') && !machine._isOperationalState(); - }); - - if (!idleMachines.length) return; - - const perMachine = percentControl / idleMachines.length; - - for (const machine of idleMachines) { - try { - await machine.handleInput('parent', 'execSequence', 'startup'); - await machine.handleInput('parent', 'execMovement', perMachine); - } catch (err) { - this.logger.error(`Failed to start idle machine "${machine.config.general.name}": ${err.message}`); - } - } - } - - _resolveVolume() { - for (const variant of this.volVariants) { - const type = 'volume'; - const unit = this.measurements.getUnit(type); - const volume = this.measurements.type(type).variant(variant).position('atEquipment').getCurrentValue(unit) || null; - - if (!volume) continue; - - return volume; - } - return null; - } - - _nextIdleMachine() { - return Object.values(this.machines).find((machine) => { - const position = machine?.config?.functionality?.positionVsParent; - return ( position === 'downstream' || position === 'atEquipment') && !machine._isOperationalState(); - }); - } - - //control logic - _controlLogic(snapshot,direction){ - const mode = this.mode; - - switch(mode){ - case "levelbased": - this.logger.debug(`Executing level-based control logic`); - this._controlLevelBased(snapshot,direction); - break; - case "flowbased": - this._controlFlowBased(); - break; - case "manual": - this._manualControl(); - break; - default: - this.logger.warn(`Unsupported control mode: ${mode}`); - } - - } - - _manualControl() { - // Nothing to do - manual mode - } - - //calibrate the predicted volume to a known value - calibratePredictedVolume(calibratedVol, timestamp = Date.now()){ - - const volumeChain = this.measurements - .type('volume') - .variant('predicted') - .position('atequipment'); - - const levelChain = this.measurements - .type('level') - .variant('predicted') - .position('atequipment'); - - //if we have existing values clear them out - const volumeMeasurement = volumeChain.exists() ? volumeChain.get() : null; - if (volumeMeasurement) { - volumeMeasurement.values = []; - volumeMeasurement.timestamps = []; - } - - const levelMeasurement = levelChain.exists() ? levelChain.get() : null; - if (levelMeasurement) { - levelMeasurement.values = []; - levelMeasurement.timestamps = []; - } - - volumeChain.value(calibratedVol, timestamp, 'm3').unit('m3'); - - levelChain.value(this._calcLevelFromVolume(calibratedVol), timestamp, 'm'); - - this._predictedFlowState = { - inflow: 0, - outflow: 0, - lastTimestamp: timestamp - }; - } - - calibratePredictedLevel(val,timestamp = Date.now(),unit = 'm'){ - - const volumeChain = this.measurements - .type('volume') - .variant('predicted') - .position('atequipment'); - - const levelChain = this.measurements - .type('level') - .variant('predicted') - .position('atequipment'); - - //if we have existing values clear them out - const volumeMeasurement = volumeChain.exists() ? volumeChain.get() : null; - if (volumeMeasurement) { - volumeMeasurement.values = []; - volumeMeasurement.timestamps = []; - } - - const levelMeasurement = levelChain.exists() ? levelChain.get() : null; - if (levelMeasurement) { - levelMeasurement.values = []; - levelMeasurement.timestamps = []; - } - - levelChain.value(val, timestamp).unit(unit); - - volumeChain.value(this._calcVolumeFromLevel(val), timestamp, 'm3'); - - this._predictedFlowState = { - inflow: 0, - outflow: 0, - lastTimestamp: timestamp - }; - } - - tick() { - const snapshot = this._takeMeasurementSnapshot(); - - this._updatePredictedVolume(snapshot); - - const netFlow = this._selectBestNetFlow(snapshot); - const remaining = this._computeRemainingTime(netFlow); - - //check safety conditions - this._safetyController(snapshot,remaining.seconds,netFlow.direction); - if(this.safetyControllerActive) return; - - //if safety not active proceed with normal control - this._controlLogic(snapshot,netFlow.direction); - - this.state = { - direction: netFlow.direction, - netFlow: netFlow.value, - flowSource: netFlow.source, - seconds: remaining.seconds, - remainingSource: remaining.source - }; - - this.logger.debug(`netflow = ${JSON.stringify(netFlow)}`); - this.logger.debug(`Height : ${this.measurements.type('level').variant('predicted').position('atequipment').getCurrentValue('m') } m`); - } - - _registerMeasurementChild(child) { - const position = child.config.functionality.positionVsParent; - const measurementType = child.config.asset.type; - const eventName = `${measurementType}.measured.${position}`; - - child.measurements.emitter.on(eventName, (eventData) => { - this.logger.debug( - `Measurement update ${eventName} <- ${eventData.childName}: ${eventData.value} ${eventData.unit}` - ); - - this.measurements - .type(measurementType) - .variant('measured') - .position(position) - .value(eventData.value, eventData.timestamp, eventData.unit); - - this._handleMeasurement(measurementType, eventData.value, position, eventData); - }); - } - - //register machines or pumping stations that can provide predicted flow data - _registerPredictedFlowChild(child) { - const position = (child.config.functionality.positionVsParent || '').toLowerCase(); - const childName = child.config.general.name; - const childId = child.config.general.id ?? childName; - - let posKey; - let eventNames; - - switch (position) { - case 'downstream': - case 'out': - case 'atequipment': - posKey = 'out'; - eventNames = [ - 'flow.predicted.downstream', - 'flow.predicted.atequipment' - ]; - break; - - case 'upstream': - case 'in': - posKey = 'in'; - eventNames = [ - 'flow.predicted.upstream', - 'flow.predicted.atequipment' - ]; - break; - - default: - this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`); - return; - } - - if (!this.predictedFlowChildren.has(childId)) { - this.predictedFlowChildren.set(childId, { in: 0, out: 0 }); - this.logger.debug(`Initialized predicted flow tracking for child ${childName} (${childId})`); - } - - const handler = (eventData = {}) => { - const flowUnit = this.measurements.getUnit('flow'); - const unit = eventData.unit || child.config?.general?.unit || flowUnit; - const ts = eventData.timestamp || Date.now(); - const posKeyBase = posKey; // 'in' or 'out' - - this.measurements - .type('flow') - .variant('predicted') - .position(posKeyBase) - .child(childId) - .value(eventData.value, ts, unit); - - }; - - eventNames.forEach((eventName) => child.measurements.emitter.on(eventName, handler)); - } - - - setManualInflow(value, timestamp = Date.now(), unit) { - const num = Number(value); - - // Write manual inflow into the aggregated bucket - this.measurements - .type('flow') - .variant('predicted') - .position('in') - .child('manual-qin') - .value(num, timestamp, unit); - - } - - _handleMeasurement(measurementType, value, position, context) { - switch (measurementType) { - case 'level': - this._onLevelMeasurement(position, value, context); - break; - case 'pressure': - this._onPressureMeasurement(position, value, context); - break; - case 'flow': - // Additional flow-specific logic could go here if needed - break; - default: - this.logger.debug(`Unhandled measurement type "${measurementType}", storing only.`); - break; - } - } - - - _onLevelMeasurement(position, value, context = {}) { - this.measurements.type('level').variant('measured').position(position).value(value).unit(context.unit); - const levelSeries = this.measurements.type('level').variant('measured').position(position); - const levelMeters = levelSeries.getCurrentValue('m'); - - if (levelMeters == null) return; - - const volume = this._calcVolumeFromLevel(levelMeters); - const percent = this.interpolate.interpolate_lin_single_point( - volume, - this.basin.minVol, - this.basin.maxVolOverflow, - 0, - 100 - ); - - this.measurements - .type('volume') - .variant('measured') - .position('atequipment') - .value(volume, context.timestamp, 'm3'); - - this.measurements - .type('volumePercent') - .variant('measured') - .position('atequipment') - .value(percent, context.timestamp, '%'); - } - - _onPressureMeasurement(position, value, context = {}) { - let kelvinTemp = - this.measurements - .type('temperature') - .variant('measured') - .position('atequipment') - .getCurrentValue('K') ?? null; - - if (kelvinTemp === null) { - this.logger.warn('No temperature measurement; assuming 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'); - } - - if (kelvinTemp == null) return; - - const density = coolprop.PropsSI('D', 'T', kelvinTemp, 'P', 101325, 'Water'); - const pressurePa = this.measurements - .type('pressure') - .variant('measured') - .position(position) - .getCurrentValue('Pa'); - - if (!Number.isFinite(pressurePa) || !Number.isFinite(density)) return; - - const g = 9.80665; - const level = pressurePa / (density * g); - - this.measurements.type('level').variant('predicted').position(position).value(level, context.timestamp, 'm'); - } - - _takeMeasurementSnapshot() { - const snapshot = { - flows: {}, - levels: {}, - levelRates: {}, - vols:{}, - }; - - for (const variant of this.flowVariants) { - snapshot.flows[variant] = this._snapshotFlowsForVariant(variant); - } - - for (const variant of this.volVariants){ - snapshot.vols[variant] = this._snapshotVolsForVariant(variant); - } - - for (const variant of this.levelVariants) { - snapshot.levels[variant] = this._snapshotLevelForVariant(variant); - snapshot.levelRates[variant] = this._estimateLevelRate(snapshot.levels[variant]); - } - - return snapshot; - } - - _snapshotVolsForVariant(variant) { - const volumeSeries = this._locateSeries('volume', variant, ['atequipment']); - - return {variant,samples: this._seriesSamples(volumeSeries)}; - } - - _snapshotFlowsForVariant(variant) { - const inflowSeries = this._locateSeries('flow', variant, this.flowPositions.inflow); - const outflowSeries = this._locateSeries('flow', variant, this.flowPositions.outflow); - - return {variant, inflow: this._seriesSamples(inflowSeries), outflow: this._seriesSamples(outflowSeries) }; - } - - _snapshotLevelForVariant(variant) { - const levelSeries = this._locateSeries('level', variant, ['atequipment']); - return { - variant, - samples: this._seriesSamples(levelSeries) - }; - } - - _seriesSamples(seriesInfo) { - if (!seriesInfo) { - return { exists: false, measurement: null, current: null, previous: null }; - } - - try { - const current = seriesInfo.measurement.getLaggedSample(0); // newest - const previous = seriesInfo.measurement.getLaggedSample(1); // previous - return { - exists: Boolean(current), - measurement: seriesInfo.measurement, - current, - previous - }; - } catch (err) { - this.logger.debug( - `Failed to read samples for ${seriesInfo.type}.${seriesInfo.variant}.${seriesInfo.position}: ${err.message}` - ); - return { exists: false, measurement: seriesInfo.measurement, current: null, previous: null }; - } - } - - _locateSeries(type, variant, positions) { - for (const position of positions) { - try { - const chain = this.measurements.type(type).variant(variant).position(position); - if (!chain.exists({ requireValues: true })) continue; - - const measurement = chain.get(); - if (!measurement) continue; - - return { type, variant, position, measurement }; - } catch (err) { - // ignore missing combinations - } - } - return null; - } - - _estimateLevelRate(levelSnapshot) { - if (!levelSnapshot.samples.exists){ return null}; - const { current, previous } = levelSnapshot.samples; - if (!current || !previous || previous.timestamp == null){return null}; - - const deltaT = (current.timestamp - previous.timestamp) / 1000; - if (!Number.isFinite(deltaT) || deltaT <= 0){ return null}; - - const deltaLevel = current.value - previous.value; - return deltaLevel / deltaT; - } - - _selectBestNetFlow(snapshot) { - const type = 'flow'; - const unit = this.measurements.getUnit(type) || 'm3/s'; - - for (const variant of this.flowVariants) { - // Check if we have *any* flows for this variant at all - const bucket = this.measurements.measurements?.[type]?.[variant]; - if (!bucket || Object.keys(bucket).length === 0) { - this.logger.debug(`No ${type}.${variant} data; skipping this variant`); - continue; - } - - // Sum all inflow/outflow positions (in/upstream vs out/downstream) - const inflow = this.measurements.sum( - type, - variant, - this.flowPositions.inflow, - unit - ) || 0; - - const outflow = this.measurements.sum( - type, - variant, - this.flowPositions.outflow, - unit - ) || 0; - - // If absolutely nothing is flowing and bucket only just got created, - // we can choose to skip this variant and try the next one. - const absIn = Math.abs(inflow); - const absOut = Math.abs(outflow); - - if (absIn < this.flowThreshold && absOut < this.flowThreshold) { - this.logger.debug( - `Flows for ${type}.${variant} below threshold; inflow=${inflow}, outflow=${outflow}, trying next variant` + Object.values(this.stations).forEach((station) => station.handleInput('parent', 'execSequence', 'shutdown')); + Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines()); + this.logger.warn( + `Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down downstream equipment` ); - continue; + this.safetyControllerActive = true; } - - const net = inflow - outflow; // >0 = filling - - this.measurements - .type('netFlowRate') - .variant(variant) - .position('atequipment') - .value(net, Date.now(), unit); - - this.logger.debug( - `netFlow (${variant}): inflow=${inflow} ${unit}, outflow=${outflow} ${unit}, net=${net} ${unit}` - ); - - return { - value: net, - source: variant, - direction: this._deriveDirection(net) - }; } - // --- Fallback: level trend - for (const variant of this.levelVariants) { - const levelRate = snapshot.levelRates[variant]; - if (!Number.isFinite(levelRate)) continue; - - const netFlow = levelRate * this.basin.surfaceArea; - return { - value: netFlow, - source: `level:${variant}`, - direction: this._deriveDirection(netFlow) - }; + if (direction === 'filling') { + const timeTriggered = timeProtectionEnabled && remainingTime != null && remainingTime < timeleftToFullOrEmptyThresholdSeconds; + const overfillTriggered = overfillEnabled && vol > triggerHighVol; + if (timeTriggered || overfillTriggered) { + Object.values(this.machines).forEach((machine) => { + const pos = machine?.config?.functionality?.positionVsParent; + if (pos === 'upstream' && machine._isOperationalState()) { + machine.handleInput('parent', 'execSequence', 'shutdown'); + } + }); + Object.values(this.machineGroups).forEach((group) => group.turnOffAllMachines()); + Object.values(this.stations).forEach((station) => station.handleInput('parent', 'execSequence', 'shutdown')); + this.logger.warn( + `Safe guard triggered: vol=${vol.toFixed(2)} m3, remainingTime=${remainingTime ? remainingTime.toFixed(1) : 'N/A'} s; shutting down upstream equipment` + ); + this.safetyControllerActive = true; + } } - - this.logger.warn('No usable measurements to compute net flow; assuming steady.'); - return { value: 0, source: null, direction: 'steady' }; } - - _computeRemainingTime(netFlow) { - - if (!netFlow || Math.abs(netFlow.value) < this.flowThreshold) { - return { seconds: null, source: null }; - } - - const { heightOverflow, heightOutlet, surfaceArea } = this.basin; - if (!Number.isFinite(surfaceArea) || surfaceArea <= 0) { - this.logger.warn('Invalid basin surface area.'); - return { seconds: null, source: null }; - } - const type = 'level'; - const unit = this.measurements.getUnit(type); - for (const variant of this.levelVariants) { - - const lvl = this.measurements - .type(type) - .variant(variant) - .position('atequipment') - .getCurrentValue() - - if (!Number.isFinite(lvl)) continue; - - const remainingHeight = - netFlow.value > 0 - ? Math.max(heightOverflow - lvl, 0) - : Math.max(lvl - heightOutlet, 0); - - const seconds = (remainingHeight * surfaceArea) / Math.abs(netFlow.value); - if (!Number.isFinite(seconds)) continue; - - return { seconds, source: `${netFlow.source}/${variant}` }; - } - - this.logger.warn('No level data available to compute remaining time.'); - return { seconds: null, source: netFlow.source }; - } - - _updatePredictedVolume(snapshot) { - const predicted = snapshot.flows.predicted; - if (!predicted) return; - - const now = Date.now(); - const inflowSample = predicted.inflow.current ?? predicted.inflow.previous ?? null; - const outflowSample = predicted.outflow.current ?? predicted.outflow.previous ?? null; - - if (!this._predictedFlowState) { - this._predictedFlowState = { - inflow: inflowSample?.value ?? 0, - outflow: outflowSample?.value ?? 0, - lastTimestamp: inflowSample?.timestamp ?? outflowSample?.timestamp ?? now - }; - } - - if (inflowSample) this._predictedFlowState.inflow = inflowSample.value; - if (outflowSample) this._predictedFlowState.outflow = outflowSample.value; - - const latestObservedTimestamp = - inflowSample?.timestamp ?? outflowSample?.timestamp ?? this._predictedFlowState.lastTimestamp; - - const timestampPrev = this._predictedFlowState.lastTimestamp ?? latestObservedTimestamp; - - let timestampNow = latestObservedTimestamp; - if (!Number.isFinite(timestampNow) || timestampNow <= timestampPrev) { - timestampNow = now; - } - - let deltaSeconds = (timestampNow - timestampPrev) / 1000; - if (!Number.isFinite(deltaSeconds) || deltaSeconds <= 0) { - deltaSeconds = 0; - } - - let netVolumeChange = 0; - if (deltaSeconds > 0) { - const avgInflow = inflowSample ? inflowSample.value : this._predictedFlowState.inflow; - const avgOutflow = outflowSample ? outflowSample.value : this._predictedFlowState.outflow; - netVolumeChange = (avgInflow - avgOutflow) * deltaSeconds; - } - - const writeTimestamp = timestampPrev + Math.max(deltaSeconds, 0) * 1000; - - const volumeSeries = this.measurements.type('volume').variant('predicted').position('atEquipment'); - - const currentVolume = volumeSeries.getCurrentValue('m3') ?? this.basin.minVol; - - const nextVolume = currentVolume + netVolumeChange; - - volumeSeries.value(nextVolume, writeTimestamp, 'm3').unit('m3'); - - const nextLevel = this._calcLevelFromVolume(nextVolume); - this.measurements - .type('level') - .variant('predicted') - .position('atEquipment') - .value(nextLevel, writeTimestamp, 'm') - .unit('m'); - - //calc how full this is in procen using minVol vs maxVolOverflow - const percent = this.interpolate.interpolate_lin_single_point( - currentVolume, - this.basin.minVol, - this.basin.maxVolOverflow, - 0, - 100 - ); - - //store this percent value - this.measurements - .type('volumePercent') - .variant('predicted') - .position('atequipment') - .value(percent); - - this._predictedFlowState.lastTimestamp = writeTimestamp; - } - - _averageSampleValues(sampleA, sampleB) { - const values = [sampleA?.value, sampleB?.value].filter((v) => Number.isFinite(v)); - if (!values.length) return 0; - return values.reduce((acc, val) => acc + val, 0) / values.length; - } - - _deriveDirection(netFlow) { - if (netFlow > this.flowThreshold) return 'filling'; - if (netFlow < -this.flowThreshold) return 'draining'; - return 'steady'; - } - - - /* ------------------------------------------------------------------ */ - /* Basin Calculations */ - /* ------------------------------------------------------------------ */ + /* --------------------------- Basin --------------------------- */ initBasinProperties() { - - //is min height based on inlet or outlet elevation? const minHeightBasedOn = this.config.hydraulics.minHeightBasedOn; + 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; - const volEmptyBasin = this.config.basin.volume; //volume when basin is empty - const heightBasin = this.config.basin.height; //total height of basin - const heightInlet = this.config.basin.heightInlet; //height at which inlet is located - const heightOutlet = this.config.basin.heightOutlet; //height at which outlet is located - const heightOverflow = this.config.basin.heightOverflow; //height at which overflow occurs - - const surfaceArea = volEmptyBasin / heightBasin; //assume uniform cross section for now - const maxVol = heightBasin * surfaceArea; //maximum volume when basin is full - const maxVolOverflow = heightOverflow * surfaceArea; //maximum volume before overflow occurs - const minVolOut = heightOutlet * surfaceArea; //minimum volume to have outlet just above basin bottom - const minVolIn = heightInlet * surfaceArea; //minimum volume to have inlet just above waterline - const minVol = (minHeightBasedOn === "inlet") ? minVolIn : minVolOut; - this.logger.debug(`Basin min volume based on ${minHeightBasedOn} : ${minVol.toFixed(2)} m3`); + const surfaceArea = volEmptyBasin / heightBasin; + const maxVol = heightBasin * surfaceArea; + const maxVolOverflow = heightOverflow * surfaceArea; + const minVolOut = heightOutlet * surfaceArea; + const minVolIn = heightInlet * surfaceArea; + const minVol = minHeightBasedOn === 'inlet' ? minVolIn : minVolOut; this.basin = { volEmptyBasin, @@ -950,11 +580,7 @@ class PumpingStation { minHeightBasedOn }; - this.measurements.type('volume').variant('predicted').position('atEquipment').value(minVol).unit('m3'); - - this.logger.debug( - `Basin initialized | area=${surfaceArea.toFixed(2)} m2, max=${maxVol.toFixed(2)} m3, overflow=${maxVolOverflow.toFixed(2)} m3` - ); + this.measurements.type('volume').variant('predicted').position('atequipment').value(minVol).unit('m3'); } _calcVolumeFromLevel(level) { @@ -965,14 +591,10 @@ class PumpingStation { return Math.max(volume, 0) / this.basin.surfaceArea; } - /* ------------------------------------------------------------------ */ - /* Output */ - /* ------------------------------------------------------------------ */ + /* --------------------------- Output --------------------------- */ getOutput() { - const output = this.measurements.getFlattenedOutput(); - output.direction = this.state.direction; output.flowSource = this.state.flowSource; output.timeleft = this.state.seconds; @@ -990,11 +612,10 @@ class PumpingStation { } module.exports = PumpingStation; - /* ------------------------------------------------------------------------- */ /* Example usage */ /* ------------------------------------------------------------------------- */ - +/* if (require.main === module) { const Measurement = require('../../measurement/src/specificClass'); const RotatingMachine = require('../../rotatingMachine/src/specificClass'); @@ -1021,6 +642,10 @@ if (require.main === module) { hydraulics: { refHeight: 'NAP', basinBottomRef: 0 + }, + safety: { + enableDryRunProtection:false, + enableOverfillProtection:false } }; } @@ -1120,22 +745,22 @@ if (require.main === module) { (async function demo() { const station = new PumpingStation(createPumpingStationConfig('PumpingStationDemo')); const pump1 = new RotatingMachine(createMachineConfig('Pump1','downstream'), createMachineStateConfig()); - const pump2 = new RotatingMachine(createMachineConfig('Pump2','upstream'), createMachineStateConfig()); + //const pump2 = new RotatingMachine(createMachineConfig('Pump2','upstream'), createMachineStateConfig()); - const levelSensor = new Measurement(createLevelMeasurementConfig('WetWellLevel')); - const inflowSensor = new Measurement(createFlowMeasurementConfig('InfluentFlow', 'in')); - const outflowSensor = new Measurement(createFlowMeasurementConfig('PumpDischargeFlow', 'out')); + //const levelSensor = new Measurement(createLevelMeasurementConfig('WetWellLevel')); + //const inflowSensor = new Measurement(createFlowMeasurementConfig('InfluentFlow', 'in')); + //const outflowSensor = new Measurement(createFlowMeasurementConfig('PumpDischargeFlow', 'out')); - station.childRegistrationUtils.registerChild(levelSensor, levelSensor.config.functionality.softwareType); + //station.childRegistrationUtils.registerChild(levelSensor, levelSensor.config.functionality.softwareType); //station.childRegistrationUtils.registerChild(inflowSensor, inflowSensor.config.functionality.softwareType); //station.childRegistrationUtils.registerChild(outflowSensor, outflowSensor.config.functionality.softwareType); station.childRegistrationUtils.registerChild(pump1, 'machine'); - station.childRegistrationUtils.registerChild(pump2, 'machine'); + //station.childRegistrationUtils.registerChild(pump2, 'machine'); // Seed initial measurements - seedSample(levelSensor, 'level', 1.8, 'm'); + //seedSample(levelSensor, 'level', 1.8, 'm'); //seedSample(inflowSensor, 'flow', 0.35, 'm3/s'); //seedSample(outflowSensor, 'flow', 0.20, 'm3/s'); @@ -1147,14 +772,13 @@ if (require.main === module) { await new Promise((resolve) => setTimeout(resolve, 10)); console.log('Initial state:', station.state); - station.setManualInflow(10,Date.now(),'l/s'); - - await pump1.handleInput('parent', 'execSequence', 'startup'); - await pump1.handleInput('parent', 'execMovement', 10); - - await pump2.handleInput('parent', 'execSequence', 'startup'); - await pump2.handleInput('parent', 'execMovement', 10); + station.setManualInflow(300,Date.now(),'l/s'); + //await pump1.handleInput('parent', 'execSequence', 'startup'); + //await pump1.handleInput('parent', 'execMovement', 10); +// + //await pump2.handleInput('parent', 'execSequence', 'startup'); + //await pump2.handleInput('parent', 'execMovement', 10); console.log('Station state:', station.state); console.log('Station output:', station.getOutput()); @@ -1162,4 +786,4 @@ if (require.main === module) { console.error('Demo failed:', err); }); } - //*/ + //*/ \ No newline at end of file