Compare commits

...

21 Commits

Author SHA1 Message Date
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
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
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
znetsixe
d2a0274eb3 changed colours and icon based on s88 2025-10-14 13:52:39 +02:00
znetsixe
2073207df1 removed old readme 2025-10-07 18:10:45 +02:00
Rene De ren
bc916c0165 log updates 2025-10-03 15:41:53 +02:00
znetsixe
5357290b41 Several fixes, especially measurement and pressure updates and triggers where flawed 2025-10-02 17:09:24 +02:00
znetsixe
000bee7190 changed some old comment 2025-09-23 15:51:16 +02:00
znetsixe
b0c18e7bae added warning when model is not specified so user knows. 2025-09-23 15:11:06 +02:00
znetsixe
38408c7bc3 updates 2025-09-22 16:06:18 +02:00
znetsixe
ddfc612894 physicalPosition 1D update 2025-09-05 16:20:27 +02:00
3 changed files with 221 additions and 141 deletions

View File

@@ -1,3 +1,13 @@
<!--
| S88-niveau | Primair (blokkleur) | Tekstkleur |
| ---------------------- | ------------------- | ---------- |
| **Area** | `#0f52a5` | wit |
| **Process Cell** | `#0c99d9` | wit |
| **Unit** | `#50a8d9` | zwart |
| **Equipment (Module)** | `#86bbdd` | zwart |
| **Control Module** | `#a9daee` | zwart |
-->
<!-- Load the dynamic menu & config endpoints --> <!-- Load the dynamic menu & config endpoints -->
<script src="/rotatingMachine/menu.js"></script> <script src="/rotatingMachine/menu.js"></script>
<script src="/rotatingMachine/configData.js"></script> <script src="/rotatingMachine/configData.js"></script>
@@ -5,10 +15,8 @@
<script> <script>
RED.nodes.registerType("rotatingMachine", { RED.nodes.registerType("rotatingMachine", {
category: "EVOLV", category: "EVOLV",
color: "#4f8582", color: "#86bbdd",
defaults: { defaults: {
// Define default properties
name: { value: ""}, // use asset category as name ?
// Define specific properties // Define specific properties
speed: { value: 1, required: true }, speed: { value: 1, required: true },
@@ -16,6 +24,7 @@
warmup: { value: 0 }, warmup: { value: 0 },
shutdown: { value: 0 }, shutdown: { value: 0 },
cooldown: { value: 0 }, cooldown: { value: 0 },
movementMode : { value: "staticspeed" }, // static or dynamic
machineCurve : { value: {}}, machineCurve : { value: {}},
//define asset properties //define asset properties
@@ -33,16 +42,20 @@
//physicalAspect //physicalAspect
positionVsParent: { value: "" }, positionVsParent: { value: "" },
positionIcon: { value: "" }, positionIcon: { value: "" },
hasDistance: { value: false },
distance: { value: 0 },
distanceUnit: { value: "m" },
distanceDescription: { value: "" }
}, },
inputs: 1, inputs: 1,
outputs: 3, outputs: 3,
inputLabels: ["Input"], inputLabels: ["Input"],
outputLabels: ["process", "dbase", "parent"], outputLabels: ["process", "dbase", "parent"],
icon: "font-awesome/fa-tachometer", icon: "font-awesome/fa-cog",
label: function () { label: function () {
return this.positionIcon + " " + this.category.slice(0, -1) || "Machine"; return this.positionIcon + " " + this.category || "Machine";
}, },
oneditprepare: function() { oneditprepare: function() {
@@ -62,10 +75,15 @@
document.getElementById("node-input-warmup"); document.getElementById("node-input-warmup");
document.getElementById("node-input-shutdown"); document.getElementById("node-input-shutdown");
document.getElementById("node-input-cooldown"); document.getElementById("node-input-cooldown");
const movementMode = document.getElementById("node-input-movementMode");
if (movementMode) {
movementMode.value = this.movementMode || "staticspeed";
}
}, },
oneditsave: function() { oneditsave: function() {
const node = this; const node = this;
// save asset fields // save asset fields
if (window.EVOLV?.nodes?.rotatingMachine?.assetMenu?.saveEditor) { if (window.EVOLV?.nodes?.rotatingMachine?.assetMenu?.saveEditor) {
window.EVOLV.nodes.rotatingMachine.assetMenu.saveEditor(this); window.EVOLV.nodes.rotatingMachine.assetMenu.saveEditor(this);
@@ -86,6 +104,9 @@
node[field] = value; node[field] = value;
}); });
node.movementMode = document.getElementById("node-input-movementMode").value;
console.log(`----------------> Saving movementMode: ${node.movementMode}`);
} }
}); });
</script> </script>
@@ -114,6 +135,13 @@
<label for="node-input-cooldown"><i class="fa fa-clock-o"></i> Cooldown Time</label> <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%;" /> <input type="number" id="node-input-cooldown" style="width:60%;" />
</div> </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>
<!-- Asset fields injected here --> <!-- Asset fields injected here -->
<div id="asset-fields-placeholder"></div> <div id="asset-fields-placeholder"></div>

View File

@@ -45,7 +45,6 @@ class nodeClass {
// Merge UI config over defaults // Merge UI config over defaults
this.config = { this.config = {
general: { general: {
name: uiConfig.name,
id: node.id, // node.id is for the child registration process 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) 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: { logging: {
@@ -63,7 +62,7 @@ class nodeClass {
unit: uiConfig.unit unit: uiConfig.unit
}, },
functionality: { functionality: {
positionVsParent: uiConfig.positionVsParent || 'atEquipment', // Default to 'atEquipment' if not specified positionVsParent: uiConfig.positionVsParent
} }
}; };
@@ -77,6 +76,8 @@ class nodeClass {
_setupSpecificClass(uiConfig) { _setupSpecificClass(uiConfig) {
const machineConfig = this.config; const machineConfig = this.config;
console.log(`----------------> Loaded movementMode in nodeClass: ${uiConfig.movementMode}`);
// need extra state for this // need extra state for this
const stateConfig = { const stateConfig = {
general: { general: {
@@ -86,7 +87,8 @@ class nodeClass {
} }
}, },
movement: { movement: {
speed: Number(uiConfig.speed) speed: Number(uiConfig.speed),
mode: uiConfig.movementMode
}, },
time: { time: {
starting: Number(uiConfig.startup), starting: Number(uiConfig.startup),
@@ -115,8 +117,8 @@ class nodeClass {
try { try {
const mode = m.currentMode; const mode = m.currentMode;
const state = m.state.getCurrentState(); const state = m.state.getCurrentState();
const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').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('upstream').getCurrentValue()); const power = Math.round(m.measurements.type("power").variant("predicted").position('atequipment').getCurrentValue('kW'));
let symbolState; let symbolState;
switch(state){ switch(state){
case "off": case "off":
@@ -146,6 +148,9 @@ class nodeClass {
case "decelerating": case "decelerating":
symbolState = "⏪"; symbolState = "⏪";
break; break;
case "maintenance":
symbolState = "🔧";
break;
} }
const position = m.state.getCurrentPosition(); const position = m.state.getCurrentPosition();
const roundedPosition = Math.round(position * 100) / 100; const roundedPosition = Math.round(position * 100) / 100;
@@ -225,8 +230,8 @@ class nodeClass {
//this.source.tick(); //this.source.tick();
const raw = this.source.getOutput(); const raw = this.source.getOutput();
const processMsg = this._output.formatMsg(raw, this.config, 'process'); const processMsg = this._output.formatMsg(raw, this.source.config, 'process');
const influxMsg = this._output.formatMsg(raw, this.config, 'influxdb'); const influxMsg = this._output.formatMsg(raw, this.source.config, 'influxdb');
// Send only updated outputs on ports 0 & 1 // Send only updated outputs on ports 0 & 1
this.node.send([processMsg, influxMsg]); this.node.send([processMsg, influxMsg]);

View File

@@ -1,50 +1,5 @@
/**
* @file machine.js
*
* Permission is hereby granted to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to use it for personal
* or non-commercial purposes, with the following restrictions:
*
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
* be copied, merged, distributed, sublicensed, or sold without explicit
* prior written permission from the author.
*
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
* a valid license, obtainable only with the explicit consent of the author.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Ownership of this code remains solely with the original author. Unauthorized
* use of this Software is strictly prohibited.
*
* @summary A class to interact and manipulate machines with a non-euclidian curve
* @description A class to interact and manipulate machines with a non-euclidian curve
* @module machine
* @exports machine
* @version 0.1.0
* @since 0.1.0
*
* Author:
* - Rene De Ren
* Email:
* - r.de.ren@brabantsedelta.nl
*
* Add functionality later
// -------- Operational Metrics -------- //
maintenanceAlert: this.state.checkMaintenanceStatus()
*/
//load local dependencies
const EventEmitter = require('events'); const EventEmitter = require('events');
const {loadCurve,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils} = require('generalFunctions'); const {loadCurve,gravity,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils,coolprop} = require('generalFunctions');
class Machine { class Machine {
@@ -61,13 +16,16 @@ class Machine {
// Load a specific curve // Load a specific curve
this.model = machineConfig.asset.model; // Get the model from the machineConfig 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 //Init config and check if it is valid
this.config = this.configUtils.initConfig(machineConfig); 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
if (!this.model || !this.curve) { if (!this.model || !this.curve) {
this.logger.warn(`${!this.model ? 'Model not specified' : 'Curve not found for model ' + this.model} in machineConfig. Cannot make predictions.`); 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 // Set prediction objects to null to prevent method calls
this.predictFlow = null; this.predictFlow = null;
this.predictPower = null; this.predictPower = null;
@@ -76,10 +34,8 @@ class Machine {
} }
else{ else{
this.hasCurve = true; this.hasCurve = true;
this.config = this.configUtils.updateConfig(this.config, { this.config = this.configUtils.updateConfig(this.config, { asset: { ...this.config.asset, machineCurve: this.curve } });
asset: { ...this.config.asset, machineCurve: this.curve } //machineConfig = { ...machineConfig, asset: { ...machineConfig.asset, machineCurve: this.curve } }; // Merge curve into machineConfig
});
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.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.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) this.predictCtrl = new predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship)
@@ -89,7 +45,17 @@ class Machine {
this.errorMetrics = new nrmse(errorMetricsConfig, this.logger); this.errorMetrics = new nrmse(errorMetricsConfig, this.logger);
// Initialize measurements // 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.interpolation = new interpolation();
this.flowDrift = null; this.flowDrift = null;
@@ -109,25 +75,61 @@ class Machine {
this.updatePosition(); 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.child = {}; // object to hold child information so we know on what to subscribe
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility 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 -------------------*/ /*------------------- Register child events -------------------*/
registerChild(child, softwareType) { registerChild(child, softwareType) {
this.logger.debug('Setting up child event listeners'); this.logger.debug('Setting up child event for softwaretype ' + softwareType);
if(softwareType === "measurement"){ if(softwareType === "measurement"){
const position = child.config.functionality.positionVsParent; const position = child.config.functionality.positionVsParent;
const distance = child.config.functionality.distanceVsParent || 0;
const measurementType = child.config.asset.type; const measurementType = child.config.asset.type;
const key = `${measurementType}_${position}`; const key = `${measurementType}_${position}`;
//rebuild to measurementype.variant no position and then switch based on values not strings or names.
const eventName = `${measurementType}.measured.${position}`; 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 // Register event listener for measurement updates
child.measurements.emitter.on(eventName, (eventData) => { child.measurements.emitter.on(eventName, (eventData) => {
this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`); 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 // Store directly in parent's measurement container
this.measurements this.measurements
.type(measurementType) .type(measurementType)
@@ -201,46 +203,68 @@ _callMeasurementHandler(measurementType, value, position, context) {
// -------- Mode and Input Management -------- // // -------- Mode and Input Management -------- //
isValidSourceForMode(source, mode) { isValidSourceForMode(source, mode) {
const allowedSourcesSet = this.config.mode.allowedSources[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) { isValidActionForMode(action, mode) {
const allowedActionsSet = this.config.mode.allowedActions[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) { 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}'.`); this.logger.info(`Handling input from source '${source}' with action '${action}' in mode '${this.currentMode}'.`);
try { try {
switch (action) { switch (action) {
case "execSequence":
await this.executeSequence(parameter); case "execsequence":
//recalc flow and power return await this.executeSequence(parameter);
this.updatePosition();
break; case "execmovement":
case "execMovement": return await this.setpoint(parameter);
await this.setpoint(parameter);
break; case "entermaintenance":
case "flowMovement":
return await this.executeSequence(parameter);
case "exitmaintenance":
return await this.executeSequence(parameter);
case "flowmovement":
// Calculate the control value for a desired flow // Calculate the control value for a desired flow
const pos = this.calcCtrl(parameter); const pos = this.calcCtrl(parameter);
// Move to the desired setpoint // Move to the desired setpoint
await this.setpoint(pos); return await this.setpoint(pos);
break;
case "emergencyStop": case "emergencystop":
this.logger.warn(`Emergency stop activated by '${source}'.`); this.logger.warn(`Emergency stop activated by '${source}'.`);
await this.executeSequence("emergencyStop"); return await this.executeSequence("emergencyStop");
break;
case "statusCheck": case "statuscheck":
this.logger.info(`Status Check: Mode = '${this.currentMode}', Source = '${source}'.`); this.logger.info(`Status Check: Mode = '${this.currentMode}', Source = '${source}'.`);
break; break;
default: default:
this.logger.warn(`Action '${action}' is not implemented.`); this.logger.warn(`Action '${action}' is not implemented.`);
break; break;
@@ -250,6 +274,13 @@ _callMeasurementHandler(measurementType, value, position, context) {
} catch (error) { } catch (error) {
this.logger.error(`Error handling input: ${error}`); this.logger.error(`Error handling input: ${error}`);
} }
}
abortMovement(reason = "group override") {
if (this.state?.abortCurrentMovement) {
this.state.abortCurrentMovement(reason);
}
} }
setMode(newMode) { setMode(newMode) {
@@ -290,6 +321,9 @@ _callMeasurementHandler(measurementType, value, position, context) {
break; // Exit sequence execution on error break; // Exit sequence execution on error
} }
} }
//recalc flow and power
this.updatePosition();
} }
async setpoint(setpoint) { async setpoint(setpoint) {
@@ -314,21 +348,23 @@ _callMeasurementHandler(measurementType, value, position, context) {
calcFlow(x) { calcFlow(x) {
if(this.hasCurve) { if(this.hasCurve) {
if (!this._isOperationalState()) { 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.`); this.logger.debug(`Machine is not operational. Setting predicted flow to 0.`);
return 0; return 0;
} }
//this.predictFlow.currentX = x; Decrepated
const cFlow = this.predictFlow.y(x); 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}`); //this.logger.debug(`Calculated flow: ${cFlow} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cFlow; return cFlow;
} }
// If no curve data is available, log a warning and return 0 // 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.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; return 0;
} }
@@ -337,20 +373,20 @@ _callMeasurementHandler(measurementType, value, position, context) {
calcPower(x) { calcPower(x) {
if(this.hasCurve) { if(this.hasCurve) {
if (!this._isOperationalState()) { if (!this._isOperationalState()) {
this.measurements.type("power").variant("predicted").position('upstream').value(0); this.measurements.type("power").variant("predicted").position('atEquipment').value(0);
this.logger.debug(`Machine is not operational. Setting predicted power to 0.`); this.logger.debug(`Machine is not operational. Setting predicted power to 0.`);
return 0; return 0;
} }
//this.predictPower.currentX = x; Decrepated //this.predictPower.currentX = x; Decrepated
const cPower = this.predictPower.y(x); const cPower = this.predictPower.y(x);
this.measurements.type("power").variant("predicted").position('upstream').value(cPower); this.measurements.type("power").variant("predicted").position('atEquipment').value(cPower);
//this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`); //this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cPower; return cPower;
} }
// If no curve data is available, log a warning and return 0 // 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.logger.warn(`No curve data available for power calculation. Returning 0.`);
this.measurements.type("power").variant("predicted").position('upstream').value(0); this.measurements.type("power").variant("predicted").position('atEquipment').value(0);
return 0; return 0;
} }
@@ -368,7 +404,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
// If no curve data is available, log a warning and return 0 // 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.logger.warn(`No curve data available for power calculation. Returning 0.`);
this.measurements.type("power").variant("predicted").position('upstream').value(0); this.measurements.type("power").variant("predicted").position('atEquipment').value(0);
return 0; return 0;
} }
@@ -378,14 +414,14 @@ _callMeasurementHandler(measurementType, value, position, context) {
if(this.hasCurve) { if(this.hasCurve) {
this.predictCtrl.currentX = x; this.predictCtrl.currentX = x;
const cCtrl = this.predictCtrl.y(x); const cCtrl = this.predictCtrl.y(x);
this.measurements.type("ctrl").variant("predicted").position('upstream').value(cCtrl); this.measurements.type("ctrl").variant("predicted").position('atEquipment').value(cCtrl);
//this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`); //this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`);
return cCtrl; return cCtrl;
} }
// If no curve data is available, log a warning and return 0 // 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.logger.warn(`No curve data available for control calculation. Returning 0.`);
this.measurements.type("ctrl").variant("predicted").position('upstream').value(0); this.measurements.type("ctrl").variant("predicted").position('atEquipment').value(0);
return 0; return 0;
} }
@@ -393,10 +429,15 @@ _callMeasurementHandler(measurementType, value, position, context) {
// returns the best available pressure measurement to use in the prediction calculation // returns the best available pressure measurement to use in the prediction calculation
// this will be either the differential pressure, downstream or upstream pressure // this will be either the differential pressure, downstream or upstream pressure
getMeasuredPressure() { 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(); const pressureDiff = this.measurements.type('pressure').variant('measured').difference();
// Both upstream & downstream => differential // Both upstream & downstream => differential
if (pressureDiff != null) { if (pressureDiff) {
this.logger.debug(`Pressure differential: ${pressureDiff.value}`); this.logger.debug(`Pressure differential: ${pressureDiff.value}`);
this.predictFlow.fDimension = pressureDiff.value; this.predictFlow.fDimension = pressureDiff.value;
this.predictPower.fDimension = pressureDiff.value; this.predictPower.fDimension = pressureDiff.value;
@@ -416,7 +457,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
// Only downstream => use it, warn that it's partial // Only downstream => use it, warn that it's partial
if (downstreamPressure != null) { if (downstreamPressure != null) {
this.logger.warn(`Using downstream pressure only for prediction: ${downstreamPressure} `); this.logger.warn(`Using downstream pressure only for prediction: ${downstreamPressure} This is less acurate!!`);
this.predictFlow.fDimension = downstreamPressure; this.predictFlow.fDimension = downstreamPressure;
this.predictPower.fDimension = downstreamPressure; this.predictPower.fDimension = downstreamPressure;
this.predictCtrl.fDimension = downstreamPressure; this.predictCtrl.fDimension = downstreamPressure;
@@ -441,6 +482,9 @@ _callMeasurementHandler(measurementType, value, position, context) {
const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted"); const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted");
//update the distance from peak //update the distance from peak
this.calcDistanceBEP(efficiency,cog,minEfficiency); 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; return 0;
} }
@@ -498,18 +542,12 @@ _callMeasurementHandler(measurementType, value, position, context) {
} }
} }
// ENHANCED: Your existing handler but with rich context // context handler for pressure updates
updateMeasuredPressure(value, position, context = {}) { updateMeasuredPressure(value, position, context = {}) {
// Your existing operational check
if (!this._isOperationalState()) {
this.logger.warn(`Machine not operational, skipping pressure update from ${context.childName || 'unknown'}`);
return;
}
// Enhanced logging with child context
this.logger.debug(`Pressure update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`); this.logger.debug(`Pressure update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`);
// Store in parent's measurement container (your existing logic) // Store in parent's measurement container
this.measurements.type("pressure").variant("measured").position(position).value(value, context.timestamp, context.unit); this.measurements.type("pressure").variant("measured").position(position).value(value, context.timestamp, context.unit);
// Determine what kind of value to use as pressure (upstream , downstream or difference) // Determine what kind of value to use as pressure (upstream , downstream or difference)
@@ -519,7 +557,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
this.logger.debug(`Using pressure: ${pressure} for calculations`); this.logger.debug(`Using pressure: ${pressure} for calculations`);
} }
// NEW: Flow handler (following your pattern) // NEW: Flow handler
updateMeasuredFlow(value, position, context = {}) { updateMeasuredFlow(value, position, context = {}) {
if (!this._isOperationalState()) { if (!this._isOperationalState()) {
this.logger.warn(`Machine not operational, skipping flow update from ${context.childName || 'unknown'}`); this.logger.warn(`Machine not operational, skipping flow update from ${context.childName || 'unknown'}`);
@@ -533,6 +571,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
// Update predicted flow if you have prediction capability // Update predicted flow if you have prediction capability
if (this.predictFlow) { 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); this.measurements.type("flow").variant("predicted").position("atEquipment").value(this.predictFlow.outputY || 0);
} }
} }
@@ -540,11 +579,13 @@ _callMeasurementHandler(measurementType, value, position, context) {
// Helper method for operational state check // Helper method for operational state check
_isOperationalState() { _isOperationalState() {
const state = this.state.getCurrentState(); 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); return ["operational", "accelerating", "decelerating"].includes(state);
} }
//what is the internal functions that need updating when something changes that has influence on this. //what is the internal functions that need updating when something changes that has influence on this.
updatePosition() { updatePosition() {
if (this._isOperationalState()) { if (this._isOperationalState()) {
const currentPosition = this.state.getCurrentPosition(); const currentPosition = this.state.getCurrentPosition();
@@ -562,6 +603,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
this.calcDistanceBEP(efficiency,cog,minEfficiency); this.calcDistanceBEP(efficiency,cog,minEfficiency);
} }
} }
calcDistanceFromPeak(currentEfficiency,peakEfficiency){ calcDistanceFromPeak(currentEfficiency,peakEfficiency){
@@ -592,7 +634,6 @@ _callMeasurementHandler(measurementType, value, position, context) {
}; };
} }
// Calculate the center of gravity for current pressure // Calculate the center of gravity for current pressure
calcCog() { calcCog() {
@@ -602,7 +643,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
const {efficiencyCurve, peak, peakIndex, minEfficiency } = this.calcEfficiencyCurve(powerCurve, flowCurve); const {efficiencyCurve, peak, peakIndex, minEfficiency } = this.calcEfficiencyCurve(powerCurve, flowCurve);
// Calculate the normalized center of gravity // 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 //store in object for later retrieval
this.currentEfficiencyCurve = efficiencyCurve; this.currentEfficiencyCurve = efficiencyCurve;
@@ -652,16 +693,39 @@ _callMeasurementHandler(measurementType, value, position, context) {
return { cPower, cFlow }; 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) { if (power != 0 && flow != 0) {
// Calculate efficiency after measurements update const specificFlow = flow / power;
this.measurements.type("efficiency").variant(variant).position('downstream').value((flow / power)); const specificEnergyConsumption = power / flow;
} else {
this.measurements.type("efficiency").variant(variant).position('downstream').value(null); 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);
}
} }
return this.measurements.type("efficiency").variant(variant).position('downstream').getCurrentValue(); //change this to nhydrefficiency ?
return this.measurements.type("efficiency").variant(variant).position('atEquipment').getCurrentValue();
} }
@@ -707,26 +771,8 @@ _callMeasurementHandler(measurementType, value, position, context) {
getOutput() { getOutput() {
// Improved output object generation // 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 output = this.measurements.getFlattenedOutput();
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;
}
});
});
//fill in the rest of the output object //fill in the rest of the output object
output["state"] = this.state.getCurrentState(); output["state"] = this.state.getCurrentState();
@@ -737,6 +783,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
output["cog"] = this.cog; // flow / power efficiency output["cog"] = this.cog; // flow / power efficiency
output["NCog"] = this.NCog; // normalized cog output["NCog"] = this.NCog; // normalized cog
output["NCogPercent"] = Math.round(this.NCog * 100 * 100) / 100 ; output["NCogPercent"] = Math.round(this.NCog * 100 * 100) / 100 ;
output["maintenanceTime"] = this.state.getMaintenanceTimeHours();
if(this.flowDrift != null){ if(this.flowDrift != null){
const flowDrift = this.flowDrift; const flowDrift = this.flowDrift;
@@ -760,8 +807,8 @@ _callMeasurementHandler(measurementType, value, position, context) {
module.exports = Machine; module.exports = Machine;
/*------------------- Testing -------------------*/ /*------------------- Testing -------------------*/
/*
/*
curve = require('C:/Users/zn375/.node-red/public/fallbackData.json'); curve = require('C:/Users/zn375/.node-red/public/fallbackData.json');
//import a child //import a child