2026-05-10 22:23:44 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
const { BaseDomain, POSITIONS, statusBadge } = require('generalFunctions');
|
2025-10-22 16:07:42 +02:00
|
|
|
|
2026-03-31 16:26:04 +02:00
|
|
|
function cloneArray(values) {
|
2026-05-10 22:23:44 +02:00
|
|
|
if (typeof structuredClone === 'function') return structuredClone(values);
|
2026-03-31 16:26:04 +02:00
|
|
|
return Array.isArray(values) ? [...values] : values;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 22:23:44 +02:00
|
|
|
// 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';
|
2025-10-23 17:15:41 +02:00
|
|
|
|
2026-05-10 22:23:44 +02:00
|
|
|
configure() {
|
2025-10-23 17:15:41 +02:00
|
|
|
this.upstreamReactor = null;
|
|
|
|
|
this.returnPump = null;
|
|
|
|
|
|
2026-05-10 22:23:44 +02:00
|
|
|
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));
|
2025-10-23 17:15:41 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 22:23:44 +02:00
|
|
|
// 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.
|
2025-10-23 17:15:41 +02:00
|
|
|
get getEffluent() {
|
2025-10-24 15:00:57 +02:00
|
|
|
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;
|
2026-03-31 16:26:04 +02:00
|
|
|
|
2025-10-24 15:00:57 +02:00
|
|
|
let F_sr = 0;
|
|
|
|
|
if (this.returnPump) {
|
2026-05-10 22:23:44 +02:00
|
|
|
F_sr = Math.min(
|
|
|
|
|
this.returnPump.measurements.type('flow').variant('measured').position(POSITIONS.AT_EQUIPMENT).getCurrentValue(),
|
|
|
|
|
F_s,
|
|
|
|
|
);
|
2025-10-24 15:00:57 +02:00
|
|
|
}
|
|
|
|
|
const F_so = F_s - F_sr;
|
|
|
|
|
|
2026-03-31 16:26:04 +02:00
|
|
|
const Cs_eff = cloneArray(this.Cs_in);
|
2026-05-10 22:23:44 +02:00
|
|
|
if (F_s > 0) for (let i = 7; i <= 12; i++) Cs_eff[i] = 0;
|
2026-03-31 16:26:04 +02:00
|
|
|
|
|
|
|
|
const Cs_s = cloneArray(this.Cs_in);
|
2026-05-10 22:23:44 +02:00
|
|
|
if (F_s > 0) for (let i = 7; i <= 12; i++) Cs_s[i] = this.F_in * this.Cs_in[i] / F_s;
|
2025-10-24 15:00:57 +02:00
|
|
|
|
2026-05-10 22:23:44 +02:00
|
|
|
const ts = Date.now();
|
2025-10-24 15:00:57 +02:00
|
|
|
return [
|
2026-05-10 22:23:44 +02:00
|
|
|
{ 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 },
|
2025-10-24 15:00:57 +02:00
|
|
|
];
|
2025-10-22 16:07:42 +02:00
|
|
|
}
|
2025-10-22 16:22:33 +02:00
|
|
|
|
2025-10-23 17:15:41 +02:00
|
|
|
_connectMeasurement(measurementChild) {
|
|
|
|
|
const position = measurementChild.config.functionality.positionVsParent;
|
|
|
|
|
const measurementType = measurementChild.config.asset.type;
|
2026-05-10 22:23:44 +02:00
|
|
|
const eventName = `${measurementType}.measured.${String(position).toLowerCase()}`;
|
2025-10-23 17:15:41 +02:00
|
|
|
|
|
|
|
|
measurementChild.measurements.emitter.on(eventName, (eventData) => {
|
|
|
|
|
this.logger.debug(`${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
|
|
|
|
|
this.measurements
|
|
|
|
|
.type(measurementType)
|
2026-05-10 22:23:44 +02:00
|
|
|
.variant('measured')
|
2025-10-23 17:15:41 +02:00
|
|
|
.position(position)
|
|
|
|
|
.value(eventData.value, eventData.timestamp, eventData.unit);
|
|
|
|
|
this._updateMeasurement(measurementType, eventData.value, position, eventData);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 22:23:44 +02:00
|
|
|
// 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).
|
2025-10-23 17:15:41 +02:00
|
|
|
_connectReactor(reactorChild) {
|
2026-05-10 22:23:44 +02:00
|
|
|
if (reactorChild.config.functionality.positionVsParent !== POSITIONS.UPSTREAM) {
|
|
|
|
|
this.logger.warn('Reactor children of settlers should be upstream.');
|
2025-10-23 17:15:41 +02:00
|
|
|
}
|
|
|
|
|
this.upstreamReactor = reactorChild;
|
|
|
|
|
|
2026-05-10 22:23:44 +02:00
|
|
|
reactorChild.emitter.on('stateChange', () => {
|
|
|
|
|
this.logger.debug('State change of upstream reactor detected.');
|
2026-03-31 16:26:04 +02:00
|
|
|
const raw = this.upstreamReactor.getEffluent;
|
|
|
|
|
const effluent = Array.isArray(raw) ? raw[0] : raw;
|
2025-10-23 17:15:41 +02:00
|
|
|
this.F_in = effluent.payload.F;
|
|
|
|
|
this.Cs_in = effluent.payload.C;
|
2026-05-10 22:23:44 +02:00
|
|
|
this.notifyOutputChanged();
|
2025-10-23 17:15:41 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_connectMachine(machineChild) {
|
2026-05-10 22:23:44 +02:00
|
|
|
if (machineChild.config.functionality.positionVsParent === POSITIONS.DOWNSTREAM) {
|
2025-11-06 14:52:41 +01:00
|
|
|
machineChild.upstreamSource = this;
|
2025-10-23 17:15:41 +02:00
|
|
|
this.returnPump = machineChild;
|
2025-11-06 14:52:41 +01:00
|
|
|
return;
|
2025-10-23 17:15:41 +02:00
|
|
|
}
|
2026-05-10 22:23:44 +02:00
|
|
|
this.logger.warn('Failed to register machine child.');
|
2025-10-23 17:15:41 +02:00
|
|
|
}
|
2025-10-24 15:00:57 +02:00
|
|
|
|
2026-05-10 22:23:44 +02:00
|
|
|
_updateMeasurement(measurementType, value /*, _position, _context */) {
|
|
|
|
|
switch (measurementType) {
|
|
|
|
|
case 'quantity (tss)':
|
2025-10-24 15:00:57 +02:00
|
|
|
this.C_TS = value;
|
2026-05-10 22:23:44 +02:00
|
|
|
this.notifyOutputChanged();
|
|
|
|
|
return;
|
2025-10-24 15:00:57 +02:00
|
|
|
default:
|
|
|
|
|
this.logger.error(`Type '${measurementType}' not recognized for measured update.`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-10 22:23:44 +02:00
|
|
|
|
|
|
|
|
// 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' });
|
|
|
|
|
}
|
2025-10-22 16:07:42 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 22:23:44 +02:00
|
|
|
module.exports = Settler;
|
|
|
|
|
module.exports.Settler = Settler;
|