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