Files
valve/src/specificClass.js

892 lines
34 KiB
JavaScript
Raw Normal View History

/**
* @file valve.js
*
* Permission is hereby granted to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to use it for personal
* or non-commercial purposes, with the following restrictions:
*
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
* be copied, merged, distributed, sublicensed, or sold without explicit
* prior written permission from the author.
*
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
* a valid license, obtainable only with the explicit consent of the author.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Ownership of this code remains solely with the original author. Unauthorized
* use of this Software is strictly prohibited.
*
* Author:
* - Rene De Ren
* Email:
* - r.de.ren@brabantsedelta.nl
*
* Future Improvements:
* - Time-based stability checks
* - Warmup handling
* - Dynamic outlier detection thresholds
* - Dynamic smoothing window and methods
* - Alarm and threshold handling
* - Maintenance mode
* - Historical data and trend analysis
*/
2025-05-14 10:06:08 +02:00
/**
* @file valveClass.js
*
* Permission is hereby granted to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to use it for personal
....
*/
//load local dependencies
2025-05-14 10:06:08 +02:00
const EventEmitter = require('events');
2026-03-11 11:13:17 +01:00
const { loadCurve, logger, configUtils, configManager, state, MeasurementContainer, predict, childRegistrationUtils, convert } = require('generalFunctions');
const { ValveHydraulicModel, normalizeServiceType } = require('./hydraulicModel');
const SERVICE_TYPES = new Set(['gas', 'liquid']);
const DEFAULT_SOURCE_SERVICE_TYPE = Object.freeze({
machine: 'liquid',
rotatingmachine: 'liquid',
machinegroup: 'liquid',
machinegroupcontrol: 'liquid',
pumpingstation: 'liquid',
});
const CANONICAL_UNITS = Object.freeze({
pressure: 'Pa',
flow: 'm3/s',
temperature: 'K',
});
const DEFAULT_IO_UNITS = Object.freeze({
pressure: 'mbar',
flow: 'm3/h',
temperature: 'C',
});
const FORMULA_UNITS = Object.freeze({
pressure: 'mbar',
flow: 'm3/h',
temperature: 'K',
});
const FALLBACK_SUPPLIER_CURVE = Object.freeze({
'1.204': {
'125': {
x: [0, 100],
y: [0, 1],
},
},
});
2025-05-14 10:06:08 +02:00
class Valve {
2026-03-11 11:13:17 +01:00
constructor(valveConfig = {}, stateConfig = {}, runtimeOptions = {}) {
//basic setup
this.emitter = new EventEmitter(); // nodig voor ontvangen en uitvoeren van events emit() --> Zien als internet berichten (niet bedraad in node-red)
this.logger = new logger(valveConfig.general.logging.enabled,valveConfig.general.logging.logLevel, valveConfig.general.name);
this.configManager = new configManager();
this.defaultConfig = this.configManager.getConfig('valve'); // Load default config for rotating machine ( use software type name ? )
this.configUtils = new configUtils(this.defaultConfig);
2026-03-11 11:13:17 +01:00
// Load supplier-specific curve data (if available for model)
this.model = valveConfig.asset.model; // Get the model from the valveConfig
this.curve = this.model ? loadCurve(this.model) : null;
//Init config and check if it is valid
this.config = this.configUtils.initConfig(valveConfig);
2026-03-11 11:13:17 +01:00
this.unitPolicy = this._buildUnitPolicy(this.config);
this.config = this.configUtils.updateConfig(this.config, {
general: { unit: this.unitPolicy.output.flow },
asset: { ...this.config.asset, unit: this.unitPolicy.output.flow },
});
2025-05-14 10:06:08 +02:00
// Initialize measurements
2026-03-11 11:13:17 +01:00
this.measurements = new MeasurementContainer({
autoConvert: true,
defaultUnits: {
pressure: this.unitPolicy.output.pressure,
flow: this.unitPolicy.output.flow,
temperature: this.unitPolicy.output.temperature,
},
preferredUnits: {
pressure: this.unitPolicy.output.pressure,
flow: this.unitPolicy.output.flow,
temperature: this.unitPolicy.output.temperature,
},
canonicalUnits: this.unitPolicy.canonical,
storeCanonical: true,
strictUnitValidation: true,
throwOnInvalidUnit: true,
requireUnitForTypes: ['pressure', 'flow', 'temperature'],
}, this.logger);
2025-05-14 10:06:08 +02:00
this.child = {}; // object to hold child information so we know on what to subscribe
// Init after config is set
this.state = new state(stateConfig, this.logger); // Init State manager and pass logger
2025-05-14 10:06:08 +02:00
this.state.stateManager.currentState = "operational"; // Set default state to operational
2026-03-11 11:13:17 +01:00
this.kv = 0; // default
const configuredServiceType = this._normalizeOptionalServiceType(runtimeOptions?.serviceType || valveConfig?.asset?.serviceType);
this.expectedServiceType = configuredServiceType;
this.serviceType = configuredServiceType || normalizeServiceType(runtimeOptions?.serviceType || valveConfig?.asset?.serviceType);
this.upstreamFluidSources = new Map();
this._fluidContractListeners = new Map();
this.fluidCompatibility = {
status: configuredServiceType ? 'pending' : 'unknown',
expectedServiceType: configuredServiceType || null,
receivedServiceType: null,
upstreamServiceTypes: [],
sourceCount: 0,
message: configuredServiceType
? `Waiting for upstream fluid contract (${configuredServiceType}).`
: 'No upstream fluid contract available.',
};
this.hydraulicModel = new ValveHydraulicModel(
{
serviceType: this.serviceType,
gasChokedRatioLimit: runtimeOptions?.gasChokedRatioLimit ?? valveConfig?.asset?.gasChokedRatioLimit,
},
this.logger
);
this.rho = this._resolvePositiveNumber(
runtimeOptions?.fluidDensity,
valveConfig?.asset?.fluidDensity,
this.hydraulicModel.defaultDensity
);
this.T = this._resolvePositiveNumber(
runtimeOptions?.fluidTemperatureK,
valveConfig?.asset?.fluidTemperatureK,
this.hydraulicModel.defaultTemperatureK
);
2025-05-14 10:06:08 +02:00
this.currentMode = this.config.mode.current;
// wanneer hij deze ontvangt is de positie van de klep verandererd en gaat hij de updateposition functie aanroepen wat dan alle metingen en standen gaat updaten
2026-03-11 11:13:17 +01:00
this._onPositionChange = (data) => {
2025-05-14 10:06:08 +02:00
this.logger.debug(`Position change detected: ${data}`);
2026-03-11 11:13:17 +01:00
this.updatePosition();
};
this.state.emitter.on("positionChange", this._onPositionChange); //To update deltaP
2025-05-14 10:06:08 +02:00
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
2026-03-11 11:13:17 +01:00
this._initSupplierCurvePredictor();
2025-05-14 10:06:08 +02:00
}
// -------- Config -------- //
updateConfig(newConfig) {
this.config = this.configUtils.updateConfig(this.config, newConfig);
}
isValidSourceForMode(source, mode) {
const allowedSourcesSet = this.config.mode.allowedSources[mode] || [];
return allowedSourcesSet.has(source);
}
async handleInput(source, action, parameter) {
if (!this.isValidSourceForMode(source, this.currentMode)) {
let 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 {
switch (action) {
case "execSequence":
await this.executeSequence(parameter);
break;
case "execMovement": // past het setpoint aan - movement van klep stand
await this.setpoint(parameter);
break;
case "emergencyStop":
this.logger.warn(`Emergency stop activated by '${source}'.`);
2026-03-11 11:13:17 +01:00
await this.executeSequence("emergencystop");
break;
case "emergencystop":
this.logger.warn(`Emergency stop activated by '${source}'.`);
await this.executeSequence("emergencystop");
2025-05-14 10:06:08 +02:00
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}`);
}
}
setMode(newMode) {
2026-02-23 13:17:22 +01:00
const availableModes = Array.isArray(this.defaultConfig?.mode?.current?.rules?.values)
? this.defaultConfig.mode.current.rules.values.map(v => v.value)
: Object.keys(this.config?.mode?.allowedSources || {});
2025-05-14 10:06:08 +02:00
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}'.`);
}
2026-03-11 11:13:17 +01:00
_buildUnitPolicy(config = {}) {
const flowUnit = this._resolveUnitOrFallback(
config?.general?.unit || config?.asset?.unit,
'volumeFlowRate',
DEFAULT_IO_UNITS.flow
);
return {
canonical: { ...CANONICAL_UNITS },
output: {
flow: flowUnit,
pressure: DEFAULT_IO_UNITS.pressure,
temperature: DEFAULT_IO_UNITS.temperature,
}
};
}
_resolveUnitOrFallback(candidate, expectedMeasure, fallbackUnit) {
const fallback = String(fallbackUnit || '').trim();
const raw = typeof candidate === 'string' ? candidate.trim() : '';
if (!raw) {
return fallback;
}
try {
const desc = convert().describe(raw);
if (expectedMeasure && desc.measure !== expectedMeasure) {
throw new Error(`expected '${expectedMeasure}', got '${desc.measure}'`);
}
return raw;
} catch (error) {
this.logger?.warn?.(`Invalid unit '${raw}' (${error.message}); falling back to '${fallback}'.`);
return fallback;
}
}
_outputUnitForType(type) {
switch (String(type || '').toLowerCase()) {
case 'flow':
return this.unitPolicy.output.flow;
case 'pressure':
return this.unitPolicy.output.pressure;
case 'temperature':
return this.unitPolicy.output.temperature;
default:
return null;
}
}
_readMeasurement(type, variant, position, unit = null) {
const requestedUnit = unit || this._outputUnitForType(type);
return this.measurements
.type(type)
.variant(variant)
.position(position)
.getCurrentValue(requestedUnit || undefined);
}
_writeMeasurement(type, variant, position, value, unit = null, timestamp = Date.now()) {
if (!Number.isFinite(value)) {
return;
}
this.measurements
.type(type)
.variant(variant)
.position(position)
.value(value, timestamp, unit || undefined);
}
_resolvePositiveNumber(...candidates) {
for (const candidate of candidates) {
const parsed = Number(candidate);
if (Number.isFinite(parsed) && parsed > 0) {
return parsed;
}
}
return undefined;
}
_normalizeOptionalServiceType(value) {
const raw = String(value || '').trim().toLowerCase();
if (SERVICE_TYPES.has(raw)) {
return raw;
}
return null;
}
_deriveDefaultServiceTypeForSoftwareType(softwareType) {
const key = String(softwareType || '').trim().toLowerCase();
return DEFAULT_SOURCE_SERVICE_TYPE[key] || null;
}
_extractFluidContractFromChild(child, softwareType) {
const sourceType = String(softwareType || child?.config?.functionality?.softwareType || '').trim().toLowerCase();
let contractFromChild = null;
if (typeof child?.getFluidContract === 'function') {
try {
contractFromChild = child.getFluidContract();
} catch (error) {
this.logger.warn(`Failed to read child fluid contract: ${error.message}`);
}
}
const contractStatus = String(contractFromChild?.status || '').trim().toLowerCase();
if (contractStatus === 'conflict') {
return {
status: 'conflict',
serviceType: null,
sourceType,
};
}
const contractType = this._normalizeOptionalServiceType(contractFromChild?.serviceType);
if (contractType) {
return {
status: 'resolved',
serviceType: contractType,
sourceType,
};
}
const directType = this._normalizeOptionalServiceType(
child?.serviceType
|| child?.expectedServiceType
|| child?.config?.asset?.serviceType
);
if (directType) {
return {
status: 'resolved',
serviceType: directType,
sourceType,
};
}
const fallbackType = this._deriveDefaultServiceTypeForSoftwareType(sourceType);
if (fallbackType) {
return {
status: 'inferred',
serviceType: fallbackType,
sourceType,
};
}
return {
status: 'unknown',
serviceType: null,
sourceType,
};
}
_bindFluidContractListener(sourceId, child, sourceType) {
if (!sourceId || this._fluidContractListeners.has(sourceId)) {
return;
}
if (!child?.emitter || typeof child.emitter.on !== 'function') {
return;
}
const handler = () => {
const latest = this._extractFluidContractFromChild(child, sourceType);
const existing = this.upstreamFluidSources.get(sourceId) || {};
existing.contract = latest;
this.upstreamFluidSources.set(sourceId, existing);
this._updateFluidCompatibilityState();
};
child.emitter.on('fluidContractChange', handler);
this._fluidContractListeners.set(sourceId, {
emitter: child.emitter,
handler,
});
}
_computeFluidCompatibilitySnapshot() {
const expectedServiceType = this.expectedServiceType || null;
const contracts = Array.from(this.upstreamFluidSources.values())
.map((entry) => entry?.contract)
.filter(Boolean);
const upstreamServiceTypes = Array.from(new Set(
contracts
.map((contract) => this._normalizeOptionalServiceType(contract.serviceType))
.filter(Boolean)
));
const hasConflict = contracts.some((contract) => String(contract.status || '').toLowerCase() === 'conflict');
const sourceCount = this.upstreamFluidSources.size;
if (hasConflict || upstreamServiceTypes.length > 1) {
return {
status: 'conflict',
expectedServiceType,
receivedServiceType: upstreamServiceTypes.length === 1 ? upstreamServiceTypes[0] : null,
upstreamServiceTypes,
sourceCount,
message: `Conflicting upstream fluids detected: ${upstreamServiceTypes.join(', ') || 'unknown'}.`,
};
}
if (upstreamServiceTypes.length === 1) {
const receivedServiceType = upstreamServiceTypes[0];
if (expectedServiceType && expectedServiceType !== receivedServiceType) {
return {
status: 'mismatch',
expectedServiceType,
receivedServiceType,
upstreamServiceTypes,
sourceCount,
message: `Expected ${expectedServiceType}, received ${receivedServiceType}.`,
};
}
return {
status: expectedServiceType ? 'match' : 'inferred',
expectedServiceType,
receivedServiceType,
upstreamServiceTypes,
sourceCount,
message: expectedServiceType
? `Fluid contract validated: ${receivedServiceType}.`
: `Fluid inferred from upstream: ${receivedServiceType}.`,
};
}
return {
status: expectedServiceType ? 'pending' : 'unknown',
expectedServiceType,
receivedServiceType: null,
upstreamServiceTypes: [],
sourceCount,
message: expectedServiceType
? `Waiting for upstream fluid contract (${expectedServiceType}).`
: 'No upstream fluid contract available.',
};
}
_updateFluidCompatibilityState() {
const next = this._computeFluidCompatibilitySnapshot();
const previous = this.fluidCompatibility || {};
const changed = (
previous.status !== next.status
|| previous.expectedServiceType !== next.expectedServiceType
|| previous.receivedServiceType !== next.receivedServiceType
|| previous.sourceCount !== next.sourceCount
|| (previous.message || '') !== (next.message || '')
);
this.fluidCompatibility = next;
if (!changed) {
return;
}
if (next.status === 'mismatch' || next.status === 'conflict') {
this.logger.warn(`Fluid compatibility warning: ${next.message}`);
} else {
this.logger.info(`Fluid compatibility update: ${next.message}`);
}
this.emitter.emit('fluidCompatibilityChange', this.getFluidCompatibility());
this.emitter.emit('fluidContractChange', this.getFluidContract());
}
getFluidCompatibility() {
const state = this.fluidCompatibility || {};
return {
status: state.status || 'unknown',
expectedServiceType: state.expectedServiceType || null,
receivedServiceType: state.receivedServiceType || null,
upstreamServiceTypes: Array.isArray(state.upstreamServiceTypes) ? [...state.upstreamServiceTypes] : [],
sourceCount: Number(state.sourceCount) || 0,
message: state.message || '',
};
}
getFluidContract() {
const compatibility = this.getFluidCompatibility();
if (compatibility.status === 'conflict') {
return {
status: 'conflict',
serviceType: null,
expectedServiceType: compatibility.expectedServiceType,
observedServiceType: compatibility.receivedServiceType,
source: 'valve',
};
}
const advertisedServiceType = compatibility.expectedServiceType || null;
return {
status: advertisedServiceType ? 'resolved' : 'unknown',
serviceType: advertisedServiceType,
expectedServiceType: compatibility.expectedServiceType,
observedServiceType: compatibility.receivedServiceType,
source: 'valve',
};
}
registerChild(child, softwareType) {
if (!child || typeof child !== 'object') {
this.logger.warn('registerChild skipped: invalid child payload');
return false;
}
const sourceType = String(softwareType || child?.config?.functionality?.softwareType || '').trim().toLowerCase();
const sourceId = child?.config?.general?.id
|| child?.config?.general?.name
|| `source-${this.upstreamFluidSources.size + 1}`;
const contract = this._extractFluidContractFromChild(child, sourceType);
this.upstreamFluidSources.set(sourceId, {
child,
sourceType,
contract,
});
this._bindFluidContractListener(sourceId, child, sourceType);
this._updateFluidCompatibilityState();
this.logger.info(`Source '${sourceId}' (${sourceType || 'unknown'}) registered for fluid contract.`);
return true;
}
_initSupplierCurvePredictor() {
const supplierCurve = this._resolveSupplierCurveData();
const densityTarget = Number.isFinite(this.rho) && this.rho > 0 ? this.rho : this.hydraulicModel.defaultDensity;
const densityKey = this._pickNearestNumericKey(Object.keys(supplierCurve), densityTarget);
const densityCurveFamily = supplierCurve[densityKey];
const diameterTarget = Number(this.config?.asset?.valveDiameter);
const diameterKey = this._pickNearestNumericKey(
Object.keys(densityCurveFamily || {}),
Number.isFinite(diameterTarget) && diameterTarget > 0 ? diameterTarget : 125
);
this.curveSelection = {
densityKey: Number(densityKey),
diameterKey: Number(diameterKey),
};
this.rho = Number.isFinite(this.rho) && this.rho > 0 ? this.rho : this.hydraulicModel.defaultDensity;
this.T = Number.isFinite(this.T) && this.T > 0 ? this.T : this.hydraulicModel.defaultTemperatureK;
this.predictKv = new predict({ curve: densityCurveFamily || FALLBACK_SUPPLIER_CURVE['1.204'] });
this.predictKv.fDimension = this.curveSelection.diameterKey;
this.logger.info(
`Using supplier curve model='${this.model || "inline"}', densityCurve=${this.curveSelection.densityKey}, diameter=${this.curveSelection.diameterKey}, serviceType=${this.serviceType}`
);
}
_resolveSupplierCurveData() {
if (this._isValidSupplierCurveData(this.curve)) {
return this.curve;
}
if (this._isValidSupplierCurveData(this.config?.asset?.valveCurve)) {
return this.config.asset.valveCurve;
}
this.logger.warn("No valid supplier curve data found, using fallback curve.");
return FALLBACK_SUPPLIER_CURVE;
}
_isValidSupplierCurveData(curveData) {
if (!curveData || typeof curveData !== "object") {
return false;
}
const densityKeys = Object.keys(curveData);
if (!densityKeys.length) {
return false;
}
for (const densityKey of densityKeys) {
const diameters = curveData[densityKey];
if (!diameters || typeof diameters !== "object") {
return false;
}
const diameterKeys = Object.keys(diameters);
if (!diameterKeys.length) {
return false;
}
for (const diameterKey of diameterKeys) {
const curve = diameters[diameterKey];
if (!Array.isArray(curve?.x) || !Array.isArray(curve?.y) || curve.x.length < 2 || curve.x.length !== curve.y.length) {
return false;
}
}
}
return true;
}
_pickNearestNumericKey(keys, target) {
const numericKeys = keys.map((key) => Number(key)).filter((value) => Number.isFinite(value));
if (!numericKeys.length) {
return String(target);
}
let selected = numericKeys[0];
let selectedDistance = Math.abs(selected - target);
for (const key of numericKeys) {
const distance = Math.abs(key - target);
if (distance < selectedDistance) {
selected = key;
selectedDistance = distance;
}
}
return String(selected);
}
_predictKvForPosition(positionPercent) {
if (!this.predictKv) {
return 0.1;
}
try {
this.predictKv.fDimension = this.curveSelection?.diameterKey || this.predictKv.fDimension;
const kv = Number(this.predictKv.y(positionPercent));
if (!Number.isFinite(kv)) {
return 0.1;
}
return Math.max(0.1, kv);
} catch (error) {
this.logger.warn(`Failed to predict Kv for position=${positionPercent}: ${error.message}`);
return 0.1;
}
}
2025-05-14 10:06:08 +02:00
// -------- Sequence Handlers -------- //
async executeSequence(sequenceName) {
const sequence = this.config.sequences[sequenceName];
if (!sequence || sequence.size === 0) {
this.logger.warn(`Sequence '${sequenceName}' not defined.`);
return;
}
if (this.state.getCurrentState() == "operational" && sequenceName == "shutdown") {
this.logger.info(`Machine will ramp down to position 0 before performing ${sequenceName} sequence`);
await this.setpoint(0);
}
this.logger.info(` --------- Executing sequence: ${sequenceName} -------------`);
for (const state of sequence) {
try {
await this.state.transitionToState(state);
// Update measurements after state change
} catch (error) {
this.logger.error(`Error during sequence '${sequenceName}': ${error}`);
break; // Exit sequence execution on error
}
}
}
async setpoint(setpoint) {
try {
// Validate setpoint
if (typeof setpoint !== 'number' || setpoint < 0) {
throw new Error("Invalid setpoint: Setpoint must be a non-negative number.");
}
// Move to the desired setpoint
await this.state.moveTo(setpoint);
} catch (error) {
2026-02-23 13:17:22 +01:00
this.logger.error(`Error setting setpoint: ${error}`);
2025-05-14 10:06:08 +02:00
}
}
2026-03-11 11:13:17 +01:00
updatePressure(variant,value,position,unit = this.unitPolicy.output.pressure) {
2025-07-31 09:07:11 +02:00
if( value === null || value === undefined) {
2026-03-11 11:13:17 +01:00
this.logger.warn(`Received null or undefined value for pressure update. Variant: ${variant}, Position: ${position}`);
2025-07-31 09:07:11 +02:00
return;
}
this.logger.debug(`Updating pressure: variant=${variant}, value=${value}, position=${position}`);
switch (variant) {
case ("measured"):
// put value in measurements container
2026-03-11 11:13:17 +01:00
this._writeMeasurement("pressure", "measured", position, Number(value), unit);
2025-07-31 09:07:11 +02:00
// get latest downstream pressure measurement
2026-03-11 11:13:17 +01:00
const measuredDownStreamP = this._readMeasurement("pressure", "measured", "downstream", FORMULA_UNITS.pressure);
const measuredFlow = this._readMeasurement("flow", "measured", "downstream", FORMULA_UNITS.flow);
const predictedFlow = this._readMeasurement("flow", "predicted", "downstream", FORMULA_UNITS.flow);
const activeFlow = Number.isFinite(predictedFlow) ? predictedFlow : measuredFlow;
2025-07-31 09:07:11 +02:00
// update predicted flow measurement
2026-03-11 11:13:17 +01:00
this.updateDeltaPKlep(activeFlow,this.kv,measuredDownStreamP,this.rho,this.T); //update deltaP based on new flow
2025-07-31 09:07:11 +02:00
break;
case ("predicted"):
// put value in measurements container
2026-03-11 11:13:17 +01:00
this._writeMeasurement("pressure", "predicted", position, Number(value), unit);
const predictedDownStreamP = this._readMeasurement("pressure", "predicted", "downstream", FORMULA_UNITS.pressure);
const measuredFlowFromPred = this._readMeasurement("flow", "measured", "downstream", FORMULA_UNITS.flow);
const predictedFlowFromPred = this._readMeasurement("flow", "predicted", "downstream", FORMULA_UNITS.flow);
const activeFlowFromPred = Number.isFinite(predictedFlowFromPred) ? predictedFlowFromPred : measuredFlowFromPred;
this.updateDeltaPKlep(activeFlowFromPred,this.kv,predictedDownStreamP,this.rho,this.T); //update deltaP based on new flow
2025-07-31 09:07:11 +02:00
break;
default:
this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
break;
}
}
2026-03-11 11:13:17 +01:00
updateMeasurement(variant, subType, value, position, unit) {
this.logger.debug(`---------------------- updating ${subType} ------------------ `);
switch (subType) {
case "pressure":
// Update pressure measurement
2026-03-11 11:13:17 +01:00
this.updatePressure(variant,value,position, unit || this.unitPolicy.output.pressure);
break;
case "flow":
2026-03-11 11:13:17 +01:00
this.updateFlow(variant,value,position, unit || this.unitPolicy.output.flow);
break;
case "power":
// Update power measurement
break;
default:
this.logger.error(`Type '${subType}' not recognized for measured update.`);
return;
}
}
2025-05-14 10:06:08 +02:00
2026-03-11 11:13:17 +01:00
// NOTE: q in m3/h (normalized basis), downstreamP in mbar(g), temp in K
2025-05-14 10:06:08 +02:00
updateDeltaPKlep(q,kv,downstreamP,rho,temp){
2026-03-11 11:13:17 +01:00
const result = this.hydraulicModel.calculateDeltaPMbar({
qM3h: q,
kv,
downstreamGaugeMbar: downstreamP,
rho,
tempK: temp,
});
if (!result || !Number.isFinite(result.deltaPMbar)) {
return;
}
2025-05-14 10:06:08 +02:00
2026-03-11 11:13:17 +01:00
const deltaP = result.deltaPMbar;
this.deltaPKlep = deltaP;
this.hydraulicDiagnostics = result.details || null;
2025-07-31 09:07:11 +02:00
2026-03-11 11:13:17 +01:00
this._writeMeasurement("pressure", "predicted", "delta", deltaP, this.unitPolicy.output.pressure);
this.logger.info('DeltaP updated to: ' + deltaP);
2025-05-14 10:06:08 +02:00
2026-03-11 11:13:17 +01:00
this.emitter.emit('deltaPChange', deltaP); // Emit event to notify valveGroupController of deltaP change
this.logger.info('DeltaPChange emitted to valveGroupController');
}
2025-05-14 10:06:08 +02:00
// Als er een nieuwe flow door de klep komt doordat de machines harder zijn gaan werken, dan update deze functie dit ook in de valve attributes en measurements
2026-03-11 11:13:17 +01:00
updateFlow(variant,value,position,unit = this.unitPolicy.output.flow) {
2025-07-31 09:07:11 +02:00
if( value === null || value === undefined) {
this.logger.warn(`Received null or undefined value for flow update. Variant: ${variant}, Position: ${position}`);
return;
}
this.logger.debug(`Updating flow: variant=${variant}, value=${value}, position=${position}`);
switch (variant) {
case ("measured"):
2025-07-31 09:07:11 +02:00
// put value in measurements container
2026-03-11 11:13:17 +01:00
this._writeMeasurement("flow", "measured", position, Number(value), unit);
2025-07-31 09:07:11 +02:00
// get latest downstream pressure measurement
2026-03-11 11:13:17 +01:00
const measuredDownStreamP = this._readMeasurement("pressure", "measured", "downstream", FORMULA_UNITS.pressure);
const measuredFlow = this._readMeasurement("flow", "measured", position, FORMULA_UNITS.flow);
2025-07-31 09:07:11 +02:00
// update predicted flow measurement
2026-03-11 11:13:17 +01:00
this.updateDeltaPKlep(measuredFlow,this.kv,measuredDownStreamP,this.rho,this.T); //update deltaP based on new flow
break;
case ("predicted"):
2025-07-31 09:07:11 +02:00
// put value in measurements container
2026-03-11 11:13:17 +01:00
this._writeMeasurement("flow", "predicted", position, Number(value), unit);
const predictedDownStreamP = this._readMeasurement("pressure", "measured", "downstream", FORMULA_UNITS.pressure);
const predictedFlow = this._readMeasurement("flow", "predicted", position, FORMULA_UNITS.flow);
this.updateDeltaPKlep(predictedFlow,this.kv,predictedDownStreamP,this.rho,this.T); //update deltaP based on new flow
break;
default:
this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
break;
}
2025-05-14 10:06:08 +02:00
}
updatePosition() { //update alle parameters nadat er een verandering is geweest in stand van klep
if (this.state.getCurrentState() == "operational" || this.state.getCurrentState() == "accelerating" || this.state.getCurrentState() == "decelerating") {
this.logger.debug('Calculating new deltaP');
const currentPosition = this.state.getCurrentPosition();
2026-03-11 11:13:17 +01:00
const measuredFlow = this._readMeasurement("flow", "measured", "downstream", FORMULA_UNITS.flow);
const predictedFlow = this._readMeasurement("flow", "predicted", "downstream", FORMULA_UNITS.flow);
const currentFlow = Number.isFinite(predictedFlow) ? predictedFlow : measuredFlow;
2025-07-31 09:07:11 +02:00
2026-03-11 11:13:17 +01:00
const downstreamP = this._readMeasurement("pressure", "measured", "downstream", FORMULA_UNITS.pressure);
2025-05-14 10:06:08 +02:00
const x = currentPosition; // dit is de positie van de klep waarvoor we delta P willen berekenen
2026-03-11 11:13:17 +01:00
const y = this._predictKvForPosition(x); // haal de waarde van kv op uit de supplierscurve
2025-05-14 10:06:08 +02:00
this.kv = y; //update de kv waarde in de valve class
this.logger.debug(`Kv value for position valve ${x} is ${this.kv}`); // log de waarde van kv
this.updateDeltaPKlep(currentFlow,this.kv,downstreamP,this.rho,this.T); //update deltaP
2025-05-14 10:06:08 +02:00
}
}
2026-03-11 11:13:17 +01:00
showCurve() {
return {
model: this.model || null,
serviceType: this.serviceType,
expectedServiceType: this.expectedServiceType,
gasChokedRatioLimit: this.hydraulicModel?.gasChokedRatioLimit,
selectedDensity: this.curveSelection?.densityKey ?? null,
selectedDiameter: this.curveSelection?.diameterKey ?? null,
curve: this.predictKv?.currentFxyCurve?.[this.predictKv?.fDimension] || null,
hydraulics: this.hydraulicDiagnostics || null,
};
}
destroy() {
if (this._onPositionChange && this.state?.emitter?.off) {
this.state.emitter.off("positionChange", this._onPositionChange);
}
for (const { emitter, handler } of this._fluidContractListeners.values()) {
if (typeof emitter?.off === 'function') {
emitter.off('fluidContractChange', handler);
} else if (typeof emitter?.removeListener === 'function') {
emitter.removeListener('fluidContractChange', handler);
}
}
this._fluidContractListeners.clear();
}
2025-05-14 10:06:08 +02:00
getOutput() {
// Improved output object generation
const output = {};
//build the output object
2026-03-11 11:13:17 +01:00
Object.entries(this.measurements.measurements || {}).forEach(([type, variants]) => {
Object.entries(variants || {}).forEach(([variant, positions]) => {
Object.keys(positions || {}).forEach((position) => {
const value = this._readMeasurement(type, variant, position, this._outputUnitForType(type));
if (value != null) {
output[`${position}_${variant}_${type}`] = value;
}
});
2025-05-14 10:06:08 +02:00
});
});
//fill in the rest of the output object
output["state"] = this.state.getCurrentState();
output["percentageOpen"] = this.state.getCurrentPosition();
output["moveTimeleft"] = this.state.getMoveTimeLeft();
output["mode"] = this.currentMode;
//this.logger.debug(`Output: ${JSON.stringify(output)}`);
return output;
}
}
module.exports = Valve;