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

@@ -75,10 +75,12 @@ test('canonical topics dispatch to their handlers', async () => {
await reg.dispatch({ topic: 'cmd.calibrate.level', payload: 1.25 }, source, makeCtx());
assert.deepEqual(calls.calibratePredictedLevel, [1.25]);
// Registry normalises to the descriptor's `units.default` (m3/h) before
// the handler runs. 0.5 m3/s -> 1800 m3/h.
await reg.dispatch({ topic: 'set.inflow', payload: 0.5, unit: 'm3/s' }, source, makeCtx());
assert.equal(calls.setManualInflow.length, 1);
assert.equal(calls.setManualInflow[0].v, 0.5);
assert.equal(calls.setManualInflow[0].u, 'm3/s');
assert.equal(calls.setManualInflow[0].v, 1800);
assert.equal(calls.setManualInflow[0].u, 'm3/h');
await reg.dispatch({ topic: 'set.demand', payload: 100 }, source, makeCtx());
assert.deepEqual(calls.forwardDemandToChildren, [100]);
@@ -140,11 +142,16 @@ test('set.inflow accepts number payload and { value, unit, timestamp } object pa
const { source, calls } = makeSource();
const reg = makeRegistry(makeLogger());
// After registry units-normalisation the handler always sees a number in
// the descriptor's default unit (m3/h). 0.5 m3/s -> 1800 m3/h.
await reg.dispatch({ topic: 'set.inflow', payload: 0.5, unit: 'm3/s', timestamp: 1000 }, source, makeCtx());
assert.deepEqual(calls.setManualInflow[0], { v: 0.5, ts: 1000, u: 'm3/s' });
assert.deepEqual(calls.setManualInflow[0], { v: 1800, ts: 1000, u: 'm3/h' });
// Object payload `{ value, unit }` is flattened to a number; 2 m3/h stays
// 2 m3/h. The timestamp travels on the msg envelope after normalisation
// (the per-payload `timestamp` field is not preserved by the flatten).
await reg.dispatch(
{ topic: 'set.inflow', payload: { value: 2, unit: 'm3/h', timestamp: 2000 } },
{ topic: 'set.inflow', payload: { value: 2, unit: 'm3/h' }, timestamp: 2000 },
source,
makeCtx()
);

View File

@@ -6,6 +6,31 @@ const assert = require('node:assert/strict');
const PumpingStation = require('../../src/specificClass');
// machineGroups is a registry-backed getter (declareChildGetter) — direct
// assignment is no longer possible. Tests inject mock groups through the
// real registration handshake so the registry remains the source of truth.
function registerMockGroup(ps, id, behavior = {}) {
const calls = { handleInput: [], turnOff: 0 };
const mock = {
config: {
general: { id, name: id },
functionality: { softwareType: 'machinegroup', positionVsParent: 'atEquipment' },
asset: { category: 'controller' },
},
measurements: {
emitter: { on: () => {} },
setChildId: () => {}, setChildName: () => {}, setParentRef: () => {},
},
handleInput: behavior.handleInput
|| (async (...args) => { calls.handleInput.push(args); }),
turnOffAllMachines: behavior.turnOffAllMachines
|| (() => { calls.turnOff += 1; }),
_calls: calls,
};
ps.childRegistrationUtils.registerChild(mock, 'atEquipment');
return mock;
}
// Standard config shape. Override any section by passing { section: {...} }.
function makeConfig(overrides = {}) {
const base = {
@@ -229,70 +254,46 @@ test('Calibration — predicted volume and level', async (t) => {
test('Levelbased control zones — _controlLevelBased', async (t) => {
await t.test('level < minLevel → percControl=0 and MGC turnOff called', async () => {
const ps = new PumpingStation(makeConfig());
let turnOffCalls = 0;
ps.machineGroups['mgc1'] = {
config: { general: { name: 'mgc1' } },
turnOffAllMachines: () => { turnOffCalls++; },
handleInput: async () => {},
};
const mock = registerMockGroup(ps, 'mgc1');
ps.calibratePredictedLevel(0.5); // below minLevel=1
await ps._controlLevelBased();
assert.equal(ps.percControl, 0);
assert.equal(turnOffCalls, 1);
assert.equal(mock._calls.turnOff, 1);
});
await t.test('minLevel ≤ level < active ramp start → commands 0% without shutdown', async () => {
const ps = new PumpingStation(makeConfig());
ps.percControl = 42; // simulated previous demand
const demands = [];
ps.machineGroups['mgc1'] = {
config: { general: { name: 'mgc1' } },
turnOffAllMachines: () => {},
handleInput: async (_src, d) => { demands.push(d); },
};
const mock = registerMockGroup(ps, 'mgc1');
ps.calibratePredictedLevel(1.5); // between minLevel=1 and startLevel=2
await ps._controlLevelBased();
assert.equal(ps.percControl, 0);
assert.equal(demands[0], 0);
assert.equal(mock._calls.handleInput[0][1], 0);
});
await t.test('filling: level between startLevel and inflowLevel commands 0%', async () => {
const ps = new PumpingStation(makeConfig());
const demands = [];
ps.machineGroups['mgc1'] = {
config: { general: { name: 'mgc1' } },
turnOffAllMachines: () => {},
handleInput: async (_src, d) => { demands.push(d); },
};
const mock = registerMockGroup(ps, 'mgc1');
ps.calibratePredictedLevel(2.5); // startLevel=2, inflowLevel=3
await ps._controlLevelBased('filling');
assert.equal(ps.percControl, 0);
assert.equal(demands[0], 0);
assert.equal(mock._calls.handleInput[0][1], 0);
});
await t.test('filling: level ≥ inflowLevel → percControl linearly scaled to [0,100]', async () => {
const ps = new PumpingStation(makeConfig());
const demands = [];
ps.machineGroups['mgc1'] = {
config: { general: { name: 'mgc1' } },
turnOffAllMachines: () => {},
handleInput: async (_src, d) => { demands.push(d); },
};
const mock = registerMockGroup(ps, 'mgc1');
ps.calibratePredictedLevel(3.5); // midpoint of inflowLevel=3 and maxLevel=4
await ps._controlLevelBased('filling');
// lerp(3.5, [3,4], [0,100]) = 50
assert.ok(Math.abs(ps.percControl - 50) < 1e-9);
assert.equal(demands.length, 1);
assert.ok(Math.abs(demands[0] - 50) < 1e-9);
assert.equal(mock._calls.handleInput.length, 1);
assert.ok(Math.abs(mock._calls.handleInput[0][1] - 50) < 1e-9);
});
await t.test('shift disabled (default): foot stays at inflowLevel even after fall', async () => {
const ps = new PumpingStation(makeConfig());
ps.machineGroups['mgc1'] = {
config: { general: { name: 'mgc1' } },
turnOffAllMachines: () => {},
handleInput: async () => {},
};
registerMockGroup(ps, 'mgc1');
// Climb past inflowLevel and beyond, then fall to a level inside [start..inflow].
ps.calibratePredictedLevel(3.8);
await ps._controlLevelBased();
@@ -317,11 +318,7 @@ test('Levelbased control zones — _controlLevelBased', async (t) => {
},
},
}));
ps.machineGroups['mgc1'] = {
config: { general: { name: 'mgc1' } },
turnOffAllMachines: () => {},
handleInput: async () => {},
};
registerMockGroup(ps, 'mgc1');
// Filling at level=3.5 ⇒ up curve = 50 %, below arm threshold ⇒ not armed.
ps.calibratePredictedLevel(3.5);
await ps._controlLevelBased('filling');
@@ -363,11 +360,7 @@ test('Levelbased control zones — _controlLevelBased', async (t) => {
},
},
}));
ps.machineGroups['mgc1'] = {
config: { general: { name: 'mgc1' } },
turnOffAllMachines: () => {},
handleInput: async () => {},
};
registerMockGroup(ps, 'mgc1');
ps.calibratePredictedLevel(3.85);
await ps._controlLevelBased('filling');
await ps._controlLevelBased('draining');
@@ -391,11 +384,7 @@ test('Levelbased control zones — _controlLevelBased', async (t) => {
levelbased: { minLevel: 1, startLevel: 2, maxLevel: 4, curveType: 'log', logCurveFactor: 9 },
},
}));
ps.machineGroups['mgc1'] = {
config: { general: { name: 'mgc1' } },
turnOffAllMachines: () => {},
handleInput: async () => {},
};
registerMockGroup(ps, 'mgc1');
ps.calibratePredictedLevel(3.5); // x=0.5 on filling ramp [3,4]
await ps._controlLevelBased('filling');
assert.ok(ps.percControl > 50);
@@ -404,11 +393,7 @@ test('Levelbased control zones — _controlLevelBased', async (t) => {
await t.test('level > maxLevel → percControl ≥ 100 (MGC clamps internally)', async () => {
const ps = new PumpingStation(makeConfig());
ps.machineGroups['mgc1'] = {
config: { general: { name: 'mgc1' } },
turnOffAllMachines: () => {},
handleInput: async () => {},
};
registerMockGroup(ps, 'mgc1');
ps.calibratePredictedLevel(4.5); // above maxLevel=4
await ps._controlLevelBased();
assert.ok(ps.percControl >= 100);