diff --git a/rotatingMachine.html b/rotatingMachine.html index 93473b4..28e210c 100644 --- a/rotatingMachine.html +++ b/rotatingMachine.html @@ -24,8 +24,8 @@ warmup: { value: 0 }, shutdown: { value: 0 }, cooldown: { value: 0 }, - machineCurve: { value: {}}, - flowNumber: { value: 1, required: true }, + movementMode : { value: "staticspeed" }, // static or dynamic + machineCurve : { value: {}}, //define asset properties uuid: { value: "" }, @@ -55,7 +55,7 @@ icon: "font-awesome/fa-cog", label: function () { - return this.positionIcon + " " + this.category.slice(0, -1) || "Machine"; + return this.positionIcon + " " + this.category || "Machine"; }, oneditprepare: function() { @@ -75,6 +75,10 @@ document.getElementById("node-input-warmup"); document.getElementById("node-input-shutdown"); document.getElementById("node-input-cooldown"); + const movementMode = document.getElementById("node-input-movementMode"); + if (movementMode) { + movementMode.value = this.movementMode || "staticspeed"; + } }, oneditsave: function() { @@ -100,6 +104,9 @@ node[field] = value; }); + node.movementMode = document.getElementById("node-input-movementMode").value; + console.log(`----------------> Saving movementMode: ${node.movementMode}`); + } }); @@ -129,8 +136,11 @@
- - + +
diff --git a/src/nodeClass.js b/src/nodeClass.js index 7bb504e..6c73b48 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -77,6 +77,8 @@ class nodeClass { _setupSpecificClass(uiConfig) { const machineConfig = this.config; + console.log(`----------------> Loaded movementMode in nodeClass: ${uiConfig.movementMode}`); + // need extra state for this const stateConfig = { general: { @@ -86,7 +88,8 @@ class nodeClass { } }, movement: { - speed: Number(uiConfig.speed) + speed: Number(uiConfig.speed), + mode: uiConfig.movementMode }, time: { starting: Number(uiConfig.startup), @@ -115,8 +118,8 @@ class nodeClass { try { const mode = m.currentMode; const state = m.state.getCurrentState(); - const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').getCurrentValue()); - const power = Math.round(m.measurements.type("power").variant("predicted").position('upstream').getCurrentValue()); + const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').getCurrentValue('m3/h')); + const power = Math.round(m.measurements.type("power").variant("predicted").position('atequipment').getCurrentValue('kW')); let symbolState; switch(state){ case "off": @@ -146,6 +149,9 @@ class nodeClass { case "decelerating": symbolState = "⏪"; break; + case "maintenance": + symbolState = "🔧"; + break; } const position = m.state.getCurrentPosition(); const roundedPosition = Math.round(position * 100) / 100; @@ -225,8 +231,8 @@ class nodeClass { //this.source.tick(); const raw = this.source.getOutput(); - const processMsg = this._output.formatMsg(raw, this.config, 'process'); - const influxMsg = this._output.formatMsg(raw, this.config, 'influxdb'); + const processMsg = this._output.formatMsg(raw, this.source.config, 'process'); + const influxMsg = this._output.formatMsg(raw, this.source.config, 'influxdb'); // Send only updated outputs on ports 0 & 1 this.node.send([processMsg, influxMsg]); diff --git a/src/specificClass.js b/src/specificClass.js index 7652922..b80f66b 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -1,6 +1,5 @@ const EventEmitter = require('events'); -const {loadCurve,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils} = require('generalFunctions'); -const { name } = require('../../generalFunctions/src/convert/lodash/lodash._shimkeys'); +const {loadCurve,gravity,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils,coolprop} = require('generalFunctions'); class Machine { @@ -17,7 +16,7 @@ class Machine { // Load a specific curve this.model = machineConfig.asset.model; // Get the model from the machineConfig - this.curve = this.model ? loadCurve(this.model) : null; + this.curve = this.model ? loadCurve(this.model) : null; // we need to convert the curve and add units to the curve information //Init config and check if it is valid this.config = this.configUtils.initConfig(machineConfig); @@ -35,10 +34,8 @@ class Machine { } else{ this.hasCurve = true; - 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.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) @@ -48,7 +45,17 @@ class Machine { this.errorMetrics = new nrmse(errorMetricsConfig, this.logger); // Initialize measurements - this.measurements = new MeasurementContainer(); + this.measurements = new MeasurementContainer({ + autoConvert: true, + windowSize: 50, + defaultUnits: { + pressure: 'mbar', + flow: this.config.general.unit, + power: 'kW', + temperature: 'C' + } + }); + this.interpolation = new interpolation(); this.flowDrift = null; @@ -68,6 +75,16 @@ class Machine { 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(); + // used for holding the source and sink unit operations or other object with setInfluent / getEffluent method for e.g. recirculation. this.upstreamSource = null; this.downstreamSink = null; @@ -75,6 +92,27 @@ class Machine { this.child = {}; // object to hold child information so we know on what to subscribe this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility + + } + + _init(){ + //assume standard temperature is 20degrees + this.measurements.type('temperature').variant('measured').position('atEquipment').value(15).unit('C'); + //assume standard atm pressure is at sea level + this.measurements.type('atmPressure').variant('measured').position('atEquipment').value(101325).unit('Pa'); + //populate min and max + const flowunit = this.config.general.unit; + 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).unit(this.config.general.unit); + } + + _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.config.general.unit); + this.measurements.type("flow").variant("predicted").position("atEquipment").value(0,Date.now(),this.config.general.unit); + } } /*------------------- Register child events -------------------*/ @@ -105,6 +143,48 @@ class Machine { //rebuild to measurementype.variant no position and then switch based on values not strings or names. const eventName = `${measurementType}.measured.${position}`; + this.logger.debug(`Setting up listener for ${eventName} from child ${child.config.general.name}`); + // Register event listener for measurement updates + child.measurements.emitter.on(eventName, (eventData) => { + this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`); + + + this.logger.debug(` Emitting... ${eventName} with data:`); + // Store directly in parent's measurement container + this.measurements + .type(measurementType) + .variant("measured") + .position(position) + .value(eventData.value, eventData.timestamp, eventData.unit); + + // Call the appropriate handler + this._callMeasurementHandler(measurementType, eventData.value, position, eventData); + }); + } + } + +// 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 '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; + } +} this.logger.debug(`Setting up listener for ${eventName} from child ${measurementChild.config.general.name}`); // Register event listener for measurement updates measurementChild.measurements.emitter.on(eventName, (eventData) => { @@ -176,43 +256,65 @@ class Machine { // -------- Mode and Input Management -------- // isValidSourceForMode(source, mode) { const allowedSourcesSet = this.config.mode.allowedSources[mode] || []; - return allowedSourcesSet.has(source); + 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] || []; - return allowedActionsSet.has(action); + 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) { - 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}; - } + //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": + + case "execsequence": return await this.executeSequence(parameter); - case "execMovement": + case "execmovement": return await this.setpoint(parameter); - case "flowMovement": + case "entermaintenance": + + return await this.executeSequence(parameter); + + + case "exitmaintenance": + return await this.executeSequence(parameter); + + case "flowmovement": // Calculate the control value for a desired flow const pos = this.calcCtrl(parameter); // Move to the desired setpoint return await this.setpoint(pos); - case "emergencyStop": + case "emergencystop": this.logger.warn(`Emergency stop activated by '${source}'.`); return await this.executeSequence("emergencyStop"); - case "statusCheck": + case "statuscheck": this.logger.info(`Status Check: Mode = '${this.currentMode}', Source = '${source}'.`); break; @@ -299,21 +401,23 @@ class Machine { calcFlow(x) { if(this.hasCurve) { if (!this._isOperationalState()) { - this.measurements.type("flow").variant("predicted").position("downstream").value(0); + this.measurements.type("flow").variant("predicted").position("downstream").value(0,Date.now(),this.config.general.unit); + this.measurements.type("flow").variant("predicted").position("atEquipment").value(0,Date.now(),this.config.general.unit); this.logger.debug(`Machine is not operational. Setting predicted flow to 0.`); return 0; } - //this.predictFlow.currentX = x; Decrepated const cFlow = this.predictFlow.y(x); - this.measurements.type("flow").variant("predicted").position("downstream").value(cFlow); + this.measurements.type("flow").variant("predicted").position("downstream").value(cFlow,Date.now(),this.config.general.unit); + this.measurements.type("flow").variant("predicted").position("atEquipment").value(cFlow,Date.now(),this.config.general.unit); //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); + this.measurements.type("flow").variant("predicted").position("downstream").value(0, Date.now(),this.config.general.unit); + this.measurements.type("flow").variant("predicted").position("atEquipment").value(0, Date.now(),this.config.general.unit); return 0; } @@ -378,6 +482,11 @@ class Machine { // 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 pressureDiff = this.measurements.type('pressure').variant('measured').difference(); // Both upstream & downstream => differential @@ -426,6 +535,9 @@ class Machine { 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).unit(this.config.general.unit); + this.measurements.type('flow').variant('predicted').position('min').value(this.predictFlow.currentFxyYMin).unit(this.config.general.unit); return 0; } @@ -517,6 +629,7 @@ class Machine { // Update predicted flow if you have prediction capability if (this.predictFlow) { + this.measurements.type("flow").variant("predicted").position("downstream").value(this.predictFlow.outputY || 0); this.measurements.type("flow").variant("predicted").position("atEquipment").value(this.predictFlow.outputY || 0); } } @@ -530,6 +643,7 @@ class Machine { // Helper method for operational state check _isOperationalState() { const state = this.state.getCurrentState(); + this.logger.debug(`Checking operational state ${this.state.getCurrentState()} ? ${["operational", "accelerating", "decelerating"].includes(state)}`); return ["operational", "accelerating", "decelerating"].includes(state); } @@ -553,6 +667,7 @@ class Machine { this.calcDistanceBEP(efficiency,cog,minEfficiency); } + } calcDistanceFromPeak(currentEfficiency,peakEfficiency){ @@ -583,7 +698,6 @@ class Machine { }; } - // Calculate the center of gravity for current pressure calcCog() { @@ -593,7 +707,7 @@ class Machine { 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); + const NCog = (flowCurve.y[peakIndex] - this.predictFlow.currentFxyYMin) / (this.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin); // //store in object for later retrieval this.currentEfficiencyCurve = efficiencyCurve; @@ -643,15 +757,38 @@ class Machine { return { cPower, cFlow }; } - calcEfficiency(power, flow, variant) { + calcEfficiency(power,flow,variant) { + + const pressureDiff = this.measurements.type('pressure').variant('measured').difference('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'); + + console.log(`--------------------calc efficiency : Pressure diff:${pressureDiff},${temp}, ${g} `); + const rho = coolprop.PropsSI('D', 'T', temp, 'P', atmPressure, 'WasteWater'); + + + 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) { - // Calculate efficiency after measurements update - this.measurements.type("efficiency").variant(variant).position('atEquipment').value((flow / power)); - } else { - this.measurements.type("efficiency").variant(variant).position('atEquipment').value(null); - } + 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 && flowM3s != null && powerWatt != null){ + const meterPerBar = pressureDiff.value / rho * g; + const nHydraulicEfficiency = rho * g * flowM3s * (pressureDiff.value * meterPerBar ) / powerWatt; + this.measurements.type("nHydraulicEfficiency").variant(variant).position('atEquipment').value(nHydraulicEfficiency); + } + + } + + //change this to nhydrefficiency ? return this.measurements.type("efficiency").variant(variant).position('atEquipment').getCurrentValue(); } @@ -698,26 +835,8 @@ class Machine { getOutput() { // Improved output object generation - const output = {}; - //build the output object - this.measurements.getTypes().forEach(type => { - this.measurements.getVariants(type).forEach(variant => { - const downstreamVal = this.measurements.type(type).variant(variant).position("downstream").getCurrentValue(); - const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue(); - - if (downstreamVal != null) { - output[`downstream_${variant}_${type}`] = downstreamVal; - } - if (upstreamVal != null) { - output[`upstream_${variant}_${type}`] = upstreamVal; - } - if (downstreamVal != null && upstreamVal != null) { - const diffVal = this.measurements.type(type).variant(variant).difference().value; - output[`differential_${variant}_${type}`] = diffVal; - } - }); - }); + const output = this.measurements.getFlattenedOutput(); //fill in the rest of the output object output["state"] = this.state.getCurrentState(); @@ -728,6 +847,7 @@ class Machine { 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; @@ -751,8 +871,8 @@ class Machine { module.exports = Machine; /*------------------- Testing -------------------*/ -/* +/* curve = require('C:/Users/zn375/.node-red/public/fallbackData.json'); //import a child