/** * @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 */ /** * @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 const EventEmitter = require('events'); 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], }, }, }); class Valve { 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); // 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); 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 }, }); // Initialize measurements 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); 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 this.state.stateManager.currentState = "operational"; // Set default state to operational 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 ); 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 this._onPositionChange = (data) => { this.logger.debug(`Position change detected: ${data}`); this.updatePosition(); }; this.state.emitter.on("positionChange", this._onPositionChange); //To update deltaP this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility this._initSupplierCurvePredictor(); } // -------- 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}'.`); await this.executeSequence("emergencystop"); break; 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}`); } } setMode(newMode) { 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 || {}); 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}'.`); } _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; } } // -------- 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) { this.logger.error(`Error setting setpoint: ${error}`); } } updatePressure(variant,value,position,unit = this.unitPolicy.output.pressure) { if( value === null || value === undefined) { this.logger.warn(`Received null or undefined value for pressure update. Variant: ${variant}, Position: ${position}`); return; } this.logger.debug(`Updating pressure: variant=${variant}, value=${value}, position=${position}`); switch (variant) { case ("measured"): { // put value in measurements container this._writeMeasurement("pressure", "measured", position, Number(value), unit); // get latest downstream pressure measurement 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; // update predicted flow measurement this.updateDeltaPKlep(activeFlow,this.kv,measuredDownStreamP,this.rho,this.T); break; } case ("predicted"): { // put value in measurements container 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); break; } default: this.logger.warn(`Unrecognized variant '${variant}' for flow update.`); break; } } updateMeasurement(variant, subType, value, position, unit) { this.logger.debug(`---------------------- updating ${subType} ------------------ `); switch (subType) { case "pressure": // Update pressure measurement this.updatePressure(variant,value,position, unit || this.unitPolicy.output.pressure); break; case "flow": 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; } } // NOTE: q in m3/h (normalized basis), downstreamP in mbar(g), temp in K updateDeltaPKlep(q,kv,downstreamP,rho,temp){ const result = this.hydraulicModel.calculateDeltaPMbar({ qM3h: q, kv, downstreamGaugeMbar: downstreamP, rho, tempK: temp, }); if (!result || !Number.isFinite(result.deltaPMbar)) { return; } const deltaP = result.deltaPMbar; this.deltaPKlep = deltaP; this.hydraulicDiagnostics = result.details || null; this._writeMeasurement("pressure", "predicted", "delta", deltaP, this.unitPolicy.output.pressure); this.logger.info('DeltaP updated to: ' + deltaP); this.emitter.emit('deltaPChange', deltaP); // Emit event to notify valveGroupController of deltaP change this.logger.info('DeltaPChange emitted to valveGroupController'); } // 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 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; } this.logger.debug(`Updating flow: variant=${variant}, value=${value}, position=${position}`); switch (variant) { case ("measured"): { // put value in measurements container this._writeMeasurement("flow", "measured", position, Number(value), unit); // get latest downstream pressure measurement const measuredDownStreamP = this._readMeasurement("pressure", "measured", "downstream", FORMULA_UNITS.pressure); const measuredFlow = this._readMeasurement("flow", "measured", position, FORMULA_UNITS.flow); // update predicted flow measurement this.updateDeltaPKlep(measuredFlow,this.kv,measuredDownStreamP,this.rho,this.T); break; } case ("predicted"): { // put value in measurements container 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); break; } default: this.logger.warn(`Unrecognized variant '${variant}' for flow update.`); break; } } 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(); 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; const downstreamP = this._readMeasurement("pressure", "measured", "downstream", FORMULA_UNITS.pressure); const x = currentPosition; // dit is de positie van de klep waarvoor we delta P willen berekenen const y = this._predictKvForPosition(x); // haal de waarde van kv op uit de supplierscurve 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 } } 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(); } getOutput() { // Improved output object generation const output = {}; //build the output object 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; } }); }); }); //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;