From 167628a4362f2c707aa70b159df7ce00e27071bb Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:14:19 +0200 Subject: [PATCH] updated valve to latest version of EVOLV eco --- dependencies/valveConfig.json | 381 ------------------ package.json | 3 +- src/nodeClass.js | 294 ++++++++++++++ .../valveClass.js => src/specificClass.js | 141 +++++-- valve.html | 348 +++++----------- valve.js | 278 ++----------- 6 files changed, 527 insertions(+), 918 deletions(-) delete mode 100644 dependencies/valveConfig.json create mode 100644 src/nodeClass.js rename dependencies/valveClass.js => src/specificClass.js (66%) diff --git a/dependencies/valveConfig.json b/dependencies/valveConfig.json deleted file mode 100644 index 0093c3a..0000000 --- a/dependencies/valveConfig.json +++ /dev/null @@ -1,381 +0,0 @@ -{ - "general": { - "name": { - "default": "Valve", - "rules": { - "type": "string", - "description": "A human-readable name or label for this valve configuration." - } - }, - "id": { - "default": null, - "rules": { - "type": "string", - "nullable": true, - "description": "A unique identifier for this configuration. If not provided, defaults to null." - } - }, - "unit": { - "default": "m3/h", - "rules": { - "type": "string", - "description": "The default measurement unit for this configuration (e.g., 'meters', 'seconds', 'unitless')." - } - }, - "logging": { - "logLevel": { - "default": "info", - "rules": { - "type": "enum", - "values": [ - { - "value": "debug", - "description": "Log messages are printed for debugging purposes." - }, - { - "value": "info", - "description": "Informational messages are printed." - }, - { - "value": "warn", - "description": "Warning messages are printed." - }, - { - "value": "error", - "description": "Error messages are printed." - } - ] - } - }, - "enabled": { - "default": true, - "rules": { - "type": "boolean", - "description": "Indicates whether logging is active. If true, log messages will be generated." - } - } - } - }, - "functionality": { - "softwareType": { - "default": "valve", - "rules": { - "type": "string", - "description": "Specified software type for this configuration." - } - }, - "role": { - "default": "valveController", - "rules": { - "type": "string", - "description": "Indicates the role this configuration plays within the system." - } - } - }, - "asset": { - "uuid": { - "default": null, - "rules": { - "type": "string", - "nullable": true, - "description": "A universally unique identifier for this asset. May be null if not assigned." - } - }, - "geoLocation": { - "default": {}, - "rules": { - "type": "object", - "description": "An object representing the asset's physical coordinates or location.", - "schema": { - "x": { - "default": 0, - "rules": { - "type": "number", - "description": "X coordinate of the asset's location." - } - }, - "y": { - "default": 0, - "rules": { - "type": "number", - "description": "Y coordinate of the asset's location." - } - }, - "z": { - "default": 0, - "rules": { - "type": "number", - "description": "Z coordinate of the asset's location." - } - } - } - } - }, - "supplier": { - "default": "Unknown", - "rules": { - "type": "string", - "description": "The supplier or manufacturer of the asset." - } - }, - "type": { - "default": "valve", - "rules": { - "type": "string", - "description": "A general classification of the asset tied to the specific software. This is not chosen from the asset dropdown menu." - } - }, - "subType": { - "default": "ecdv", - "rules": { - "type": "string", - "description": "A more specific classification within 'type'. For example, 'ecdv valve'." - } - }, - "model": { - "default": "Unknown", - "rules": { - "type": "string", - "description": "A user-defined or manufacturer-defined model identifier for the asset." - } - }, - "accuracy": { - "default": null, - "rules": { - "type": "number", - "nullable": true, - "description": "The accuracy of the valve or sensor, typically as a percentage or absolute value." - } - }, - "valveCurve": { - "default": { - "nq": { - "1": { - "x": [ - 1, - 2, - 3, - 4, - 5 - ], - "y": [ - 10, - 20, - 30, - 40, - 50 - ] - } - }, - "np": { - "1": { - "x": [ - 1, - 2, - 3, - 4, - 5 - ], - "y": [ - 10, - 20, - 30, - 40, - 50 - ] - } - } - }, - "rules": { - "type": "valveCurve", - "description": "All valves curves must have a 'nq' and 'np' curve. nq stands for the flow curve, np stands for the power curve. Together they form the efficiency curve." - } - } - }, - "mode": { - "current": { - "default": "auto", - "rules": { - "type": "enum", - "values": [ - { - "value": "auto", - "description": "Accepts setpoints from a parent controller and runs autonomously." - }, - { - "value": "virtualControl", - "description": "Controlled via GUI setpoints; ignores parent commands." - }, - { - "value": "fysicalControl", - "description": "Controlled via physical buttons or switches; ignores external automated commands." - }, - { - "value": "maintenance", - "description": "No active control from auto, virtual, or fysical sources." - } - ], - "description": "The operational mode." - } - }, - "allowedActions":{ - "default":{}, - "rules": { - "type": "object", - "schema":{ - "auto": { - "default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Actions allowed in auto mode." - } - }, - "virtualControl": { - "default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Actions allowed in virtualControl mode." - } - }, - "fysicalControl": { - "default": ["statusCheck", "emergencyStop"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Actions allowed in fysicalControl mode." - } - }, - "maintenance": { - "default": ["statusCheck"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Actions allowed in maintenance mode." - } - } - }, - "description": "Information about valid command sources recognized." - } - }, - "allowedSources":{ - "default": {}, - "rules": { - "type": "object", - "schema":{ - "auto": { - "default": ["parent", "GUI", "fysical"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sources allowed in auto mode." - } - }, - "virtualControl": { - "default": ["GUI", "fysical"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sources allowed in virtualControl mode." - } - }, - "fysicalControl": { - "default": ["fysical"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sources allowed in fysicalControl mode." - } - } - }, - "description": "Information about valid command sources recognized." - } - } - }, - "source": { - "default": "parent", - "rules": { - "type": "enum", - "values": [ - { - "value": "parent", - "description": "Commands are received from a parent controller." - }, - { - "value": "GUI", - "description": "Commands are received from a graphical user interface." - }, - { - "value": "fysical", - "description": "Commands are received from physical buttons or switches." - } - ], - "description": "Information about valid command sources recognized." - } - }, - "sequences":{ - "default":{}, - "rules": { - "type": "object", - "schema": { - "startup": { - "default": ["starting","warmingup","operational"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sequence of states for starting up." - } - }, - "shutdown": { - "default": ["stopping","coolingdown","idle"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sequence of states for shutting down." - } - }, - "emergencystop": { - "default": ["emergencystop","off"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sequence of states for an emergency stop." - } - }, - "boot": { - "default": ["idle","starting","warmingup","operational"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sequence of states for booting up." - } - } - } - }, - "description": "Predefined sequences of states." - - }, - "calculationMode": { - "default": "medium", - "rules": { - "type": "enum", - "values": [ - { - "value": "low", - "description": "Calculations run at fixed intervals (time-based)." - }, - { - "value": "medium", - "description": "Calculations run when new setpoints arrive or measured changes occur (event-driven)." - }, - { - "value": "high", - "description": "Calculations run on all event-driven info, including every movement." - } - ], - "description": "The frequency at which calculations are performed." - } - } - } - \ No newline at end of file diff --git a/package.json b/package.json index 4b06ece..cea43a0 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ "author": "Rene De Ren / Janneke Tack", "license": "SEE LICENSE", "dependencies": { - "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git", - "convert": "git+https://gitea.centraal.wbd-rd.nl/RnD/convert.git" + "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git" }, "node-red": { "nodes": { diff --git a/src/nodeClass.js b/src/nodeClass.js new file mode 100644 index 0000000..5ea37c5 --- /dev/null +++ b/src/nodeClass.js @@ -0,0 +1,294 @@ +/** + * Encapsulates all node logic in a reusable class. In future updates we can split this into multiple generic classes and use the config to specifiy which ones to use. + * This allows us to keep the Node-RED node clean and focused on wiring up the UI and event handlers. + */ +const { outputUtils, configManager } = require('generalFunctions'); +const Specific = require("./specificClass"); + + +class nodeClass { + /** + * Create a MeasurementNode. + * @param {object} uiConfig - Node-RED node configuration. + * @param {object} RED - Node-RED runtime API. + * @param {object} nodeInstance - The Node-RED node instance. + * @param {string} nameOfNode - The name of the node, used for + */ + constructor(uiConfig, RED, nodeInstance, nameOfNode) { + + // Preserve RED reference for HTTP endpoints if needed + this.node = nodeInstance; + this.RED = RED; + this.name = nameOfNode; + this.source = null; // Will hold the specific class instance + this.config = null; // Will hold the merged configuration + + // Load default & UI config + this._loadConfig(uiConfig,this.node); + + // Instantiate core Measurement class + this._setupSpecificClass(uiConfig); + + // Wire up event and lifecycle handlers + this._bindEvents(); + this._registerChild(); + this._startTickLoop(); + this._attachInputHandler(); + this._attachCloseHandler(); + } + + /** + * Load and merge default config with user-defined settings. + * @param {object} uiConfig - Raw config from Node-RED UI. + */ + _loadConfig(uiConfig,node) { + + // Merge UI config over defaults + this.config = { + general: { + name: uiConfig.name, + 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 || 'atEquipment', // Default to 'atEquipment' if not specified + } + }; + + // Utility for formatting outputs + this._output = new outputUtils(); + } + + /** + * Instantiate the core logic and store as source. + */ + _setupSpecificClass(uiConfig) { + const vconfig = this.config; + + // need extra state for this + const stateConfig = { + general: { + logging: { + enabled: vconfig.eneableLog, + logLevel: vconfig.logLevel + } + }, + movement: { + speed: Number(uiConfig.speed) + }, + time: { + starting: Number(uiConfig.startup), + warmingup: Number(uiConfig.warmup), + stopping: Number(uiConfig.shutdown), + coolingdown: Number(uiConfig.cooldown) + } + }; + + this.source = new Specific(vconfig, stateConfig); + + //store in node + this.node.source = this.source; // Store the source in the node instance for easy access + + } + + /** + * Bind Measurement events to Node-RED status updates. Using internal emitter. --> REMOVE LATER WE NEED ONLY COMPLETE CHILDS AND THEN CHECK FOR UPDATES + */ + _bindEvents() { + + } + + _updateNodeStatus() { + const v = this.source; + + try { + const mode = v.currentMode; // modus is bijv. auto, manual, etc. + const state = v.state.getCurrentState(); //is bijv. operational, idle, off, etc. + const flow = Math.round(v.measurements.type("flow").variant("measured").position("downstream").getCurrentValue()); + let deltaP = v.measurements.type("pressure").variant("predicted").position("delta").getCurrentValue(); + if (deltaP !== null) { + deltaP = parseFloat(deltaP.toFixed(0)); + } //afronden op 4 decimalen indien geen "null" + if(isNaN(deltaP)) { + deltaP = "∞"; + } + const roundedPosition = Math.round(v.state.getCurrentPosition() * 100) / 100; + let symbolState; + switch(state){ + case "off": + symbolState = "⬛"; + break; + case "idle": + symbolState = "⏸️"; + break; + case "operational": + symbolState = "⏵️"; + break; + case "starting": + symbolState = "⏯️"; + break; + case "warmingup": + symbolState = "🔄"; + break; + case "accelerating": + symbolState = "⏩"; + break; + case "stopping": + symbolState = "⏹️"; + break; + case "coolingdown": + symbolState = "❄️"; + break; + case "decelerating": + symbolState = "⏪"; + break; + } + + + let status; + switch (state) { + case "off": + status = { fill: "red", shape: "dot", text: `${mode}: OFF` }; + break; + case "idle": + status = { fill: "blue", shape: "dot", text: `${mode}: ${symbolState}` }; + break; + case "operational": + status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ΔP${deltaP} mbar`}; //deltaP toegevoegd + break; + case "starting": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; + break; + case "warmingup": + status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ΔP${deltaP} mbar`}; //deltaP toegevoegd + break; + case "accelerating": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ΔP${deltaP} mbar` }; //deltaP toegevoegd + break; + case "stopping": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; + break; + case "coolingdown": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; + break; + case "decelerating": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} - ${roundedPosition}% | 💨${flow}m³/h | ΔP${deltaP} mbar`}; //deltaP toegevoegd + break; + default: + status = { fill: "grey", shape: "dot", text: `${mode}: ${symbolState}` }; + } + return status; + } catch (error) { + node.error("Error in updateNodeStatus: " + error.message); + return { fill: "red", shape: "ring", text: "Status Error" }; + } + } + + /** + * Register this node as a child upstream and downstream. + * Delayed to avoid Node-RED startup race conditions. + */ + _registerChild() { + setTimeout(() => { + this.node.send([ + null, + null, + { topic: 'registerChild', payload: this.node.id , positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' }, + ]); + }, 100); + } + + /** + * Start the periodic tick loop. + */ + _startTickLoop() { + setTimeout(() => { + this._tickInterval = setInterval(() => this._tick(), 1000); + + // Update node status on nodered screen every second ( this is not the best way to do this, but it works for now) + this._statusInterval = setInterval(() => { + const status = this._updateNodeStatus(); + this.node.status(status); + }, 1000); + + }, 1000); + } + + /** + * Execute a single tick: update measurement, format and send outputs. + */ + _tick() { + //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'); + + // Send only updated outputs on ports 0 & 1 + this.node.send([processMsg, influxMsg]); + } + + /** + * Attach the node's input handler, routing control messages to the class. + */ + _attachInputHandler() { + this.node.on('input', (msg, send, done) => { + const v = this.source; + switch(msg.topic) { + case 'registerChild': + const childId = msg.payload; + const childObj = this.RED.nodes.getNode(childId); + v.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent); + break; + case 'setMode': + v.setMode(msg.payload); + break; + case 'execSequence': + const { source: seqSource, action: seqAction, parameter } = msg.payload; + v.handleInput(seqSource, seqAction, parameter); + break; + case 'execMovement': + const { source: mvSource, action: mvAction, setpoint } = msg.payload; + v.handleInput(mvSource, mvAction, Number(setpoint)); + break; + case 'emergencystop': + const { source: esSource, action: esAction } = msg.payload; + v.handleInput(esSource, esAction); + break; + case 'showcurve': + v.showCurve(); + send({ topic : "Showing curve" , payload: v.showCurve() }); + break; + case 'updateFlow': //Als nieuwe flow van header node dan moet deltaP weer opnieuw worden berekend en doorgegeven aan header node + v.updateFlow(msg.payload.variant, msg.payload.value, msg.payload.position); + } + done(); + }); + } + + /** + * Clean up timers and intervals when Node-RED stops the node. + */ + _attachCloseHandler() { + this.node.on('close', (done) => { + clearInterval(this._tickInterval); + clearInterval(this._statusInterval); + done(); + }); + } +} + +module.exports = nodeClass; diff --git a/dependencies/valveClass.js b/src/specificClass.js similarity index 66% rename from dependencies/valveClass.js rename to src/specificClass.js index 725424b..edfafc2 100644 --- a/dependencies/valveClass.js +++ b/src/specificClass.js @@ -1,3 +1,42 @@ +/** + * @file valve.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. + * + * Author: + * - Rene De Ren + * Email: + * - r.de.ren@brabantsedelta.nl + * + * Future Improvements: + * - Time-based stability checks + * - Warmup handling + * - Dynamic outlier detection thresholds + * - Dynamic smoothing window and methods + * - Alarm and threshold handling + * - Maintenance mode + * - Historical data and trend analysis + */ /** * @file valveClass.js * @@ -6,34 +45,34 @@ .... */ - -//load local dependencies #NOTE: Vul hier nog de juiste dependencies in als meer nodig of sommige niet nodig +//load local dependencies const EventEmitter = require('events'); -const Logger = require('../../generalFunctions/helper/logger'); -const State = require('../../generalFunctions/helper/state/state'); -const Predict = require('../../predict/dependencies/predict/predict_class'); -const { MeasurementContainer } = require('../../generalFunctions/helper/measurements/index'); - -//load all config modules #NOTE: Vul hier nog de juiste dependencies in als meer nodig of sommige niet nodig -const defaultConfig = require('./valveConfig.json'); -const ConfigUtils = require('../../generalFunctions/helper/configUtils'); - -//load registration utility #NOTE: Vul hier nog de juiste dependencies in als meer nodig of sommige niet nodig -const ChildRegistrationUtils = require('../../generalFunctions/helper/childRegistrationUtils'); +const {loadCurve,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils} = require('generalFunctions'); class Valve { constructor(valveConfig = {}, stateConfig = {}) { - this.emitter = new EventEmitter(); // nodig voor ontvangen en uitvoeren van events emit() en on() --> Zien als internet berichten (niet bedraad in node-red) - this.configUtils = new ConfigUtils(defaultConfig); // nodig voor het ophalen van de default configuaratie - this.config = this.configUtils.initConfig(valveConfig); //valve configurations die bij invoer in node-red worden gegeven - + //basic setup + this.emitter = new EventEmitter(); // nodig voor ontvangen en uitvoeren van events emit() --> Zien als internet berichten (niet bedraad in node-red) + + this.logger = new logger(valveConfig.general.logging.enabled,valveConfig.general.logging.logLevel, valveConfig.general.name); + this.configManager = new configManager(); + this.defaultConfig = this.configManager.getConfig('valve'); // Load default config for rotating machine ( use software type name ? ) + this.configUtils = new configUtils(this.defaultConfig); + + // Load a specific curve + this.model = valveConfig.asset.model; // Get the model from the valveConfig + this.curve = this.model ? loadCurve(this.model) : null; + + //Init config and check if it is valid + this.config = this.configUtils.initConfig(valveConfig); + // Initialize measurements this.measurements = new MeasurementContainer(); this.child = {}; // object to hold child information so we know on what to subscribe // Init after config is set - this.logger = new Logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, this.config.general.name); - this.state = new State(stateConfig, this.logger); // Init State manager and pass logger + this.state = new state(stateConfig, this.logger); // Init State manager and pass logger + this.state.stateManager.currentState = "operational"; // Set default state to operational this.kv = 0; //default @@ -48,11 +87,10 @@ class Valve { this.updatePosition()}); //To update deltaP - this.childRegistrationUtils = new ChildRegistrationUtils(this); // Child registration utility - - //replace v_curve loadspecs with config file afterwards !!!!!!!!!! - this.vCurve = this.loadSpecs().v_curve - this.predictKv = new Predict({curve:this.vCurve}); // load valve size (x : ctrl , y : kv relationship) + this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility + this.vCurve = this.curve[1.204]; // specificy the desired density RECALC THIS AUTOMTICALLY BASED ON DENSITY OF AIR LATER OLIFANT!! + this.predictKv = new predict({curve:this.vCurve}); // load valve size (x : ctrl , y : kv relationship) + console.log(`PredictKv initialized with curve: ${JSON.stringify(this.predictKv)}`); } // -------- Config -------- // @@ -187,7 +225,24 @@ class Valve { } } - + updateMeasurement(variant, subType, value, position) { + this.logger.debug(`---------------------- updating ${subType} ------------------ `); + switch (subType) { + case "pressure": + // Update pressure measurement + //this.updatePressure(variant,value,position); + break; + case "flow": + this.updateFlow(variant,value,position); + break; + case "power": + // Update power measurement + break; + default: + this.logger.error(`Type '${subType}' not recognized for measured update.`); + return; + } + } // NOTE: Omdat met zeer kleine getallen wordt gewerkt en er kwadraten in de formule zitten kan het zijn dat we alles *1000 moeten doen updateDeltaPKlep(q,kv,downstreamP,rho,temp){ @@ -218,15 +273,25 @@ class Valve { } - // Als er een nieuwe flow door de klep komt doordat de pompen harder zijn gaan pompen, dan update deze functie dit ook in de valve attributes en measurements - //NOTE: samenvoegen met updateFlow als header node er is - updateFlowKlep(q){ - //q must be in Nm3/h - // Opslaan in measurement container van valve object - this.measurements.type("flow").variant("predicted").position("downstream").value(q); - this.logger.info('FlowKlep updated to: ' + q); - this.logger.info('Calculating new deltaP based on new flow'); - this.updateDeltaPKlep(q,this.kv,this.downstreamP,this.rho,this.T); //update deltaP based on new flow + // Als er een nieuwe flow door de klep komt doordat de machines harder zijn gaan werken, dan update deze functie dit ook in de valve attributes en measurements + updateFlow(variant,value,position) { + + switch (variant) { + case ("measured"): + // put value in measurements + this.measurements.type("flow").variant("measured").position(position).value(value); + const downStreamP = this.measurements.type("pressure").variant("measured").position("downstream").getCurrentValue(); //update downstream pressure measurement + this.updateDeltaPKlep(value,this.kv,downStreamP,this.rho,this.T); //update deltaP based on new flow + break; + + case ("predicted"): + this.logger.debug('not doing anythin yet'); + break; + + default: + this.logger.warn(`Unrecognized variant '${variant}' for flow update.`); + break; + } } updatePosition() { //update alle parameters nadat er een verandering is geweest in stand van klep @@ -234,11 +299,10 @@ class Valve { this.logger.debug('Calculating new deltaP'); const currentPosition = this.state.getCurrentPosition(); - const currentFlow = this.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); // haal de flow op uit de measurement containe + const currentFlow = this.measurements.type("flow").variant("measured").position("downstream").getCurrentValue(); // haal de flow op uit de measurement containe + const downstreamP = this.measurements.type("pressure").variant("measured").position("downstream").getCurrentValue(); // haal de downstream pressure op uit de measurement container //const valveSize = 125; //NOTE: nu nog hardcoded maar moet een attribute van de valve worden this.predictKv.fDimension = 125; //load valve size by defining fdimension in predict class - //const vCurve = this.loadSpecs().v_curve[valveSize]; // haal de curve op van de valve - //const Spline = require('cubic-spline'); // spline library -> nodig om kv waarde te benaderen op curve const x = currentPosition; // dit is de positie van de klep waarvoor we delta P willen berekenen const y = this.predictKv.y(x); // haal de waarde van kv op uit de spline @@ -247,7 +311,8 @@ class Valve { this.kv = 0.1; //minimum waarde voor kv } this.logger.debug(`Kv value for position valve ${x} is ${this.kv}`); // log de waarde van kv - this.updateDeltaPKlep(currentFlow,this.kv,this.downstreamP,this.rho,this.T); //update deltaP + + this.updateDeltaPKlep(currentFlow,this.kv,downstreamP,this.rho,this.T); //update deltaP } } diff --git a/valve.html b/valve.html index e2bd435..be0c10f 100644 --- a/valve.html +++ b/valve.html @@ -1,275 +1,117 @@ - - + - //define general asset properties - supplier: { value: "" }, // laten staan als voorbeeld - /*NOT USED - subType: { value: "" }, - model: { value: "" }, - unit: { value: "" },*/ + + + + + - - - - - + + +
+ + + + + + + + + + + diff --git a/valve.js b/valve.js index a90f275..6c83bdb 100644 --- a/valve.js +++ b/valve.js @@ -1,250 +1,40 @@ -module.exports = function (RED) { - function valve(config) { - //create node +const nameOfNode = 'valve'; // this is the name of the node, it should match the file name and the node type in Node-RED +const nodeClass = require('./src/nodeClass.js'); // this is the specific node class +const { MenuManager, configManager } = require('generalFunctions'); + +// This is the main entry point for the Node-RED node, it will register the node and setup the endpoints +module.exports = function(RED) { + // Register the node type + RED.nodes.registerType(nameOfNode, function(config) { + // Initialize the Node-RED node first RED.nodes.createNode(this, config); - //call this => node so whenver you want to call a node function type node and the function behind it - var node = this; + // Then create your custom class and attach it + this.nodeClass = new nodeClass(config, RED, this, nameOfNode); + }); + // Setup admin UIs + const menuMgr = new MenuManager(); //this will handle the menu endpoints so we can load them dynamically + const cfgMgr = new configManager(); // this will handle the config endpoints so we can load them dynamically + + // Register the different menu's for the measurement node (in the future we could automate this further by refering to the config) + RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => { try { - const Valve = require("./dependencies/valveClass"); // Importeer de valve class - const OutputUtils = require("../generalFunctions/helper/outputUtils"); // Importeer de OutputUtils class - - const valveConfig = { // Configuratie van de valve - general: { - name: config.name || "Default Valve", - id: node.id, - logging: { - enabled: config.eneableLog, - logLevel: config.logLevel - } - }, - asset: { - supplier: config.supplier || "Unknown", - /* NOT USED - type: config.valveType || "generic", - subType: config.subType || "generic", - model: config.model || "generic", - valveCurve: config.valveCurve */ - } - }; - - const stateConfig = { // Configuratie van de state - general: { - logging: { - enabled: config.eneableLog, - logLevel: config.logLevel - } - }, - - /* NOT USED - movement: { - speed: Number(config.speed) - }, - time: { - starting: Number(config.startup), - warmingup: Number(config.warmup), - stopping: Number(config.shutdown), - coolingdown: Number(config.cooldown) - } */ - }; - - // Create valve instance - const v = new Valve(valveConfig, stateConfig); - - // put m on node memory as source - node.source = v; - - //load output utils - const output = new OutputUtils(); - - //Hier worden node-red statussen en metingen geupdate - function updateNodeStatus() { - try { - const mode = v.currentMode; // modus is bijv. auto, manual, etc. - const state = v.state.getCurrentState(); //is bijv. operational, idle, off, etc. - const flow = Math.round(v.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue()); - let deltaP = v.measurements.type("pressure").variant("predicted").position("delta").getCurrentValue(); - if (deltaP !== null) { - deltaP = parseFloat(deltaP.toFixed(0)); - } //afronden op 4 decimalen indien geen "null" - if(isNaN(deltaP)) { - deltaP = "∞"; - } - const roundedPosition = Math.round(v.state.getCurrentPosition() * 100) / 100; - let symbolState; - switch(state){ - case "off": - symbolState = "⬛"; - break; - case "idle": - symbolState = "⏸️"; - break; - case "operational": - symbolState = "⏵️"; - break; - case "starting": - symbolState = "⏯️"; - break; - case "warmingup": - symbolState = "🔄"; - break; - case "accelerating": - symbolState = "⏩"; - break; - case "stopping": - symbolState = "⏹️"; - break; - case "coolingdown": - symbolState = "❄️"; - break; - case "decelerating": - symbolState = "⏪"; - break; - } - - - let status; - switch (state) { - case "off": - status = { fill: "red", shape: "dot", text: `${mode}: OFF` }; - break; - case "idle": - status = { fill: "blue", shape: "dot", text: `${mode}: ${symbolState}` }; - break; - case "operational": - status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ΔP${deltaP} mbar`}; //deltaP toegevoegd - break; - case "starting": - status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; - break; - case "warmingup": - status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ΔP${deltaP} mbar`}; //deltaP toegevoegd - break; - case "accelerating": - status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ΔP${deltaP} mbar` }; //deltaP toegevoegd - break; - case "stopping": - status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; - break; - case "coolingdown": - status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; - break; - case "decelerating": - status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} - ${roundedPosition}% | 💨${flow}m³/h | ΔP${deltaP} mbar`}; //deltaP toegevoegd - break; - default: - status = { fill: "grey", shape: "dot", text: `${mode}: ${symbolState}` }; - } - return status; - } catch (error) { - node.error("Error in updateNodeStatus: " + error.message); - return { fill: "red", shape: "ring", text: "Status Error" }; - } - } - - function tick() { // versturen van output messages --> tick van tick op de klop. Is tijd based en niet event based - try { - const status = updateNodeStatus(); - node.status(status); - - //v.tick(); - - //get output - const classOutput = v.getOutput(); - const dbOutput = output.formatMsg(classOutput, v.config, "influxdb"); - const pOutput = output.formatMsg(classOutput, v.config, "process"); - - //only send output on values that changed - let msgs = []; - msgs[0] = pOutput; - msgs[1] = dbOutput; - - node.send(msgs); - - } catch (error) { - node.error("Error in tick function: " + error); - node.status({ fill: "red", shape: "ring", text: "Tick Error" }); - } - } - - // register child on first output this timeout is needed because of node - red stuff - setTimeout( - () => { - - /*---execute code on first start----*/ - let msgs = []; - - msgs[2] = { topic : "registerChild" , payload: node.id, positionVsParent: "upStream" }; - msgs[3] = { topic : "registerChild" , payload: node.id, positionVsParent: "downStream" }; - - //send msg - node.send(msgs); - }, - 100 - ); - - //declare refresh interval internal node - - setTimeout( - () => { - //---execute code on first start---- - this.interval_id = setInterval(function(){ tick() },1000) - }, - 1000 - ); - - node.on("input", function(msg, send, done) { // Functie die wordt aangeroepen wanneer er een input wordt ontvangen - console.log("CKECK! Input received: ", msg.topic, msg.payload); // CHECKPOINT - try { - let result; - switch(msg.topic) { - case 'registerChild': - const childId = msg.payload; - const childObj = RED.nodes.getNode(childId); - v.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent); - break; - case 'setMode': - v.setMode(msg.payload); - break; - case 'execSequence': - const { source: seqSource, action: seqAction, parameter } = msg.payload; - v.handleInput(seqSource, seqAction, parameter); - break; - case 'execMovement': - const { source: mvSource, action: mvAction, setpoint } = msg.payload; - v.handleInput(mvSource, mvAction, Number(setpoint)); - break; - case 'emergencystop': - const { source: esSource, action: esAction } = msg.payload; - v.handleInput(esSource, esAction); - break; - case 'showcurve': - v.showCurve(); - send({ topic : "Showing curve" , payload: v.showCurve() }); - break; - case 'newFlow': //Als nieuwe flow van header node dan moet deltaP weer opnieuw worden berekend en doorgegeven aan header node - const { source: nfSource, action: nfAction, parameter: nfParameter } = msg.payload; //parameter is new flow, action should be "calcNewDeltaP" - v.handleInput(nfSource, nfAction, nfParameter); - } - - if (done) done(); - } catch (error) { - node.error("Error processing input: " + error.message); - if (done) done(error); - } - }); - - node.on('close', function(done) { // Functie die wordt aangeroepen wanneer de node wordt gesloten - if (node.interval_id) clearTimeout(node.interval_id); - if (node.tick_interval) clearInterval(node.tick_interval); - if (done) done(); - }); - - } catch (error) { - node.error("Fatal error in node initialization: " + error.stack); - node.status({fill: "red", shape: "ring", text: "Fatal Error"}); + const script = menuMgr.createEndpoint(nameOfNode, ['asset','logger','position']); + res.type('application/javascript').send(script); + } catch (err) { + res.status(500).send(`// Error generating menu: ${err.message}`); } + }); + + // Endpoint to get the configuration data for the specific node + RED.httpAdmin.get(`/${nameOfNode}/configData.js`, (req, res) => { + try { + const script = cfgMgr.createEndpoint(nameOfNode); + // Send the configuration data as JSON response + res.type('application/javascript').send(script); + } catch (err) { + res.status(500).send(`// Error generating configData: ${err.message}`); + } + }); - } - RED.nodes.registerType("valve", valve); }; \ No newline at end of file