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