diff --git a/settler.html b/settler.html
index 24578fb..703a588 100644
--- a/settler.html
+++ b/settler.html
@@ -1,68 +1,88 @@
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
diff --git a/src/nodeClass.js b/src/nodeClass.js
index f0a8c11..c1c3526 100644
--- a/src/nodeClass.js
+++ b/src/nodeClass.js
@@ -1,33 +1,34 @@
-const { Settler } = require('./specificClass.js');
-
-
-class nodeClass {
- /**
- * Node-RED node class for settler.
- * @param {object} uiConfig - Node-RED node configuration
- * @param {object} RED - Node-RED runtime API
- * @param {object} nodeInstance - Node-RED node instance
- * @param {string} nameOfNode - Name of the node
- */
- constructor(uiConfig, RED, nodeInstance, nameOfNode) {
- // Preserve RED reference for HTTP endpoints if needed
- this.node = nodeInstance;
- this.RED = RED;
- this.name = nameOfNode;
- this.source = null;
-
- this._loadConfig(uiConfig)
- this._setupClass();
-
- this._attachInputHandler();
- this._registerChild();
- this._startTickLoop();
- this._attachCloseHandler();
- }
-
- /**
- * Handle node-red input messages
- */
+const { Settler } = require('./specificClass.js');
+const { configManager } = require('generalFunctions');
+
+
+class nodeClass {
+ /**
+ * Node-RED node class for settler.
+ * @param {object} uiConfig - Node-RED node configuration
+ * @param {object} RED - Node-RED runtime API
+ * @param {object} nodeInstance - Node-RED node instance
+ * @param {string} nameOfNode - Name of the node
+ */
+ constructor(uiConfig, RED, nodeInstance, nameOfNode) {
+ // Preserve RED reference for HTTP endpoints if needed
+ this.node = nodeInstance;
+ this.RED = RED;
+ this.name = nameOfNode;
+ this.source = null;
+
+ this._loadConfig(uiConfig)
+ this._setupClass();
+
+ this._attachInputHandler();
+ this._registerChild();
+ this._startTickLoop();
+ this._attachCloseHandler();
+ }
+
+ /**
+ * Handle node-red input messages
+ */
_attachInputHandler() {
this.node.on('input', (msg, send, done) => {
try {
@@ -54,62 +55,49 @@ class nodeClass {
}
});
}
-
- /**
- * Parse node configuration
- * @param {object} uiConfig Config set in UI in node-red
- */
- _loadConfig(uiConfig) {
- this.config = {
- general: {
- name: uiConfig.name || this.name,
- id: this.node.id,
- unit: null,
- logging: {
- enabled: uiConfig.enableLog,
- logLevel: uiConfig.logLevel
- }
- },
- functionality: {
- positionVsParent: uiConfig.positionVsParent || 'atEquipment', // Default to 'atEquipment' if not specified
- softwareType: "settler" // should be set in config manager
- }
- }
- }
-
- /**
- * Register this node as a child upstream and downstream.
- * Delayed to avoid Node-RED startup race conditions.
- */
- _registerChild() {
- setTimeout(() => {
- this.node.send([
- null,
- null,
- { topic: 'registerChild', payload: this.node.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' }
- ]);
- }, 100);
- }
-
- /**
- * Setup settler class
- */
- _setupClass() {
-
- this.source = new Settler(this.config); // protect from reassignment
- this.node.source = this.source;
- }
-
- _startTickLoop() {
- setTimeout(() => {
- this._tickInterval = setInterval(() => this._tick(), 1000);
- }, 1000);
- }
-
- _tick(){
- this.node.send([this.source.getEffluent, null, null]);
- }
-
+
+ /**
+ * Parse node configuration
+ * @param {object} uiConfig Config set in UI in node-red
+ */
+ _loadConfig(uiConfig) {
+ const cfgMgr = new configManager();
+ this.config = cfgMgr.buildConfig('settler', uiConfig, this.node.id);
+ }
+
+ /**
+ * Register this node as a child upstream and downstream.
+ * Delayed to avoid Node-RED startup race conditions.
+ */
+ _registerChild() {
+ setTimeout(() => {
+ this.node.send([
+ null,
+ null,
+ { topic: 'registerChild', payload: this.node.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' }
+ ]);
+ }, 100);
+ }
+
+ /**
+ * Setup settler class
+ */
+ _setupClass() {
+
+ this.source = new Settler(this.config); // protect from reassignment
+ this.node.source = this.source;
+ }
+
+ _startTickLoop() {
+ setTimeout(() => {
+ this._tickInterval = setInterval(() => this._tick(), 1000);
+ }, 1000);
+ }
+
+ _tick(){
+ this.node.send([this.source.getEffluent, null, null]);
+ }
+
_attachCloseHandler() {
this.node.on('close', (done) => {
clearInterval(this._tickInterval);
@@ -117,5 +105,5 @@ class nodeClass {
});
}
}
-
+
module.exports = nodeClass;
diff --git a/src/specificClass.js b/src/specificClass.js
index 3f2627c..6886946 100644
--- a/src/specificClass.js
+++ b/src/specificClass.js
@@ -1,157 +1,157 @@
-const { childRegistrationUtils, logger, MeasurementContainer } = require('generalFunctions');
-const EventEmitter = require('events');
-
-// Compatibility-safe array clone for Node runtimes without global structuredClone.
-function cloneArray(values) {
- if (typeof structuredClone === 'function') {
- return structuredClone(values);
- }
- return Array.isArray(values) ? [...values] : values;
-}
-
-/**
- * Settler domain model.
- * Splits influent into effluent, sludge and return sludge based on solids balance.
- */
-class Settler {
- constructor(config) {
- this.config = config;
- // EVOLV stuff
- this.logger = new logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, config.general.name);
- this.emitter = new EventEmitter();
- this.measurements = new MeasurementContainer();
- this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
-
- this.upstreamReactor = null;
- this.returnPump = null;
-
- // state variables
- this.F_in = 0; // debit in
- this.Cs_in = new Array(13).fill(0); // Concentrations in
- this.C_TS = 2500; // Total solids concentration sludge
- }
-
- get getEffluent() {
- // constrain flow to prevent negatives
- const F_s = Math.min((this.F_in * this.Cs_in[12]) / this.C_TS, this.F_in);
- const F_eff = this.F_in - F_s;
-
- let F_sr = 0;
- if (this.returnPump) {
- F_sr = Math.min(this.returnPump.measurements.type("flow").variant("measured").position("atEquipment").getCurrentValue(), F_s);
- }
- const F_so = F_s - F_sr;
-
- // effluent
- const Cs_eff = cloneArray(this.Cs_in);
- if (F_s > 0) {
- Cs_eff[7] = 0;
- Cs_eff[8] = 0;
- Cs_eff[9] = 0;
- Cs_eff[10] = 0;
- Cs_eff[11] = 0;
- Cs_eff[12] = 0;
- }
-
- // sludge
- const Cs_s = cloneArray(this.Cs_in);
- if (F_s > 0) {
- Cs_s[7] = this.F_in * this.Cs_in[7] / F_s;
- Cs_s[8] = this.F_in * this.Cs_in[8] / F_s;
- Cs_s[9] = this.F_in * this.Cs_in[9] / F_s;
- Cs_s[10] = this.F_in * this.Cs_in[10] / F_s;
- Cs_s[11] = this.F_in * this.Cs_in[11] / F_s;
- Cs_s[12] = this.F_in * this.Cs_in[12] / F_s;
- }
-
- return [
- { topic: "Fluent", payload: { inlet: 0, F: F_eff, C: Cs_eff }, timestamp: Date.now() },
- { topic: "Fluent", payload: { inlet: 1, F: F_so, C: Cs_s }, timestamp: Date.now() },
- { topic: "Fluent", payload: { inlet: 2, F: F_sr, C: Cs_s }, timestamp: Date.now() }
- ];
- }
-
- registerChild(child, softwareType) {
- if(!child) {
- this.logger.error(`Invalid ${softwareType} child provided.`);
- return;
- }
-
- switch (softwareType) {
- case "measurement":
- this.logger.debug(`Registering measurement child...`);
- this._connectMeasurement(child);
- break;
- case "reactor":
- this.logger.debug(`Registering reactor child...`);
- this._connectReactor(child);
- break;
- case "machine":
- this.logger.debug(`Registering machine child...`);
- this._connectMachine(child);
- break;
-
- default:
- this.logger.error(`Unrecognized softwareType: ${softwareType}`);
- }
- }
-
- _connectMeasurement(measurementChild) {
- const position = measurementChild.config.functionality.positionVsParent;
- const measurementType = measurementChild.config.asset.type;
- const eventName = `${measurementType}.measured.${position}`;
-
- // Register event listener for measurement updates
- measurementChild.measurements.emitter.on(eventName, (eventData) => {
- this.logger.debug(`${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
-
- // Store directly in parent's measurement container
- this.measurements
- .type(measurementType)
- .variant("measured")
- .position(position)
- .value(eventData.value, eventData.timestamp, eventData.unit);
-
- this._updateMeasurement(measurementType, eventData.value, position, eventData);
- });
- }
-
- _connectReactor(reactorChild) {
- if (reactorChild.config.functionality.positionVsParent != "upstream") {
- this.logger.warn("Reactor children of settlers should be upstream.");
- }
-
- this.upstreamReactor = reactorChild;
-
- reactorChild.emitter.on("stateChange", (eventData) => {
- this.logger.debug(`State change of upstream reactor detected.`);
- const raw = this.upstreamReactor.getEffluent;
- const effluent = Array.isArray(raw) ? raw[0] : raw;
- this.F_in = effluent.payload.F;
- this.Cs_in = effluent.payload.C;
- });
- }
-
- _connectMachine(machineChild) {
- if (machineChild.config.functionality.positionVsParent == "downstream") {
- machineChild.upstreamSource = this;
- this.returnPump = machineChild;
- return;
- }
- this.logger.warn(`Failed to register machine child.`);
- }
-
- _updateMeasurement(measurementType, value, position, context) {
- switch(measurementType) {
- case "quantity (tss)":
- this.C_TS = value;
- break;
-
- default:
- this.logger.error(`Type '${measurementType}' not recognized for measured update.`);
- return;
- }
- }
-}
-
-module.exports = { Settler };
+const { childRegistrationUtils, logger, MeasurementContainer, POSITIONS } = require('generalFunctions');
+const EventEmitter = require('events');
+
+// Compatibility-safe array clone for Node runtimes without global structuredClone.
+function cloneArray(values) {
+ if (typeof structuredClone === 'function') {
+ return structuredClone(values);
+ }
+ return Array.isArray(values) ? [...values] : values;
+}
+
+/**
+ * Settler domain model.
+ * Splits influent into effluent, sludge and return sludge based on solids balance.
+ */
+class Settler {
+ constructor(config) {
+ this.config = config;
+ // EVOLV stuff
+ this.logger = new logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, config.general.name);
+ this.emitter = new EventEmitter();
+ this.measurements = new MeasurementContainer();
+ this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
+
+ this.upstreamReactor = null;
+ this.returnPump = null;
+
+ // state variables
+ this.F_in = 0; // debit in
+ this.Cs_in = new Array(13).fill(0); // Concentrations in
+ this.C_TS = 2500; // Total solids concentration sludge
+ }
+
+ get getEffluent() {
+ // constrain flow to prevent negatives
+ const F_s = Math.min((this.F_in * this.Cs_in[12]) / this.C_TS, this.F_in);
+ const F_eff = this.F_in - F_s;
+
+ let F_sr = 0;
+ if (this.returnPump) {
+ F_sr = Math.min(this.returnPump.measurements.type("flow").variant("measured").position(POSITIONS.AT_EQUIPMENT).getCurrentValue(), F_s);
+ }
+ const F_so = F_s - F_sr;
+
+ // effluent
+ const Cs_eff = cloneArray(this.Cs_in);
+ if (F_s > 0) {
+ Cs_eff[7] = 0;
+ Cs_eff[8] = 0;
+ Cs_eff[9] = 0;
+ Cs_eff[10] = 0;
+ Cs_eff[11] = 0;
+ Cs_eff[12] = 0;
+ }
+
+ // sludge
+ const Cs_s = cloneArray(this.Cs_in);
+ if (F_s > 0) {
+ Cs_s[7] = this.F_in * this.Cs_in[7] / F_s;
+ Cs_s[8] = this.F_in * this.Cs_in[8] / F_s;
+ Cs_s[9] = this.F_in * this.Cs_in[9] / F_s;
+ Cs_s[10] = this.F_in * this.Cs_in[10] / F_s;
+ Cs_s[11] = this.F_in * this.Cs_in[11] / F_s;
+ Cs_s[12] = this.F_in * this.Cs_in[12] / F_s;
+ }
+
+ return [
+ { topic: "Fluent", payload: { inlet: 0, F: F_eff, C: Cs_eff }, timestamp: Date.now() },
+ { topic: "Fluent", payload: { inlet: 1, F: F_so, C: Cs_s }, timestamp: Date.now() },
+ { topic: "Fluent", payload: { inlet: 2, F: F_sr, C: Cs_s }, timestamp: Date.now() }
+ ];
+ }
+
+ registerChild(child, softwareType) {
+ if(!child) {
+ this.logger.error(`Invalid ${softwareType} child provided.`);
+ return;
+ }
+
+ switch (softwareType) {
+ case "measurement":
+ this.logger.debug(`Registering measurement child...`);
+ this._connectMeasurement(child);
+ break;
+ case "reactor":
+ this.logger.debug(`Registering reactor child...`);
+ this._connectReactor(child);
+ break;
+ case "machine":
+ this.logger.debug(`Registering machine child...`);
+ this._connectMachine(child);
+ break;
+
+ default:
+ this.logger.error(`Unrecognized softwareType: ${softwareType}`);
+ }
+ }
+
+ _connectMeasurement(measurementChild) {
+ const position = measurementChild.config.functionality.positionVsParent;
+ const measurementType = measurementChild.config.asset.type;
+ const eventName = `${measurementType}.measured.${position}`;
+
+ // Register event listener for measurement updates
+ measurementChild.measurements.emitter.on(eventName, (eventData) => {
+ this.logger.debug(`${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
+
+ // Store directly in parent's measurement container
+ this.measurements
+ .type(measurementType)
+ .variant("measured")
+ .position(position)
+ .value(eventData.value, eventData.timestamp, eventData.unit);
+
+ this._updateMeasurement(measurementType, eventData.value, position, eventData);
+ });
+ }
+
+ _connectReactor(reactorChild) {
+ if (reactorChild.config.functionality.positionVsParent != POSITIONS.UPSTREAM) {
+ this.logger.warn("Reactor children of settlers should be upstream.");
+ }
+
+ this.upstreamReactor = reactorChild;
+
+ reactorChild.emitter.on("stateChange", (_eventData) => {
+ this.logger.debug(`State change of upstream reactor detected.`);
+ const raw = this.upstreamReactor.getEffluent;
+ const effluent = Array.isArray(raw) ? raw[0] : raw;
+ this.F_in = effluent.payload.F;
+ this.Cs_in = effluent.payload.C;
+ });
+ }
+
+ _connectMachine(machineChild) {
+ if (machineChild.config.functionality.positionVsParent == POSITIONS.DOWNSTREAM) {
+ machineChild.upstreamSource = this;
+ this.returnPump = machineChild;
+ return;
+ }
+ this.logger.warn(`Failed to register machine child.`);
+ }
+
+ _updateMeasurement(measurementType, value, _position, _context) {
+ switch(measurementType) {
+ case "quantity (tss)":
+ this.C_TS = value;
+ break;
+
+ default:
+ this.logger.error(`Type '${measurementType}' not recognized for measured update.`);
+ return;
+ }
+ }
+}
+
+module.exports = { Settler };
diff --git a/test/specificClass.test.js b/test/specificClass.test.js
new file mode 100644
index 0000000..83e3e72
--- /dev/null
+++ b/test/specificClass.test.js
@@ -0,0 +1,263 @@
+/**
+ * Tests for settler specificClass (domain logic).
+ *
+ * The Settler class is a simple mass-balance separator:
+ * - Splits influent into effluent (clarified), surplus sludge, and return sludge
+ * - Concentrates particulate species (indices 7-12) into sludge stream
+ * - Removes particulates from effluent stream
+ * - registerChild: connects measurements, upstream reactors, machines
+ */
+
+const { Settler } = require('../src/specificClass');
+
+// --------------- helpers ---------------
+
+const NUM_SPECIES = 13;
+
+function makeConfig(overrides = {}) {
+ return {
+ general: {
+ name: 'TestSettler',
+ id: 'settler-test-1',
+ logging: { enabled: false, logLevel: 'error' },
+ },
+ functionality: {
+ softwareType: 'settler',
+ role: 'separator',
+ positionVsParent: 'downstream',
+ },
+ ...overrides,
+ };
+}
+
+// --------------- tests ---------------
+
+describe('Settler specificClass', () => {
+
+ describe('constructor / initialization', () => {
+ it('should create an instance with default values', () => {
+ const s = new Settler(makeConfig());
+ expect(s).toBeDefined();
+ expect(s.F_in).toBe(0);
+ expect(s.Cs_in).toEqual(new Array(NUM_SPECIES).fill(0));
+ expect(s.C_TS).toBe(2500);
+ });
+
+ it('should have null upstreamReactor and returnPump initially', () => {
+ const s = new Settler(makeConfig());
+ expect(s.upstreamReactor).toBeNull();
+ expect(s.returnPump).toBeNull();
+ });
+
+ it('should initialize an EventEmitter', () => {
+ const s = new Settler(makeConfig());
+ expect(s.emitter).toBeDefined();
+ expect(typeof s.emitter.on).toBe('function');
+ });
+
+ it('should initialize a MeasurementContainer', () => {
+ const s = new Settler(makeConfig());
+ expect(s.measurements).toBeDefined();
+ });
+ });
+
+ describe('getEffluent', () => {
+
+ describe('with zero inflow', () => {
+ it('should return three streams with zero flows', () => {
+ const s = new Settler(makeConfig());
+ s.F_in = 0;
+ s.Cs_in = new Array(NUM_SPECIES).fill(0);
+ const result = s.getEffluent;
+ expect(result).toHaveLength(3);
+ expect(result[0].payload.F).toBe(0); // effluent
+ expect(result[1].payload.F).toBe(0); // surplus sludge
+ expect(result[2].payload.F).toBe(0); // return sludge
+ });
+ });
+
+ describe('with normal inflow and particulates', () => {
+ let s;
+ let result;
+
+ beforeAll(() => {
+ s = new Settler(makeConfig());
+ s.F_in = 100;
+ s.C_TS = 5000;
+ // Set concentrations: solubles at indices 0-6, particulates at 7-12
+ const C = new Array(NUM_SPECIES).fill(10);
+ C[12] = 5000; // X_TS
+ s.Cs_in = C;
+ result = s.getEffluent;
+ });
+
+ it('should return 3 output streams', () => {
+ expect(result).toHaveLength(3);
+ });
+
+ it('should have effluent topic "Fluent"', () => {
+ expect(result[0].topic).toBe('Fluent');
+ });
+
+ it('should calculate sludge flow F_s = min(F_in * X_TS_in / C_TS, F_in)', () => {
+ // F_s = min(100 * 5000 / 5000, 100) = min(100, 100) = 100
+ const F_eff = result[0].payload.F;
+ const F_so = result[1].payload.F;
+ const F_sr = result[2].payload.F;
+ // Total out should equal F_in (mass balance)
+ expect(F_eff + F_so + F_sr).toBeCloseTo(100, 5);
+ });
+
+ it('should set effluent particulate indices to zero when F_s > 0', () => {
+ const Cs_eff = result[0].payload.C;
+ for (let i = 7; i <= 12; i++) {
+ expect(Cs_eff[i]).toBe(0);
+ }
+ });
+
+ it('should keep effluent soluble indices unchanged', () => {
+ const Cs_eff = result[0].payload.C;
+ for (let i = 0; i < 7; i++) {
+ expect(Cs_eff[i]).toBe(10);
+ }
+ });
+
+ it('should concentrate particulates in sludge stream', () => {
+ const Cs_s = result[1].payload.C;
+ const F_s = Math.min((s.F_in * s.Cs_in[12]) / s.C_TS, s.F_in);
+ // Cs_s[i] = F_in * Cs_in[i] / F_s for particulate indices
+ for (let i = 7; i <= 12; i++) {
+ const expected = s.F_in * s.Cs_in[i] / F_s;
+ expect(Cs_s[i]).toBeCloseTo(expected, 5);
+ }
+ });
+ });
+
+ describe('with low X_TS and high C_TS (dilute sludge)', () => {
+ it('should produce mostly effluent flow', () => {
+ const s = new Settler(makeConfig());
+ s.F_in = 100;
+ s.C_TS = 10000; // high target concentration
+ const C = new Array(NUM_SPECIES).fill(10);
+ C[12] = 100; // low X_TS in
+ s.Cs_in = C;
+ const result = s.getEffluent;
+ // F_s = min(100 * 100 / 10000, 100) = min(1, 100) = 1
+ expect(result[0].payload.F).toBeCloseTo(99, 5); // most flow is effluent
+ });
+ });
+
+ describe('mass balance', () => {
+ it('should conserve total flow (F_eff + F_so + F_sr = F_in)', () => {
+ const s = new Settler(makeConfig());
+ s.F_in = 200;
+ s.C_TS = 3000;
+ const C = new Array(NUM_SPECIES).fill(5);
+ C[12] = 2000;
+ s.Cs_in = C;
+ const result = s.getEffluent;
+ const totalOut = result[0].payload.F + result[1].payload.F + result[2].payload.F;
+ expect(totalOut).toBeCloseTo(200, 5);
+ });
+
+ it('should not produce negative flows', () => {
+ const s = new Settler(makeConfig());
+ s.F_in = 50;
+ s.C_TS = 1000;
+ const C = new Array(NUM_SPECIES).fill(0);
+ C[12] = 500;
+ s.Cs_in = C;
+ const result = s.getEffluent;
+ result.forEach(stream => {
+ expect(stream.payload.F).toBeGreaterThanOrEqual(0);
+ });
+ });
+ });
+
+ describe('no return pump', () => {
+ it('should have F_sr = 0 when there is no return pump', () => {
+ const s = new Settler(makeConfig());
+ s.F_in = 100;
+ s.C_TS = 5000;
+ s.Cs_in = new Array(NUM_SPECIES).fill(10);
+ s.Cs_in[12] = 3000;
+ const result = s.getEffluent;
+ expect(result[2].payload.F).toBe(0);
+ });
+ });
+
+ describe('edge case: X_TS > C_TS (F_s clamped to F_in)', () => {
+ it('should clamp F_s to F_in when X_TS/C_TS ratio exceeds 1', () => {
+ const s = new Settler(makeConfig());
+ s.F_in = 100;
+ s.C_TS = 1000;
+ s.Cs_in = new Array(NUM_SPECIES).fill(10);
+ s.Cs_in[12] = 5000; // X_TS_in > C_TS => F_s = min(500, 100) = 100
+ const result = s.getEffluent;
+ expect(result[0].payload.F).toBe(0); // all flow goes to sludge
+ });
+ });
+ });
+
+ describe('registerChild()', () => {
+ it('should not throw for null child', () => {
+ const s = new Settler(makeConfig());
+ // null child should trigger the error log but not crash
+ expect(() => s.registerChild(null, 'measurement')).not.toThrow();
+ });
+
+ it('should not throw for unknown software type with valid child', () => {
+ const s = new Settler(makeConfig());
+ const fakeChild = {
+ config: {
+ general: { name: 'fake', id: 'fake-1' },
+ functionality: { positionVsParent: 'upstream' },
+ asset: { type: 'pressure' },
+ },
+ measurements: { emitter: { on: jest.fn() } },
+ };
+ expect(() => s.registerChild(fakeChild, 'unknownType')).not.toThrow();
+ });
+ });
+
+ describe('_connectMachine()', () => {
+ it('should set returnPump for downstream machine', () => {
+ const s = new Settler(makeConfig());
+ const fakeMachine = {
+ config: {
+ general: { name: 'pump', id: 'pump-1' },
+ functionality: { positionVsParent: 'downstream' },
+ },
+ };
+ s._connectMachine(fakeMachine);
+ expect(s.returnPump).toBe(fakeMachine);
+ expect(fakeMachine.upstreamSource).toBe(s);
+ });
+
+ it('should not set returnPump for non-downstream machine', () => {
+ const s = new Settler(makeConfig());
+ const fakeMachine = {
+ config: {
+ general: { name: 'pump', id: 'pump-2' },
+ functionality: { positionVsParent: 'upstream' },
+ },
+ };
+ s._connectMachine(fakeMachine);
+ expect(s.returnPump).toBeNull();
+ });
+ });
+
+ describe('_updateMeasurement()', () => {
+ it('should update C_TS when measurement type is "quantity (tss)"', () => {
+ const s = new Settler(makeConfig());
+ s._updateMeasurement('quantity (tss)', 7000, 'atEquipment', {});
+ expect(s.C_TS).toBe(7000);
+ });
+
+ it('should not change C_TS for unrecognized measurement type', () => {
+ const s = new Settler(makeConfig());
+ s._updateMeasurement('temperature', 25, 'atEquipment', {});
+ expect(s.C_TS).toBe(2500); // unchanged
+ });
+ });
+});