changes
This commit is contained in:
318
dependencies/heatExchanger/heatExchanger.js
vendored
Normal file
318
dependencies/heatExchanger/heatExchanger.js
vendored
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* @file heatExchanger.js
|
||||
*
|
||||
* Permission is hereby granted to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to use it for personal
|
||||
* or non-commercial purposes, with the following restrictions:
|
||||
*
|
||||
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
|
||||
* be copied, merged, distributed, sublicensed, or sold without explicit
|
||||
* prior written permission from the author.
|
||||
*
|
||||
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
|
||||
* a valid license, obtainable only with the explicit consent of the author.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* Ownership of this code remains solely with the original author. Unauthorized
|
||||
* use of this Software is strictly prohibited.
|
||||
*
|
||||
* Author:
|
||||
* - Rene De Ren
|
||||
* Email:
|
||||
* - rene@thegoldenbasket.nl
|
||||
*
|
||||
* Future Improvements:
|
||||
* - Time-based stability checks
|
||||
* - Warmup handling
|
||||
* - Dynamic outlier detection thresholds
|
||||
* - Dynamic smoothing window and methods
|
||||
* - Alarm and threshold handling
|
||||
* - Maintenance mode
|
||||
* - Historical data and trend analysis
|
||||
*/
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const Logger = require('../../../generalFunctions/helper/logger');
|
||||
const defaultConfig = require('./heatExchangerConfig.json');
|
||||
const ConfigUtils = require('../../../generalFunctions/helper/configUtils');
|
||||
|
||||
class heatExchanger {
|
||||
constructor(config={}) {
|
||||
|
||||
this.emitter = new EventEmitter(); // Own EventEmitter
|
||||
this.configUtils = new ConfigUtils(defaultConfig);
|
||||
this.config = this.configUtils.initConfig(config);
|
||||
|
||||
// Init after config is set
|
||||
this.logger = new Logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name);
|
||||
this.measurements = {};
|
||||
|
||||
// defining all parameters of a heat exchanger
|
||||
this.maxFlow = this.config.asset.maxFlowRate; // expressed in m3/h
|
||||
this.volume = 0.36; // expressed in liters
|
||||
this.tempRange = {min: -195 , max : 225}; // expressed in °C
|
||||
this.pressureRange = {min: 0, max: 20}; // expressed in bar
|
||||
this.maxPower = this.config.asset.maxPower ; // expressed in kW
|
||||
this.type = this.config.asset.subType ; // type of heat exchanger
|
||||
|
||||
//keep track of last known values
|
||||
this.mFlowRateHot = 0; // kg/s
|
||||
this.mFlowRateCold = 0; // kg/s
|
||||
this.tempInHot = 0; // °C
|
||||
this.tempOutHot = 0; // °C
|
||||
this.tempInCold = 0; // °C
|
||||
this.tempOutCold = 0; // °C
|
||||
this.Q_act = 0; // kW
|
||||
|
||||
//medium properties
|
||||
this.inputMedium = this.config.medium.input; // type of medium
|
||||
this.outputMedium = this.config.medium.output; // type of medium
|
||||
//build lookup table for medium properties (to do later)
|
||||
this.CpIn = 4.18; // specific heat capacity of water in kJ/kg°C
|
||||
this.CpOut = 4.18; // specific heat capacity of water in kJ/kg°C
|
||||
|
||||
this.flowConfig = this.config.flowConfiguration; // Type of flow arrangement: "counterflow", "parallel", "crossflow", etc.
|
||||
|
||||
this.U = this.config.asset.U; // average for stainaless steel plate heat exchangers water to water - 2000-5000 W/m2K
|
||||
this.A = this.config.asset.A; // heat exchanger area in m2 (for example 0.24m2)
|
||||
this.UA = this.calcUA(this.U,this.A) / 1000; // heat exchanger UA value in kW/K
|
||||
|
||||
this.logger.debug(`heatExchanger id: ${this.config.general.id}, initialized successfully.`);
|
||||
|
||||
}
|
||||
|
||||
//later add conversions for different units
|
||||
setHotFlowRate(flowRate) {
|
||||
this.mFlowRateHot = this.m3hToKgs(flowRate);
|
||||
this.calcOutputTemps();
|
||||
return this.mFlowRateHot;
|
||||
}
|
||||
|
||||
setColdFlowRate(flowRate) {
|
||||
if (flowRate === undefined || flowRate === null || isNaN(parseFloat(flowRate))){
|
||||
this.logger.error(`Invalid flow rate value: ${flowRate}`);
|
||||
this.mFlowRateCold = 0;
|
||||
return this.mFlowRateCold;
|
||||
}
|
||||
this.mFlowRateCold = this.m3hToKgs(flowRate);
|
||||
this.calcOutputTemps();
|
||||
return this.mFlowRateCold;
|
||||
}
|
||||
|
||||
setHotInput(temp) {
|
||||
this.tempInHot = temp;
|
||||
this.calcOutputTemps();
|
||||
return this.tempInHot;
|
||||
}
|
||||
|
||||
setColdInput(temp) {
|
||||
this.tempInCold = temp;
|
||||
this.calcOutputTemps();
|
||||
return this.tempInCold;
|
||||
}
|
||||
|
||||
calcUA(U, A) {
|
||||
// UA = U * A
|
||||
const UA = U * A;
|
||||
return UA;
|
||||
}
|
||||
|
||||
getHeatTransferRate(mFlowRate, Cp, deltaT) {
|
||||
// Q heat transfer rate kW = m mass flow rate kg/s * Cp specific heat capacity kJ/kg°C * ΔT temperature difference °C
|
||||
const Q = mFlowRate * Cp * deltaT;
|
||||
return Q;
|
||||
}
|
||||
|
||||
calcOutputTemps() {
|
||||
// Calculate outlet temperatures based on last known input temperatures and heat transfer rate
|
||||
const result = this.calcOutletTempsEffectivenessNTU(this.mFlowRateHot, this.tempInHot, this.mFlowRateCold, this.tempInCold);
|
||||
this.tempOutHot = result.tempOutHot;
|
||||
this.tempOutCold = result.tempOutCold;
|
||||
this.Q_act = result.Q;
|
||||
this.logger.debug(`Hot Outlet Temperature: ${this.tempOutHot.toFixed(2)} °C, Cold Outlet Temperature: ${this.tempOutCold.toFixed(2)} °C, Heat Transfer Rate: ${this.Q_act.toFixed(2)} kW`);
|
||||
return result;
|
||||
}
|
||||
|
||||
calcOutletTempsEffectivenessNTU(mFlowRateHot, tempInHot, mFlowRateCold, tempInCold) {
|
||||
// Calculate heat capacity rates
|
||||
const C_hot = mFlowRateHot * this.CpIn;
|
||||
const C_cold = mFlowRateCold * this.CpOut;
|
||||
const UA = this.UA;
|
||||
const flowConfig = this.flowConfig;
|
||||
|
||||
// Find C_min and C_max
|
||||
const C_min = Math.min(C_hot, C_cold);
|
||||
const C_max = Math.max(C_hot, C_cold);
|
||||
const Cr = C_min / C_max;
|
||||
|
||||
// Calculate NTU (Number of Transfer Units)
|
||||
const NTU = (C_min > 0) ? UA / C_min : 0;
|
||||
|
||||
// Calculate effectiveness based on exchanger type
|
||||
let epsilon;
|
||||
switch (flowConfig) {
|
||||
case "counterflow":
|
||||
// Special case for Cr = 1 (balanced flow)
|
||||
if (Math.abs(Cr - 1) < 0.00001) {
|
||||
epsilon = NTU / (1 + NTU);
|
||||
} else {
|
||||
epsilon = (1 - Math.exp(-NTU * (1 - Cr))) / (1 - Cr * Math.exp(-NTU * (1 - Cr)));
|
||||
}
|
||||
break;
|
||||
case "parallelflow":
|
||||
epsilon = (1 - Math.exp(-NTU * (1 + Cr))) / (1 + Cr);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported flow configuration ${flowConfig}`);
|
||||
|
||||
}
|
||||
// Calculate maximum heat transfer in
|
||||
const Q_max = C_min * (tempInHot - tempInCold);
|
||||
|
||||
// Calculate actual heat transfer
|
||||
const Q = Math.min ( epsilon * Q_max, this.maxPower) ;
|
||||
|
||||
// Calculate outlet temperatures
|
||||
const tempOutHot = C_hot > 0 ? ( tempInHot - Q / C_hot) : tempInHot;
|
||||
const tempOutCold = C_cold > 0 ? ( tempInCold + Q / C_cold ) : tempInCold;
|
||||
|
||||
return { tempOutHot, tempOutCold, Q };
|
||||
}
|
||||
|
||||
// mFlowRate in kg/s, Cp in kJ/kg°C, tempIn in °C without effectiveness
|
||||
calcOutletTemps(mFlowRateHot, CpHot, tempInHot, mFlowRateCold, CpCold, tempInCold, Q) {
|
||||
// Calculate hot outlet temperature (heat is lost)
|
||||
const tempOutHot = tempInHot - (Q / (mFlowRateHot * CpHot));
|
||||
|
||||
// Calculate cold outlet temperature (heat is gained)
|
||||
const tempOutCold = tempInCold + (Q / (mFlowRateCold * CpCold));
|
||||
|
||||
return { tempOutHot, tempOutCold };
|
||||
}
|
||||
|
||||
// Use Q to get the output temperature of the same stream temp in °C, mFlowRate in kg/s, Cp in kJ/kg°C
|
||||
getTempOut(mFlowRate, Cp, tempIn, Q) {
|
||||
// T_out = T_in + Q / (m * Cp)
|
||||
const tempOut = tempIn + Q / (mFlowRate * Cp);
|
||||
return tempOut;
|
||||
}
|
||||
|
||||
m3hToKgs(m3h) {
|
||||
// 1 m3/h = 0.000277778 m3/s
|
||||
const m3s = m3h * 0.000277778;
|
||||
// 1 m3 = 1000 kg
|
||||
const kgs = m3s * 1000;
|
||||
return kgs;
|
||||
}
|
||||
|
||||
// use Q to get the input temperature of the same stream
|
||||
getTempIn(mFlowRate, Cp, tempOut, Q) {
|
||||
// T_in = T_out - Q / (m * Cp)
|
||||
const tempIn = tempOut - Q / (mFlowRate * Cp);
|
||||
return tempIn;
|
||||
}
|
||||
|
||||
getDeltaT(tempIn, tempOut) {
|
||||
// ΔT = T_out - T_in
|
||||
const deltaT = tempOut - tempIn;
|
||||
return deltaT;
|
||||
}
|
||||
|
||||
getOutput() {
|
||||
|
||||
// Improved output object generation
|
||||
const output = {};
|
||||
//build the output object
|
||||
if(Object.keys(this.measurements).length > 0) {
|
||||
this.measurements.getTypes().forEach(type => {
|
||||
this.measurements.getVariants(type).forEach(variant => {
|
||||
|
||||
const downstreamVal = this.measurements.type(type).variant(variant).position("downstream").getCurrentValue();
|
||||
const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue();
|
||||
|
||||
if (downstreamVal != null) {
|
||||
output[`downstream_${variant}_${type}`] = downstreamVal;
|
||||
}
|
||||
if (upstreamVal != null) {
|
||||
output[`upstream_${variant}_${type}`] = upstreamVal;
|
||||
}
|
||||
if (downstreamVal != null && upstreamVal != null) {
|
||||
const diffVal = this.measurements.type(type).variant(variant).difference().value;
|
||||
output[`differential_${variant}_${type}`] = diffVal;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
output["flowRateHot"] = this.mFlowRateHot;
|
||||
output["flowRateCold"] = this.mFlowRateCold;
|
||||
output["tempOutHot"] = this.tempOutHot;
|
||||
output["tempOutCold"] = this.tempOutCold;
|
||||
output["Q"] = this.Q_act;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = heatExchanger;
|
||||
|
||||
/*
|
||||
// Testing the class
|
||||
const configuration = {
|
||||
general: {
|
||||
name: "he1",
|
||||
logging: {
|
||||
enabled: true,
|
||||
logLevel: "debug",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const he = new heatExchanger(configuration);
|
||||
|
||||
he.logger.info(`heatExchanger created with config : ${JSON.stringify(he.config)}`);
|
||||
|
||||
he.logger.setLogLevel("debug");
|
||||
|
||||
// hot side (water)
|
||||
const mFlowHot = he.m3hToKgs(1); // kg/s
|
||||
const CpHot = 4.18; // kJ/kg.K (Water)
|
||||
const Th_in = 100; // °C
|
||||
|
||||
// cold side (water)
|
||||
const mFlowCold = he.m3hToKgs(1); // kg/s
|
||||
const CpCold = 4.18; // kJ/kg.K (Water)
|
||||
const Tc_in = 10; // °C
|
||||
|
||||
console.log(`UA: ${he.UA} W/K`);
|
||||
|
||||
const results = he.calcOutletTempsEffectivenessNTU(mFlowHot, CpHot, Th_in, mFlowCold, CpCold, Tc_in);
|
||||
//loop over different temperatures and calculate the heat transfer rate
|
||||
for(let i = 0; i < 10; i++) {
|
||||
const tempOutHot = results.tempOutHot - i;
|
||||
const tempOutCold = results.tempOutCold + i;
|
||||
const Q = he.getHeatTransferRate(mFlowHot, CpHot, Th_in - tempOutHot);
|
||||
const deltaT = he.getDeltaT(tempOutHot, tempOutCold);
|
||||
console.log(`Hot Outlet Temperature: ${tempOutHot.toFixed(2)} °C, Cold Outlet Temperature: ${tempOutCold.toFixed(2)} °C, Heat Transfer Rate: ${Q.toFixed(2)} kW, ΔT: ${deltaT.toFixed(2)} °C`);
|
||||
}
|
||||
|
||||
console.log(`---------------------------------`);
|
||||
console.log(`Hot Inlet Temperature: ${Th_in} °C`);
|
||||
console.log(`Hot Outlet Temperature: ${results.tempOutHot.toFixed(2)} °C`);
|
||||
console.log(`flow rate hot: ${mFlowHot} kg/s`);
|
||||
console.log(`---------------------------------`);
|
||||
console.log(`Cold Inlet Temperature: ${Tc_in} °C`);
|
||||
console.log(`Cold Outlet Temperature: ${results.tempOutCold.toFixed(2)} °C`);
|
||||
console.log(`flow rate cold: ${mFlowCold} kg/s`);
|
||||
console.log(`---------------------------------`);
|
||||
console.log(`Heat Transfer Rate: ${results.Q.toFixed(2)} kW`);
|
||||
|
||||
// */
|
||||
471
dependencies/heatExchanger/heatExchanger.test.js
vendored
Normal file
471
dependencies/heatExchanger/heatExchanger.test.js
vendored
Normal file
@@ -0,0 +1,471 @@
|
||||
const HeatExchanger = require('./heatExchanger');
|
||||
|
||||
// Suppress console output from the actual class during testing
|
||||
const silentLogger = {
|
||||
error: () => {},
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
setLogLevel: () => {}
|
||||
};
|
||||
|
||||
// Base configuration for testing
|
||||
const testConfig = {
|
||||
general: {
|
||||
name: "test_heat_exchanger",
|
||||
id: "HX-001",
|
||||
logging: {
|
||||
enabled: false,
|
||||
logLevel: "error"
|
||||
}
|
||||
},
|
||||
asset: {
|
||||
maxFlowRate: 10, // m3/h
|
||||
maxPower: 100, // kW
|
||||
subType: "plate",
|
||||
U: 3000, // W/m²K - Overall heat transfer coefficient
|
||||
A: 0.24 // m² - Heat transfer area
|
||||
},
|
||||
medium: {
|
||||
input: "water",
|
||||
output: "water"
|
||||
},
|
||||
flowConfiguration: "counterflow" // Default to counterflow
|
||||
};
|
||||
|
||||
class HeatExchangerTester {
|
||||
constructor() {
|
||||
this.totalTests = 0;
|
||||
this.passedTests = 0;
|
||||
this.failedTests = 0;
|
||||
this.heatExchanger = new HeatExchanger(testConfig);
|
||||
|
||||
// Override logger to prevent console output during tests
|
||||
this.heatExchanger.logger = silentLogger;
|
||||
}
|
||||
|
||||
assert(condition, message) {
|
||||
this.totalTests++;
|
||||
if (condition) {
|
||||
console.log(`✓ PASS: ${message}`);
|
||||
this.passedTests++;
|
||||
} else {
|
||||
console.log(`✗ FAIL: ${message}`);
|
||||
this.failedTests++;
|
||||
}
|
||||
}
|
||||
|
||||
assertApproxEqual(actual, expected, tolerance = 0.01, message) {
|
||||
const diff = Math.abs(actual - expected);
|
||||
const relativeError = expected !== 0 ? diff / Math.abs(expected) : diff;
|
||||
|
||||
this.assert(
|
||||
relativeError <= tolerance,
|
||||
`${message} - Expected: ${expected.toFixed(3)}, Got: ${actual.toFixed(3)}, Relative Error: ${(relativeError * 100).toFixed(2)}%`
|
||||
);
|
||||
}
|
||||
|
||||
testM3hToKgsConversion() {
|
||||
console.log("\nTesting m3/h to kg/s Conversion...");
|
||||
|
||||
// Standard water conversion
|
||||
const m3h = 3.6; // 3.6 m³/h
|
||||
const expectedKgs = 1.0; // 1.0 kg/s
|
||||
|
||||
const convertedValue = this.heatExchanger.m3hToKgs(m3h);
|
||||
this.assertApproxEqual(convertedValue, expectedKgs, 0.001, "3.6 m³/h should convert to 1.0 kg/s");
|
||||
|
||||
// Zero case
|
||||
this.assertApproxEqual(this.heatExchanger.m3hToKgs(0), 0, 0.001, "0 m³/h should convert to 0 kg/s");
|
||||
|
||||
// Large value
|
||||
this.assertApproxEqual(this.heatExchanger.m3hToKgs(36), 10, 0.001, "36 m³/h should convert to 10 kg/s");
|
||||
}
|
||||
|
||||
testUACalculation() {
|
||||
console.log("\nTesting UA Calculation...");
|
||||
|
||||
// Simple multiplication
|
||||
const U = 3; // kW/m²K
|
||||
const A = 0.24; // m²
|
||||
const expectedUA = 0.720; // kW/K
|
||||
|
||||
const calculatedUA = this.heatExchanger.calcUA(U, A);
|
||||
this.assertApproxEqual(calculatedUA, expectedUA, 0.001, "UA should be U × A");
|
||||
|
||||
// Ensure class initialization sets UA correctly
|
||||
this.assertApproxEqual(this.heatExchanger.UA, expectedUA, 0.001, "UA should be initialized correctly");
|
||||
}
|
||||
|
||||
testHeatTransferRate() {
|
||||
console.log("\nTesting Heat Transfer Rate Calculation...");
|
||||
|
||||
// Basic heat transfer equation: Q = ṁ × Cp × ΔT
|
||||
const mFlowRate = 1.0; // kg/s
|
||||
const Cp = 4.18; // kJ/kgK (water)
|
||||
const deltaT = 10; // 10°C temperature difference
|
||||
const expectedQ = 41.8; // kW
|
||||
|
||||
const calculatedQ = this.heatExchanger.getHeatTransferRate(mFlowRate, Cp, deltaT);
|
||||
this.assertApproxEqual(calculatedQ, expectedQ, 0.001, "Heat transfer rate should match Q = ṁ × Cp × ΔT");
|
||||
|
||||
// Zero case
|
||||
this.assertApproxEqual(this.heatExchanger.getHeatTransferRate(0, Cp, deltaT), 0, 0.001, "No flow should result in zero heat transfer");
|
||||
this.assertApproxEqual(this.heatExchanger.getHeatTransferRate(mFlowRate, Cp, 0), 0, 0.001, "No temperature difference should result in zero heat transfer");
|
||||
}
|
||||
|
||||
testDeltaTCalculation() {
|
||||
console.log("\nTesting Delta T Calculation...");
|
||||
|
||||
const tempIn = 20; // °C
|
||||
const tempOut = 30; // °C
|
||||
const expectedDeltaT = 10; // °C
|
||||
|
||||
const calculatedDeltaT = this.heatExchanger.getDeltaT(tempIn, tempOut);
|
||||
this.assertApproxEqual(calculatedDeltaT, expectedDeltaT, 0.001, "Delta T calculation should be correct");
|
||||
|
||||
// Negative delta (cooling)
|
||||
this.assertApproxEqual(
|
||||
this.heatExchanger.getDeltaT(50, 20),
|
||||
-30,
|
||||
0.001,
|
||||
"Delta T should handle cooling correctly"
|
||||
);
|
||||
}
|
||||
|
||||
testCalcOutletTempsEffectivenessNTU_Counterflow() {
|
||||
console.log("\nTesting Outlet Temperature Calculation (Counterflow)...");
|
||||
|
||||
// Create a specific instance for this test with counterflow
|
||||
const config = JSON.parse(JSON.stringify(testConfig));
|
||||
config.flowConfiguration = "counterflow";
|
||||
const counterflowHE = new HeatExchanger(config);
|
||||
counterflowHE.logger = silentLogger;
|
||||
|
||||
// Test case: balanced flow (C_hot = C_cold)
|
||||
const mFlowRateHot = 1.0; // kg/s
|
||||
const tempInHot = 100; // °C
|
||||
const mFlowRateCold = 1.0; // kg/s
|
||||
const tempInCold = 20; // °C
|
||||
|
||||
// UA = 720 W/K = 0.72 kW/K
|
||||
// C_min = m * Cp = 1 * 4.18 = 4.18 kW/K
|
||||
// NTU = UA/C_min = 0.72 / 4.18 = 0.172
|
||||
// Cr = 1 (balanced flow)
|
||||
// Effectiveness (counterflow) for Cr=1: ε = NTU/(1+NTU) = 0.172/(1+0.172) = 0.147
|
||||
// Q_max = C_min * (Th_in - Tc_in) = 4.18 * (100 - 20) = 334.4 kW
|
||||
// Q = ε * Q_max = 0.147 * 334.4 = 49.2 kW (but limited by maxPower = 100 kW)
|
||||
// Th_out = Th_in - Q/C_hot = 100 - 49.2/4.18 = 88.2°C
|
||||
// Tc_out = Tc_in + Q/C_cold = 20 + 49.2/4.18 = 31.8°C
|
||||
|
||||
const result = counterflowHE.calcOutletTempsEffectivenessNTU(
|
||||
mFlowRateHot,
|
||||
tempInHot,
|
||||
mFlowRateCold,
|
||||
tempInCold
|
||||
);
|
||||
|
||||
// Using calculated effectiveness value to check outputs
|
||||
this.assertApproxEqual(result.tempOutHot, 88.2, 0.1, "Hot outlet temperature should match for counterflow");
|
||||
this.assertApproxEqual(result.tempOutCold, 31.8, 0.1, "Cold outlet temperature should match for counterflow");
|
||||
this.assertApproxEqual(result.Q, 49.2, 0.3, "Heat transfer rate should match for counterflow");
|
||||
|
||||
// Check energy balance
|
||||
const QHot = mFlowRateHot * counterflowHE.CpIn * (tempInHot - result.tempOutHot);
|
||||
const QCold = mFlowRateCold * counterflowHE.CpOut * (result.tempOutCold - tempInCold);
|
||||
this.assertApproxEqual(QHot, QCold, 0.01, "Energy balance should be maintained (Q_hot = Q_cold)");
|
||||
}
|
||||
|
||||
testCalcOutletTempsEffectivenessNTU_Parallel() {
|
||||
console.log("\nTesting Outlet Temperature Calculation (Parallel Flow)...");
|
||||
|
||||
// Create a specific instance for this test with parallel flow
|
||||
const config = JSON.parse(JSON.stringify(testConfig));
|
||||
config.flowConfiguration = "parallelflow";
|
||||
const parallelHE = new HeatExchanger(config);
|
||||
parallelHE.logger = silentLogger;
|
||||
|
||||
// Same inputs as the counterflow test for comparison
|
||||
const mFlowRateHot = 1.0; // kg/s
|
||||
const tempInHot = 100; // °C
|
||||
const mFlowRateCold = 1.0; // kg/s
|
||||
const tempInCold = 20; // °C
|
||||
|
||||
// Calculate expected values with the correct formula
|
||||
// NTU = 0.172, Cr = 1
|
||||
// Effectiveness (parallel flow): ε = (1 - exp(-NTU * (1 + Cr))) / (1 + Cr)
|
||||
// ε = (1 - exp(-0.172 * 2)) / 2 ≈ 0.146
|
||||
// Q = ε * Q_max = 0.146 * 334.4 ≈ 48.8 kW
|
||||
// Th_out = Th_in - Q/C_hot = 100 - 48.8/4.18 ≈ 88.3°C
|
||||
// Tc_out = Tc_in + Q/C_cold = 20 + 48.8/4.18 ≈ 31.7°C
|
||||
|
||||
const result = parallelHE.calcOutletTempsEffectivenessNTU(
|
||||
mFlowRateHot,
|
||||
tempInHot,
|
||||
mFlowRateCold,
|
||||
tempInCold
|
||||
);
|
||||
|
||||
// Using corrected calculated effectiveness value to check outputs
|
||||
this.assertApproxEqual(result.tempOutHot, 88.3, 0.1, "Hot outlet temperature should match for parallel flow");
|
||||
this.assertApproxEqual(result.tempOutCold, 31.7, 0.1, "Cold outlet temperature should match for parallel flow");
|
||||
this.assertApproxEqual(result.Q, 48.8, 0.3, "Heat transfer rate should match for parallel flow");
|
||||
|
||||
// Check energy balance
|
||||
const QHot = mFlowRateHot * parallelHE.CpIn * (tempInHot - result.tempOutHot);
|
||||
const QCold = mFlowRateCold * parallelHE.CpOut * (result.tempOutCold - tempInCold);
|
||||
this.assertApproxEqual(QHot, QCold, 0.01, "Energy balance should be maintained (Q_hot = Q_cold)");
|
||||
}
|
||||
|
||||
testMaxPowerLimit() {
|
||||
console.log("\nTesting Maximum Power Limit...");
|
||||
|
||||
// Create a specific instance with low maximum power
|
||||
const config = JSON.parse(JSON.stringify(testConfig));
|
||||
config.asset.maxPower = 20; // kW - intentionally low
|
||||
const limitedHE = new HeatExchanger(config);
|
||||
limitedHE.logger = silentLogger;
|
||||
|
||||
// Set up a scenario that would exceed the maximum power
|
||||
const mFlowRateHot = 2.0; // kg/s
|
||||
const tempInHot = 100; // °C
|
||||
const mFlowRateCold = 2.0; // kg/s
|
||||
const tempInCold = 10; // °C
|
||||
|
||||
// With these parameters, the theoretical heat transfer would be higher,
|
||||
// but should be limited to 20 kW by the maxPower setting
|
||||
|
||||
const result = limitedHE.calcOutletTempsEffectivenessNTU(
|
||||
mFlowRateHot,
|
||||
tempInHot,
|
||||
mFlowRateCold,
|
||||
tempInCold
|
||||
);
|
||||
|
||||
this.assertApproxEqual(result.Q, 20, 0.01, "Heat transfer rate should be limited by maxPower");
|
||||
|
||||
// Calculate what the outlet temperatures should be with the limited power
|
||||
// Th_out = Th_in - Q/C_hot = 100 - 20/(2*4.18) = 97.6°C
|
||||
// Tc_out = Tc_in + Q/C_cold = 10 + 20/(2*4.18) = 12.4°C
|
||||
|
||||
this.assertApproxEqual(result.tempOutHot, 97.6, 0.1, "Hot outlet temperature should reflect power limit");
|
||||
this.assertApproxEqual(result.tempOutCold, 12.4, 0.1, "Cold outlet temperature should reflect power limit");
|
||||
}
|
||||
|
||||
testImbalancedFlowRates() {
|
||||
console.log("\nTesting Imbalanced Flow Rates...");
|
||||
|
||||
// Create a specific instance for this test
|
||||
const imbalancedHE = new HeatExchanger(testConfig);
|
||||
imbalancedHE.logger = silentLogger;
|
||||
|
||||
// Test case: C_hot < C_cold
|
||||
const mFlowRateHot = 0.5; // kg/s
|
||||
const tempInHot = 80; // °C
|
||||
const mFlowRateCold = 2.0; // kg/s
|
||||
const tempInCold = 15; // °C
|
||||
|
||||
// C_hot = 0.5 * 4.18 = 2.09 kW/K
|
||||
// C_cold = 2.0 * 4.18 = 8.36 kW/K
|
||||
// C_min = 2.09, C_max = 8.36
|
||||
// Cr = 2.09/8.36 = 0.25
|
||||
// NTU = UA/C_min = 0.72/2.09 = 0.344
|
||||
// Effectiveness calculation for counterflow with Cr=0.25:
|
||||
// ε = (1 - exp(-NTU * (1 - Cr))) / (1 - Cr * exp(-NTU * (1 - Cr)))
|
||||
|
||||
const result = imbalancedHE.calcOutletTempsEffectivenessNTU(
|
||||
mFlowRateHot,
|
||||
tempInHot,
|
||||
mFlowRateCold,
|
||||
tempInCold
|
||||
);
|
||||
|
||||
// Verify reasonable outputs
|
||||
this.assert(result.tempOutHot < tempInHot, "Hot outlet temperature should be less than inlet");
|
||||
this.assert(result.tempOutCold > tempInCold, "Cold outlet temperature should be greater than inlet");
|
||||
|
||||
// Check energy balance
|
||||
const QHot = mFlowRateHot * imbalancedHE.CpIn * (tempInHot - result.tempOutHot);
|
||||
const QCold = mFlowRateCold * imbalancedHE.CpOut * (result.tempOutCold - tempInCold);
|
||||
this.assertApproxEqual(QHot, QCold, 0.01, "Energy balance should be maintained with imbalanced flow rates");
|
||||
this.assertApproxEqual(QHot, result.Q, 0.01, "Calculated Q should match energy extracted from hot stream");
|
||||
}
|
||||
|
||||
testZeroFlowRate() {
|
||||
console.log("\nTesting Zero Flow Rate Handling...");
|
||||
|
||||
// Create a specific instance for this test
|
||||
const zeroFlowHE = new HeatExchanger(testConfig);
|
||||
zeroFlowHE.logger = silentLogger;
|
||||
|
||||
// Test with zero hot flow rate
|
||||
const resultZeroHot = zeroFlowHE.calcOutletTempsEffectivenessNTU(
|
||||
0, // kg/s - zero flow rate
|
||||
80, // °C
|
||||
1.0, // kg/s
|
||||
15 // °C
|
||||
);
|
||||
|
||||
this.assertApproxEqual(resultZeroHot.Q, 0, 0.001, "Zero hot flow rate should result in zero heat transfer");
|
||||
this.assertApproxEqual(resultZeroHot.tempOutHot, 80, 0.001, "Zero hot flow should result in unchanged hot outlet temperature");
|
||||
this.assertApproxEqual(resultZeroHot.tempOutCold, 15, 0.001, "Zero hot flow should result in unchanged cold outlet temperature");
|
||||
|
||||
// Test with zero cold flow rate
|
||||
const resultZeroCold = zeroFlowHE.calcOutletTempsEffectivenessNTU(
|
||||
1.0, // kg/s
|
||||
80, // °C
|
||||
0, // kg/s - zero flow rate
|
||||
15 // °C
|
||||
);
|
||||
|
||||
this.assertApproxEqual(resultZeroCold.Q, 0, 0.001, "Zero cold flow rate should result in zero heat transfer");
|
||||
this.assertApproxEqual(resultZeroCold.tempOutHot, 80, 0.001, "Zero cold flow should result in unchanged hot outlet temperature");
|
||||
this.assertApproxEqual(resultZeroCold.tempOutCold, 15, 0.001, "Zero cold flow should result in unchanged cold outlet temperature");
|
||||
}
|
||||
|
||||
testSetter_HotFlowRate() {
|
||||
console.log("\nTesting Hot Flow Rate Setter...");
|
||||
|
||||
const setterHE = new HeatExchanger(testConfig);
|
||||
setterHE.logger = silentLogger;
|
||||
|
||||
// Set temperatures first to have a baseline
|
||||
setterHE.setHotInput(80);
|
||||
setterHE.setColdInput(20);
|
||||
setterHE.setColdFlowRate(2);
|
||||
|
||||
// Initial calculation happens
|
||||
const originalOutletHot = setterHE.tempOutHot;
|
||||
|
||||
// Now change hot flow rate
|
||||
const m3h = 5;
|
||||
const expectedKgs = 5 * 0.000277778 * 1000; // convert to kg/s
|
||||
|
||||
const result = setterHE.setHotFlowRate(m3h);
|
||||
|
||||
this.assertApproxEqual(result, expectedKgs, 0.001, "setHotFlowRate should return converted flow rate");
|
||||
this.assertApproxEqual(setterHE.mFlowRateHot, expectedKgs, 0.001, "mFlowRateHot should be updated correctly");
|
||||
|
||||
// Outlet temperature should change with different flow rate
|
||||
this.assert(setterHE.tempOutHot !== originalOutletHot, "Hot outlet temperature should change after flow rate update");
|
||||
}
|
||||
|
||||
testSetter_HotInput() {
|
||||
console.log("\nTesting Hot Input Temperature Setter...");
|
||||
|
||||
const setterHE = new HeatExchanger(testConfig);
|
||||
setterHE.logger = silentLogger;
|
||||
|
||||
// Set other parameters first
|
||||
setterHE.setColdInput(20);
|
||||
setterHE.setHotFlowRate(3);
|
||||
setterHE.setColdFlowRate(3);
|
||||
|
||||
const originalOutletCold = setterHE.tempOutCold;
|
||||
|
||||
// Now set hot input temperature
|
||||
const newTemp = 90;
|
||||
const result = setterHE.setHotInput(newTemp);
|
||||
|
||||
this.assertApproxEqual(result, newTemp, 0.001, "setHotInput should return the set temperature");
|
||||
this.assertApproxEqual(setterHE.tempInHot, newTemp, 0.001, "tempInHot should be updated correctly");
|
||||
|
||||
// Cold outlet temperature should change with different hot input
|
||||
this.assert(setterHE.tempOutCold !== originalOutletCold,
|
||||
"Cold outlet temperature should change after hot input temperature update");
|
||||
}
|
||||
|
||||
testPhysicalLimits() {
|
||||
console.log("\nTesting Physical Limits and Constraints...");
|
||||
|
||||
// Test that extreme temperature differences don't break the model
|
||||
const extremeHE = new HeatExchanger(testConfig);
|
||||
extremeHE.logger = silentLogger;
|
||||
|
||||
// Extreme temperature difference
|
||||
const result = extremeHE.calcOutletTempsEffectivenessNTU(
|
||||
1.0, // kg/s
|
||||
1000, // °C - extremely high temperature
|
||||
1.0, // kg/s
|
||||
0, // °C - very low temperature
|
||||
);
|
||||
|
||||
// Check energy balance is still maintained
|
||||
const QHot = 1.0 * extremeHE.CpIn * (1000 - result.tempOutHot);
|
||||
const QCold = 1.0 * extremeHE.CpOut * (result.tempOutCold - 0);
|
||||
this.assertApproxEqual(QHot, QCold, 0.01, "Energy balance should be maintained even with extreme temperatures");
|
||||
|
||||
// Make sure temps are capped at maxPower
|
||||
this.assert(result.Q <= extremeHE.maxPower, "Heat transfer rate should not exceed maxPower");
|
||||
}
|
||||
|
||||
testOutputGeneration() {
|
||||
console.log("\nTesting Output Generation...");
|
||||
|
||||
const outputHE = new HeatExchanger(testConfig);
|
||||
outputHE.logger = silentLogger;
|
||||
|
||||
// Set some values
|
||||
outputHE.setHotInput(80);
|
||||
outputHE.setColdInput(20);
|
||||
outputHE.setHotFlowRate(3);
|
||||
outputHE.setColdFlowRate(2);
|
||||
|
||||
// Get output object
|
||||
const output = outputHE.getOutput();
|
||||
|
||||
// Check that the output contains the right keys
|
||||
this.assert(output.hasOwnProperty('flowRateHot'), "Output should contain flowRateHot");
|
||||
this.assert(output.hasOwnProperty('flowRateCold'), "Output should contain flowRateCold");
|
||||
this.assert(output.hasOwnProperty('tempOutHot'), "Output should contain tempOutHot");
|
||||
this.assert(output.hasOwnProperty('tempOutCold'), "Output should contain tempOutCold");
|
||||
this.assert(output.hasOwnProperty('Q'), "Output should contain Q");
|
||||
|
||||
// Check values match the internal state
|
||||
this.assertApproxEqual(output.flowRateHot, outputHE.mFlowRateHot, 0.001, "Output flowRateHot should match internal state");
|
||||
this.assertApproxEqual(output.tempOutHot, outputHE.tempOutHot, 0.001, "Output tempOutHot should match internal state");
|
||||
this.assertApproxEqual(output.Q, outputHE.Q_act, 0.001, "Output Q should match internal state");
|
||||
}
|
||||
|
||||
async runAllTests() {
|
||||
console.log("\nStarting Heat Exchanger Tests...\n");
|
||||
|
||||
// Basic functionality tests
|
||||
this.testM3hToKgsConversion();
|
||||
this.testUACalculation();
|
||||
this.testHeatTransferRate();
|
||||
this.testDeltaTCalculation();
|
||||
|
||||
// Heat exchanger physics tests
|
||||
this.testCalcOutletTempsEffectivenessNTU_Counterflow();
|
||||
this.testCalcOutletTempsEffectivenessNTU_Parallel();
|
||||
this.testMaxPowerLimit();
|
||||
this.testImbalancedFlowRates();
|
||||
this.testZeroFlowRate();
|
||||
|
||||
// API tests
|
||||
this.testSetter_HotFlowRate();
|
||||
this.testSetter_HotInput();
|
||||
this.testPhysicalLimits();
|
||||
this.testOutputGeneration();
|
||||
|
||||
console.log("\nTest Summary:");
|
||||
console.log(`Total Tests: ${this.totalTests}`);
|
||||
console.log(`Passed: ${this.passedTests}`);
|
||||
console.log(`Failed: ${this.failedTests}`);
|
||||
console.log(`Success Rate: ${((this.passedTests / this.totalTests) * 100).toFixed(1)}%`);
|
||||
|
||||
return this.failedTests === 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
const tester = new HeatExchangerTester();
|
||||
tester.runAllTests()
|
||||
.then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error running tests:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
258
dependencies/heatExchanger/heatExchangerConfig.json
vendored
Normal file
258
dependencies/heatExchanger/heatExchangerConfig.json
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
{
|
||||
"general": {
|
||||
"name": {
|
||||
"default": "heatExchanger Configuration",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "A human-readable name or label for this heatExchanger 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 unit of heatExchanger 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": "heatExchanger",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "Specified software type for this configuration."
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"default": "Sensor",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "Indicates the role this configuration plays (e.g., sensor, controller, etc.)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"asset": {
|
||||
"uuid": {
|
||||
"default": null,
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Asset tag number which is a universally unique identifier for this asset. May be null if not assigned."
|
||||
}
|
||||
},
|
||||
"tagCode":{
|
||||
"default": null,
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Asset tag code which is a unique identifier for this asset. May be null if not assigned."
|
||||
}
|
||||
},
|
||||
"geoLocation": {
|
||||
"default": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"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": "heatExchanger",
|
||||
"rules": {
|
||||
"type": "enum",
|
||||
"values": [
|
||||
{
|
||||
"value": "heatExchanger",
|
||||
"description": "A device used to transfer heat between two or more fluids."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"subType": {
|
||||
"default": "plate",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "A user-defined or manufacturer-defined subtype for the asset."
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"default": "Unknown",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "A user-defined or manufacturer-defined model identifier for the asset."
|
||||
}
|
||||
},
|
||||
"U": {
|
||||
"default": 3000,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "The overall heat transfer coefficient for the heat exchanger in W/m2K."
|
||||
}
|
||||
},
|
||||
"A": {
|
||||
"default": 0.24,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "The heat transfer area for the heat exchanger in m2."
|
||||
}
|
||||
},
|
||||
"maxPower": {
|
||||
"default": 44,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "The maximum power that the heat exchanger can handle in kW."
|
||||
}
|
||||
},
|
||||
"maxFlowRate": {
|
||||
"default": 4,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "The maximum flow rate that the heat exchanger can handle in m3/h."
|
||||
}
|
||||
}
|
||||
},
|
||||
"medium": {
|
||||
"input":{
|
||||
"default": "water",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "The fluid that is allowed to flow through the heat exchanger."
|
||||
}
|
||||
},
|
||||
"output":{
|
||||
"default": "water",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "The fluid that is allowed to flow through the heat exchanger."
|
||||
}
|
||||
},
|
||||
"allowed": {
|
||||
"default": [
|
||||
"water",
|
||||
"glycol",
|
||||
"oil",
|
||||
"steam"
|
||||
],
|
||||
"rules": {
|
||||
"type": "set",
|
||||
"description": "An array of allowed mediums for the heat exchanger."
|
||||
}
|
||||
}
|
||||
},
|
||||
"flowConfiguration": {
|
||||
"default": "counterflow",
|
||||
"rules": {
|
||||
"type": "enum",
|
||||
"values":[
|
||||
{
|
||||
"value": "counterflow",
|
||||
"description": "The direction of the flow of the two fluids is opposite to each other."
|
||||
},
|
||||
{
|
||||
"value": "parallelflow",
|
||||
"description": "The direction of the flow of the two fluids is the same."
|
||||
},
|
||||
{
|
||||
"value": "crossflow",
|
||||
"description": "The direction of the flow of the two fluids is perpendicular to each other."
|
||||
}
|
||||
],
|
||||
"description": "The direction of the flow of the two fluids."
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"default": "plate",
|
||||
"rules": {
|
||||
"type": "enum",
|
||||
"values":[
|
||||
{
|
||||
"value": "shellAndTube",
|
||||
"description": "A type of heat exchanger that consists of a shell (a large pressure vessel) with a bundle of tubes inside it."
|
||||
},
|
||||
{
|
||||
"value": "plate",
|
||||
"description": "A type of heat exchanger that uses metal plates to transfer heat between two fluids."
|
||||
},
|
||||
{
|
||||
"value": "regenerative",
|
||||
"description": "A type of heat exchanger that uses a rotating wheel to transfer heat between two fluids."
|
||||
}
|
||||
],
|
||||
"description": "The type of heat exchanger."
|
||||
}
|
||||
}
|
||||
}
|
||||
328
heatExchanger.html
Normal file
328
heatExchanger.html
Normal file
@@ -0,0 +1,328 @@
|
||||
<script type="module">
|
||||
import * as menuUtils from "/generalFunctions/helper/menuUtils.js";
|
||||
|
||||
RED.nodes.registerType("heatExchanger", {
|
||||
|
||||
category: "digital twin",
|
||||
color: "#e4a363",
|
||||
|
||||
defaults: {
|
||||
|
||||
// Define default properties
|
||||
name: { value: "", required: true },
|
||||
enableLog: { value: false },
|
||||
logLevel: { value: "error" },
|
||||
|
||||
// Define specific properties
|
||||
scaling: { value: false },
|
||||
i_min: { value: 0, required: true },
|
||||
i_max: { value: 0, required: true },
|
||||
i_offset: { value: 0 },
|
||||
o_min: { value: 0, required: true },
|
||||
o_max: { value: 1, required: true },
|
||||
simulator: { value: false },
|
||||
unit: { value: "unit", required: true },
|
||||
smooth_method: { value: "" },
|
||||
count: { value: "10", required: true },
|
||||
|
||||
//define asset properties
|
||||
supplier: { value: "" },
|
||||
subType: { value: "" },
|
||||
model: { value: "" },
|
||||
unit: { value: "" },
|
||||
|
||||
},
|
||||
|
||||
inputs: 1,
|
||||
outputs: 4,
|
||||
inputLabels: ["heatExchanger Input"],
|
||||
outputLabels: ["process", "dbase", "upstreamParent", "downstreamParent"],
|
||||
icon: "font-awesome/fa-tachometer",
|
||||
|
||||
label: function () {
|
||||
return this.name || "heatExchanger";
|
||||
},
|
||||
|
||||
|
||||
oneditprepare: function() {
|
||||
|
||||
const node = this;
|
||||
|
||||
// Define UI html elements
|
||||
const elements = {
|
||||
// Basic fields
|
||||
name: document.getElementById("node-input-name"),
|
||||
// specific fields
|
||||
scalingCheckbox: document.getElementById("node-input-scaling"),
|
||||
rowInputMin: document.getElementById("row-input-i_min"),
|
||||
rowInputMax: document.getElementById("row-input-i_max"),
|
||||
smoothMethod: document.getElementById("node-input-smooth_method"),
|
||||
count: document.getElementById("node-input-count"),
|
||||
iOffset: document.getElementById("node-input-i_offset"),
|
||||
oMin: document.getElementById("node-input-o_min"),
|
||||
oMax: document.getElementById("node-input-o_max"),
|
||||
// Logging fields
|
||||
logCheckbox: document.getElementById("node-input-enableLog"),
|
||||
logLevelSelect: document.getElementById("node-input-logLevel"),
|
||||
rowLogLevel: document.getElementById("row-logLevel"),
|
||||
// Asset fields
|
||||
supplier: document.getElementById("node-input-supplier"),
|
||||
subType: document.getElementById("node-input-subType"),
|
||||
model: document.getElementById("node-input-model"),
|
||||
unit: document.getElementById("node-input-unit"),
|
||||
};
|
||||
|
||||
//this needs to live somewhere and for now we add it to every node file for simplicity
|
||||
const projecSettingstURL = "http://localhost:1880/generalFunctions/settings/projectSettings.json";
|
||||
|
||||
try{
|
||||
|
||||
// Fetch project settings
|
||||
menuUtils.fetchProjectData(projecSettingstURL)
|
||||
.then((projectSettings) => {
|
||||
|
||||
//assign to node vars
|
||||
node.configUrls = projectSettings.configUrls;
|
||||
|
||||
const { cloudConfigURL, localConfigURL } = menuUtils.getSpecificConfigUrl("heatExchanger",node.configUrls.cloud.taggcodeAPI);
|
||||
node.configUrls.cloud.config = cloudConfigURL; // first call
|
||||
node.configUrls.local.config = localConfigURL; // backup call
|
||||
|
||||
node.locationId = projectSettings.locationId;
|
||||
node.uuid = projectSettings.uuid;
|
||||
|
||||
// Gets the ID of the active workspace (Flow)
|
||||
const activeFlowId = RED.workspaces.active(); //fetches active flow id
|
||||
node.processId = 1;//activeFlowId;
|
||||
|
||||
|
||||
// UI elements specific for node
|
||||
menuUtils.initMeasurementToggles(elements);
|
||||
menuUtils.populateSmoothingMethods(node.configUrls, elements, node);
|
||||
// UI elements across all nodes
|
||||
menuUtils.fetchAndPopulateDropdowns(node.configUrls, elements, node); // function for all assets
|
||||
menuUtils.initBasicToggles(elements);
|
||||
|
||||
})
|
||||
}catch(e){
|
||||
console.log("Error fetching project settings", e);
|
||||
}
|
||||
|
||||
if(node.d){
|
||||
//this means node is disabled
|
||||
console.log("Current status of node is disabled");
|
||||
}
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
oneditsave: function () {
|
||||
const node = this;
|
||||
|
||||
console.log(`------------ Saving changes to node ------------`);
|
||||
console.log(`${node.uuid}`);
|
||||
|
||||
// Save basic properties
|
||||
["name", "supplier", "subType", "model", "unit", "smooth_method"].forEach(
|
||||
(field) => (node[field] = document.getElementById(`node-input-${field}`).value || "")
|
||||
);
|
||||
|
||||
// Save numeric and boolean properties
|
||||
["scaling", "enableLog", "simulator"].forEach(
|
||||
(field) => (node[field] = document.getElementById(`node-input-${field}`).checked)
|
||||
);
|
||||
|
||||
["i_min", "i_max", "i_offset", "o_min", "o_max", "count"].forEach(
|
||||
(field) => (node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0)
|
||||
);
|
||||
|
||||
node.logLevel = document.getElementById("node-input-logLevel").value || "info";
|
||||
|
||||
// Validation checks
|
||||
if (node.scaling && (isNaN(node.i_min) || isNaN(node.i_max))) {
|
||||
RED.notify("Scaling enabled, but input range is incomplete!", "error");
|
||||
}
|
||||
|
||||
if (!node.unit) {
|
||||
RED.notify("Unit selection is required.", "error");
|
||||
}
|
||||
|
||||
if (node.subType && !node.unit) {
|
||||
RED.notify("Unit must be set when specifying a subtype.", "error");
|
||||
}
|
||||
|
||||
console.log("stored node modelData", node.modelMetadata);
|
||||
console.log("------------ Changes saved to heatExchanger node preparing to save to API ------------");
|
||||
|
||||
try{
|
||||
// Fetch project settings
|
||||
menuUtils.apiCall(node,node.configUrls)
|
||||
.then((response) => {
|
||||
|
||||
//save response to node information
|
||||
node.assetTagCode = response.asset_tag_number;
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error during API call", error);
|
||||
});
|
||||
|
||||
}catch(e){
|
||||
console.log("Error saving assetID and tagnumber", e);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Main UI -->
|
||||
|
||||
<script type="text/html" data-template-name="heatExchanger">
|
||||
|
||||
<!-- Node Name -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="node-input-name"
|
||||
placeholder="heatExchanger Name"
|
||||
style="width:70%;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Scaling Checkbox -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-scaling"
|
||||
><i class="fa fa-compress"></i> Scaling</label
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="node-input-scaling"
|
||||
style="width:20px; vertical-align:baseline;"
|
||||
/>
|
||||
<span>Enable input scaling?</span>
|
||||
</div>
|
||||
|
||||
<!-- Source Min/Max (only if scaling is true) -->
|
||||
<div class="form-row" id="row-input-i_min">
|
||||
<label for="node-input-i_min"
|
||||
><i class="fa fa-arrow-down"></i> Source Min</label>
|
||||
<input type="number" id="node-input-i_min" placeholder="0" />
|
||||
</div>
|
||||
|
||||
<div class="form-row" id="row-input-i_max">
|
||||
<label for="node-input-i_max"
|
||||
><i class="fa fa-arrow-up"></i> Source Max</label>
|
||||
<input type="number" id="node-input-i_max" placeholder="3000" />
|
||||
</div>
|
||||
|
||||
<!-- Offset -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-i_offset"
|
||||
><i class="fa fa-adjust"></i> Input Offset</label>
|
||||
<input type="number" id="node-input-i_offset" placeholder="0" />
|
||||
</div>
|
||||
|
||||
<!-- Output / Process Min/Max -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-o_min"><i class="fa fa-tag"></i> Process Min</label>
|
||||
<input type="number" id="node-input-o_min" placeholder="0" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-o_max"><i class="fa fa-tag"></i> Process Max</label>
|
||||
<input type="number" id="node-input-o_max" placeholder="1" />
|
||||
</div>
|
||||
|
||||
<!-- Simulator Checkbox -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-simulator"
|
||||
><i class="fa fa-cog"></i> Simulator</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="node-input-simulator"
|
||||
style="width:20px; vertical-align:baseline;"
|
||||
/>
|
||||
<span>Activate internal simulation?</span>
|
||||
</div>
|
||||
|
||||
<!-- Smoothing Method -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-smooth_method"
|
||||
><i class="fa fa-line-chart"></i> Smoothing</label>
|
||||
<select id="node-input-smooth_method" style="width:60%;">
|
||||
<!-- Filled dynamically from heatExchangerConfig.json or fallback -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Smoothing Window -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-count">Window</label>
|
||||
<input
|
||||
type="number"
|
||||
id="node-input-count"
|
||||
placeholder="10"
|
||||
style="width:60px;"
|
||||
/>
|
||||
<div class="form-tips">Number of samples for smoothing</div>
|
||||
</div>
|
||||
|
||||
<!-- Optional Extended Fields: supplier, type, subType, model -->
|
||||
<hr />
|
||||
<div class="form-row">
|
||||
<label for="node-input-supplier"
|
||||
><i class="fa fa-industry"></i> Supplier</label>
|
||||
<select id="node-input-supplier" style="width:60%;">
|
||||
<option value="">(optional)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-subType"
|
||||
><i class="fa fa-puzzle-piece"></i> SubType</label>
|
||||
<select id="node-input-subType" style="width:60%;">
|
||||
<option value="">(optional)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-model"><i class="fa fa-wrench"></i> Model</label>
|
||||
<select id="node-input-model" style="width:60%;">
|
||||
<option value="">(optional)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-unit"><i class="fa fa-balance-scale"></i> Unit</label>
|
||||
<select id="node-input-unit" style="width:60%;"></select>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- loglevel checkbox -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-enableLog"
|
||||
><i class="fa fa-cog"></i> Enable Log</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="node-input-enableLog"
|
||||
style="width:20px; vertical-align:baseline;"
|
||||
/>
|
||||
<span>Enable logging</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row" id="row-logLevel">
|
||||
<label for="node-input-logLevel"><i class="fa fa-cog"></i> Log Level</label>
|
||||
<select id="node-input-logLevel" style="width:60%;">
|
||||
<option value="info">Info</option>
|
||||
<option value="debug">Debug</option>
|
||||
<option value="warn">Warn</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/html" data-help-name="heatExchanger">
|
||||
<p><b>heatExchanger Node</b>: Scales, smooths, and simulates heatExchanger data.</p>
|
||||
<p>Use this node to scale, smooth, and simulate heatExchanger data. The node can be configured to scale input data to a specified range, smooth the data using a variety of methods, and simulate data for testing purposes.</p>
|
||||
|
||||
</script>
|
||||
147
heatExchanger.js
Normal file
147
heatExchanger.js
Normal file
@@ -0,0 +1,147 @@
|
||||
module.exports = function (RED) {
|
||||
function heatExchanger(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{
|
||||
|
||||
//fetch heatExchanger object from source.js
|
||||
const heatExchanger = require("./dependencies/heatExchanger/heatExchanger");
|
||||
const OutputUtils = require("../generalFunctions/helper/outputUtils");
|
||||
|
||||
//load user defined config in the node-red UI
|
||||
const heConfig={
|
||||
general: {
|
||||
name: config.name,
|
||||
id: node.id,
|
||||
unit: config.unit,
|
||||
logging:{
|
||||
logLevel: config.logLevel,
|
||||
enabled: config.enableLog,
|
||||
},
|
||||
},
|
||||
asset: {
|
||||
tagCode: config.assetTagCode,
|
||||
supplier: config.supplier,
|
||||
subType: config.subType,
|
||||
model: config.model,
|
||||
}
|
||||
}
|
||||
|
||||
//make new heatExchanger on creation to work with.
|
||||
const he = new heatExchanger(heConfig);
|
||||
|
||||
// put m on node memory as source
|
||||
node.source = he;
|
||||
|
||||
//load output utils
|
||||
const output = new OutputUtils();
|
||||
|
||||
function updateNodeStatus(val) {
|
||||
//display status
|
||||
node.status({ fill: "green", shape: "dot", text: val + " " + heConfig.general.unit });
|
||||
}
|
||||
|
||||
function tick() {
|
||||
try {
|
||||
//const status = updateNodeStatus();
|
||||
//node.status(status);
|
||||
|
||||
//get output
|
||||
const classOutput = he.getOutput();
|
||||
const dbOutput = output.formatMsg(classOutput, he.config, "influxdb");
|
||||
const pOutput = output.formatMsg(classOutput, he.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
|
||||
this.send(msgs);
|
||||
},
|
||||
100
|
||||
);
|
||||
|
||||
//declare refresh interval internal node
|
||||
setTimeout(
|
||||
() => {
|
||||
/*---execute code on first start----*/
|
||||
this.intervalId = setInterval(function(){ tick() },1000)
|
||||
},
|
||||
1000
|
||||
);
|
||||
|
||||
//-------------------------------------------------------------------->>what to do on input
|
||||
node.on("input", function (msg,send,done) {
|
||||
|
||||
// augment this later with child connections
|
||||
if(msg.topic == "TempHotInput")
|
||||
{
|
||||
if(msg.payload.mAbs){
|
||||
he.setHotInput(msg.payload.mAbs);
|
||||
console.log("HotInput: " + msg.payload);
|
||||
}
|
||||
}
|
||||
|
||||
if(msg.topic == "TempColdInput")
|
||||
{
|
||||
if(msg.payload.mAbs){
|
||||
he.setColdInput(msg.payload.mAbs);
|
||||
console.log("ColdInput: " + msg.payload);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(msg.topic == "FlowRateHot"){
|
||||
if(msg.payload.downstream_predicted_flow){
|
||||
he.setHotFlowRate(msg.payload.downstream_predicted_flow);
|
||||
console.log("FlowRateHot: " + msg.payload.downstream_predicted_flow);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(msg.topic == "FlowRateCold"){
|
||||
if(msg.payload.downstream_predicted_flow){
|
||||
he.setColdFlowRate(msg.payload.downstream_predicted_flow);
|
||||
console.log("FlowRateCold: " + msg.payload.downstream_predicted_flow);
|
||||
}
|
||||
}
|
||||
|
||||
done();
|
||||
|
||||
});
|
||||
|
||||
// tidy up any async code here - shutdown connections and so on.
|
||||
node.on('close', function(done) {
|
||||
if (node.intervalId) clearTimeout(node.intervalId);
|
||||
if (node.tickInterval) clearInterval(node.tickInterval);
|
||||
if (done) done();
|
||||
});
|
||||
|
||||
}catch(e){
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType("heatExchanger", heatExchanger);
|
||||
};
|
||||
29
package.json
Normal file
29
package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "heatExchanger",
|
||||
"version": "0.9.0",
|
||||
"description": "Control module heatExchanger",
|
||||
"main": "heatExchanger.js",
|
||||
"scripts": {
|
||||
"test": "heatExchanger.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitea.centraal.wbd-rd.nl/RnD/heatExchanger.git"
|
||||
},
|
||||
"keywords": [
|
||||
"heat exchanger",
|
||||
"node-red"
|
||||
],
|
||||
"author": "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",
|
||||
"predict": "git+https://gitea.centraal.wbd-rd.nl/RnD/predict.git"
|
||||
},
|
||||
"node-red": {
|
||||
"nodes": {
|
||||
"heatExchanger": "heatExchanger.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user