diff --git a/src/specificClass.js b/src/specificClass.js index 4fb2ffe..b179014 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -64,25 +64,27 @@ class PumpingStation { } // for pumping stations register them for control - if(softwareType === 'pumpingStation'){ + 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'){ + if(softwareType === 'machinegroup'){ 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}"`); + 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 === 'machineGroupController') { + 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}`); + //this.logger.warn(`Unsupported child software type: ${softwareType}`); } _safetyController(snapshot,remainingTime,direction){ @@ -117,21 +119,49 @@ class PumpingStation { 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}"`); + 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.handleInput('parent', 'execSequence', 'shutdown'); + 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'}` + ); + if( (remainingTime < timeThreshhold && remainingTime !== null) || vol > triggerHighVol || vol == null){ + //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()) { + 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){ @@ -146,7 +176,10 @@ class PumpingStation { } + /* old levelcontrol async _controlLevelBased(snapshot, remainingTime) { + + // current volume as a percentage of usable capacity const vol = this._resolveVolume(snapshot); if (vol == null) { @@ -168,6 +201,7 @@ class PumpingStation { this.logger.debug(`Level-based control: waiting ${timeThresholdSeconds}s before acting`); return; } + this.logger.debug(`Level-based control: dwelling for another ${Math.round((this._levelState.dwellUntil - now) / 1000)} seconds`); if (now < this._levelState.dwellUntil) return; // still waiting @@ -183,6 +217,98 @@ class PumpingStation { await nextMachine.handleInput('parent', 'execSequence', 'startup'); } } +*/ + async _controlLevelBased(snapshot, remainingTime) { + + + // current volume as a percentage of usable capacity + const vol = this._resolveVolume(snapshot); + if (vol == null) { + this.logger.warn('No valid volume found for level-based control'); + return; + } + + const { thresholds, timeThresholdSeconds } = this.config.control.levelbased; + const percentFull = (vol / this.basin.maxVolOverflow) * 100; + + // pick thresholds that are now crossed but were not crossed before + const newlyCrossed = thresholds.filter(t => percentFull >= t && !this._levelState.crossed.has(t)); + this.logger.debug(`Level-based control: vol=${vol.toFixed(2)} m³ (${percentFull.toFixed(1)}%), newly crossed thresholds: [${newlyCrossed.join(', ')}]`); + if (!newlyCrossed.length) return; + + const now = Date.now(); + if (!this._levelState.dwellUntil) { + this._levelState.dwellUntil = now + timeThresholdSeconds * 1000; + this.logger.debug(`Level-based control: waiting ${timeThresholdSeconds}s before acting`); + return; + } + + this.logger.debug(`Level-based control: dwelling for another ${Math.round((this._levelState.dwellUntil - now) / 1000)} seconds`); + if (now < this._levelState.dwellUntil) return; // still waiting + + this._levelState.dwellUntil = null; // dwell satisfied, let pumps start + + this._levelState.dwellUntil = null; + + newlyCrossed.forEach((threshold) => this._levelState.crossed.add(threshold)); + + const percentControl = this._calculateLevelControlPercent(thresholds); + if (percentControl <= 0) { + this.logger.debug('Level-based control: percent control resolved to 0%, skipping commands'); + return; + } + + this.logger.info( + `level-based control: thresholds [${newlyCrossed.join(', ')}]% reached, requesting ${percentControl.toFixed(1)}% control (vol=${vol.toFixed(2)} m³)` + ); + + await this._applyMachineGroupLevelControl(percentControl); + await this._applyIdleMachineLevelControl(percentControl); + } + + 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}`); + } + } + } + + _calculateLevelControlPercent(thresholds = []) { + if (!thresholds.length) return 0; + const total = thresholds.length; + const crossed = this._levelState.crossed.size; + const pct = (crossed / total) * 100; + return Math.min(100, Math.max(0, pct)); + } + _resolveVolume(snapshot) { for (const variant of this.volVariants) { @@ -281,7 +407,6 @@ class PumpingStation { 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; @@ -304,24 +429,41 @@ class PumpingStation { //register machines or pumping stations that can provide predicted flow data _registerPredictedFlowChild(child) { - const position = child.config.functionality.positionVsParent; + const position = (child.config.functionality.positionVsParent || '').toLowerCase(); const childName = child.config.general.name; const childId = child.config.general.id ?? childName; - const posKey = - position === 'downstream' || position === 'out' || position === 'atequipment' - ? 'out' - : position === 'upstream' || position === 'in' - ? 'in' - : null; + let posKey; + let eventNames; - if (!posKey) { - this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`); - return; + 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 = {}) => { @@ -335,14 +477,7 @@ class PumpingStation { this._refreshAggregatedPredictedFlow(posKey, timestamp, unit); }; - const eventNames = - posKey === 'in' - ? ['flow.predicted.downstream', 'flow.predicted.upstream'] - : ['flow.predicted.downstream']; - - for (const eventName of eventNames) { - child.measurements.emitter.on(eventName, handler); - } + eventNames.forEach((eventName) => child.measurements.emitter.on(eventName, handler)); } _refreshAggregatedPredictedFlow(direction, timestamp = Date.now(), unit = 'm3/s') { @@ -725,7 +860,7 @@ class PumpingStation { minVolOut }; - this.measurements.type('volume').variant('predicted').position('atEquipment').value(maxVolOverflow).unit('m3'); + this.measurements.type('volume').variant('predicted').position('atEquipment').value(heightOutlet).unit('m3'); this.logger.debug( `Basin initialized | area=${surfaceArea.toFixed(2)} m2, max=${maxVol.toFixed(2)} m3, overflow=${maxVolOverflow.toFixed(2)} m3`