Mirrors the rotatingMachine cutover: assetResolver derives supplier/type/ units from the model id; nodeClass throws a clear "re-select model and save" error if the saved node still carries denormalized supplier/ category/assetType strings. valve.html defaults trimmed accordingly. 14/14 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
215 lines
9.0 KiB
JavaScript
215 lines
9.0 KiB
JavaScript
'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;
|