Files
valveGroupControl/src/specificClass.js
znetsixe 778b2e0c79 P11.5 + B2.1/B2.2: per-command units + description (where applicable)
Adds  to scalar setters whose payloads are
plain numbers OR {value, unit}. Skipped where payload is compound or
mode-dependent (control-%, {F, C: [...]}, etc.) — documented inline.
Every command gains a description field for wikiGen consumption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:41:17 +02:00

256 lines
10 KiB
JavaScript

'use strict';
// ValveGroupControl — S88 Unit orchestrator coordinating valve children.
// Concern modules under src/{groupOps,sources,io,commands} carry the
// real work; this file stitches them together: registration, valve event
// routing, source fluid-contract aggregation, mode/sequence dispatch.
const { BaseDomain, UnitPolicy, state } = require('generalFunctions');
const flowDist = require('./groupOps/flowDistribution');
const sources = require('./sources/fluidContract');
const io = require('./io/output');
// Source softwareTypes after BaseDomain canonicalisation
// (rotatingmachine→machine, machinegroupcontrol→machinegroup).
const SOURCE_SOFTWARE_TYPES = ['machine', 'machinegroup', 'pumpingstation', 'valvegroupcontrol'];
class ValveGroupControl extends BaseDomain {
static name = 'valveGroupControl';
static unitPolicy = UnitPolicy.declare({
canonical: { flow: 'm3/s', pressure: 'Pa' },
output: { flow: 'm3/h', pressure: 'mbar' },
requireUnitForTypes: ['pressure', 'flow'],
});
configure() {
this.config = this.configUtils.updateConfig(this.config, {
general: { unit: this.unitPolicy.output('flow') },
});
this.valves = {};
this._valveListeners = new Map();
this.sources = {};
this._sourceListeners = new Map();
this.fluidContract = {
status: 'unknown', serviceType: null, upstreamServiceTypes: [],
sourceCount: 0, message: 'No upstream fluid sources registered.',
};
this.flowReconciliation = { ...flowDist.DEFAULT_RECONCILIATION };
this.lastFlowSolve = { passes: 0, residual: 0, targetTotal: 0, assignedTotal: 0 };
this.maxDeltaP = 0;
this.currentMode = this.config.mode.current;
this.state = new state({}, this.logger);
this.state.stateManager.currentState = 'operational';
this.router.onRegister('valve', (child) => this._registerValve(child));
for (const swType of SOURCE_SOFTWARE_TYPES) {
this.router.onRegister(swType, (child, canonicalKey) => this._registerSource(child, canonicalKey));
}
}
_isValveLike(child) {
return Boolean(
child
&& typeof child.updateFlow === 'function'
&& child.state && typeof child.state.getCurrentState === 'function'
&& child.measurements
);
}
_registerValve(child) {
if (!this._isValveLike(child)) {
this.logger.warn('registerChild skipped: child is not valve-like');
return false;
}
const id = child.config?.general?.id || child.config?.general?.name || `valve-${Object.keys(this.valves).length + 1}`;
if (this.valves[id]) {
this.logger.debug(`registerChild skipped: valve ${id} already registered`);
return true;
}
const positionVsParent = child.positionVsParent
|| child.config?.functionality?.positionVsParent
|| 'atEquipment';
child.positionVsParent = positionVsParent;
this.valves[id] = child;
this._bindValveEvents(id, child);
this.calcValveFlows();
this.calcMaxDeltaP();
sources.refreshFluidContract(this);
this.logger.info(`Valve '${id}' registered at ${positionVsParent}.`);
return true;
}
_registerSource(child, softwareType) {
const positionVsParent = child.positionVsParent
|| child.config?.functionality?.positionVsParent
|| 'atEquipment';
return sources.registerSource(this, child, positionVsParent, softwareType);
}
_bindValveEvents(valveId, valve) {
const handlers = {
onPositionChange: () => { this.logger.debug(`Valve ${valveId} position changed, recalculating flows.`); this.calcValveFlows(); },
onDeltaPChange: () => { this.logger.debug(`Valve ${valveId} deltaP changed, recalculating max deltaP.`); this.calcMaxDeltaP(); },
};
if (valve.state?.emitter?.on) valve.state.emitter.on('positionChange', handlers.onPositionChange);
if (valve.emitter?.on) valve.emitter.on('deltaPChange', handlers.onDeltaPChange);
this._valveListeners.set(valveId, { valve, handlers });
}
_unbindValveEvents(valveId) {
const entry = this._valveListeners.get(valveId);
if (!entry) return;
const { valve, handlers } = entry;
if (handlers.onPositionChange && valve.state?.emitter?.off) valve.state.emitter.off('positionChange', handlers.onPositionChange);
if (handlers.onDeltaPChange && valve.emitter?.off) valve.emitter.off('deltaPChange', handlers.onDeltaPChange);
this._valveListeners.delete(valveId);
}
registerOnChildEvents() {}
destroy() {
for (const id of this._valveListeners.keys()) this._unbindValveEvents(id);
for (const id of this._sourceListeners.keys()) sources.unbindSource(this, id);
}
// ── measurement read/write helpers used by concern modules ─────────
_outputUnitForType(type) {
switch (String(type || '').toLowerCase()) {
case 'flow': return this.unitPolicy.output('flow');
case 'pressure': return this.unitPolicy.output('pressure');
default: return null;
}
}
_read(type, variant, position, unit = null) {
const u = unit || this._outputUnitForType(type);
return this.measurements.type(type).variant(variant).position(position).getCurrentValue(u || undefined);
}
_write(type, variant, position, value, unit = null, timestamp = Date.now()) {
const v = Number(value);
if (!Number.isFinite(v)) return;
this.measurements.type(type).variant(variant).position(position).value(v, timestamp, unit || undefined);
}
// ── public surface used by adapter, tests, commands, valves ────────
getAvailableValves() { return flowDist.listAvailableValves(this.valves); }
calcValveFlows() { flowDist.distributeFlow(this); this.notifyOutputChanged(); }
calcMaxDeltaP() { flowDist.calcMaxDeltaP(this); }
getFluidContract() { return sources.getFluidContract(this); }
isValidSourceForMode(source, mode) {
const allowedSourcesSet = this.config.mode.allowedSources[mode] || [];
return allowedSourcesSet.has(source);
}
setMode(newMode) {
const availableModes = Array.isArray(this.defaultConfig?.mode?.current?.rules?.values)
? this.defaultConfig.mode.current.rules.values.map((m) => m.value)
: Object.keys(this.config?.mode?.allowedSources || {});
if (!availableModes.includes(newMode)) {
this.logger.warn(`Invalid mode '${newMode}'. Allowed modes are: ${availableModes.join(', ')}`);
return;
}
this.currentMode = newMode;
this.logger.info(`Mode successfully changed to '${newMode}'.`);
this.notifyOutputChanged();
}
async executeSequence(sequenceName) {
const sequence = this.config.sequences[sequenceName];
if (!sequence || sequence.size === 0) {
this.logger.warn(`Sequence '${sequenceName}' not defined.`);
return;
}
this.logger.info(` --------- Executing sequence: ${sequenceName} -------------`);
for (const stateName of sequence) {
try { await this.state.transitionToState(stateName); }
catch (error) { this.logger.error(`Error during sequence '${sequenceName}': ${error}`); break; }
}
}
updateFlow(variant, value, position, unit = this.unitPolicy.output('flow')) {
if (value === null || value === undefined) {
this.logger.warn(`Received null or undefined value for flow update. Variant: ${variant}, Position: ${position}`);
return;
}
if (variant !== 'measured' && variant !== 'predicted') {
this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
return;
}
this.logger.debug(`Updating ${variant} flow for position ${position} with value ${value}`);
this._write('flow', variant, position, value, unit);
this.calcValveFlows();
}
updateMeasurement(variant, subType, value, position, unit) {
this.logger.debug(`---------------------- updating ${subType} ------------------ `);
if (subType === 'flow') {
this.updateFlow(variant, value, position, unit || this.unitPolicy.output('flow'));
return;
}
this.logger.error(`Type '${subType}' not recognized for measured update.`);
}
async handleInput(source, action, parameter) {
if (!this.isValidSourceForMode(source, this.currentMode)) {
const warningTxt = `Source '${source}' is not valid for mode '${this.currentMode}'.`;
this.logger.warn(warningTxt);
return { status: false, feedback: warningTxt };
}
this.logger.info(`Handling input from source '${source}' with action '${action}' in mode '${this.currentMode}'.`);
try {
const flowUnit = this.unitPolicy.output('flow');
switch (action) {
case 'execSequence':
await this.executeSequence(parameter);
break;
case 'totalFlowChange':
if (parameter && typeof parameter === 'object' && Object.prototype.hasOwnProperty.call(parameter, 'value')) {
await this.updateFlow(parameter.variant || 'measured', parameter.value, parameter.position || 'atEquipment', parameter.unit || flowUnit);
} else if (parameter && typeof parameter === 'object' && Object.prototype.hasOwnProperty.call(parameter, 'q')) {
await this.updateFlow('measured', Number(parameter.q), 'atEquipment', parameter.unit || flowUnit);
} else {
await this.updateFlow('measured', Number(parameter), 'atEquipment', flowUnit);
}
break;
case 'emergencyStop':
case 'emergencystop':
this.logger.warn(`Emergency stop activated by '${source}'.`);
await this.executeSequence('emergencystop');
break;
case 'statusCheck':
this.logger.info(`Status Check: Mode = '${this.currentMode}', Source = '${source}'.`);
break;
default:
this.logger.warn(`Action '${action}' is not implemented.`);
break;
}
this.logger.debug(`Action '${action}' successfully executed`);
return { status: true, feedback: `Action '${action}' successfully executed.` };
} catch (error) {
this.logger.error(`Error handling input: ${error}`);
return { status: false, feedback: `Error handling input: ${error.message || error}` };
}
}
setReconcileIntervalSeconds(sec) {
const ms = Math.max(100, Math.round(Number(sec) * 1000));
this.emitter.emit('reconcileIntervalChange', ms);
this.logger.info(`Flow reconciliation interval updated to ${sec}s (${ms}ms).`);
}
// Periodic reconciliation — adapter fires this each tickInterval. Keeps
// per-valve assigned flow in sync if a child's accepted value drifts
// between event-driven recalcs.
tick() { this.calcValveFlows(); }
getOutput() { return io.getOutput(this); }
getStatusBadge() { return io.getStatusBadge(this); }
}
module.exports = ValveGroupControl;