The operating-point series (flow.predicted.{downstream,atequipment},
power.predicted.atequipment) were only written by calcFlow/calcPower while
operational, or by _updateState on a state transition. A machine that boots
into idle and never runs therefore emitted these keys NEVER — so InfluxDB
carried only the flow envelope (max/min) and dashboard panels querying the
operating point rendered blank, unable to show even the off/0 state.
Seed them to 0 in _init() alongside max/min, so telemetry always carries the
operating point: 0 while idle, real values once the pump runs. Verified end to
end: keys now present in InfluxDB, the Grafana flow panel resolves, and the
real prediction path produces non-zero values (~98 m3/h, ~13 kW) that flow
through getOutput to Port 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
147 lines
5.8 KiB
JavaScript
147 lines
5.8 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const Machine = require('../../src/specificClass');
|
|
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
|
|
|
|
test('getOutput contains all required fields in idle state', () => {
|
|
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
|
|
|
const output = machine.getOutput();
|
|
|
|
// Core state fields
|
|
assert.equal(output.state, 'idle');
|
|
assert.ok('runtime' in output);
|
|
assert.ok('ctrl' in output);
|
|
assert.ok('moveTimeleft' in output);
|
|
assert.ok('mode' in output);
|
|
assert.ok('maintenanceTime' in output);
|
|
|
|
// Efficiency fields
|
|
assert.ok('cog' in output);
|
|
assert.ok('NCog' in output);
|
|
assert.ok('NCogPercent' in output);
|
|
assert.ok('effDistFromPeak' in output);
|
|
assert.ok('effRelDistFromPeak' in output);
|
|
|
|
// Prediction health fields
|
|
assert.ok('predictionQuality' in output);
|
|
assert.ok('predictionConfidence' in output);
|
|
assert.ok('predictionPressureSource' in output);
|
|
assert.ok('predictionFlags' in output);
|
|
|
|
// Pressure drift fields
|
|
assert.ok('pressureDriftLevel' in output);
|
|
assert.ok('pressureDriftSource' in output);
|
|
assert.ok('pressureDriftFlags' in output);
|
|
});
|
|
|
|
test('getOutput seeds operating-point flow/power telemetry at boot (idle = 0, not absent)', () => {
|
|
// Regression: an idle-from-boot machine must still emit the operating-point
|
|
// series so dashboards can show the off/0 state. These keys are otherwise
|
|
// only written once the pump runs (calcFlow/calcPower) or on a state
|
|
// transition, leaving them absent in telemetry for a pump that never starts.
|
|
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
|
const output = machine.getOutput();
|
|
|
|
const hasPrefix = (p) => Object.keys(output).some((k) => k.startsWith(p));
|
|
const valueFor = (p) => output[Object.keys(output).find((k) => k.startsWith(p))];
|
|
|
|
for (const prefix of [
|
|
'flow.predicted.downstream',
|
|
'flow.predicted.atequipment',
|
|
'power.predicted.atequipment',
|
|
]) {
|
|
assert.ok(hasPrefix(prefix), `${prefix}.* must be present at boot (idle)`);
|
|
assert.equal(valueFor(prefix), 0, `${prefix}.* should be 0 while idle`);
|
|
}
|
|
|
|
// The envelope keys remain present too.
|
|
assert.ok(hasPrefix('flow.predicted.max'));
|
|
assert.ok(hasPrefix('flow.predicted.min'));
|
|
});
|
|
|
|
test('getOutput flow drift fields appear after sufficient measured flow samples', async () => {
|
|
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
|
|
|
await machine.handleInput('parent', 'execSequence', 'startup');
|
|
machine.updateMeasuredPressure(1000, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt' });
|
|
await machine.handleInput('parent', 'execMovement', 50);
|
|
|
|
// Provide multiple measured flow samples to trigger valid drift assessment
|
|
const baseTime = Date.now();
|
|
for (let i = 0; i < 12; i++) {
|
|
machine.updateMeasuredFlow(100 + i, 'downstream', {
|
|
timestamp: baseTime + (i * 1000),
|
|
unit: 'm3/h',
|
|
childId: 'flow-sensor',
|
|
childName: 'FT-1',
|
|
});
|
|
}
|
|
|
|
const output = machine.getOutput();
|
|
|
|
// Drift fields should appear once enough samples provide a valid assessment
|
|
if ('flowNrmse' in output) {
|
|
assert.ok(typeof output.flowNrmse === 'number');
|
|
assert.ok('flowDriftValid' in output);
|
|
}
|
|
// At minimum, prediction health fields should always be present
|
|
assert.ok('predictionQuality' in output);
|
|
assert.ok('predictionConfidence' in output);
|
|
});
|
|
|
|
test('getOutput prediction confidence is 0 in non-operational state', () => {
|
|
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
|
|
|
const output = machine.getOutput();
|
|
|
|
assert.equal(output.predictionConfidence, 0);
|
|
});
|
|
|
|
test('getOutput prediction confidence reflects differential pressure', () => {
|
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
|
|
|
// Differential pressure → high confidence
|
|
machine.updateMeasuredPressure(800, 'upstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-up' });
|
|
machine.updateMeasuredPressure(1200, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-down' });
|
|
|
|
const output = machine.getOutput();
|
|
|
|
assert.ok(output.predictionConfidence >= 0.8, `Confidence ${output.predictionConfidence} should be >= 0.8 with differential pressure`);
|
|
assert.equal(output.predictionPressureSource, 'differential');
|
|
});
|
|
|
|
test('getOutput values are in configured output units not canonical', () => {
|
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
|
|
|
machine.updateMeasuredPressure(1000, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt' });
|
|
machine.updatePosition();
|
|
|
|
const output = machine.getOutput();
|
|
|
|
// Flow keys should contain values in m3/h (configured), not m3/s (canonical)
|
|
// Predicted flow at minimum pressure should be in a reasonable m3/h range, not ~0.003 m3/s
|
|
const flowKey = Object.keys(output).find(k => k.startsWith('flow.predicted.downstream'));
|
|
if (flowKey) {
|
|
const flowVal = output[flowKey];
|
|
assert.ok(typeof flowVal === 'number', 'Flow output should be a number');
|
|
// m3/h values are typically 0-300, m3/s values are 0-0.08
|
|
// If in canonical units it would be very small
|
|
if (flowVal > 0) {
|
|
assert.ok(flowVal > 0.1, `Flow value ${flowVal} looks like canonical m3/s, should be m3/h`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('getOutput NCogPercent is correctly derived from NCog', () => {
|
|
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
|
|
|
machine.updateMeasuredPressure(1000, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt' });
|
|
machine.updatePosition();
|
|
|
|
const output = machine.getOutput();
|
|
const expected = Math.round(output.NCog * 100 * 100) / 100;
|
|
assert.equal(output.NCogPercent, expected, 'NCogPercent should be NCog * 100, rounded to 2 decimals');
|
|
});
|