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
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]) => {
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) {
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`