diff --git a/src/nodeClass.js b/src/nodeClass.js index 8d7b9a0..07cc409 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -223,12 +223,12 @@ class nodeClass { .variant('measured') .position('atequipment') .getCurrentValue('m3'); - this.source.calibratePredictedVolume(calibratedVolume); + 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 unit = msg?.unit || 'l/s'; const ts = msg?.timestamp || Date.now(); this.source.setManualInflow(val, ts, unit); break; diff --git a/src/specificClass.js b/src/specificClass.js index 7956b71..35f52a4 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -11,12 +11,11 @@ class PumpingStation { this.interpolate = new interpolation(); 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.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.measurements = new MeasurementContainer({ + autoConvert: true, + preferredUnits: { flow: 'm3/s', netFlowRate: 'm3/s', level: 'm', volume: 'm3' } + }); + this.childRegistrationUtils = new childRegistrationUtils(this); this.machines = {}; @@ -196,67 +195,62 @@ class PumpingStation { _scaleLevelToFlowPercent(level,minflow,maxflow) { const { minFlowLevel, maxFlowLevel } = this.config.control.levelbased; - const output = this.interpolate_lin_single_point(level,minFlowLevel,maxFlowLevel,minflow,maxflow); + const output = this.interpolate.interpolate_lin_single_point(level,minFlowLevel,maxFlowLevel,minflow,maxflow); return output; } - 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]; + async _controlLevelBased(snapshot, direction) { + const { startLevel, stopLevel } = this.config.control.levelbased; + const flowUnit = this.measurements.getUnit('flow'); // use container as source of truth + let percControl = 0; + + const level = (snap) => { + for (const variant of this.levelVariants) { + const levelSnap = snap.levels?.[variant]; if (levelSnap?.samples?.current?.value !== undefined) { return levelSnap.samples.current.value; } } return null; }; - - if (level == null) { + + const levelVal = level(snapshot); + if (levelVal == null || !Number.isFinite(levelVal)) { this.logger.warn('No valid level found'); return; } - if(level > startLevel){ - let maxFlow = 0; + if (levelVal > startLevel && direction === 'filling') { + let sumFlow = 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}; - + Object.values(this.machines).forEach((m) => { + const max = m.measurements.type('flow').variant('predicted').position('max').getCurrentValue(flowUnit); + const min = m.measurements.type('flow').variant('predicted').position('min').getCurrentValue(flowUnit); + if (Number.isFinite(max)) sumFlow += max; + if (Number.isFinite(min) && min < minTotalFlow) minTotalFlow = min; }); - Object.entries(this.machineGroups).forEach(([groupId, group]) => { - sumFlow = group.dynamicTotals.flow.max; - - }); - - - //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(`showing level : ${levelVal}, minTotalFlow: ${minTotalFlow}, sumFlow ${sumFlow}`); + percControl = this._scaleLevelToFlowPercent(levelVal, minTotalFlow, sumFlow); + await this._applyIdleMachineLevelControl(percControl); } - const percControl = this._scaleLevelToFlowPercent(level); - - - await this._applyMachineGroupLevelControl(percControl); - await this._applyIdleMachineLevelControl(percControl); + if (levelVal < stopLevel && direction === 'draining') { + 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'); + } + }); + Object.entries(this.stations).forEach(([stationId, station]) => { + station.handleInput('parent', 'execSequence', 'shutdown'); + }); + Object.entries(this.machineGroups).forEach(([groupId, group]) => { + group.turnOffAllMachines(); + }); + } } async _applyMachineGroupLevelControl(percentControl) { @@ -311,13 +305,13 @@ class PumpingStation { } //control logic - _controlLogic(snapshot){ + _controlLogic(snapshot,direction){ const mode = this.mode; switch(mode){ case "levelbased": this.logger.debug(`Executing level-based control logic`); - this._controlLevelBased(snapshot); + this._controlLevelBased(snapshot,direction); break; case "flowbased": this._controlFlowBased(); @@ -421,7 +415,7 @@ class PumpingStation { if(this.safetyControllerActive) return; //if safety not active proceed with normal control - this._controlLogic(snapshot,remaining.seconds); + this._controlLogic(snapshot,netFlow.direction); this.state = { direction: netFlow.direction, @@ -495,58 +489,50 @@ class PumpingStation { } const handler = (eventData = {}) => { - 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 flowUnit = this.measurements.getUnit('flow'); + const unit = eventData.unit || child.config?.general?.unit || flowUnit; 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); + const posKeyBase = posKey; // 'in' or 'out' + + this.measurements + .type('flow') + .variant('predicted') + .position(posKeyBase) + .child(childId) + .value(eventData.value, ts, unit); + + this._refreshAggregatedPredictedFlow(); }; eventNames.forEach((eventName) => child.measurements.emitter.on(eventName, handler)); } - _refreshAggregatedPredictedFlow(direction) { - const preferredUnit = this.preferredUnits.flow; + _refreshAggregatedPredictedFlow() { + const preferredUnit = this.measurements.getUnit('flow'); + const childPositions = Object.keys(this.measurements.measurements?.flow?.predictedChild || {}); + const inflowPositions = childPositions.filter((p) => p === 'in'); + const outflowPositions = childPositions.filter((p) => p === 'out'); - const sum = Array.from(this.predictedFlowChildren.values()) - .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); + const sumIn = this.measurements.sum('flow', 'predicted', inflowPositions, preferredUnit); + const sumOut = this.measurements.sum('flow', 'predicted', outflowPositions, preferredUnit); - this.measurements - .type('flow') - .variant('predicted') - .position(direction) - .value(sum, Date.now(), preferredUnit); + this.measurements.type('flow').variant('predicted').position('in').value(sumIn, Date.now(), preferredUnit); + this.measurements.type('flow').variant('predicted').position('out').value(sumOut, Date.now(), preferredUnit); } setManualInflow(value, timestamp = Date.now(), unit) { const num = Number(value); - const preferredUnit = this.preferredUnits.flow; + const unit = this.measurements.getUnit('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 + // Write manual inflow into the aggregated bucket + this.measurements .type('flow') - .variant('manual') + .variant('predicted') .position('in') - .getCurrentValue(preferredUnit); + .child('manual-qin') + .value(num, timestamp, unit); - 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); + this._refreshAggregatedPredictedFlow(); } _handleMeasurement(measurementType, value, position, context) { @@ -737,11 +723,12 @@ class PumpingStation { if (!flow.inflow.exists && !flow.outflow.exists) continue; - const inflow = flow.inflow.current?.value ?? 0; - const outflow = flow.outflow.current?.value ?? 0; - const net = inflow - outflow; // positive => filling + const unit = this.measurements.getUnit('flow'); + const inflow = flow.inflow.current?.value ?? flow.inflow.previous?.value ?? 0; + const outflow = flow.outflow.current?.value ?? flow.outflow.previous?.value ?? 0; + const net = inflow - outflow; // -> pos is filling - this.measurements.type('netFlowRate').variant(variant).position('atequipment').value(net).unit('m3/s'); + this.measurements.type('netFlowRate').variant(variant).position('atequipment').value(net, Date.now(), unit); this.logger.debug(`inflow : ${inflow} - outflow : ${outflow}`); return { value: net,source: variant,direction: this._deriveDirection(net) }; @@ -945,14 +932,8 @@ class PumpingStation { /* ------------------------------------------------------------------ */ getOutput() { - const output = {}; - Object.entries(this.measurements.measurements).forEach(([type, variants]) => { - Object.entries(variants).forEach(([variant, positions]) => { - Object.entries(positions).forEach(([position, measurement]) => { - output[`${type}.${variant}.${position}`] = measurement.getCurrentValue(); - }); - }); - }); + + const output = this.measurements.getFlattenedOutput(); output.direction = this.state.direction; output.flowSource = this.state.flowSource;