updating to corrospend with reality
This commit is contained in:
@@ -66,6 +66,15 @@ class nodeClass {
|
||||
minHeightBasedOn: uiConfig.minHeightBasedOn,
|
||||
basinBottomRef: uiConfig.basinBottomRef,
|
||||
},
|
||||
control:{
|
||||
mode: uiConfig.controlMode,
|
||||
levelbased:{
|
||||
startLevel:uiConfig.startLevel,
|
||||
stopLevel:uiConfig.stopLevel,
|
||||
minFlowLevel:uiConfig.minFlowLevel,
|
||||
maxFlowLevel:uiConfig.maxFlowLevel
|
||||
}
|
||||
},
|
||||
safety:{
|
||||
enableDryRunProtection: uiConfig.enableDryRunProtection,
|
||||
dryRunThresholdPercent: uiConfig.dryRunThresholdPercent,
|
||||
@@ -216,6 +225,16 @@ class nodeClass {
|
||||
.getCurrentValue('m3');
|
||||
this.source.calibratePredictedVolume(calibratedVolume);
|
||||
break;
|
||||
case 'q_in': {
|
||||
// payload can be number or { value, unit, timestamp }
|
||||
const val = Number(msg.payload);
|
||||
const unit = msg?.unit || 'm3/s';
|
||||
const ts = msg?.timestamp || Date.now();
|
||||
this.source.setManualInflow(val, ts, unit);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const EventEmitter = require('events');
|
||||
const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation} = require('generalFunctions');
|
||||
const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation,convert} = require('generalFunctions');
|
||||
|
||||
class PumpingStation {
|
||||
constructor(config = {}) {
|
||||
@@ -12,10 +12,12 @@ class PumpingStation {
|
||||
this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel,this.config.general.name);
|
||||
|
||||
this.measurements = new MeasurementContainer({ autoConvert: true });
|
||||
this.measurements.setPreferredUnit('flow', 'm3/s');
|
||||
this.preferredUnits = {flow: "m3/s"};
|
||||
this.measurements.setPreferredUnit('flow', this.preferredUnits.flow);
|
||||
this.measurements.setPreferredUnit('netFlowRate', 'm3/s');
|
||||
this.measurements.setPreferredUnit('level', 'm');
|
||||
this.measurements.setPreferredUnit('volume', 'm3');
|
||||
|
||||
this.childRegistrationUtils = new childRegistrationUtils(this);
|
||||
this.machines = {};
|
||||
this.stations = {};
|
||||
@@ -192,52 +194,69 @@ class PumpingStation {
|
||||
|
||||
}
|
||||
|
||||
async _controlLevelBased(snapshot, remainingTime) {
|
||||
_scaleLevelToFlowPercent(level,minflow,maxflow) {
|
||||
const { minFlowLevel, maxFlowLevel } = this.config.control.levelbased;
|
||||
const output = this.interpolate_lin_single_point(level,minFlowLevel,maxFlowLevel,minflow,maxflow);
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
// 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');
|
||||
async _controlLevelBased(snapshot) {
|
||||
const {startLevel, stopLevel} = this.config.control.levelbased;
|
||||
|
||||
//pick level prefering measured then predicted
|
||||
const level = (snapshot) => {
|
||||
for (const variant of this.levelVariants) {
|
||||
const levelSnap = snapshot.levels?.[variant];
|
||||
|
||||
if (levelSnap?.samples?.current?.value !== undefined) {
|
||||
return levelSnap.samples.current.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if (level == null) {
|
||||
this.logger.warn('No valid level found');
|
||||
return;
|
||||
}
|
||||
|
||||
const { thresholds, timeThresholdSeconds } = this.config.control.levelbased;
|
||||
const percentFull = (vol / this.basin.maxVolOverflow) * 100;
|
||||
if(level > startLevel){
|
||||
let maxFlow = 0;
|
||||
let minTotalFlow = Infinity;
|
||||
Object.entries(this.machines).forEach(([machineId, machine]) => {
|
||||
sumFlow =+ machine.measurements.type('flow').variant('predicted').variant('max').getCurrentValue(this.preferredUnits.flow);
|
||||
const minflow = machine.measurements.type('flow').variant('predicted').variant('min').getCurrentValue(this.preferredUnits.flow);
|
||||
if(minTotalFlow < minflow){ minflow = minTotalFlow};
|
||||
|
||||
});
|
||||
|
||||
// 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;
|
||||
Object.entries(this.machineGroups).forEach(([groupId, group]) => {
|
||||
sumFlow = group.dynamicTotals.flow.max;
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
|
||||
//start machines and or give group % control
|
||||
Object.entries(this.machines).forEach(([machineId, machine]) => {
|
||||
const position = machine?.config?.functionality?.positionVsParent;
|
||||
if ((position === 'downstream' || position === 'atEquipment') && machine._isOperationalState()) {
|
||||
machine.handleInput('parent', 'execSequence', 'startup');
|
||||
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.machineGroups).forEach(([groupId, group]) => {
|
||||
group.handleInput(Qd);
|
||||
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.debug(`Level-based control: dwelling for another ${Math.round((this._levelState.dwellUntil - now) / 1000)} seconds`);
|
||||
if (now < this._levelState.dwellUntil) return; // still waiting
|
||||
const percControl = this._scaleLevelToFlowPercent(level);
|
||||
|
||||
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);
|
||||
await this._applyMachineGroupLevelControl(percControl);
|
||||
await this._applyIdleMachineLevelControl(percControl);
|
||||
}
|
||||
|
||||
async _applyMachineGroupLevelControl(percentControl) {
|
||||
@@ -275,14 +294,6 @@ class PumpingStation {
|
||||
}
|
||||
}
|
||||
|
||||
_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) {
|
||||
@@ -300,13 +311,13 @@ class PumpingStation {
|
||||
}
|
||||
|
||||
//control logic
|
||||
_controlLogic(snapshot, remainingTime){
|
||||
_controlLogic(snapshot){
|
||||
const mode = this.mode;
|
||||
|
||||
switch(mode){
|
||||
case "levelbased":
|
||||
this.logger.debug(`Executing level-based control logic`);
|
||||
this._controlLevelBased(snapshot, remainingTime);
|
||||
this._controlLevelBased(snapshot);
|
||||
break;
|
||||
case "flowbased":
|
||||
this._controlFlowBased();
|
||||
@@ -332,6 +343,11 @@ class PumpingStation {
|
||||
.variant('predicted')
|
||||
.position('atequipment');
|
||||
|
||||
const levelChain = this.measurements
|
||||
.type('level')
|
||||
.variant('predicted')
|
||||
.position('atequipment');
|
||||
|
||||
//if we have existing values clear them out
|
||||
const volumeMeasurement = volumeChain.exists() ? volumeChain.get() : null;
|
||||
if (volumeMeasurement) {
|
||||
@@ -339,20 +355,51 @@ class PumpingStation {
|
||||
volumeMeasurement.timestamps = [];
|
||||
}
|
||||
|
||||
const levelMeasurement = levelChain.exists() ? levelChain.get() : null;
|
||||
if (levelMeasurement) {
|
||||
levelMeasurement.values = [];
|
||||
levelMeasurement.timestamps = [];
|
||||
}
|
||||
|
||||
volumeChain.value(calibratedVol, timestamp, 'm3').unit('m3');
|
||||
|
||||
levelChain.value(this._calcLevelFromVolume(calibratedVol), timestamp, 'm');
|
||||
|
||||
this._predictedFlowState = {
|
||||
inflow: 0,
|
||||
outflow: 0,
|
||||
lastTimestamp: timestamp
|
||||
};
|
||||
}
|
||||
|
||||
calibratePredictedLevel(val,timestamp = Date.now(),unit = 'm'){
|
||||
|
||||
const volumeChain = this.measurements
|
||||
.type('volume')
|
||||
.variant('predicted')
|
||||
.position('atequipment');
|
||||
|
||||
const levelChain = this.measurements
|
||||
.type('level')
|
||||
.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 = [];
|
||||
}
|
||||
|
||||
const levelMeasurement = levelChain.exists() ? levelChain.get() : null;
|
||||
if (levelMeasurement) {
|
||||
levelMeasurement.values = [];
|
||||
levelMeasurement.timestamps = [];
|
||||
}
|
||||
|
||||
levelChain.value(this._calcLevelFromVolume(calibratedVol), timestamp, 'm');
|
||||
levelChain.value(val, timestamp).unit(unit);
|
||||
|
||||
volumeChain.value(this._calcVolumeFromLevel(val), timestamp, 'm3');
|
||||
|
||||
this._predictedFlowState = {
|
||||
inflow: 0,
|
||||
@@ -448,29 +495,58 @@ class PumpingStation {
|
||||
}
|
||||
|
||||
const handler = (eventData = {}) => {
|
||||
const value = Number.isFinite(eventData.value) ? eventData.value : 0;
|
||||
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.predictedFlowChildren.get(childId)[posKey] = value;
|
||||
this._refreshAggregatedPredictedFlow(posKey, timestamp, unit);
|
||||
const preferred = this.preferredUnits?.flow || 'm3/s';
|
||||
const unit = eventData.unit || 'l/s'; // don’t assume m3/s
|
||||
const raw = Number.isFinite(eventData.value) ? eventData.value : 0;
|
||||
const ts = eventData.timestamp || Date.now();
|
||||
const normalized = convert(raw).from(unit).to(preferred);
|
||||
this.predictedFlowChildren.get(childId)[posKey] = normalized;
|
||||
this._refreshAggregatedPredictedFlow(posKey, ts, preferred);
|
||||
};
|
||||
|
||||
eventNames.forEach((eventName) => child.measurements.emitter.on(eventName, handler));
|
||||
}
|
||||
|
||||
_refreshAggregatedPredictedFlow(direction, timestamp = Date.now(), unit = 'm3/s') {
|
||||
_refreshAggregatedPredictedFlow(direction) {
|
||||
const preferredUnit = this.preferredUnits.flow;
|
||||
|
||||
const sum = Array.from(this.predictedFlowChildren.values())
|
||||
.map((entry) => (Number.isFinite(entry[direction]) ? entry[direction] : 0))
|
||||
.map((entry) => {
|
||||
const v = entry[direction];
|
||||
const num = Number(v?.value ?? v);
|
||||
if (!Number.isFinite(num)) return 0;
|
||||
const unit = v?.unit || preferredUnit; // default if none stored
|
||||
return convert(num).from(unit).to(preferredUnit);
|
||||
})
|
||||
.reduce((acc, val) => acc + val, 0);
|
||||
|
||||
this.measurements
|
||||
.type('flow')
|
||||
.variant('predicted')
|
||||
.position(direction)
|
||||
.value(sum, timestamp, unit);
|
||||
.value(sum, Date.now(), preferredUnit);
|
||||
}
|
||||
|
||||
setManualInflow(value, timestamp = Date.now(), unit) {
|
||||
const num = Number(value);
|
||||
const preferredUnit = this.preferredUnits.flow;
|
||||
|
||||
// Store the manual inflow in the measurement container with its source unit.
|
||||
this.measurements.type('flow').variant('manual').position('in').value(num, timestamp, unit);
|
||||
|
||||
// Read back in preferred units so the aggregated predicted flow uses consistent units.
|
||||
const entry = this.measurements
|
||||
.type('flow')
|
||||
.variant('manual')
|
||||
.position('in')
|
||||
.getCurrentValue(preferredUnit);
|
||||
|
||||
const predFlow = this.predictedFlowChildren.get('manual-qin') || { in: 0, out: 0 };
|
||||
predFlow.in = Number.isFinite(entry) ? entry : 0;
|
||||
this.predictedFlowChildren.set('manual-qin', predFlow);
|
||||
|
||||
// Pass preferred unit so we don't double-convert when writing the aggregate series.
|
||||
this._refreshAggregatedPredictedFlow('in', timestamp, preferredUnit);
|
||||
}
|
||||
|
||||
_handleMeasurement(measurementType, value, position, context) {
|
||||
|
||||
Reference in New Issue
Block a user