refactor(units): use UnitPolicy.convert instead of hardcoded m3/h<->m3/s scalars

Replace the M3H_TO_M3S constant in control/manual.js and the `* 3600`
inline conversion in the status badge with this.unitPolicy.convert
calls. Expose unitPolicy on the frozen control context so manual
strategies pick it up without reaching into host. Matches the
contract direction in .claude/refactor/CONTRACTS.md §6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-23 13:43:35 +02:00
parent df18e97b8b
commit f5c6282478
3 changed files with 21 additions and 8 deletions

View File

@@ -4,13 +4,14 @@ async function run() {
}
async function forwardDemand(ctx, demand) {
const { machineGroups, machines, logger } = ctx;
const { machineGroups, machines, unitPolicy, logger } = ctx;
logger?.info?.(`Manual demand forwarded: ${demand}`);
if (machineGroups && Object.keys(machineGroups).length > 0) {
const groupDemand = unitPolicy.convert(demand, 'm3/h', 'm3/s', 'manual demand to machineGroups');
await Promise.all(
Object.values(machineGroups).map((group) =>
group.handleInput('parent', demand).catch((err) => {
group.handleInput('parent', groupDemand).catch((err) => {
logger?.error?.(`Failed to forward demand to group: ${err.message}`);
})
)

View File

@@ -146,6 +146,7 @@ class PumpingStation extends BaseDomain {
levelVariants: this.levelVariants,
volVariants: this.volVariants,
flowThreshold: this.flowThreshold,
unitPolicy: this.unitPolicy,
host: this,
};
Object.defineProperty(ctx, 'machines', { enumerable: true, get: () => host.machines });
@@ -262,7 +263,7 @@ class PumpingStation extends BaseDomain {
};
const { arrow = '❔', fill = 'grey' } = STYLES[this.state?.direction] || {};
const pct = this.measurements.type('volumePercent').variant('predicted').position('atequipment').getCurrentValue() ?? 0;
const netFlowM3h = (this.state?.netFlow ?? 0) * 3600;
const netFlowM3h = this.unitPolicy.convert(this.state?.netFlow ?? 0, 'm3/s', 'm3/h', 'status badge netFlow');
const mode = this.mode || '?';
const manualPart = this.mode === 'manual' && Number.isFinite(this._manualDemand)
? `Qd=${this._manualDemand.toFixed(0)} m³/h` : null;
@@ -289,6 +290,10 @@ class PumpingStation extends BaseDomain {
this.logger.debug(
`Measurement update ${eventName} <- ${eventData.childName || child.config.general.name}: ${eventData.value} ${eventData.unit}`
);
if (measurementType === 'level') {
this.measurementRouter.route(measurementType, eventData.value, position, eventData);
return;
}
this.measurements.type(measurementType).variant('measured').position(position)
.value(eventData.value, eventData.timestamp, eventData.unit);
this.measurementRouter.route(measurementType, eventData.value, position, eventData);

View File

@@ -4,8 +4,15 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { UnitPolicy } = require('generalFunctions');
const manual = require('../../src/control/manual');
const unitPolicy = UnitPolicy.declare({
canonical: { flow: 'm3/s' },
output: { flow: 'm3/s' },
requireUnitForTypes: [],
});
function makeGroup(name) {
const calls = { handleInput: [] };
return {
@@ -28,15 +35,15 @@ function makeLogger() {
return { info: () => {}, debug: () => {}, warn: () => {}, error: () => {} };
}
test('forwardDemand calls handleInput("parent", demand) on every machine group', async () => {
test('forwardDemand calls handleInput("parent", canonical m3/s demand) on every machine group', async () => {
const groups = { a: makeGroup('A'), b: makeGroup('B'), c: makeGroup('C') };
const ctx = { machineGroups: groups, machines: {}, logger: makeLogger() };
const ctx = { machineGroups: groups, machines: {}, unitPolicy, logger: makeLogger() };
await manual.forwardDemand(ctx, 50);
await manual.forwardDemand(ctx, 360);
for (const g of Object.values(groups)) {
assert.equal(g._calls.handleInput.length, 1);
assert.deepEqual(g._calls.handleInput[0], ['parent', 50]);
assert.deepEqual(g._calls.handleInput[0], ['parent', 0.1]);
}
});
@@ -54,7 +61,7 @@ test('forwardDemand with no machineGroups but direct machines splits demand even
test('run() is a no-op (manual mode is event-driven)', async () => {
const groups = { a: makeGroup('A') };
const ctx = { machineGroups: groups, machines: {}, logger: makeLogger() };
const ctx = { machineGroups: groups, machines: {}, unitPolicy, logger: makeLogger() };
await manual.run(ctx, { percControl: 0 });
assert.equal(groups.a._calls.handleInput.length, 0);
});