Compare commits
1 Commits
ef81013e96
...
5f1c9ae2ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f1c9ae2ff |
@@ -12,6 +12,7 @@ module.exports = [
|
|||||||
topic: 'set.mode',
|
topic: 'set.mode',
|
||||||
aliases: ['changemode'],
|
aliases: ['changemode'],
|
||||||
payloadSchema: { type: 'string' },
|
payloadSchema: { type: 'string' },
|
||||||
|
description: 'Switch the station between auto / manual control modes.',
|
||||||
handler: handlers.setMode,
|
handler: handlers.setMode,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -19,6 +20,7 @@ module.exports = [
|
|||||||
aliases: ['registerChild'],
|
aliases: ['registerChild'],
|
||||||
// payload is the Node-RED id (string) of the child node.
|
// payload is the Node-RED id (string) of the child node.
|
||||||
payloadSchema: { type: 'string' },
|
payloadSchema: { type: 'string' },
|
||||||
|
description: 'Register a child node (machine group, measurement, …) with this station.',
|
||||||
handler: handlers.registerChild,
|
handler: handlers.registerChild,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -26,12 +28,16 @@ module.exports = [
|
|||||||
aliases: ['calibratePredictedVolume'],
|
aliases: ['calibratePredictedVolume'],
|
||||||
// any: payload may be a number or numeric string.
|
// any: payload may be a number or numeric string.
|
||||||
payloadSchema: { type: 'any' },
|
payloadSchema: { type: 'any' },
|
||||||
|
units: { measure: 'volume', default: 'm3' },
|
||||||
|
description: 'Calibrate the predicted-volume integrator to a known basin volume.',
|
||||||
handler: handlers.calibrateVolume,
|
handler: handlers.calibrateVolume,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
topic: 'cmd.calibrate.level',
|
topic: 'cmd.calibrate.level',
|
||||||
aliases: ['calibratePredictedLevel'],
|
aliases: ['calibratePredictedLevel'],
|
||||||
payloadSchema: { type: 'any' },
|
payloadSchema: { type: 'any' },
|
||||||
|
units: { measure: 'length', default: 'm' },
|
||||||
|
description: 'Calibrate the predicted-volume integrator to a known basin level.',
|
||||||
handler: handlers.calibrateLevel,
|
handler: handlers.calibrateLevel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -39,18 +45,24 @@ module.exports = [
|
|||||||
aliases: ['q_in'],
|
aliases: ['q_in'],
|
||||||
// any: number, numeric string, or { value, unit, timestamp } object.
|
// any: number, numeric string, or { value, unit, timestamp } object.
|
||||||
payloadSchema: { type: 'any' },
|
payloadSchema: { type: 'any' },
|
||||||
|
units: { measure: 'volumeFlowRate', default: 'm3/h' },
|
||||||
|
description: 'Push a measured inflow value into the basin balance.',
|
||||||
handler: handlers.setInflow,
|
handler: handlers.setInflow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
topic: 'set.outflow',
|
topic: 'set.outflow',
|
||||||
aliases: ['q_out'],
|
aliases: ['q_out'],
|
||||||
payloadSchema: { type: 'any' },
|
payloadSchema: { type: 'any' },
|
||||||
|
units: { measure: 'volumeFlowRate', default: 'm3/h' },
|
||||||
|
description: 'Push a measured outflow value into the basin balance.',
|
||||||
handler: handlers.setOutflow,
|
handler: handlers.setOutflow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
topic: 'set.demand',
|
topic: 'set.demand',
|
||||||
aliases: ['Qd'],
|
aliases: ['Qd'],
|
||||||
payloadSchema: { type: 'any' },
|
payloadSchema: { type: 'any' },
|
||||||
|
units: { measure: 'volumeFlowRate', default: 'm3/h' },
|
||||||
|
description: 'Operator outflow demand setpoint for the station.',
|
||||||
handler: handlers.setDemand,
|
handler: handlers.setDemand,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -91,35 +91,29 @@ class PumpingStation extends BaseDomain {
|
|||||||
this.measurements.type('volume').variant('predicted').position('atequipment')
|
this.measurements.type('volume').variant('predicted').position('atequipment')
|
||||||
.value(this.basin.minVol, Date.now(), 'm3').unit('m3');
|
.value(this.basin.minVol, Date.now(), 'm3').unit('m3');
|
||||||
|
|
||||||
// Plain id-keyed maps. Tests assign into them directly (legacy contract);
|
// Registry-as-truth — `this.machines / machineGroups / stations` are
|
||||||
// ChildRouter onRegister handlers below also populate them.
|
// read-only getters flattening `this.child[softwareType]` (BaseDomain
|
||||||
this.machines = {};
|
// helper). Mutations go through `childRegistrationUtils.registerChild`.
|
||||||
this.stations = {};
|
this.declareChildGetter('machines', 'machine');
|
||||||
this.machineGroups = {};
|
this.declareChildGetter('machineGroups', 'machinegroup');
|
||||||
this.predictedFlowChildren = new Map();
|
this.declareChildGetter('stations', 'pumpingstation');
|
||||||
|
|
||||||
// SafetyController constructed after child maps so its captured ctx
|
// SafetyController's captured ctx exposes the same three names as live
|
||||||
// references the live dicts rather than undefined.
|
// getters (installed in context()), so the registry remains the single
|
||||||
|
// source of truth long after configure() returns.
|
||||||
this.safety = new SafetyController(this.context());
|
this.safety = new SafetyController(this.context());
|
||||||
|
|
||||||
this.router
|
this.router
|
||||||
.onRegister('measurement', (child) => this._subscribeMeasurement(child))
|
.onRegister('measurement', (child) => this._subscribeMeasurement(child))
|
||||||
.onRegister('machine', (child) => {
|
.onRegister('machine', (child) => {
|
||||||
this.machines[child.config.general.id] = child;
|
|
||||||
// Skip individual machines when a machineGroup parent is present —
|
// Skip individual machines when a machineGroup parent is present —
|
||||||
// the group's flow.predicted already aggregates child machines.
|
// the group's flow.predicted already aggregates child machines.
|
||||||
if (Object.keys(this.machineGroups).length === 0) {
|
if (Object.keys(this.machineGroups).length === 0) {
|
||||||
this._subscribePredictedFlow(child);
|
this._subscribePredictedFlow(child);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.onRegister('machinegroup', (child) => {
|
.onRegister('machinegroup', (child) => this._subscribePredictedFlow(child))
|
||||||
this.machineGroups[child.config.general.id] = child;
|
.onRegister('pumpingstation', (child) => this._subscribePredictedFlow(child));
|
||||||
this._subscribePredictedFlow(child);
|
|
||||||
})
|
|
||||||
.onRegister('pumpingstation', (child) => {
|
|
||||||
this.stations[child.config.general.id] = child;
|
|
||||||
this._subscribePredictedFlow(child);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.debug('PumpingStation initialized');
|
this.logger.debug('PumpingStation initialized');
|
||||||
}
|
}
|
||||||
@@ -130,21 +124,28 @@ class PumpingStation extends BaseDomain {
|
|||||||
// `_lastDirection`, `_stopHystRunning`) write straight to the live
|
// `_lastDirection`, `_stopHystRunning`) write straight to the live
|
||||||
// instance — Object.freeze on the view itself is fine because these
|
// instance — Object.freeze on the view itself is fine because these
|
||||||
// flags live on the host, not in the view.
|
// 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() {
|
context() {
|
||||||
return Object.freeze({
|
const host = this;
|
||||||
|
const ctx = {
|
||||||
...super.context(),
|
...super.context(),
|
||||||
basin: this.basin,
|
basin: this.basin,
|
||||||
flowAggregator: this.flowAggregator,
|
flowAggregator: this.flowAggregator,
|
||||||
machines: this.machines,
|
|
||||||
machineGroups: this.machineGroups,
|
|
||||||
stations: this.stations,
|
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
flowVariants: this.flowVariants,
|
flowVariants: this.flowVariants,
|
||||||
levelVariants: this.levelVariants,
|
levelVariants: this.levelVariants,
|
||||||
volVariants: this.volVariants,
|
volVariants: this.volVariants,
|
||||||
flowThreshold: this.flowThreshold,
|
flowThreshold: this.flowThreshold,
|
||||||
host: this,
|
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() {
|
tick() {
|
||||||
@@ -301,9 +302,6 @@ class PumpingStation extends BaseDomain {
|
|||||||
const [posKey, eventName] = mapped;
|
const [posKey, eventName] = mapped;
|
||||||
const childId = child.config.general.id ?? child.config.general.name;
|
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 = {}) => {
|
child.measurements.emitter.on(eventName, (eventData = {}) => {
|
||||||
const unit = eventData.unit || child.config?.general?.unit;
|
const unit = eventData.unit || child.config?.general?.unit;
|
||||||
const ts = eventData.timestamp || Date.now();
|
const ts = eventData.timestamp || Date.now();
|
||||||
|
|||||||
@@ -75,10 +75,12 @@ test('canonical topics dispatch to their handlers', async () => {
|
|||||||
await reg.dispatch({ topic: 'cmd.calibrate.level', payload: 1.25 }, source, makeCtx());
|
await reg.dispatch({ topic: 'cmd.calibrate.level', payload: 1.25 }, source, makeCtx());
|
||||||
assert.deepEqual(calls.calibratePredictedLevel, [1.25]);
|
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());
|
await reg.dispatch({ topic: 'set.inflow', payload: 0.5, unit: 'm3/s' }, source, makeCtx());
|
||||||
assert.equal(calls.setManualInflow.length, 1);
|
assert.equal(calls.setManualInflow.length, 1);
|
||||||
assert.equal(calls.setManualInflow[0].v, 0.5);
|
assert.equal(calls.setManualInflow[0].v, 1800);
|
||||||
assert.equal(calls.setManualInflow[0].u, 'm3/s');
|
assert.equal(calls.setManualInflow[0].u, 'm3/h');
|
||||||
|
|
||||||
await reg.dispatch({ topic: 'set.demand', payload: 100 }, source, makeCtx());
|
await reg.dispatch({ topic: 'set.demand', payload: 100 }, source, makeCtx());
|
||||||
assert.deepEqual(calls.forwardDemandToChildren, [100]);
|
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 { source, calls } = makeSource();
|
||||||
const reg = makeRegistry(makeLogger());
|
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());
|
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(
|
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,
|
source,
|
||||||
makeCtx()
|
makeCtx()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,31 @@ const assert = require('node:assert/strict');
|
|||||||
|
|
||||||
const PumpingStation = require('../../src/specificClass');
|
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: {...} }.
|
// Standard config shape. Override any section by passing { section: {...} }.
|
||||||
function makeConfig(overrides = {}) {
|
function makeConfig(overrides = {}) {
|
||||||
const base = {
|
const base = {
|
||||||
@@ -229,70 +254,46 @@ test('Calibration — predicted volume and level', async (t) => {
|
|||||||
test('Levelbased control zones — _controlLevelBased', async (t) => {
|
test('Levelbased control zones — _controlLevelBased', async (t) => {
|
||||||
await t.test('level < minLevel → percControl=0 and MGC turnOff called', async () => {
|
await t.test('level < minLevel → percControl=0 and MGC turnOff called', async () => {
|
||||||
const ps = new PumpingStation(makeConfig());
|
const ps = new PumpingStation(makeConfig());
|
||||||
let turnOffCalls = 0;
|
const mock = registerMockGroup(ps, 'mgc1');
|
||||||
ps.machineGroups['mgc1'] = {
|
|
||||||
config: { general: { name: 'mgc1' } },
|
|
||||||
turnOffAllMachines: () => { turnOffCalls++; },
|
|
||||||
handleInput: async () => {},
|
|
||||||
};
|
|
||||||
ps.calibratePredictedLevel(0.5); // below minLevel=1
|
ps.calibratePredictedLevel(0.5); // below minLevel=1
|
||||||
await ps._controlLevelBased();
|
await ps._controlLevelBased();
|
||||||
assert.equal(ps.percControl, 0);
|
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 () => {
|
await t.test('minLevel ≤ level < active ramp start → commands 0% without shutdown', async () => {
|
||||||
const ps = new PumpingStation(makeConfig());
|
const ps = new PumpingStation(makeConfig());
|
||||||
ps.percControl = 42; // simulated previous demand
|
ps.percControl = 42; // simulated previous demand
|
||||||
const demands = [];
|
const mock = registerMockGroup(ps, 'mgc1');
|
||||||
ps.machineGroups['mgc1'] = {
|
|
||||||
config: { general: { name: 'mgc1' } },
|
|
||||||
turnOffAllMachines: () => {},
|
|
||||||
handleInput: async (_src, d) => { demands.push(d); },
|
|
||||||
};
|
|
||||||
ps.calibratePredictedLevel(1.5); // between minLevel=1 and startLevel=2
|
ps.calibratePredictedLevel(1.5); // between minLevel=1 and startLevel=2
|
||||||
await ps._controlLevelBased();
|
await ps._controlLevelBased();
|
||||||
assert.equal(ps.percControl, 0);
|
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 () => {
|
await t.test('filling: level between startLevel and inflowLevel commands 0%', async () => {
|
||||||
const ps = new PumpingStation(makeConfig());
|
const ps = new PumpingStation(makeConfig());
|
||||||
const demands = [];
|
const mock = registerMockGroup(ps, 'mgc1');
|
||||||
ps.machineGroups['mgc1'] = {
|
|
||||||
config: { general: { name: 'mgc1' } },
|
|
||||||
turnOffAllMachines: () => {},
|
|
||||||
handleInput: async (_src, d) => { demands.push(d); },
|
|
||||||
};
|
|
||||||
ps.calibratePredictedLevel(2.5); // startLevel=2, inflowLevel=3
|
ps.calibratePredictedLevel(2.5); // startLevel=2, inflowLevel=3
|
||||||
await ps._controlLevelBased('filling');
|
await ps._controlLevelBased('filling');
|
||||||
assert.equal(ps.percControl, 0);
|
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 () => {
|
await t.test('filling: level ≥ inflowLevel → percControl linearly scaled to [0,100]', async () => {
|
||||||
const ps = new PumpingStation(makeConfig());
|
const ps = new PumpingStation(makeConfig());
|
||||||
const demands = [];
|
const mock = registerMockGroup(ps, 'mgc1');
|
||||||
ps.machineGroups['mgc1'] = {
|
|
||||||
config: { general: { name: 'mgc1' } },
|
|
||||||
turnOffAllMachines: () => {},
|
|
||||||
handleInput: async (_src, d) => { demands.push(d); },
|
|
||||||
};
|
|
||||||
ps.calibratePredictedLevel(3.5); // midpoint of inflowLevel=3 and maxLevel=4
|
ps.calibratePredictedLevel(3.5); // midpoint of inflowLevel=3 and maxLevel=4
|
||||||
await ps._controlLevelBased('filling');
|
await ps._controlLevelBased('filling');
|
||||||
// lerp(3.5, [3,4], [0,100]) = 50
|
// lerp(3.5, [3,4], [0,100]) = 50
|
||||||
assert.ok(Math.abs(ps.percControl - 50) < 1e-9);
|
assert.ok(Math.abs(ps.percControl - 50) < 1e-9);
|
||||||
assert.equal(demands.length, 1);
|
assert.equal(mock._calls.handleInput.length, 1);
|
||||||
assert.ok(Math.abs(demands[0] - 50) < 1e-9);
|
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 () => {
|
await t.test('shift disabled (default): foot stays at inflowLevel even after fall', async () => {
|
||||||
const ps = new PumpingStation(makeConfig());
|
const ps = new PumpingStation(makeConfig());
|
||||||
ps.machineGroups['mgc1'] = {
|
registerMockGroup(ps, 'mgc1');
|
||||||
config: { general: { name: 'mgc1' } },
|
|
||||||
turnOffAllMachines: () => {},
|
|
||||||
handleInput: async () => {},
|
|
||||||
};
|
|
||||||
// Climb past inflowLevel and beyond, then fall to a level inside [start..inflow].
|
// Climb past inflowLevel and beyond, then fall to a level inside [start..inflow].
|
||||||
ps.calibratePredictedLevel(3.8);
|
ps.calibratePredictedLevel(3.8);
|
||||||
await ps._controlLevelBased();
|
await ps._controlLevelBased();
|
||||||
@@ -317,11 +318,7 @@ test('Levelbased control zones — _controlLevelBased', async (t) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
ps.machineGroups['mgc1'] = {
|
registerMockGroup(ps, 'mgc1');
|
||||||
config: { general: { name: 'mgc1' } },
|
|
||||||
turnOffAllMachines: () => {},
|
|
||||||
handleInput: async () => {},
|
|
||||||
};
|
|
||||||
// Filling at level=3.5 ⇒ up curve = 50 %, below arm threshold ⇒ not armed.
|
// Filling at level=3.5 ⇒ up curve = 50 %, below arm threshold ⇒ not armed.
|
||||||
ps.calibratePredictedLevel(3.5);
|
ps.calibratePredictedLevel(3.5);
|
||||||
await ps._controlLevelBased('filling');
|
await ps._controlLevelBased('filling');
|
||||||
@@ -363,11 +360,7 @@ test('Levelbased control zones — _controlLevelBased', async (t) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
ps.machineGroups['mgc1'] = {
|
registerMockGroup(ps, 'mgc1');
|
||||||
config: { general: { name: 'mgc1' } },
|
|
||||||
turnOffAllMachines: () => {},
|
|
||||||
handleInput: async () => {},
|
|
||||||
};
|
|
||||||
ps.calibratePredictedLevel(3.85);
|
ps.calibratePredictedLevel(3.85);
|
||||||
await ps._controlLevelBased('filling');
|
await ps._controlLevelBased('filling');
|
||||||
await ps._controlLevelBased('draining');
|
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 },
|
levelbased: { minLevel: 1, startLevel: 2, maxLevel: 4, curveType: 'log', logCurveFactor: 9 },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
ps.machineGroups['mgc1'] = {
|
registerMockGroup(ps, 'mgc1');
|
||||||
config: { general: { name: 'mgc1' } },
|
|
||||||
turnOffAllMachines: () => {},
|
|
||||||
handleInput: async () => {},
|
|
||||||
};
|
|
||||||
ps.calibratePredictedLevel(3.5); // x=0.5 on filling ramp [3,4]
|
ps.calibratePredictedLevel(3.5); // x=0.5 on filling ramp [3,4]
|
||||||
await ps._controlLevelBased('filling');
|
await ps._controlLevelBased('filling');
|
||||||
assert.ok(ps.percControl > 50);
|
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 () => {
|
await t.test('level > maxLevel → percControl ≥ 100 (MGC clamps internally)', async () => {
|
||||||
const ps = new PumpingStation(makeConfig());
|
const ps = new PumpingStation(makeConfig());
|
||||||
ps.machineGroups['mgc1'] = {
|
registerMockGroup(ps, 'mgc1');
|
||||||
config: { general: { name: 'mgc1' } },
|
|
||||||
turnOffAllMachines: () => {},
|
|
||||||
handleInput: async () => {},
|
|
||||||
};
|
|
||||||
ps.calibratePredictedLevel(4.5); // above maxLevel=4
|
ps.calibratePredictedLevel(4.5); // above maxLevel=4
|
||||||
await ps._controlLevelBased();
|
await ps._controlLevelBased();
|
||||||
assert.ok(ps.percControl >= 100);
|
assert.ok(ps.percControl >= 100);
|
||||||
|
|||||||
@@ -50,17 +50,34 @@ function makeConfig() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// machineGroups is a registry-backed getter (declareChildGetter) — inject
|
||||||
|
// the fake MGC via the real child-registration handshake so the registry
|
||||||
|
// stays the source of truth across configure() and tick().
|
||||||
|
function registerMockGroup(ps, id, demands) {
|
||||||
|
const mock = {
|
||||||
|
config: {
|
||||||
|
general: { id, name: id },
|
||||||
|
functionality: { softwareType: 'machinegroup', positionVsParent: 'atEquipment' },
|
||||||
|
asset: { category: 'controller' },
|
||||||
|
},
|
||||||
|
measurements: {
|
||||||
|
emitter: { on: () => {} },
|
||||||
|
setChildId: () => {}, setChildName: () => {}, setParentRef: () => {},
|
||||||
|
},
|
||||||
|
handleInput: async (_src, d) => { demands.push(d); },
|
||||||
|
turnOffAllMachines: () => {},
|
||||||
|
};
|
||||||
|
ps.childRegistrationUtils.registerChild(mock, 'atEquipment');
|
||||||
|
return mock;
|
||||||
|
}
|
||||||
|
|
||||||
// Build a PS with a fake MGC that captures every demand sent to it,
|
// Build a PS with a fake MGC that captures every demand sent to it,
|
||||||
// and a clock we control so _updatePredictedVolume integrates over a
|
// and a clock we control so _updatePredictedVolume integrates over a
|
||||||
// known dt regardless of wall-clock.
|
// known dt regardless of wall-clock.
|
||||||
function buildHarness() {
|
function buildHarness() {
|
||||||
const ps = new PumpingStation(makeConfig());
|
const ps = new PumpingStation(makeConfig());
|
||||||
const demands = [];
|
const demands = [];
|
||||||
ps.machineGroups['mgc1'] = {
|
registerMockGroup(ps, 'mgc1', demands);
|
||||||
config: { general: { name: 'mgc1' } },
|
|
||||||
turnOffAllMachines: () => {},
|
|
||||||
handleInput: async (_src, d) => { demands.push(d); },
|
|
||||||
};
|
|
||||||
// Seed level at startLevel so the run begins idle.
|
// Seed level at startLevel so the run begins idle.
|
||||||
ps.calibratePredictedLevel(2.0);
|
ps.calibratePredictedLevel(2.0);
|
||||||
// Override Date.now via a controllable clock that advances `step()`.
|
// Override Date.now via a controllable clock that advances `step()`.
|
||||||
|
|||||||
Reference in New Issue
Block a user