Compare commits

..

1 Commits

Author SHA1 Message Date
znetsixe
0a6c7ee2e1 Further bug fixes and optimized level control for groups and machines alike 2025-11-13 19:37:41 +01:00

View File

@@ -64,25 +64,27 @@ class PumpingStation {
} }
// for pumping stations register them for control // for pumping stations register them for control
if(softwareType === 'pumpingStation'){ if(softwareType === 'pumpingstation'){
const childId = child.config.general.id; const childId = child.config.general.id;
this.stations[childId] = child; this.stations[childId] = child;
this.logger.debug(`Registered pumping station child "${child.config.general.name}" with id "${childId}"`); this.logger.debug(`Registered pumping station child "${child.config.general.name}" with id "${childId}"`);
} }
// for machine group controllers register them for control // for machine group controllers register them for control
if(softwareType === 'machineGroupController'){ if(softwareType === 'machinegroup'){
const childId = child.config.general.id; const childId = child.config.general.id;
this.machineGroups[childId] = child; 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 //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._registerPredictedFlowChild(child);
} }
this.logger.warn(`Unsupported child software type: ${softwareType}`); //this.logger.warn(`Unsupported child software type: ${softwareType}`);
} }
_safetyController(snapshot,remainingTime,direction){ _safetyController(snapshot,remainingTime,direction){
@@ -117,21 +119,49 @@ class PumpingStation {
if( (remainingTime < timeThreshhold && remainingTime !== null) || vol < triggerLowVol || vol == null){ if( (remainingTime < timeThreshhold && remainingTime !== null) || vol < triggerLowVol || vol == null){
//shut down all downstream or atequipment machines,pumping stations and machine groups //shut down all downstream or atequipment machines,pumping stations and machine groups
Object.entries(this.machines).forEach(([machineId, machine]) => { 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'); 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}"`); 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]) => { Object.entries(this.stations).forEach(([stationId, station]) => {
station.handleInput('parent', 'execSequence', 'shutdown'); 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}"`); 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]) => { 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.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; 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){ changeMode(newMode){
@@ -146,7 +176,10 @@ class PumpingStation {
} }
/* old levelcontrol
async _controlLevelBased(snapshot, remainingTime) { async _controlLevelBased(snapshot, remainingTime) {
// current volume as a percentage of usable capacity // current volume as a percentage of usable capacity
const vol = this._resolveVolume(snapshot); const vol = this._resolveVolume(snapshot);
if (vol == null) { if (vol == null) {
@@ -168,6 +201,7 @@ class PumpingStation {
this.logger.debug(`Level-based control: waiting ${timeThresholdSeconds}s before acting`); this.logger.debug(`Level-based control: waiting ${timeThresholdSeconds}s before acting`);
return; return;
} }
this.logger.debug(`Level-based control: dwelling for another ${Math.round((this._levelState.dwellUntil - now) / 1000)} seconds`); 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 if (now < this._levelState.dwellUntil) return; // still waiting
@@ -183,6 +217,98 @@ class PumpingStation {
await nextMachine.handleInput('parent', 'execSequence', 'startup'); 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) { _resolveVolume(snapshot) {
for (const variant of this.volVariants) { 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`); this.logger.debug(`Height : ${this.measurements.type('level').variant('predicted').position('atequipment').getCurrentValue('m') } m`);
} }
_registerMeasurementChild(child) { _registerMeasurementChild(child) {
const position = child.config.functionality.positionVsParent; const position = child.config.functionality.positionVsParent;
const measurementType = child.config.asset.type; const measurementType = child.config.asset.type;
@@ -304,24 +429,41 @@ class PumpingStation {
//register machines or pumping stations that can provide predicted flow data //register machines or pumping stations that can provide predicted flow data
_registerPredictedFlowChild(child) { _registerPredictedFlowChild(child) {
const position = child.config.functionality.positionVsParent; const position = (child.config.functionality.positionVsParent || '').toLowerCase();
const childName = child.config.general.name; const childName = child.config.general.name;
const childId = child.config.general.id ?? childName; const childId = child.config.general.id ?? childName;
const posKey = let posKey;
position === 'downstream' || position === 'out' || position === 'atequipment' let eventNames;
? 'out'
: position === 'upstream' || position === 'in'
? 'in'
: null;
if (!posKey) { 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}`); this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`);
return; return;
} }
if (!this.predictedFlowChildren.has(childId)) { if (!this.predictedFlowChildren.has(childId)) {
this.predictedFlowChildren.set(childId, { in: 0, out: 0 }); this.predictedFlowChildren.set(childId, { in: 0, out: 0 });
this.logger.debug(`Initialized predicted flow tracking for child ${childName} (${childId})`);
} }
const handler = (eventData = {}) => { const handler = (eventData = {}) => {
@@ -335,14 +477,7 @@ class PumpingStation {
this._refreshAggregatedPredictedFlow(posKey, timestamp, unit); this._refreshAggregatedPredictedFlow(posKey, timestamp, unit);
}; };
const eventNames = eventNames.forEach((eventName) => child.measurements.emitter.on(eventName, handler));
posKey === 'in'
? ['flow.predicted.downstream', 'flow.predicted.upstream']
: ['flow.predicted.downstream'];
for (const eventName of eventNames) {
child.measurements.emitter.on(eventName, handler);
}
} }
_refreshAggregatedPredictedFlow(direction, timestamp = Date.now(), unit = 'm3/s') { _refreshAggregatedPredictedFlow(direction, timestamp = Date.now(), unit = 'm3/s') {
@@ -725,7 +860,7 @@ class PumpingStation {
minVolOut 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( this.logger.debug(
`Basin initialized | area=${surfaceArea.toFixed(2)} m2, max=${maxVol.toFixed(2)} m3, overflow=${maxVolOverflow.toFixed(2)} m3` `Basin initialized | area=${surfaceArea.toFixed(2)} m2, max=${maxVol.toFixed(2)} m3, overflow=${maxVolOverflow.toFixed(2)} m3`