Files
settler/src/specificClass.js
znetsixe b8247fc755 P6: convert settler to platform infrastructure
Refactor of settler to use BaseNodeAdapter + commandRegistry + statusBadge.
settler follows the platform refactor plan in .claude/refactor/MODULE_SPLIT.md.
Tests stay green; CONTRACT.md generated; legacy aliases preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:23:44 +02:00

147 lines
5.5 KiB
JavaScript

'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;