From f5c6282478fa064a14885794dfbef8b556c9ef5e Mon Sep 17 00:00:00 2001 From: znetsixe Date: Sat, 23 May 2026 13:43:35 +0200 Subject: [PATCH] refactor(units): use UnitPolicy.convert instead of hardcoded m3/h<->m3/s scalars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/control/manual.js | 5 +++-- src/specificClass.js | 7 ++++++- test/basic/control-manual.basic.test.js | 17 ++++++++++++----- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/control/manual.js b/src/control/manual.js index 4d9cc44..e86f890 100644 --- a/src/control/manual.js +++ b/src/control/manual.js @@ -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}`); }) ) diff --git a/src/specificClass.js b/src/specificClass.js index fc46a2d..d34170d 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -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); diff --git a/test/basic/control-manual.basic.test.js b/test/basic/control-manual.basic.test.js index c2b8267..36ef444 100644 --- a/test/basic/control-manual.basic.test.js +++ b/test/basic/control-manual.basic.test.js @@ -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); });