diff --git a/LICENSE b/LICENSE
index 1fa1b39..2b247c4 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,9 +1,15 @@
MIT License
-Copyright (c) 2025 RnD
+Copyright (c) 2025 Janneke Tack, 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 deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to use,
+copy, modify, merge, publish, and distribute the Software for **personal, scientific, or educational purposes**, subject to the following conditions:
+
+**Commercial use of the Software or any derivative work is explicitly prohibited without prior written consent from the authors.**
+This includes but is not limited to resale, inclusion in paid products or services, and monetized distribution.
+Any commercial usage must be governed by a shared license or explicit contractual agreement with the authors.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-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.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED...
diff --git a/dependencies/valveGroupControlClass.js b/dependencies/valveGroupControlClass.js
new file mode 100644
index 0000000..b45d171
--- /dev/null
+++ b/dependencies/valveGroupControlClass.js
@@ -0,0 +1,245 @@
+//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
new file mode 100644
index 0000000..1fd26a2
--- /dev/null
+++ b/dependencies/valveGroupControlConfig.json
@@ -0,0 +1,371 @@
+{
+ "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
new file mode 100644
index 0000000..e843712
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "valveGroupControl",
+ "version": "0.0.1",
+ "description": "Valve group control module",
+ "main": "valveGroupControl.js",
+ "scripts": {
+ "test": "node valveGroupControl.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://gitea.centraal.wbd-rd.nl/RnD/valveGroupControl.git"
+ },
+ "keywords": [
+ "valveGroupControl",
+ "node-red"
+ ],
+ "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"
+ },
+ "node-red": {
+ "nodes": {
+ "valveGroupControl": "valveGroupControl.js"
+ }
+ }
+}
diff --git a/valveGroupControl.html b/valveGroupControl.html
new file mode 100644
index 0000000..459685a
--- /dev/null
+++ b/valveGroupControl.html
@@ -0,0 +1,286 @@
+
+
+
+
+
+
+
+
+
diff --git a/valveGroupControl.js b/valveGroupControl.js
new file mode 100644
index 0000000..c355b9b
--- /dev/null
+++ b/valveGroupControl.js
@@ -0,0 +1,250 @@
+//Deze file is nu aangepast voor de valveGroupControl node specifiek
+
+const { max } = require("mathjs");
+
+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);
+};
+
+
+