diff --git a/diffuser.html b/diffuser.html
index bfbb310..a5d9d39 100644
--- a/diffuser.html
+++ b/diffuser.html
@@ -1,89 +1,68 @@
-
-
-
-
-
-
+
+
+
+
+
diff --git a/diffuser.js b/diffuser.js
index bd2f31a..8674480 100644
--- a/diffuser.js
+++ b/diffuser.js
@@ -1,162 +1,9 @@
-module.exports = function (RED) {
- function diffuser(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;
-
- //fetch class
- var Diffuser = require("./dependencies/diffuser_class");
-
- //make new class on creation to work with.
- var diffuser = new Diffuser();
-
- //fetch name from node into the measurement code
- diffuser.name = config.name;
- diffuser.number = config.number;
- diffuser.id = diffuser.name+diffuser.number;
- diffuser.i_n_elements = config.i_elements;
- diffuser.i_diff_density = Number(config.i_diff_density);
- diffuser.i_m_water = config.i_m_water;
-
- // create internal vars
- this.interval_id = null;
- let internalTickRate = 1;
- function update_node_state() {
- //alarm has prio over warning so comes first in the if else statement
- if (diffuser.alarm.state == true) {
- //display status
- node.status({ fill: "red", shape: "dot", text: diffuser.alarm.text[0] });
- }
- else if(diffuser.warning.state == true ){
- //display status
- node.status({ fill: "yellow", shape: "dot", text: diffuser.warning.text[0] });
- }
- else if(diffuser.idle == true){
- node.status({ fill: "gray", shape: "dot", text: diffuser.o_kgo2_h + " Kg o2 / h"});
- }
- else{
- node.status({fill: "green", shape: "dot", text: diffuser.o_kgo2_h + " Kg o2 / h"});
- }
- }
-
- //update on creation
- update_node_state();
-
- function send_output(){
-
- //define empty msgs
- let msgs = [];
-
- let dynList = {
- iPressure: diffuser.i_pressure,
- iMWater: diffuser.i_m_water,
- iFlow: diffuser.i_flow,
- nFlow: diffuser.n_flow,
- oOtr: diffuser.o_otr,
- oPLoss: diffuser.o_p_total,
- oKgo2H: diffuser.o_kgo2_h,
- oFlowElement: diffuser.o_flow_element,
- efficiency: diffuser.o_combined_eff,
- //threshold: diffuser.threshold //EXPERIMENTAL!
- }
-
- let specList = {
- name: diffuser.name,
- number: diffuser.number,
- //supplier: diffuser.supplier,
- //type: diffuser.type,
- density: diffuser.i_diff_density,
- nElements: diffuser.i_n_elements,
- alfaF: diffuser.i_alfa_factor
- }
-
- msgs[0] = {topic: "kgo2/h" , payload: {kgo2h: diffuser.o_kgo2_h , tot_p_loss : diffuser.o_p_total} ,id: diffuser.id };
- msgs[1] = {topic: "object", payload : diffuser};
- msgs[2] = {
- topic:"outputdbase",
- payload:[
-
- {
- measurement: diffuser.name+diffuser.number,
- fields : dynList,
- tags: {
- group: "values",
- },
- timestamp: new Date()
- },
- {
- measurement: diffuser.name+diffuser.number,
- fields : specList,
- tags: {
- group: "specs",
- },
- timestamp: new Date()
- },
- ]
- } // output to broker
-
- //send outputs
- node.send(msgs);
- }
-
- //never ending functions
- function tick(){
- update_node_state();
- send_output();
-
- }
-
- // register child on first output this timeout is needed because of node - red stuff
- setTimeout(
- () => {
-
- /*---execute code on first start----*/
- let msgs = [];
- msgs[3] = { topic : "registerChild" , payload: diffuser };
- //send msg
- this.send(msgs);
- },
- 100
- );
-
- //declare refresh interval internal node
- setTimeout(
- () => {
- /*---execute code on first start----*/
- this.interval_id = setInterval(function(){ tick() },(internalTickRate * 1000))
- },
- 1000
- );
-
-
- //-------------------------------------------------------------------->>what to do on input
- node.on("input", function (msg,send,done) {
-
- //change density of diffusers
- if(msg.topic == "density"){
- diffuser.i_diff_density = Number(msg.payload);
- }
-
- //change input flow
- if(msg.topic == "air_flow"){
- diffuser.i_flow = Number(msg.payload);
- }
-
- //change water height
- if(msg.topic == "height_water"){
- diffuser.i_m_water = Number(msg.payload);
- }
-
- done();
-
- });
-
- // tidy up any async code here - shutdown connections and so on.
- node.on('close', function() {
- clearTimeout(this.interval_id);
- });
- }
- RED.nodes.registerType("diffuser", diffuser);
-};
\ No newline at end of file
+const nameOfNode = 'diffuser';
+const nodeClass = require('./src/nodeClass.js');
+
+module.exports = function(RED) {
+ RED.nodes.registerType(nameOfNode, function(config) {
+ RED.nodes.createNode(this, config);
+ this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
+ });
+};
diff --git a/diffuser_class.js b/diffuser_class.js
index f240940..46d375f 100644
--- a/diffuser_class.js
+++ b/diffuser_class.js
@@ -1,589 +1 @@
-/*
-Copyright:
-Year : (c) 2023
-Author : Rene De Ren
-Contact details : zn375ix3@gmail.com
-Location : The Netherlands
-
-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:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-The author shall be notified of any and all improvements or adaptations this 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,
-OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-*/
-
-//Diffised Aeration Devices (example fine bubble aeration)
-
-class Diffuser{
-
- /*------------------- Construct and set vars -------------------*/
- constructor(supplier,type) {
-
- this.init = false;
-
- /* --------------------load depenencies ------------- */
- this.Interpolation = require('../../predict/dependencies/interpolation') ; //load class
- this.interpolation = new this.Interpolation; //general use of interpolation object
-
- this.Fysics = require('./../../convert/dependencies/fysics') ; //load class
- this.fysics = new this.Fysics; //general use of fysics
-
- this.Graph = require('./graph') ; //load class
- this.graph = new this.Graph; //general use of fysics
-
- //load default pressure curve depending on type
- this.specs = this.load_specs();
-
- //after loading specs load curve builder
- this.Predict = require('../../predict/dependencies/predict_class') ; //load class
- this.predict_otr = new this.Predict(); //load otr curve
- this.predict_otr.i_curve = this.specs.otr_curve;
-
- this.predict_p = new this.Predict();
- this.predict_p.i_curve = this.specs.p_curve; // load pressure curve
- this.predict_p.i_f = 0; // set f to dim 0 because there is no other dim for pressure
-
- this.predictO2saturation = new this.Predict();
- this.predictO2saturation.i_curve = this.fysics.o2Solubility; // load solubility curve for o2 in water
-
-
- //load converter ./dependencies/index
- this.convert = require('./../../convert/dependencies/index');
-
- /* ------------ static vars ---------------*/
- this.name = ""; // user defined name
- this.number = 0 ; // user defined number
- this.id = ""; // unique id from node red?
- this.idle = true; // is this idle (not outputting kg o2 / h)
- this.desc = "diffuser"; // description of the current object
- this.supplier = supplier; // supplier of diffuser
- this.type = type; // type of diffuser
-
- /* ---------- Inputs -------------- */
- this.i_flow = 0 ; // input actual flow rate expressed in Nm3/h
- this.i_diff_density = 5; // intput actual density of diffusers
- this.i_pressure = 0 ; // this is pressure in header expressed in mbar to calculated air density.
- this.i_local_atm_pressure = 1013,25 ; // local atm pressure in mbar
- this.i_alfa_factor = 0.7 ; // stnd alfa factor
- this.i_water_density = 997 ; // this.water_molar_mass * this.num_moles_water // water density in kg/m3;
- this.i_m_water = 0; // input actual height in meter water ABOVE the diffuser
- this.i_n_elements = 1; // input for amount of diffusers we need to divide the flow over to get the nm per element
-
- /*---------calculated parameters ----------*/
- this.n_flow = 0 ; // converted input to normalized conditions of the diffusers flow parameter (x)
- this.n_kg = this.fysics.calc_air_dens(this.specs.units.Nm3.pressure,this.specs.units.Nm3.RH,this.specs.units.Nm3.temp); // calculated normalized kg for air density depending on specs kg /m3
-
- /* ---------- Outputs -------------- */
- this.o_otr = 0; // predicted oxygen transfer rate
- this.o_p_flow = 0; // predicted pressure loss over flow rate
- this.o_p_water = 0; // predicted pressure loss over meter water above diffuser
- this.o_p_total = 0; // predicted total pressure loss
- this.o_kg = 0; // predicted kg of air
- this.o_kg_h = 0; // predicted kg per hour
- this.o_kgo2_h = 0; // predicted oxygen transfer per hour
- this.o_kgo2_h_min = 0; // predicted min kgo2 / hour for zone controller
- this.o_kgo2_h_max = 0; // predicted max kgo2 / hour for zone controller
- this.o_kgo2 = 0 ; // current oxygen input
- this.o_flow_element = 0 ; // flow per element
- this.o_otr_max = 0; // store max otr for easy access
- this.o_otr_min = 0; // store min otr for easy access
- this.o_combined_eff = 0; // combined efficiency
- this.o_histogram = 0; // keep track of histogram x % of time for OTR (efficiency) over a max historical value ? or a counter which gets higher over time without.
- this.o_slope = 0;
-
- /*-----------alarms---------------*/
- //putting alarms in 1 array always gets all the alarms that are currently active or inactive
- //an alarm is a trigger to stop any process feeding the diffusers
- this.alarm = {
- text:[],
- state:false,
- flow:{
- min:{state:false,hyst:10}, //when there isnt enough flow to ensure the correct distribution of air
- max:{state:false,hyst:10},//when there is to much flow per diffuser and exceeds the suppliers limits
- },
- pressure:{
- min:{state:false,hyst:10}, // see min flow
- max:{state:false,hyst:10}, // see max flow
- },
- };
-
- /*-----------warnings---------------*/
- //putting alarms in 1 array always gets all the alarms that are currently active or inactive
- //a warning is a trigger to alert users on eradic behavior or some other activity that might cause damage in the future so users can investigate what is going on
- this.warning = {
- text:[], // fill this with the warnings we want to display
- state:false,
- deviation:{
- pressure:{state:false,hyst:2},//when there is a deviation versus expected pressures of the baseline that exceeds a hyst of x % of deviation before becomming true
- },
- flow:{
- min:{state:false,hyst:2}, //when there isnt enough flow to ensure the correct distribution of air
- max:{state:false,hyst:2},//when there is to much flow per diffuser and exceeds the suppliers limits
- },
- pressure:{
- min:{state:false,hyst:2}, // see min flow
- max:{state:false,hyst:2}, // see max flow
- },
- };
-
- /*-------------error handling -----------*/
- this.error = {
- text:[],
- state:false,
- }
-
- this.init = true;
- }
-
- /*------------------- GETTER/SETTERS -------------------*/
-
- set i_n_elements(x){
-
- //check if this input is a number
- if(Number.isNaN(x)){
- this.error.state = true;
- this.error.text.push("number of elements not of type number");
- }
-
- // you cant have partial elements
- x = Math.round(x);
-
- //check if init has allready been exectued
- if ( x <= 0 ) {
- this.error.state = true;
- this.error.text.push("0 elements input");
- }
-
- //set densi3ty to value
- this._i_n_elements = x;
-
- }
-
- get i_n_elements(){
- return this._i_n_elements;
- }
-
- get i_diff_density(){
- return this._i_diff_density;
- }
-
- set i_diff_density(value){
-
- //set densi3ty to value
- this._i_diff_density = value;
-
- //check if init has allready been exectued
- if(this.init == true){
- //submit new value to prediction
- this.predict_otr.i_f = value;
- //refresh predictions
- this.o_otr = this.predict_otr.o_y;
-
- }
-
- }
-
- set o_otr(value){
-
- //set density to value
- this._o_otr = Math.round( value * 100 ) / 100;
-
- //check if init has allready been exectued
- if(this.init == true){
-
- //current output in kg o2 / h
- this.o_kgo2_h = this.convert ( this.o_otr * this.n_flow * this.i_m_water ).from('g').to('kg') ;
-
- /*make max and min calculations to use them in a kg load zone controller*/
- this.o_kgo2_h_min = this.convert ( this.o_otr_min * this.n_flow * this.i_m_water ).from('g').to('kg') ;
- this.o_kgo2_h_max = this.convert ( this.o_otr_max * this.n_flow * this.i_m_water ).from('g').to('kg') ;
-
- //divide by 3600 to get the current ouput in kg
- this.o_kgo2 = this.o_kgo2_h / 3600 ;
-
- }
-
- }
-
- get o_otr(){
- return this._o_otr;
- }
-
- //set meter water column above the diffuser in meters
- set i_m_water(value){
-
- //set density to value
- this._i_m_water = value;
-
- //convert height to pressure in mbar
- this.o_p_water = this.fysics.heigth_to_pressure(this.i_water_density,value);
- this.o_p_total = this.o_p_water + this.o_p_flow ;
-
- //recalc values
- this.i_flow = this.i_flow;
- }
-
- get i_m_water(){
- return this._i_m_water
- }
-
- set o_kgo2_h(value){
- this._o_kgo2_h = Math.round( value * 100 ) / 100;
- }
-
- get o_kgo2_h(){
- return this._o_kgo2_h;
- }
-
- set o_p_total(value){
- this._o_p_total = Math.round( value * 100 ) / 100;
- }
-
- get o_p_total(){
- return this._o_p_total;
- }
-
- set o_p_flow(value){
-
- //set density to value
- this._o_p_flow = Math.round( value * 100 ) / 100;
-
- //recalc total
- this.o_p_total = this.o_p_water + value ;
- }
-
- get o_p_flow(){
- return this._o_p_flow;
- }
-
- set i_flow(value){
- //set density to value
- this._i_flow = value;
-
- if(this.init == true){
- //any flow smaller or equal to zero means diffusers are not being supplied with air and are not active.
- if(value > 0){
-
- // idle to off
- this.idle = false;
-
- //calc otr and p
- this.calc_otr_p(value);
-
- }
- else{
- this.idle = true;
- this.o_otr = 0;
- this.o_p_flow = 0;
- this.o_flow_element = 0;
- this.o_p_total = 0;
- }
- }
- }
-
- get i_flow(){
- return this._i_flow;
- }
-
-
- /*------------------------- functions ----------------------------*/
- //a diffuser has a combined efficiency
- combine_eff(o_otr,o_otr_min,o_otr_max,o_p_flow,o_p_min,o_p_max){
- //highest otr is best efficiency possible
- let eff1 = this.interpolation.interpolate_lin_single_point(o_otr,o_otr_min,o_otr_max,0,1);
- //lowest pressure is best pressure possible
- let eff2 = this.interpolation.interpolate_lin_single_point(o_p_flow,o_p_min,o_p_max,1,0);
-
- let result = eff1 * eff2 * 100 ;
-
- return result;
-
- }
-
- // do all actions in order to calculate the outputs for flow and pressure related stuf
- calc_otr_p(flow){
- //convert to kg using pressure,rh,temperature we need to get actual data to do this else we assume that normalized equals site
- //total input pressure equals atm pressure + measured pressure in system
- let tot_i_pressure = this.convert(this.i_local_atm_pressure + this.i_pressure).from('mbar').to('bar'); //calculated output in kg air
- this.o_kg = this.fysics.calc_air_dens(tot_i_pressure,0,20);
- this.o_kg_h = this.o_kg * flow ;
-
- //convert this to the normal m3 of diffuser density data
- this.n_flow = ( this.o_kg / this.n_kg ) * flow;
-
- //calculate how much flow per element
- this.o_flow_element = Math.round( ( this.n_flow / this.i_n_elements ) * 100 ) / 100 ;
-
- // input x values in predictors (we could make functions and return outputs dunno whats better)
- this.predict_otr.i_x = this.o_flow_element;
- this.predict_p.i_x = this.o_flow_element;
-
- //store otr max and min
- this.o_otr_min = this.predict_otr.c_fxy_y_min;
- this.o_otr_max = this.predict_otr.c_fxy_y_max;
-
- //store min and max pressure
- this.o_p_min = this.predict_p.c_fxy_y_min;
- this.o_p_max = this.predict_p.c_fxy_y_max;
-
- // predict oxygen transfer rate
- this.o_otr = this.predict_otr.o_y;
- // predict pressure output of diffuser
- this.o_p_flow = this.predict_p.o_y;
-
- //calculated combined efficiency in % where 100 % is ideal efficieny and 0 is worst
- this.o_combined_eff = Math.round(this.combine_eff(this.o_otr,this.o_otr_min,this.o_otr_max,this.o_p_flow,this.o_p_min,this.o_p_max) * 100 ) /100;
-
- //slope otr curve
- this.graph.calc(this.predict_otr.i_x,this.predict_otr.o_y);
- this.o_slope = this.graph.slope;
-
- //go through functions
- this.warning_check();
- this.alarm_check();
-
- }
-
- // calculate average saturation over the water depth of the diffuser
- calcAvgSolubility(temp){
-
- //calculate average pressure in water column
- let avgPMbar = ( this.o_p_water + this.i_local_atm_pressure ) / 2;
- let avgPBar = this.convert(avgPMbar).from('mbar').to('bar');
-
- // input x values in predictors (we could make functions and return outputs dunno whats better)
- this.predictO2saturation.i_f = avgPBar; // set f to temperature
- this.predictO2saturation.i_x = temp; // set temperature
-
- // predict oxygen transfer rate
- return this.predictO2saturation.o_y; //return average saturation value
-
-
- }
-
- warning_check(){
-
- //warnings do not require to be resetted. we need to log them somewhere in the database so its easy to check if these values are being exceeded
-
- //empty tekst before filling again and always reset state
- this.warning.text = [];
- this.warning.state = false;
-
- /* ------------------ Flow warnings ------------------*/
- //define hyst for flow
- let min_flow_hyst = this.predict_p.c_fxy_x_min * 1 * ( this.warning.flow.min.hyst / 100);
- let max_flow_hyst = this.predict_p.c_fxy_x_min * 1 * ( this.warning.flow.max.hyst / 100);
-
- //min flow
- if(this.o_flow_element < this.predict_p.c_fxy_x_min - min_flow_hyst ){
- this.warning.flow.min.state = true;
- this.warning.state = true;
- this.warning.text.push(" Warning : input flow " + this.o_flow_element + " is less then minimum allowed flow of " + ( this.predict_p.c_fxy_x_min - min_flow_hyst ));
- }
- else{ //auto reset
- this.warning.flow.min.state = false;
- }
-
- //max flow
- if(this.o_flow_element > this.predict_p.c_fxy_x_max + max_flow_hyst ){
- this.warning.flow.max.state = true;
- this.warning.state = true;
- this.warning.text.push("Warning input flow " + this.o_flow_element + " is exceeding nominal value of " + ( this.predict_p.c_fxy_x_max + max_flow_hyst ));
- }
- else{ //auto reset
- this.warning.flow.max.state = false;
- }
- }
-
- alarm_check(){
-
- //warnings do not require to be resetted. we need to log them somewhere in the database so its easy to check if these values are being exceeded
-
- //empty tekst before filling again and reset general state
- this.alarm.text = [];
- this.alarm.state = false;
-
- /* ------------------ Flow warnings ------------------*/
- //define hyst for flow
- let min_flow_hyst = this.predict_p.c_fxy_x_min * 1 * ( this.alarm.flow.min.hyst / 100);
- let max_flow_hyst = this.predict_p.c_fxy_x_min * 1 * ( this.alarm.flow.max.hyst / 100);
-
- //min flow
- if(this.o_flow_element < this.predict_p.c_fxy_x_min - min_flow_hyst ){
- this.alarm.flow.min.state = true;
- this.alarm.state = true;
- this.alarm.text.push("Alarm input flow " + this.o_flow_element + " is less then minimum allowed flow of " + ( this.predict_p.c_fxy_x_min - min_flow_hyst ));
- }
- else{ //auto reset
- this.warning.flow.min.state = false;
- }
-
- //max flow
- if(this.o_flow_element > this.predict_p.c_fxy_x_max + max_flow_hyst ){
- this.alarm.flow.max.state = true;
- this.alarm.state = true;
- this.alarm.text.push("Alarm input flow " + this.o_flow_element + " is exceeding an absolute max value of " + ( this.predict_p.c_fxy_x_max + max_flow_hyst ));
- }
- else{ //auto reset
- this.alarm.flow.max.state = false;
- }
-
-
- }
-
- //fetch curve and load it into object var
- load_specs(){
-
- //for now hardcoded will do as example from sulzer
- let Sulzerspecs = {
- supplier : "sulzer",
- type : "pik300",
- units:{
- Nm3: { "temp": 20, "pressure" : 1.01325 , "RH" : 0 },
- t_otr_curve : { f : "diffuser density in % ", x : "Nm3/h", y: { o2_weight : "g", flow : "Nm3/h" , height: "m" } } , // according to DIN
- t_p_curve : { x : "flow", y : "mbar" }
- },
- otr_curve: {
- 5: // diffuser density expressed in %
- {
- x:[1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10], // flow expressed in normal m3/h
- y:[28, 27, 26, 25, 24.5, 24, 23.5, 23.2, 23, 22.9], //oxygen transfer rate expressed in gram o2 / normal m3/h / per m
- },
- 10: // diffuser density expressed in %
- {
- x:[1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10], // flow expressed in normal m3/h
- y:[31, 29.5, 28.5, 27, 26, 25.5, 25, 24.7, 24.2, 24], //oxygen transfer rate expressed in gram o2 / normal m3/h / per m
- }
- },
- p_curve:{ //difuser pressure loss curve
- 0: // if curve doesnt have more than 1 dimension just fill in zero here or whatever
- {
- x:[1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10], // Flow expressed in nm3/h
- y:[25,26,28,30,35,38,42,48,55,68] //pressure expressed in mbar
- }
- },
- solubility_curve:{
- 1: // abs bar
- {
- x:[0,5,10,15,20,25,30,35,40,45,50], // temp in degrees celcius
- y:[14.6,12.8,11.3,10.1,9.1,8.3,7.6,7,6.5,6,5.6], // mg/l
- },
- 2: // abs bar
- {
- x:[0,5,10,15,20,25,30,35,40,45,50], // temp in degrees celcius
- y:[29.2,25.5,22.6,20.2,18.2,16.5,15.2,14,12.9,12,11.3], // mg/l
- },
- 4: // abs bar
- {
- x:[0,5,10,15,20,25,30,35,40,45,50], // temp in degrees celcius
- y:[58.4,51.1,45.1,40.3,36.4,33.1,30.3,27.9,25.9,24,22.7], // mg/l
- },
- }
- }
-
- //for now hardcoded will do as example from sulzer
- let specs = {
- supplier : "GVA",
- type : "ELASTOX-R",
- units:{
- Nm3: { "temp": 20, "pressure" : 1.01325 , "RH" : 0 },
- t_otr_curve : { f : "diffuser density in % ", x : "Nm3/h", y: { o2_weight : "g", flow : "Nm3/h" , height: "m" } } , // according to DIN
- t_p_curve : { x : "flow", y : "mbar" }
- },
- otr_curve: {
- 2.4: // diffuser density expressed in %
- {
- x:[2,3,4,5,6,7,8,9,10], // flow expressed in normal m3/h
- y:[26,25,24,23.5,23,22.75,22.5,22.25,22], //oxygen transfer rate expressed in gram o2 / normal m3/h / per m
- }
- },
- p_curve:{ //difuser pressure loss curve
- 0: // if curve doesnt have more than 1 dimension just fill in zero here or whatever
- {
- x:[2,3,4,5,6,7,8,9,10,11,12], // Flow expressed in nm3/h
- y:[40,42.5,45,47.5,50,51.5,53,54.5,56,57.5,59] //pressure expressed in mbar
- }
- }
- }
-
- return specs;
- }
-
-
- //testing converter function
- converttest(){
- let test = this.convert(1).from('l').to('ml');
- return test ;
- }
-
-
-} // end of class
-
-/*
-var diffuser = new Diffuser("sulzer","PIK300");
-
-// define inputs for diffuser
-diffuser.i_m_water = 5; // set water column above diffuser
-diffuser.i_n_elements = 1;
-diffuser.i_diff_density = 5; // set density in %
-diffuser.i_flow = 4; // set flow rate in m3/h
-
-// outputs are
-console.log("--------------Inputs--------------")
-console.log("Diffuser density : "+ diffuser.i_diff_density + " %");
-console.log("Flow : " + diffuser.i_flow + " m3/h" );
-console.log("Flow rate per element : " + diffuser.o_flow_element + " m3/h");
-console.log("Number of elements : " + diffuser.i_n_elements );
-console.log("--------------Outputs--------------")
-console.log("converted input flow to diffuser normalized flow : " + diffuser.n_flow + " m3/h ");
-console.log("Oxygen transfer rate : " + diffuser.o_otr + " o2 / m3/h / meter");
-console.log("Pressure loss over diffusers : " + diffuser.o_p_flow + " mbar" );
-console.log("Pressure loss over water column:" + diffuser.o_p_water + " mbar" );
-console.log("Total pressure loss : " + diffuser.o_p_total + " mbar");
-console.log("predicted diffuser oxygen input for bio : " + diffuser.o_kgo2_h + " kg o2 / h");
-console.log("Predicted actual input o2 : " + diffuser.o_kgo2 + " kg o2 / s " );
-console.log("max otr " + diffuser.o_otr_max);
-console.log("displaying warnings : " + JSON.stringify(diffuser.warning.text) );
-
-//change flow
-diffuser.i_diff_density = 5; // set density in %
-//diffuser.i_flow = 4;
-
-console.log("--------------Inputs--------------")
-console.log("Diffuser density : "+ diffuser.i_diff_density + " %");
-console.log("Flow : " + diffuser.i_flow + " m3/h" );
-console.log("--------------Outputs--------------")
-console.log("Oxygen transfer rate : " + diffuser.o_otr + " o2 / m3/h / meter");
-console.log("Pressure loss: " + diffuser.o_p_flow + " mbar" );
-console.log("slope: " + diffuser.o_slope );
-
-diffuser.i_flow = 5;
-console.log("--------------Inputs--------------")
-console.log("Diffuser density : "+ diffuser.i_diff_density + " %");
-console.log("Flow : " + diffuser.i_flow + " m3/h" );
-console.log("--------------Outputs--------------")
-console.log("Oxygen transfer rate : " + diffuser.o_otr + " o2 / m3/h / meter");
-console.log("Pressure loss: " + diffuser.o_p_flow + " mbar" );
-console.log("slope: " + diffuser.o_slope );
-
-diffuser.i_diff_density = 0;
-
-console.log("--------------Inputs--------------")
-console.log("Diffuser density : "+ diffuser.i_diff_density + " %");
-console.log("Flow : " + diffuser.i_flow + " m3/h" );
-console.log("--------------Outputs--------------")
-console.log("Oxygen transfer rate : " + diffuser.o_otr + " o2 / m3/h / meter");
-console.log("Pressure loss: " + diffuser.o_p_flow + " mbar" );
-//
-//*/
-module.exports = Diffuser;
+module.exports = require('./src/specificClass');
diff --git a/src/nodeClass.js b/src/nodeClass.js
new file mode 100644
index 0000000..c2c55ee
--- /dev/null
+++ b/src/nodeClass.js
@@ -0,0 +1,123 @@
+const { outputUtils, configManager } = require('generalFunctions');
+const Specific = require('./specificClass');
+
+class nodeClass {
+ constructor(uiConfig, RED, nodeInstance, nameOfNode) {
+ this.node = nodeInstance;
+ this.RED = RED;
+ this.name = nameOfNode;
+
+ this._loadConfig(uiConfig);
+ this._setupSpecificClass();
+ this._registerChild();
+ this._startTickLoop();
+ this._attachInputHandler();
+ this._attachCloseHandler();
+ }
+
+ _loadConfig(uiConfig) {
+ const cfgMgr = new configManager();
+ const suffix = uiConfig.number !== undefined && uiConfig.number !== '' ? `_${uiConfig.number}` : '';
+ const resolvedUiConfig = {
+ ...uiConfig,
+ name: uiConfig.name ? `${uiConfig.name}${suffix}` : this.name,
+ unit: uiConfig.unit || 'kg o2/h',
+ };
+
+ this.config = cfgMgr.buildConfig(this.name, resolvedUiConfig, this.node.id, {
+ functionality: {
+ softwareType: this.name,
+ role: 'aeration diffuser',
+ },
+ diffuser: {
+ number: Number(uiConfig.number) || 0,
+ elements: Number(uiConfig.i_elements) || 1,
+ density: Number(uiConfig.i_diff_density) || 2.4,
+ waterHeight: Number(uiConfig.i_m_water) || 0,
+ alfaFactor: Number(uiConfig.alfaf ?? 0.7) || 0.7,
+ headerPressure: Number(uiConfig.i_pressure) || 0,
+ localAtmPressure: Number(uiConfig.i_local_atm_pressure) || 1013.25,
+ waterDensity: Number(uiConfig.i_water_density) || 997,
+ },
+ });
+
+ this._output = new outputUtils();
+ }
+
+ _setupSpecificClass() {
+ this.source = new Specific(this.config);
+ this.node.source = this.source;
+ }
+
+ _registerChild() {
+ setTimeout(() => {
+ this.node.send([
+ null,
+ null,
+ {
+ topic: 'registerChild',
+ payload: this.node.id,
+ positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment',
+ },
+ ]);
+ }, 100);
+ }
+
+ _startTickLoop() {
+ setTimeout(() => {
+ this._tickInterval = setInterval(() => this._tick(), 1000);
+ }, 1000);
+ }
+
+ _tick() {
+ const raw = this.source.getOutput();
+ const processMsg = this._output.formatMsg(raw, this.config, 'process');
+ const influxMsg = this._output.formatMsg(raw, this.config, 'influxdb');
+ this.node.status(this.source.getStatus());
+ this.node.send([processMsg, influxMsg, null]);
+ }
+
+ _attachInputHandler() {
+ this.node.on('input', (msg, send, done) => {
+ try {
+ switch (msg.topic) {
+ case 'density':
+ this.source.setDensity(msg.payload);
+ break;
+ case 'air_flow':
+ this.source.setFlow(msg.payload);
+ break;
+ case 'height_water':
+ this.source.setWaterHeight(msg.payload);
+ break;
+ case 'header_pressure':
+ this.source.setHeaderPressure(msg.payload);
+ break;
+ case 'elements':
+ this.source.setElementCount(msg.payload);
+ break;
+ case 'alfaFactor':
+ this.source.setAlfaFactor(msg.payload);
+ break;
+ default:
+ this.source.logger.warn(`Unknown topic: ${msg.topic}`);
+ break;
+ }
+ done();
+ } catch (error) {
+ this.node.status({ fill: 'red', shape: 'ring', text: 'Bad request data' });
+ this.node.error(`Bad request data: ${error.message}`, msg);
+ done(error);
+ }
+ });
+ }
+
+ _attachCloseHandler() {
+ this.node.on('close', (done) => {
+ clearInterval(this._tickInterval);
+ done();
+ });
+ }
+}
+
+module.exports = nodeClass;
diff --git a/src/specificClass.js b/src/specificClass.js
new file mode 100644
index 0000000..dd3bd7a
--- /dev/null
+++ b/src/specificClass.js
@@ -0,0 +1,316 @@
+const { logger, interpolation, gravity, convert } = require('generalFunctions');
+
+class Diffuser {
+ constructor(config = {}) {
+ this.config = config;
+ this.logger = new logger(
+ this.config.general?.logging?.enabled,
+ this.config.general?.logging?.logLevel,
+ this.config.general?.name,
+ );
+
+ this.interpolation = new interpolation({ type: 'linear' });
+ this.fysics = gravity.fysics;
+ this.convert = convert;
+ this.specs = this.loadSpecs();
+
+ this.idle = true;
+ this.warning = { state: false, text: [], flow: { min: { hyst: 2 }, max: { hyst: 2 } } };
+ this.alarm = { state: false, text: [], flow: { min: { hyst: 10 }, max: { hyst: 10 } } };
+
+ this.i_pressure = this.config.diffuser?.headerPressure || 0;
+ this.i_local_atm_pressure = this.config.diffuser?.localAtmPressure || 1013.25;
+ this.i_water_density = this.config.diffuser?.waterDensity || 997;
+ this.i_alfa_factor = this.config.diffuser?.alfaFactor || 0.7;
+ this.i_n_elements = this.normalizePositiveInteger(this.config.diffuser?.elements, 1);
+ this.i_diff_density = this.normalizeNumber(this.config.diffuser?.density, 2.4);
+ this.i_m_water = this.normalizeNumber(this.config.diffuser?.waterHeight, 0);
+ this.i_flow = 0;
+
+ this.n_kg = this.fysics.calc_air_dens(1013.25, 0, 20);
+
+ this.n_flow = 0;
+ this.o_otr = 0;
+ this.o_p_flow = 0;
+ this.o_p_water = this.fysics.heigth_to_pressure(this.i_water_density, this.i_m_water);
+ this.o_p_total = this.o_p_water;
+ this.o_kg = 0;
+ this.o_kg_h = 0;
+ this.o_kgo2_h = 0;
+ this.o_kgo2 = 0;
+ this.o_kgo2_h_min = 0;
+ this.o_kgo2_h_max = 0;
+ this.o_flow_element = 0;
+ this.o_otr_min = 0;
+ this.o_otr_max = 0;
+ this.o_p_min = 0;
+ this.o_p_max = 0;
+ this.o_combined_eff = 0;
+ this.o_slope = 0;
+ }
+
+ normalizeNumber(value, fallback = 0) {
+ const parsed = Number(value);
+ return Number.isFinite(parsed) ? parsed : fallback;
+ }
+
+ normalizePositiveInteger(value, fallback = 1) {
+ const parsed = Math.round(Number(value));
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
+ }
+
+ setDensity(value) {
+ this.i_diff_density = this.normalizeNumber(value, this.i_diff_density);
+ this.recalculate();
+ }
+
+ setFlow(value) {
+ this.i_flow = Math.max(0, this.normalizeNumber(value, 0));
+ this.recalculate();
+ }
+
+ setWaterHeight(value) {
+ this.i_m_water = Math.max(0, this.normalizeNumber(value, this.i_m_water));
+ this.o_p_water = this.fysics.heigth_to_pressure(this.i_water_density, this.i_m_water);
+ this.recalculate();
+ }
+
+ setHeaderPressure(value) {
+ this.i_pressure = this.normalizeNumber(value, this.i_pressure);
+ this.recalculate();
+ }
+
+ setElementCount(value) {
+ this.i_n_elements = this.normalizePositiveInteger(value, this.i_n_elements);
+ this.recalculate();
+ }
+
+ setAlfaFactor(value) {
+ this.i_alfa_factor = this.normalizeNumber(value, this.i_alfa_factor);
+ this.recalculate();
+ }
+
+ recalculate() {
+ if (this.i_flow <= 0) {
+ this.idle = true;
+ this.n_flow = 0;
+ this.o_otr = 0;
+ this.o_p_flow = 0;
+ this.o_flow_element = 0;
+ this.o_p_total = this.o_p_water;
+ this.o_kg = 0;
+ this.o_kg_h = 0;
+ this.o_kgo2_h = 0;
+ this.o_kgo2 = 0;
+ this.o_combined_eff = 0;
+ this.o_slope = 0;
+ this.warning.text = [];
+ this.warning.state = false;
+ this.alarm.text = [];
+ this.alarm.state = false;
+ return;
+ }
+
+ this.idle = false;
+ this.calcOtrPressure(this.i_flow);
+ }
+
+ getCurveKeys(curve) {
+ return Object.keys(curve)
+ .map(Number)
+ .sort((a, b) => a - b);
+ }
+
+ interpolateSeries(points, x) {
+ this.interpolation.load_spline(points.x, points.y, 'linear');
+ return this.interpolation.interpolate(x);
+ }
+
+ interpolateCurveByDensity(curve, density, x) {
+ const keys = this.getCurveKeys(curve);
+ if (keys.length === 1) {
+ const only = curve[keys[0]];
+ return {
+ value: this.interpolateSeries(only, x),
+ minY: Math.min(...only.y),
+ maxY: Math.max(...only.y),
+ minX: Math.min(...only.x),
+ maxX: Math.max(...only.x),
+ slope: this.getSegmentSlope(only, x),
+ };
+ }
+
+ const lowerKey = keys.reduce((acc, key) => (key <= density ? key : acc), keys[0]);
+ const upperKey = keys.find((key) => key >= density) ?? keys[keys.length - 1];
+ const lowerCurve = curve[lowerKey];
+ const upperCurve = curve[upperKey];
+
+ if (lowerKey === upperKey) {
+ return {
+ value: this.interpolateSeries(lowerCurve, x),
+ minY: Math.min(...lowerCurve.y),
+ maxY: Math.max(...lowerCurve.y),
+ minX: Math.min(...lowerCurve.x),
+ maxX: Math.max(...lowerCurve.x),
+ slope: this.getSegmentSlope(lowerCurve, x),
+ };
+ }
+
+ const lowerValue = this.interpolateSeries(lowerCurve, x);
+ const upperValue = this.interpolateSeries(upperCurve, x);
+ const ratio = (density - lowerKey) / (upperKey - lowerKey);
+
+ return {
+ value: lowerValue + (upperValue - lowerValue) * ratio,
+ minY: Math.min(...lowerCurve.y) + (Math.min(...upperCurve.y) - Math.min(...lowerCurve.y)) * ratio,
+ maxY: Math.max(...lowerCurve.y) + (Math.max(...upperCurve.y) - Math.max(...lowerCurve.y)) * ratio,
+ minX: Math.min(...lowerCurve.x),
+ maxX: Math.max(...lowerCurve.x),
+ slope: this.getSegmentSlope(lowerCurve, x),
+ };
+ }
+
+ getSegmentSlope(curvePoints, x) {
+ const xs = curvePoints.x;
+ const ys = curvePoints.y;
+ for (let i = 0; i < xs.length - 1; i += 1) {
+ if (x <= xs[i + 1]) {
+ return (ys[i + 1] - ys[i]) / (xs[i + 1] - xs[i]);
+ }
+ }
+ const last = xs.length - 1;
+ return (ys[last] - ys[last - 1]) / (xs[last] - xs[last - 1]);
+ }
+
+ combineEff(oOtr, oOtrMin, oOtrMax, oPFlow, oPMin, oPMax) {
+ const otrSpan = oOtrMax - oOtrMin;
+ const pSpan = oPMax - oPMin;
+ const eff1 = otrSpan > 0 ? (oOtr - oOtrMin) / otrSpan : 0;
+ const eff2 = pSpan > 0 ? 1 - ((oPFlow - oPMin) / pSpan) : 0;
+ return Math.max(0, eff1 * eff2 * 100);
+ }
+
+ calcOtrPressure(flow) {
+ const totalInputPressureMbar = this.i_local_atm_pressure + this.i_pressure;
+ this.o_kg = this.fysics.calc_air_dens(totalInputPressureMbar, 0, 20);
+ this.o_kg_h = this.o_kg * flow;
+ this.n_flow = (this.o_kg / this.n_kg) * flow;
+ this.o_flow_element = Math.round((this.n_flow / this.i_n_elements) * 100) / 100;
+
+ const otr = this.interpolateCurveByDensity(this.specs.otr_curve, this.i_diff_density, this.o_flow_element);
+ const pressure = this.interpolateCurveByDensity(this.specs.p_curve, 0, this.o_flow_element);
+
+ this.o_otr_min = otr.minY;
+ this.o_otr_max = otr.maxY;
+ this.o_p_min = pressure.minY;
+ this.o_p_max = pressure.maxY;
+ this.o_otr = Math.round(otr.value * 100) / 100;
+ this.o_p_flow = Math.round(pressure.value * 100) / 100;
+ this.o_p_total = Math.round((this.o_p_water + this.o_p_flow) * 100) / 100;
+ this.o_kgo2_h = Math.round(this.convert(this.o_otr * this.n_flow * this.i_m_water * this.i_alfa_factor).from('g').to('kg') * 100) / 100;
+ this.o_kgo2_h_min = Math.round(this.convert(this.o_otr_min * this.n_flow * this.i_m_water * this.i_alfa_factor).from('g').to('kg') * 100) / 100;
+ this.o_kgo2_h_max = Math.round(this.convert(this.o_otr_max * this.n_flow * this.i_m_water * this.i_alfa_factor).from('g').to('kg') * 100) / 100;
+ this.o_kgo2 = this.o_kgo2_h / 3600;
+ this.o_combined_eff = Math.round(this.combineEff(
+ this.o_otr,
+ this.o_otr_min,
+ this.o_otr_max,
+ this.o_p_flow,
+ this.o_p_min,
+ this.o_p_max,
+ ) * 100) / 100;
+ this.o_slope = Math.round(otr.slope * 1000) / 1000;
+
+ this.warningCheck(pressure.minX, pressure.maxX);
+ this.alarmCheck(pressure.minX, pressure.maxX);
+ }
+
+ warningCheck(minFlow, maxFlow) {
+ this.warning.text = [];
+ this.warning.state = false;
+ const minHyst = minFlow * (this.warning.flow.min.hyst / 100);
+ const maxHyst = maxFlow * (this.warning.flow.max.hyst / 100);
+
+ if (this.o_flow_element < minFlow - minHyst) {
+ this.warning.state = true;
+ this.warning.text.push(`Warning: flow per element ${this.o_flow_element} is below ${Math.round((minFlow - minHyst) * 100) / 100}`);
+ }
+
+ if (this.o_flow_element > maxFlow + maxHyst) {
+ this.warning.state = true;
+ this.warning.text.push(`Warning: flow per element ${this.o_flow_element} exceeds ${Math.round((maxFlow + maxHyst) * 100) / 100}`);
+ }
+ }
+
+ alarmCheck(minFlow, maxFlow) {
+ this.alarm.text = [];
+ this.alarm.state = false;
+ const minHyst = minFlow * (this.alarm.flow.min.hyst / 100);
+ const maxHyst = maxFlow * (this.alarm.flow.max.hyst / 100);
+
+ if (this.o_flow_element < minFlow - minHyst) {
+ this.alarm.state = true;
+ this.alarm.text.push(`Alarm: flow per element ${this.o_flow_element} is below ${Math.round((minFlow - minHyst) * 100) / 100}`);
+ }
+
+ if (this.o_flow_element > maxFlow + maxHyst) {
+ this.alarm.state = true;
+ this.alarm.text.push(`Alarm: flow per element ${this.o_flow_element} exceeds ${Math.round((maxFlow + maxHyst) * 100) / 100}`);
+ }
+ }
+
+ getStatus() {
+ if (this.alarm.state) {
+ return { fill: 'red', shape: 'dot', text: this.alarm.text[0] };
+ }
+ if (this.warning.state) {
+ return { fill: 'yellow', shape: 'dot', text: this.warning.text[0] };
+ }
+ if (this.idle) {
+ return { fill: 'grey', shape: 'dot', text: `${this.o_kgo2_h} kg o2 / h` };
+ }
+ return { fill: 'green', shape: 'dot', text: `${this.o_kgo2_h} kg o2 / h` };
+ }
+
+ getOutput() {
+ return {
+ iPressure: this.i_pressure,
+ iMWater: this.i_m_water,
+ iFlow: this.i_flow,
+ nFlow: Math.round(this.n_flow * 100) / 100,
+ oOtr: this.o_otr,
+ oPLoss: this.o_p_total,
+ oKgo2H: this.o_kgo2_h,
+ oFlowElement: this.o_flow_element,
+ efficiency: this.o_combined_eff,
+ slope: this.o_slope,
+ idle: this.idle,
+ warning: [...this.warning.text],
+ alarm: [...this.alarm.text],
+ };
+ }
+
+ loadSpecs() {
+ return {
+ supplier: 'GVA',
+ type: 'ELASTOX-R',
+ units: {
+ Nm3: { temp: 20, pressure: 1.01325, RH: 0 },
+ },
+ otr_curve: {
+ 2.4: {
+ x: [2, 3, 4, 5, 6, 7, 8, 9, 10],
+ y: [26, 25, 24, 23.5, 23, 22.75, 22.5, 22.25, 22],
+ },
+ },
+ p_curve: {
+ 0: {
+ x: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
+ y: [40, 42.5, 45, 47.5, 50, 51.5, 53, 54.5, 56, 57.5, 59],
+ },
+ },
+ };
+ }
+}
+
+module.exports = Diffuser;
diff --git a/test/specificClass.test.js b/test/specificClass.test.js
new file mode 100644
index 0000000..44e96b1
--- /dev/null
+++ b/test/specificClass.test.js
@@ -0,0 +1,72 @@
+const Diffuser = require('../src/specificClass');
+
+function makeConfig(overrides = {}) {
+ return {
+ general: {
+ name: 'Zone_1',
+ logging: {
+ enabled: false,
+ logLevel: 'error',
+ },
+ },
+ functionality: {
+ softwareType: 'diffuser',
+ role: 'aeration diffuser',
+ },
+ diffuser: {
+ number: 1,
+ elements: 4,
+ density: 2.4,
+ waterHeight: 4.5,
+ alfaFactor: 0.7,
+ headerPressure: 0,
+ localAtmPressure: 1013.25,
+ waterDensity: 997,
+ ...overrides,
+ },
+ };
+}
+
+describe('diffuser specificClass', () => {
+ it('starts idle with zero production', () => {
+ const diffuser = new Diffuser(makeConfig());
+
+ expect(diffuser.idle).toBe(true);
+ expect(diffuser.getOutput()).toEqual(expect.objectContaining({
+ oKgo2H: 0,
+ oPLoss: expect.any(Number),
+ }));
+ });
+
+ it('calculates oxygen transfer and pressure once airflow is applied', () => {
+ const diffuser = new Diffuser(makeConfig());
+ diffuser.setFlow(24);
+
+ const output = diffuser.getOutput();
+ expect(diffuser.idle).toBe(false);
+ expect(output.oFlowElement).toBeGreaterThan(0);
+ expect(output.oOtr).toBeGreaterThan(0);
+ expect(output.oPLoss).toBeGreaterThan(diffuser.o_p_water);
+ expect(output.oKgo2H).toBeGreaterThan(0);
+ });
+
+ it('increases total pressure when water height rises', () => {
+ const diffuser = new Diffuser(makeConfig());
+ diffuser.setFlow(24);
+ const lowHeadLoss = diffuser.getOutput().oPLoss;
+
+ diffuser.setWaterHeight(6);
+ const highHeadLoss = diffuser.getOutput().oPLoss;
+
+ expect(highHeadLoss).toBeGreaterThan(lowHeadLoss);
+ });
+
+ it('raises warnings and alarms when flow per element is too low', () => {
+ const diffuser = new Diffuser(makeConfig({ elements: 1, waterHeight: 3 }));
+ diffuser.setFlow(0.5);
+
+ expect(diffuser.warning.state).toBe(true);
+ expect(diffuser.alarm.state).toBe(true);
+ expect(diffuser.getStatus().fill).toBe('red');
+ });
+});