Compare commits

...

19 Commits

Author SHA1 Message Date
Woutverheijen
fa5083840b merging 2025-11-06 09:34:35 +01:00
Woutverheijen
f450eab56e Merge branch 'main' of https://gitea.centraal.wbd-rd.nl/woutverheijen/rotatingMachine 2025-11-06 08:51:21 +01:00
Woutverheijen
9e7c951aa8 updates 2025-11-06 08:51:19 +01:00
818c989a86 merge upstream 2025-11-06 07:03:29 +00: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
Woutverheijen
59dec46846 test 2025-10-24 09:40:40 +02:00
Woutverheijen
133cadf225 remove test 2025-10-23 12:23:45 +02:00
Woutverheijen
60d7dcb1ac test 2025-10-23 12:20:46 +02:00
5a14f44fdd Merge pull request 'dev-Rene' (#2) from dev-Rene into main
Reviewed-on: RnD/rotatingMachine#2
2025-10-16 13:21:38 +00:00
znetsixe
d2a0274eb3 changed colours and icon based on s88 2025-10-14 13:52:39 +02:00
c081acae4e Remove non-implemented temperature handling function 2025-10-10 13:27:31 +02:00
znetsixe
2073207df1 removed old readme 2025-10-07 18:10:45 +02:00
08185243bc Merge pull request 'dev-Rene' (#1) from dev-Rene into main
Reviewed-on: RnD/rotatingMachine#1
2025-10-06 14:16:18 +00: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
4 changed files with 391 additions and 137 deletions

View File

@@ -1 +1,23 @@
# rotating machine # rotating machine
---Explanation KPIs---
Mean Time Between Failures: MTBF is a key indicator of asset reliability. It represents the average amount of time between two consecutive failures during normal operation. The higher the value, the better.
- Formula: MTBF = runtime / failures
Mean Time to Repair: MTTR focuses on the speed of failure recovery. It indicates how long it takes on average to repair a failure and restore the system to operational condition. The lower the value, the better.
- Formula: MTTR = downtime / failures
Asset Availability: Asset availability indicates how often machines are available for use. Higher availability means that the equipment experiences fewer breakdowns and remains operational for a greater amount of time.
- Formula: availability = MTBF / (MTBF + MTTR) * 100%
Asset Health Index: A score ranging from 0 (optimal condition) to 5 (worst case) that reflects the overall health status of an asset.
---KPI message---
The message consists of the following components:
- asset tagnumber:
- asset availability
- mean time between failures
- mean time to repair
- asset health index
- asset health color: Gives a color based on the asset health index (0 = Darkgreen, 1 = Green, 2 = Yellow, 3 = Orange, 4 = Red, 5 = Darkred.)
- total failures: the total number of failures that have occured for a particular asset

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,7 +15,7 @@
<script> <script>
RED.nodes.registerType("rotatingMachine", { RED.nodes.registerType("rotatingMachine", {
category: "EVOLV", category: "EVOLV",
color: "#4f8582", color: "#86bbdd",
defaults: { defaults: {
// Define specific properties // Define specific properties
@@ -41,7 +51,7 @@
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.slice(0, -1) || "Machine";
@@ -68,6 +78,7 @@
}, },
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);

View File

@@ -62,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
} }
}; };
@@ -114,8 +114,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":
@@ -145,6 +145,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;

View File

@@ -1,48 +1,3 @@
/**
* @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,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils} = require('generalFunctions');
const { name } = require('../../generalFunctions/src/convert/lodash/lodash._shimkeys'); const { name } = require('../../generalFunctions/src/convert/lodash/lodash._shimkeys');
@@ -71,7 +26,7 @@ class Machine {
this.config = this.configUtils.updateConfig(this.config, {general:{name: this.config.functionality?.softwareType + "_" + machineConfig.general.id}}); // add unique name if not present 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;
@@ -93,7 +48,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;
@@ -113,25 +78,114 @@ 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();
});
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
// --- KPI tracking ---
this.kpi = {
failures: 0,
totalRuntimeHours: 0,
totalDowntimeHours: 0,
lastFailureTime: null,
lastRepairTime: null,
MTBF: 0,
MTTR: 0,
availability: 0
};
this.assetHealth = {
index: 0 // 0 = optimal, 5 = failure
};
this.state.emitter.on('stateChange', (payload) => {
const stateStr = typeof payload === 'string'
? payload
: (payload?.state ?? payload?.newState ?? payload);
if (typeof stateStr !== 'string') {
this.logger.warn(`stateChange event without parsable state: ${JSON.stringify(payload)}`);
return;
}
this._handleStateChangeForKPI(stateStr);
});
// --- KPI tracking ---
this.kpi = {
failures: 0,
totalRuntimeHours: 0,
totalDowntimeHours: 0,
lastFailureTime: null,
lastRepairTime: null,
MTBF: 0,
MTTR: 0,
availability: 0
};
this.assetHealth = {
index: 0 // 0 = optimal, 5 = failure
};
this.state.emitter.on('stateChange', (payload) => {
const stateStr = typeof payload === 'string'
? payload
: (payload?.state ?? payload?.newState ?? payload);
if (typeof stateStr !== 'string') {
this.logger.warn(`stateChange event without parsable state: ${JSON.stringify(payload)}`);
return;
}
this._handleStateChangeForKPI(stateStr);
});
}
_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);
}
}
_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);
}
} }
/*------------------- 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}`);
console.log(` 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)
@@ -156,10 +210,6 @@ _callMeasurementHandler(measurementType, value, position, context) {
this.updateMeasuredFlow(value, position, context); this.updateMeasuredFlow(value, position, context);
break; break;
case 'temperature':
this.updateMeasuredTemperature(value, position, context);
break;
default: default:
this.logger.warn(`No handler for measurement type: ${measurementType}`); this.logger.warn(`No handler for measurement type: ${measurementType}`);
// Generic handler - just update position // Generic handler - just update position
@@ -205,46 +255,71 @@ _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};
}
this.logger.debug("hello");
//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.debug("hello2");
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;
@@ -254,6 +329,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) {
@@ -294,6 +376,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) {
@@ -341,20 +426,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;
} }
@@ -372,7 +457,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;
} }
@@ -382,14 +467,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;
} }
@@ -400,7 +485,7 @@ _callMeasurementHandler(measurementType, value, position, context) {
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;
@@ -420,7 +505,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;
@@ -502,18 +587,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)
@@ -523,7 +602,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'}`);
@@ -537,18 +616,20 @@ _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("atEquipment").value(this.predictFlow.outputY || 0); this.measurements.type("flow").variant("predicted").position("downstream").value(this.predictFlow.outputY || 0);
} }
} }
// 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();
@@ -568,6 +649,136 @@ _callMeasurementHandler(measurementType, value, position, context) {
} }
} }
/////////////////////////////
/**
* Compute a single drift score in [0..1] using predicted vs measured series.
* Uses min/max of the *predicted* window as normalization range.
* If no usable data -> returns 0 (neutral).
*/
_computeDriftScore() {
try {
const metrics = [
{ key: "pressure", pos: "downstream" },
{ key: "flow", pos: "downstream" },
{ key: "power", pos: "atEquipment" }
];
const values = [];
for (const m of metrics) {
const pred = this.measurements.type(m.key).variant("predicted").position(m.pos).getAllValues()?.values;
const meas = this.measurements.type(m.key).variant("measured").position(m.pos).getAllValues()?.values;
if (!Array.isArray(pred) || !Array.isArray(meas) || pred.length < 2 || meas.length < 2) continue;
const expectedMin = Math.min(...pred);
const expectedMax = Math.max(...pred);
if (!Number.isFinite(expectedMin) || !Number.isFinite(expectedMax) || expectedMax === expectedMin) continue;
const drift = this.errorMetrics.assessDrift(pred, meas, expectedMin, expectedMax);
if (Number.isFinite(drift)) {
// assessDrift is already normalized; keep it in [0..1]
values.push(Math.max(0, Math.min(1, Math.abs(drift))));
}
}
if (values.length === 0) return 0; // neutral if no data
const avg = values.reduce((s, v) => s + v, 0) / values.length;
return Math.max(0, Math.min(1, avg));
} catch (e) {
this.logger?.warn?.(`Drift score error: ${e.message}`);
return 0;
}
}
_calculateAssetHealthIndex() {
try {
// 1) Hard fail -> worst health
// if (this.state?.getCurrentState && this.state.getCurrentState() === "failed")
if (["off"].includes(this.state?.getCurrentState?.())){
this.assetHealth.index = 5;
return 5;
}
// 2) Inputs (clamped to 0..1)
const availability = typeof this.kpi?.availability === 'number' ? this.kpi.availability : 1;
const unavailability = 1 - Math.max(0, Math.min(1, availability));
const effPenalty = Math.max(0, Math.min(1, typeof this.relDistFromPeak === 'number' ? this.relDistFromPeak : 0));
const driftScore = this._computeDriftScore(); // 0..1
// 3) Blend (weights sum to 1.0)
// Tweak these if you like: e.g. make drift more/less important.
const wAvail = 0.4; // unavailability weight
const wDrift = 0.4; // drift weight
const wEff = 0.2; // efficiency distance weight
const score01 = (wAvail * unavailability) + (wDrift * driftScore) + (wEff * effPenalty);
// 4) Scale to 0..5 integer, clamp
const index = Math.max(0, Math.min(5, Math.round(score01 * 5)));
this.assetHealth.index = index;
return index;
} catch (err) {
this.logger?.error?.(`AHI calc error: ${err.message}`);
this.assetHealth.index = 0;
return 0;
}
}
_handleStateChangeForKPI(newState) {
const now = Date.now();
const runtime = this.state.getRunTimeHours();
const lastState = this.state.getPreviousState?.() || "unknown";
// --- Treat OFF as failure and start of downtime ---
if (newState === "off") {
this.kpi.failures++; // always count a new failure when OFF
this.kpi.lastFailureTime = now; // mark the start of downtime
this.logger.warn(`Machine OFF (counted as failure). Total failures: ${this.kpi.failures}`);
}
// --- When we leave OFF and become OPERATIONAL, book downtime ---
if (newState === "operational") {
// Only calculate downtime if we had an OFF period before
if (this.kpi.lastFailureTime != null) {
const downtimeHours = (now - this.kpi.lastFailureTime) / 3600000;
this.kpi.totalDowntimeHours += downtimeHours;
this.kpi.lastRepairTime = now; // moment of "repaired"
this.kpi.lastFailureTime = null; // close downtime window
this.logger.info(`OFF → OPERATIONAL. Added ${downtimeHours.toFixed(2)}h downtime.`);
}
}
// --- Compute KPI Metrics ---
const failures = this.kpi.failures;
const downtime = this.kpi.totalDowntimeHours;
// If no failures yet: MTBF = total runtime; MTTR = 0
this.kpi.MTBF = failures > 0 ? runtime / failures : runtime;
this.kpi.MTTR = failures > 0 ? downtime / failures : 0;
// --- Compute Availability ---
const mtbf = this.kpi.MTBF ?? 0;
const mttr = this.kpi.MTTR ?? 0;
if (mtbf <= 0 && mttr <= 0) {
this.kpi.availability = 1; // Default: 100% if no data
} else {
const availability = mtbf / (mtbf + mttr);
this.kpi.availability = Math.min(1, Math.max(0, availability)); // clamp 01
}
this.logger.debug(
`KPI updated — MTBF: ${this.kpi.MTBF.toFixed(2)}h, MTTR: ${this.kpi.MTTR.toFixed(2)}h, ` +
`Availability: ${(this.kpi.availability * 100).toFixed(2)}%`
);
}
//////////////////////////////////////////////
calcDistanceFromPeak(currentEfficiency,peakEfficiency){ calcDistanceFromPeak(currentEfficiency,peakEfficiency){
return Math.abs(currentEfficiency - peakEfficiency); return Math.abs(currentEfficiency - peakEfficiency);
} }
@@ -596,7 +807,6 @@ _callMeasurementHandler(measurementType, value, position, context) {
}; };
} }
// Calculate the center of gravity for current pressure // Calculate the center of gravity for current pressure
calcCog() { calcCog() {
@@ -660,12 +870,12 @@ _callMeasurementHandler(measurementType, value, position, context) {
if (power != 0 && flow != 0) { if (power != 0 && flow != 0) {
// Calculate efficiency after measurements update // Calculate efficiency after measurements update
this.measurements.type("efficiency").variant(variant).position('downstream').value((flow / power)); this.measurements.type("efficiency").variant(variant).position('atEquipment').value((flow / power));
} else { } else {
this.measurements.type("efficiency").variant(variant).position('downstream').value(null); this.measurements.type("efficiency").variant(variant).position('atEquipment').value(null);
} }
return this.measurements.type("efficiency").variant(variant).position('downstream').getCurrentValue(); return this.measurements.type("efficiency").variant(variant).position('atEquipment').getCurrentValue();
} }
@@ -712,23 +922,12 @@ _callMeasurementHandler(measurementType, value, position, context) {
// Improved output object generation // Improved output object generation
const output = {}; 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(); Object.entries(this.measurements.measurements).forEach(([type, variants]) => {
const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue(); Object.entries(variants).forEach(([variant, positions]) => {
Object.entries(positions).forEach(([position, measurement]) => {
if (downstreamVal != null) { output[`${type}.${variant}.${position}`] = measurement.getCurrentValue();
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;
}
}); });
}); });
@@ -741,6 +940,14 @@ _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["kpi_MTBF"] = this.kpi.MTBF;
output["kpi_MTTR"] = this.kpi.MTTR;
output["kpi_assetAvailability"] = Math.round(this.kpi.availability * 100 * 100) / 100;
output["kpi_totalFailuresCount"] = this.kpi.failures;
output["asset_tag_number"] = 'L001';
// output["asset_tag_number"] = this.assetTagNumber;
output["maintenanceTime"] = this.state.getMaintenanceTimeHours();
if(this.flowDrift != null){ if(this.flowDrift != null){
const flowDrift = this.flowDrift; const flowDrift = this.flowDrift;
@@ -755,6 +962,21 @@ _callMeasurementHandler(measurementType, value, position, context) {
output["effRelDistFromPeak"] = this.relDistFromPeak; output["effRelDistFromPeak"] = this.relDistFromPeak;
//this.logger.debug(`Output: ${JSON.stringify(output)}`); //this.logger.debug(`Output: ${JSON.stringify(output)}`);
/////////////////////////////////
// this._calculateAssetHealthIndex();
// output["assetHealthIndex"] = this.assetHealth.index;
this._calculateAssetHealthIndex();
output["assetHealthIndex"] = this.assetHealth.index;
// 0 = darkgreen, 1 = green, 2 = yellow, 3 = orange, 4 = red, 5 = darkred
// const healthColors = ["darkgreen", "green", "yellow", "orange", "red", "darkred"];
const healthColors = ["#006400", "#008000", "#FFFF00", "#FFA500", "#FF0000", "#8B0000"];
output["assetHealthColor"] = healthColors[this.assetHealth.index] || "unknown";
//////////////////////////
return output; return output;
} }
@@ -764,9 +986,9 @@ _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
const Child = require('../../measurement/src/specificClass'); const Child = require('../../measurement/src/specificClass');
@@ -782,7 +1004,6 @@ const PT1 = new Child(config={
}, },
functionality:{ functionality:{
softwareType:"measurement", softwareType:"measurement",
positionVsParent:"upstream",
}, },
asset:{ asset:{
supplier:"Vega", supplier:"Vega",
@@ -804,7 +1025,6 @@ const PT2 = new Child(config={
}, },
functionality:{ functionality:{
softwareType:"measurement", softwareType:"measurement",
positionVsParent:"upstream",
}, },
asset:{ asset:{
supplier:"Vega", supplier:"Vega",
@@ -820,18 +1040,17 @@ console.log(`Creating machine...`);
const machineConfig = { const machineConfig = {
general: { general: {
name: "Hydrostal", name: "Hidrostal",
logging: { logging: {
enabled: true, enabled: true,
logLevel: "debug", logLevel: "debug",
} }
}, },
asset: { asset: {
supplier: "Hydrostal", supplier: "Hidrostal",
type: "pump", type: "pump",
category: "centrifugal", category: "centrifugal",
model: "H05K-S03R+HGM1X-X280KO", // Ensure this field is present. model: "hidrostal-H05K-S03R", // Ensure this field is present.
machineCurve: curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"],
} }
} }
@@ -860,18 +1079,17 @@ const machine = new Machine(machineConfig, stateConfig);
machine.logger.info(`Registering child...`); machine.logger.info(`Registering child...`);
machine.childRegistrationUtils.registerChild(PT1, "upstream"); machine.childRegistrationUtils.registerChild(PT1, "upstream");
machine.childRegistrationUtils.registerChild(PT2, "downstream"); machine.childRegistrationUtils.registerChild(PT2, "downstream");
/*
//feed curve to the machine class
//machine.updateCurve(curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"]);
PT1.logger.info(`Enable sim...`); PT1.logger.info(`Enable sim...`);
PT1.toggleSimulation(); PT1.toggleSimulation();
PT2.logger.info(`Enable sim...`); PT2.logger.info(`Enable sim...`);
PT2.toggleSimulation(); PT2.toggleSimulation();
machine.getOutput(); */
//manual test //manual test
//machine.handleInput("parent", "execSequence", "startup"); //machine.handleInput("parent", "execSequence", "startup");
/*
machine.measurements.type("pressure").variant("measured").position('upstream').value(-200); 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('downstream').value(1000);
@@ -881,8 +1099,8 @@ const tickLoop = setInterval(changeInput,1000);
function changeInput(){ function changeInput(){
PT1.logger.info(`tick...`); PT1.logger.info(`tick...`);
PT1.tick(); //PT1.tick();
PT2.tick(); //PT2.tick();
} }
async function testingSequences(){ async function testingSequences(){