Files
valve/src/specificClass.js
znetsixe 68ebe4ebce feat(valve): resolve supplier+type from asset registry, reject legacy asset fields
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>
2026-05-12 17:12:47 +02:00

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;