From 43eb97407f2613bdded42fa1e9ba321e1246911d Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:07:56 +0100 Subject: [PATCH] added safeguarding when vol gets too low for machines, --- src/nodeClass.js | 8 +++ src/specificClass.js | 126 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 117 insertions(+), 17 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index e0a4b2a..3d1c376 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -204,6 +204,14 @@ class nodeClass { const childObj = this.RED.nodes.getNode(childId); this.source.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent); break; + case 'calibratePredictedVolume': + const calibratedVolume = this.source.measurements + .type('volume') + .variant('measured') + .position('atequipment') + .getCurrentValue('m3'); + this.source.calibratePredictedVolume(calibratedVolume); + break; } done(); }); diff --git a/src/specificClass.js b/src/specificClass.js index d2a79bb..ceb8628 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -16,9 +16,9 @@ class PumpingStation { this.measurements.setPreferredUnit('level', 'm'); this.measurements.setPreferredUnit('volume', 'm3'); this.childRegistrationUtils = new childRegistrationUtils(this); - this.machines = {}; - this.stations = {}; - + this.machines = {}; + this.stations = {}; + this.machineGroups = {}; //variants in determining what gets priority this.flowVariants = ['measured', 'predicted']; @@ -51,15 +51,36 @@ 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 === 'machineGroupController'){ + const childId = child.config.general.id; + this.machineGroups[childId] = child; + this.logger.debug(`Registered machine group controller child "${child.config.general.name}" with id "${childId}"`); + } + + //for all childs that can provide predicted flow data if (softwareType === 'machine' || softwareType === 'pumpingStation' || softwareType === 'machineGroupController') { this._registerPredictedFlowChild(child); - return; } this.logger.warn(`Unsupported child software type: ${softwareType}`); } - _safeGuardSystem(snapshot,remainingTime){ + _safeGuardSystem(snapshot,remainingTime,direction){ let vol = null; for (const variant of this.volVariants){ @@ -67,17 +88,89 @@ class PumpingStation { //go through with variants until we find one that exists if (!volsnap.samples.exists){ continue}; - const vol = volsnap.samples.current?.value ?? null; + vol = volsnap.samples.current?.value ?? null; } if(vol == null){ - //if we cant get a volume, we must force whole system off. - - }; -/* - if(remainingTime < timeThreshhold || vol > maxVolume || vol < minVolume){} - */ + //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'); + }); + this.logger.warn('No volume data available to safe guard system; shutting down all machines.'); + return; + } + //get threshholds from config + const timeThreshhold = this.config.control.timeThreshholdSeconds; //seconds + const triggerHighVol = this.basin.maxVolOverflow * ( this.config.control.thresholdHighVolume/100 ); + const triggerLowVol = this.basin.minVolOut * ( this.config.control.thresholdLowVolume/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( (remainingTime < timeThreshhold && remainingTime !== null) || vol < triggerLowVol || vol == null){ + //shut down all downstream or atequipment machines,pumping stations and machine groups + Object.entries(this.machines).forEach(([machineId, machine]) => { + 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.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 group "${groupId}"`); + }); + } + } + + // trigger conditions for filling + if(direction == "filling"){ + + } + } + + //calibrate the predicted volume to a known value + calibratePredictedVolume(calibratedVol, timestamp = Date.now()){ + + const volumeChain = this.measurements + .type('volume') + .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 = []; + } + + volumeChain.value(calibratedVol, timestamp, 'm3').unit('m3'); + + const levelChain = this.measurements + .type('level') + .variant('predicted') + .position('atequipment'); + + const levelMeasurement = levelChain.exists() ? levelChain.get() : null; + if (levelMeasurement) { + levelMeasurement.values = []; + levelMeasurement.timestamps = []; + } + + levelChain.value(this._calcLevelFromVolume(calibratedVol), timestamp, 'm'); + + this._predictedFlowState = { + inflow: 0, + outflow: 0, + lastTimestamp: timestamp + }; } tick() { @@ -89,7 +182,7 @@ class PumpingStation { const netFlow = this._selectBestNetFlow(snapshot); const remaining = this._computeRemainingTime(snapshot, netFlow); - this._safeGuardSystem(snapshot,remaining.seconds); + this._safeGuardSystem(snapshot,remaining.seconds,netFlow.direction); this.state = { direction: netFlow.direction, @@ -151,9 +244,7 @@ class PumpingStation { const timestamp = eventData.timestamp ?? Date.now(); const unit = eventData.unit ?? 'm3/s'; - this.logger.debug( - `Predicted flow update from ${childName} (${childId}, ${posKey}) -> ${value} ${unit}` - ); + this.logger.debug(`Predicted flow update from ${childName} (${childId}, ${posKey}) -> ${value} ${unit}`); this.predictedFlowChildren.get(childId)[posKey] = value; this._refreshAggregatedPredictedFlow(posKey, timestamp, unit); @@ -201,6 +292,7 @@ class PumpingStation { _onLevelMeasurement(position, value, context = {}) { const levelSeries = this.measurements.type('level').variant('measured').position(position); const levelMeters = levelSeries.getCurrentValue('m'); + if (levelMeters == null) return; const volume = this._calcVolumeFromLevel(levelMeters); @@ -598,7 +690,7 @@ module.exports = PumpingStation; /* ------------------------------------------------------------------------- */ /* Example usage */ /* ------------------------------------------------------------------------- */ -/* + if (require.main === module) { const Measurement = require('../../measurement/src/specificClass'); const RotatingMachine = require('../../rotatingMachine/src/specificClass');