'use strict'; // valve — S88 Equipment Module domain orchestrator. Concern modules under // src/{fluid,curve,measurement,flow,state,io,commands} carry the bulk of // the logic; this file wires them together and preserves the public surface // the test suite + parents (VGC, MGC, pumpingStation) depend on. const { BaseDomain, UnitPolicy, state, assetResolver } = require('generalFunctions'); const { ValveHydraulicModel, normalizeServiceType } = require('./hydraulicModel'); const { FluidCompatibility, normalizeOptional } = require('./fluid/fluidCompatibility'); const { SupplierCurvePredictor } = require('./curve/supplierCurve'); const { MeasurementRouter } = require('./measurement/measurementRouter'); const FlowController = require('./flow/flowController'); const { bindStateEvents } = require('./state/stateBindings'); const io = require('./io/output'); class Valve extends BaseDomain { static name = 'valve'; static unitPolicy = UnitPolicy.declare({ canonical: { pressure: 'Pa', flow: 'm3/s', temperature: 'K' }, output: { pressure: 'mbar', flow: 'm3/h', temperature: 'C' }, requireUnitForTypes: ['pressure', 'flow', 'temperature'], }); constructor(valveConfig = {}, stateConfig = {}, runtimeOptions = {}) { Valve._pendingExtras = { stateConfig, runtimeOptions }; super(valveConfig); } configure() { const extras = Valve._pendingExtras || {}; Valve._pendingExtras = null; const stateConfig = extras.stateConfig || {}; const runtimeOptions = extras.runtimeOptions || {}; this._unitPolicyInstance = this.unitPolicy; this.unitPolicyView = this._freezeUnitView(this.unitPolicy); this.unitPolicy = this.unitPolicyView; this.config = this.configUtils.updateConfig(this.config, { general: { unit: this.unitPolicyView.output.flow }, asset: { ...this.config.asset, unit: this.unitPolicyView.output.flow }, }); this.child = {}; this.state = new state(stateConfig, this.logger); this.state.stateManager.currentState = 'operational'; this.kv = 0; this.currentMode = this.config.mode.current; const configuredServiceType = normalizeOptional(runtimeOptions.serviceType || this.config?.asset?.serviceType); this.expectedServiceType = configuredServiceType; this.serviceType = configuredServiceType || normalizeServiceType(runtimeOptions.serviceType || this.config?.asset?.serviceType); this.hydraulicModel = new ValveHydraulicModel( { serviceType: this.serviceType, gasChokedRatioLimit: runtimeOptions.gasChokedRatioLimit ?? this.config?.asset?.gasChokedRatioLimit }, this.logger ); this.rho = _positive(runtimeOptions.fluidDensity, this.config?.asset?.fluidDensity, this.hydraulicModel.defaultDensity); this.T = _positive(runtimeOptions.fluidTemperatureK, this.config?.asset?.fluidTemperatureK, this.hydraulicModel.defaultTemperatureK); this.fluid = new FluidCompatibility({ logger: this.logger, emitter: this.emitter, expectedServiceType: configuredServiceType, }); this.model = this.config.asset?.model; // Derived asset metadata (supplier, type, allowed units) — null if the // model isn't in the registry. Valve tolerates a null model + inline // configCurve, so we don't hard-fail here; the curve predictor logs. this.assetMetadata = this.model ? assetResolver.resolveAssetMetadata('valve', this.model) : null; this.curvePredictor = new SupplierCurvePredictor({ logger: this.logger, model: this.model, configCurve: this.config?.asset?.valveCurve, defaultDensity: this.hydraulicModel.defaultDensity, defaultTemperatureK: this.hydraulicModel.defaultTemperatureK, rho: this.rho, temperatureK: this.T, valveDiameter: this.config?.asset?.valveDiameter, }); this.rho = this.curvePredictor.rho; this.T = this.curvePredictor.T; this.curveSelection = this.curvePredictor.curveSelection; this.predictKv = this.curvePredictor.predictKv; this.curve = this.curvePredictor.curve; this.logger.info(`Using supplier curve model='${this.model || 'inline'}', densityCurve=${this.curveSelection.densityKey}, diameter=${this.curveSelection.diameterKey}, serviceType=${this.serviceType}`); this.measurementRouter = new MeasurementRouter(this); this.flowController = new FlowController(this); // BaseDomain pre-installs a `registerChild` that routes through // ChildRouter. Valve owns its own upstream-fluid tracking — override // here so the parent-side registration falls into FluidCompatibility. this.registerChild = (child, softwareType) => this.fluid.registerChild(child, softwareType); this._stateUnbind = bindStateEvents({ state: this.state, onPositionChange: () => this.updatePosition(), }); } _freezeUnitView(p) { const slot = (m, k) => (typeof p[m] === 'function' ? p[m](k) : p[m]?.[k]); return Object.freeze({ canonical: Object.freeze({ pressure: slot('canonical', 'pressure'), flow: slot('canonical', 'flow'), temperature: slot('canonical', 'temperature'), }), output: Object.freeze({ pressure: slot('output', 'pressure'), flow: slot('output', 'flow'), temperature: slot('output', 'temperature'), }), }); } // ── config + mode ────────────────────────────────────────────────── updateConfig(newConfig) { this.config = this.configUtils.updateConfig(this.config, newConfig); } setMode(newMode) { const available = Array.isArray(this.defaultConfig?.mode?.current?.rules?.values) ? this.defaultConfig.mode.current.rules.values.map((v) => v.value) : Object.keys(this.config?.mode?.allowedSources || {}); if (!available.includes(newMode)) { this.logger.warn(`Invalid mode '${newMode}'. Allowed modes are: ${available.join(', ')}`); return; } this.currentMode = newMode; this.logger.info(`Mode successfully changed to '${newMode}'.`); } isValidSourceForMode(source, mode) { return this.flowController.isValidSourceForMode(source, mode); } handleInput(source, action, parameter) { return this.flowController.handleInput(source, action, parameter); } executeSequence(name) { return this.flowController.executeSequence(name); } setpoint(value) { return this.flowController.setpoint(value); } // ── measurement helpers used by router + io ──────────────────────── _outputUnitForType(type) { switch (String(type || '').toLowerCase()) { case 'flow': return this.unitPolicyView.output.flow; case 'pressure': return this.unitPolicyView.output.pressure; case 'temperature': return this.unitPolicyView.output.temperature; default: return null; } } _readMeasurement(type, variant, position, unit) { const u = unit || this._outputUnitForType(type); return this.measurements.type(type).variant(variant).position(position).getCurrentValue(u || undefined); } _writeMeasurement(type, variant, position, value, unit, timestamp = Date.now()) { if (!Number.isFinite(value)) return; this.measurements.type(type).variant(variant).position(position).value(value, timestamp, unit || undefined); } updatePressure(variant, value, position, unit) { return this.measurementRouter.updatePressure(variant, value, position, unit); } updateFlow(variant, value, position, unit) { return this.measurementRouter.updateFlow(variant, value, position, unit); } updateMeasurement(variant, subType, value, position, unit) { return this.measurementRouter.updateMeasurement(variant, subType, value, position, unit); } updateDeltaPKlep(q, kv, downstreamP /*, rho, T */) { return this.measurementRouter.updateDeltaP(q, kv, downstreamP); } updatePosition() { return this.measurementRouter.updatePositionDependent(); } // ── fluid contract delegates ─────────────────────────────────────── getFluidCompatibility() { return this.fluid.getCompatibility(); } getFluidContract() { return this.fluid.getContract(); } // ── display + diagnostics ────────────────────────────────────────── showCurve() { return { model: this.model || null, serviceType: this.serviceType, expectedServiceType: this.expectedServiceType, gasChokedRatioLimit: this.hydraulicModel?.gasChokedRatioLimit, ...this.curvePredictor.snapshot(), hydraulics: this.hydraulicDiagnostics || null, }; } getOutput() { return io.buildOutput(this); } getStatusBadge() { return io.buildStatusBadge(this); } destroy() { this.close(); } close() { this._stateUnbind?.(); this.fluid?.destroy(); super.close?.(); } } function _positive(...candidates) { for (const c of candidates) { const n = Number(c); if (Number.isFinite(n) && n > 0) return n; } return undefined; } module.exports = Valve;