'use strict'; const { BaseDomain, POSITIONS, statusBadge } = require('generalFunctions'); function cloneArray(values) { if (typeof structuredClone === 'function') return structuredClone(values); return Array.isArray(values) ? [...values] : values; } // Settler — secondary clarifier / sludge separator (Unit level). // Splits influent into effluent, surplus sludge and return sludge based // on a TSS mass balance. State updates come from an upstream reactor // (stateChange → pull `getEffluent`) or operator-supplied influent via // the `data.influent` command. The 3-port Fluent stream is produced by // `getEffluent` and pushed onto Port 0 by the nodeClass. class Settler extends BaseDomain { static name = 'settler'; configure() { this.upstreamReactor = null; this.returnPump = null; this.F_in = 0; this.Cs_in = new Array(13).fill(0); this.C_TS = 2500; this.router .onRegister('measurement', (child) => this._connectMeasurement(child)) .onRegister('reactor', (child) => this._connectReactor(child)) .onRegister('machine', (child) => this._connectMachine(child)); } // Three-stream output: effluent (inlet=0), surplus sludge (inlet=1), // return sludge (inlet=2). Downstream consumers (reactor inlets, // returnPump) read these by `payload.inlet`. F_s is clamped to F_in // to prevent negative effluent when X_TS_in/C_TS exceeds 1. get getEffluent() { 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; const Cs_eff = cloneArray(this.Cs_in); if (F_s > 0) for (let i = 7; i <= 12; i++) Cs_eff[i] = 0; const Cs_s = cloneArray(this.Cs_in); if (F_s > 0) for (let i = 7; i <= 12; i++) Cs_s[i] = this.F_in * this.Cs_in[i] / F_s; const ts = Date.now(); return [ { topic: 'Fluent', payload: { inlet: 0, F: F_eff, C: Cs_eff }, timestamp: ts }, { topic: 'Fluent', payload: { inlet: 1, F: F_so, C: Cs_s }, timestamp: ts }, { topic: 'Fluent', payload: { inlet: 2, F: F_sr, C: Cs_s }, timestamp: ts }, ]; } _connectMeasurement(measurementChild) { const position = measurementChild.config.functionality.positionVsParent; const measurementType = measurementChild.config.asset.type; const eventName = `${measurementType}.measured.${String(position).toLowerCase()}`; measurementChild.measurements.emitter.on(eventName, (eventData) => { this.logger.debug(`${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`); this.measurements .type(measurementType) .variant('measured') .position(position) .value(eventData.value, eventData.timestamp, eventData.unit); this._updateMeasurement(measurementType, eventData.value, position, eventData); }); } // Reactor → settler integration: the reactor pushes a `stateChange` event // on its own emitter (NOT measurements.emitter), so router.onMeasurement // can't subscribe — we wire the listener manually here, mirroring the // pre-refactor `_connectReactor`. The settler pulls `getEffluent` rather // than receiving it pushed; reactor.getEffluent may return an array or a // single envelope (the 2026-03-02 bug fix preserved both shapes). _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', () => { 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; this.notifyOutputChanged(); }); } _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; this.notifyOutputChanged(); return; default: this.logger.error(`Type '${measurementType}' not recognized for measured update.`); } } // Telemetry snapshot for Port 1 (InfluxDB). Port 0 carries the 3-message // Fluent stream directly; this scalar view feeds dashboards. getOutput() { const streams = this.getEffluent; return { ...this.measurements.getFlattenedOutput?.(), F_in: this.F_in, C_TS: this.C_TS, F_eff: streams[0].payload.F, F_surplus: streams[1].payload.F, F_return: streams[2].payload.F, }; } getStatusBadge() { if (this.F_in <= 0) return statusBadge.idle('no influent'); const streams = this.getEffluent; const eff = streams[0].payload.F.toFixed(2); const sur = streams[1].payload.F.toFixed(2); return statusBadge.compose([`F_in=${this.F_in.toFixed(2)}`, `eff=${eff}`, `surplus=${sur}`], { fill: 'green', shape: 'dot' }); } } module.exports = Settler; module.exports.Settler = Settler;