Compare commits

...

1 Commits

Author SHA1 Message Date
znetsixe
abd1f41b44 converted j. Tack version to main stack v0.1 2025-07-31 09:08:25 +02:00
11 changed files with 749 additions and 1156 deletions

View File

@@ -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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to use, of this software and associated documentation files (the "Software"), to use,

View File

@@ -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 ## 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.

View File

@@ -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));
*/

View File

@@ -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."
}
}
}

View File

@@ -17,12 +17,11 @@
"author": "Janneke Tack / Rene De Ren", "author": "Janneke Tack / Rene De Ren",
"license": "SEE LICENSE", "license": "SEE LICENSE",
"dependencies": { "dependencies": {
"generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git", "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git"
"convert": "git+https://gitea.centraal.wbd-rd.nl/RnD/convert.git"
}, },
"node-red": { "node-red": {
"nodes": { "nodes": {
"valveGroupControl": "valveGroupControl.js" "valveGroupControl": "vgc.js"
} }
} }
} }

218
src/nodeClass.js Normal file
View File

@@ -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

333
src/specificClass.js Normal file
View File

@@ -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

View File

@@ -1,286 +0,0 @@
<!-- Deze file is nu aangepast voor de valveGroupControl node specifiek -->
<script type="module">
import * as menuUtils from "/generalFunctions/helper/menuUtils.js";
RED.nodes.registerType("valveGroupControl", {
category: "digital twin",
color: "#CC8400", //Color of node
defaults: {
// Define default properties
name: { value: "", required: true },
enableLog: { value: false },
logLevel: { value: "error" },
// Define specific properties
/* NOT USED
speed: { value: 1, required: true },
startup: { value: 0 },
warmup: { value: 0 },
shutdown: { value: 0 },
cooldown: { value: 0 },
*/
//define general asset properties
//supplier: { value: "" },
/*NOT USED
subType: { value: "" },
model: { value: "" },
unit: { value: "" },*/
//define specific asset properties
/*NOT USED
valveCurve : { value: {}},*/
},
//inputs en outputs voor node
inputs: 1,
outputs: 4,
inputLabels: ["ValveGroupControl Input"], //CHANGED
outputLabels: ["proces", "dbase", "upstream parent", "downstream parent"],
icon: "font-awesome/fa-sitemap", //CHANGED to other icon from Font Awesome 4
label: function () {
return this.name || "ValveGroupControl"; //CHANGED to ValveGrouControl
},
// Prepare for opening bewerkingsinterface van node in de Node-RED editor
oneditprepare: function () {
const node = this;
console.log("ValveGroupControl Node: Edit Prepare"); //CHANGED
const elements = {
// Basic fields
name: document.getElementById("node-input-name"),
// specific fields
/*NOT USED
speed: document.getElementById("node-input-speed"),
startup: document.getElementById("node-input-startup"),
warmup: document.getElementById("node-input-warmup"),
shutdown: document.getElementById("node-input-shutdown"),
cooldown: document.getElementById("node-input-cooldown"),*/
// Logging fields
logCheckbox: document.getElementById("node-input-enableLog"),
logLevelSelect: document.getElementById("node-input-logLevel"),
rowLogLevel: document.getElementById("row-logLevel"),
// Asset fields
/*NOT USED
supplier: document.getElementById("node-input-supplier"),
subType: document.getElementById("node-input-subType"),
model: document.getElementById("node-input-model"),
unit: document.getElementById("node-input-unit"), */
};
const projecSettingstURL = "http://localhost:1880/generalFunctions/settings/projectSettings.json"; //volgens mij hoeft deze niet aangepast?
try{
// Fetch project settings
menuUtils.fetchProjectData(projecSettingstURL)
.then((projectSettings) => {
//assign to node vars
node.configUrls = projectSettings.configUrls;
const { cloudConfigURL, localConfigURL } = menuUtils.getSpecificConfigUrl("valveGroupControl",node.configUrls.cloud.taggcodeAPI); //CHANGED to valve
node.configUrls.cloud.config = cloudConfigURL; // first call
node.configUrls.local.config = localConfigURL; // backup call
node.locationId = projectSettings.locationId;
node.uuid = projectSettings.uuid;
// Gets the ID of the active workspace (Flow)
const activeFlowId = RED.workspaces.active(); //fetches active flow id
node.processId = activeFlowId;
// UI elements
menuUtils.initBasicToggles(elements);
menuUtils.fetchAndPopulateDropdowns(node.configUrls, elements, node); // function for all assets
})
}catch(e){
console.log("Error fetching project settings", e);
}
if(node.d){
//this means node is disabled
console.log("Current status of node is disabled");
}
},
oneditsave: function () {
const node = this;
console.log(`------------ Saving changes to node ------------`);
console.log(`${node.uuid}`);
//save basic properties
["name"].forEach( //CHANGED to only save the properties we have now
(field) => {
const element = document.getElementById(`node-input-${field}`);
if (element) {
node[field] = element.value || "";
}
}
);
//save meta data curve
/*NOT USED yet
["valveCurve"].forEach(
(field) => {
node[field] = node["modelMetadata"][field];
console.log("Valve curve saved");
}
); */
const logLevelElement = document.getElementById("node-input-logLevel");
node.logLevel = logLevelElement ? logLevelElement.value || "info" : "info";
if (!node.unit) {
RED.notify("Unit selection is required.", "error");
}
if (node.subType && !node.unit) {
RED.notify("Unit must be set when specifying a subtype.", "error");
}
try{
// Fetch project settings
menuUtils.apiCall(node,node.configUrls)
.then((response) => {
//save response to node information
node.assetId = response.asset_id;
node.assetTagNumber = response.asset_tag_number;
})
.catch((error) => {
console.log("Error during API call", error);
});
}catch(e){
console.log("Error saving assetID and tagnumber", e);
}
},
});
</script>
<!-- Main UI Template -->
<script type="text/html" data-template-name="valveGroupControl">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input
type="text"
id="node-input-name"
placeholder="ValveGroupControl Name"
style="width:70%;"
/>
</div>
<!-- NOT USED
<div class="form-row">
<label for="node-input-startup"><i class="fa fa-clock-o"></i> Startup Time</label>
<input type="number" id="node-input-startup" placeholder="0" />
</div>
<div class="form-row">
<label for="node-input-warmup"><i class="fa fa-clock-o"></i> Warmup Time</label>
<input type="number" id="node-input-warmup" placeholder="0" />
</div>
<div class="form-row">
<label for="node-input-shutdown"><i class="fa fa-clock-o"></i> Shutdown Time</label>
<input type="number" id="node-input-shutdown" placeholder="0" />
</div>
<div class="form-row">
<label for="node-input-cooldown"><i class="fa fa-clock-o"></i> Cooldown Time</label>
<input type="number" id="node-input-cooldown" placeholder="0" />
</div>
-->
<!-- Optional Extended Fields: supplier, type, subType, model
<hr />
<div class="form-row">
<label for="node-input-supplier"
><i class="fa fa-industry"></i> Supplier</label
>
<select id="node-input-supplier" style="width:60%;">
<option value="">(optional)</option>
</select>
</div>
-->
<!-- NOT USED
<div class="form-row">
<label for="node-input-subType"
><i class="fa fa-puzzle-piece"></i> SubType</label
>
<select id="node-input-subType" style="width:60%;">
<option value="">(optional)</option>
</select>
</div>
<div class="form-row">
<label for="node-input-model"><i class="fa fa-wrench"></i> Model</label>
<select id="node-input-model" style="width:60%;">
<option value="">(optional)</option>
</select>
</div>
<div class="form-row">
<label for="node-input-unit"><i class="fa fa-balance-scale"></i> Unit</label>
<select id="node-input-unit" style="width:60%;"></select>
</div>
-->
<hr />
<!-- loglevel checkbox -->
<div class="form-row">
<label for="node-input-enableLog"
><i class="fa fa-cog"></i> Enable Log</label
>
<input
type="checkbox"
id="node-input-enableLog"
style="width:20px; vertical-align:baseline;"
/>
<span>Enable logging</span>
</div>
<div class="form-row" id="row-logLevel">
<label for="node-input-logLevel"><i class="fa fa-cog"></i> Log Level</label>
<select id="node-input-logLevel" style="width:60%;">
<option value="info">Info</option>
<option value="debug">Debug</option>
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
</div>
</script>
<!-- Help Text Template -->
<script type="text/html" data-help-name="valveGroupControl">
<p>
<b>ValveGrouControl Node</b>: Configure the behavior of a valveGroupControl
used in a digital twin.
</p>
<ul>
<!-- NOT USED
<li><b>Supplier:</b> Select a supplier to populate valve options.</li>
<li><b>SubType:</b> Select a subtype if applicable to further categorize the asset.</li>
<li><b>Model:</b> Define the specific model for more granular asset configuration.</li>
<li><b>Unit:</b> Assign a unit to standardize measurements or operations.</li>
<li><b>Speed:</b> Reaction speed of the machine in response to inputs.</li>
<li><b>Startup:</b> Define the startup time for the machine.</li>
<li><b>Warmup:</b> Define the warmup time for the machine.</li>
<li><b>Shutdown:</b> Define the shutdown time for the machine.</li>
<li><b>Cooldown:</b> Define the cooldown time for the machine.</li>
-->
<li><b>Enable Log:</b> Enable or disable logging for the valveGroupControl.</li>
<li><b>Log Level:</b> Set the log level (Info, Debug, Warn, Error).</li>
</ul>
</script>

View File

@@ -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);
};

86
vgc.html Normal file
View File

@@ -0,0 +1,86 @@
<!--
brabantse delta kleuren:
#eaf4f1
#86bbdd
#bad33b
#0c99d9
#a9daee
#0f52a5
#50a8d9
#cade63
#4f8582
#c4cce0
-->
<script src="/valveGroupControl/menu.js"></script> <!-- Load the menu script for dynamic dropdowns -->
<script src="/valveGroupControl/configData.js"></script> <!-- Load the config script for node information -->
<script>
RED.nodes.registerType('valveGroupControl',{
category: "EVOLV",
color: "#eaf4f1",
defaults: {
// Define default properties
name: { value: "" },
// Logger properties
enableLog: { value: false },
logLevel: { value: "error" },
// Physical aspect
positionVsParent: { value: "" },
positionLabel: { value: "" },
positionIcon: { value: "" },
},
inputs:1,
outputs:3,
inputLabels: ["Input"],
outputLabels: ["process", "dbase", "parent"],
icon: "font-awesome/fa-tachometer",
label: function () {
return this.positionIcon + " " + "valveGroupControl";
},
oneditprepare: function() {
// Initialize the menu data for the node
const waitForMenuData = () => {
if (window.EVOLV?.nodes?.valveGroupControl?.initEditor) {
window.EVOLV.nodes.valveGroupControl.initEditor(this);
} else {
setTimeout(waitForMenuData, 50);
}
};
// Wait for the menu data to be ready before initializing the editor
waitForMenuData();
},
oneditsave: function(){
const node = this;
// Validate logger properties using the logger menu
if (window.EVOLV?.nodes?.valveGroupControl?.loggerMenu?.saveEditor) {
success = window.EVOLV.nodes.valveGroupControl.loggerMenu.saveEditor(node);
}
// save position field
if (window.EVOLV?.nodes?.valveGroupControl?.positionMenu?.saveEditor) {
window.EVOLV.nodes.valveGroupControl.positionMenu.saveEditor(this);
}
}
});
</script>
<script type="text/html" data-template-name="valveGroupControl">
<!-- Logger fields injected here -->
<div id="logger-fields-placeholder"></div>
<!-- Position fields injected here -->
<div id="position-fields-placeholder"></div>
</script>
<script type="text/html" data-help-name="valveGroupControl">
<p>A valveGroupControl node</p>
</script>

39
vgc.js Normal file
View File

@@ -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}`);
}
});
};