const EventEmitter = require('events'); const {loadCurve,gravity,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils,coolprop, convert, POSITIONS} = require('generalFunctions'); const CANONICAL_UNITS = Object.freeze({ pressure: 'Pa', atmPressure: 'Pa', flow: 'm3/s', power: 'W', temperature: 'K', }); const DEFAULT_IO_UNITS = Object.freeze({ pressure: 'mbar', flow: 'm3/h', power: 'kW', temperature: 'C', }); const DEFAULT_CURVE_UNITS = Object.freeze({ pressure: 'mbar', flow: 'm3/h', power: 'kW', control: '%', }); /** * Rotating machine domain model. * Combines machine curves, state transitions and measurement reconciliation * to produce flow/power/efficiency behavior for pumps and similar assets. */ class Machine { /*------------------- Construct and set vars -------------------*/ constructor(machineConfig = {}, stateConfig = {}, errorMetricsConfig = {}) { //basic setup this.emitter = new EventEmitter(); // Own EventEmitter this.logger = new logger(machineConfig.general.logging.enabled,machineConfig.general.logging.logLevel, machineConfig.general.name); this.configManager = new configManager(); this.defaultConfig = this.configManager.getConfig('rotatingMachine'); // Load default config for rotating machine ( use software type name ? ) this.configUtils = new configUtils(this.defaultConfig); // Load a specific curve this.model = machineConfig.asset.model; // Get the model from the machineConfig this.rawCurve = this.model ? loadCurve(this.model) : null; this.curve = null; //Init config and check if it is valid this.config = this.configUtils.initConfig(machineConfig); //add unique name for this node. this.config = this.configUtils.updateConfig(this.config, {general:{name: this.config.functionality?.softwareType + "_" + machineConfig.general.id}}); // add unique name if not present 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, curveUnits: this.unitPolicy.curve, }, }); if (!this.model || !this.rawCurve) { this.logger.error(`${!this.model ? 'Model not specified' : 'Curve not found for model ' + this.model} in machineConfig. Cannot make predictions.`); // Set prediction objects to null to prevent method calls this.predictFlow = null; this.predictPower = null; this.predictCtrl = null; this.hasCurve = false; } else{ try { this.hasCurve = true; this.curve = this._normalizeMachineCurve(this.rawCurve); this.config = this.configUtils.updateConfig(this.config, { asset: { ...this.config.asset, machineCurve: this.curve } }); //machineConfig = { ...machineConfig, asset: { ...machineConfig.asset, machineCurve: this.curve } }; // Merge curve into machineConfig this.predictFlow = new predict({ curve: this.config.asset.machineCurve.nq }); // load nq (x : ctrl , y : flow relationship) this.predictPower = new predict({ curve: this.config.asset.machineCurve.np }); // load np (x : ctrl , y : power relationship) this.predictCtrl = new predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship) } catch (error) { this.logger.error(`Curve normalization failed for model '${this.model}': ${error.message}`); this.predictFlow = null; this.predictPower = null; this.predictCtrl = null; this.hasCurve = false; } } this.state = new state(stateConfig, this.logger); // Init State manager and pass logger this.errorMetrics = new nrmse(errorMetricsConfig, this.logger); // Initialize measurements this.measurements = new MeasurementContainer({ autoConvert: true, windowSize: 50, defaultUnits: { pressure: this.unitPolicy.output.pressure, flow: this.unitPolicy.output.flow, power: this.unitPolicy.output.power, temperature: this.unitPolicy.output.temperature, atmPressure: 'Pa', }, preferredUnits: { pressure: this.unitPolicy.output.pressure, flow: this.unitPolicy.output.flow, power: this.unitPolicy.output.power, temperature: this.unitPolicy.output.temperature, atmPressure: 'Pa', }, canonicalUnits: this.unitPolicy.canonical, storeCanonical: true, strictUnitValidation: true, throwOnInvalidUnit: true, requireUnitForTypes: ['pressure', 'flow', 'power', 'temperature', 'atmPressure'], }, this.logger); this.interpolation = new interpolation(); this.flowDrift = null; this.powerDrift = null; this.pressureDrift = { level: 0, flags: ["nominal"], source: null }; this.driftProfiles = { flow: { windowSize: 30, minSamplesForLongTerm: 10, ewmaAlpha: 0.15, alignmentToleranceMs: 2500, strictValidation: true, }, power: { windowSize: 30, minSamplesForLongTerm: 10, ewmaAlpha: 0.15, alignmentToleranceMs: 2500, strictValidation: true, }, }; this.errorMetrics.registerMetric("flow", this.driftProfiles.flow); this.errorMetrics.registerMetric("power", this.driftProfiles.power); this.predictionHealth = { quality: "invalid", confidence: 0, pressureSource: null, flags: ["not_initialized"], }; this.currentMode = this.config.mode.current; this.currentEfficiencyCurve = {}; this.cog = 0; this.NCog = 0; this.cogIndex = 0; this.minEfficiency = 0; this.absDistFromPeak = 0; this.relDistFromPeak = 0; // When position state changes, update position this.state.emitter.on("positionChange", (data) => { this.logger.debug(`Position change detected: ${data}`); this.updatePosition(); }); //When state changes look if we need to do other updates this.state.emitter.on("stateChange", (newState) => { this.logger.debug(`State change detected: ${newState}`); this._updateState(); }); //perform init for certain values this._init(); this.child = {}; // object to hold child information so we know on what to subscribe this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility this.virtualPressureChildIds = { upstream: "dashboard-sim-upstream", downstream: "dashboard-sim-downstream", }; this.virtualPressureChildren = {}; this.realPressureChildIds = { upstream: new Set(), downstream: new Set(), }; this.childMeasurementListeners = new Map(); this._initVirtualPressureChildren(); } _initVirtualPressureChildren() { const createVirtualChild = (position) => { const id = this.virtualPressureChildIds[position]; const name = `dashboard-sim-${position}`; const measurements = new MeasurementContainer({ autoConvert: true, defaultUnits: { pressure: this.unitPolicy.output.pressure, flow: this.unitPolicy.output.flow, power: this.unitPolicy.output.power, temperature: this.unitPolicy.output.temperature, }, preferredUnits: { pressure: this.unitPolicy.output.pressure, flow: this.unitPolicy.output.flow, power: this.unitPolicy.output.power, temperature: this.unitPolicy.output.temperature, }, canonicalUnits: this.unitPolicy.canonical, storeCanonical: true, strictUnitValidation: true, throwOnInvalidUnit: true, requireUnitForTypes: ['pressure'], }, this.logger); measurements.setChildId(id); measurements.setChildName(name); measurements.setParentRef(this); return { config: { general: { id, name }, functionality: { softwareType: "measurement", positionVsParent: position, }, asset: { type: "pressure", unit: this.unitPolicy.output.pressure, }, }, measurements, }; }; const upstreamChild = createVirtualChild("upstream"); const downstreamChild = createVirtualChild("downstream"); this.virtualPressureChildren.upstream = upstreamChild; this.virtualPressureChildren.downstream = downstreamChild; this.registerChild(upstreamChild, "measurement"); this.registerChild(downstreamChild, "measurement"); } _init(){ //assume standard temperature is 20degrees this.measurements.type('temperature').variant('measured').position('atEquipment').value(15, Date.now(), this.unitPolicy.output.temperature); //assume standard atm pressure is at sea level this.measurements.type('atmPressure').variant('measured').position('atEquipment').value(101325, Date.now(), 'Pa'); //populate min and max when curve data is available const flowunit = this.unitPolicy.canonical.flow; if (this.predictFlow) { this.measurements.type('flow').variant('predicted').position('max').value(this.predictFlow.currentFxyYMax, Date.now() , flowunit); this.measurements.type('flow').variant('predicted').position('min').value(this.predictFlow.currentFxyYMin, Date.now(), flowunit); } else { this.measurements.type('flow').variant('predicted').position('max').value(0, Date.now(), flowunit); this.measurements.type('flow').variant('predicted').position('min').value(0, Date.now(), flowunit); } } _updateState(){ const isOperational = this._isOperationalState(); if(!isOperational){ //overrule the last prediction this should be 0 now this.measurements.type("flow").variant("predicted").position("downstream").value(0,Date.now(),this.unitPolicy.canonical.flow); this.measurements.type("flow").variant("predicted").position("atEquipment").value(0,Date.now(),this.unitPolicy.canonical.flow); this.measurements.type("power").variant("predicted").position("atEquipment").value(0,Date.now(),this.unitPolicy.canonical.power); } this._updatePredictionHealth(); } /*------------------- Register child events -------------------*/ registerChild(child, softwareType) { const resolvedSoftwareType = softwareType || child?.config?.functionality?.softwareType || "measurement"; this.logger.debug('Setting up child event for softwaretype ' + resolvedSoftwareType); if(resolvedSoftwareType === "measurement"){ const position = String(child.config.functionality.positionVsParent || "atEquipment").toLowerCase(); const measurementType = child.config.asset.type; const childId = child.config?.general?.id || `${measurementType}-${position}-unknown`; const isVirtualPressureChild = Object.values(this.virtualPressureChildIds).includes(childId); if (measurementType === "pressure" && !isVirtualPressureChild) { this.realPressureChildIds[position]?.add(childId); } //rebuild to measurementype.variant no position and then switch based on values not strings or names. const eventName = `${measurementType}.measured.${position}`; const listenerKey = `${childId}:${eventName}`; const existingListener = this.childMeasurementListeners.get(listenerKey); if (existingListener) { if (typeof existingListener.emitter.off === "function") { existingListener.emitter.off(existingListener.eventName, existingListener.handler); } else if (typeof existingListener.emitter.removeListener === "function") { existingListener.emitter.removeListener(existingListener.eventName, existingListener.handler); } } this.logger.debug(`Setting up listener for ${eventName} from child ${child.config.general.name}`); // Register event listener for measurement updates const listener = (eventData) => { this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`); this.logger.debug(` Emitting... ${eventName} with data:`); // Route through centralized handlers so unit validation/conversion is applied once. this._callMeasurementHandler(measurementType, eventData.value, position, eventData); }; child.measurements.emitter.on(eventName, listener); this.childMeasurementListeners.set(listenerKey, { emitter: child.measurements.emitter, eventName, handler: listener, }); } } // Centralized handler dispatcher _callMeasurementHandler(measurementType, value, position, context) { switch (measurementType) { case 'pressure': this.updateMeasuredPressure(value, position, context); break; case 'flow': this.updateMeasuredFlow(value, position, context); break; case 'power': this.updateMeasuredPower(value, position, context); break; case 'temperature': this.updateMeasuredTemperature(value, position, context); break; default: this.logger.warn(`No handler for measurement type: ${measurementType}`); // Generic handler - just update position this.updatePosition(); break; } } //---------------- END child stuff -------------// _buildUnitPolicy(config) { const flowOutputUnit = this._resolveUnitOrFallback( config?.general?.unit, 'volumeFlowRate', DEFAULT_IO_UNITS.flow, 'general.flow' ); const pressureOutputUnit = this._resolveUnitOrFallback( config?.asset?.pressureUnit, 'pressure', DEFAULT_IO_UNITS.pressure, 'asset.pressure' ); const powerOutputUnit = this._resolveUnitOrFallback( config?.asset?.powerUnit, 'power', DEFAULT_IO_UNITS.power, 'asset.power' ); const temperatureOutputUnit = this._resolveUnitOrFallback( config?.asset?.temperatureUnit, 'temperature', DEFAULT_IO_UNITS.temperature, 'asset.temperature' ); const curveUnits = this._resolveCurveUnits(config?.asset?.curveUnits || {}, flowOutputUnit); return { canonical: { ...CANONICAL_UNITS }, output: { pressure: pressureOutputUnit, flow: flowOutputUnit, power: powerOutputUnit, temperature: temperatureOutputUnit, atmPressure: 'Pa', }, curve: curveUnits, }; } _resolveCurveUnits(curveUnits = {}, fallbackFlowUnit = DEFAULT_CURVE_UNITS.flow) { const pressure = this._resolveUnitOrFallback( curveUnits.pressure, 'pressure', DEFAULT_CURVE_UNITS.pressure, 'asset.curveUnits.pressure' ); const flow = this._resolveUnitOrFallback( curveUnits.flow, 'volumeFlowRate', fallbackFlowUnit || DEFAULT_CURVE_UNITS.flow, 'asset.curveUnits.flow' ); const power = this._resolveUnitOrFallback( curveUnits.power, 'power', DEFAULT_CURVE_UNITS.power, 'asset.curveUnits.power' ); const control = typeof curveUnits.control === 'string' && curveUnits.control.trim() ? curveUnits.control.trim() : DEFAULT_CURVE_UNITS.control; return { pressure, flow, power, control }; } _resolveUnitOrFallback(candidate, expectedMeasure, fallbackUnit, label) { 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} but got ${desc.measure}`); } return raw; } catch (error) { this.logger.warn(`Invalid ${label} unit '${raw}' (${error.message}). Falling back to '${fallback}'.`); return fallback; } } _convertUnitValue(value, fromUnit, toUnit, contextLabel = 'unit conversion') { const numeric = Number(value); if (!Number.isFinite(numeric)) { throw new Error(`${contextLabel}: value '${value}' is not finite`); } if (!fromUnit || !toUnit || fromUnit === toUnit) { return numeric; } return convert(numeric).from(fromUnit).to(toUnit); } _normalizeCurveSection(section, fromYUnit, toYUnit, fromPressureUnit, toPressureUnit, sectionName) { const normalized = {}; for (const [pressureKey, pair] of Object.entries(section || {})) { const canonicalPressure = this._convertUnitValue( Number(pressureKey), fromPressureUnit, toPressureUnit, `${sectionName} pressure axis` ); const xArray = Array.isArray(pair?.x) ? pair.x.map(Number) : []; const yArray = Array.isArray(pair?.y) ? pair.y.map((v) => this._convertUnitValue(v, fromYUnit, toYUnit, `${sectionName} output`)) : []; if (!xArray.length || !yArray.length || xArray.length !== yArray.length) { throw new Error(`Invalid ${sectionName} section at pressure '${pressureKey}'.`); } normalized[String(canonicalPressure)] = { x: xArray, y: yArray, }; } return normalized; } _normalizeMachineCurve(rawCurve, curveUnits = this.unitPolicy.curve) { if (!rawCurve || typeof rawCurve !== 'object' || !rawCurve.nq || !rawCurve.np) { throw new Error('Machine curve is missing required nq/np sections.'); } return { nq: this._normalizeCurveSection( rawCurve.nq, curveUnits.flow, this.unitPolicy.canonical.flow, curveUnits.pressure, this.unitPolicy.canonical.pressure, 'nq' ), np: this._normalizeCurveSection( rawCurve.np, curveUnits.power, this.unitPolicy.canonical.power, curveUnits.pressure, this.unitPolicy.canonical.pressure, 'np' ), }; } isUnitValidForType(type, unit) { return this.measurements?.isUnitCompatible?.(type, unit) === true; } _resolveMeasurementUnit(type, providedUnit) { const unit = typeof providedUnit === 'string' ? providedUnit.trim() : ''; if (!unit) { throw new Error(`Missing unit for ${type} measurement.`); } if (!this.isUnitValidForType(type, unit)) { throw new Error(`Unsupported unit '${unit}' for ${type} measurement.`); } return unit; } _measurementPositionForMetric(metricId) { if (metricId === "power") return "atEquipment"; return "downstream"; } _resolveProcessRangeForMetric(metricId, predictedValue, measuredValue) { let processMin = NaN; let processMax = NaN; if (metricId === "flow") { processMin = Number(this.predictFlow?.currentFxyYMin); processMax = Number(this.predictFlow?.currentFxyYMax); } else if (metricId === "power") { processMin = Number(this.predictPower?.currentFxyYMin); processMax = Number(this.predictPower?.currentFxyYMax); } if (!Number.isFinite(processMin) || !Number.isFinite(processMax) || processMax <= processMin) { const p = Number(predictedValue); const m = Number(measuredValue); const localMin = Math.min(p, m); const localMax = Math.max(p, m); processMin = Number.isFinite(localMin) ? localMin : 0; processMax = Number.isFinite(localMax) && localMax > processMin ? localMax : processMin + 1; } return { processMin, processMax }; } _updateMetricDrift(metricId, measuredValue, context = {}) { const position = this._measurementPositionForMetric(metricId); const predictedValue = Number( this.measurements .type(metricId) .variant("predicted") .position(position) .getCurrentValue() ); const measured = Number(measuredValue); if (!Number.isFinite(predictedValue) || !Number.isFinite(measured)) return null; const { processMin, processMax } = this._resolveProcessRangeForMetric(metricId, predictedValue, measured); const timestamp = Number(context.timestamp || Date.now()); const profile = this.driftProfiles[metricId] || {}; try { const drift = this.errorMetrics.assessPoint(metricId, predictedValue, measured, { ...profile, processMin, processMax, predictedTimestamp: timestamp, measuredTimestamp: timestamp, }); if (drift && drift.valid) { if (metricId === "flow") this.flowDrift = drift; if (metricId === "power") this.powerDrift = drift; } return drift; } catch (error) { this.logger.warn(`Drift update failed for metric '${metricId}': ${error.message}`); return null; } } _updatePressureDriftStatus() { const status = this.getPressureInitializationStatus(); const flags = []; let level = 0; if (!status.initialized) { level = 2; flags.push("no_pressure_input"); } else if (!status.hasDifferential) { level = 1; flags.push("single_side_pressure"); } if (status.hasDifferential) { const upstream = this._getPreferredPressureValue("upstream"); const downstream = this._getPreferredPressureValue("downstream"); const diff = Number(downstream) - Number(upstream); if (Number.isFinite(diff) && diff < 0) { level = Math.max(level, 3); flags.push("negative_pressure_differential"); } } this.pressureDrift = { level, source: status.source, flags: flags.length ? flags : ["nominal"], }; return this.pressureDrift; } assessDrift(measurement, processMin, processMax) { const metricId = String(measurement || "").toLowerCase(); const position = this._measurementPositionForMetric(metricId); const predictedMeasurement = this.measurements.type(metricId).variant("predicted").position(position).getAllValues(); const measuredMeasurement = this.measurements.type(metricId).variant("measured").position(position).getAllValues(); if (!predictedMeasurement?.values || !measuredMeasurement?.values) return null; return this.errorMetrics.assessDrift( predictedMeasurement.values, measuredMeasurement.values, processMin, processMax, { metricId, predictedTimestamps: predictedMeasurement.timestamps, measuredTimestamps: measuredMeasurement.timestamps, ...(this.driftProfiles[metricId] || {}), } ); } _applyDriftPenalty(drift, confidence, flags, prefix) { if (!drift || !drift.valid || !Number.isFinite(drift.nrmse)) return confidence; if (drift.immediateLevel >= 3) { confidence -= 0.3; flags.push(`${prefix}_high_immediate_drift`); } else if (drift.immediateLevel === 2) { confidence -= 0.2; flags.push(`${prefix}_medium_immediate_drift`); } else if (drift.immediateLevel === 1) { confidence -= 0.1; flags.push(`${prefix}_low_immediate_drift`); } if (drift.longTermLevel >= 2) { confidence -= 0.1; flags.push(`${prefix}_long_term_drift`); } return confidence; } _updatePredictionHealth() { const status = this.getPressureInitializationStatus(); const pressureDrift = this._updatePressureDriftStatus(); const flags = [...pressureDrift.flags]; let confidence = 0; const pressureSource = status.source; if (pressureSource === "differential") { confidence = 0.9; } else if (pressureSource === "upstream" || pressureSource === "downstream") { confidence = 0.55; } else { confidence = 0.2; } if (!this._isOperationalState()) { confidence = 0; flags.push("not_operational"); } if (pressureDrift.level >= 3) confidence -= 0.35; else if (pressureDrift.level === 2) confidence -= 0.2; else if (pressureDrift.level === 1) confidence -= 0.1; const currentPosition = Number(this.state?.getCurrentPosition?.()); const { min, max } = this._resolveSetpointBounds(); if (Number.isFinite(currentPosition) && Number.isFinite(min) && Number.isFinite(max) && max > min) { const span = max - min; const edgeDistance = Math.min(Math.abs(currentPosition - min), Math.abs(max - currentPosition)); if (edgeDistance < span * 0.05) { confidence -= 0.1; flags.push("near_curve_edge"); } } confidence = this._applyDriftPenalty(this.flowDrift, confidence, flags, "flow"); confidence = this._applyDriftPenalty(this.powerDrift, confidence, flags, "power"); confidence = Math.max(0, Math.min(1, confidence)); let quality = "invalid"; if (confidence >= 0.8) quality = "high"; else if (confidence >= 0.55) quality = "medium"; else if (confidence >= 0.3) quality = "low"; this.predictionHealth = { quality, confidence, pressureSource, flags: flags.length ? Array.from(new Set(flags)) : ["nominal"], }; return this.predictionHealth; } reverseCurve(curve) { const reversedCurve = {}; for (const [pressure, values] of Object.entries(curve)) { reversedCurve[pressure] = { x: [...values.y], // Previous y becomes new x y: [...values.x] // Previous x becomes new y }; } return reversedCurve; } // -------- Config -------- // updateConfig(newConfig) { this.config = this.configUtils.updateConfig(this.config, newConfig); } // -------- Mode and Input Management -------- // isValidSourceForMode(source, mode) { const allowedSourcesSet = this.config.mode.allowedSources[mode] || []; const allowed = allowedSourcesSet.has(source); allowed? this.logger.debug(`source is allowed proceeding with ${source} for mode ${mode}`) : this.logger.warn(`${source} is not allowed in mode ${mode}`); return allowed; } isValidActionForMode(action, mode) { const allowedActionsSet = this.config.mode.allowedActions[mode] || []; const allowed = allowedActionsSet.has(action); allowed ? this.logger.debug(`Action is allowed proceeding with ${action} for mode ${mode}`) : this.logger.warn(`${action} is not allowed in mode ${mode}`); return allowed; } async handleInput(source, action, parameter) { //sanitize input if( typeof action !== 'string'){this.logger.error(`Action must be string`); return;} //convert to lower case to avoid to many mistakes in commands action = action.toLowerCase(); // check for validity of the request if(!this.isValidActionForMode(action,this.currentMode)){return ;} if (!this.isValidSourceForMode(source, this.currentMode)) {return ;} this.logger.info(`Handling input from source '${source}' with action '${action}' in mode '${this.currentMode}'.`); try { switch (action) { case "execsequence": return await this.executeSequence(parameter); case "execmovement": return await this.setpoint(parameter); case "entermaintenance": return await this.executeSequence(parameter); case "exitmaintenance": return await this.executeSequence(parameter); case "flowmovement": // External flow setpoint is interpreted in configured output flow unit. const canonicalFlowSetpoint = this._convertUnitValue( parameter, this.unitPolicy.output.flow, this.unitPolicy.canonical.flow, 'flowmovement setpoint' ); // Calculate the control value for a desired flow const pos = this.calcCtrl(canonicalFlowSetpoint); // Move to the desired setpoint return await this.setpoint(pos); case "emergencystop": this.logger.warn(`Emergency stop activated by '${source}'.`); return await this.executeSequence("emergencyStop"); 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}`); } } abortMovement(reason = "group override") { if (this.state?.abortCurrentMovement) { this.state.abortCurrentMovement(reason); } } setMode(newMode) { const availableModes = this.defaultConfig.mode.current.rules.values.map(v => v.value); 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}'.`); } // -------- 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 } } //recalc flow and power this.updatePosition(); } async setpoint(setpoint) { try { // Validate and normalize setpoint if (!Number.isFinite(setpoint)) { this.logger.error("Invalid setpoint: Setpoint must be a finite number."); return; } const { min, max } = this._resolveSetpointBounds(); const constrainedSetpoint = Math.min(Math.max(setpoint, min), max); if (constrainedSetpoint !== setpoint) { this.logger.warn(`Requested setpoint ${setpoint} constrained to ${constrainedSetpoint} (min=${min}, max=${max})`); } this.logger.info(`Setting setpoint to ${constrainedSetpoint}. Current position: ${this.state.getCurrentPosition()}`); // Move to the desired setpoint await this.state.moveTo(constrainedSetpoint); } catch (error) { this.logger.error(`Error setting setpoint: ${error}`); } } _resolveSetpointBounds() { const stateMin = Number(this.state?.movementManager?.minPosition); const stateMax = Number(this.state?.movementManager?.maxPosition); const curveMin = Number(this.predictFlow?.currentFxyXMin); const curveMax = Number(this.predictFlow?.currentFxyXMax); const minCandidates = [stateMin, curveMin].filter(Number.isFinite); const maxCandidates = [stateMax, curveMax].filter(Number.isFinite); const fallbackMin = Number.isFinite(stateMin) ? stateMin : 0; const fallbackMax = Number.isFinite(stateMax) ? stateMax : 100; let min = minCandidates.length ? Math.max(...minCandidates) : fallbackMin; let max = maxCandidates.length ? Math.min(...maxCandidates) : fallbackMax; if (min > max) { this.logger.warn(`Invalid setpoint bounds detected (min=${min}, max=${max}). Falling back to movement bounds.`); min = fallbackMin; max = fallbackMax; } return { min, max }; } // Calculate flow based on current pressure and position calcFlow(x) { if(this.hasCurve) { if (!this._isOperationalState()) { this.measurements.type("flow").variant("predicted").position("downstream").value(0,Date.now(),this.unitPolicy.canonical.flow); this.measurements.type("flow").variant("predicted").position("atEquipment").value(0,Date.now(),this.unitPolicy.canonical.flow); this.logger.debug(`Machine is not operational. Setting predicted flow to 0.`); return 0; } const cFlow = this.predictFlow.y(x); this.measurements.type("flow").variant("predicted").position("downstream").value(cFlow,Date.now(),this.unitPolicy.canonical.flow); this.measurements.type("flow").variant("predicted").position("atEquipment").value(cFlow,Date.now(),this.unitPolicy.canonical.flow); //this.logger.debug(`Calculated flow: ${cFlow} for pressure: ${this.getMeasuredPressure()} and position: ${x}`); return cFlow; } // If no curve data is available, log a warning and return 0 this.logger.warn(`No curve data available for flow calculation. Returning 0.`); this.measurements.type("flow").variant("predicted").position("downstream").value(0, Date.now(),this.unitPolicy.canonical.flow); this.measurements.type("flow").variant("predicted").position("atEquipment").value(0, Date.now(),this.unitPolicy.canonical.flow); return 0; } // Calculate power based on current pressure and position calcPower(x) { if(this.hasCurve) { if (!this._isOperationalState()) { this.measurements.type("power").variant("predicted").position('atEquipment').value(0, Date.now(), this.unitPolicy.canonical.power); this.logger.debug(`Machine is not operational. Setting predicted power to 0.`); return 0; } //this.predictPower.currentX = x; Decrepated const cPower = this.predictPower.y(x); this.measurements.type("power").variant("predicted").position('atEquipment').value(cPower, Date.now(), this.unitPolicy.canonical.power); //this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`); return cPower; } // If no curve data is available, log a warning and return 0 this.logger.warn(`No curve data available for power calculation. Returning 0.`); this.measurements.type("power").variant("predicted").position('atEquipment').value(0, Date.now(), this.unitPolicy.canonical.power); return 0; } // calculate the power consumption using only flow and pressure inputFlowCalcPower(flow) { if(this.hasCurve) { this.predictCtrl.currentX = flow; const cCtrl = this.predictCtrl.y(flow); this.predictPower.currentX = cCtrl; const cPower = this.predictPower.y(cCtrl); return cPower; } // If no curve data is available, log a warning and return 0 this.logger.warn(`No curve data available for power calculation. Returning 0.`); this.measurements.type("power").variant("predicted").position('atEquipment').value(0, Date.now(), this.unitPolicy.canonical.power); return 0; } // Function to predict control value for a desired flow calcCtrl(x) { if(this.hasCurve) { this.predictCtrl.currentX = x; const cCtrl = this.predictCtrl.y(x); this.measurements.type("ctrl").variant("predicted").position('atEquipment').value(cCtrl); //this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`); return cCtrl; } // If no curve data is available, log a warning and return 0 this.logger.warn(`No curve data available for control calculation. Returning 0.`); this.measurements.type("ctrl").variant("predicted").position('atEquipment').value(0, Date.now()); return 0; } // returns the best available pressure measurement to use in the prediction calculation // this will be either the differential pressure, downstream or upstream pressure getMeasuredPressure() { if(this.hasCurve === false){ this.logger.error(`No valid curve available to calculate prediction using last known pressure`); return 0; } const upstreamPressure = this._getPreferredPressureValue("upstream"); const downstreamPressure = this._getPreferredPressureValue("downstream"); // Both upstream & downstream => differential if (upstreamPressure != null && downstreamPressure != null) { const pressureDiffValue = downstreamPressure - upstreamPressure; this.logger.debug(`Pressure differential: ${pressureDiffValue}`); this.predictFlow.fDimension = pressureDiffValue; this.predictPower.fDimension = pressureDiffValue; this.predictCtrl.fDimension = pressureDiffValue; //update the cog const { cog, minEfficiency } = this.calcCog(); // calc efficiency const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted"); //update the distance from peak this.calcDistanceBEP(efficiency,cog,minEfficiency); return pressureDiffValue; } // Only downstream => use it, warn that it's partial if (downstreamPressure != null) { this.logger.warn(`Using downstream pressure only for prediction: ${downstreamPressure} This is less acurate!!`); this.predictFlow.fDimension = downstreamPressure; this.predictPower.fDimension = downstreamPressure; this.predictCtrl.fDimension = downstreamPressure; //update the cog const { cog, minEfficiency } = this.calcCog(); // calc efficiency const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted"); //update the distance from peak this.calcDistanceBEP(efficiency,cog,minEfficiency); return downstreamPressure; } // Only upstream => use it, warn that it's partial if (upstreamPressure != null) { this.logger.warn(`Using upstream pressure only for prediction: ${upstreamPressure} This is less acurate!!`); this.predictFlow.fDimension = upstreamPressure; this.predictPower.fDimension = upstreamPressure; this.predictCtrl.fDimension = upstreamPressure; //update the cog const { cog, minEfficiency } = this.calcCog(); // calc efficiency const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted"); //update the distance from peak this.calcDistanceBEP(efficiency,cog,minEfficiency); return upstreamPressure; } this.logger.error(`No valid pressure measurements available to calculate prediction using last known pressure`); //set default at 0 => lowest pressure possible this.predictFlow.fDimension = 0; this.predictPower.fDimension = 0; this.predictCtrl.fDimension = 0; //update the cog const { cog, minEfficiency } = this.calcCog(); // calc efficiency const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted"); //update the distance from peak this.calcDistanceBEP(efficiency,cog,minEfficiency); //place min and max flow capabilities in containerthis.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin this.measurements.type('flow').variant('predicted').position('max').value(this.predictFlow.currentFxyYMax, Date.now(), this.unitPolicy.canonical.flow); this.measurements.type('flow').variant('predicted').position('min').value(this.predictFlow.currentFxyYMin, Date.now(), this.unitPolicy.canonical.flow); return 0; } _getPreferredPressureValue(position) { const realIds = Array.from(this.realPressureChildIds[position] || []); for (const childId of realIds) { const value = this.measurements .type("pressure") .variant("measured") .position(position) .child(childId) .getCurrentValue(); if (value != null) return value; } const virtualId = this.virtualPressureChildIds[position]; if (virtualId) { const simulatedValue = this.measurements .type("pressure") .variant("measured") .position(position) .child(virtualId) .getCurrentValue(); if (simulatedValue != null) return simulatedValue; } return this.measurements .type("pressure") .variant("measured") .position(position) .getCurrentValue(); } getPressureInitializationStatus() { const upstreamPressure = this._getPreferredPressureValue("upstream"); const downstreamPressure = this._getPreferredPressureValue("downstream"); const hasUpstream = upstreamPressure != null; const hasDownstream = downstreamPressure != null; const hasDifferential = hasUpstream && hasDownstream; return { hasUpstream, hasDownstream, hasDifferential, initialized: hasUpstream || hasDownstream || hasDifferential, source: hasDifferential ? 'differential' : hasDownstream ? 'downstream' : hasUpstream ? 'upstream' : null, }; } updateSimulatedMeasurement(type, position, value, context = {}) { const normalizedType = String(type || "").toLowerCase(); const normalizedPosition = String(position || "atEquipment").toLowerCase(); if (normalizedType !== "pressure") { this._callMeasurementHandler(normalizedType, value, normalizedPosition, context); return; } if (!this.virtualPressureChildIds[normalizedPosition]) { this.logger.warn(`Unsupported simulated pressure position '${normalizedPosition}'`); return; } const child = this.virtualPressureChildren[normalizedPosition]; if (!child?.measurements) { this.logger.error(`Virtual pressure child '${normalizedPosition}' is missing`); return; } let measurementUnit; try { measurementUnit = this._resolveMeasurementUnit('pressure', context.unit); } catch (error) { this.logger.warn(`Rejected simulated pressure measurement: ${error.message}`); return; } child.measurements .type("pressure") .variant("measured") .position(normalizedPosition) .value(value, context.timestamp || Date.now(), measurementUnit); } handleMeasuredFlow() { const flowDiff = this.measurements.type('flow').variant('measured').difference(); // If both are present if (flowDiff != null) { // In theory, mass flow in = mass flow out, so they should match or be close. if (flowDiff.value < 0.001) { // flows match within tolerance this.logger.debug(`Flow match: ${flowDiff.value}`); return flowDiff.value; } else { // Mismatch => decide how to handle. Maybe take the average? // Or bail out with an error. Example: we bail out here. this.logger.error(`Something wrong with down or upstream flow measurement. Bailing out!`); return null; } } // get const upstreamFlow = this.measurements.type('flow').variant('measured').position('upstream').getCurrentValue(); // Only upstream => might still accept it, but warn if (upstreamFlow != null) { this.logger.warn(`Only upstream flow is present. Using it but results may be incomplete!`); return upstreamFlow; } // get const downstreamFlow = this.measurements.type('flow').variant('measured').position('downstream').getCurrentValue(); // Only downstream => might still accept it, but warn if (downstreamFlow != null) { this.logger.warn(`Only downstream flow is present. Using it but results may be incomplete!`); return downstreamFlow; } // Neither => error this.logger.error(`No upstream or downstream flow measurement. Bailing out!`); return null; } handleMeasuredPower() { const power = this.measurements.type("power").variant("measured").position("atEquipment").getCurrentValue(); // If your system calls it "upstream" or just a single "value", adjust accordingly if (power != null) { this.logger.debug(`Measured power: ${power}`); return power; } else { this.logger.error(`No measured power found. Bailing out!`); return null; } } updateMeasuredTemperature(value, position, context = {}) { this.logger.debug(`Temperature update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`); let measurementUnit; try { measurementUnit = this._resolveMeasurementUnit('temperature', context.unit); } catch (error) { this.logger.warn(`Rejected temperature update: ${error.message}`); return; } this.measurements.type("temperature").variant("measured").position(position || 'atEquipment').child(context.childId).value(value, context.timestamp, measurementUnit); } // context handler for pressure updates updateMeasuredPressure(value, position, context = {}) { this.logger.debug(`Pressure update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`); let measurementUnit; try { measurementUnit = this._resolveMeasurementUnit('pressure', context.unit); } catch (error) { this.logger.warn(`Rejected pressure update: ${error.message}`); return; } // Store in parent's measurement container this.measurements.type("pressure").variant("measured").position(position).child(context.childId).value(value, context.timestamp, measurementUnit); // Determine what kind of value to use as pressure (upstream , downstream or difference) const pressure = this.getMeasuredPressure(); this.updatePosition(); this._updatePressureDriftStatus(); this._updatePredictionHealth(); this.logger.debug(`Using pressure: ${pressure} for calculations`); } // NEW: Flow handler updateMeasuredFlow(value, position, context = {}) { if (!this._isOperationalState()) { this.logger.warn(`Machine not operational, skipping flow update from ${context.childName || 'unknown'}`); return; } this.logger.debug(`Flow update: ${value} at ${position} from ${context.childName || 'child'}`); let measurementUnit; try { measurementUnit = this._resolveMeasurementUnit('flow', context.unit); } catch (error) { this.logger.warn(`Rejected flow update: ${error.message}`); return; } // Store in parent's measurement container this.measurements.type("flow").variant("measured").position(position).child(context.childId).value(value, context.timestamp, measurementUnit); // Update predicted flow if you have prediction capability if (this.predictFlow) { this.measurements.type("flow").variant("predicted").position("downstream").value(this.predictFlow.outputY || 0, Date.now(), this.unitPolicy.canonical.flow); this.measurements.type("flow").variant("predicted").position("atEquipment").value(this.predictFlow.outputY || 0, Date.now(), this.unitPolicy.canonical.flow); } const measuredCanonical = this.measurements .type("flow") .variant("measured") .position(position) .getCurrentValue(this.unitPolicy.canonical.flow); this._updateMetricDrift("flow", measuredCanonical, context); this._updatePredictionHealth(); } updateMeasuredPower(value, position, context = {}) { if (!this._isOperationalState()) { this.logger.warn(`Machine not operational, skipping power update from ${context.childName || 'unknown'}`); return; } this.logger.debug(`Power update: ${value} at ${position} from ${context.childName || 'child'}`); let measurementUnit; try { measurementUnit = this._resolveMeasurementUnit('power', context.unit); } catch (error) { this.logger.warn(`Rejected power update: ${error.message}`); return; } this.measurements.type("power").variant("measured").position(position).child(context.childId).value(value, context.timestamp, measurementUnit); if (this.predictPower) { this.measurements.type("power").variant("predicted").position("atEquipment").value(this.predictPower.outputY || 0, Date.now(), this.unitPolicy.canonical.power); } const measuredCanonical = this.measurements .type("power") .variant("measured") .position(position) .getCurrentValue(this.unitPolicy.canonical.power); this._updateMetricDrift("power", measuredCanonical, context); this._updatePredictionHealth(); } // Helper method for operational state check _isOperationalState() { const state = this.state.getCurrentState(); const activeStates = ["operational", "warmingup", "accelerating", "decelerating"]; this.logger.debug(`Checking operational state ${this.state.getCurrentState()} ? ${activeStates.includes(state)}`); return activeStates.includes(state); } //what is the internal functions that need updating when something changes that has influence on this. updatePosition() { if (this._isOperationalState()) { const currentPosition = this.state.getCurrentPosition(); // Update the predicted values based on the new position const { cPower, cFlow } = this.calcFlowPower(currentPosition); // Calc predicted efficiency const efficiency = this.calcEfficiency(cPower, cFlow, "predicted"); //update the cog const { cog, minEfficiency } = this.calcCog(); //update the distance from peak this.calcDistanceBEP(efficiency,cog,minEfficiency); } this._updatePredictionHealth(); } calcDistanceFromPeak(currentEfficiency,peakEfficiency){ return Math.abs(currentEfficiency - peakEfficiency); } calcRelativeDistanceFromPeak(currentEfficiency,maxEfficiency,minEfficiency){ let distance = 1; if(currentEfficiency != null){ distance = this.interpolation.interpolate_lin_single_point(currentEfficiency,maxEfficiency, minEfficiency, 0, 1); } return distance; } showWorkingCurves() { // Show the current curves for debugging const { powerCurve, flowCurve } = this.getCurrentCurves(); return { powerCurve: powerCurve, flowCurve: flowCurve, cog: this.cog, cogIndex: this.cogIndex, NCog: this.NCog, minEfficiency: this.minEfficiency, currentEfficiencyCurve: this.currentEfficiencyCurve, absDistFromPeak: this.absDistFromPeak, relDistFromPeak: this.relDistFromPeak }; } // Calculate the center of gravity for current pressure calcCog() { //fetch current curve data for power and flow const { powerCurve, flowCurve } = this.getCurrentCurves(); const {efficiencyCurve, peak, peakIndex, minEfficiency } = this.calcEfficiencyCurve(powerCurve, flowCurve); // Calculate the normalized center of gravity const NCog = (flowCurve.y[peakIndex] - this.predictFlow.currentFxyYMin) / (this.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin); // //store in object for later retrieval this.currentEfficiencyCurve = efficiencyCurve; this.cog = peak; this.cogIndex = peakIndex; this.NCog = NCog; this.minEfficiency = minEfficiency; return { cog: peak, cogIndex: peakIndex, NCog: NCog, minEfficiency: minEfficiency }; } calcEfficiencyCurve(powerCurve, flowCurve) { const efficiencyCurve = []; let peak = 0; let peakIndex = 0; let minEfficiency = 0; // Calculate efficiency curve based on power and flow curves powerCurve.y.forEach((power, index) => { // Get flow for the current power const flow = flowCurve.y[index]; // higher efficiency is better efficiencyCurve.push( Math.round( ( flow / power ) * 100 ) / 100); // Keep track of peak efficiency peak = Math.max(peak, efficiencyCurve[index]); peakIndex = peak == efficiencyCurve[index] ? index : peakIndex; minEfficiency = Math.min(...efficiencyCurve); }); return { efficiencyCurve, peak, peakIndex, minEfficiency }; } //calc flow power based on pressure and current position calcFlowPower(x) { // Calculate flow and power const cFlow = this.calcFlow(x); const cPower = this.calcPower(x); return { cPower, cFlow }; } calcEfficiency(power,flow,variant) { // Request a pressure differential explicitly in Pascal for hydraulic efficiency. const pressureDiff = this.measurements .type('pressure') .variant('measured') .difference({ unit: 'Pa' }); const g = gravity.getStandardGravity(); const temp = this.measurements.type('temperature').variant('measured').position('atEquipment').getCurrentValue('K'); const atmPressure = this.measurements.type('atmPressure').variant('measured').position('atEquipment').getCurrentValue('Pa'); let rho = null; try { rho = coolprop.PropsSI('D', 'T', temp, 'P', atmPressure, 'WasteWater'); } catch (error) { // coolprop can throw transient initialization errors; keep machine calculations running. this.logger.warn(`CoolProp density lookup failed: ${error.message}. Using fallback density.`); rho = 1000; // kg/m3 fallback for water-like fluids } this.logger.debug(`temp: ${temp} atmPressure : ${atmPressure} rho : ${rho} pressureDiff: ${pressureDiff?.value || 0}`); const flowM3s = this.measurements.type('flow').variant('predicted').position('atEquipment').getCurrentValue('m3/s'); const powerWatt = this.measurements.type('power').variant('predicted').position('atEquipment').getCurrentValue('W'); this.logger.debug(`Flow : ${flowM3s} power: ${powerWatt}`); if (power != 0 && flow != 0) { const specificFlow = flow / power; const specificEnergyConsumption = power / flow; this.measurements.type("efficiency").variant(variant).position('atEquipment').value(specificFlow); this.measurements.type("specificEnergyConsumption").variant(variant).position('atEquipment').value(specificEnergyConsumption); if (pressureDiff?.value != null && Number.isFinite(flowM3s) && Number.isFinite(powerWatt) && powerWatt > 0) { // Engineering references: P_h = Q * Δp = ρ g Q H, η_h = P_h / P_in const pressureDiffPa = Number(pressureDiff.value); const headMeters = (Number.isFinite(rho) && rho > 0) ? pressureDiffPa / (rho * g) : null; const hydraulicPowerW = pressureDiffPa * flowM3s; const nHydraulicEfficiency = hydraulicPowerW / powerWatt; if (Number.isFinite(headMeters)) { this.measurements.type("pumpHead").variant(variant).position('atEquipment').value(headMeters, Date.now(), 'm'); } this.measurements.type("hydraulicPower").variant(variant).position('atEquipment').value(hydraulicPowerW, Date.now(), 'W'); this.measurements.type("nHydraulicEfficiency").variant(variant).position('atEquipment').value(nHydraulicEfficiency); } } //change this to nhydrefficiency ? return this.measurements.type("efficiency").variant(variant).position('atEquipment').getCurrentValue(); } updateCurve(newCurve) { this.logger.info(`Updating machine curve`); const normalizedCurve = this._normalizeMachineCurve(newCurve); const newConfig = { asset: { machineCurve: normalizedCurve, curveUnits: this.unitPolicy.curve, }, }; //validate input of new curve fed to the machine this.config = this.configUtils.updateConfig(this.config, newConfig); //After we passed validation load the curves into their predictors this.predictFlow.updateCurve(this.config.asset.machineCurve.nq); this.predictPower.updateCurve(this.config.asset.machineCurve.np); this.predictCtrl.updateCurve(this.reverseCurve(this.config.asset.machineCurve.nq)); } getCompleteCurve() { const powerCurve = this.predictPower.inputCurveData; const flowCurve = this.predictFlow.inputCurveData; return { powerCurve, flowCurve }; } getCurrentCurves() { const powerCurve = this.predictPower.currentFxyCurve[this.predictPower.currentF]; const flowCurve = this.predictFlow.currentFxyCurve[this.predictFlow.currentF]; return { powerCurve, flowCurve }; } calcDistanceBEP(efficiency,maxEfficiency,minEfficiency) { const absDistFromPeak = this.calcDistanceFromPeak(efficiency,maxEfficiency); const relDistFromPeak = this.calcRelativeDistanceFromPeak(efficiency,maxEfficiency,minEfficiency); //store internally this.absDistFromPeak = absDistFromPeak ; this.relDistFromPeak = relDistFromPeak; return { absDistFromPeak: absDistFromPeak, relDistFromPeak: relDistFromPeak }; } getOutput() { // Improved output object generation const output = this.measurements.getFlattenedOutput({ requestedUnits: this.unitPolicy.output, }); //fill in the rest of the output object output["state"] = this.state.getCurrentState(); output["runtime"] = this.state.getRunTimeHours(); output["ctrl"] = this.state.getCurrentPosition(); output["moveTimeleft"] = this.state.getMoveTimeLeft(); output["mode"] = this.currentMode; output["cog"] = this.cog; // flow / power efficiency output["NCog"] = this.NCog; // normalized cog output["NCogPercent"] = Math.round(this.NCog * 100 * 100) / 100 ; output["maintenanceTime"] = this.state.getMaintenanceTimeHours(); if(this.flowDrift != null){ const flowDrift = this.flowDrift; output["flowNrmse"] = flowDrift.nrmse; output["flowLongterNRMSD"] = flowDrift.longTermNRMSD; output["flowLongTermNRMSD"] = flowDrift.longTermNRMSD; output["flowImmediateLevel"] = flowDrift.immediateLevel; output["flowLongTermLevel"] = flowDrift.longTermLevel; output["flowDriftValid"] = flowDrift.valid; } if(this.powerDrift != null){ const powerDrift = this.powerDrift; output["powerNrmse"] = powerDrift.nrmse; output["powerLongTermNRMSD"] = powerDrift.longTermNRMSD; output["powerImmediateLevel"] = powerDrift.immediateLevel; output["powerLongTermLevel"] = powerDrift.longTermLevel; output["powerDriftValid"] = powerDrift.valid; } output["pressureDriftLevel"] = this.pressureDrift.level; output["pressureDriftSource"] = this.pressureDrift.source; output["pressureDriftFlags"] = this.pressureDrift.flags; output["predictionQuality"] = this.predictionHealth.quality; output["predictionConfidence"] = Math.round(this.predictionHealth.confidence * 1000) / 1000; output["predictionPressureSource"] = this.predictionHealth.pressureSource; output["predictionFlags"] = this.predictionHealth.flags; //should this all go in the container of measurements? output["effDistFromPeak"] = this.absDistFromPeak; output["effRelDistFromPeak"] = this.relDistFromPeak; //this.logger.debug(`Output: ${JSON.stringify(output)}`); return output; } } // end of class module.exports = Machine;