forked from RnD/pumpingStation
Further bug fixes and optimized level control for groups and machines alike
This commit is contained in:
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user