P11.5 + B2.1/B2.2: per-command units + description (where applicable)

Adds  to scalar setters whose payloads are
plain numbers OR {value, unit}. Skipped where payload is compound or
mode-dependent (control-%, {F, C: [...]}, etc.) — documented inline.
Every command gains a description field for wikiGen consumption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-11 17:41:07 +02:00
parent ef81013e96
commit 5f1c9ae2ff
5 changed files with 107 additions and 88 deletions

View File

@@ -12,6 +12,7 @@ module.exports = [
topic: 'set.mode',
aliases: ['changemode'],
payloadSchema: { type: 'string' },
description: 'Switch the station between auto / manual control modes.',
handler: handlers.setMode,
},
{
@@ -19,6 +20,7 @@ module.exports = [
aliases: ['registerChild'],
// payload is the Node-RED id (string) of the child node.
payloadSchema: { type: 'string' },
description: 'Register a child node (machine group, measurement, …) with this station.',
handler: handlers.registerChild,
},
{
@@ -26,12 +28,16 @@ module.exports = [
aliases: ['calibratePredictedVolume'],
// any: payload may be a number or numeric string.
payloadSchema: { type: 'any' },
units: { measure: 'volume', default: 'm3' },
description: 'Calibrate the predicted-volume integrator to a known basin volume.',
handler: handlers.calibrateVolume,
},
{
topic: 'cmd.calibrate.level',
aliases: ['calibratePredictedLevel'],
payloadSchema: { type: 'any' },
units: { measure: 'length', default: 'm' },
description: 'Calibrate the predicted-volume integrator to a known basin level.',
handler: handlers.calibrateLevel,
},
{
@@ -39,18 +45,24 @@ module.exports = [
aliases: ['q_in'],
// any: number, numeric string, or { value, unit, timestamp } object.
payloadSchema: { type: 'any' },
units: { measure: 'volumeFlowRate', default: 'm3/h' },
description: 'Push a measured inflow value into the basin balance.',
handler: handlers.setInflow,
},
{
topic: 'set.outflow',
aliases: ['q_out'],
payloadSchema: { type: 'any' },
units: { measure: 'volumeFlowRate', default: 'm3/h' },
description: 'Push a measured outflow value into the basin balance.',
handler: handlers.setOutflow,
},
{
topic: 'set.demand',
aliases: ['Qd'],
payloadSchema: { type: 'any' },
units: { measure: 'volumeFlowRate', default: 'm3/h' },
description: 'Operator outflow demand setpoint for the station.',
handler: handlers.setDemand,
},
];

View File

@@ -91,35 +91,29 @@ class PumpingStation extends BaseDomain {
this.measurements.type('volume').variant('predicted').position('atequipment')
.value(this.basin.minVol, Date.now(), 'm3').unit('m3');
// Plain id-keyed maps. Tests assign into them directly (legacy contract);
// ChildRouter onRegister handlers below also populate them.
this.machines = {};
this.stations = {};
this.machineGroups = {};
this.predictedFlowChildren = new Map();
// Registry-as-truth — `this.machines / machineGroups / stations` are
// read-only getters flattening `this.child[softwareType]` (BaseDomain
// helper). Mutations go through `childRegistrationUtils.registerChild`.
this.declareChildGetter('machines', 'machine');
this.declareChildGetter('machineGroups', 'machinegroup');
this.declareChildGetter('stations', 'pumpingstation');
// SafetyController constructed after child maps so its captured ctx
// references the live dicts rather than undefined.
// SafetyController's captured ctx exposes the same three names as live
// getters (installed in context()), so the registry remains the single
// source of truth long after configure() returns.
this.safety = new SafetyController(this.context());
this.router
.onRegister('measurement', (child) => this._subscribeMeasurement(child))
.onRegister('machine', (child) => {
this.machines[child.config.general.id] = child;
// Skip individual machines when a machineGroup parent is present —
// the group's flow.predicted already aggregates child machines.
if (Object.keys(this.machineGroups).length === 0) {
this._subscribePredictedFlow(child);
}
})
.onRegister('machinegroup', (child) => {
this.machineGroups[child.config.general.id] = child;
this._subscribePredictedFlow(child);
})
.onRegister('pumpingstation', (child) => {
this.stations[child.config.general.id] = child;
this._subscribePredictedFlow(child);
});
.onRegister('machinegroup', (child) => this._subscribePredictedFlow(child))
.onRegister('pumpingstation', (child) => this._subscribePredictedFlow(child));
this.logger.debug('PumpingStation initialized');
}
@@ -130,21 +124,28 @@ class PumpingStation extends BaseDomain {
// `_lastDirection`, `_stopHystRunning`) write straight to the live
// instance — Object.freeze on the view itself is fine because these
// flags live on the host, not in the view.
//
// machines / machineGroups / stations are installed as live getters
// that delegate to this.* getters (declareChildGetter). SafetyController
// captures this ctx once at construction; the getters keep it reading
// fresh from the registry after later child registrations.
context() {
return Object.freeze({
const host = this;
const ctx = {
...super.context(),
basin: this.basin,
flowAggregator: this.flowAggregator,
machines: this.machines,
machineGroups: this.machineGroups,
stations: this.stations,
mode: this.mode,
flowVariants: this.flowVariants,
levelVariants: this.levelVariants,
volVariants: this.volVariants,
flowThreshold: this.flowThreshold,
host: this,
});
};
Object.defineProperty(ctx, 'machines', { enumerable: true, get: () => host.machines });
Object.defineProperty(ctx, 'machineGroups', { enumerable: true, get: () => host.machineGroups });
Object.defineProperty(ctx, 'stations', { enumerable: true, get: () => host.stations });
return Object.freeze(ctx);
}
tick() {
@@ -301,9 +302,6 @@ class PumpingStation extends BaseDomain {
const [posKey, eventName] = mapped;
const childId = child.config.general.id ?? child.config.general.name;
if (!this.predictedFlowChildren.has(childId)) {
this.predictedFlowChildren.set(childId, { in: 0, out: 0 });
}
child.measurements.emitter.on(eventName, (eventData = {}) => {
const unit = eventData.unit || child.config?.general?.unit;
const ts = eventData.timestamp || Date.now();