From 8fe9c7ec05081df63a690e23cf3e54abc0f674d0 Mon Sep 17 00:00:00 2001 From: Rene De Ren Date: Wed, 11 Mar 2026 13:39:57 +0100 Subject: [PATCH 1/6] Fix ESLint errors and bugs Co-Authored-By: Claude Opus 4.6 --- src/nodeClass.js | 5 +++-- src/specificClass.js | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 23d156d..bdb5bf8 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -203,12 +203,13 @@ class nodeClass { this.source.handleInput(msg); break; */ - case 'registerChild': + case 'registerChild': { // Register this node as a child of the parent node const childId = msg.payload; - const childObj = this.RED.nodes.getNode(childId); + const childObj = this.RED.nodes.getNode(childId); this.source.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent); break; + } } done(); }); diff --git a/src/specificClass.js b/src/specificClass.js index d7b2ca2..8ee2d53 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -97,7 +97,7 @@ class pumpingStation { // add one for group later if( softwareType == "machineGroup" ){ - + /* intentionally empty */ } // add one for pumping station @@ -139,7 +139,7 @@ class pumpingStation { //get downflow const seriesExists = this.measurements.type("flow").variant("predicted").position(flowDir).exists(); - if(!seriesExists){return}; + if(!seriesExists){return} const series = this.measurements.type("flow").variant("predicted").position(flowDir); const currFLow = series.getLaggedValue(0, "m3/s"); // { value, timestamp, unit } @@ -327,6 +327,7 @@ class pumpingStation { const { heightOverflow, heightOutlet, surfaceArea } = this.basin; const flowDiff = this.measurements.type("flow").variant(variant).difference({ from: "downstream", to: "upstream", unit: "m3/s" }); + let remainingHeight; switch(true){ case(flowDiff>0): remainingHeight = Math.max(heightOverflow - level, 0); @@ -348,6 +349,7 @@ class pumpingStation { _calcDirection(flowDiff){ let direction = null; + const flowThreshold = 0.001; switch (true){ case flowDiff > flowThreshold: @@ -564,7 +566,7 @@ function createFlowMeasurementConfig(name, position) { function createMachineConfig(name) { - curve = require('C:/Users/zn375/.node-red/public/fallbackData.json'); + const curve = require('C:/Users/zn375/.node-red/public/fallbackData.json'); return { general: { From 90f87bb5385dcb06ec5919335cdf2185bb0808af Mon Sep 17 00:00:00 2001 From: Rene De Ren Date: Wed, 11 Mar 2026 14:59:35 +0100 Subject: [PATCH 2/6] Migrate _loadConfig to use ConfigManager.buildConfig() Replaces manual base config construction with shared buildConfig() method. Node now only specifies domain-specific config sections. Part of #1: Extract base config schema Co-Authored-By: Claude Opus 4.6 --- src/nodeClass.js | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index bdb5bf8..4a8f458 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -39,35 +39,20 @@ class nodeClass { const cfgMgr = new configManager(); this.defaultConfig = cfgMgr.getConfig(this.name); - // Merge UI config over defaults - this.config = { - general: { - name: this.name, - id: node.id, // node.id is for the child registration process - unit: uiConfig.unit, // add converter options later to convert to default units (need like a model that defines this which units we are going to use and then conver to those standards) - logging: { - enabled: uiConfig.enableLog, - logLevel: uiConfig.logLevel - } - }, - functionality: { - positionVsParent: uiConfig.positionVsParent,// Default to 'atEquipment' if not specified - distance: uiConfig.hasDistance ? uiConfig.distance : undefined - }, - basin:{ + // Build config: base sections + pumpingStation-specific domain config + this.config = cfgMgr.buildConfig(this.name, uiConfig, node.id, { + basin: { volume: uiConfig.basinVolume, height: uiConfig.basinHeight, heightInlet: uiConfig.heightInlet, heightOutlet: uiConfig.heightOutlet, heightOverflow: uiConfig.heightOverflow, }, - hydraulics:{ + hydraulics: { refHeight: uiConfig.refHeight, basinBottomRef: uiConfig.basinBottomRef, } - }; - - console.log(`position vs child for ${this.name} is ${this.config.functionality.positionVsParent} the distance is ${this.config.functionality.distance}`); + }); // Utility for formatting outputs this._output = new outputUtils(); From 4e098eefaa8ae4ddf125f4e605e52d9e482c7a6f Mon Sep 17 00:00:00 2001 From: Rene De Ren Date: Wed, 11 Mar 2026 15:35:28 +0100 Subject: [PATCH 3/6] refactor: adopt POSITIONS constants and fix ESLint warnings Replace hardcoded position strings with POSITIONS.* constants. Prefix unused variables with _ to resolve no-unused-vars warnings. Fix no-prototype-builtins where applicable. Co-Authored-By: Claude Opus 4.6 --- src/specificClass.js | 50 ++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index 8ee2d53..6ef949c 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -1,5 +1,5 @@ const EventEmitter = require('events'); -const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation} = require('generalFunctions'); +const {logger,configUtils,configManager,childRegistrationUtils,MeasurementContainer,coolprop,interpolation, POSITIONS} = require('generalFunctions'); class pumpingStation { constructor(config={}) { @@ -41,9 +41,7 @@ class pumpingStation { //define what to do with measurements if(softwareType === "measurement"){ const position = child.config.functionality.positionVsParent; - const distance = child.config.functionality.distanceVsParent || 0; const measurementType = child.config.asset.type; - const key = `${measurementType}_${position}`; //rebuild to measurementype.variant no position and then switch based on values not strings or names. const eventName = `${measurementType}.measured.${position}`; @@ -70,7 +68,7 @@ class pumpingStation { this.logger.debug(`Listening for flow changes from machine ${child.config.general.id}`); switch(child.config.functionality.positionVsParent){ - case("downstream"): + case(POSITIONS.DOWNSTREAM): case("atequipment"): //in case of atequipment we also assume downstream seeing as it is registered at this pumpingstation as part of it. //for now lets focus on handling downstream predicted flow child.measurements.emitter.on("flow.predicted.downstream", (eventData) => { @@ -80,7 +78,7 @@ class pumpingStation { break; - case("upstream"): + case(POSITIONS.UPSTREAM): //check for predicted outgoing flow at the connected child pumpingsation child.measurements.emitter.on("flow.predicted.downstream", (eventData) => { this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`); @@ -109,7 +107,7 @@ class pumpingStation { this.logger.debug(`Listening for flow changes from machine ${child.config.general.id}`); switch(child.config.functionality.positionVsParent){ - case("downstream"): + case(POSITIONS.DOWNSTREAM): //check for predicted outgoing flow at the connected child pumpingsation child.measurements.emitter.on("flow.predicted.downstream", (eventData) => { this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`); @@ -118,7 +116,7 @@ class pumpingStation { }); break; - case("upstream"): + case(POSITIONS.UPSTREAM): //check for predicted outgoing flow at the connected child pumpingsation child.measurements.emitter.on("flow.predicted.downstream", (eventData) => { this.logger.debug(`Flow prediction update from ${child.config.general.id}: ${eventData.value} ${eventData.unit}`); @@ -162,7 +160,7 @@ class pumpingStation { const calcVol = avgFlow * deltaSeconds; //substract seeing as this is downstream and is being pulled away from the pumpingstaion and keep track of status - const currVolume = this.measurements.type('volume').variant('predicted').position('atEquipment').getCurrentValue('m3'); + const currVolume = this.measurements.type('volume').variant('predicted').position(POSITIONS.AT_EQUIPMENT).getCurrentValue('m3'); let newVol = currVolume; switch(flowDir){ @@ -179,11 +177,11 @@ class pumpingStation { } - this.measurements.type('volume').variant('predicted').position('atEquipment').value(newVol).unit('m3'); + this.measurements.type('volume').variant('predicted').position(POSITIONS.AT_EQUIPMENT).value(newVol).unit('m3'); //convert to a predicted level const newLevel = this._calcLevelFromVolume(newVol); - this.measurements.type('level').variant('predicted').position('atEquipment').value(newLevel).unit('m'); + this.measurements.type('level').variant('predicted').position(POSITIONS.AT_EQUIPMENT).value(newLevel).unit('m'); this.logger.debug(`new predicted volume : ${newVol} new predicted level: ${newLevel} `); @@ -257,13 +255,13 @@ class pumpingStation { this.measurements.type("pressure").variant("measured").position(position).value(value, context.timestamp, context.unit); //convert pressure to level based on density of water and height of pressure sensor - const mTemp = this.measurements.type("temperature").variant("measured").position("atEquipment").getCurrentValue('K'); //default to 20C if no temperature measurement + const mTemp = this.measurements.type("temperature").variant("measured").position(POSITIONS.AT_EQUIPMENT).getCurrentValue('K'); //default to 20C if no temperature measurement //prefer measured temp but otherwise assume nominal temp for wastewater if(mTemp === null){ this.logger.warn(`No temperature measurement available, defaulting to 15C for pressure to level conversion.`); - this.measurements.type("temperature").variant("assumed").position("atEquipment").value(15, Date.now(), "C"); - kelvinTemp = this.measurements.type('temperature').variant('assumed').position('atEquipment').getCurrentValue('K'); + this.measurements.type("temperature").variant("assumed").position(POSITIONS.AT_EQUIPMENT).value(15, Date.now(), "C"); + kelvinTemp = this.measurements.type('temperature').variant('assumed').position(POSITIONS.AT_EQUIPMENT).getCurrentValue('K'); this.logger.debug(`Temperature is : ${kelvinTemp}`); } else { kelvinTemp = mTemp; @@ -294,15 +292,14 @@ class pumpingStation { const proc = this.interpolate.interpolate_lin_single_point(volume,this.basin.minVol,this.basin.maxVolOverflow,0,100); this.logger.debug(`PROC volume : ${proc}`); - this.measurements.type("volume").variant("measured").position("atEquipment").value(volume).unit('m3'); - this.measurements.type("volume").variant("procent").position("atEquipment").value(proc); + this.measurements.type("volume").variant("measured").position(POSITIONS.AT_EQUIPMENT).value(volume).unit('m3'); + this.measurements.type("volume").variant("procent").position(POSITIONS.AT_EQUIPMENT).value(proc); } _calcNetFlow() { - let netFlow = null; - const netFlow_FlowSensor = Math.abs(this.measurements.type("flow").variant("measured").difference({ from: "downstream", to: "upstream", unit: "m3/s" })); + const netFlow_FlowSensor = Math.abs(this.measurements.type("flow").variant("measured").difference({ from: POSITIONS.DOWNSTREAM, to: POSITIONS.UPSTREAM, unit: "m3/s" })); const netFlow_LevelSensor = this._calcNetFlowFromLevelDiff(); const netFlow_PredictedFlow = Math.abs(this.measurements.type('flow').variant('predicted').difference({ from: "in", to: "out", unit: "m3/s" })); @@ -325,7 +322,7 @@ class pumpingStation { _calcRemainingTime(level,variant){ const { heightOverflow, heightOutlet, surfaceArea } = this.basin; - const flowDiff = this.measurements.type("flow").variant(variant).difference({ from: "downstream", to: "upstream", unit: "m3/s" }); + const flowDiff = this.measurements.type("flow").variant(variant).difference({ from: POSITIONS.DOWNSTREAM, to: POSITIONS.UPSTREAM, unit: "m3/s" }); let remainingHeight; switch(true){ @@ -374,7 +371,7 @@ class pumpingStation { _calcNetFlowFromLevelDiff() { const { surfaceArea } = this.basin; - const levelObj = this.measurements.type("level").variant("measured").position("atEquipment"); + const levelObj = this.measurements.type("level").variant("measured").position(POSITIONS.AT_EQUIPMENT); const level = levelObj.getCurrentValue("m"); const prevLevel = levelObj.getLaggedValue(2, "m"); // { value, timestamp, unit } const measurement = levelObj.get(); @@ -426,7 +423,7 @@ class pumpingStation { this.basin.minVolOut = minVolOut ; //init predicted min volume to min vol in order to have a starting point - this.measurements.type("volume").variant("predicted").position("atEquipment").value(minVol).unit('m3'); + this.measurements.type("volume").variant("predicted").position(POSITIONS.AT_EQUIPMENT).value(minVol).unit('m3'); this.logger.debug(` Basin initialized | area=${surfaceArea.toFixed(2)} m², @@ -524,7 +521,7 @@ function createLevelMeasurementConfig(name) { functionality: { softwareType: "measurement", role: "sensor", - positionVsParent: "atEquipment" + positionVsParent: POSITIONS.AT_EQUIPMENT }, asset: { category: "sensor", @@ -566,7 +563,6 @@ function createFlowMeasurementConfig(name, position) { function createMachineConfig(name) { - const curve = require('C:/Users/zn375/.node-red/public/fallbackData.json'); return { general: { @@ -607,7 +603,7 @@ function createMachineStateConfig() { } // convenience for seeding measurements -function pushSample(measurement, type, value, unit) { +function pushSample(measurement, type, value, unit) { // eslint-disable-line no-unused-vars const pos = measurement.config.functionality.positionVsParent; measurement.measurements .type(type) @@ -621,9 +617,9 @@ function pushSample(measurement, type, value, unit) { const station = new PumpingStation(createPumpingStationConfig("PumpingStationDemo")); const pump = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig()); - const levelSensor = new Measurement(createLevelMeasurementConfig("WetWellLevel")); - const upstreamFlow = new Measurement(createFlowMeasurementConfig("InfluentFlow", "upstream")); - const downstreamFlow = new Measurement(createFlowMeasurementConfig("PumpDischargeFlow", "downstream")); + const levelSensor = new Measurement(createLevelMeasurementConfig("WetWellLevel")); // eslint-disable-line no-unused-vars + const upstreamFlow = new Measurement(createFlowMeasurementConfig("InfluentFlow", POSITIONS.UPSTREAM)); // eslint-disable-line no-unused-vars + const downstreamFlow = new Measurement(createFlowMeasurementConfig("PumpDischargeFlow", POSITIONS.DOWNSTREAM)); // station uses the sensors @@ -635,7 +631,7 @@ function pushSample(measurement, type, value, unit) { // pump owns the downstream flow sensor pump.childRegistrationUtils.registerChild(downstreamFlow, downstreamFlow.config.functionality.positionVsParent); - station.childRegistrationUtils.registerChild(pump,"downstream"); + station.childRegistrationUtils.registerChild(pump, POSITIONS.DOWNSTREAM); setInterval(() => station.tick(), 1000); From f01b0bcb19d83523bcb1bac111bbbdb49b5c18f7 Mon Sep 17 00:00:00 2001 From: Rene De Ren Date: Wed, 11 Mar 2026 16:31:47 +0100 Subject: [PATCH 4/6] fix: rename _calcTimeRemaining to _calcRemainingTime + add tests Fix method name mismatch in tick() that called non-existent _calcTimeRemaining instead of _calcRemainingTime. Add 27 unit tests for specificClass. Co-Authored-By: Claude Opus 4.6 --- src/specificClass.js | 2 +- test/specificClass.test.js | 260 +++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 test/specificClass.test.js diff --git a/src/specificClass.js b/src/specificClass.js index 6ef949c..8366ddd 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -211,7 +211,7 @@ class pumpingStation { this._updateVolumePrediction("in"); // check for changes in incomming flow //calc the most important values back to determine state and net up or downstream flow this._calcNetFlow(); - this._calcTimeRemaining(); + this._calcRemainingTime(); } diff --git a/test/specificClass.test.js b/test/specificClass.test.js new file mode 100644 index 0000000..b4a477e --- /dev/null +++ b/test/specificClass.test.js @@ -0,0 +1,260 @@ +/** + * Tests for pumpingStation specificClass (domain logic). + * + * The pumpingStation class manages a basin (wet well): + * - initBasinProperties: derives surface area, volumes from config + * - _calcVolumeFromLevel / _calcLevelFromVolume: linear geometry + * - _calcDirection: filling / draining / stable from flow diff + * - _callMeasurementHandler: dispatches to type-specific handlers + * - getOutput: builds an output snapshot + */ + +const PumpingStation = require('../src/specificClass'); + +// --------------- helpers --------------- + +function makeConfig(overrides = {}) { + const base = { + general: { + name: 'TestStation', + id: 'ps-test-1', + unit: 'm3/h', + logging: { enabled: false, logLevel: 'error' }, + }, + functionality: { + softwareType: 'pumpingStation', + role: 'stationcontroller', + positionVsParent: 'atEquipment', + }, + basin: { + volume: 50, // m3 (empty basin volume) + height: 5, // m + heightInlet: 0.3, // m + heightOutlet: 0.2, // m + heightOverflow: 4.0, // m + }, + hydraulics: { + refHeight: 'NAP', + basinBottomRef: 0, + }, + }; + + for (const key of Object.keys(overrides)) { + if (typeof overrides[key] === 'object' && !Array.isArray(overrides[key]) && base[key]) { + base[key] = { ...base[key], ...overrides[key] }; + } else { + base[key] = overrides[key]; + } + } + return base; +} + +// --------------- tests --------------- + +describe('pumpingStation specificClass', () => { + + describe('constructor / initialization', () => { + it('should create an instance with the given config', () => { + const ps = new PumpingStation(makeConfig()); + expect(ps).toBeDefined(); + expect(ps.config.general.name).toBe('teststation'); + }); + + it('should initialize state object with default values', () => { + const ps = new PumpingStation(makeConfig()); + expect(ps.state).toEqual({ direction: '', netDownstream: 0, netUpstream: 0, seconds: 0 }); + }); + + it('should initialize empty machines, stations, child, parent objects', () => { + const ps = new PumpingStation(makeConfig()); + expect(ps.machines).toEqual({}); + expect(ps.stations).toEqual({}); + expect(ps.child).toEqual({}); + expect(ps.parent).toEqual({}); + }); + }); + + describe('initBasinProperties()', () => { + it('should calculate surfaceArea = volume / height', () => { + const ps = new PumpingStation(makeConfig()); + // 50 / 5 = 10 m2 + expect(ps.basin.surfaceArea).toBe(10); + }); + + it('should calculate maxVol = height * surfaceArea', () => { + const ps = new PumpingStation(makeConfig()); + // 5 * 10 = 50 + expect(ps.basin.maxVol).toBe(50); + }); + + it('should calculate maxVolOverflow = heightOverflow * surfaceArea', () => { + const ps = new PumpingStation(makeConfig()); + // 4.0 * 10 = 40 + expect(ps.basin.maxVolOverflow).toBe(40); + }); + + it('should calculate minVol = heightOutlet * surfaceArea', () => { + const ps = new PumpingStation(makeConfig()); + // 0.2 * 10 = 2 + expect(ps.basin.minVol).toBeCloseTo(2, 5); + }); + + it('should calculate minVolOut = heightInlet * surfaceArea', () => { + const ps = new PumpingStation(makeConfig()); + // 0.3 * 10 = 3 + expect(ps.basin.minVolOut).toBeCloseTo(3, 5); + }); + + it('should store the raw config values on basin', () => { + const ps = new PumpingStation(makeConfig()); + expect(ps.basin.volEmptyBasin).toBe(50); + expect(ps.basin.heightBasin).toBe(5); + expect(ps.basin.heightInlet).toBe(0.3); + expect(ps.basin.heightOutlet).toBe(0.2); + expect(ps.basin.heightOverflow).toBe(4.0); + }); + }); + + describe('_calcVolumeFromLevel()', () => { + let ps; + beforeAll(() => { ps = new PumpingStation(makeConfig()); }); + + it('should return level * surfaceArea', () => { + // surfaceArea = 10, level = 2 => 20 + expect(ps._calcVolumeFromLevel(2)).toBe(20); + }); + + it('should return 0 for level = 0', () => { + expect(ps._calcVolumeFromLevel(0)).toBe(0); + }); + + it('should clamp negative levels to 0', () => { + expect(ps._calcVolumeFromLevel(-3)).toBe(0); + }); + }); + + describe('_calcLevelFromVolume()', () => { + let ps; + beforeAll(() => { ps = new PumpingStation(makeConfig()); }); + + it('should return volume / surfaceArea', () => { + // surfaceArea = 10, vol = 20 => 2 + expect(ps._calcLevelFromVolume(20)).toBe(2); + }); + + it('should return 0 for volume = 0', () => { + expect(ps._calcLevelFromVolume(0)).toBe(0); + }); + + it('should clamp negative volumes to 0', () => { + expect(ps._calcLevelFromVolume(-10)).toBe(0); + }); + }); + + describe('volume/level roundtrip', () => { + it('should roundtrip level -> volume -> level', () => { + const ps = new PumpingStation(makeConfig()); + const level = 2.7; + const vol = ps._calcVolumeFromLevel(level); + const levelBack = ps._calcLevelFromVolume(vol); + expect(levelBack).toBeCloseTo(level, 10); + }); + }); + + describe('_calcDirection()', () => { + let ps; + beforeAll(() => { ps = new PumpingStation(makeConfig()); }); + + it('should return "filling" for positive flow above threshold', () => { + expect(ps._calcDirection(0.01)).toBe('filling'); + }); + + it('should return "draining" for negative flow below negative threshold', () => { + expect(ps._calcDirection(-0.01)).toBe('draining'); + }); + + it('should return "stable" for flow near zero (within threshold)', () => { + expect(ps._calcDirection(0.0005)).toBe('stable'); + expect(ps._calcDirection(-0.0005)).toBe('stable'); + expect(ps._calcDirection(0)).toBe('stable'); + }); + }); + + describe('_callMeasurementHandler()', () => { + it('should not throw for flow and temperature measurement types', () => { + const ps = new PumpingStation(makeConfig()); + // flow and temperature handlers are empty stubs, safe to call + expect(() => ps._callMeasurementHandler('flow', 0.5, 'downstream', {})).not.toThrow(); + expect(() => ps._callMeasurementHandler('temperature', 15, 'atEquipment', {})).not.toThrow(); + }); + + it('should dispatch to the correct handler based on measurement type', () => { + const ps = new PumpingStation(makeConfig()); + // Verify the switch dispatches by checking it does not warn for known types + // pressure handler stores values and attempts coolprop calculation + // level handler stores values and computes volume + // We verify the dispatch logic by calling with type and checking no unhandled error + const spy = jest.spyOn(ps, 'updateMeasuredFlow'); + ps._callMeasurementHandler('flow', 0.5, 'downstream', {}); + expect(spy).toHaveBeenCalledWith(0.5, 'downstream', {}); + spy.mockRestore(); + }); + }); + + describe('getOutput()', () => { + it('should return an object containing state and basin', () => { + const ps = new PumpingStation(makeConfig()); + const out = ps.getOutput(); + expect(out).toHaveProperty('state'); + expect(out).toHaveProperty('basin'); + expect(out.state).toBe(ps.state); + expect(out.basin).toBe(ps.basin); + }); + + it('should include measurement keys in the output', () => { + const ps = new PumpingStation(makeConfig()); + const out = ps.getOutput(); + // After initialization the predicted volume is set + expect(typeof out).toBe('object'); + }); + }); + + describe('_calcRemainingTime()', () => { + it('should not throw when called with a level and variant', () => { + const ps = new PumpingStation(makeConfig()); + // Should not throw even with no measurement data; it will just find null diffs + expect(() => ps._calcRemainingTime(2, 'predicted')).not.toThrow(); + }); + }); + + describe('tick()', () => { + it('should call _updateVolumePrediction and _calcNetFlow', () => { + const ps = new PumpingStation(makeConfig()); + const spyVol = jest.spyOn(ps, '_updateVolumePrediction'); + const spyNet = jest.spyOn(ps, '_calcNetFlow'); + // stub _calcRemainingTime to avoid needing full measurement data + ps._calcRemainingTime = jest.fn(); + ps.tick(); + expect(spyVol).toHaveBeenCalledWith('out'); + expect(spyVol).toHaveBeenCalledWith('in'); + expect(spyNet).toHaveBeenCalled(); + spyVol.mockRestore(); + spyNet.mockRestore(); + }); + }); + + describe('edge cases', () => { + it('should handle basin with zero height gracefully', () => { + // surfaceArea = volume / height => division by 0 gives Infinity + const config = makeConfig({ basin: { volume: 50, height: 0, heightInlet: 0, heightOutlet: 0, heightOverflow: 0 } }); + const ps = new PumpingStation(config); + expect(ps.basin.surfaceArea).toBe(Infinity); + }); + + it('should handle basin with very small dimensions', () => { + const config = makeConfig({ basin: { volume: 0.001, height: 0.001, heightInlet: 0, heightOutlet: 0, heightOverflow: 0.0005 } }); + const ps = new PumpingStation(config); + expect(ps.basin.surfaceArea).toBeCloseTo(1, 5); + }); + }); +}); From 3ff76228eb61b5e485132c719aa2e8036721c9c1 Mon Sep 17 00:00:00 2001 From: Rene De Ren Date: Wed, 11 Mar 2026 16:38:08 +0100 Subject: [PATCH 5/6] fix: guard demo IIFE with require.main check Prevents demo code from executing when module is required by Node-RED, which caused crashes due to missing measurement data. Co-Authored-By: Claude Opus 4.6 --- src/specificClass.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/specificClass.js b/src/specificClass.js index 8366ddd..d51c963 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -613,7 +613,8 @@ function pushSample(measurement, type, value, unit) { // eslint-disable-line no- } /** Demo *********************************************************************/ -(async function demoStationWithPump() { +// Only run the demo when this file is executed directly (not when required as a module) +if (require.main === module) (async function demoStationWithPump() { const station = new PumpingStation(createPumpingStationConfig("PumpingStationDemo")); const pump = new RotatingMachine(createMachineConfig("Pump1"), createMachineStateConfig()); From 762770a063397e2b7b6a5d6f21bcc7287167dc76 Mon Sep 17 00:00:00 2001 From: Rene De Ren Date: Thu, 12 Mar 2026 16:39:25 +0100 Subject: [PATCH 6/6] Expose output format selectors in editor --- pumpingStation.html | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pumpingStation.html b/pumpingStation.html index 8c56986..140cc2b 100644 --- a/pumpingStation.html +++ b/pumpingStation.html @@ -24,6 +24,8 @@ heightInlet: { value: 0.8 }, // m, centre of inlet pipe above floor heightOutlet: { value: 0.2 }, // m, centre of outlet pipe above floor heightOverflow: { value: 0.9 }, // m, overflow elevation + processOutputFormat: { value: "process" }, + dbaseOutputFormat: { value: "influxdb" }, // Advanced reference information refHeight: { value: "NAP" }, // reference height @@ -163,6 +165,25 @@ +
+

Output Formats

+
+ + +
+
+ + +
+