fix: deduplicate predicted-flow child registration + single event subscription

Three bugs in registerChild caused multi-counted outflow in _updatePredictedVolume:

1. machinegroup registered twice (line 66 + line 70 both called
   _registerPredictedFlowChild). Fixed: only register in the
   machinegroup branch.

2. Individual machines registered alongside their machinegroup parent.
   Each pump's predicted flow is already included in MGC's aggregated
   total — subscribing to both triple-counts. Fixed: only register
   individual machines when no machinegroup is present (direct-wired
   pumps without MGC).

3. _registerPredictedFlowChild subscribed to BOTH flow.predicted.downstream
   AND flow.predicted.atequipment events. These carry the same total flow
   on two event names — the handler wrote the value twice per tick.
   Fixed: subscribe to ONE event per child (downstream for outflow,
   upstream for inflow).

These are generalizable patterns:
- When a group aggregator exists, subscribe to IT, not its children.
- One event per measurement type per child — pick the most specific.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-04-14 13:10:16 +02:00
parent f869296832
commit c62d8bc275

View File

@@ -63,10 +63,21 @@ class PumpingStation {
this.stations[child.config.general.id] = child;
} else if (softwareType === 'machinegroup') {
this.machineGroups[child.config.general.id] = child;
this._registerPredictedFlowChild(child);
}
if (softwareType === 'machine' || softwareType === 'pumpingstation' || softwareType === 'machinegroup') {
// Register predicted-flow subscription. Only register the HIGHEST-
// level aggregator: if a machinegroup is present, subscribe to IT
// (its flow.predicted already aggregates all child machines). Do NOT
// also subscribe to individual machines — that would double-count
// because each pump's flow is included in the group total.
//
// Individual machines (softwareType='machine') are only subscribed
// when there is NO machinegroup parent — i.e., pumps wired directly
// to the pumping station without an MGC in between.
if (softwareType === 'machinegroup' || softwareType === 'pumpingstation') {
this._registerPredictedFlowChild(child);
} else if (softwareType === 'machine' && Object.keys(this.machineGroups).length === 0) {
// Direct-child machine, no group above it — register its flow.
this._registerPredictedFlowChild(child);
}
}
@@ -97,18 +108,21 @@ class PumpingStation {
const childId = child.config.general.id ?? childName;
let posKey;
let eventNames;
let eventName;
switch (position) {
case 'downstream':
case 'out':
case 'atequipment':
posKey = 'out';
eventNames = ['flow.predicted.downstream', 'flow.predicted.atequipment'];
// Subscribe to ONE event only. 'downstream' is the most specific
// — avoids double-counting from 'atequipment' which carries the
// same total flow on a different event name.
eventName = 'flow.predicted.downstream';
break;
case 'upstream':
case 'in':
posKey = 'in';
eventNames = ['flow.predicted.upstream', 'flow.predicted.atequipment'];
eventName = 'flow.predicted.upstream';
break;
default:
this.logger.warn(`Unsupported predicted flow position "${position}" from ${childName}`);
@@ -132,7 +146,7 @@ class PumpingStation {
.value(eventData.value, ts, unit);
};
eventNames.forEach((ev) => child.measurements.emitter.on(ev, handler));
child.measurements.emitter.on(eventName, handler);
}
/* --------------------------- Calibration --------------------------- */