258 lines
6.8 KiB
JavaScript
258 lines
6.8 KiB
JavaScript
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const Monster = require('../src/specificClass');
|
|
const { MeasurementContainer } = require('generalFunctions');
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(`ok - ${name}`);
|
|
} catch (err) {
|
|
console.error(`not ok - ${name}`);
|
|
console.error(err);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
function withMockedDate(iso, fn) {
|
|
const RealDate = Date;
|
|
let now = new RealDate(iso).getTime();
|
|
|
|
class MockDate extends RealDate {
|
|
constructor(...args) {
|
|
if (args.length === 0) {
|
|
super(now);
|
|
} else {
|
|
super(...args);
|
|
}
|
|
}
|
|
|
|
static now() {
|
|
return now;
|
|
}
|
|
}
|
|
|
|
global.Date = MockDate;
|
|
try {
|
|
return fn({
|
|
advance(ms) {
|
|
now += ms;
|
|
}
|
|
});
|
|
} finally {
|
|
global.Date = RealDate;
|
|
}
|
|
}
|
|
|
|
function buildConfig(overrides = {}) {
|
|
return {
|
|
general: {
|
|
name: 'Monster Test',
|
|
logging: { enabled: false, logLevel: 'error' }
|
|
},
|
|
asset: {
|
|
emptyWeightBucket: 3
|
|
},
|
|
constraints: {
|
|
samplingtime: 1,
|
|
minVolume: 5,
|
|
maxWeight: 23,
|
|
nominalFlowMin: 1,
|
|
flowMax: 10
|
|
},
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
function parseMonsternametijdenCsv(filePath) {
|
|
const raw = fs.readFileSync(filePath, 'utf8').trim();
|
|
const lines = raw.split(/\r?\n/);
|
|
const header = lines.shift();
|
|
const columns = header.split(',');
|
|
|
|
return lines
|
|
.filter((line) => line && !line.startsWith('-----------'))
|
|
.map((line) => {
|
|
const parts = [];
|
|
let cur = '';
|
|
let inQ = false;
|
|
for (let i = 0; i < line.length; i++) {
|
|
const ch = line[i];
|
|
if (ch === '"') {
|
|
inQ = !inQ;
|
|
continue;
|
|
}
|
|
if (ch === ',' && !inQ) {
|
|
parts.push(cur);
|
|
cur = '';
|
|
} else {
|
|
cur += ch;
|
|
}
|
|
}
|
|
parts.push(cur);
|
|
const obj = {};
|
|
columns.forEach((col, idx) => {
|
|
obj[col] = parts[idx];
|
|
});
|
|
return obj;
|
|
});
|
|
}
|
|
|
|
test('measured + manual flow averages into effective flow', () => {
|
|
withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => {
|
|
const monster = new Monster(buildConfig());
|
|
|
|
const child = {
|
|
config: {
|
|
general: { id: 'child-1', name: 'FlowSensor' },
|
|
asset: { type: 'flow' }
|
|
},
|
|
measurements: new MeasurementContainer({
|
|
autoConvert: true,
|
|
defaultUnits: { flow: 'm3/h' }
|
|
})
|
|
};
|
|
|
|
monster.registerChild(child, 'measurement');
|
|
|
|
child.measurements
|
|
.type('flow')
|
|
.variant('measured')
|
|
.position('downstream')
|
|
.value(60, Date.now(), 'm3/h');
|
|
|
|
monster.handleInput('input_q', { value: 20, unit: 'm3/h' });
|
|
|
|
advance(1000);
|
|
monster.tick();
|
|
|
|
assert.strictEqual(monster.q, 40);
|
|
});
|
|
});
|
|
|
|
test('invalid flow bounds prevent sampling start', () => {
|
|
const monster = new Monster(buildConfig({
|
|
constraints: {
|
|
samplingtime: 1,
|
|
minVolume: 5,
|
|
maxWeight: 23,
|
|
nominalFlowMin: 10,
|
|
flowMax: 5
|
|
}
|
|
}));
|
|
|
|
monster.handleInput('i_start', true);
|
|
monster.sampling_program();
|
|
|
|
assert.strictEqual(monster.invalidFlowBounds, true);
|
|
assert.strictEqual(monster.running, false);
|
|
assert.strictEqual(monster.i_start, false);
|
|
});
|
|
|
|
test('flowCalc uses elapsed time to compute m3PerTick', () => {
|
|
withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => {
|
|
const monster = new Monster(buildConfig());
|
|
monster.q = 36; // m3/h
|
|
|
|
monster.flowCalc();
|
|
assert.strictEqual(monster.m3PerTick, 0);
|
|
|
|
advance(10000);
|
|
monster.flowCalc();
|
|
|
|
const expected = 0.1; // 36 m3/h -> 0.01 m3/s over 10s
|
|
assert.ok(Math.abs(monster.m3PerTick - expected) < 1e-6);
|
|
});
|
|
});
|
|
|
|
test('prediction fallback uses nominalFlowMin * sampling_time when rain is stale', () => {
|
|
const monster = new Monster(buildConfig());
|
|
monster.nominalFlowMin = 4;
|
|
monster.flowMax = 10;
|
|
monster.rainMaxRef = 8;
|
|
monster.sampling_time = 24;
|
|
monster.lastRainUpdate = 0;
|
|
const pred = monster.get_model_prediction();
|
|
assert.strictEqual(pred, 96);
|
|
});
|
|
|
|
test('pulses increment when running with manual flow and zero nominalFlowMin', () => {
|
|
withMockedDate('2024-10-15T00:00:00Z', ({ advance }) => {
|
|
const monster = new Monster(buildConfig({
|
|
constraints: {
|
|
samplingtime: 1,
|
|
minVolume: 5,
|
|
maxWeight: 23,
|
|
nominalFlowMin: 0,
|
|
flowMax: 6000,
|
|
minSampleIntervalSec: 60,
|
|
maxRainRef: 10
|
|
}
|
|
}));
|
|
|
|
monster.handleInput('input_q', { value: 200, unit: 'm3/h' });
|
|
monster.handleInput('i_start', true);
|
|
|
|
for (let i = 0; i < 80; i++) {
|
|
advance(1000);
|
|
monster.tick();
|
|
}
|
|
|
|
assert.ok(monster.sumPuls > 0);
|
|
assert.ok(monster.bucketVol > 0);
|
|
assert.ok(monster.missedSamples > 0);
|
|
assert.ok(monster.getSampleCooldownMs() > 0);
|
|
});
|
|
});
|
|
|
|
test('rain data aggregation produces totals', () => {
|
|
const monster = new Monster(buildConfig());
|
|
const rainPath = path.join(__dirname, 'seed_data', 'raindataFormat.json');
|
|
const rainData = JSON.parse(fs.readFileSync(rainPath, 'utf8'));
|
|
|
|
monster.updateRainData(rainData);
|
|
|
|
assert.ok(Object.keys(monster.aggregatedOutput).length > 0);
|
|
assert.ok(monster.sumRain >= 0);
|
|
assert.ok(monster.avgRain >= 0);
|
|
});
|
|
|
|
test('monsternametijden schedule sets next date', () => {
|
|
withMockedDate('2024-10-15T00:00:00Z', () => {
|
|
const monster = new Monster(buildConfig());
|
|
const csvPath = path.join(__dirname, 'seed_data', 'monsternametijden.csv');
|
|
const rows = parseMonsternametijdenCsv(csvPath);
|
|
|
|
monster.aquonSampleName = '112100';
|
|
monster.updateMonsternametijden(rows);
|
|
|
|
const nextDate = monster.nextDate instanceof Date
|
|
? monster.nextDate.getTime()
|
|
: Number(monster.nextDate);
|
|
|
|
assert.ok(Number.isFinite(nextDate));
|
|
assert.ok(nextDate > Date.now());
|
|
});
|
|
});
|
|
|
|
test('output includes pulse and flow fields', () => {
|
|
const monster = new Monster(buildConfig());
|
|
const output = monster.getOutput();
|
|
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulse'));
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'q'));
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'm3PerPuls'));
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'm3PerPulse'));
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulsesRemaining'));
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'pulseFraction'));
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'flowToNextPulseM3'));
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'timeToNextPulseSec'));
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetVolumeM3'));
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetProgressPct'));
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetDeltaL'));
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'targetDeltaM3'));
|
|
assert.ok(Object.prototype.hasOwnProperty.call(output, 'predictedRateM3h'));
|
|
});
|