From 9b1af8ffa2f4f0e4da376c5b9da3e309e0b8dd8d Mon Sep 17 00:00:00 2001
From: znetsixe <73483679+znetsixe@users.noreply.github.com>
Date: Wed, 14 May 2025 10:06:08 +0200
Subject: [PATCH] first commit
---
LICENSE | 12 +-
dependencies/valveClass.js | 290 ++++++++++++++++++++++++++
dependencies/valveConfig.json | 381 ++++++++++++++++++++++++++++++++++
package.json | 28 +++
valve.html | 275 ++++++++++++++++++++++++
valve.js | 250 ++++++++++++++++++++++
6 files changed, 1233 insertions(+), 3 deletions(-)
create mode 100644 dependencies/valveClass.js
create mode 100644 dependencies/valveConfig.json
create mode 100644 package.json
create mode 100644 valve.html
create mode 100644 valve.js
diff --git a/LICENSE b/LICENSE
index 1fa1b39..da9b1dd 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/valveClass.js b/dependencies/valveClass.js
new file mode 100644
index 0000000..725424b
--- /dev/null
+++ b/dependencies/valveClass.js
@@ -0,0 +1,290 @@
+/**
+ * @file valveClass.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 Predict = require('../../predict/dependencies/predict/predict_class');
+const { MeasurementContainer } = require('../../generalFunctions/helper/measurements/index');
+
+//load all config modules #NOTE: Vul hier nog de juiste dependencies in als meer nodig of sommige niet nodig
+const defaultConfig = require('./valveConfig.json');
+const ConfigUtils = require('../../generalFunctions/helper/configUtils');
+
+//load registration utility #NOTE: Vul hier nog de juiste dependencies in als meer nodig of sommige niet nodig
+const ChildRegistrationUtils = require('../../generalFunctions/helper/childRegistrationUtils');
+
+class Valve {
+ constructor(valveConfig = {}, stateConfig = {}) {
+ this.emitter = new EventEmitter(); // nodig voor ontvangen en uitvoeren van events emit() en on() --> Zien als internet berichten (niet bedraad in node-red)
+ this.configUtils = new ConfigUtils(defaultConfig); // nodig voor het ophalen van de default configuaratie
+ this.config = this.configUtils.initConfig(valveConfig); //valve configurations die bij invoer in node-red worden gegeven
+
+ // Initialize measurements
+ this.measurements = new MeasurementContainer();
+ this.child = {}; // object to hold child information so we know on what to subscribe
+
+ // Init after config is set
+ this.logger = new Logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, this.config.general.name);
+ this.state = new State(stateConfig, this.logger); // Init State manager and pass logger
+ this.state.stateManager.currentState = "operational"; // Set default state to operational
+
+ this.kv = 0; //default
+ this.rho = 1,225 //dichtheid van lucht standaard
+ this.T = 293; // temperatuur in K standaard
+ this.downstreamP = 0.54 //hardcodes for now --> assumed to be constant watercolumn and deltaP diffuser
+ this.currentMode = this.config.mode.current;
+
+ // wanneer hij deze ontvangt is de positie van de klep verandererd en gaat hij de updateposition functie aanroepen wat dan alle metingen en standen gaat updaten
+ this.state.emitter.on("positionChange", (data) => {
+ this.logger.debug(`Position change detected: ${data}`);
+ this.updatePosition()}); //To update deltaP
+
+
+ this.childRegistrationUtils = new ChildRegistrationUtils(this); // Child registration utility
+
+ //replace v_curve loadspecs with config file afterwards !!!!!!!!!!
+ this.vCurve = this.loadSpecs().v_curve
+ this.predictKv = new Predict({curve:this.vCurve}); // load valve size (x : ctrl , y : kv relationship)
+ }
+
+ // -------- Config -------- //
+ updateConfig(newConfig) {
+ this.config = this.configUtils.updateConfig(this.config, newConfig);
+ }
+
+ isValidSourceForMode(source, mode) {
+ const allowedSourcesSet = this.config.mode.allowedSources[mode] || [];
+ 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 "execMovement": // past het setpoint aan - movement van klep stand
+ await this.setpoint(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(v => v.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}'.`);
+ }
+
+ loadSpecs(){ //static betekend dat die in andere classes kan worden aangeroepen met const specs = Valve.loadSpecs()
+ //lateron based on valve caracteristics --> then it searches for right valve
+ let specs = {
+ supplier : "Binder",
+ type : "HDCV",
+ units:{
+ Nm3: { "temp": 20, "pressure" : 1.01325 , "RH" : 0 }, // according to DIN
+ v_curve : { x : "% stroke", y : "Kv value"} ,
+ },
+ v_curve: {
+ 125: // valve size
+ {
+ x:[0,10,20,30,40,50,60,70,80,90,100], //stroke in %
+ y:[0,18,50,95,150,216,337,564,882,1398,1870], //Kv value expressed in m3/h
+ },
+ 150: // valve size
+ {
+ x:[0,10,20,30,40,50,60,70,80,90,100], //stroke in %
+ y:[0,25,73,138,217,314,490,818,1281,2029,2715], //oxygen transfer rate expressed in gram o2 / normal m3/h / per m
+ },
+ 400: // valve size
+ {
+ x:[0,10,20,30,40,50,60,70,80,90,100], //stroke in %
+ y:[0,155,443,839,1322,1911,2982,4980,7795,12349,16524], //oxygen transfer rate expressed in gram o2 / normal m3/h / per m
+ },
+ }
+ }
+
+ return specs;
+ }
+
+ // -------- Sequence Handlers -------- //
+ async executeSequence(sequenceName) {
+
+ const sequence = this.config.sequences[sequenceName];
+
+ if (!sequence || sequence.size === 0) {
+ this.logger.warn(`Sequence '${sequenceName}' not defined.`);
+ return;
+ }
+
+ if (this.state.getCurrentState() == "operational" && sequenceName == "shutdown") {
+ this.logger.info(`Machine will ramp down to position 0 before performing ${sequenceName} sequence`);
+ await this.setpoint(0);
+ }
+
+ 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
+ }
+ }
+ }
+
+ async setpoint(setpoint) {
+
+ try {
+ // Validate setpoint
+ if (typeof setpoint !== 'number' || setpoint < 0) {
+ throw new Error("Invalid setpoint: Setpoint must be a non-negative number.");
+ }
+
+ // Move to the desired setpoint
+ await this.state.moveTo(setpoint);
+
+ } catch (error) {
+ console.error(`Error setting setpoint: ${error}`);
+ }
+ }
+
+
+
+ // NOTE: Omdat met zeer kleine getallen wordt gewerkt en er kwadraten in de formule zitten kan het zijn dat we alles *1000 moeten doen
+ updateDeltaPKlep(q,kv,downstreamP,rho,temp){
+ //q must be in Nm3/h
+ //temp must be in K
+ //q must be in m3/h
+
+ //downstreamP must be in bar so transfer from mbar to bar
+ downstreamP = downstreamP / 1000;
+ //convert downstreamP to absolute bar
+ downstreamP += 1.01325;
+
+ //calculate deltaP
+ let deltaP = ( q**2 * rho * temp ) / ( 514**2 * kv**2 * downstreamP);
+
+ //convert deltaP to mbar
+ deltaP = deltaP * 1000;
+
+ // Synchroniseer deltaP met het Valve-object
+ this.deltaPKlep = deltaP
+
+ // Opslaan in measurement container
+ this.measurements.type("pressure").variant("predicted").position("delta").value(deltaP);
+ this.logger.info('DeltaP updated to: ' + deltaP);
+
+ this.emitter.emit('deltaPChange', deltaP); // Emit event to notify valveGroupController of deltaP change
+ this.logger.info('DeltaPChange emitted to valveGroupController');
+ }
+
+
+ // Als er een nieuwe flow door de klep komt doordat de pompen harder zijn gaan pompen, dan update deze functie dit ook in de valve attributes en measurements
+ //NOTE: samenvoegen met updateFlow als header node er is
+ updateFlowKlep(q){
+ //q must be in Nm3/h
+ // Opslaan in measurement container van valve object
+ this.measurements.type("flow").variant("predicted").position("downstream").value(q);
+ this.logger.info('FlowKlep updated to: ' + q);
+ this.logger.info('Calculating new deltaP based on new flow');
+ this.updateDeltaPKlep(q,this.kv,this.downstreamP,this.rho,this.T); //update deltaP based on new flow
+ }
+
+ updatePosition() { //update alle parameters nadat er een verandering is geweest in stand van klep
+ if (this.state.getCurrentState() == "operational" || this.state.getCurrentState() == "accelerating" || this.state.getCurrentState() == "decelerating") {
+
+ this.logger.debug('Calculating new deltaP');
+ const currentPosition = this.state.getCurrentPosition();
+ const currentFlow = this.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); // haal de flow op uit de measurement containe
+ //const valveSize = 125; //NOTE: nu nog hardcoded maar moet een attribute van de valve worden
+ this.predictKv.fDimension = 125; //load valve size by defining fdimension in predict class
+ //const vCurve = this.loadSpecs().v_curve[valveSize]; // haal de curve op van de valve
+ //const Spline = require('cubic-spline'); // spline library -> nodig om kv waarde te benaderen op curve
+ const x = currentPosition; // dit is de positie van de klep waarvoor we delta P willen berekenen
+ const y = this.predictKv.y(x); // haal de waarde van kv op uit de spline
+
+ this.kv = y; //update de kv waarde in de valve class
+ if (this.kv < 0.1){
+ this.kv = 0.1; //minimum waarde voor kv
+ }
+ this.logger.debug(`Kv value for position valve ${x} is ${this.kv}`); // log de waarde van kv
+ this.updateDeltaPKlep(currentFlow,this.kv,this.downstreamP,this.rho,this.T); //update deltaP
+
+ }
+ }
+
+ 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["percentageOpen"] = this.state.getCurrentPosition();
+ output["moveTimeleft"] = this.state.getMoveTimeLeft();
+ output["mode"] = this.currentMode;
+
+ //this.logger.debug(`Output: ${JSON.stringify(output)}`);
+
+ return output;
+ }
+
+}
+
+module.exports = Valve;
+
+
+
diff --git a/dependencies/valveConfig.json b/dependencies/valveConfig.json
new file mode 100644
index 0000000..0093c3a
--- /dev/null
+++ b/dependencies/valveConfig.json
@@ -0,0 +1,381 @@
+{
+ "general": {
+ "name": {
+ "default": "Valve",
+ "rules": {
+ "type": "string",
+ "description": "A human-readable name or label for this valve configuration."
+ }
+ },
+ "id": {
+ "default": null,
+ "rules": {
+ "type": "string",
+ "nullable": true,
+ "description": "A unique identifier for this configuration. If not provided, defaults to null."
+ }
+ },
+ "unit": {
+ "default": "m3/h",
+ "rules": {
+ "type": "string",
+ "description": "The default measurement unit for this configuration (e.g., 'meters', 'seconds', 'unitless')."
+ }
+ },
+ "logging": {
+ "logLevel": {
+ "default": "info",
+ "rules": {
+ "type": "enum",
+ "values": [
+ {
+ "value": "debug",
+ "description": "Log messages are printed for debugging purposes."
+ },
+ {
+ "value": "info",
+ "description": "Informational messages are printed."
+ },
+ {
+ "value": "warn",
+ "description": "Warning messages are printed."
+ },
+ {
+ "value": "error",
+ "description": "Error messages are printed."
+ }
+ ]
+ }
+ },
+ "enabled": {
+ "default": true,
+ "rules": {
+ "type": "boolean",
+ "description": "Indicates whether logging is active. If true, log messages will be generated."
+ }
+ }
+ }
+ },
+ "functionality": {
+ "softwareType": {
+ "default": "valve",
+ "rules": {
+ "type": "string",
+ "description": "Specified software type for this configuration."
+ }
+ },
+ "role": {
+ "default": "valveController",
+ "rules": {
+ "type": "string",
+ "description": "Indicates the role this configuration plays within the system."
+ }
+ }
+ },
+ "asset": {
+ "uuid": {
+ "default": null,
+ "rules": {
+ "type": "string",
+ "nullable": true,
+ "description": "A universally unique identifier for this asset. May be null if not assigned."
+ }
+ },
+ "geoLocation": {
+ "default": {},
+ "rules": {
+ "type": "object",
+ "description": "An object representing the asset's physical coordinates or location.",
+ "schema": {
+ "x": {
+ "default": 0,
+ "rules": {
+ "type": "number",
+ "description": "X coordinate of the asset's location."
+ }
+ },
+ "y": {
+ "default": 0,
+ "rules": {
+ "type": "number",
+ "description": "Y coordinate of the asset's location."
+ }
+ },
+ "z": {
+ "default": 0,
+ "rules": {
+ "type": "number",
+ "description": "Z coordinate of the asset's location."
+ }
+ }
+ }
+ }
+ },
+ "supplier": {
+ "default": "Unknown",
+ "rules": {
+ "type": "string",
+ "description": "The supplier or manufacturer of the asset."
+ }
+ },
+ "type": {
+ "default": "valve",
+ "rules": {
+ "type": "string",
+ "description": "A general classification of the asset tied to the specific software. This is not chosen from the asset dropdown menu."
+ }
+ },
+ "subType": {
+ "default": "ecdv",
+ "rules": {
+ "type": "string",
+ "description": "A more specific classification within 'type'. For example, 'ecdv valve'."
+ }
+ },
+ "model": {
+ "default": "Unknown",
+ "rules": {
+ "type": "string",
+ "description": "A user-defined or manufacturer-defined model identifier for the asset."
+ }
+ },
+ "accuracy": {
+ "default": null,
+ "rules": {
+ "type": "number",
+ "nullable": true,
+ "description": "The accuracy of the valve or sensor, typically as a percentage or absolute value."
+ }
+ },
+ "valveCurve": {
+ "default": {
+ "nq": {
+ "1": {
+ "x": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5
+ ],
+ "y": [
+ 10,
+ 20,
+ 30,
+ 40,
+ 50
+ ]
+ }
+ },
+ "np": {
+ "1": {
+ "x": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5
+ ],
+ "y": [
+ 10,
+ 20,
+ 30,
+ 40,
+ 50
+ ]
+ }
+ }
+ },
+ "rules": {
+ "type": "valveCurve",
+ "description": "All valves curves must have a 'nq' and 'np' curve. nq stands for the flow curve, np stands for the power curve. Together they form the efficiency curve."
+ }
+ }
+ },
+ "mode": {
+ "current": {
+ "default": "auto",
+ "rules": {
+ "type": "enum",
+ "values": [
+ {
+ "value": "auto",
+ "description": "Accepts setpoints from a parent controller and runs autonomously."
+ },
+ {
+ "value": "virtualControl",
+ "description": "Controlled via GUI setpoints; ignores parent commands."
+ },
+ {
+ "value": "fysicalControl",
+ "description": "Controlled via physical buttons or switches; ignores external automated commands."
+ },
+ {
+ "value": "maintenance",
+ "description": "No active control from auto, virtual, or fysical sources."
+ }
+ ],
+ "description": "The operational mode."
+ }
+ },
+ "allowedActions":{
+ "default":{},
+ "rules": {
+ "type": "object",
+ "schema":{
+ "auto": {
+ "default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Actions allowed in auto mode."
+ }
+ },
+ "virtualControl": {
+ "default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Actions allowed in virtualControl mode."
+ }
+ },
+ "fysicalControl": {
+ "default": ["statusCheck", "emergencyStop"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Actions allowed in fysicalControl mode."
+ }
+ },
+ "maintenance": {
+ "default": ["statusCheck"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Actions allowed in maintenance mode."
+ }
+ }
+ },
+ "description": "Information about valid command sources recognized."
+ }
+ },
+ "allowedSources":{
+ "default": {},
+ "rules": {
+ "type": "object",
+ "schema":{
+ "auto": {
+ "default": ["parent", "GUI", "fysical"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sources allowed in auto mode."
+ }
+ },
+ "virtualControl": {
+ "default": ["GUI", "fysical"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sources allowed in virtualControl mode."
+ }
+ },
+ "fysicalControl": {
+ "default": ["fysical"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sources allowed in fysicalControl mode."
+ }
+ }
+ },
+ "description": "Information about valid command sources recognized."
+ }
+ }
+ },
+ "source": {
+ "default": "parent",
+ "rules": {
+ "type": "enum",
+ "values": [
+ {
+ "value": "parent",
+ "description": "Commands are received from a parent controller."
+ },
+ {
+ "value": "GUI",
+ "description": "Commands are received from a graphical user interface."
+ },
+ {
+ "value": "fysical",
+ "description": "Commands are received from physical buttons or switches."
+ }
+ ],
+ "description": "Information about valid command sources recognized."
+ }
+ },
+ "sequences":{
+ "default":{},
+ "rules": {
+ "type": "object",
+ "schema": {
+ "startup": {
+ "default": ["starting","warmingup","operational"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sequence of states for starting up."
+ }
+ },
+ "shutdown": {
+ "default": ["stopping","coolingdown","idle"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sequence of states for shutting down."
+ }
+ },
+ "emergencystop": {
+ "default": ["emergencystop","off"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sequence of states for an emergency stop."
+ }
+ },
+ "boot": {
+ "default": ["idle","starting","warmingup","operational"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sequence of states for booting up."
+ }
+ }
+ }
+ },
+ "description": "Predefined sequences of states."
+
+ },
+ "calculationMode": {
+ "default": "medium",
+ "rules": {
+ "type": "enum",
+ "values": [
+ {
+ "value": "low",
+ "description": "Calculations run at fixed intervals (time-based)."
+ },
+ {
+ "value": "medium",
+ "description": "Calculations run when new setpoints arrive or measured changes occur (event-driven)."
+ },
+ {
+ "value": "high",
+ "description": "Calculations run on all event-driven info, including every movement."
+ }
+ ],
+ "description": "The frequency at which calculations are performed."
+ }
+ }
+ }
+
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4b06ece
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "valve",
+ "version": "0.0.1",
+ "description": "Control module valve",
+ "main": "valve.js",
+ "scripts": {
+ "test": "node valve.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://gitea.centraal.wbd-rd.nl/RnD/valve.git"
+ },
+ "keywords": [
+ "valve",
+ "node-red"
+ ],
+ "author": "Rene De Ren / Janneke Tack",
+ "license": "SEE LICENSE",
+ "dependencies": {
+ "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git",
+ "convert": "git+https://gitea.centraal.wbd-rd.nl/RnD/convert.git"
+ },
+ "node-red": {
+ "nodes": {
+ "valve": "valve.js"
+ }
+ }
+}
diff --git a/valve.html b/valve.html
new file mode 100644
index 0000000..e2bd435
--- /dev/null
+++ b/valve.html
@@ -0,0 +1,275 @@
+
+
+
+
+
+
+
+
diff --git a/valve.js b/valve.js
new file mode 100644
index 0000000..a90f275
--- /dev/null
+++ b/valve.js
@@ -0,0 +1,250 @@
+module.exports = function (RED) {
+ function valve(config) {
+ //create node
+ RED.nodes.createNode(this, config);
+ //call this => node so whenver you want to call a node function type node and the function behind it
+ var node = this;
+
+ try {
+ const Valve = require("./dependencies/valveClass"); // Importeer de valve class
+ const OutputUtils = require("../generalFunctions/helper/outputUtils"); // Importeer de OutputUtils class
+
+ const valveConfig = { // Configuratie van de valve
+ general: {
+ name: config.name || "Default Valve",
+ id: node.id,
+ logging: {
+ enabled: config.eneableLog,
+ logLevel: config.logLevel
+ }
+ },
+ asset: {
+ supplier: config.supplier || "Unknown",
+ /* NOT USED
+ type: config.valveType || "generic",
+ subType: config.subType || "generic",
+ model: config.model || "generic",
+ valveCurve: config.valveCurve */
+ }
+ };
+
+ const stateConfig = { // Configuratie van de state
+ general: {
+ logging: {
+ enabled: config.eneableLog,
+ logLevel: config.logLevel
+ }
+ },
+
+ /* NOT USED
+ movement: {
+ speed: Number(config.speed)
+ },
+ time: {
+ starting: Number(config.startup),
+ warmingup: Number(config.warmup),
+ stopping: Number(config.shutdown),
+ coolingdown: Number(config.cooldown)
+ } */
+ };
+
+ // Create valve instance
+ const v = new Valve(valveConfig, stateConfig);
+
+ // put m on node memory as source
+ node.source = v;
+
+ //load output utils
+ const output = new OutputUtils();
+
+ //Hier worden node-red statussen en metingen geupdate
+ function updateNodeStatus() {
+ try {
+ const mode = v.currentMode; // modus is bijv. auto, manual, etc.
+ const state = v.state.getCurrentState(); //is bijv. operational, idle, off, etc.
+ const flow = Math.round(v.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue());
+ let deltaP = v.measurements.type("pressure").variant("predicted").position("delta").getCurrentValue();
+ if (deltaP !== null) {
+ deltaP = parseFloat(deltaP.toFixed(0));
+ } //afronden op 4 decimalen indien geen "null"
+ if(isNaN(deltaP)) {
+ deltaP = "∞";
+ }
+ const roundedPosition = Math.round(v.state.getCurrentPosition() * 100) / 100;
+ let symbolState;
+ switch(state){
+ case "off":
+ symbolState = "⬛";
+ break;
+ case "idle":
+ symbolState = "⏸️";
+ break;
+ case "operational":
+ symbolState = "⏵️";
+ break;
+ case "starting":
+ symbolState = "⏯️";
+ break;
+ case "warmingup":
+ symbolState = "🔄";
+ break;
+ case "accelerating":
+ symbolState = "⏩";
+ break;
+ case "stopping":
+ symbolState = "⏹️";
+ break;
+ case "coolingdown":
+ symbolState = "❄️";
+ break;
+ case "decelerating":
+ symbolState = "⏪";
+ break;
+ }
+
+
+ let status;
+ switch (state) {
+ case "off":
+ status = { fill: "red", shape: "dot", text: `${mode}: OFF` };
+ break;
+ case "idle":
+ status = { fill: "blue", shape: "dot", text: `${mode}: ${symbolState}` };
+ break;
+ case "operational":
+ status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ΔP${deltaP} mbar`}; //deltaP toegevoegd
+ break;
+ case "starting":
+ status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
+ break;
+ case "warmingup":
+ status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ΔP${deltaP} mbar`}; //deltaP toegevoegd
+ break;
+ case "accelerating":
+ status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ΔP${deltaP} mbar` }; //deltaP toegevoegd
+ break;
+ case "stopping":
+ status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
+ break;
+ case "coolingdown":
+ status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` };
+ break;
+ case "decelerating":
+ status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} - ${roundedPosition}% | 💨${flow}m³/h | ΔP${deltaP} mbar`}; //deltaP toegevoegd
+ break;
+ default:
+ status = { fill: "grey", shape: "dot", text: `${mode}: ${symbolState}` };
+ }
+ return status;
+ } catch (error) {
+ node.error("Error in updateNodeStatus: " + error.message);
+ return { fill: "red", shape: "ring", text: "Status Error" };
+ }
+ }
+
+ function tick() { // versturen van output messages --> tick van tick op de klop. Is tijd based en niet event based
+ try {
+ const status = updateNodeStatus();
+ node.status(status);
+
+ //v.tick();
+
+ //get output
+ const classOutput = v.getOutput();
+ const dbOutput = output.formatMsg(classOutput, v.config, "influxdb");
+ const pOutput = output.formatMsg(classOutput, v.config, "process");
+
+ //only send output on values that changed
+ let msgs = [];
+ msgs[0] = pOutput;
+ msgs[1] = dbOutput;
+
+ node.send(msgs);
+
+ } catch (error) {
+ node.error("Error in tick function: " + error);
+ node.status({ fill: "red", shape: "ring", text: "Tick Error" });
+ }
+ }
+
+ // register child on first output this timeout is needed because of node - red stuff
+ setTimeout(
+ () => {
+
+ /*---execute code on first start----*/
+ let msgs = [];
+
+ msgs[2] = { topic : "registerChild" , payload: node.id, positionVsParent: "upStream" };
+ msgs[3] = { topic : "registerChild" , payload: node.id, positionVsParent: "downStream" };
+
+ //send msg
+ node.send(msgs);
+ },
+ 100
+ );
+
+ //declare refresh interval internal node
+
+ setTimeout(
+ () => {
+ //---execute code on first start----
+ this.interval_id = setInterval(function(){ tick() },1000)
+ },
+ 1000
+ );
+
+ node.on("input", function(msg, send, done) { // Functie die wordt aangeroepen wanneer er een input wordt ontvangen
+ console.log("CKECK! Input received: ", msg.topic, msg.payload); // CHECKPOINT
+ try {
+ let result;
+ switch(msg.topic) {
+ case 'registerChild':
+ const childId = msg.payload;
+ const childObj = RED.nodes.getNode(childId);
+ v.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
+ break;
+ case 'setMode':
+ v.setMode(msg.payload);
+ break;
+ case 'execSequence':
+ const { source: seqSource, action: seqAction, parameter } = msg.payload;
+ v.handleInput(seqSource, seqAction, parameter);
+ break;
+ case 'execMovement':
+ const { source: mvSource, action: mvAction, setpoint } = msg.payload;
+ v.handleInput(mvSource, mvAction, Number(setpoint));
+ break;
+ case 'emergencystop':
+ const { source: esSource, action: esAction } = msg.payload;
+ v.handleInput(esSource, esAction);
+ break;
+ case 'showcurve':
+ v.showCurve();
+ send({ topic : "Showing curve" , payload: v.showCurve() });
+ break;
+ case 'newFlow': //Als nieuwe flow van header node dan moet deltaP weer opnieuw worden berekend en doorgegeven aan header node
+ const { source: nfSource, action: nfAction, parameter: nfParameter } = msg.payload; //parameter is new flow, action should be "calcNewDeltaP"
+ v.handleInput(nfSource, nfAction, nfParameter);
+ }
+
+ if (done) done();
+ } catch (error) {
+ node.error("Error processing input: " + error.message);
+ if (done) done(error);
+ }
+ });
+
+ node.on('close', function(done) { // Functie die wordt aangeroepen wanneer de node wordt gesloten
+ if (node.interval_id) clearTimeout(node.interval_id);
+ if (node.tick_interval) clearInterval(node.tick_interval);
+ if (done) done();
+ });
+
+ } catch (error) {
+ node.error("Fatal error in node initialization: " + error.stack);
+ node.status({fill: "red", shape: "ring", text: "Fatal Error"});
+ }
+
+ }
+ RED.nodes.registerType("valve", valve);
+};
\ No newline at end of file