Compare commits

..

30 Commits

Author SHA1 Message Date
Rene De Ren
4cf46f33c9 Expose output format selectors in editor 2026-03-12 16:39:25 +01:00
Rene De Ren
7b9fdd7342 fix: correct logging config path and child registration ID
Fixed eneableLog typo accessing wrong config path — now uses
machineConfig.general.logging.enabled/logLevel. Changed _registerChild
to use this.node.id consistent with all other nodes. Removed debug console.log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 09:33:28 +01:00
Rene De Ren
bb986c2dc8 refactor: adopt POSITIONS constants and fix ESLint warnings
Replace hardcoded position strings with POSITIONS.* constants.
Prefix unused variables with _ to resolve no-unused-vars warnings.
Fix no-prototype-builtins where applicable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 15:35:28 +01:00
Rene De Ren
46dd2ca37a Migrate _loadConfig to use ConfigManager.buildConfig()
Replaces manual base config construction with shared buildConfig() method.
Node now only specifies domain-specific config sections.

Part of #1: Extract base config schema

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:59:35 +01:00
Rene De Ren
ccfa90394b Fix ESLint errors and bugs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:39:57 +01:00
znetsixe
e236cccfd6 Merge branch 'dev-Rene' 2025-12-19 10:23:25 +01:00
znetsixe
108d2e23ca bug fixes 2025-11-30 09:24:37 +01:00
znetsixe
446ef81f24 adjusted input for measurement container 2025-11-28 09:59:51 +01:00
znetsixe
966ba06faa some minor addons to measurement container 2025-11-27 17:46:56 +01:00
znetsixe
e8c96c4b1e removed useless parameter 2025-11-25 16:19:23 +01:00
znetsixe
f083e7596a update 2025-11-20 22:29:24 +01:00
znetsixe
6ca6e536a5 fixed dropdown speed selection 2025-11-20 11:09:44 +01:00
p.vanderwilt
99b45c87e4 Rename _updateSourceSink to updateSourceSink for outside access 2025-11-14 12:55:11 +01:00
znetsixe
fb75fb8a11 Removed error when machine doesnt have curve so node-red doesnt crash when you dont select a machine 2025-11-13 19:39:05 +01:00
znetsixe
6528c966d8 added default liquid temp and atm pressure, added nhyd - specific flow and specific energy consumption 2025-11-12 17:40:38 +01:00
znetsixe
994cf641a3 removed some old comments 2025-11-07 15:10:46 +01:00
p.vanderwilt
0a98b12224 Merge pull request 'Implement reactor recirculation' (#4) from dev-Pieter into main
Reviewed-on: https://gitea.centraal.wbd-rd.nl/RnD/rotatingMachine/pulls/4
2025-11-06 13:58:25 +00:00
p.vanderwilt
b6d268659a Refactor flow handling: rename reactor references to source and sink and fix config minor bug 2025-11-06 14:50:40 +01:00
znetsixe
6ae622b6bf fixed bugs with db output formatting 2025-11-06 11:19:08 +01:00
znetsixe
4b5ec33c1d fixed bugs for rotating machine execSequence 2025-11-05 17:15:47 +01:00
znetsixe
51f966cfb9 Added sanitizing of input for handleInput for rotating machine 2025-11-05 15:47:39 +01:00
znetsixe
4ae6beba37 updated measurement node to match selected units from user and convert it properly 2025-10-31 18:35:40 +01:00
p.vanderwilt
303dfc477d Add flow number configuration and UI input for rotating machine 2025-10-31 14:16:00 +01:00
p.vanderwilt
ac40a93ef1 Simplify child registration error handling 2025-10-31 13:07:52 +01:00
p.vanderwilt
a8fb56bfb8 Add upstream and downstream reactor handling; improve error logging 2025-10-22 14:41:35 +02:00
HorriblePerson555
d7cc6a4a8b Enhance child registration logging and add validation for measurement child 2025-10-17 13:38:05 +02:00
HorriblePerson555
37e6523c55 Refactor child registration to use dedicated connection methods for measurement and reactor types 2025-10-16 16:32:20 +02:00
5a14f44fdd Merge pull request 'dev-Rene' (#2) from dev-Rene into main
Reviewed-on: https://gitea.centraal.wbd-rd.nl/RnD/rotatingMachine/pulls/2
2025-10-16 13:21:38 +00:00
p.vanderwilt
c081acae4e Remove non-implemented temperature handling function 2025-10-10 13:27:31 +02:00
08185243bc Merge pull request 'dev-Rene' (#1) from dev-Rene into main
Reviewed-on: https://gitea.centraal.wbd-rd.nl/RnD/rotatingMachine/pulls/1
2025-10-06 14:16:18 +00:00
3 changed files with 310 additions and 173 deletions

View File

@@ -24,7 +24,10 @@
warmup: { value: 0 },
shutdown: { value: 0 },
cooldown: { value: 0 },
movementMode : { value: "staticspeed" }, // static or dynamic
machineCurve : { value: {}},
processOutputFormat: { value: "process" },
dbaseOutputFormat: { value: "influxdb" },
//define asset properties
uuid: { value: "" },
@@ -54,7 +57,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() {
@@ -74,6 +77,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() {
@@ -99,6 +106,9 @@
node[field] = value;
});
node.movementMode = document.getElementById("node-input-movementMode").value;
console.log(`----------------> Saving movementMode: ${node.movementMode}`);
}
});
</script>
@@ -127,6 +137,31 @@
<label for="node-input-cooldown"><i class="fa fa-clock-o"></i> Cooldown Time</label>
<input type="number" id="node-input-cooldown" style="width:60%;" />
</div>
<div class="form-row">
<label for="node-input-movementMode"><i class="fa fa-exchange"></i> Movement Mode</label>
<select id="node-input-movementMode" style="width:60%;">
<option value="staticspeed">Static</option>
<option value="dynspeed">Dynamic</option>
</select>
</div>
<h3>Output Formats</h3>
<div class="form-row">
<label for="node-input-processOutputFormat"><i class="fa fa-random"></i> Process Output</label>
<select id="node-input-processOutputFormat" style="width:60%;">
<option value="process">process</option>
<option value="json">json</option>
<option value="csv">csv</option>
</select>
</div>
<div class="form-row">
<label for="node-input-dbaseOutputFormat"><i class="fa fa-database"></i> Database Output</label>
<select id="node-input-dbaseOutputFormat" style="width:60%;">
<option value="influxdb">influxdb</option>
<option value="json">json</option>
<option value="csv">csv</option>
</select>
</div>
<!-- Asset fields injected here -->
<div id="asset-fields-placeholder"></div>

View File

@@ -41,30 +41,12 @@ class nodeClass {
* @param {object} uiConfig - Raw config from Node-RED UI.
*/
_loadConfig(uiConfig,node) {
const cfgMgr = new configManager();
// Merge UI config over defaults
this.config = {
general: {
id: node.id, // node.id is for the child registration process
unit: uiConfig.unit, // add converter options later to convert to default units (need like a model that defines this which units we are going to use and then conver to those standards)
logging: {
enabled: uiConfig.enableLog,
logLevel: uiConfig.logLevel
}
},
asset: {
uuid: uiConfig.assetUuid, //need to add this later to the asset model
tagCode: uiConfig.assetTagCode, //need to add this later to the asset model
supplier: uiConfig.supplier,
category: uiConfig.category, //add later to define as the software type
type: uiConfig.assetType,
model: uiConfig.model,
unit: uiConfig.unit
},
functionality: {
positionVsParent: uiConfig.positionVsParent
}
};
// Build config: base sections + rotatingMachine-specific domain config
this.config = cfgMgr.buildConfig(this.name, uiConfig, node.id, {
flowNumber: uiConfig.flowNumber
});
// Utility for formatting outputs
this._output = new outputUtils();
@@ -80,12 +62,13 @@ class nodeClass {
const stateConfig = {
general: {
logging: {
enabled: machineConfig.eneableLog,
logLevel: machineConfig.logLevel
enabled: machineConfig.general.logging.enabled,
logLevel: machineConfig.general.logging.logLevel
}
},
movement: {
speed: Number(uiConfig.speed)
speed: Number(uiConfig.speed),
mode: uiConfig.movementMode
},
time: {
starting: Number(uiConfig.startup),
@@ -114,8 +97,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":
@@ -145,6 +128,9 @@ class nodeClass {
case "decelerating":
symbolState = "⏪";
break;
case "maintenance":
symbolState = "🔧";
break;
}
const position = m.state.getCurrentPosition();
const roundedPosition = Math.round(position * 100) / 100;
@@ -183,7 +169,7 @@ class nodeClass {
}
return status;
} catch (error) {
node.error("Error in updateNodeStatus: " + error.message);
this.node.error("Error in updateNodeStatus: " + error.message);
return { fill: "red", shape: "ring", text: "Status Error" };
}
}
@@ -196,7 +182,7 @@ class nodeClass {
this.node.send([
null,
null,
{ topic: 'registerChild', payload: this.config.general.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' },
{ topic: 'registerChild', payload: this.node.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' },
]);
}, 100);
}
@@ -224,8 +210,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]);
@@ -235,36 +221,40 @@ class nodeClass {
* Attach the node's input handler, routing control messages to the class.
*/
_attachInputHandler() {
this.node.on('input', (msg, send, done) => {
this.node.on('input', (msg, send, _done) => {
/* Update to complete event based node by putting the tick function after an input event */
const m = this.source;
switch(msg.topic) {
case 'registerChild':
case 'registerChild': {
// Register this node as a child of the parent node
const childId = msg.payload;
const childObj = this.RED.nodes.getNode(childId);
m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
break;
}
case 'setMode':
m.setMode(msg.payload);
break;
case 'execSequence':
case 'execSequence': {
const { source, action, parameter } = msg.payload;
m.handleInput(source, action, parameter);
break;
case 'execMovement':
}
case 'execMovement': {
const { source: mvSource, action: mvAction, setpoint } = msg.payload;
m.handleInput(mvSource, mvAction, Number(setpoint));
break;
case 'flowMovement':
}
case 'flowMovement': {
const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload;
m.handleInput(fmSource, fmAction, Number(fmSetpoint));
break;
case 'emergencystop':
}
case 'emergencystop': {
const { source: esSource, action: esAction } = msg.payload;
m.handleInput(esSource, esAction);
break;
}
case 'showWorkingCurves':
m.showWorkingCurves();
send({ topic : "Showing curve" , payload: m.showWorkingCurves() });

View File

@@ -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, POSITIONS} = 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,30 +75,80 @@ 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;
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(POSITIONS.AT_EQUIPMENT).value(15).unit('C');
//assume standard atm pressure is at sea level
this.measurements.type('atmPressure').variant('measured').position(POSITIONS.AT_EQUIPMENT).value(101325).unit('Pa');
//populate min and max
if (this.predictFlow) {
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(POSITIONS.DOWNSTREAM).value(0,Date.now(),this.config.general.unit);
this.measurements.type("flow").variant("predicted").position(POSITIONS.AT_EQUIPMENT).value(0,Date.now(),this.config.general.unit);
}
}
/*------------------- Register child events -------------------*/
registerChild(child, softwareType) {
this.logger.debug('Setting up child event for softwaretype ' + softwareType);
if(!child) {
this.logger.error(`Invalid ${softwareType} child provided.`);
return;
}
if(softwareType === "measurement"){
const position = child.config.functionality.positionVsParent;
const distance = child.config.functionality.distanceVsParent || 0;
const measurementType = child.config.asset.type;
const key = `${measurementType}_${position}`;
switch (softwareType) {
case "measurement":
this.logger.debug(`Registering measurement child...`);
this._connectMeasurement(child);
break;
case "reactor":
this.logger.debug(`Registering reactor child...`);
this._connectReactor(child);
break;
default:
this.logger.error(`Unrecognized softwareType: ${softwareType}`);
}
}
_connectMeasurement(measurementChild) {
const position = measurementChild.config.functionality.positionVsParent;
const measurementType = measurementChild.config.asset.type;
//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}`);
this.logger.debug(`Setting up listener for ${eventName} from child ${measurementChild.config.general.name}`);
// Register event listener for measurement updates
child.measurements.emitter.on(eventName, (eventData) => {
measurementChild.measurements.emitter.on(eventName, (eventData) => {
this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
console.log(` Emitting... ${eventName} with data:`);
// Store directly in parent's measurement container
this.measurements
.type(measurementType)
@@ -103,7 +160,6 @@ class Machine {
this._callMeasurementHandler(measurementType, eventData.value, position, eventData);
});
}
}
// Centralized handler dispatcher
_callMeasurementHandler(measurementType, value, position, context) {
@@ -128,13 +184,17 @@ _callMeasurementHandler(measurementType, value, position, context) {
}
}
_connectReactor(reactorChild) {
this.downstreamSink = reactorChild; // downstream from the pumps perpective
}
//---------------- END child stuff -------------//
// Method to assess drift using errorMetrics
assessDrift(measurement, processMin, processMax) {
this.logger.debug(`Assessing drift for measurement: ${measurement} processMin: ${processMin} processMax: ${processMax}`);
const predictedMeasurement = this.measurements.type(measurement).variant("predicted").position("downstream").getAllValues().values;
const measuredMeasurement = this.measurements.type(measurement).variant("measured").position("downstream").getAllValues().values;
const predictedMeasurement = this.measurements.type(measurement).variant("predicted").position(POSITIONS.DOWNSTREAM).getAllValues().values;
const measuredMeasurement = this.measurements.type(measurement).variant("measured").position(POSITIONS.DOWNSTREAM).getAllValues().values;
if (!predictedMeasurement || !measuredMeasurement) return null;
@@ -165,43 +225,66 @@ _callMeasurementHandler(measurementType, value, position, context) {
// -------- 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;
@@ -288,21 +371,23 @@ _callMeasurementHandler(measurementType, value, position, context) {
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(POSITIONS.DOWNSTREAM).value(0,Date.now(),this.config.general.unit);
this.measurements.type("flow").variant("predicted").position(POSITIONS.AT_EQUIPMENT).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(POSITIONS.DOWNSTREAM).value(cFlow,Date.now(),this.config.general.unit);
this.measurements.type("flow").variant("predicted").position(POSITIONS.AT_EQUIPMENT).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(POSITIONS.DOWNSTREAM).value(0, Date.now(),this.config.general.unit);
this.measurements.type("flow").variant("predicted").position(POSITIONS.AT_EQUIPMENT).value(0, Date.now(),this.config.general.unit);
return 0;
}
@@ -311,20 +396,20 @@ _callMeasurementHandler(measurementType, value, position, context) {
calcPower(x) {
if(this.hasCurve) {
if (!this._isOperationalState()) {
this.measurements.type("power").variant("predicted").position('atEquipment').value(0);
this.measurements.type("power").variant("predicted").position(POSITIONS.AT_EQUIPMENT).value(0);
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);
this.measurements.type("power").variant("predicted").position(POSITIONS.AT_EQUIPMENT).value(cPower);
//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);
this.measurements.type("power").variant("predicted").position(POSITIONS.AT_EQUIPMENT).value(0);
return 0;
}
@@ -342,7 +427,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
// 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);
this.measurements.type("power").variant("predicted").position(POSITIONS.AT_EQUIPMENT).value(0);
return 0;
}
@@ -352,14 +437,14 @@ _callMeasurementHandler(measurementType, value, position, context) {
if(this.hasCurve) {
this.predictCtrl.currentX = x;
const cCtrl = this.predictCtrl.y(x);
this.measurements.type("ctrl").variant("predicted").position('atEquipment').value(cCtrl);
this.measurements.type("ctrl").variant("predicted").position(POSITIONS.AT_EQUIPMENT).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);
this.measurements.type("ctrl").variant("predicted").position(POSITIONS.AT_EQUIPMENT).value(0);
return 0;
}
@@ -367,6 +452,11 @@ _callMeasurementHandler(measurementType, value, position, context) {
// 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
@@ -386,7 +476,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
}
// get downstream
const downstreamPressure = this.measurements.type('pressure').variant('measured').position('downstream').getCurrentValue();
const downstreamPressure = this.measurements.type('pressure').variant('measured').position(POSITIONS.DOWNSTREAM).getCurrentValue();
// Only downstream => use it, warn that it's partial
if (downstreamPressure != null) {
@@ -415,6 +505,9 @@ _callMeasurementHandler(measurementType, value, position, context) {
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;
}
@@ -437,7 +530,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
}
// get
const upstreamFlow = this.measurements.type('flow').variant('measured').position('upstream').getCurrentValue();
const upstreamFlow = this.measurements.type('flow').variant('measured').position(POSITIONS.UPSTREAM).getCurrentValue();
// Only upstream => might still accept it, but warn
if (upstreamFlow != null) {
@@ -446,7 +539,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
}
// get
const downstreamFlow = this.measurements.type('flow').variant('measured').position('downstream').getCurrentValue();
const downstreamFlow = this.measurements.type('flow').variant('measured').position(POSITIONS.DOWNSTREAM).getCurrentValue();
// Only downstream => might still accept it, but warn
if (downstreamFlow != null) {
@@ -460,7 +553,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
}
handleMeasuredPower() {
const power = this.measurements.type("power").variant("measured").position("atEquipment").getCurrentValue();
const power = this.measurements.type("power").variant("measured").position(POSITIONS.AT_EQUIPMENT).getCurrentValue();
// If your system calls it "upstream" or just a single "value", adjust accordingly
if (power != null) {
@@ -489,6 +582,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
// NEW: Flow handler
updateMeasuredFlow(value, position, context = {}) {
if (!this._isOperationalState()) {
this.logger.warn(`Machine not operational, skipping flow update from ${context.childName || 'unknown'}`);
return;
@@ -496,18 +590,30 @@ _callMeasurementHandler(measurementType, value, position, context) {
this.logger.debug(`Flow update: ${value} at ${position} from ${context.childName || 'child'}`);
if (this.upstreamSource && this.downstreamSink) {
this.updateSourceSink();
}
// Store in parent's measurement container
this.measurements.type("flow").variant("measured").position(position).value(value, context.timestamp, context.unit);
// Update predicted flow if you have prediction capability
if (this.predictFlow) {
this.measurements.type("flow").variant("predicted").position("atEquipment").value(this.predictFlow.outputY || 0);
this.measurements.type("flow").variant("predicted").position(POSITIONS.DOWNSTREAM).value(this.predictFlow.outputY || 0);
this.measurements.type("flow").variant("predicted").position(POSITIONS.AT_EQUIPMENT).value(this.predictFlow.outputY || 0);
}
}
updateSourceSink() {
// Handles flow according to the configured "flow number"
this.logger.debug(`Updating source-sink pair: ${this.upstreamSource.config.functionality.softwareType} - ${this.downstreamSink.config.functionality.softwareType}`);
this.downstreamSink.setInfluent = this.upstreamSource.getEffluent[this.config.flowNumber];
}
// 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);
}
@@ -531,6 +637,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
this.calcDistanceBEP(efficiency,cog,minEfficiency);
}
}
calcDistanceFromPeak(currentEfficiency,peakEfficiency){
@@ -561,7 +668,6 @@ _callMeasurementHandler(measurementType, value, position, context) {
};
}
// Calculate the center of gravity for current pressure
calcCog() {
@@ -571,7 +677,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
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;
@@ -623,14 +729,37 @@ _callMeasurementHandler(measurementType, value, position, context) {
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(POSITIONS.AT_EQUIPMENT).getCurrentValue('K');
const atmPressure = this.measurements.type('atmPressure').variant('measured').position(POSITIONS.AT_EQUIPMENT).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(POSITIONS.AT_EQUIPMENT).getCurrentValue('m3/s');
const powerWatt = this.measurements.type('power').variant('predicted').position(POSITIONS.AT_EQUIPMENT).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(POSITIONS.AT_EQUIPMENT).value(specificFlow);
this.measurements.type("specificEnergyConsumption").variant(variant).position(POSITIONS.AT_EQUIPMENT).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(POSITIONS.AT_EQUIPMENT).value(nHydraulicEfficiency);
}
return this.measurements.type("efficiency").variant(variant).position('atEquipment').getCurrentValue();
}
//change this to nhydrefficiency ?
return this.measurements.type("efficiency").variant(variant).position(POSITIONS.AT_EQUIPMENT).getCurrentValue();
}
@@ -676,26 +805,8 @@ _callMeasurementHandler(measurementType, value, position, context) {
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();
@@ -706,6 +817,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
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;
@@ -729,8 +841,8 @@ _callMeasurementHandler(measurementType, value, position, context) {
module.exports = Machine;
/*------------------- Testing -------------------*/
/*
/*
curve = require('C:/Users/zn375/.node-red/public/fallbackData.json');
//import a child
@@ -747,7 +859,7 @@ const PT1 = new Child(config={
},
functionality:{
softwareType:"measurement",
positionVsParent:"upstream",
positionVsParent: POSITIONS.UPSTREAM,
},
asset:{
supplier:"Vega",
@@ -769,7 +881,7 @@ const PT2 = new Child(config={
},
functionality:{
softwareType:"measurement",
positionVsParent:"upstream",
positionVsParent: POSITIONS.UPSTREAM,
},
asset:{
supplier:"Vega",
@@ -823,8 +935,8 @@ const machine = new Machine(machineConfig, stateConfig);
//machine.logger.info(JSON.stringify(curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"]));
machine.logger.info(`Registering child...`);
machine.childRegistrationUtils.registerChild(PT1, "upstream");
machine.childRegistrationUtils.registerChild(PT2, "downstream");
machine.childRegistrationUtils.registerChild(PT1, POSITIONS.UPSTREAM);
machine.childRegistrationUtils.registerChild(PT2, POSITIONS.DOWNSTREAM);
//feed curve to the machine class
//machine.updateCurve(curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"]);
@@ -837,8 +949,8 @@ machine.getOutput();
//manual test
//machine.handleInput("parent", "execSequence", "startup");
machine.measurements.type("pressure").variant("measured").position('upstream').value(-200);
machine.measurements.type("pressure").variant("measured").position('downstream').value(1000);
machine.measurements.type("pressure").variant("measured").position(POSITIONS.UPSTREAM).value(-200);
machine.measurements.type("pressure").variant("measured").position(POSITIONS.DOWNSTREAM).value(1000);
testingSequences();