From abd1f41b44f4f9d59af4b7c4423b9cd1909b00c5 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Thu, 31 Jul 2025 09:08:25 +0200 Subject: [PATCH] converted j. Tack version to main stack v0.1 --- LICENSE | 4 +- README.md | 71 ++++- dependencies/valveGroupControlClass.js | 245 -------------- dependencies/valveGroupControlConfig.json | 371 ---------------------- package.json | 5 +- src/nodeClass.js | 218 +++++++++++++ src/specificClass.js | 333 +++++++++++++++++++ valveGroupControl.html | 286 ----------------- valveGroupControl.js | 247 -------------- vgc.html | 86 +++++ vgc.js | 39 +++ 11 files changed, 749 insertions(+), 1156 deletions(-) delete mode 100644 dependencies/valveGroupControlClass.js delete mode 100644 dependencies/valveGroupControlConfig.json create mode 100644 src/nodeClass.js create mode 100644 src/specificClass.js delete mode 100644 valveGroupControl.html delete mode 100644 valveGroupControl.js create mode 100644 vgc.html create mode 100644 vgc.js diff --git a/LICENSE b/LICENSE index 2b247c4..cc2b57d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +WBD License -Copyright (c) 2025 Janneke Tack, Rene De Ren +Copyright (c) 2025 Rene De Ren Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, diff --git a/README.md b/README.md index f33b006..c420061 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,70 @@ -# convert +# Valve Group Control Node +The Valve Group Control Node is an intelligent software component that manages multiple valves as a single coordinated group. It acts as the “master controller” for all valves in a group, automatically distributing total flow, monitoring pressure drops, and optimizing system performance and safety. -Makes unit conversions \ No newline at end of file +## What Processes Can It Connect With? +Control Modules: Receives total flow setpoints or operational commands and orchestrates all connected valves to deliver the required process conditions. + +Equipment Monitoring: Monitors the real-time status, position, and pressure drop (deltaP) of each valve, detecting problems or imbalances early. + +Data Analysis: Aggregates valve data for dashboards, system analytics, and compliance reporting. + +Other Nodes: Integrates with rotating machine nodes, measurement nodes, or other group controllers to support system-wide coordination and optimization. + +## Inputs / Outputs +### Inputs: +Total flow setpoint (from operator, automation, or higher-level control) + +Operational commands (sequences, emergency stop, status checks) + +Configuration settings (operational modes, allowed sources, group properties) + +Real-time data from all connected (child) valve nodes + +### Outputs: +Calculated flow assigned to each valve, based on Kv value and position + +Max deltaP across the valve group (critical for diagnostics and protection) + +Group state and operational status + +Real-time events for dashboards, alarms, and connected systems + +## Key Capabilities +Automatic Flow Distribution: Divides total flow among all connected valves in proportion to their Kv (capacity) and actual position. + +Pressure Monitoring: Continuously calculates the highest (max) deltaP in the group, providing a single indicator for potential issues or maintenance needs. + +Flexible Control: Supports sequences (open/close cycles), emergency stops, and other automation strategies for coordinated valve management. + +Event-Driven Updates: Reacts instantly to flow changes, setpoint adjustments, or changes in valve positions—keeping the system in sync at all times. + +Child Valve Integration: Registers and manages all connected valves, directly updating each child valve with new flow assignments as process needs change. + +Configurable Modes: Can operate in different modes or accept control from multiple sources, depending on plant requirements. + +## Why is this relevant: +Balanced Process Control: Prevents overloading or starving any single valve, extending equipment life and maintaining process stability. + +Fault Detection: Makes it easy to spot when one valve is experiencing excessive pressure drop, helping to avoid costly failures. + +Easy Integration: Plug-and-play with any number of valves or control strategies; adapts easily to system upgrades or expansions. + +## Potential Use Cases +Aeration Systems: Controls multiple air valves in parallel to distribute air across tanks or zones. + +Distribution Networks: Manages groups of water, gas, or chemical valves to meet variable demand. + +Critical Processes: Ensures redundancy and reliability by automatically balancing flows across backup or parallel valves. + +Operator Dashboards: Aggregates and simplifies complex valve group data for real-time process monitoring. + +### Summary Table +Feature Description +Input Total flow setpoint, commands, configs, valve data +Output Assigned flow per valve, max deltaP, group state +Connects To Valve nodes, measurement nodes, controllers, dashboards +Smartness Auto flow split, pressure monitoring, event-driven sync +Setup Configurable per group, handles any number of valves +Benefit Optimized, reliable, and easy valve group management + +If you operate systems with multiple valves working together, the Valve Group Control Node automates flow balancing and monitoring—improving efficiency, reliability, and process transparency. \ No newline at end of file diff --git a/dependencies/valveGroupControlClass.js b/dependencies/valveGroupControlClass.js deleted file mode 100644 index b45d171..0000000 --- a/dependencies/valveGroupControlClass.js +++ /dev/null @@ -1,245 +0,0 @@ -//TODO Moet een attribute in die valves = {} houd zodat daar alle child valves in bijgehouden wordt - -/** - * @file valveGroupControlClass.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 -.... -*/ - - -//load local dependencies #NOTE: Vul hier nog de juiste dependencies in als meer nodig of sommige niet nodig -const EventEmitter = require('events'); -const Logger = require('../../generalFunctions/helper/logger'); -const State = require('../../generalFunctions/helper/state/state'); -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('./valveGroupControlConfig.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'); - -class ValveGroupControl { - constructor(valveGroupControlConfig = {}, 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(valveGroupControlConfig); //valve configurations die bij invoer in node-red worden gegeven - - // Initialize measurements - this.measurements = new MeasurementContainer(); - this.valves = {}; // hold child object so we can get information from its child valves - - // Initialize variables - this.maxDeltaP = 0; // max deltaP is 0 als er geen child valves zijn - - // Init after config is set - this.logger = new Logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, this.config.general.logging.name); - this.state = new State(stateConfig, this.logger); // Init State manager and pass logger - this.state.stateManager.currentState = "operational"; // Set default state to operational - - this.currentMode = this.config.mode.current; - - this.childRegistrationUtils = new ChildRegistrationUtils(this); // Child registration utility - } - - // -------- Config -------- // - updateConfig(newConfig) { - this.config = this.configUtils.updateConfig(this.config, newConfig); - } - - isValidSourceForMode(source, mode) { - const allowedSourcesSet = this.config.mode.allowedSources[mode] || []; - this.logger.info(`Allowed sources for mode '${mode}': ${allowedSourcesSet}`); - return allowedSourcesSet.has(source); - } - - 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.info(`Handling input from source '${source}' with action '${action}' in mode '${this.currentMode}'.`); - try { - switch (action) { - case "execSequence": - await this.executeSequence(parameter); - break; - case "totalFlowChange": // total flow veranderd dus nieuwe flow per valve berekenen. - this.measurements.type("totalFlow").variant("predicted").position("upstream").value(parameter); - const totalFlow = this.measurements.type("totalFlow").variant("predicted").position("upstream").getCurrentValue(); //CHECKPOINT - this.logger.info('Total flow changed to: ' + totalFlow); //CHECKPOINT - await this.calcValveFlows(); - break; - case "emergencyStop": - this.logger.warn(`Emergency stop activated by '${source}'.`); - await this.executeSequence("emergencyStop"); - break; - case "statusCheck": - this.logger.info(`Status Check: Mode = '${this.currentMode}', Source = '${source}'.`); - break; - default: - this.logger.warn(`Action '${action}' is not implemented.`); - break; - } - this.logger.debug(`Action '${action}' successfully executed`); - return {status : true , feedback: `Action '${action}' successfully executed.`}; - } catch (error) { - this.logger.error(`Error handling input: ${error}`); - } - - } - - setMode(newMode) { - const availableModes = defaultConfig.mode.current.rules.values.map(vgc => vgc.value); - if (!availableModes.includes(newMode)) { - this.logger.warn(`Invalid mode '${newMode}'. Allowed modes are: ${availableModes.join(', ')}`); - return; - } - - this.currentMode = newMode; - this.logger.info(`Mode successfully changed to '${newMode}'.`); - } - - - - // -------- Sequence Handlers -------- // - async executeSequence(sequenceName) { - - const sequence = this.config.sequences[sequenceName]; - - if (!sequence || sequence.size === 0) { - this.logger.warn(`Sequence '${sequenceName}' not defined.`); - return; - } - - this.logger.info(` --------- Executing sequence: ${sequenceName} -------------`); - - for (const state of sequence) { - try { - await this.state.transitionToState(state); - // Update measurements after state change - - } catch (error) { - this.logger.error(`Error during sequence '${sequenceName}': ${error}`); - break; // Exit sequence execution on error - } - } - } - - calcValveFlows() { - const totalFlow = this.measurements.type("totalFlow").variant("predicted").position("upstream").getCurrentValue(); // haal de totalFlow op uit de measurement container - let totalKv = 0; - this.logger.info('this.valves = ' + this.valves); //CHECKPOINT - for (const key in this.valves){ //bereken sum kv values om verdeling total flow te maken - this.logger.info('kv: ' + this.valves[key].kv); //CHECKPOINT - if (this.valves[key].state.getCurrentPosition() != null) { - totalKv += this.valves[key].kv; - this.logger.info('Total Kv = ' + totalKv); //CHECKPOINT - } - } - - for (const key in this.valves){ - const valve = this.valves[key]; - const ratio = valve.kv / totalKv; - const flow = ratio * totalFlow; // bereken flow per valve - - // Check of update in valve object vanuit valvegroupcontrol werk - this.logger.info(`Flow for valve ${key} is ${flow} and updateFlowKlep event triggered in valve object`); - const currentFlow = valve.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); - this.logger.info('Current flow valve = ' + currentFlow); - - //update flow per valve in de object zelf wat daar vervolgens weer de nieuwe deltaP berekent - valve.updateFlowKlep(flow); - this.logger.info('--> Sending updated flow to valves --> ') //Checkpoint - - - // Check of update in valve object vanuit valvegroupcontrol werk - const updatedFlow = valve.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); - this.logger.info('Updated flow valve = ' + updatedFlow); - } - } - - calcMaxDeltaP() { // bereken de max deltaP van alle child valves - let maxDeltaP = 0; //max deltaP is 0 als er geen child valves zijn - this.logger.info('CHECK!'); //CHECKPOINT - this.logger.info('CHECK! Valves: ' + this.valves); //CHECKPOINT - this.logger.info('Calculating new max deltaP...'); - for (const key in this.valves) { - const valve = this.valves[key]; //haal de child valve object op - const deltaP = valve.measurements.type("pressure").variant("predicted").position("delta").getCurrentValue(); //get delta P - if (deltaP > maxDeltaP) { //als de deltaP van de child valve groter is dan de huidige maxDeltaP, dan update deze - maxDeltaP = deltaP; - } - } - this.logger.info('Max Delta P updated to: ' + maxDeltaP); - - this.maxDeltaP = maxDeltaP; //update de max deltaP in de measurement container van de valveGroupControl class - -} - - - getOutput() { - - // Improved output object generation - const output = {}; - //build the output object - this.measurements.getTypes().forEach(type => { - this.measurements.getVariants().forEach(variant => { - this.measurements.getPositions().forEach(position => { - - const value = this.measurements.type(type).variant(variant).position(position).getCurrentValue(); //get the current value of the measurement - - - if (value != null) { - output[`${position}_${variant}_${type}`] = value; - } - }); - }); - }); - - //fill in the rest of the output object - output["state"] = this.state.getCurrentState(); - output["moveTimeleft"] = this.state.getMoveTimeLeft(); - output["mode"] = this.currentMode; - output["maxDeltaP"] = this.maxDeltaP; - - //this.logger.debug(`Output: ${JSON.stringify(output)}`); - - return output; - } - -} - -module.exports = ValveGroupControl; - - -/* -const valve = require('../../valve/dependencies/valveClass.js'); -const valve1 = new valve(); -const valve2 = new valve(); -const valve3 = new valve(); - -const vgc = new ValveGroupControl(); - -vgc.childRegistrationUtils.registerChild(valve1, "downStream"); -vgc.childRegistrationUtils.registerChild(valve2, "downStream"); -vgc.childRegistrationUtils.registerChild(valve3, "downStream"); - -vgc.handleInput("parent", "totalFlowChange", Number(1600)); -*/ - - - - - - - - - - diff --git a/dependencies/valveGroupControlConfig.json b/dependencies/valveGroupControlConfig.json deleted file mode 100644 index 1fd26a2..0000000 --- a/dependencies/valveGroupControlConfig.json +++ /dev/null @@ -1,371 +0,0 @@ -{ - "general": { - "name": { - "default": "ValveGroupControl", - "rules": { - "type": "string", - "description": "A human-readable name or label for this valveGroupControl configuration." - } - }, - "id": { - "default": null, - "rules": { - "type": "string", - "nullable": true, - "description": "A unique identifier for this configuration. If not provided, defaults to null." - } - }, - "unit": { - "default": "unitless", - "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": "valveGroupControl", - "rules": { - "type": "string", - "description": "Specified software type for this configuration." - } - }, - "role": { - "default": "ValveGroupController", - "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": "Unknown", - "rules": { - "type": "string", - "description": "A more specific classification within 'type'. For example, 'centrifugal' for a centrifugal pump." - } - }, - "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." - } - } - }, - "mode": { - "current": { - "default": "auto", - "rules": { - "type": "enum", - "values": [ - { - "value": "auto", - "description": "ValveGroupController accepts inputs from a parents and childs 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 of the valveGroupControl." - } - }, - "allowedActions":{ - "default":{}, - "rules": { - "type": "object", - "schema":{ - "auto": { - "default": ["statusCheck", "execSequence", "emergencyStop", "valvePositionChange", "totalFlowChange", "valveDeltaPchange"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Actions allowed in auto mode." - } - }, - "virtualControl": { - "default": ["statusCheck", "execSequence", "emergencyStop", "valvePositionChange", "totalFlowChange", "valveDeltaPchange"], - "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 by the valve." - } - }, - "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 by the valveGroupControl." - } - } - }, - "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 by the valveGroupControl." - } - }, - "action": { - "default": "statusCheck", - "rules": { - "type": "enum", - "values": [ - { - "value": "statusCheck", - "description": "Checks the valveGroupControl's state (mode, submode, operational status)." - }, - { - "value": "valvePositionChange", - "description": "If child valve position change, the new flow for each child valve is determined" - }, - { - "value": "execSequence", - "description": "Allows execution of sequences through auto or GUI controls." - }, - { - "value": "totalFlowChange", - "description": "If total flow change, the new flow for each child valve is determined" - }, - { - "value": "valveDeltaPchange", - "description": "If deltaP change, the deltaPmax is determined" - }, - { - "value": "emergencyStop", - "description": "Overrides all commands and stops the valveGroupControl immediately (safety scenarios)." - } - ], - "description": "Defines the possible actions that can be performed on the valveGroupControl." - } - }, - "sequences":{ - "default":{}, - "rules": { - "type": "object", - "schema": { - "startup": { - "default": ["starting","warmingup","operational"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sequence of states for starting up the valve." - } - }, - "shutdown": { - "default": ["stopping","coolingdown","idle"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sequence of states for shutting down the valveGroupControl." - } - }, - "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 the valveGroupControl." - } - } - } - }, - "description": "Predefined sequences of states for the valveGroupControl." - - }, - "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." - } - } - } diff --git a/package.json b/package.json index e843712..4caaf9a 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,11 @@ "author": "Janneke Tack / Rene De Ren", "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": { - "valveGroupControl": "valveGroupControl.js" + "valveGroupControl": "vgc.js" } } } diff --git a/src/nodeClass.js b/src/nodeClass.js new file mode 100644 index 0000000..c7e730a --- /dev/null +++ b/src/nodeClass.js @@ -0,0 +1,218 @@ +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 is the Node-RED node instance, we can use this to send messages and update status + this.RED = RED; // This is the Node-RED runtime API, we can use this to create endpoints if needed + this.name = nameOfNode; // This is the name of the node, it should match the file name and the node type in Node-RED + this.source = null; // Will hold the specific class instance + + // Load default & UI config + this._loadConfig(uiConfig, this.node); + + // Instantiate core Measurement class + this._setupSpecificClass(); + + // 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) { + const cfgMgr = new configManager(); + this.defaultConfig = cfgMgr.getConfig(this.name); + + // 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, + }, + }, + functionality: { + positionVsParent: uiConfig.positionVsParent || "atEquipment", // Default to 'atEquipment' if not set + }, + }; + // Utility for formatting outputs + this._output = new outputUtils(); + } + + _updateNodeStatus() { + const vg = this.source; + const mode = vg.mode; + const scaling = vg.scaling; + const totalFlow = + Math.round( + vg.measurements + .type("flow") + .variant("measured") + .position("downstream") + .getCurrentValue() * 1 + ) / 1; + + // Calculate total capacity based on available valves + const availableValves = Object.values(vg.valves).filter((valve) => { + const state = valve.state.getCurrentState(); + const mode = valve.currentMode; + return !( + state === "off" || + state === "maintenance" || + mode === "maintenance" + ); + }); + + // const totalCapacity = Math.round(vg.dynamicTotals.flow.max * 1) / 1; ADD LATER? + + // Determine overall status based on available valves + const status = + availableValves.length > 0 + ? `${availableValves.length} valve(s) connected` + : "No valves"; + + + // Generate status text in a single line + const text = ` ${mode} | 💨=${totalFlow} | ${status}`; + + return { + fill: availableValves.length > 0 ? "green" : "red", + shape: "dot", + text, + }; + } + + /** + * Instantiate the core logic and store as source. + */ + _setupSpecificClass() { + this.source = new Specific(this.config); + this.node.source = this.source; // Store the source in the node instance for easy access + } + + /** + * Bind events to Node-RED status updates. Using internal emitter. --> REMOVE LATER WE NEED ONLY COMPLETE CHILDS AND THEN CHECK FOR UPDATES + */ + _bindEvents() { + + } + + /** + * 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 to drive the Measurement class. + */ + _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() { + 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", + async (msg, send, done) => { + const vg = this.source; + const RED = this.RED; + switch (msg.topic) { + case "registerChild": + //console.log(`Registering child in mgc: ${msg.payload}`); + const childId = msg.payload; + const childObj = RED.nodes.getNode(childId); + vg.childRegistrationUtils.registerChild( + childObj.source, + msg.positionVsParent + ); + break; + + case 'setMode': + vg.setMode(msg.payload); + break; + case 'execSequence': + const { source: seqSource, action: seqAction, parameter } = msg.payload; + vg.handleInput(seqSource, seqAction, parameter); + break; + + case 'totalFlowChange': // een van valves is van stand veranderd waardoor total flow is veranderd + const { source: tfcSource, action: tfcAction, q} = msg.payload; + vg.handleInput(tfcSource, tfcAction, Number(q)); + break; + + default: + // Handle unknown topics if needed + vg.logger.warn(`Unknown topic: ${msg.topic}`); + break; + } + 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; // Export the class for Node-RED to use diff --git a/src/specificClass.js b/src/specificClass.js new file mode 100644 index 0000000..e1a1c62 --- /dev/null +++ b/src/specificClass.js @@ -0,0 +1,333 @@ +/** + * @file valveGroupControl.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 valveGroupControl.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 +.... +*/ + +//load local dependencies +const EventEmitter = require('events'); +const {loadCurve,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils} = require('generalFunctions'); + +class ValveGroupControl { + constructor(valveGroupControlConfig = {}) { + this.emitter = new EventEmitter(); // nodig voor ontvangen en uitvoeren van events emit() en on() --> Zien als internet berichten (niet bedraad in node-red) + this.configManager = new configManager(); + this.defaultConfig = this.configManager.getConfig('valveGroupControl'); // Load default config for rotating machine ( use software type name ? ) + this.configUtils = new configUtils(this.defaultConfig); + this.config = this.configUtils.initConfig(valveGroupControlConfig); // verify and set the config for the valve group + + // Init after config is set + this.logger = new logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, this.config.general.name); + + // Initialize measurements + this.measurements = new MeasurementContainer(); + this.child = {}; + this.valves = {}; // hold child object so we can get information from its child valves + + // Initialize variables + this.maxDeltaP = 0; // max deltaP is 0 als er geen child valves zijn + this.currentMode = this.config.mode.current; + this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility + + } + + isValidSourceForMode(source, mode) { + const allowedSourcesSet = this.config.mode.allowedSources[mode] || []; + this.logger.info(`Allowed sources for mode '${mode}': ${allowedSourcesSet}`); + return allowedSourcesSet.has(source); + } + + 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.info(`Handling input from source '${source}' with action '${action}' in mode '${this.currentMode}'.`); + try { + switch (action) { + case "execSequence": + await this.executeSequence(parameter); + break; + case "totalFlowChange": + await this.updateFlow(parameter); + break; + case "emergencyStop": + this.logger.warn(`Emergency stop activated by '${source}'.`); + await this.executeSequence("emergencyStop"); + break; + case "statusCheck": + this.logger.info(`Status Check: Mode = '${this.currentMode}', Source = '${source}'.`); + break; + default: + this.logger.warn(`Action '${action}' is not implemented.`); + break; + } + this.logger.debug(`Action '${action}' successfully executed`); + return {status : true , feedback: `Action '${action}' successfully executed.`}; + } catch (error) { + this.logger.error(`Error handling input: ${error}`); + } + + } + + setMode(newMode) { + const availableModes = defaultConfig.mode.current.rules.values.map(vgc => vgc.value); + if (!availableModes.includes(newMode)) { + this.logger.warn(`Invalid mode '${newMode}'. Allowed modes are: ${availableModes.join(', ')}`); + return; + } + + this.currentMode = newMode; + this.logger.info(`Mode successfully changed to '${newMode}'.`); + } + + + + // -------- Sequence Handlers -------- // + async executeSequence(sequenceName) { + + const sequence = this.config.sequences[sequenceName]; + + if (!sequence || sequence.size === 0) { + this.logger.warn(`Sequence '${sequenceName}' not defined.`); + return; + } + + this.logger.info(` --------- Executing sequence: ${sequenceName} -------------`); + + for (const state of sequence) { + try { + await this.state.transitionToState(state); + // Update measurements after state change + + } catch (error) { + this.logger.error(`Error during sequence '${sequenceName}': ${error}`); + break; // Exit sequence execution on error + } + } + } + +updateFlow(variant,value,position) { + + switch (variant) { + case ("measured"): + // put value in measurements container + this.logger.debug(`Updating measured flow for position ${position} with value ${value}`); + this.measurements.type("flow").variant("measured").position(position).value(value); + this.calcValveFlows(); + break; + + case ("predicted"): + this.logger.debug(`Updating predicted flow for position ${position} with value ${value}`); + this.measurements.type("flow").variant("predicted").position(position).value(value); + this.calcValveFlows(); // Pass the value to calculate valve flows + break; + + default: + this.logger.warn(`Unrecognized variant '${variant}' for flow update.`); + break; + } + } + + 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; + } + } + + calcValveFlows() { + const totalFlow = this.measurements.type("flow").variant("measured").position("atEquipment").getCurrentValue(); // get the total flow from the measurement container + let totalKv = 0; + + this.logger.debug(`Calculating valve flows... ${totalFlow}`); //Checkpoint + + for (const key in this.valves){ //bereken sum kv values om verdeling total flow te maken + this.logger.info('kv: ' + this.valves[key].kv); //CHECKPOINT + if (this.valves[key].state.getCurrentPosition() != null) { + totalKv += this.valves[key].kv; + this.logger.info('Total Kv = ' + totalKv); //CHECKPOINT + } + if(totalKv === 0) { + this.logger.warn('Total Kv is 0, cannot calculate flow distribution.'); + return; // Avoid division by zero + } + } + + for (const key in this.valves){ + const valve = this.valves[key]; + this.logger.debug(`Calculating ratio for valve total: ${totalKv} valve.kv: ${valve.kv} ratio : ${valve.kv / totalKv}`); //Checkpoint + const ratio = valve.kv / totalKv; + const flow = ratio * totalFlow; // bereken flow per valve + + //update flow per valve in de object zelf wat daar vervolgens weer de nieuwe deltaP berekent + valve.updateFlow("predicted", flow, "downstream"); + this.logger.info(`--> Sending updated flow to valves --> ${flow} `); //Checkpoint + + } + } + + calcMaxDeltaP() { // bereken de max deltaP van alle child valves + let maxDeltaP = 0; //max deltaP is 0 als er geen child valves zijn + this.logger.info('Calculating new max deltaP...'); + for (const key in this.valves) { + const valve = this.valves[key]; //haal de child valve object op + const deltaP = valve.measurements.type("pressure").variant("predicted").position("delta").getCurrentValue(); //get delta P + this.logger.info(`Delta P for valve ${key}: ${deltaP}`); + if (deltaP > maxDeltaP) { //als de deltaP van de child valve groter is dan de huidige maxDeltaP, dan update deze + maxDeltaP = deltaP; + } + } + this.logger.info('Max Delta P updated to: ' + maxDeltaP); + + this.maxDeltaP = maxDeltaP; //update de max deltaP in de measurement container van de valveGroupControl class + +} + + + getOutput() { + + // Improved output object generation + const output = {}; + //build the output object + this.measurements.getTypes().forEach(type => { + this.measurements.getVariants().forEach(variant => { + this.measurements.getPositions().forEach(position => { + + const value = this.measurements.type(type).variant(variant).position(position).getCurrentValue(); //get the current value of the measurement + + if (value != null) { + output[`${position}_${variant}_${type}`] = value; + } + }); + }); + }); + + //fill in the rest of the output object + output["mode"] = this.currentMode; + output["maxDeltaP"] = this.maxDeltaP; + + //this.logger.debug(`Output: ${JSON.stringify(output)}`); + + return output; + } + +} + +module.exports = ValveGroupControl; + + +const valve = require('../../valve/src/specificClass.js'); +const valveConfig = { + general: { + name: "valve", + logging: { + enabled: true, + logLevel: "debug" + } + }, + asset: { + supplier: "binder", + category: "valve", + type: "control", + model: "ECDV", + unit: "m3/h" + }, + functionality: { + positionVsParent: 'atEquipment', // Default to 'atEquipment' if not specified + } + }; + +const stateConfig = { + general: { + logging: { + enabled: true, + logLevel: "debug" + } + }, + movement: { + speed: 1 + }, + time: { + starting: 1, + warmingup: 1, + stopping: 1, + coolingdown: 1 + } + }; + +const valve1 = new valve(valveConfig, stateConfig); +//const valve2 = new valve(valveConfig, stateConfig); +//const valve3 = new valve(valveConfig, stateConfig); + +valve1.kv = 10; // Set Kv value for valve1 +//valve2.kv = 20; // Set Kv value for valve2 +//valve3.kv = 30; // Set Kv value for valve3 + +valve1.updateMeasurement("measured", "pressure" , 500, "downstream"); +//valve2.updateMeasurement("measured" , "pressure" , 500, "downstream"); +//valve3.updateMeasurement("measured" , "pressure" , 500, "downstream"); + +const vgc = new ValveGroupControl(); + +vgc.childRegistrationUtils.registerChild(valve1, "atEquipment"); +//vgc.childRegistrationUtils.registerChild(valve2, "atEquipment"); +//vgc.childRegistrationUtils.registerChild(valve3, "atEquipment"); + +vgc.updateFlow("measured", 1000, "atEquipment"); // Update total flow to 100 m3/h + diff --git a/valveGroupControl.html b/valveGroupControl.html deleted file mode 100644 index 459685a..0000000 --- a/valveGroupControl.html +++ /dev/null @@ -1,286 +0,0 @@ - - - - - - - - - diff --git a/valveGroupControl.js b/valveGroupControl.js deleted file mode 100644 index 1d903a9..0000000 --- a/valveGroupControl.js +++ /dev/null @@ -1,247 +0,0 @@ - -module.exports = function (RED) { // Export function zodat deze door Node-RED kan worden gebruikt - function valveGroupControl(config) { // Functie die wordt aangeroepen wanneer de node wordt aangemaakt - changed to valveGroupControl - RED.nodes.createNode(this, config); - var node = this; - - try { - // Load valve class (and curve data - not used yet) - const valveGroupControl = require("./dependencies/valveGroupControlClass"); // Importeer de valveGroupControl class - const OutputUtils = require("../generalFunctions/helper/outputUtils"); // Importeer de OutputUtils class - - const valveGroupControlConfig = { // Configuratie van de valveGroupControl - general: { - name: config.name || "Default ValveGroupControl ", - id: node.id, - logging: { - enabled: config.eneableLog, - logLevel: config.logLevel - } - }, - /* NOT USED - asset: { - supplier: config.supplier || "Unknown", - - 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 vgc = new valveGroupControl(valveGroupControlConfig, stateConfig); - - // put m on node memory as source - node.source = vgc; - - //load output utils - const output = new OutputUtils(); - - //Hier worden node-red statussen en metingen geupdate - function updateNodeStatus() { - try { - const mode = vgc.currentMode; // modus is bijv. auto, manual, etc. //QUESTION: altijd auto dus mag er denk ander in - const state = vgc.state.getCurrentState(); //is bijv. operational, idle, off, etc. //QUESTION: altijd operational dus mag er denk anders in - let maxDeltaP = vgc.maxDeltaP; // maximum delta P over child kleppen - if (maxDeltaP !== null) { - maxDeltaP = parseFloat(maxDeltaP.toFixed(0));} //afronden op 4 decimalen indien geen "null" - let totalFlow = vgc.measurements.type("totalFlow").variant("predicted").position("upstream").getCurrentValue(); // totale flow van de kleppen - let symbolState; - if (maxDeltaP === NaN) { - maxDeltaP = "∞"; - } - 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} | ΔPmax${maxDeltaP} mbar | 💨${totalFlow} m3/h`}; - break; - case "starting": - status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; - break; - case "warmingup": - status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ΔPmax${maxDeltaP} mbar | 💨${totalFlow} m3/h`}; - break; - case "accelerating": - status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ΔPmax${maxDeltaP} mbar | 💨${totalFlow} m3/h`}; - 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} | ΔPmax${maxDeltaP} mbar | 💨${totalFlow} m3/h`}; - 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 klok. Is tijd based en niet event based - try { - const status = updateNodeStatus(); - node.status(status); - - //vgc.tick(); - - //get output - const classOutput = vgc.getOutput(); - const dbOutput = output.formatMsg(classOutput, vgc.config, "influxdb"); - const pOutput = output.formatMsg(classOutput, vgc.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 - try { - let result; - switch(msg.topic) { - case 'registerChild': - vgc.logger.info(`Registering child started}`); //CHECKPOINT - const childId = msg.payload; - const childObj = RED.nodes.getNode(childId); - vgc.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent); - break; - case 'setMode': - vgc.setMode(msg.payload); - break; - case 'execSequence': - const { source: seqSource, action: seqAction, parameter } = msg.payload; - vgc.handleInput(seqSource, seqAction, parameter); - break; - - case 'totalFlowChange': // als pomp harder gaat pompen dan veranderd de totale flow --> dan moet de nieuwe flow per valve berekend worden - const { source: tfcSource, action: tfcAction, q} = msg.payload; - vgc.handleInput(tfcSource, tfcAction, Number(q)); - break; - - case 'emergencystop': - const { source: esSource, action: esAction } = msg.payload; - vgc.handleInput(esSource, esAction); - break; - - } - - 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"}); - } - } - RED.nodes.registerType("valveGroupControl", valveGroupControl); -}; - - - diff --git a/vgc.html b/vgc.html new file mode 100644 index 0000000..83ae1af --- /dev/null +++ b/vgc.html @@ -0,0 +1,86 @@ + + + + + + + + + diff --git a/vgc.js b/vgc.js new file mode 100644 index 0000000..01ee8bd --- /dev/null +++ b/vgc.js @@ -0,0 +1,39 @@ +const nameOfNode = 'valveGroupControl'; // 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); + // 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 node + RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => { + try { + const script = menuMgr.createEndpoint(nameOfNode, ['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}`); + } + }); +}; \ No newline at end of file